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, CommandProcessLimitExt,
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`, a
190/// `process_limit` or `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    // Every program the shell starts gets the embedder's limit on the user
297    // id's processes; the fork of this child has already been counted.
298    if let Some(limit) = context.shell.options().spawn.process_limit {
299        cmd.set_process_limit(limit)?;
300    }
301
302    Ok(cmd)
303}
304
305pub(crate) async fn on_preexecute(
306    cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
307) -> Result<(), error::Error> {
308    // Set BASH_COMMAND before invoking the DEBUG trap (and generally before
309    // executing commands).
310    let full_cmd = format_command_args(&cmd.args);
311    cmd.shell.env_mut().update_or_add(
312        "BASH_COMMAND",
313        variables::ShellValueLiteral::Scalar(full_cmd),
314        |_| Ok(()),
315        env::EnvironmentLookup::Anywhere,
316        env::EnvironmentScope::Global,
317    )?;
318
319    // Fire the DEBUG trap if one is registered.
320    if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
321        let _ = cmd
322            .shell
323            .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
324            .await?;
325    }
326
327    Ok(())
328}
329
330fn format_command_args(args: &[CommandArg]) -> String {
331    let capacity = args
332        .iter()
333        .map(CommandArg::to_string_len_hint)
334        .sum::<usize>()
335        + args.len().saturating_sub(1);
336    let mut result = String::with_capacity(capacity);
337
338    for (i, arg) in args.iter().enumerate() {
339        if i > 0 {
340            result.push(' ');
341        }
342        let _ = write!(result, "{arg}");
343    }
344
345    result
346}
347
348/// Represents a simple command to be executed.
349pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
350    /// The shell to run the command in.
351    shell: ShellForCommand<'a, SE>,
352
353    /// The execution parameters for the command.
354    pub params: ExecutionParameters,
355
356    /// The name of the command to execute.
357    pub command_name: String,
358
359    /// The arguments to the command, including the command itself.
360    pub args: Vec<CommandArg>,
361
362    /// Whether to consider shell functions when looking up the command name.
363    /// If true, shell functions will be checked; if false, they will be
364    /// ignored.
365    pub use_functions: bool,
366
367    /// Optional list of directories to search for external commands. If left
368    /// `None`, the default search logic will be used.
369    pub path_dirs: Option<Vec<PathBuf>>,
370
371    /// The process group ID to use for externally executed commands. This may
372    /// be `None`, in which case the default behavior will be used.
373    pub process_group_id: Option<i32>,
374
375    /// Optional override for the `argv[0]` value presented to an externally
376    /// spawned process. When `None`, `command_name` is used.
377    pub argv0: Option<String>,
378}
379
380impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
381    /// Creates a new `SimpleCommand` instance.
382    ///
383    /// # Arguments
384    ///
385    /// * `shell` - The shell in which to execute the command.
386    /// * `params` - The execution parameters for the command.
387    /// * `command_name` - The name of the command to execute.
388    /// * `args` - The arguments to the command, including the command itself.
389    pub fn new<I>(
390        shell: ShellForCommand<'a, SE>,
391        params: ExecutionParameters,
392        command_name: String,
393        args: I,
394    ) -> Self
395    where
396        I: IntoIterator<Item = CommandArg>,
397    {
398        Self {
399            shell,
400            params,
401            command_name,
402            args: args.into_iter().collect(),
403            use_functions: true,
404            path_dirs: None,
405            process_group_id: None,
406            argv0: None,
407        }
408    }
409
410    /// Executes the simple command.
411    ///
412    /// The command may be a builtin, a shell function, or an externally
413    /// executed command. This function's implementation is responsible for
414    /// dispatching it appropriately according to the context provided.
415    ///
416    /// # Errors
417    ///
418    /// * [`ErrorKind::CommandNotFound`] if the name is not a builtin or
419    ///   function and is not found in the search path.
420    /// * Any error from starting a builtin, function, or external process; for
421    ///   example [`ErrorKind::FailedToExecuteCommand`] or
422    ///   [`ErrorKind::WorkingDirMissing`] when an external command cannot be
423    ///   spawned.
424    ///
425    /// # Panics
426    ///
427    /// Does not panic in practice: the `unwrap` runs only after the builtin
428    /// lookup was checked to be `Some`.
429    pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
430        // First see if it's the name of a builtin.
431        let builtin = self.shell.builtins().get(&self.command_name).cloned();
432
433        // If we're in POSIX mode and found a special builtin (that's not
434        // disabled), then invoke it without considering functions.
435        if self.shell.options().posix_mode
436            && builtin
437                .as_ref()
438                .is_some_and(|r| !r.disabled && r.special_builtin)
439        {
440            #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
441            let builtin = builtin.unwrap();
442            return self.execute_via_builtin(builtin).await;
443        }
444
445        // Assuming we weren't requested not to do so, check if it's the name of
446        // a shell function.
447        if self.use_functions {
448            if let Some(func_registration) =
449                self.shell.funcs().get(self.command_name.as_str()).cloned()
450            {
451                return self.execute_via_function(func_registration).await;
452            }
453        }
454
455        // If we haven't yet resolved the command name and found a builtin
456        // that's not disabled, then invoke it.
457        if let Some(builtin) = builtin {
458            if !builtin.disabled {
459                return self.execute_via_builtin(builtin).await;
460            }
461        }
462
463        // We still haven't found a command to invoke. We'll need to look for an
464        // external command.
465        if !sys::fs::contains_path_separator(&self.command_name) {
466            // All else failed; if we were given path directories to search,
467            // look through them for a match. Otherwise, use our
468            // default search logic.
469            let path = if let Some(path_dirs) = &self.path_dirs {
470                pathsearch::resolve_command(path_dirs, self.command_name.as_str())
471            } else {
472                self.shell
473                    .resolve_command_in_path_using_cache(&self.command_name)
474            };
475
476            if let Some(path) = path {
477                self.execute_via_external(&path)
478            } else {
479                // Bash updates $_ even when the command is not found, so mirror
480                // that here before reporting the error.
481                let last_arg = Self::take_last_arg(&self.args);
482                self.shell.update_last_arg_variable(last_arg);
483
484                Err(ErrorKind::CommandNotFound(self.command_name).into())
485            }
486        } else {
487            let command_name = sys::fs::resolve_executable_path(
488                PathBuf::from(&self.command_name),
489                self.shell.working_dir(),
490            );
491            self.execute_via_external(command_name.as_path())
492        }
493    }
494
495    /// Extracts the owned string representation of the last argument of a
496    /// command, suitable for recording into `$_`.
497    fn take_last_arg(args: &[CommandArg]) -> Option<String> {
498        args.last().map(ToString::to_string)
499    }
500
501    async fn execute_via_builtin(
502        self,
503        builtin: builtins::Registration<SE>,
504    ) -> Result<ExecutionSpawnResult, error::Error> {
505        match self.shell {
506            ShellForCommand::OwnedShell { target, .. } => {
507                Ok(Self::execute_via_builtin_in_owned_shell(
508                    *target,
509                    self.params,
510                    builtin,
511                    self.command_name,
512                    self.args,
513                )
514                .await)
515            }
516            ShellForCommand::ParentShell(..) => {
517                self.execute_via_builtin_in_parent_shell(builtin).await
518            }
519        }
520    }
521
522    // Not an `async fn`: its future would hold `shell`, and that would grow the
523    // future of every simple command, not just this rare case.
524    fn execute_via_builtin_in_owned_shell(
525        mut shell: Shell<SE>,
526        params: ExecutionParameters,
527        builtin: builtins::Registration<SE>,
528        command_name: String,
529        args: Vec<CommandArg>,
530    ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
531        let last_arg = Self::take_last_arg(&args);
532        // Builtins may block (e.g., reading input), so keep them from holding
533        // up the rest of the pipeline.
534        let task = sys::tasks::spawn_blocking(async move {
535            let cmd_context = ExecutionContext {
536                shell: &mut shell,
537                command_name,
538                params,
539            };
540
541            let result = execute_builtin_command(&builtin, cmd_context, args).await;
542
543            // Update $_ after command execution.
544            shell.update_last_arg_variable(last_arg);
545
546            result
547        });
548
549        async move { ExecutionSpawnResult::StartedTask(task.await) }
550    }
551
552    async fn execute_via_builtin_in_parent_shell(
553        self,
554        builtin: builtins::Registration<SE>,
555    ) -> Result<ExecutionSpawnResult, error::Error> {
556        let mut shell = self.shell;
557        let last_arg = Self::take_last_arg(&self.args);
558
559        let cmd_context = ExecutionContext {
560            shell: &mut shell,
561            command_name: self.command_name,
562            params: self.params,
563        };
564
565        let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
566
567        // Update $_ after command execution.
568        shell.update_last_arg_variable(last_arg);
569
570        let result = result?;
571
572        Ok(result.into())
573    }
574
575    async fn execute_via_function(
576        self,
577        func_registration: functions::Registration,
578    ) -> Result<ExecutionSpawnResult, error::Error> {
579        match self.shell {
580            ShellForCommand::OwnedShell { target, .. } => {
581                Ok(Self::execute_via_function_in_owned_shell(
582                    *target,
583                    self.params,
584                    func_registration,
585                    self.command_name,
586                    self.args,
587                )
588                .await)
589            }
590            ShellForCommand::ParentShell(..) => {
591                self.execute_via_function_in_parent_shell(func_registration)
592                    .await
593            }
594        }
595    }
596
597    // As a non-last pipeline stage, a function must run as a background task
598    // (mirroring `execute_via_builtin_in_owned_shell` above) rather than
599    // being awaited inline in `spawn_pipeline_processes`'s own per-stage
600    // loop: awaiting it inline there blocks that loop until the function's
601    // body fully returns, so the *next* pipeline stage (the one that would
602    // actually drain this function's stdout pipe) is never even spawned
603    // until this one is done. A function that writes more to its stdout
604    // pipe than the OS pipe buffer holds (~64KiB on Linux) before returning
605    // then blocks on that `write()` forever, since nothing is reading the
606    // other end yet -- a plain deadlock, not a slow completion. Confirmed
607    // empirically with a two-line repro having nothing to do with any
608    // external command or builtin: `f() { for i in $(seq 1 5000); do echo
609    // line $i; done; }; f | cat` hangs indefinitely. Real bash always runs
610    // every pipeline stage as its own concurrently-running process, so this
611    // case never arises there.
612    fn execute_via_function_in_owned_shell(
613        mut shell: Shell<SE>,
614        params: ExecutionParameters,
615        func_registration: functions::Registration,
616        command_name: String,
617        args: Vec<CommandArg>,
618    ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
619        let last_arg = Self::take_last_arg(&args);
620        let task = sys::tasks::spawn_blocking(async move {
621            let cmd_context = ExecutionContext {
622                shell: &mut shell,
623                command_name,
624                params,
625            };
626
627            let spawn_result =
628                invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
629
630            // Update $_ after command execution.
631            shell.update_last_arg_variable(last_arg);
632
633            match spawn_result? {
634                ExecutionSpawnResult::Completed(result) => Ok(result),
635                ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
636                    // invoke_shell_function() only ever returns Completed
637                    // today; this guards against a silent behavior change
638                    // rather than dropping already-started work.
639                    error::unimp("invoke_shell_function returned an unexpected spawn result")
640                }
641            }
642        });
643
644        async move { ExecutionSpawnResult::StartedTask(task.await) }
645    }
646
647    async fn execute_via_function_in_parent_shell(
648        self,
649        func_registration: functions::Registration,
650    ) -> Result<ExecutionSpawnResult, error::Error> {
651        let mut shell = self.shell;
652        let last_arg = Self::take_last_arg(&self.args);
653
654        let cmd_context = ExecutionContext {
655            shell: &mut shell,
656            command_name: self.command_name,
657            params: self.params,
658        };
659
660        // Strip the function name off args.
661        let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
662
663        // $_ is reset *after* the function body runs, to the last argument of
664        // the invocation (or the function name itself if zero args). Any
665        // mutations made inside the body are overwritten — this matches bash,
666        // where the caller observes only the invocation's last argument.
667        shell.update_last_arg_variable(last_arg);
668
669        result
670    }
671
672    fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
673        let mut shell = self.shell;
674        let last_arg = Self::take_last_arg(&self.args);
675
676        let cmd_context = ExecutionContext {
677            shell: &mut shell,
678            command_name: self.command_name,
679            params: self.params,
680        };
681
682        let resolved_path = path.to_string_lossy();
683        let result = execute_external_command(
684            cmd_context,
685            resolved_path.as_ref(),
686            self.process_group_id,
687            self.argv0.as_deref(),
688            &self.args[1..],
689        );
690
691        // Update $_ after command execution.
692        shell.update_last_arg_variable(last_arg);
693
694        result
695    }
696}
697
698pub(crate) fn execute_external_command(
699    context: ExecutionContext<'_, impl extensions::ShellExtensions>,
700    executable_path: &str,
701    process_group_id: Option<i32>,
702    argv0_override: Option<&str>,
703    args: &[CommandArg],
704) -> Result<ExecutionSpawnResult, error::Error> {
705    // Filter out the args; we only want strings.
706    let cmd_args = args
707        .iter()
708        .filter_map(|e| {
709            if let CommandArg::String(s) = e {
710                Some(s)
711            } else {
712                None
713            }
714        })
715        .collect::<Vec<_>>();
716
717    // Before we lose ownership of the open files, figure out if stdin will be a
718    // terminal.
719    let child_stdin_is_terminal = context
720        .try_fd(openfiles::OpenFiles::STDIN_FD)
721        .is_some_and(|f| f.is_terminal());
722
723    // Figure out if we should be setting up a new process group.
724    let new_pg = matches!(
725        context.params.process_group_policy,
726        ProcessGroupPolicy::NewProcessGroup
727    );
728
729    // Compose the std::process::Command that encapsulates what we want to
730    // launch. argv[0] defaults to context.command_name (the user-facing
731    // name of the command) unless the caller specified an explicit
732    // override.
733    let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
734    #[allow(unused_mut, reason = "only mutated on unix platforms")]
735    let mut cmd = compose_std_command(
736        &context,
737        executable_path,
738        argv0,
739        cmd_args.as_slice(),
740        false, /* empty environment? */
741    )?;
742
743    // Set up process group state.
744    if new_pg {
745        // Check if we'll be doing terminal control setup (which includes
746        // setsid)
747        if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
748            // Don't set process_group(0) - setsid() in pre_exec will handle it
749            cmd.lead_session();
750        } else {
751            // Normal case: create new process group in current session
752            cmd.process_group(0);
753            if child_stdin_is_terminal {
754                cmd.take_foreground();
755            }
756        }
757    } else {
758        // We need to join an established process group.
759        if let Some(pgid) = process_group_id {
760            cmd.process_group(pgid);
761        }
762    }
763
764    // When tracing is enabled, report.
765    tracing::debug!(
766        target: trace_categories::COMMANDS,
767        "Spawning: cmd='{} {}'",
768        cmd.get_program().to_string_lossy().to_string(),
769        cmd.get_args()
770            .map(|a| a.to_string_lossy().to_string())
771            .join(" ")
772    );
773
774    // Held until `on_spawn` returns, or until the spawn fails.
775    let _guard = context
776        .shell
777        .options()
778        .spawn
779        .before_spawn
780        .as_ref()
781        .map(|before_spawn| before_spawn());
782    match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
783        Ok(child) => {
784            // Retrieve the pid.
785            #[expect(clippy::cast_possible_wrap)]
786            let pid = child.id().map(|id| id as i32);
787            let mut actual_pgid = process_group_id;
788            if let Some(pid) = &pid {
789                if new_pg {
790                    actual_pgid = Some(*pid);
791                }
792            } else {
793                tracing::warn!("could not retrieve pid for child process");
794            }
795
796            if let (Some(on_spawn), Some(pid)) = (&context.shell.options().spawn.on_spawn, pid) {
797                on_spawn(options::SpawnedProcess {
798                    pid,
799                    process_group: actual_pgid,
800                });
801            }
802
803            Ok(ExecutionSpawnResult::StartedProcess(
804                processes::ChildProcess::new(child, pid, actual_pgid),
805            ))
806        }
807        Err(spawn_err) => {
808            if context.shell.options().interactive {
809                sys::terminal::move_self_to_foreground()?;
810            }
811
812            Err(spawn_error(context, spawn_err))
813        }
814    }
815}
816
817/// The error for a spawn or `exec` that failed with
818/// [`std::io::ErrorKind::NotFound`] because the cgroup that `options` name in
819/// [`options::SpawnOptions::cgroup_procs`] is gone, or `None` when it is not.
820///
821/// A child reports only an errno from before its program runs, so a cgroup
822/// file that is gone reads as a missing program; this tells them apart.
823pub fn missing_cgroup(options: &options::RuntimeOptions) -> Option<std::io::Error> {
824    let cgroup_procs = options.spawn.cgroup_procs.as_ref()?;
825    sys::commands::is_missing(cgroup_procs).then(|| {
826        std::io::Error::new(
827            std::io::ErrorKind::NotFound,
828            std::format!(
829                "cannot join the cgroup: {} is missing",
830                cgroup_procs.to_string_lossy()
831            ),
832        )
833    })
834}
835
836/// The error for an external command whose spawn failed with `spawn_err`.
837fn spawn_error(
838    context: ExecutionContext<'_, impl extensions::ShellExtensions>,
839    spawn_err: std::io::Error,
840) -> error::Error {
841    if spawn_err.kind() != std::io::ErrorKind::NotFound {
842        return error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err).into();
843    }
844
845    if let Some(missing) = missing_cgroup(context.shell.options()) {
846        error::ErrorKind::FailedToExecuteCommand(context.command_name, missing).into()
847    } else if !context.shell.working_dir().exists() {
848        error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned()).into()
849    } else {
850        error::ErrorKind::CommandNotFound(context.command_name).into()
851    }
852}
853
854async fn execute_builtin_command<SE: extensions::ShellExtensions>(
855    builtin: &builtins::Registration<SE>,
856    context: ExecutionContext<'_, SE>,
857    args: Vec<CommandArg>,
858) -> Result<ExecutionResult, error::Error> {
859    // In POSIX mode, special builtins that return errors are to be treated as
860    // fatal.
861    let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
862
863    match (builtin.execute_func)(context, args).await {
864        Ok(result) => Ok(result),
865        Err(e) => {
866            // Broken pipe errors should silently return the appropriate exit
867            // code
868            if let Some(io_err) = e.as_io_error() {
869                if io_err.kind() == std::io::ErrorKind::BrokenPipe {
870                    return Ok(ExecutionExitCode::from(io_err).into());
871                }
872            }
873
874            Err(if mark_errors_fatal { e.into_fatal() } else { e })
875        }
876    }
877}
878
879pub(crate) async fn invoke_shell_function(
880    function: functions::Registration,
881    mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
882    args: &[CommandArg],
883) -> Result<ExecutionSpawnResult, error::Error> {
884    let ast::FunctionBody(body, redirects) = &function.definition().body;
885
886    // Apply any redirects specified at function definition-time.
887    if let Some(redirects) = redirects {
888        for redirect in &redirects.0 {
889            interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
890        }
891    }
892
893    let positional_args = args.iter().map(|a| a.to_string());
894
895    // Enter the function; it's left however its body ends, even if it's
896    // cancelled.
897    let mut shell =
898        context
899            .shell
900            .enter_function(context.command_name.as_str(), &function, positional_args)?;
901
902    // A function executes within the current shell process and shares its
903    // caller's open files, so the parameters are passed through by shared
904    // reference rather than cloned. This prevents direct mutation of the
905    // caller's `ExecutionParameters` open-file table, though the function
906    // may still change the shell's persistent open files via builtins (e.g.
907    // `exec`).
908    let result = body.execute(&mut shell, &context.params).await;
909
910    // Leave the function: pop its frame and its local variables.
911    drop(shell);
912
913    // Get the actual execution result from the body of the function.
914    let mut result = result?;
915
916    // Handle control-flow.
917    match result.next_control_flow {
918        ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
919            return error::unimp("break or continue returned from function invocation");
920        }
921        ExecutionControlFlow::ReturnFromFunctionOrScript => {
922            // It's now been handled.
923            result.next_control_flow = ExecutionControlFlow::Normal;
924        }
925        _ => {}
926    }
927
928    Ok(result.into())
929}
930
931pub(crate) async fn invoke_command_in_subshell_and_get_output(
932    shell: &mut Shell<impl extensions::ShellExtensions>,
933    params: &ExecutionParameters,
934    s: String,
935) -> Result<String, error::Error> {
936    // Instantiate a subshell to run the command in.
937    let mut subshell = shell.clone();
938
939    // Command substitutions don't inherit errexit by default. Only inherit it
940    // when command_subst_inherits_errexit is enabled, otherwise disable
941    // errexit in the subshell.
942    if !shell.options().command_subst_inherits_errexit {
943        subshell.options_mut().exit_on_nonzero_command_exit = false;
944    }
945
946    // Get our own set of parameters we can customize and use.
947    let mut params = params.clone();
948    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
949
950    // Set up pipe so we can read the output.
951    let (reader, writer) = sys::pipes::pipe()?;
952    params.set_fd(OpenFiles::STDOUT_FD, writer.into());
953
954    let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
955
956    let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
957
958    let output_str = async_reader.read_to_string().await?;
959
960    // Now observe the command's completion.
961    let run_result = cmd_join_handle.await?;
962    let cmd_result = run_result?;
963
964    // Store the status.
965    shell.set_last_exit_status(cmd_result.exit_code.into());
966
967    // Note: $_ is naturally isolated from the parent because we cloned the
968    // shell to run the substitution.
969
970    Ok(output_str)
971}
972
973async fn run_substitution_command(
974    mut shell: Shell<impl extensions::ShellExtensions>,
975    mut params: ExecutionParameters,
976    command: String,
977) -> Result<ExecutionResult, error::Error> {
978    // Parse the string into a whole shell program.
979    let parse_result = shell.parse_string(command);
980
981    // Check for a command that is only an input redirection ("< file").
982    // If detected, emulate `cat file` to stdout and return immediately.
983    // If we failed to parse, then we'll fall below and handle it there.
984    if let Ok(program) = &parse_result {
985        if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
986            interp::setup_redirect(&mut shell, &mut params, redir).await?;
987            std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
988            return Ok(ExecutionResult::new(0));
989        }
990    }
991
992    // TODO(source-info): review this
993    let source_info = crate::SourceInfo::from("main");
994
995    // Handle the parse result using default shell behavior.
996    shell
997        .run_parsed_result(parse_result, &source_info, &params)
998        .await
999}
1000
1001// Detects a subshell command that consists solely of a single input redirection
1002// (e.g., "< file"), returning the IoRedirect when present.
1003fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
1004    // We're looking for exactly one complete command...
1005    let [complete] = program.complete_commands.as_slice() else {
1006        return None;
1007    };
1008
1009    // ...a single list item...
1010    let ast::CompoundList(items) = complete;
1011    let [item] = items.as_slice() else {
1012        return None;
1013    };
1014
1015    // ...with a single pipeline (no && or || chaining)...
1016    let and_or = &item.0;
1017    if !and_or.additional.is_empty() {
1018        return None;
1019    }
1020
1021    // ...not negated...
1022    let pipeline = &and_or.first;
1023    if pipeline.bang {
1024        return None;
1025    }
1026
1027    // ...with a single command in the pipeline...
1028    let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
1029        return None;
1030    };
1031
1032    // ...with no program word/name and no suffix...
1033    if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
1034        return None;
1035    }
1036
1037    // ...and exactly one prefix containing an I/O redirect...
1038    let prefix = simple_cmd.prefix.as_ref()?;
1039    let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
1040        return None;
1041    };
1042
1043    // ...that is a file input redirection to a filename, targeting stdin.
1044    match redir {
1045        ast::IoRedirect::File(
1046            fd,
1047            ast::IoFileRedirectKind::Read,
1048            ast::IoFileRedirectTarget::Filename(..),
1049        ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
1050        _ => None,
1051    }
1052}