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