Skip to main content

shrub_core/
commands.rs

1//! Command execution
2
3use std::borrow::Cow;
4use std::ffi::OsStr;
5use std::fmt::{Display, Write as _};
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8
9use itertools::Itertools;
10use shrub_parser::ast;
11use sys::commands::{
12    CommandCgroupExt, CommandExt, CommandFdInjectionExt, CommandFgControlExt,
13    CommandOpenFileLimitExt, CommandPidsLimitsExt,
14};
15
16use crate::extensions::{self, ShellExtensions};
17use crate::interp::{self, Execute, ProcessGroupPolicy};
18use crate::openfiles::{self, OpenFile, OpenFiles};
19use crate::results::ExecutionSpawnResult;
20use crate::{
21    ErrorKind, ExecutionControlFlow, ExecutionExitCode, ExecutionParameters, ExecutionResult,
22    Shell, ShellFd, builtins, commands, env, error, escape, functions, options, pathsearch,
23    processes, sys, trace_categories, traps, variables,
24};
25
26/// Encapsulates the result of waiting for a command to complete.
27pub enum CommandWaitResult {
28    /// The command completed.
29    CommandCompleted(ExecutionResult),
30    /// The command was stopped before it completed.
31    CommandStopped(ExecutionResult, processes::ChildProcess),
32}
33
34/// Represents the context for executing a command.
35pub struct ExecutionContext<'a, SE: ShellExtensions = extensions::DefaultShellExtensions> {
36    /// The shell in which the command is being executed.
37    pub shell: &'a mut Shell<SE>,
38    /// The name of the command being executed.
39    pub command_name: String,
40    /// The parameters for the execution.
41    pub params: ExecutionParameters,
42}
43
44impl<SE: ShellExtensions> ExecutionContext<'_, SE> {
45    /// Returns the standard input file; usable with `write!` et al.
46    pub fn stdin(&self) -> impl std::io::Read + 'static {
47        self.params.stdin(self.shell)
48    }
49
50    /// Returns the standard output file; usable with `write!` et al.
51    pub fn stdout(&self) -> impl std::io::Write + 'static {
52        self.params.stdout(self.shell)
53    }
54
55    /// Returns the standard error file; usable with `write!` et al.
56    pub fn stderr(&self) -> impl std::io::Write + 'static {
57        self.params.stderr(self.shell)
58    }
59
60    /// Returns the file descriptor with the given number. Returns `None`
61    /// if the file descriptor is not open.
62    ///
63    /// # Arguments
64    ///
65    /// * `fd` - The file descriptor number to retrieve.
66    pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
67        self.params.try_fd(self.shell, fd)
68    }
69
70    /// Iterates over all open file descriptors.
71    pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
72        self.params.iter_fds(self.shell)
73    }
74}
75
76/// An argument to a command.
77#[derive(Clone, Debug)]
78pub enum CommandArg {
79    /// A simple string argument.
80    String(String),
81    /// An assignment/declaration; typically treated as a string, but will
82    /// be specially handled by a limited set of built-in commands.
83    Assignment(ast::Assignment),
84}
85
86impl Display for CommandArg {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::String(s) => f.write_str(s),
90            Self::Assignment(a) => write!(f, "{a}"),
91        }
92    }
93}
94
95impl From<String> for CommandArg {
96    fn from(s: String) -> Self {
97        Self::String(s)
98    }
99}
100
101impl From<&String> for CommandArg {
102    fn from(value: &String) -> Self {
103        Self::String(value.clone())
104    }
105}
106
107impl CommandArg {
108    const fn to_string_len_hint(&self) -> usize {
109        match self {
110            Self::String(s) => s.len(),
111            Self::Assignment(_) => 0,
112        }
113    }
114
115    pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
116        match self {
117            Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
118            Self::Assignment(a) => {
119                let mut s = a.name.to_string();
120                let op = if a.append { "+=" } else { "=" };
121                s.push_str(op);
122                s.push_str(&escape::quote_if_needed(
123                    a.value.to_string().as_str(),
124                    escape::QuoteMode::SingleQuote,
125                ));
126                s.into()
127            }
128        }
129    }
130}
131
132/// Encapsulates a possibly-owned reference to a `Shell` for command execution.
133pub enum ShellForCommand<'a, SE: extensions::ShellExtensions> {
134    /// The command is run in the same shell as its parent; the provided
135    /// mutable reference allows modifying the parent shell.
136    ParentShell(&'a mut Shell<SE>),
137    /// The command is run in its own owned shell (which is also provided).
138    OwnedShell {
139        /// The owned shell.
140        target: Box<Shell<SE>>,
141        /// The parent shell.
142        parent: &'a mut Shell<SE>,
143    },
144}
145
146impl<SE: extensions::ShellExtensions> std::ops::Deref for ShellForCommand<'_, SE> {
147    type Target = Shell<SE>;
148
149    fn deref(&self) -> &Self::Target {
150        match self {
151            ShellForCommand::ParentShell(shell) => shell,
152            ShellForCommand::OwnedShell { target, .. } => target,
153        }
154    }
155}
156
157impl<SE: extensions::ShellExtensions> std::ops::DerefMut for ShellForCommand<'_, SE> {
158    fn deref_mut(&mut self) -> &mut Self::Target {
159        match self {
160            ShellForCommand::ParentShell(shell) => shell,
161            ShellForCommand::OwnedShell { target, .. } => target,
162        }
163    }
164}
165
166/// Composes a `std::process::Command` to execute the given command.
167/// Appropriately configures the command name and arguments, redirections,
168/// injected file descriptors, environment variables, etc.
169///
170/// # Arguments
171///
172/// * `context` - The execution context in which the command is being composed.
173/// * `command_name` - The name of the command to execute.
174/// * `argv0` - The value to use for `argv[0]` (may be different from the
175///   command).
176/// * `args` - The arguments to pass to the command.
177/// * `empty_env` - If true, the command will be executed with an empty
178///   environment; if false, the command will inherit environment variables
179///   marked as exported in the provided `Shell`.
180///
181/// # Errors
182///
183/// Returns [`error::ErrorKind::IoError`] if a redirected file descriptor cannot
184/// be duplicated into a `Stdio` for the child (for example under descriptor
185/// exhaustion), or an error if injecting the shell's other open file
186/// descriptors into the child fails, or, where the shell's
187/// [`SpawnOptions`](crate::options::SpawnOptions) name a `cgroup_procs` file,
188/// [`error::ErrorKind::NotSupportedOnThisPlatform`] on a platform that cannot
189/// join a cgroup, and likewise where they set an `open_file_limit` or
190/// `pids_limits` on a platform that cannot apply them.
191#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
192pub fn compose_std_command<S: AsRef<OsStr>, SE: extensions::ShellExtensions>(
193    context: &ExecutionContext<'_, SE>,
194    command_name: &str,
195    argv0: &str,
196    args: &[S],
197    empty_env: bool,
198) -> Result<std::process::Command, error::Error> {
199    let mut cmd = std::process::Command::new(command_name);
200
201    // Override argv[0].
202    // NOTE: Not supported on all platforms.
203    cmd.arg0(argv0);
204
205    // Pass through args.
206    cmd.args(args);
207
208    // Use the shell's current working dir.
209    cmd.current_dir(context.shell.working_dir());
210
211    // Start with a clear environment.
212    cmd.env_clear();
213
214    // Add in exported variables.
215    if !empty_env {
216        for (k, v) in context.shell.env().iter_exported() {
217            // NOTE: To match bash behavior, we only include exported variables
218            // that are set (i.e., have a value). This means a variable that
219            // shows up in `declare -p` but has no *set* value will be omitted.
220            if v.value().is_set() {
221                cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
222            }
223        }
224        // Set _ to the resolved command path for external commands.
225        cmd.env("_", command_name);
226    }
227
228    // Add in exported functions.
229    if !empty_env {
230        for (func_name, registration) in context.shell.funcs().iter() {
231            if registration.is_exported() {
232                let var_name = std::format!("BASH_FUNC_{func_name}%%");
233                let value = std::format!("() {}", registration.definition().body);
234                cmd.env(var_name, value);
235            }
236        }
237    }
238
239    // Redirect stdin, if applicable.
240    match context.try_fd(OpenFiles::STDIN_FD) {
241        Some(OpenFile::Stdin(_)) | None => (),
242        Some(stdin_file) => {
243            let as_stdio: Stdio = stdin_file.try_into()?;
244            cmd.stdin(as_stdio);
245        }
246    }
247
248    // Redirect stdout, if applicable.
249    match context.try_fd(OpenFiles::STDOUT_FD) {
250        Some(OpenFile::Stdout(_)) | None => (),
251        Some(stdout_file) => {
252            let as_stdio: Stdio = stdout_file.try_into()?;
253            cmd.stdout(as_stdio);
254        }
255    }
256
257    // Redirect stderr, if applicable.
258    match context.try_fd(OpenFiles::STDERR_FD) {
259        Some(OpenFile::Stderr(_)) | None => {}
260        Some(stderr_file) => {
261            let as_stdio: Stdio = stderr_file.try_into()?;
262            cmd.stderr(as_stdio);
263        }
264    }
265
266    // Move the child into the embedder's cgroup before it runs anything. It
267    // is set here, where every spawn path and `exec` compose their command,
268    // so no program the shell starts runs outside the cgroup.
269    if let Some(cgroup_procs) = &context.shell.options().spawn.cgroup_procs {
270        cmd.join_cgroup(std::sync::Arc::clone(cgroup_procs))?;
271    }
272
273    // Registered after the cgroup join, so the child counts itself in the
274    // cgroup it moved into, and before the descriptors are injected, which
275    // could place a redirected file on a `pids.current` descriptor's number.
276    // A refusal then comes before every later step, which matters for
277    // `exec`, whose steps run in the shell's own process.
278    let pids_limits = &context.shell.options().spawn.pids_limits;
279    if !pids_limits.is_empty() {
280        cmd.check_pids_limits(std::sync::Arc::clone(pids_limits))?;
281    }
282
283    // Inject any other fds.
284    let other_files = context.iter_fds().filter(|(fd, _)| {
285        *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
286    });
287    cmd.inject_fds(other_files)?;
288
289    // Every program the shell starts gets the open-file limit the embedder
290    // chose. Set last, so a lower limit cannot make dup2 refuse a descriptor
291    // number above it while the descriptors are injected.
292    if let Some(limit) = context.shell.options().spawn.open_file_limit {
293        cmd.set_open_file_limit(limit)?;
294    }
295
296    Ok(cmd)
297}
298
299pub(crate) async fn on_preexecute(
300    cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
301) -> Result<(), error::Error> {
302    // Set BASH_COMMAND before invoking the DEBUG trap (and generally before
303    // executing commands).
304    let full_cmd = format_command_args(&cmd.args);
305    cmd.shell.env_mut().update_or_add(
306        "BASH_COMMAND",
307        variables::ShellValueLiteral::Scalar(full_cmd),
308        |_| Ok(()),
309        env::EnvironmentLookup::Anywhere,
310        env::EnvironmentScope::Global,
311    )?;
312
313    // Fire the DEBUG trap if one is registered.
314    if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
315        let _ = cmd
316            .shell
317            .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
318            .await?;
319    }
320
321    Ok(())
322}
323
324fn format_command_args(args: &[CommandArg]) -> String {
325    let capacity = args
326        .iter()
327        .map(CommandArg::to_string_len_hint)
328        .sum::<usize>()
329        + args.len().saturating_sub(1);
330    let mut result = String::with_capacity(capacity);
331
332    for (i, arg) in args.iter().enumerate() {
333        if i > 0 {
334            result.push(' ');
335        }
336        let _ = write!(result, "{arg}");
337    }
338
339    result
340}
341
342/// Represents a simple command to be executed.
343pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
344    /// The shell to run the command in.
345    shell: ShellForCommand<'a, SE>,
346
347    /// The execution parameters for the command.
348    pub params: ExecutionParameters,
349
350    /// The name of the command to execute.
351    pub command_name: String,
352
353    /// The arguments to the command, including the command itself.
354    pub args: Vec<CommandArg>,
355
356    /// Whether to consider shell functions when looking up the command name.
357    /// If true, shell functions will be checked; if false, they will be
358    /// ignored.
359    pub use_functions: bool,
360
361    /// Optional list of directories to search for external commands. If left
362    /// `None`, the default search logic will be used.
363    pub path_dirs: Option<Vec<PathBuf>>,
364
365    /// The process group ID to use for externally executed commands. This may
366    /// be `None`, in which case the default behavior will be used.
367    pub process_group_id: Option<i32>,
368
369    /// Optional override for the `argv[0]` value presented to an externally
370    /// spawned process. When `None`, `command_name` is used.
371    pub argv0: Option<String>,
372}
373
374impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
375    /// Creates a new `SimpleCommand` instance.
376    ///
377    /// # Arguments
378    ///
379    /// * `shell` - The shell in which to execute the command.
380    /// * `params` - The execution parameters for the command.
381    /// * `command_name` - The name of the command to execute.
382    /// * `args` - The arguments to the command, including the command itself.
383    pub fn new<I>(
384        shell: ShellForCommand<'a, SE>,
385        params: ExecutionParameters,
386        command_name: String,
387        args: I,
388    ) -> Self
389    where
390        I: IntoIterator<Item = CommandArg>,
391    {
392        Self {
393            shell,
394            params,
395            command_name,
396            args: args.into_iter().collect(),
397            use_functions: true,
398            path_dirs: None,
399            process_group_id: None,
400            argv0: None,
401        }
402    }
403
404    /// Executes the simple command.
405    ///
406    /// The command may be a builtin, a shell function, or an externally
407    /// executed command. This function's implementation is responsible for
408    /// dispatching it appropriately according to the context provided.
409    ///
410    /// # Errors
411    ///
412    /// * [`ErrorKind::CommandNotFound`] if the name is not a builtin or
413    ///   function and is not found in the search path.
414    /// * Any error from starting a builtin, function, or external process; for
415    ///   example [`ErrorKind::FailedToExecuteCommand`] or
416    ///   [`ErrorKind::WorkingDirMissing`] when an external command cannot be
417    ///   spawned.
418    ///
419    /// # Panics
420    ///
421    /// Does not panic in practice: the `unwrap` runs only after the builtin
422    /// lookup was checked to be `Some`.
423    pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
424        // First see if it's the name of a builtin.
425        let builtin = self.shell.builtins().get(&self.command_name).cloned();
426
427        // If we're in POSIX mode and found a special builtin (that's not
428        // disabled), then invoke it without considering functions.
429        if self.shell.options().posix_mode
430            && builtin
431                .as_ref()
432                .is_some_and(|r| !r.disabled && r.special_builtin)
433        {
434            #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
435            let builtin = builtin.unwrap();
436            return self.execute_via_builtin(builtin).await;
437        }
438
439        // Assuming we weren't requested not to do so, check if it's the name of
440        // a shell function.
441        if self.use_functions {
442            if let Some(func_registration) =
443                self.shell.funcs().get(self.command_name.as_str()).cloned()
444            {
445                return self.execute_via_function(func_registration).await;
446            }
447        }
448
449        // If we haven't yet resolved the command name and found a builtin
450        // that's not disabled, then invoke it.
451        if let Some(builtin) = builtin {
452            if !builtin.disabled {
453                return self.execute_via_builtin(builtin).await;
454            }
455        }
456
457        // We still haven't found a command to invoke. We'll need to look for an
458        // external command.
459        if !sys::fs::contains_path_separator(&self.command_name) {
460            // All else failed; if we were given path directories to search,
461            // look through them for a match. Otherwise, use our
462            // default search logic.
463            let path = if let Some(path_dirs) = &self.path_dirs {
464                pathsearch::resolve_command(path_dirs, self.command_name.as_str())
465            } else {
466                self.shell
467                    .resolve_command_in_path_using_cache(&self.command_name)
468            };
469
470            if let Some(path) = path {
471                self.execute_via_external(&path)
472            } else {
473                // Bash updates $_ even when the command is not found, so mirror
474                // that here before reporting the error.
475                let last_arg = Self::take_last_arg(&self.args);
476                self.shell.update_last_arg_variable(last_arg);
477
478                Err(ErrorKind::CommandNotFound(self.command_name).into())
479            }
480        } else {
481            let command_name = sys::fs::resolve_executable_path(
482                PathBuf::from(&self.command_name),
483                self.shell.working_dir(),
484            );
485            self.execute_via_external(command_name.as_path())
486        }
487    }
488
489    /// Extracts the owned string representation of the last argument of a
490    /// command, suitable for recording into `$_`.
491    fn take_last_arg(args: &[CommandArg]) -> Option<String> {
492        args.last().map(ToString::to_string)
493    }
494
495    async fn execute_via_builtin(
496        self,
497        builtin: builtins::Registration<SE>,
498    ) -> Result<ExecutionSpawnResult, error::Error> {
499        match self.shell {
500            ShellForCommand::OwnedShell { target, .. } => {
501                Ok(Self::execute_via_builtin_in_owned_shell(
502                    *target,
503                    self.params,
504                    builtin,
505                    self.command_name,
506                    self.args,
507                )
508                .await)
509            }
510            ShellForCommand::ParentShell(..) => {
511                self.execute_via_builtin_in_parent_shell(builtin).await
512            }
513        }
514    }
515
516    // Not an `async fn`: its future would hold `shell`, and that would grow the
517    // future of every simple command, not just this rare case.
518    fn execute_via_builtin_in_owned_shell(
519        mut shell: Shell<SE>,
520        params: ExecutionParameters,
521        builtin: builtins::Registration<SE>,
522        command_name: String,
523        args: Vec<CommandArg>,
524    ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
525        let last_arg = Self::take_last_arg(&args);
526        // Builtins may block (e.g., reading input), so keep them from holding
527        // up the rest of the pipeline.
528        let task = sys::tasks::spawn_blocking(async move {
529            let cmd_context = ExecutionContext {
530                shell: &mut shell,
531                command_name,
532                params,
533            };
534
535            let result = execute_builtin_command(&builtin, cmd_context, args).await;
536
537            // Update $_ after command execution.
538            shell.update_last_arg_variable(last_arg);
539
540            result
541        });
542
543        async move { ExecutionSpawnResult::StartedTask(task.await) }
544    }
545
546    async fn execute_via_builtin_in_parent_shell(
547        self,
548        builtin: builtins::Registration<SE>,
549    ) -> Result<ExecutionSpawnResult, error::Error> {
550        let mut shell = self.shell;
551        let last_arg = Self::take_last_arg(&self.args);
552
553        let cmd_context = ExecutionContext {
554            shell: &mut shell,
555            command_name: self.command_name,
556            params: self.params,
557        };
558
559        let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
560
561        // Update $_ after command execution.
562        shell.update_last_arg_variable(last_arg);
563
564        let result = result?;
565
566        Ok(result.into())
567    }
568
569    async fn execute_via_function(
570        self,
571        func_registration: functions::Registration,
572    ) -> Result<ExecutionSpawnResult, error::Error> {
573        match self.shell {
574            ShellForCommand::OwnedShell { target, .. } => {
575                Ok(Self::execute_via_function_in_owned_shell(
576                    *target,
577                    self.params,
578                    func_registration,
579                    self.command_name,
580                    self.args,
581                )
582                .await)
583            }
584            ShellForCommand::ParentShell(..) => {
585                self.execute_via_function_in_parent_shell(func_registration)
586                    .await
587            }
588        }
589    }
590
591    // As a non-last pipeline stage, a function must run as a background task
592    // (mirroring `execute_via_builtin_in_owned_shell` above) rather than
593    // being awaited inline in `spawn_pipeline_processes`'s own per-stage
594    // loop: awaiting it inline there blocks that loop until the function's
595    // body fully returns, so the *next* pipeline stage (the one that would
596    // actually drain this function's stdout pipe) is never even spawned
597    // until this one is done. A function that writes more to its stdout
598    // pipe than the OS pipe buffer holds (~64KiB on Linux) before returning
599    // then blocks on that `write()` forever, since nothing is reading the
600    // other end yet -- a plain deadlock, not a slow completion. Confirmed
601    // empirically with a two-line repro having nothing to do with any
602    // external command or builtin: `f() { for i in $(seq 1 5000); do echo
603    // line $i; done; }; f | cat` hangs indefinitely. Real bash always runs
604    // every pipeline stage as its own concurrently-running process, so this
605    // case never arises there.
606    fn execute_via_function_in_owned_shell(
607        mut shell: Shell<SE>,
608        params: ExecutionParameters,
609        func_registration: functions::Registration,
610        command_name: String,
611        args: Vec<CommandArg>,
612    ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
613        let last_arg = Self::take_last_arg(&args);
614        let task = sys::tasks::spawn_blocking(async move {
615            let cmd_context = ExecutionContext {
616                shell: &mut shell,
617                command_name,
618                params,
619            };
620
621            let spawn_result =
622                invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
623
624            // Update $_ after command execution.
625            shell.update_last_arg_variable(last_arg);
626
627            match spawn_result? {
628                ExecutionSpawnResult::Completed(result) => Ok(result),
629                ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
630                    // invoke_shell_function() only ever returns Completed
631                    // today; this guards against a silent behavior change
632                    // rather than dropping already-started work.
633                    error::unimp("invoke_shell_function returned an unexpected spawn result")
634                }
635            }
636        });
637
638        async move { ExecutionSpawnResult::StartedTask(task.await) }
639    }
640
641    async fn execute_via_function_in_parent_shell(
642        self,
643        func_registration: functions::Registration,
644    ) -> Result<ExecutionSpawnResult, error::Error> {
645        let mut shell = self.shell;
646        let last_arg = Self::take_last_arg(&self.args);
647
648        let cmd_context = ExecutionContext {
649            shell: &mut shell,
650            command_name: self.command_name,
651            params: self.params,
652        };
653
654        // Strip the function name off args.
655        let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
656
657        // $_ is reset *after* the function body runs, to the last argument of
658        // the invocation (or the function name itself if zero args). Any
659        // mutations made inside the body are overwritten — this matches bash,
660        // where the caller observes only the invocation's last argument.
661        shell.update_last_arg_variable(last_arg);
662
663        result
664    }
665
666    fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
667        let mut shell = self.shell;
668        let last_arg = Self::take_last_arg(&self.args);
669
670        let cmd_context = ExecutionContext {
671            shell: &mut shell,
672            command_name: self.command_name,
673            params: self.params,
674        };
675
676        let resolved_path = path.to_string_lossy();
677        let result = execute_external_command(
678            cmd_context,
679            resolved_path.as_ref(),
680            self.process_group_id,
681            self.argv0.as_deref(),
682            &self.args[1..],
683        );
684
685        // Update $_ after command execution.
686        shell.update_last_arg_variable(last_arg);
687
688        result
689    }
690}
691
692pub(crate) fn execute_external_command(
693    context: ExecutionContext<'_, impl extensions::ShellExtensions>,
694    executable_path: &str,
695    process_group_id: Option<i32>,
696    argv0_override: Option<&str>,
697    args: &[CommandArg],
698) -> Result<ExecutionSpawnResult, error::Error> {
699    // Filter out the args; we only want strings.
700    let cmd_args = args
701        .iter()
702        .filter_map(|e| {
703            if let CommandArg::String(s) = e {
704                Some(s)
705            } else {
706                None
707            }
708        })
709        .collect::<Vec<_>>();
710
711    // Before we lose ownership of the open files, figure out if stdin will be a
712    // terminal.
713    let child_stdin_is_terminal = context
714        .try_fd(openfiles::OpenFiles::STDIN_FD)
715        .is_some_and(|f| f.is_terminal());
716
717    // Figure out if we should be setting up a new process group.
718    let new_pg = matches!(
719        context.params.process_group_policy,
720        ProcessGroupPolicy::NewProcessGroup
721    );
722
723    // Compose the std::process::Command that encapsulates what we want to
724    // launch. argv[0] defaults to context.command_name (the user-facing
725    // name of the command) unless the caller specified an explicit
726    // override.
727    let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
728    #[allow(unused_mut, reason = "only mutated on unix platforms")]
729    let mut cmd = compose_std_command(
730        &context,
731        executable_path,
732        argv0,
733        cmd_args.as_slice(),
734        false, /* empty environment? */
735    )?;
736
737    // Set up process group state.
738    if new_pg {
739        // Check if we'll be doing terminal control setup (which includes
740        // setsid)
741        if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
742            // Don't set process_group(0) - setsid() in pre_exec will handle it
743            cmd.lead_session();
744        } else {
745            // Normal case: create new process group in current session
746            cmd.process_group(0);
747            if child_stdin_is_terminal {
748                cmd.take_foreground();
749            }
750        }
751    } else {
752        // We need to join an established process group.
753        if let Some(pgid) = process_group_id {
754            cmd.process_group(pgid);
755        }
756    }
757
758    // When tracing is enabled, report.
759    tracing::debug!(
760        target: trace_categories::COMMANDS,
761        "Spawning: cmd='{} {}'",
762        cmd.get_program().to_string_lossy().to_string(),
763        cmd.get_args()
764            .map(|a| a.to_string_lossy().to_string())
765            .join(" ")
766    );
767
768    match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
769        Ok(child) => {
770            // Retrieve the pid.
771            #[expect(clippy::cast_possible_wrap)]
772            let pid = child.id().map(|id| id as i32);
773            let mut actual_pgid = process_group_id;
774            if let Some(pid) = &pid {
775                if new_pg {
776                    actual_pgid = Some(*pid);
777                }
778            } else {
779                tracing::warn!("could not retrieve pid for child process");
780            }
781
782            if let (Some(on_spawn), Some(pid)) = (&context.shell.options().spawn.on_spawn, pid) {
783                on_spawn(options::SpawnedProcess {
784                    pid,
785                    process_group: actual_pgid,
786                });
787            }
788
789            Ok(ExecutionSpawnResult::StartedProcess(
790                processes::ChildProcess::new(child, pid, actual_pgid),
791            ))
792        }
793        Err(spawn_err) => {
794            if context.shell.options().interactive {
795                sys::terminal::move_self_to_foreground()?;
796            }
797
798            Err(spawn_error(context, spawn_err))
799        }
800    }
801}
802
803/// The error for a spawn or `exec` that failed with
804/// [`std::io::ErrorKind::NotFound`] because the cgroup that `options` name in
805/// [`options::SpawnOptions::cgroup_procs`] is gone, or `None` when it is not.
806///
807/// A child reports only an errno from before its program runs, so a cgroup
808/// file that is gone reads as a missing program; this tells them apart.
809pub fn missing_cgroup(options: &options::RuntimeOptions) -> Option<std::io::Error> {
810    let cgroup_procs = options.spawn.cgroup_procs.as_ref()?;
811    sys::commands::is_missing(cgroup_procs).then(|| {
812        std::io::Error::new(
813            std::io::ErrorKind::NotFound,
814            std::format!(
815                "cannot join the cgroup: {} is missing",
816                cgroup_procs.to_string_lossy()
817            ),
818        )
819    })
820}
821
822/// The error for an external command whose spawn failed with `spawn_err`.
823fn spawn_error(
824    context: ExecutionContext<'_, impl extensions::ShellExtensions>,
825    spawn_err: std::io::Error,
826) -> error::Error {
827    if spawn_err.kind() != std::io::ErrorKind::NotFound {
828        return error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err).into();
829    }
830
831    if let Some(missing) = missing_cgroup(context.shell.options()) {
832        error::ErrorKind::FailedToExecuteCommand(context.command_name, missing).into()
833    } else if !context.shell.working_dir().exists() {
834        error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned()).into()
835    } else {
836        error::ErrorKind::CommandNotFound(context.command_name).into()
837    }
838}
839
840async fn execute_builtin_command<SE: extensions::ShellExtensions>(
841    builtin: &builtins::Registration<SE>,
842    context: ExecutionContext<'_, SE>,
843    args: Vec<CommandArg>,
844) -> Result<ExecutionResult, error::Error> {
845    // In POSIX mode, special builtins that return errors are to be treated as
846    // fatal.
847    let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
848
849    match (builtin.execute_func)(context, args).await {
850        Ok(result) => Ok(result),
851        Err(e) => {
852            // Broken pipe errors should silently return the appropriate exit
853            // code
854            if let Some(io_err) = e.as_io_error() {
855                if io_err.kind() == std::io::ErrorKind::BrokenPipe {
856                    return Ok(ExecutionExitCode::from(io_err).into());
857                }
858            }
859
860            Err(if mark_errors_fatal { e.into_fatal() } else { e })
861        }
862    }
863}
864
865pub(crate) async fn invoke_shell_function(
866    function: functions::Registration,
867    mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
868    args: &[CommandArg],
869) -> Result<ExecutionSpawnResult, error::Error> {
870    let ast::FunctionBody(body, redirects) = &function.definition().body;
871
872    // Apply any redirects specified at function definition-time.
873    if let Some(redirects) = redirects {
874        for redirect in &redirects.0 {
875            interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
876        }
877    }
878
879    let positional_args = args.iter().map(|a| a.to_string());
880
881    // Enter the function; it's left however its body ends, even if it's
882    // cancelled.
883    let mut shell =
884        context
885            .shell
886            .enter_function(context.command_name.as_str(), &function, positional_args)?;
887
888    // A function executes within the current shell process and shares its
889    // caller's open files, so the parameters are passed through by shared
890    // reference rather than cloned. This prevents direct mutation of the
891    // caller's `ExecutionParameters` open-file table, though the function
892    // may still change the shell's persistent open files via builtins (e.g.
893    // `exec`).
894    let result = body.execute(&mut shell, &context.params).await;
895
896    // Leave the function: pop its frame and its local variables.
897    drop(shell);
898
899    // Get the actual execution result from the body of the function.
900    let mut result = result?;
901
902    // Handle control-flow.
903    match result.next_control_flow {
904        ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
905            return error::unimp("break or continue returned from function invocation");
906        }
907        ExecutionControlFlow::ReturnFromFunctionOrScript => {
908            // It's now been handled.
909            result.next_control_flow = ExecutionControlFlow::Normal;
910        }
911        _ => {}
912    }
913
914    Ok(result.into())
915}
916
917pub(crate) async fn invoke_command_in_subshell_and_get_output(
918    shell: &mut Shell<impl extensions::ShellExtensions>,
919    params: &ExecutionParameters,
920    s: String,
921) -> Result<String, error::Error> {
922    // Instantiate a subshell to run the command in.
923    let mut subshell = shell.clone();
924
925    // Command substitutions don't inherit errexit by default. Only inherit it
926    // when command_subst_inherits_errexit is enabled, otherwise disable
927    // errexit in the subshell.
928    if !shell.options().command_subst_inherits_errexit {
929        subshell.options_mut().exit_on_nonzero_command_exit = false;
930    }
931
932    // Get our own set of parameters we can customize and use.
933    let mut params = params.clone();
934    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
935
936    // Set up pipe so we can read the output.
937    let (reader, writer) = sys::pipes::pipe()?;
938    params.set_fd(OpenFiles::STDOUT_FD, writer.into());
939
940    let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
941
942    let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
943
944    let output_str = async_reader.read_to_string().await?;
945
946    // Now observe the command's completion.
947    let run_result = cmd_join_handle.await?;
948    let cmd_result = run_result?;
949
950    // Store the status.
951    shell.set_last_exit_status(cmd_result.exit_code.into());
952
953    // Note: $_ is naturally isolated from the parent because we cloned the
954    // shell to run the substitution.
955
956    Ok(output_str)
957}
958
959async fn run_substitution_command(
960    mut shell: Shell<impl extensions::ShellExtensions>,
961    mut params: ExecutionParameters,
962    command: String,
963) -> Result<ExecutionResult, error::Error> {
964    // Parse the string into a whole shell program.
965    let parse_result = shell.parse_string(command);
966
967    // Check for a command that is only an input redirection ("< file").
968    // If detected, emulate `cat file` to stdout and return immediately.
969    // If we failed to parse, then we'll fall below and handle it there.
970    if let Ok(program) = &parse_result {
971        if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
972            interp::setup_redirect(&mut shell, &mut params, redir).await?;
973            std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
974            return Ok(ExecutionResult::new(0));
975        }
976    }
977
978    // TODO(source-info): review this
979    let source_info = crate::SourceInfo::from("main");
980
981    // Handle the parse result using default shell behavior.
982    shell
983        .run_parsed_result(parse_result, &source_info, &params)
984        .await
985}
986
987// Detects a subshell command that consists solely of a single input redirection
988// (e.g., "< file"), returning the IoRedirect when present.
989fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
990    // We're looking for exactly one complete command...
991    let [complete] = program.complete_commands.as_slice() else {
992        return None;
993    };
994
995    // ...a single list item...
996    let ast::CompoundList(items) = complete;
997    let [item] = items.as_slice() else {
998        return None;
999    };
1000
1001    // ...with a single pipeline (no && or || chaining)...
1002    let and_or = &item.0;
1003    if !and_or.additional.is_empty() {
1004        return None;
1005    }
1006
1007    // ...not negated...
1008    let pipeline = &and_or.first;
1009    if pipeline.bang {
1010        return None;
1011    }
1012
1013    // ...with a single command in the pipeline...
1014    let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
1015        return None;
1016    };
1017
1018    // ...with no program word/name and no suffix...
1019    if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
1020        return None;
1021    }
1022
1023    // ...and exactly one prefix containing an I/O redirect...
1024    let prefix = simple_cmd.prefix.as_ref()?;
1025    let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
1026        return None;
1027    };
1028
1029    // ...that is a file input redirection to a filename, targeting stdin.
1030    match redir {
1031        ast::IoRedirect::File(
1032            fd,
1033            ast::IoFileRedirectKind::Read,
1034            ast::IoFileRedirectTarget::Filename(..),
1035        ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
1036        _ => None,
1037    }
1038}