use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::ast::{Assignment, IoRedirect, SimpleCommand};
use crate::builtin::BuiltinHost;
use crate::embed::{DiagnosticCategory, DiagnosticKind};
use crate::plan::{CommandResolutionError, PlannedSimpleCommand, PlannedSimpleCommandKind};
use crate::policy::VariableAttributes;
use crate::sys::Runtime;
use super::frontend::{emit_diagnostic, range_for_line};
use super::*;
#[derive(Debug, Clone)]
pub(crate) enum CommandResolution {
NotExecutable(PathBuf),
NotFound,
}
#[derive(Debug, Clone)]
pub(crate) enum CommandLookup {
Empty,
Function,
SpecialBuiltin,
Builtin,
UnspecifiedUtility,
ShellOverride,
External { program: String },
ExternalFailed(CommandResolution),
CommandNotFoundHandler,
}
#[derive(Debug)]
struct ResolvedSimpleCommand<'ast> {
assignments: &'ast [Assignment],
redirects: &'ast [IoRedirect],
argv: Vec<String>,
command_name: Option<String>,
lookup: CommandLookup,
should_restore_assignments: bool,
has_command_substitution: bool,
source_line: Option<u32>,
}
fn resolution_error(resolution: CommandResolution) -> CommandResolutionError {
match resolution {
CommandResolution::NotExecutable(path) => CommandResolutionError::NotExecutable(path),
CommandResolution::NotFound => CommandResolutionError::NotFound,
}
}
impl<'ast> ResolvedSimpleCommand<'ast> {
fn into_planned(self, state: &ShellState) -> PlannedSimpleCommand<'ast> {
let Self {
assignments,
redirects,
argv,
command_name,
lookup,
should_restore_assignments,
has_command_substitution,
source_line,
} = self;
let kind = match command_name {
None => PlannedSimpleCommandKind::AssignmentsOnly {
has_command_substitution,
},
Some(command_name) => match lookup {
CommandLookup::UnspecifiedUtility => {
PlannedSimpleCommandKind::UnspecifiedUtility { command_name }
}
CommandLookup::SpecialBuiltin | CommandLookup::Builtin => {
PlannedSimpleCommandKind::Builtin { argv }
}
CommandLookup::ShellOverride => PlannedSimpleCommandKind::ShellOverride { argv },
CommandLookup::External { program } => {
PlannedSimpleCommandKind::External { program, argv }
}
CommandLookup::ExternalFailed(resolution) => {
PlannedSimpleCommandKind::ResolutionFailure {
command_name,
resolution: resolution_error(resolution),
}
}
CommandLookup::CommandNotFoundHandler => {
PlannedSimpleCommandKind::CommandNotFoundHandler { argv }
}
CommandLookup::Function => {
PlannedSimpleCommandKind::Function { command_name, argv }
}
CommandLookup::Empty => {
unreachable!(
"resolved command name should be present for non-empty command lookup"
)
}
},
};
let assignment_attrib = match kind {
PlannedSimpleCommandKind::AssignmentsOnly { .. } => {
if state.has_option(OPT_ALLEXPORT) {
VariableAttributes::EXPORT
} else {
VariableAttributes::empty()
}
}
_ => VariableAttributes::EXPORT,
};
PlannedSimpleCommand {
assignments,
redirects,
assignment_attrib,
restore_assignments: should_restore_assignments,
kind,
source_line,
}
}
}
pub(super) fn shell_override_for<'a>(
state: &'a ShellState,
argv: &[String],
) -> Option<&'a crate::policy::CommandOverride> {
let name = argv.first()?;
let command_override = state.definition.command_policy.override_for(name)?;
command_override.matches(argv).then_some(command_override)
}
pub(super) fn has_shell_override(state: &ShellState, argv: &[String]) -> bool {
shell_override_for(state, argv).is_some()
}
pub(super) fn run_shell_override<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
argv: &[String],
) -> Option<i32> {
let _ = runtime;
let command_override = shell_override_for(state, argv)?.clone();
let mut context = BuiltinHost::new(state);
Some(command_override.run(&mut context, &argv[1..]))
}
pub(super) fn run_command_not_found_handler<R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
argv: &[String],
) -> Option<i32> {
let _ = runtime;
let handler = state
.definition
.command_policy
.command_not_found_handler()?
.clone();
let mut context = BuiltinHost::new(state);
Some(handler(&mut context, argv))
}
fn resolve_source_path_candidate(path: &Path) -> Option<PathBuf> {
let Ok(meta) = fs::metadata(path) else {
return None;
};
if !meta.is_file() {
return None;
}
if fs::File::open(path).is_err() {
return None;
}
Some(path.to_path_buf())
}
pub(super) fn command_resolution_from_error(program: &str, err: &io::Error) -> CommandResolution {
match err.kind() {
io::ErrorKind::NotFound => CommandResolution::NotFound,
io::ErrorKind::PermissionDenied => {
let err_text = err.to_string();
let path = err_text
.split(": ")
.next()
.filter(|part| !part.is_empty())
.unwrap_or(program);
CommandResolution::NotExecutable(PathBuf::from(path))
}
_ => CommandResolution::NotExecutable(PathBuf::from(program)),
}
}
pub(super) fn resolve_dot_path(state: &ShellState, name: &str) -> PathBuf {
if name.contains('/') {
return PathBuf::from(name);
}
let path_var = state.env_get("PATH").unwrap_or("/usr/bin:/bin");
for dir in path_var.split(':') {
let candidate = Path::new(dir).join(name);
if let Some(path) = resolve_source_path_candidate(&candidate) {
return path;
}
}
PathBuf::from(name)
}
pub(super) fn command_failure_status(resolution: &CommandResolution) -> i32 {
match resolution {
CommandResolution::NotExecutable(_) => 126,
CommandResolution::NotFound => 127,
}
}
pub(super) fn report_command_resolution_error(
state: &ShellState,
program: &str,
resolution: &CommandResolution,
source_line: Option<u32>,
) {
let (code, message) = match resolution {
CommandResolution::NotExecutable(path) => (
"resolve.not_executable",
format!("{}: Permission denied", path.display()),
),
CommandResolution::NotFound => {
("resolve.not_found", format!("{program}: command not found"))
}
};
if let (Some(source), Some(line)) = (state.current_source.as_deref(), source_line) {
let rendered = format!("{source}: line {line}: {message}");
let _ = state.stderr_fd.write_line(&rendered);
emit_diagnostic(
state,
DiagnosticKind::Error,
DiagnosticCategory::CommandLookup,
code,
&message,
Some(source.to_string()),
Some(range_for_line(line)),
);
} else {
let _ = state.stderr_fd.write_line(&message);
emit_diagnostic(
state,
DiagnosticKind::Error,
DiagnosticCategory::CommandLookup,
code,
&message,
None,
None,
);
}
}
pub(crate) fn resolve_simple_command<'ast, R: Runtime>(
state: &mut ShellState,
runtime: &mut R,
sc: &'ast SimpleCommand,
) -> Result<PlannedSimpleCommand<'ast>, i32> {
if sc.name.is_none() {
let has_command_substitution = sc
.assignments
.iter()
.any(|assign| shell_expand::word_contains_command_substitution(&assign.value));
return Ok(ResolvedSimpleCommand {
assignments: &sc.assignments,
redirects: &sc.io_redirects,
argv: Vec::new(),
command_name: None,
lookup: CommandLookup::Empty,
should_restore_assignments: false,
has_command_substitution,
source_line: None,
}
.into_planned(state));
}
let name_word = sc.name.as_ref().unwrap();
let Some(expanded_name) = shell_expand::expand_command_name(state, runtime, name_word) else {
return Ok(ResolvedSimpleCommand {
assignments: &sc.assignments,
redirects: &sc.io_redirects,
argv: Vec::new(),
command_name: None,
lookup: CommandLookup::Empty,
should_restore_assignments: false,
has_command_substitution: false,
source_line: shell_expand::word_begin_line(name_word),
}
.into_planned(state));
};
if let Some(status) = state.take_expansion_error() {
return Err(status);
}
let argv = shell_expand::build_argv(state, runtime, expanded_name.clone(), &sc.arguments);
if let Some(status) = state.take_expansion_error() {
return Err(status);
}
let command_name = argv.first().cloned().unwrap_or(expanded_name);
let lookup = if has_shell_override(state, &argv) {
CommandLookup::ShellOverride
} else if state
.definition
.command_policy
.is_unspecified_utility(&command_name)
{
CommandLookup::UnspecifiedUtility
} else if state.functions.contains_key(&command_name) {
CommandLookup::Function
} else if super::builtins::is_special_builtin_in(state, &command_name) {
CommandLookup::SpecialBuiltin
} else if super::builtins::is_builtin_in(state, &command_name) {
CommandLookup::Builtin
} else {
let path_var = state.env_get("PATH").unwrap_or("/usr/bin:/bin");
match runtime.resolve_command_path(&command_name, path_var) {
Ok(path) => CommandLookup::External {
program: path.display().to_string(),
},
Err(err) => {
let resolution = command_resolution_from_error(&command_name, &err);
if matches!(resolution, CommandResolution::NotFound)
&& state
.definition
.command_policy
.command_not_found_handler()
.is_some()
{
CommandLookup::CommandNotFoundHandler
} else {
CommandLookup::ExternalFailed(resolution)
}
}
}
};
let should_restore_assignments = !sc.assignments.is_empty()
&& !matches!(
lookup,
CommandLookup::SpecialBuiltin | CommandLookup::Function
);
Ok(ResolvedSimpleCommand {
assignments: &sc.assignments,
redirects: &sc.io_redirects,
argv,
command_name: Some(command_name),
lookup,
should_restore_assignments,
has_command_substitution: false,
source_line: shell_expand::word_begin_line(name_word),
}
.into_planned(state))
}