#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::{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,
interp::{self, Execute},
openfiles::{self, OpenFile, OpenFiles},
sys, Shell,
};
pub(crate) enum SpawnResult {
SpawnedChild(sys::process::Child),
ImmediateExit(u8),
ExitShell(u8),
ReturnFromFunctionOrScript(u8),
BreakLoop(u8),
ContinueLoop(u8),
}
pub struct ExecutionContext<'a> {
pub shell: &'a mut Shell,
pub command_name: String,
pub open_files: openfiles::OpenFiles,
}
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.open_files.files.get(&fd).map(|f| f.try_dup().unwrap())
}
}
#[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())
}
}
#[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<'_>,
args: Vec<CommandArg>,
use_functions: bool,
) -> Result<SpawnResult, 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;
}
}
}
execute_external_command(cmd_context, &args[1..])
}
#[allow(clippy::too_many_lines)]
pub(crate) fn execute_external_command(
context: ExecutionContext<'_>,
args: &[CommandArg],
) -> Result<SpawnResult, error::Error> {
let mut cmd_args = vec![];
for arg in args {
if let CommandArg::String(s) = arg {
cmd_args.push(s);
}
}
let cmd = compose_std_command(
context.shell,
context.command_name.as_str(),
context.command_name.as_str(),
cmd_args.as_slice(),
context.open_files,
false, )?;
tracing::debug!(
target: "commands",
"Spawning: {} {}",
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) => Ok(SpawnResult::SpawnedChild(child)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
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(SpawnResult::ImmediateExit(127))
}
Err(e) => {
tracing::error!("error: {}", e);
Ok(SpawnResult::ImmediateExit(126))
}
}
}
async fn execute_builtin_command(
builtin: &builtins::Registration,
context: ExecutionContext<'_>,
args: Vec<CommandArg>,
) -> Result<SpawnResult, 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(SpawnResult::ExitShell(code)),
builtins::ExitCode::ReturnFromFunctionOrScript(code) => {
return Ok(SpawnResult::ReturnFromFunctionOrScript(code))
}
builtins::ExitCode::BreakLoop(count) => return Ok(SpawnResult::BreakLoop(count)),
builtins::ExitCode::ContinueLoop(count) => return Ok(SpawnResult::ContinueLoop(count)),
},
Err(e) => {
tracing::error!("error: {}", e);
1
}
};
Ok(SpawnResult::ImmediateExit(exit_code))
}
pub(crate) async fn invoke_shell_function(
function_definition: Arc<ast::FunctionDefinition>,
mut context: ExecutionContext<'_>,
args: &[CommandArg],
) -> Result<SpawnResult, 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.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 = interp::ExecutionParameters {
open_files: context.open_files.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(SpawnResult::ImmediateExit(result?.exit_code))
}