#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
use brush_parser::ast;
#[cfg(unix)]
use command_fds::{CommandFdExt, FdMapping};
use itertools::Itertools;
use crate::{
builtins, error, escape,
interp::{self, Execute, ProcessGroupPolicy},
openfiles::{self, OpenFile, OpenFiles},
processes, sys, trace_categories, ExecutionParameters, ExecutionResult, Shell,
};
pub(crate) enum CommandSpawnResult {
SpawnedProcess(processes::ChildProcess),
ImmediateExit(u8),
ExitShell(u8),
ReturnFromFunctionOrScript(u8),
BreakLoop(u8),
ContinueLoop(u8),
}
impl CommandSpawnResult {
#[allow(clippy::too_many_lines)]
pub async fn wait(self, no_wait: bool) -> Result<CommandWaitResult, error::Error> {
#[allow(clippy::ignored_unit_patterns)]
match self {
CommandSpawnResult::SpawnedProcess(mut child) => {
let process_wait_result = if !no_wait {
child.wait().await?
} else {
processes::ProcessWaitResult::Stopped
};
let command_wait_result = match process_wait_result {
processes::ProcessWaitResult::Completed(output) => {
CommandWaitResult::CommandCompleted(ExecutionResult::from(output))
}
processes::ProcessWaitResult::Stopped => CommandWaitResult::CommandStopped(
ExecutionResult::from(processes::ProcessWaitResult::Stopped),
child,
),
};
Ok(command_wait_result)
}
CommandSpawnResult::ImmediateExit(exit_code) => Ok(
CommandWaitResult::CommandCompleted(ExecutionResult::new(exit_code)),
),
CommandSpawnResult::ExitShell(exit_code) => {
Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
exit_code,
exit_shell: true,
..ExecutionResult::default()
}))
}
CommandSpawnResult::ReturnFromFunctionOrScript(exit_code) => {
Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
exit_code,
return_from_function_or_script: true,
..ExecutionResult::default()
}))
}
CommandSpawnResult::BreakLoop(count) => {
Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
exit_code: 0,
break_loop: Some(count),
..ExecutionResult::default()
}))
}
CommandSpawnResult::ContinueLoop(count) => {
Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
exit_code: 0,
continue_loop: Some(count),
..ExecutionResult::default()
}))
}
}
}
}
pub(crate) enum CommandWaitResult {
CommandCompleted(ExecutionResult),
CommandStopped(ExecutionResult, processes::ChildProcess),
}
pub struct ExecutionContext<'a> {
pub shell: &'a mut Shell,
pub command_name: String,
pub params: ExecutionParameters,
}
impl ExecutionContext<'_> {
pub fn stdin(&self) -> openfiles::OpenFile {
self.fd(0).unwrap()
}
pub fn stdout(&self) -> openfiles::OpenFile {
self.fd(1).unwrap()
}
pub fn stderr(&self) -> openfiles::OpenFile {
self.fd(2).unwrap()
}
#[allow(clippy::unwrap_in_result)]
pub fn fd(&self, fd: u32) -> Option<openfiles::OpenFile> {
self.params
.open_files
.files
.get(&fd)
.map(|f| f.try_dup().unwrap())
}
pub(crate) fn should_cmd_lead_own_process_group(&self) -> bool {
self.shell.options.interactive
&& matches!(
self.params.process_group_policy,
ProcessGroupPolicy::NewProcessGroup
)
}
}
#[derive(Clone, Debug)]
pub enum CommandArg {
String(String),
Assignment(ast::Assignment),
}
impl Display for CommandArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CommandArg::String(s) => f.write_str(s),
CommandArg::Assignment(a) => write!(f, "{a}"),
}
}
}
impl From<String> for CommandArg {
fn from(s: String) -> Self {
CommandArg::String(s)
}
}
impl From<&String> for CommandArg {
fn from(value: &String) -> Self {
CommandArg::String(value.clone())
}
}
impl CommandArg {
pub fn quote_for_tracing(&self) -> Cow<'_, str> {
match self {
CommandArg::String(s) => escape::quote_if_needed(s, escape::QuoteMode::Quote),
CommandArg::Assignment(a) => {
let mut s = a.name.to_string();
let op = if a.append { "+=" } else { "=" };
s.push_str(op);
s.push_str(&escape::quote_if_needed(
a.value.to_string().as_str(),
escape::QuoteMode::Quote,
));
s.into()
}
}
}
}
#[allow(unused_variables)]
pub(crate) fn compose_std_command<S: AsRef<OsStr>>(
shell: &mut Shell,
command_name: &str,
argv0: &str,
args: &[S],
mut open_files: OpenFiles,
empty_env: bool,
) -> Result<std::process::Command, error::Error> {
let mut cmd = std::process::Command::new(command_name);
#[cfg(unix)]
cmd.arg0(argv0);
for arg in args {
cmd.arg(arg);
}
cmd.current_dir(shell.working_dir.as_path());
cmd.env_clear();
if !empty_env {
for (name, var) in shell.env.iter() {
if var.is_exported() {
let value_as_str = var.value().to_cow_string();
cmd.env(name, value_as_str.as_ref());
}
}
}
match open_files.files.remove(&0) {
Some(OpenFile::Stdin) | None => (),
Some(stdin_file) => {
let as_stdio: Stdio = stdin_file.into();
cmd.stdin(as_stdio);
}
}
match open_files.files.remove(&1) {
Some(OpenFile::Stdout) | None => (),
Some(stdout_file) => {
let as_stdio: Stdio = stdout_file.into();
cmd.stdout(as_stdio);
}
}
match open_files.files.remove(&2) {
Some(OpenFile::Stderr) | None => {}
Some(stderr_file) => {
let as_stdio: Stdio = stderr_file.into();
cmd.stderr(as_stdio);
}
}
#[cfg(unix)]
{
let fd_mappings = open_files
.files
.into_iter()
.map(|(child_fd, open_file)| FdMapping {
child_fd: i32::try_from(child_fd).unwrap(),
parent_fd: open_file.into_owned_fd().unwrap(),
})
.collect();
cmd.fd_mappings(fd_mappings)
.map_err(|_e| error::Error::ChildCreationFailure)?;
}
#[cfg(not(unix))]
{
if !open_files.files.is_empty() {
return error::unimp("fd redirections on non-Unix platform");
}
}
Ok(cmd)
}
pub(crate) async fn execute(
cmd_context: ExecutionContext<'_>,
process_group_id: &mut Option<i32>,
args: Vec<CommandArg>,
use_functions: bool,
) -> Result<CommandSpawnResult, error::Error> {
if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
let builtin = cmd_context
.shell
.builtins
.get(&cmd_context.command_name)
.cloned();
if builtin
.as_ref()
.is_some_and(|r| !r.disabled && r.special_builtin)
{
return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
}
if use_functions {
if let Some(func_reg) = cmd_context
.shell
.funcs
.get(cmd_context.command_name.as_str())
{
return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
.await;
}
}
if let Some(builtin) = builtin {
if !builtin.disabled {
return execute_builtin_command(&builtin, cmd_context, args).await;
}
}
if let Some(path) = cmd_context
.shell
.find_first_executable_in_path_using_cache(&cmd_context.command_name)
{
let resolved_path = path.to_string_lossy();
execute_external_command(
cmd_context,
resolved_path.as_ref(),
process_group_id,
&args[1..],
)
} else {
tracing::error!("{}: command not found", cmd_context.command_name);
Ok(CommandSpawnResult::ImmediateExit(127))
}
} else {
let resolved_path = cmd_context.command_name.clone();
execute_external_command(
cmd_context,
resolved_path.as_str(),
process_group_id,
&args[1..],
)
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn execute_external_command(
context: ExecutionContext<'_>,
executable_path: &str,
process_group_id: &mut Option<i32>,
args: &[CommandArg],
) -> Result<CommandSpawnResult, error::Error> {
let mut cmd_args = vec![];
for arg in args {
if let CommandArg::String(s) = arg {
cmd_args.push(s);
}
}
#[allow(unused_variables)]
let child_stdin_is_terminal = context
.params
.open_files
.stdin()
.is_some_and(|f| f.is_term());
let new_pg = context.should_cmd_lead_own_process_group();
#[allow(unused_mut)]
let mut cmd = compose_std_command(
context.shell,
executable_path,
context.command_name.as_str(),
cmd_args.as_slice(),
context.params.open_files,
false, )?;
if new_pg {
#[cfg(unix)]
cmd.process_group(0);
} else if let Some(pgid) = process_group_id {
#[cfg(unix)]
cmd.process_group(*pgid);
}
#[cfg(unix)]
if new_pg && child_stdin_is_terminal {
unsafe {
cmd.pre_exec(setup_process_before_exec);
}
}
tracing::debug!(
target: trace_categories::COMMANDS,
"Spawning: cmd='{} {}'",
cmd.get_program().to_string_lossy().to_string(),
cmd.get_args()
.map(|a| a.to_string_lossy().to_string())
.join(" ")
);
match sys::process::spawn(cmd) {
Ok(child) => {
#[allow(clippy::cast_possible_wrap)]
let pid = child.id().map(|id| id as i32);
if let Some(pid) = &pid {
if new_pg {
*process_group_id = Some(*pid);
}
} else {
tracing::warn!("could not retrieve pid for child process");
}
Ok(CommandSpawnResult::SpawnedProcess(
processes::ChildProcess::new(pid, child),
))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if context.shell.options.interactive {
sys::terminal::move_self_to_foreground()?;
}
if context.shell.options.sh_mode {
tracing::error!(
"{}: {}: {}: not found",
context.shell.shell_name.as_ref().unwrap_or(&String::new()),
context.shell.get_current_input_line_number(),
context.command_name
);
} else {
tracing::error!("{}: not found", context.command_name);
}
Ok(CommandSpawnResult::ImmediateExit(127))
}
Err(e) => {
if context.shell.options.interactive {
sys::terminal::move_self_to_foreground()?;
}
tracing::error!("error: {}", e);
Ok(CommandSpawnResult::ImmediateExit(126))
}
}
}
#[cfg(unix)]
fn setup_process_before_exec() -> Result<(), std::io::Error> {
sys::terminal::move_self_to_foreground().map_err(std::io::Error::other)?;
Ok(())
}
async fn execute_builtin_command(
builtin: &builtins::Registration,
context: ExecutionContext<'_>,
args: Vec<CommandArg>,
) -> Result<CommandSpawnResult, error::Error> {
let exit_code = match (builtin.execute_func)(context, args).await {
Ok(builtin_result) => match builtin_result.exit_code {
builtins::ExitCode::Success => 0,
builtins::ExitCode::InvalidUsage => 2,
builtins::ExitCode::Unimplemented => 99,
builtins::ExitCode::Custom(code) => code,
builtins::ExitCode::ExitShell(code) => return Ok(CommandSpawnResult::ExitShell(code)),
builtins::ExitCode::ReturnFromFunctionOrScript(code) => {
return Ok(CommandSpawnResult::ReturnFromFunctionOrScript(code))
}
builtins::ExitCode::BreakLoop(count) => {
return Ok(CommandSpawnResult::BreakLoop(count))
}
builtins::ExitCode::ContinueLoop(count) => {
return Ok(CommandSpawnResult::ContinueLoop(count))
}
},
Err(e) => {
tracing::error!("error: {}", e);
1
}
};
Ok(CommandSpawnResult::ImmediateExit(exit_code))
}
pub(crate) async fn invoke_shell_function(
function_definition: Arc<ast::FunctionDefinition>,
mut context: ExecutionContext<'_>,
args: &[CommandArg],
) -> Result<CommandSpawnResult, error::Error> {
let ast::FunctionBody(body, redirects) = &function_definition.body;
if let Some(redirects) = redirects {
for redirect in &redirects.0 {
interp::setup_redirect(&mut context.params.open_files, context.shell, redirect).await?;
}
}
let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
let params = context.params.clone();
context
.shell
.enter_function(context.command_name.as_str(), &function_definition)?;
let result = body.execute(context.shell, ¶ms).await;
drop(params);
context.shell.leave_function()?;
context.shell.positional_parameters = prior_positional_params;
Ok(CommandSpawnResult::ImmediateExit(result?.exit_code))
}