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
}