mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use std::collections::{HashMap, HashSet};
use std::ffi::{CStr, CString};
use std::fs::{self, OpenOptions};
use std::io;
use std::os::unix::io::IntoRawFd;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use serde::{Deserialize, Serialize};

#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use crate::args::shell_option_labels;
pub(crate) use crate::args::{OptionParseMode, parse_option_args_with_schema};
use crate::args::{shell_long_options_with_schema, shell_option_labels_with_schema};
use crate::ast::{
    AndOrList, ArithmAssignOp, ArithmBinOp, ArithmExpr, ArithmUnOp, Assignment, BinOpType,
    CaseClause, Command as AstCommand, CommandList, ElsePart, ForClause, FunctionDefinition,
    IfClause, IoRedirect, IoRedirectOp, LoopClause, LoopType, ParameterOp, Pipeline, Program,
    SimpleCommand, Word,
};
use crate::sys::{self, Runtime};

mod arithm;
mod builtins;
mod command;
mod compound;
mod driver;
mod events;
mod exec;
mod expand;
mod frontend;
mod jobs;
mod launch;
mod machine;
mod path;
mod read;
mod redirects;
mod resolve;
mod run;
mod simple_command;
mod startup;
mod state;
mod traps;

use self::arithm::eval_arithm;
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use self::arithm::{eval_arithm_binop, eval_assign_op};
use self::builtins as shell_builtins;
pub(crate) use self::builtins::{Builtin as StandardBuiltin, standard_builtin_registry};
use self::command::run_exec_command_list_array;
pub(crate) use self::driver::initialize_shell_session;
#[cfg(feature = "frontend")]
pub(crate) use self::driver::run_non_interactive;
use self::events as shell_events;
#[cfg(feature = "frontend")]
pub(crate) use self::exec::run_machine_payload;
use self::expand as shell_expand;
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use self::frontend::append_history_line;
pub(crate) use self::frontend::maybe_warn_vi_unsupported;
use self::frontend::{shell_errln, shell_out, shell_out_bytes, shell_outln};
use self::jobs as shell_jobs;
#[cfg(feature = "frontend")]
pub(crate) use self::jobs::maybe_notify_jobs;
use self::launch::{ChildFdDisposition, ChildLaunchPlan, ProcessGroupPlan};
#[cfg(feature = "frontend")]
pub(crate) use self::machine::load_machine_payload_text;
pub(crate) use self::machine::resolve_machine_program_path;
use self::read as shell_read;
use self::redirects::{
    AssignmentRestore, RedirectGuard, restore_assignment_values, set_assignment_values,
};
use self::resolve as shell_resolve;
pub use self::run::run_program;
#[cfg(any(feature = "frontend", all(test, feature = "test-support")))]
use self::run::run_string_with_source;
pub(crate) use self::run::{run_planned_program, run_string};
pub(crate) use self::simple_command::plan_simple_command;
#[cfg(any(
    feature = "frontend",
    all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(crate) use self::startup::is_login_shell_argv;
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use self::startup::run;
#[cfg(feature = "frontend")]
pub(crate) use self::startup::run_interactive;
#[cfg(feature = "frontend")]
pub(crate) use self::startup::{finalize_shell_run, report_arg_error};
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
use self::startup::{run_with_state, should_source_profile};
pub(crate) use self::state::DetachedStateCheckpoint;
pub(crate) use self::state::ExecutionContextKind;
use self::state::{
    BackgroundMachinePayload, CommandOutcome, ControlFlow, ErrexitContext, JobInfo, JobState,
    ProcessGlobalGuard, ShellFunction, ShellJob,
};
pub use self::state::{ShellState, Variable};
use self::traps as shell_traps;
#[cfg(feature = "frontend")]
pub(crate) use self::traps::clear_pending_traps;
use crate::embed::{DeferredWorkDetail, DeferredWorkKind};
use crate::plan::{
    DeferredExpansion, DeferredPipelineStageWork, DeferredReason, PlannedAndOr as ExecAndOrList,
    PlannedCommandList as ExecCommandList, PlannedLazyAst as ExecLazyAst,
    PlannedLazyNode as ExecLazyNode, PlannedPipeline as ShellExecPipeline,
    PlannedPipelineStage as ExecPipelineStage, PlannedProgram as ShellExecProgram,
    PlannedSimpleCommand, PlannedSimpleCommandKind, PreparedExternalStagePlan,
};
use crate::status::normalize_exit_status;

pub const OPT_ALLEXPORT: u32 = 1 << 0;
pub const OPT_NOTIFY: u32 = 1 << 1;
pub const OPT_NOCLOBBER: u32 = 1 << 2;
pub const OPT_ERREXIT: u32 = 1 << 3;
pub const OPT_NOGLOB: u32 = 1 << 4;
pub const OPT_MONITOR: u32 = 1 << 6;
pub const OPT_NOEXEC: u32 = 1 << 7;
pub const OPT_IGNOREEOF: u32 = 1 << 8;
pub const OPT_NOLOG: u32 = 1 << 9;
pub const OPT_VI: u32 = 1 << 10;
pub const OPT_NOUNSET: u32 = 1 << 11;
pub const OPT_VERBOSE: u32 = 1 << 12;
pub const OPT_XTRACE: u32 = 1 << 13;

pub const VAR_EXPORT: u32 = 1 << 0;
pub const VAR_READONLY: u32 = 1 << 1;
const TRAP_EXIT: i32 = 0;

#[derive(Debug, Clone, Copy, Default)]
struct MonitorSignalState {
    sigttou: Option<libc::sighandler_t>,
    sigttin: Option<libc::sighandler_t>,
    sigtstp: Option<libc::sighandler_t>,
}

const UNSPECIFIED_UTILITIES: &[&str] = &[
    "alloc",
    "autoload",
    "bind",
    "bindkey",
    "bye",
    "caller",
    "cap",
    "chdir",
    "clone",
    "comparguments",
    "compcall",
    "compctl",
    "compdescribe",
    "compfiles",
    "compgen",
    "compgroups",
    "complete",
    "compquote",
    "comptags",
    "comptry",
    "compvalues",
    "declare",
    "dirs",
    "disable",
    "disown",
    "dosh",
    "echotc",
    "echoti",
    "help",
    "history",
    "hist",
    "let",
    "local",
    "login",
    "logout",
    "map",
    "mapfile",
    "popd",
    "print",
    "pushd",
    "readarray",
    "repeat",
    "savehistory",
    "source",
    "shopt",
    "stop",
    "suspend",
    "typeset",
    "whence",
];

pub(crate) fn default_unspecified_utility_names() -> &'static [&'static str] {
    UNSPECIFIED_UTILITIES
}

const SHELL_KEYWORDS: &[&str] = &[
    "if", "then", "else", "elif", "fi", "for", "while", "until", "do", "done", "case", "in",
    "esac", "{", "}", "!",
];

fn sync_monitor_mode(state: &mut ShellState) {
    if !state.manage_signals {
        return;
    }
    let want_monitor = state.has_option(OPT_MONITOR);
    if want_monitor == state.monitor_mode_active {
        return;
    }
    if want_monitor {
        state.monitor_signals = MonitorSignalState {
            sigttou: Some(unsafe { libc::signal(libc::SIGTTOU, libc::SIG_IGN) }),
            sigttin: Some(unsafe { libc::signal(libc::SIGTTIN, libc::SIG_IGN) }),
            sigtstp: Some(unsafe { libc::signal(libc::SIGTSTP, libc::SIG_IGN) }),
        };
        state.monitor_mode_active = true;
        return;
    }
    if let Some(handler) = state.monitor_signals.sigttou.take() {
        let _ = unsafe { libc::signal(libc::SIGTTOU, handler) };
    }
    if let Some(handler) = state.monitor_signals.sigttin.take() {
        let _ = unsafe { libc::signal(libc::SIGTTIN, handler) };
    }
    if let Some(handler) = state.monitor_signals.sigtstp.take() {
        let _ = unsafe { libc::signal(libc::SIGTSTP, handler) };
    }
    state.monitor_mode_active = false;
}

fn restore_process_signal_state(state: &mut ShellState) {
    state.options &= !OPT_MONITOR;
    sync_monitor_mode(state);
    shell_traps::restore_trap_signals(state);
}

fn shell_job_control_active(state: &ShellState) -> bool {
    state.interactive
        && state.has_option(OPT_MONITOR)
        && !matches!(
            state.execution_context.kind,
            ExecutionContextKind::BackgroundJob
        )
}

/// Format active options as a flag string (e.g., "aCe").
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
fn format_options(options: u32) -> String {
    shell_option_labels()
        .filter_map(|(opt, ch)| ((options & opt) != 0).then_some(ch))
        .collect()
}

fn format_options_with_schema(options: u32, schema: &crate::policy::ShellOptionSchema) -> String {
    shell_option_labels_with_schema(schema)
        .filter_map(|(opt, ch)| ((options & opt) != 0).then_some(ch))
        .collect()
}

fn print_long_options(state: &mut ShellState) -> std::io::Result<()> {
    let options: Vec<_> = shell_long_options_with_schema(&state.definition.option_schema)
        .map(|(name, value)| (name.to_string(), value))
        .collect();
    for (name, value) in options {
        let mode = if (state.options & value) != 0 {
            '-'
        } else {
            '+'
        };
        shell_outln(state, &format!("set {mode}o {name}"))?;
    }
    Ok(())
}

fn readonly_assignment_status(
    state: &mut ShellState,
    name: &str,
    context: &str,
    expansion_context: bool,
) -> i32 {
    let message = if context.is_empty() {
        format!("{name}: readonly variable")
    } else {
        format!("{context}: {name}: readonly variable")
    };
    shell_errln(state, &message);
    if expansion_context {
        state.record_expansion_error(1);
        if !state.interactive {
            state.exit_code = 1;
        }
    }
    1
}

// ── External Command ─────────────────────────────────────────────────

/// Spawn an external process via the runtime as a single-element pipeline.
fn spawn_external_in_path<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    argv: &[String],
    source_line: Option<u32>,
    path_var: &str,
) -> i32 {
    let program = match argv.first() {
        Some(program) => program.clone(),
        None => return 0,
    };
    let resolved_program = match path::resolve_command_path(state, runtime, &program, path_var) {
        Ok(path) => path.display().to_string(),
        Err(err) => {
            let resolution = shell_resolve::command_resolution_from_error(&program, &err);
            shell_resolve::report_command_resolution_error(
                state,
                &program,
                &resolution,
                source_line,
            );
            return shell_resolve::command_failure_status(&resolution);
        }
    };
    let process_group = if shell_job_control_active(state) {
        ProcessGroupPlan::New
    } else {
        ProcessGroupPlan::Inherit
    };
    let launch = ChildLaunchPlan::new(state, resolved_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(crate) fn build_program_execution<'ast, R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    program: &'ast Program,
) -> ShellExecProgram<'ast> {
    exec::build_program_execution(state, runtime, program)
}

pub(crate) fn build_pipeline_execution<'ast, R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    pipeline: &'ast Pipeline,
) -> ShellExecPipeline<'ast> {
    exec::build_pipeline_execution(state, runtime, pipeline)
}

pub(crate) fn execute_program_plan<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    program: &ShellExecProgram<'_>,
) -> i32 {
    let status = run_exec_command_list_array(state, runtime, &program.body);
    shell_traps::run_pending_traps(state, runtime);
    status
}

pub(crate) fn execute_pipeline_plan<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    pipeline: &ShellExecPipeline<'_>,
) -> i32 {
    let previous_command_id = state.active_command_id.clone();
    if previous_command_id.is_none() && shell_events::observability_enabled(state) {
        state.set_active_command_id(Some(shell_events::new_command_id()));
    }
    let status = exec::run_exec_pipeline(state, runtime, pipeline);
    shell_traps::run_pending_traps(state, runtime);
    state.set_active_command_id(previous_command_id);
    status
}

pub(crate) fn finalize_shell_state<R: Runtime>(
    state: &mut ShellState,
    runtime: &mut R,
    status: i32,
) -> i32 {
    shell_traps::run_exit_trap(state, runtime);
    let code = if state.exit_code >= 0 {
        state.exit_code
    } else {
        status
    };
    restore_process_signal_state(state);
    code
}

pub(crate) fn discard_process_signal_state(state: &mut ShellState) {
    restore_process_signal_state(state);
}

#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
mod tests;