mxsh 0.1.0

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

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

struct PreparedSimpleCommand {
    redirects: RedirectGuard,
    old_vars: Vec<(String, Option<Variable>)>,
    restore_assignments: bool,
}

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)
}

fn prepare_simple_command<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    plan: &PlannedSimpleCommand<'_>,
) -> Result<PreparedSimpleCommand, 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 { .. } => {
            set_assignment_values(
                state,
                runtime,
                plan.assignments(),
                plan.assignment_attributes().bits(),
                false,
                "",
            )?;
            let redirects = RedirectGuard::apply(state, runtime, plan.redirects())?;
            Ok(PreparedSimpleCommand {
                redirects,
                old_vars: Vec::new(),
                restore_assignments: false,
            })
        }
        _ => {
            let redirects = RedirectGuard::apply(state, runtime, plan.redirects())?;
            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) => {
                    redirects.restore(state);
                    return Err(status);
                }
            };
            Ok(PreparedSimpleCommand {
                redirects,
                old_vars,
                restore_assignments: plan.restore_assignments(),
            })
        }
    }
}

fn commit_simple_command(state: &mut ShellState, prepared: PreparedSimpleCommand) {
    if prepared.restore_assignments {
        restore_assignment_values(state, prepared.old_vars);
    }
    prepared.redirects.restore(state);
}

pub(super) fn run_planned_simple_command<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    plan: &PlannedSimpleCommand<'_>,
) -> i32 {
    let prepared = match prepare_simple_command(state, runtime, plan) {
        Ok(prepared) => prepared,
        Err(status) => return status,
    };
    let status = match plan.kind() {
        PlannedSimpleCommandKind::AssignmentsOnly {
            has_command_substitution,
        } => {
            if *has_command_substitution {
                state.last_status
            } else {
                0
            }
        }
        PlannedSimpleCommandKind::Function { command_name, argv } => {
            let Some(func_body) = state.functions.get(command_name).cloned() else {
                commit_simple_command(state, prepared);
                return 0;
            };
            let old_frame = std::mem::replace(&mut state.frame, argv.clone());
            state.function_depth += 1;
            let status = super::exec::LazyNode::owned_command(
                func_body,
                DeferredReason::NeedsCurrentShellState,
            )
            .execute_command(state, runtime);
            state.function_depth -= 1;
            state.frame = old_frame;
            if state.branch == BranchControl::Return {
                state.branch = BranchControl::None;
            }
            status
        }
        PlannedSimpleCommandKind::Builtin { argv } => {
            shell_builtins::run_builtin(state, runtime, argv).unwrap_or(1)
        }
        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 } => match runtime
            .spawn_external_command(
                &sys::ExternalCommand {
                    program: program.clone(),
                    argv: argv.clone(),
                    env: state.exported_env(),
                    cwd: state.cwd.clone(),
                    create_process_group: false,
                    passed_fds: state.inherited_fds(),
                },
                sys::SpawnStdio {
                    stdin_fd: state.stdin_fd,
                    stdout_fd: state.stdout_fd,
                    stderr_fd: state.stderr_fd,
                },
                &[],
                sys::SpawnMode::Foreground,
            ) {
            Ok(child) => runtime.wait_child(child.handle),
            Err(err) => sys::spawn_error_exit_status(&err),
        },
        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)
        }
    };
    commit_simple_command(state, prepared);
    status
}

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