mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use super::*;

use crate::plan::{CommandResolutionError, PlannedSimpleCommand, PlannedSimpleCommandKind};

struct SimpleCommandTransaction {
    redirects: RedirectGuard,
    old_vars: Vec<AssignmentRestore>,
    restore_assignments: bool,
    command_substitution_status: Option<i32>,
}

pub(crate) fn plan_simple_command<'ast, R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    sc: &'ast SimpleCommand,
) -> Result<PlannedSimpleCommand<'ast>, i32> {
    shell_resolve::resolve_simple_command(state, runtime, sc)
}

impl SimpleCommandTransaction {
    fn prepare<R: Runtime>(
        state: &mut ShellState,
        runtime: &mut R,
        plan: &PlannedSimpleCommand<'_>,
    ) -> Result<Self, i32> {
        if !matches!(
            plan.kind(),
            PlannedSimpleCommandKind::AssignmentsOnly { .. }
        ) {
            let argv = plan.argv();
            if state.has_option(OPT_XTRACE) && !argv.is_empty() {
                let ps4 = state.env_get("PS4").unwrap_or("+ ").to_string();
                let trace = format!("{ps4}{}", argv.join(" "));
                shell_errln(state, &trace);
            }
        }

        match plan.kind() {
            PlannedSimpleCommandKind::AssignmentsOnly { .. } => {
                let command_name_status = state.take_command_substitution_status();
                if let Err(status) = set_assignment_values(
                    state,
                    runtime,
                    plan.assignments(),
                    plan.assignment_attributes().bits(),
                    false,
                    "",
                ) {
                    state.clear_command_substitution_status();
                    return Err(status);
                }
                let assignment_command_substitution_status =
                    state.take_command_substitution_status();
                let redirects = match RedirectGuard::apply(state, runtime, plan.redirects()) {
                    Ok(redirects) => redirects,
                    Err(status) => {
                        state.clear_command_substitution_status();
                        return Err(status);
                    }
                };
                let redirect_command_substitution_status = state.take_command_substitution_status();
                Ok(Self {
                    redirects,
                    old_vars: Vec::new(),
                    restore_assignments: false,
                    command_substitution_status: redirect_command_substitution_status
                        .or(assignment_command_substitution_status)
                        .or(command_name_status),
                })
            }
            _ => {
                state.clear_command_substitution_status();
                let context = plan.command_name().unwrap_or("");
                let old_vars = match set_assignment_values(
                    state,
                    runtime,
                    plan.assignments(),
                    plan.assignment_attributes().bits(),
                    plan.restore_assignments(),
                    context,
                ) {
                    Ok(old_vars) => old_vars,
                    Err(status) => {
                        state.clear_command_substitution_status();
                        return Err(status);
                    }
                };
                let redirects = match RedirectGuard::apply(state, runtime, plan.redirects()) {
                    Ok(redirects) => redirects,
                    Err(status) => {
                        state.clear_command_substitution_status();
                        if plan.restore_assignments() {
                            restore_assignment_values(state, old_vars);
                        }
                        return Err(status);
                    }
                };
                state.clear_command_substitution_status();
                Ok(Self {
                    redirects,
                    old_vars,
                    restore_assignments: plan.restore_assignments(),
                    command_substitution_status: None,
                })
            }
        }
    }

    fn commit(self, state: &mut ShellState, persist_redirects: bool) {
        if self.restore_assignments {
            restore_assignment_values(state, self.old_vars);
        }
        if persist_redirects {
            self.redirects.commit(state);
        } else {
            self.redirects.restore(state);
        }
    }
}

fn run_external_simple_command<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    plan: &PlannedSimpleCommand<'_>,
    program: &str,
    argv: &[String],
) -> i32 {
    let program = if plan.resolve_external_after_assignments {
        let command_name = argv.first().map(String::as_str).unwrap_or(program);
        let path_var = shell_resolve::command_search_path(state);
        match super::path::resolve_command_path(state, runtime, command_name, &path_var) {
            Ok(path) => path.display().to_string(),
            Err(err) => {
                let resolution = shell_resolve::command_resolution_from_error(command_name, &err);
                if matches!(resolution, shell_resolve::CommandResolution::NotFound)
                    && state
                        .definition
                        .command_policy
                        .command_not_found_handler()
                        .is_some()
                {
                    return shell_resolve::run_command_not_found_handler(state, runtime, argv)
                        .unwrap_or(127);
                }
                shell_resolve::report_command_resolution_error(
                    state,
                    command_name,
                    &resolution,
                    plan.source_line(),
                );
                return shell_resolve::command_failure_status(&resolution);
            }
        }
    } else {
        program.to_string()
    };

    let process_group = if shell_job_control_active(state) {
        ProcessGroupPlan::New
    } else {
        ProcessGroupPlan::Inherit
    };
    let launch = ChildLaunchPlan::new(state, program, argv.to_vec(), process_group);
    match launch.spawn(runtime, sys::SpawnMode::Foreground) {
        Ok(child) => launch.wait_foreground(state, runtime, child),
        Err(err) => sys::spawn_error_exit_status(&err),
    }
}

pub(super) fn run_planned_simple_command<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    plan: &PlannedSimpleCommand<'_>,
) -> CommandOutcome {
    let transaction = match SimpleCommandTransaction::prepare(state, runtime, plan) {
        Ok(prepared) => prepared,
        Err(status) => {
            maybe_abort_for_simple_command_error(state, plan, status, true);
            return CommandOutcome::from_state(status, state);
        }
    };
    let mut persist_redirects = false;
    let status = match plan.kind() {
        PlannedSimpleCommandKind::AssignmentsOnly {
            has_command_substitution,
        } => {
            if *has_command_substitution {
                transaction.command_substitution_status.unwrap_or(0)
            } else {
                0
            }
        }
        PlannedSimpleCommandKind::Function { command_name, argv } => {
            let Some(function) = state.function_definition(command_name).cloned() else {
                transaction.commit(state, false);
                return CommandOutcome::from_state(0, state);
            };
            let old_frame = state.variable_store.frame.clone();
            let mut function_frame = Vec::with_capacity(argv.len().max(1));
            function_frame.push(
                old_frame
                    .first()
                    .cloned()
                    .unwrap_or_else(|| state.shell_name().to_string()),
            );
            function_frame.extend(argv.iter().skip(1).cloned());
            state.variable_store.frame = function_frame;
            state.function_depth += 1;
            let previous_context =
                state.enter_local_execution_context(ExecutionContextKind::Function);
            let status = if function.definition_redirects.is_empty() {
                super::exec::LazyNode::owned_command(
                    function.body,
                    DeferredReason::NeedsCurrentShellState,
                )
                .execute_command(state, runtime)
            } else {
                run_command_with_definition_redirects(state, runtime, function)
            };
            state.restore_execution_context(previous_context);
            state.function_depth -= 1;
            state.variable_store.frame = old_frame;
            if matches!(state.control_flow, ControlFlow::Return(_)) {
                state.control_flow = ControlFlow::None;
            }
            status
        }
        PlannedSimpleCommandKind::Builtin { argv } => {
            let status = shell_builtins::run_builtin(state, runtime, argv).unwrap_or(1);
            persist_redirects =
                status == 0 && argv.first().is_some_and(|name| name == "exec") && argv.len() == 1;
            status
        }
        PlannedSimpleCommandKind::ShellOverride { argv } => {
            shell_resolve::run_shell_override(state, runtime, argv).unwrap_or(1)
        }
        PlannedSimpleCommandKind::CommandNotFoundHandler { argv } => {
            shell_resolve::run_command_not_found_handler(state, runtime, argv).unwrap_or(127)
        }
        PlannedSimpleCommandKind::External { program, argv } => {
            run_external_simple_command(state, runtime, plan, program, argv)
        }
        PlannedSimpleCommandKind::UnspecifiedUtility { command_name } => {
            if state.interactive {
                shell_errln(
                    state,
                    &format!("{command_name}: The behavior of this command is undefined."),
                );
            } else {
                shell_errln(
                    state,
                    &format!(
                        "{command_name}: The behavior of this command is undefined. This is an error in your script. Aborting."
                    ),
                );
                state.exit_code = 1;
            }
            1
        }
        PlannedSimpleCommandKind::ResolutionFailure {
            command_name,
            resolution,
        } => {
            let resolution = match resolution {
                CommandResolutionError::NotExecutable(path) => {
                    shell_resolve::CommandResolution::NotExecutable(path.clone())
                }
                CommandResolutionError::NotFound => shell_resolve::CommandResolution::NotFound,
            };
            shell_resolve::report_command_resolution_error(
                state,
                command_name,
                &resolution,
                plan.source_line(),
            );
            shell_resolve::command_failure_status(&resolution)
        }
    };
    let status = normalize_exit_status(status);
    transaction.commit(state, persist_redirects);
    maybe_abort_for_simple_command_error(state, plan, status, false);
    CommandOutcome::from_state(status, state)
}

fn maybe_abort_for_simple_command_error(
    state: &mut ShellState,
    plan: &PlannedSimpleCommand<'_>,
    status: i32,
    preparation_error: bool,
) {
    if status == 0 || state.interactive || state.exit_code >= 0 {
        return;
    }
    let should_abort = match plan.kind() {
        PlannedSimpleCommandKind::AssignmentsOnly { .. } => preparation_error,
        PlannedSimpleCommandKind::Builtin { argv } => argv.first().is_some_and(|name| {
            if !shell_builtins::is_special_builtin_in(state, name) {
                return false;
            }
            if preparation_error {
                return true;
            }
            !matches!(
                name.as_str(),
                "." | "break" | "continue" | "eval" | "exec" | "return"
            )
        }),
        _ => false,
    };
    if should_abort {
        state.set_exit_code(status);
    }
}

pub(super) fn run_simple_command<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    sc: &SimpleCommand,
) -> CommandOutcome {
    let plan = match plan_simple_command(state, runtime, sc) {
        Ok(plan) => plan,
        Err(status) => return CommandOutcome::from_state(status, state),
    };
    run_planned_simple_command(state, runtime, &plan)
}

fn run_command_with_definition_redirects<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    function: ShellFunction,
) -> i32 {
    let Ok(redirects) = RedirectGuard::apply(state, runtime, &function.definition_redirects) else {
        return 1;
    };
    let status =
        super::exec::LazyNode::owned_command(function.body, DeferredReason::NeedsCurrentShellState)
            .execute_command(state, runtime);
    redirects.restore(state);
    status
}