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,
}
const DEFAULT_COMMAND_PATH: &str = "/usr/bin:/bin";
fn sanitize_command_path(state: &ShellState, path_var: &str) -> String {
if state.definition.security_policy.allow_path_search() {
return path_var.to_string();
}
let cleaned: Vec<String> = path_var
.split(':')
.filter_map(|path| {
let normalized = normalize_command_path_entry(path);
let normalized_path = Path::new(&normalized);
(!normalized.is_empty() && normalized_path.is_absolute()).then_some(normalized)
})
.collect();
cleaned.join(":")
}
fn normalize_command_path_entry(path: &str) -> String {
let mut components = Path::new(path).components();
let mut normalized = PathBuf::new();
for component in components.by_ref() {
if component == std::path::Component::CurDir {
continue;
}
normalized.push(component.as_os_str());
}
normalized.to_string_lossy().to_string()
}
pub(super) fn command_search_path(state: &ShellState) -> String {
let path_var = state.env_get("PATH").unwrap_or(DEFAULT_COMMAND_PATH);
sanitize_command_path(state, path_var)
}
pub(super) fn default_command_search_path(state: &ShellState) -> String {
sanitize_command_path(state, DEFAULT_COMMAND_PATH)
}
fn sanitize_exec_environment(
state: &ShellState,
env: Vec<(String, String)>,
) -> Vec<(String, String)> {
if state.definition.security_policy.allow_path_search() {
return env;
}
env.into_iter()
.map(|(key, value)| {
if key == "PATH" {
(key, sanitize_command_path(state, &value))
} else {
(key, value)
}
})
.collect()
}
pub(super) fn exported_exec_environment(state: &ShellState) -> Vec<(String, String)> {
sanitize_exec_environment(state, state.exported_env())
}
#[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,
resolve_external_after_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,
resolve_external_after_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,
resolve_external_after_assignments,
kind,
source_line,
}
}
}
fn has_path_assignment(assignments: &[Assignment]) -> bool {
assignments
.iter()
.any(|assignment| assignment.name == "PATH")
}
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(normalize_exit_status(
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(normalize_exit_status(handler(&mut context, argv)))
}
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 command_failure_status(resolution: &CommandResolution) -> i32 {
match resolution {
CommandResolution::NotExecutable(_) => 126,
CommandResolution::NotFound => 127,
}
}
fn redirect_contains_command_substitution(redir: &IoRedirect) -> bool {
shell_expand::word_contains_command_substitution(redir.name())
|| (redir.here_document_expand()
&& redir
.here_document()
.iter()
.any(shell_expand::word_contains_command_substitution))
}
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))
|| sc
.io_redirects
.iter()
.any(redirect_contains_command_substitution);
return Ok(ResolvedSimpleCommand {
assignments: &sc.assignments,
redirects: &sc.io_redirects,
argv: Vec::new(),
command_name: None,
lookup: CommandLookup::Empty,
should_restore_assignments: false,
resolve_external_after_assignments: false,
has_command_substitution,
source_line: None,
}
.into_planned(state));
}
let name_word = sc.name.as_ref().unwrap();
let expanded_name = shell_expand::expand_command_name(state, runtime, name_word);
if let Some(status) = state.take_expansion_error() {
return Err(status);
}
let declaration_utility = expanded_name
.first()
.is_some_and(|name| matches!(name.as_str(), "export" | "readonly"));
let argv = if declaration_utility {
shell_expand::build_declaration_argv(state, runtime, expanded_name, &sc.arguments)
} else {
shell_expand::build_argv(state, runtime, expanded_name, &sc.arguments)
};
if let Some(status) = state.take_expansion_error() {
return Err(status);
}
let Some(command_name) = argv.first().cloned().filter(|name| !name.is_empty()) else {
return Ok(ResolvedSimpleCommand {
assignments: &sc.assignments,
redirects: &sc.io_redirects,
argv: Vec::new(),
command_name: None,
lookup: CommandLookup::Empty,
should_restore_assignments: false,
resolve_external_after_assignments: false,
has_command_substitution: state.command_substitution_status.is_some(),
source_line: shell_expand::word_begin_line(name_word),
}
.into_planned(state));
};
let mut resolve_external_after_assignments = false;
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 super::builtins::is_special_builtin_in(state, &command_name) {
CommandLookup::SpecialBuiltin
} else if state.definition_store.functions.contains_key(&command_name) {
CommandLookup::Function
} else if super::builtins::is_builtin_in(state, &command_name) {
CommandLookup::Builtin
} else if has_path_assignment(&sc.assignments) {
resolve_external_after_assignments = true;
CommandLookup::External {
program: command_name.clone(),
}
} else {
let path_var = command_search_path(state);
match super::path::resolve_command_path(state, runtime, &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,
resolve_external_after_assignments,
has_command_substitution: false,
source_line: shell_expand::word_begin_line(name_word),
}
.into_planned(state))
}