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
)
}
#[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
}
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;