brush_core/
commands.rs

1//! Command execution
2
3use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
4
5use brush_parser::ast;
6use itertools::Itertools;
7use sys::commands::{CommandExt, CommandFdInjectionExt, CommandFgControlExt};
8
9use crate::{
10    ErrorKind, ExecutionControlFlow, ExecutionParameters, ExecutionResult, Shell, ShellFd,
11    builtins, env, error, escape,
12    interp::{self, Execute, ProcessGroupPolicy},
13    openfiles::{self, OpenFile, OpenFiles},
14    pathsearch, processes,
15    results::ExecutionSpawnResult,
16    sys, trace_categories, traps, variables,
17};
18
19/// Encapsulates the result of waiting for a command to complete.
20pub enum CommandWaitResult {
21    /// The command completed.
22    CommandCompleted(ExecutionResult),
23    /// The command was stopped before it completed.
24    CommandStopped(ExecutionResult, processes::ChildProcess),
25}
26
27/// Represents the context for executing a command.
28pub struct ExecutionContext<'a> {
29    /// The shell in which the command is being executed.
30    pub shell: &'a mut Shell,
31    /// The name of the command being executed.    
32    pub command_name: String,
33    /// The parameters for the execution.
34    pub params: ExecutionParameters,
35}
36
37impl ExecutionContext<'_> {
38    /// Returns the standard input file; usable with `write!` et al.
39    pub fn stdin(&self) -> impl std::io::Read + 'static {
40        self.params.stdin(self.shell)
41    }
42
43    /// Returns the standard output file; usable with `write!` et al.
44    pub fn stdout(&self) -> impl std::io::Write + 'static {
45        self.params.stdout(self.shell)
46    }
47
48    /// Returns the standard error file; usable with `write!` et al.
49    pub fn stderr(&self) -> impl std::io::Write + 'static {
50        self.params.stderr(self.shell)
51    }
52
53    /// Returns the file descriptor with the given number. Returns `None`
54    /// if the file descriptor is not open.
55    ///
56    /// # Arguments
57    ///
58    /// * `fd` - The file descriptor number to retrieve.
59    pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
60        self.params.try_fd(self.shell, fd)
61    }
62
63    /// Iterates over all open file descriptors.
64    pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
65        self.params.iter_fds(self.shell)
66    }
67
68    pub(crate) const fn should_cmd_lead_own_process_group(&self) -> bool {
69        self.shell.options.interactive
70            && matches!(
71                self.params.process_group_policy,
72                ProcessGroupPolicy::NewProcessGroup
73            )
74    }
75}
76
77/// An argument to a command.
78#[derive(Clone, Debug)]
79pub enum CommandArg {
80    /// A simple string argument.
81    String(String),
82    /// An assignment/declaration; typically treated as a string, but will
83    /// be specially handled by a limited set of built-in commands.
84    Assignment(ast::Assignment),
85}
86
87impl Display for CommandArg {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Self::String(s) => f.write_str(s),
91            Self::Assignment(a) => write!(f, "{a}"),
92        }
93    }
94}
95
96impl From<String> for CommandArg {
97    fn from(s: String) -> Self {
98        Self::String(s)
99    }
100}
101
102impl From<&String> for CommandArg {
103    fn from(value: &String) -> Self {
104        Self::String(value.clone())
105    }
106}
107
108impl CommandArg {
109    pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
110        match self {
111            Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
112            Self::Assignment(a) => {
113                let mut s = a.name.to_string();
114                let op = if a.append { "+=" } else { "=" };
115                s.push_str(op);
116                s.push_str(&escape::quote_if_needed(
117                    a.value.to_string().as_str(),
118                    escape::QuoteMode::SingleQuote,
119                ));
120                s.into()
121            }
122        }
123    }
124}
125
126/// Composes a `std::process::Command` to execute the given command. Appropriately
127/// configures the command name and arguments, redirections, injected file
128/// descriptors, environment variables, etc.
129///
130/// # Arguments
131///
132/// * `context` - The execution context in which the command is being composed.
133/// * `command_name` - The name of the command to execute.
134/// * `argv0` - The value to use for `argv[0]` (may be different from the command).
135/// * `args` - The arguments to pass to the command.
136/// * `empty_env` - If true, the command will be executed with an empty
137///   environment; if false, the command will inherit environment variables
138///   marked as exported in the provided `Shell`.
139#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
140pub fn compose_std_command<S: AsRef<OsStr>>(
141    context: &ExecutionContext<'_>,
142    command_name: &str,
143    argv0: &str,
144    args: &[S],
145    empty_env: bool,
146) -> Result<std::process::Command, error::Error> {
147    let mut cmd = std::process::Command::new(command_name);
148
149    // Override argv[0].
150    // NOTE: Not supported on all platforms.
151    cmd.arg0(argv0);
152
153    // Pass through args.
154    cmd.args(args);
155
156    // Use the shell's current working dir.
157    cmd.current_dir(context.shell.working_dir());
158
159    // Start with a clear environment.
160    cmd.env_clear();
161
162    // Add in exported variables.
163    if !empty_env {
164        for (k, v) in context.shell.env.iter_exported() {
165            // NOTE: To match bash behavior, we only include exported variables
166            // that are set (i.e., have a value). This means a variable that
167            // shows up in `declare -p` but has no *set* value will be omitted.
168            if v.value().is_set() {
169                cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
170            }
171        }
172    }
173
174    // Add in exported functions.
175    if !empty_env {
176        for (func_name, registration) in context.shell.funcs().iter() {
177            if registration.is_exported() {
178                let var_name = std::format!("BASH_FUNC_{func_name}%%");
179                let value = std::format!("() {}", registration.definition.body);
180                cmd.env(var_name, value);
181            }
182        }
183    }
184
185    // Redirect stdin, if applicable.
186    match context.try_fd(OpenFiles::STDIN_FD) {
187        Some(OpenFile::Stdin(_)) | None => (),
188        Some(stdin_file) => {
189            let as_stdio: Stdio = stdin_file.into();
190            cmd.stdin(as_stdio);
191        }
192    }
193
194    // Redirect stdout, if applicable.
195    match context.try_fd(OpenFiles::STDOUT_FD) {
196        Some(OpenFile::Stdout(_)) | None => (),
197        Some(stdout_file) => {
198            let as_stdio: Stdio = stdout_file.into();
199            cmd.stdout(as_stdio);
200        }
201    }
202
203    // Redirect stderr, if applicable.
204    match context.try_fd(OpenFiles::STDERR_FD) {
205        Some(OpenFile::Stderr(_)) | None => {}
206        Some(stderr_file) => {
207            let as_stdio: Stdio = stderr_file.into();
208            cmd.stderr(as_stdio);
209        }
210    }
211
212    // Inject any other fds.
213    let other_files = context.iter_fds().filter(|(fd, _)| {
214        *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
215    });
216    cmd.inject_fds(other_files)?;
217
218    Ok(cmd)
219}
220
221pub(crate) async fn on_preexecute(
222    context: &mut ExecutionContext<'_>,
223    args: &[CommandArg],
224) -> Result<(), error::Error> {
225    // See if we have a DEBUG trap handler registered; call it if we do.
226    invoke_debug_trap_handler_if_registered(context, args).await?;
227
228    Ok(())
229}
230
231async fn invoke_debug_trap_handler_if_registered(
232    context: &mut ExecutionContext<'_>,
233    args: &[CommandArg],
234) -> Result<(), error::Error> {
235    if context.shell.traps.handler_depth == 0 {
236        let debug_trap_handler = context
237            .shell
238            .traps
239            .handlers
240            .get(&traps::TrapSignal::Debug)
241            .cloned();
242        if let Some(debug_trap_handler) = debug_trap_handler {
243            // TODO: Confirm whether trap handlers should be executed in the same process group.
244            let mut handler_params = context.params.clone();
245            handler_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
246
247            let full_cmd = args.iter().map(|arg| arg.to_string()).join(" ");
248
249            // TODO: This shouldn't *just* be set in a trap situation.
250            context.shell.env.update_or_add(
251                "BASH_COMMAND",
252                variables::ShellValueLiteral::Scalar(full_cmd),
253                |_| Ok(()),
254                env::EnvironmentLookup::Anywhere,
255                env::EnvironmentScope::Global,
256            )?;
257
258            context.shell.traps.handler_depth += 1;
259
260            // TODO: Discard result?
261            let _ = context
262                .shell
263                .run_string(debug_trap_handler, &handler_params)
264                .await;
265
266            context.shell.traps.handler_depth -= 1;
267        }
268    }
269
270    Ok(())
271}
272
273/// Executes a simple command.
274///
275/// The command may be a builtin, a shell function, or an externally
276/// executed command. This function's implementation is responsible for
277/// dispatching it appropriately according to the context provided.
278///
279/// # Arguments
280///
281/// * `cmd_context` - The context in which the command is being executed.
282/// * `process_group_id` - The process group ID to use for externally
283///   executed commands. This may be modified if a new process group is
284///   created.
285/// * `args` - The arguments to the command.
286/// * `use_functions` - If true, the command name will be checked against
287///   shell functions; if not, shell functions will not be consulted.
288/// * `path_dirs` - If provided, these directories will be searched for
289///   external commands; if not provided, the default search logic will
290///   be used.
291pub async fn execute(
292    cmd_context: ExecutionContext<'_>,
293    process_group_id: &mut Option<i32>,
294    args: Vec<CommandArg>,
295    use_functions: bool,
296    path_dirs: Option<Vec<String>>,
297) -> Result<ExecutionSpawnResult, error::Error> {
298    // First see if it's the name of a builtin.
299    let builtin = cmd_context
300        .shell
301        .builtins()
302        .get(&cmd_context.command_name)
303        .cloned();
304
305    // If we found a special builtin (that's not disabled), then invoke it.
306    if builtin
307        .as_ref()
308        .is_some_and(|r| !r.disabled && r.special_builtin)
309    {
310        return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
311    }
312
313    // Assuming we weren't requested not to do so, check if it's the name of
314    // a shell function.
315    if use_functions {
316        if let Some(func_reg) = cmd_context
317            .shell
318            .funcs()
319            .get(cmd_context.command_name.as_str())
320        {
321            // Strip the function name off args.
322            return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
323                .await;
324        }
325    }
326
327    // If we found a (non-special) builtin and it's not disabled, then invoke it.
328    if let Some(builtin) = builtin {
329        if !builtin.disabled {
330            return execute_builtin_command(&builtin, cmd_context, args).await;
331        }
332    }
333
334    // We still haven't found a command to invoke. We'll need to look for an external command.
335    if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
336        // All else failed; if we were given path directories to search, try to look through them
337        // for a matching executable. Otherwise, use our default search logic.
338        let path = if let Some(path_dirs) = path_dirs {
339            pathsearch::search_for_executable(
340                path_dirs.iter().map(String::as_str),
341                cmd_context.command_name.as_str(),
342            )
343            .next()
344        } else {
345            cmd_context
346                .shell
347                .find_first_executable_in_path_using_cache(&cmd_context.command_name)
348        };
349
350        if let Some(path) = path {
351            let resolved_path = path.to_string_lossy();
352            execute_external_command(
353                cmd_context,
354                resolved_path.as_ref(),
355                process_group_id,
356                &args[1..],
357            )
358        } else {
359            Err(ErrorKind::CommandNotFound(cmd_context.command_name).into())
360        }
361    } else {
362        let resolved_path = cmd_context.command_name.clone();
363
364        // Strip the command name off args.
365        execute_external_command(
366            cmd_context,
367            resolved_path.as_str(),
368            process_group_id,
369            &args[1..],
370        )
371    }
372}
373
374pub(crate) fn execute_external_command(
375    context: ExecutionContext<'_>,
376    executable_path: &str,
377    process_group_id: &mut Option<i32>,
378    args: &[CommandArg],
379) -> Result<ExecutionSpawnResult, error::Error> {
380    // Filter out the args; we only want strings.
381    let mut cmd_args = vec![];
382    for arg in args {
383        if let CommandArg::String(s) = arg {
384            cmd_args.push(s);
385        }
386    }
387
388    // Before we lose ownership of the open files, figure out if stdin will be a terminal.
389    let child_stdin_is_terminal = context
390        .try_fd(openfiles::OpenFiles::STDIN_FD)
391        .is_some_and(|f| f.is_term());
392
393    // Figure out if we should be setting up a new process group.
394    let new_pg = context.should_cmd_lead_own_process_group();
395
396    // Compose the std::process::Command that encapsulates what we want to launch.
397    #[allow(unused_mut, reason = "only mutated on unix platforms")]
398    let mut cmd = compose_std_command(
399        &context,
400        executable_path,
401        context.command_name.as_str(),
402        cmd_args.as_slice(),
403        false, /* empty environment? */
404    )?;
405
406    // Set up process group state.
407    if new_pg {
408        // We need to set up a new process group.
409        cmd.process_group(0);
410    } else {
411        // We need to join an established process group.
412        if let Some(pgid) = process_group_id {
413            cmd.process_group(*pgid);
414        }
415    }
416
417    // If we're to lead our own process group and stdin is a terminal,
418    // then we need to arrange for the new process to move itself
419    // to the foreground.
420    if new_pg && child_stdin_is_terminal {
421        cmd.take_foreground();
422    }
423
424    // When tracing is enabled, report.
425    tracing::debug!(
426        target: trace_categories::COMMANDS,
427        "Spawning: cmd='{} {}'",
428        cmd.get_program().to_string_lossy().to_string(),
429        cmd.get_args()
430            .map(|a| a.to_string_lossy().to_string())
431            .join(" ")
432    );
433
434    match sys::process::spawn(cmd) {
435        Ok(child) => {
436            // Retrieve the pid.
437            #[expect(clippy::cast_possible_wrap)]
438            let pid = child.id().map(|id| id as i32);
439            if let Some(pid) = &pid {
440                if new_pg {
441                    *process_group_id = Some(*pid);
442                }
443            } else {
444                tracing::warn!("could not retrieve pid for child process");
445            }
446
447            Ok(ExecutionSpawnResult::StartedProcess(
448                processes::ChildProcess::new(pid, child),
449            ))
450        }
451        Err(spawn_err) => {
452            if context.shell.options.interactive {
453                sys::terminal::move_self_to_foreground()?;
454            }
455
456            if spawn_err.kind() == std::io::ErrorKind::NotFound {
457                if !context.shell.working_dir().exists() {
458                    Err(
459                        error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned())
460                            .into(),
461                    )
462                } else {
463                    Err(error::ErrorKind::CommandNotFound(context.command_name).into())
464                }
465            } else {
466                Err(
467                    error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err)
468                        .into(),
469                )
470            }
471        }
472    }
473}
474
475async fn execute_builtin_command(
476    builtin: &builtins::Registration,
477    context: ExecutionContext<'_>,
478    args: Vec<CommandArg>,
479) -> Result<ExecutionSpawnResult, error::Error> {
480    let result = (builtin.execute_func)(context, args).await?;
481    Ok(result.into())
482}
483
484pub(crate) async fn invoke_shell_function(
485    function_definition: Arc<ast::FunctionDefinition>,
486    mut context: ExecutionContext<'_>,
487    args: &[CommandArg],
488) -> Result<ExecutionSpawnResult, error::Error> {
489    let ast::FunctionBody(body, redirects) = &function_definition.body;
490
491    // Apply any redirects specified at function definition-time.
492    if let Some(redirects) = redirects {
493        for redirect in &redirects.0 {
494            interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
495        }
496    }
497
498    // Temporarily replace positional parameters.
499    let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
500    context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
501
502    // Pass through open files.
503    let params = context.params.clone();
504
505    // Note that we're going deeper. Once we do this, we need to make sure we don't bail early
506    // before "exiting" the function.
507    context
508        .shell
509        .enter_function(context.command_name.as_str(), &function_definition)?;
510
511    // Invoke the function.
512    let result = body.execute(context.shell, &params).await;
513
514    // Clean up parameters so any owned files are closed.
515    drop(params);
516
517    // We've come back out, reflect it.
518    context.shell.leave_function()?;
519
520    // Restore positional parameters.
521    context.shell.positional_parameters = prior_positional_params;
522
523    // Get the actual execution result from the body of the function.
524    let mut result = result?;
525
526    // Handle control-flow.
527    match result.next_control_flow {
528        ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
529            return error::unimp("break or continue returned from function invocation");
530        }
531        ExecutionControlFlow::ReturnFromFunctionOrScript => {
532            // It's now been handled.
533            result.next_control_flow = ExecutionControlFlow::Normal;
534        }
535        _ => {}
536    }
537
538    Ok(result.into())
539}
540
541pub(crate) async fn invoke_command_in_subshell_and_get_output(
542    shell: &mut Shell,
543    params: &ExecutionParameters,
544    s: String,
545) -> Result<String, error::Error> {
546    // Instantiate a subshell to run the command in.
547    let subshell = shell.clone();
548
549    // Get our own set of parameters we can customize and use.
550    let mut params = params.clone();
551    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
552
553    // Set up pipe so we can read the output.
554    let (reader, writer) = std::io::pipe()?;
555    params.set_fd(OpenFiles::STDOUT_FD, writer.into());
556
557    // Start the execution of the command, but don't wait for it to
558    // complete. In case the command generates lots of output, we
559    // need to start reading in parallel so the command doesn't block
560    // when the pipe's buffer fills up. We pass ownership of the
561    // subshell and params to run_substitution_command; we must
562    // ensure that they're both dropped by the time this call
563    // returns (so they're not holding onto the write end of the pipe).
564    let cmd_join_handle = tokio::task::spawn_blocking(move || {
565        let rt = tokio::runtime::Handle::current();
566        rt.block_on(run_substitution_command(subshell, params, s))
567    });
568
569    // Extract output.
570    let output_str = std::io::read_to_string(reader)?;
571
572    // Now observe the command's completion.
573    let run_result = cmd_join_handle.await?;
574    let cmd_result = run_result?;
575
576    // Store the status.
577    *shell.last_exit_status_mut() = cmd_result.exit_code.into();
578
579    Ok(output_str)
580}
581
582async fn run_substitution_command(
583    mut shell: Shell,
584    mut params: ExecutionParameters,
585    command: String,
586) -> Result<ExecutionResult, error::Error> {
587    // Parse the string into a whole shell program.
588    let parse_result = shell.parse_string(command);
589
590    // Check for a command that is only an input redirection ("< file").
591    // If detected, emulate `cat file` to stdout and return immediately.
592    // If we failed to parse, then we'll fall below and handle it there.
593    if let Ok(program) = &parse_result {
594        if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
595            interp::setup_redirect(&mut shell, &mut params, redir).await?;
596            std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
597            return Ok(ExecutionResult::new(0));
598        }
599    }
600
601    let source_info = brush_parser::SourceInfo {
602        source: String::from("main"),
603    };
604
605    // Handle the parse result using default shell behavior.
606    shell
607        .run_parsed_result(parse_result, &source_info, &params)
608        .await
609}
610
611// Detects a subshell command that consists solely of a single input redirection
612// (e.g., "< file"), returning the IoRedirect when present.
613fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
614    // We're looking for exactly one complete command...
615    let [complete] = program.complete_commands.as_slice() else {
616        return None;
617    };
618
619    // ...a single list item...
620    let ast::CompoundList(items) = complete;
621    let [item] = items.as_slice() else {
622        return None;
623    };
624
625    // ...with a single pipeline (no && or || chaining)...
626    let and_or = &item.0;
627    if !and_or.additional.is_empty() {
628        return None;
629    }
630
631    // ...not negated...
632    let pipeline = &and_or.first;
633    if pipeline.bang {
634        return None;
635    }
636
637    // ...with a single command in the pipeline...
638    let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
639        return None;
640    };
641
642    // ...with no program word/name and no suffix...
643    if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
644        return None;
645    }
646
647    // ...and exactly one prefix containing an I/O redirect...
648    let prefix = simple_cmd.prefix.as_ref()?;
649    let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
650        return None;
651    };
652
653    // ...that is a file input redirection to a filename, targeting stdin.
654    match redir {
655        ast::IoRedirect::File(
656            fd,
657            ast::IoFileRedirectKind::Read,
658            ast::IoFileRedirectTarget::Filename(..),
659        ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
660        _ => None,
661    }
662}