brush_core/
commands.rs

1use std::io::Write;
2#[cfg(unix)]
3use std::os::unix::process::CommandExt;
4use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
5
6use brush_parser::ast;
7#[cfg(unix)]
8use command_fds::{CommandFdExt, FdMapping};
9use itertools::Itertools;
10
11use crate::{
12    builtins, error, escape,
13    interp::{self, Execute, ProcessGroupPolicy},
14    openfiles::{self, OpenFile, OpenFiles},
15    processes, sys, trace_categories, ExecutionParameters, ExecutionResult, Shell,
16};
17
18/// Represents the result of spawning a command.
19pub(crate) enum CommandSpawnResult {
20    /// The child process was spawned.
21    SpawnedProcess(processes::ChildProcess),
22    /// The command immediatedly exited with the given numeric exit code.
23    ImmediateExit(u8),
24    /// The shell should exit after this command, yielding the given numeric exit code.
25    ExitShell(u8),
26    /// The shell should return from the current function or script, yielding the given numeric
27    /// exit code.
28    ReturnFromFunctionOrScript(u8),
29    /// The shell should break out of the containing loop, identified by the given depth count.
30    BreakLoop(u8),
31    /// The shell should continue the containing loop, identified by the given depth count.
32    ContinueLoop(u8),
33}
34
35impl CommandSpawnResult {
36    // TODO: jobs: remove `no_wait`; it doesn't make any sense
37    #[allow(clippy::too_many_lines)]
38    pub async fn wait(self, no_wait: bool) -> Result<CommandWaitResult, error::Error> {
39        #[allow(clippy::ignored_unit_patterns)]
40        match self {
41            CommandSpawnResult::SpawnedProcess(mut child) => {
42                let process_wait_result = if !no_wait {
43                    // Wait for the process to exit or for a relevant signal, whichever happens
44                    // first.
45                    child.wait().await?
46                } else {
47                    processes::ProcessWaitResult::Stopped
48                };
49
50                let command_wait_result = match process_wait_result {
51                    processes::ProcessWaitResult::Completed(output) => {
52                        CommandWaitResult::CommandCompleted(ExecutionResult::from(output))
53                    }
54                    processes::ProcessWaitResult::Stopped => CommandWaitResult::CommandStopped(
55                        ExecutionResult::from(processes::ProcessWaitResult::Stopped),
56                        child,
57                    ),
58                };
59
60                Ok(command_wait_result)
61            }
62            CommandSpawnResult::ImmediateExit(exit_code) => Ok(
63                CommandWaitResult::CommandCompleted(ExecutionResult::new(exit_code)),
64            ),
65            CommandSpawnResult::ExitShell(exit_code) => {
66                Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
67                    exit_code,
68                    exit_shell: true,
69                    ..ExecutionResult::default()
70                }))
71            }
72            CommandSpawnResult::ReturnFromFunctionOrScript(exit_code) => {
73                Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
74                    exit_code,
75                    return_from_function_or_script: true,
76                    ..ExecutionResult::default()
77                }))
78            }
79            CommandSpawnResult::BreakLoop(count) => {
80                Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
81                    exit_code: 0,
82                    break_loop: Some(count),
83                    ..ExecutionResult::default()
84                }))
85            }
86            CommandSpawnResult::ContinueLoop(count) => {
87                Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
88                    exit_code: 0,
89                    continue_loop: Some(count),
90                    ..ExecutionResult::default()
91                }))
92            }
93        }
94    }
95}
96
97/// Encapsulates the result of waiting for a command to complete.
98pub(crate) enum CommandWaitResult {
99    /// The command completed.
100    CommandCompleted(ExecutionResult),
101    /// The command was stopped before it completed.
102    CommandStopped(ExecutionResult, processes::ChildProcess),
103}
104
105/// Represents the context for executing a command.
106pub struct ExecutionContext<'a> {
107    /// The shell in which the command is being executed.
108    pub shell: &'a mut Shell,
109    /// The name of the command being executed.    
110    pub command_name: String,
111    /// The parameters for the execution.
112    pub params: ExecutionParameters,
113}
114
115impl ExecutionContext<'_> {
116    /// Returns the standard input file; usable with `write!` et al.
117    pub fn stdin(&self) -> openfiles::OpenFile {
118        self.params.stdin()
119    }
120
121    /// Returns the standard output file; usable with `write!` et al.
122    pub fn stdout(&self) -> openfiles::OpenFile {
123        self.params.stdout()
124    }
125
126    /// Returns the standard error file; usable with `write!` et al.
127    pub fn stderr(&self) -> openfiles::OpenFile {
128        self.params.stderr()
129    }
130
131    pub(crate) fn should_cmd_lead_own_process_group(&self) -> bool {
132        self.shell.options.interactive
133            && matches!(
134                self.params.process_group_policy,
135                ProcessGroupPolicy::NewProcessGroup
136            )
137    }
138}
139
140/// An argument to a command.
141#[derive(Clone, Debug)]
142pub enum CommandArg {
143    /// A simple string argument.
144    String(String),
145    /// An assignment/declaration; typically treated as a string, but will
146    /// be specially handled by a limited set of built-in commands.
147    Assignment(ast::Assignment),
148}
149
150impl Display for CommandArg {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        match self {
153            CommandArg::String(s) => f.write_str(s),
154            CommandArg::Assignment(a) => write!(f, "{a}"),
155        }
156    }
157}
158
159impl From<String> for CommandArg {
160    fn from(s: String) -> Self {
161        CommandArg::String(s)
162    }
163}
164
165impl From<&String> for CommandArg {
166    fn from(value: &String) -> Self {
167        CommandArg::String(value.clone())
168    }
169}
170
171impl CommandArg {
172    pub fn quote_for_tracing(&self) -> Cow<'_, str> {
173        match self {
174            CommandArg::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
175            CommandArg::Assignment(a) => {
176                let mut s = a.name.to_string();
177                let op = if a.append { "+=" } else { "=" };
178                s.push_str(op);
179                s.push_str(&escape::quote_if_needed(
180                    a.value.to_string().as_str(),
181                    escape::QuoteMode::SingleQuote,
182                ));
183                s.into()
184            }
185        }
186    }
187}
188
189#[allow(unused_variables)]
190pub(crate) fn compose_std_command<S: AsRef<OsStr>>(
191    shell: &mut Shell,
192    command_name: &str,
193    argv0: &str,
194    args: &[S],
195    mut open_files: OpenFiles,
196    empty_env: bool,
197) -> Result<std::process::Command, error::Error> {
198    let mut cmd = std::process::Command::new(command_name);
199
200    // Override argv[0].
201    #[cfg(unix)]
202    cmd.arg0(argv0);
203
204    // Pass through args.
205    cmd.args(args);
206
207    // Use the shell's current working dir.
208    cmd.current_dir(shell.working_dir.as_path());
209
210    // Start with a clear environment.
211    cmd.env_clear();
212
213    // Add in exported variables.
214    if !empty_env {
215        for (k, v) in shell.env.iter_exported() {
216            cmd.env(k.as_str(), v.value().to_cow_str(shell).as_ref());
217        }
218    }
219
220    // Redirect stdin, if applicable.
221    match open_files.files.remove(&0) {
222        Some(OpenFile::Stdin) | None => (),
223        Some(stdin_file) => {
224            let as_stdio: Stdio = stdin_file.into();
225            cmd.stdin(as_stdio);
226        }
227    }
228
229    // Redirect stdout, if applicable.
230    match open_files.files.remove(&1) {
231        Some(OpenFile::Stdout) | None => (),
232        Some(stdout_file) => {
233            let as_stdio: Stdio = stdout_file.into();
234            cmd.stdout(as_stdio);
235        }
236    }
237
238    // Redirect stderr, if applicable.
239    match open_files.files.remove(&2) {
240        Some(OpenFile::Stderr) | None => {}
241        Some(stderr_file) => {
242            let as_stdio: Stdio = stderr_file.into();
243            cmd.stderr(as_stdio);
244        }
245    }
246
247    // Inject any other fds.
248    #[cfg(unix)]
249    {
250        let fd_mappings = open_files
251            .files
252            .into_iter()
253            .map(|(child_fd, open_file)| FdMapping {
254                child_fd: i32::try_from(child_fd).unwrap(),
255                parent_fd: open_file.into_owned_fd().unwrap(),
256            })
257            .collect();
258        cmd.fd_mappings(fd_mappings)
259            .map_err(|_e| error::Error::ChildCreationFailure)?;
260    }
261    #[cfg(not(unix))]
262    {
263        if !open_files.files.is_empty() {
264            return error::unimp("fd redirections on non-Unix platform");
265        }
266    }
267
268    Ok(cmd)
269}
270
271pub(crate) async fn execute(
272    cmd_context: ExecutionContext<'_>,
273    process_group_id: &mut Option<i32>,
274    args: Vec<CommandArg>,
275    use_functions: bool,
276    path_dirs: Option<Vec<String>>,
277) -> Result<CommandSpawnResult, error::Error> {
278    if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
279        let builtin = cmd_context
280            .shell
281            .builtins
282            .get(&cmd_context.command_name)
283            .cloned();
284
285        // Ignore the builtin if it's marked as disabled.
286        if builtin
287            .as_ref()
288            .is_some_and(|r| !r.disabled && r.special_builtin)
289        {
290            return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
291        }
292
293        if use_functions {
294            if let Some(func_reg) = cmd_context
295                .shell
296                .funcs
297                .get(cmd_context.command_name.as_str())
298            {
299                // Strip the function name off args.
300                return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
301                    .await;
302            }
303        }
304
305        if let Some(builtin) = builtin {
306            if !builtin.disabled {
307                return execute_builtin_command(&builtin, cmd_context, args).await;
308            }
309        }
310
311        let path = if let Some(path_dirs) = path_dirs {
312            cmd_context
313                .shell
314                .find_executables_in(path_dirs.iter(), &cmd_context.command_name)
315                .first()
316                .cloned()
317        } else {
318            cmd_context
319                .shell
320                .find_first_executable_in_path_using_cache(&cmd_context.command_name)
321        };
322
323        if let Some(path) = path {
324            let resolved_path = path.to_string_lossy();
325            execute_external_command(
326                cmd_context,
327                resolved_path.as_ref(),
328                process_group_id,
329                &args[1..],
330            )
331        } else {
332            writeln!(
333                cmd_context.stderr(),
334                "{}: command not found",
335                cmd_context.command_name
336            )?;
337            Ok(CommandSpawnResult::ImmediateExit(127))
338        }
339    } else {
340        let resolved_path = cmd_context.command_name.clone();
341
342        // Strip the command name off args.
343        execute_external_command(
344            cmd_context,
345            resolved_path.as_str(),
346            process_group_id,
347            &args[1..],
348        )
349    }
350}
351
352#[allow(clippy::too_many_lines)]
353#[allow(unused_variables)]
354pub(crate) fn execute_external_command(
355    context: ExecutionContext<'_>,
356    executable_path: &str,
357    process_group_id: &mut Option<i32>,
358    args: &[CommandArg],
359) -> Result<CommandSpawnResult, error::Error> {
360    // Filter out the args; we only want strings.
361    let mut cmd_args = vec![];
362    for arg in args {
363        if let CommandArg::String(s) = arg {
364            cmd_args.push(s);
365        }
366    }
367
368    // Before we lose ownership of the open files, figure out if stdin will be a terminal.
369    #[allow(unused_variables)]
370    let child_stdin_is_terminal = context
371        .params
372        .open_files
373        .stdin()
374        .is_some_and(|f| f.is_term());
375
376    // Figure out if we should be setting up a new process group.
377    let new_pg = context.should_cmd_lead_own_process_group();
378
379    // Save copy of stderr for errors.
380    let mut stderr = context.stderr();
381
382    // Compose the std::process::Command that encapsulates what we want to launch.
383    #[allow(unused_mut)]
384    let mut cmd = compose_std_command(
385        context.shell,
386        executable_path,
387        context.command_name.as_str(),
388        cmd_args.as_slice(),
389        context.params.open_files,
390        false, /* empty environment? */
391    )?;
392
393    // Set up process group state.
394    if new_pg {
395        // We need to set up a new process group.
396        #[cfg(unix)]
397        cmd.process_group(0);
398    } else if let Some(pgid) = process_group_id {
399        // We need to join an established process group.
400        #[cfg(unix)]
401        cmd.process_group(*pgid);
402    }
403
404    // Register some code to run in the forked child process before it execs
405    // the target command.
406    #[cfg(unix)]
407    if new_pg && child_stdin_is_terminal {
408        unsafe {
409            cmd.pre_exec(setup_process_before_exec);
410        }
411    }
412
413    // When tracing is enabled, report.
414    tracing::debug!(
415        target: trace_categories::COMMANDS,
416        "Spawning: cmd='{} {}'",
417        cmd.get_program().to_string_lossy().to_string(),
418        cmd.get_args()
419            .map(|a| a.to_string_lossy().to_string())
420            .join(" ")
421    );
422
423    match sys::process::spawn(cmd) {
424        Ok(child) => {
425            // Retrieve the pid.
426            #[allow(clippy::cast_possible_wrap)]
427            let pid = child.id().map(|id| id as i32);
428            if let Some(pid) = &pid {
429                if new_pg {
430                    *process_group_id = Some(*pid);
431                }
432            } else {
433                tracing::warn!("could not retrieve pid for child process");
434            }
435
436            Ok(CommandSpawnResult::SpawnedProcess(
437                processes::ChildProcess::new(pid, child),
438            ))
439        }
440        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
441            if context.shell.options.interactive {
442                sys::terminal::move_self_to_foreground()?;
443            }
444
445            if !context.shell.working_dir.exists() {
446                // We may have failed because the working directory doesn't exist.
447                writeln!(
448                    stderr,
449                    "{}: working directory does not exist: {}",
450                    context.shell.shell_name.as_ref().unwrap_or(&String::new()),
451                    context.shell.working_dir.display()
452                )?;
453            } else if context.shell.options.sh_mode {
454                writeln!(
455                    stderr,
456                    "{}: {}: {}: not found",
457                    context.shell.shell_name.as_ref().unwrap_or(&String::new()),
458                    context.shell.get_current_input_line_number(),
459                    context.command_name
460                )?;
461            } else {
462                writeln!(stderr, "{}: not found", context.command_name)?;
463            }
464            Ok(CommandSpawnResult::ImmediateExit(127))
465        }
466        Err(e) => {
467            if context.shell.options.interactive {
468                sys::terminal::move_self_to_foreground()?;
469            }
470
471            tracing::error!("error: {}", e);
472            Ok(CommandSpawnResult::ImmediateExit(126))
473        }
474    }
475}
476
477#[cfg(unix)]
478fn setup_process_before_exec() -> Result<(), std::io::Error> {
479    sys::terminal::move_self_to_foreground().map_err(std::io::Error::other)?;
480    Ok(())
481}
482
483async fn execute_builtin_command(
484    builtin: &builtins::Registration,
485    context: ExecutionContext<'_>,
486    args: Vec<CommandArg>,
487) -> Result<CommandSpawnResult, error::Error> {
488    let exit_code = match (builtin.execute_func)(context, args).await {
489        Ok(builtin_result) => match builtin_result.exit_code {
490            builtins::ExitCode::Success => 0,
491            builtins::ExitCode::InvalidUsage => 2,
492            builtins::ExitCode::Unimplemented => 99,
493            builtins::ExitCode::Custom(code) => code,
494            builtins::ExitCode::ExitShell(code) => return Ok(CommandSpawnResult::ExitShell(code)),
495            builtins::ExitCode::ReturnFromFunctionOrScript(code) => {
496                return Ok(CommandSpawnResult::ReturnFromFunctionOrScript(code))
497            }
498            builtins::ExitCode::BreakLoop(count) => {
499                return Ok(CommandSpawnResult::BreakLoop(count))
500            }
501            builtins::ExitCode::ContinueLoop(count) => {
502                return Ok(CommandSpawnResult::ContinueLoop(count))
503            }
504        },
505        Err(e) => {
506            tracing::error!("error: {}", e);
507            1
508        }
509    };
510
511    Ok(CommandSpawnResult::ImmediateExit(exit_code))
512}
513
514pub(crate) async fn invoke_shell_function(
515    function_definition: Arc<ast::FunctionDefinition>,
516    mut context: ExecutionContext<'_>,
517    args: &[CommandArg],
518) -> Result<CommandSpawnResult, error::Error> {
519    let ast::FunctionBody(body, redirects) = &function_definition.body;
520
521    // Apply any redirects specified at function definition-time.
522    if let Some(redirects) = redirects {
523        for redirect in &redirects.0 {
524            interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
525        }
526    }
527
528    // Temporarily replace positional parameters.
529    let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
530    context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
531
532    // Pass through open files.
533    let params = context.params.clone();
534
535    // Note that we're going deeper. Once we do this, we need to make sure we don't bail early
536    // before "exiting" the function.
537    context
538        .shell
539        .enter_function(context.command_name.as_str(), &function_definition)?;
540
541    // Invoke the function.
542    let result = body.execute(context.shell, &params).await;
543
544    // Clean up parameters so any owned files are closed.
545    drop(params);
546
547    // We've come back out, reflect it.
548    context.shell.leave_function()?;
549
550    // Restore positional parameters.
551    context.shell.positional_parameters = prior_positional_params;
552
553    // Get the actual execution result from the body of the function.
554    let result = result?;
555
556    // Report back the exit code, and honor any requests to exit the whole shell.
557    Ok(if result.exit_shell {
558        CommandSpawnResult::ExitShell(result.exit_code)
559    } else {
560        CommandSpawnResult::ImmediateExit(result.exit_code)
561    })
562}
563
564pub(crate) async fn invoke_command_in_subshell_and_get_output(
565    shell: &mut Shell,
566    params: &ExecutionParameters,
567    s: String,
568) -> Result<String, error::Error> {
569    // Instantiate a subshell to run the command in.
570    let mut subshell = shell.clone();
571
572    // Get our own set of parameters we can customize and use.
573    let mut params = params.clone();
574    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
575
576    // Set up pipe so we can read the output.
577    let (reader, writer) = sys::pipes::pipe()?;
578    params
579        .open_files
580        .files
581        .insert(1, openfiles::OpenFile::PipeWriter(writer));
582
583    // Run the command.
584    let result = subshell.run_string(s, &params).await?;
585
586    // Make sure the subshell and params are closed; among other things, this
587    // ensures they're not holding onto the write end of the pipe.
588    drop(subshell);
589    drop(params);
590
591    // Store the status.
592    shell.last_exit_status = result.exit_code;
593
594    // Extract output.
595    let output_str = std::io::read_to_string(reader)?;
596
597    Ok(output_str)
598}