Skip to main content

brush_core/
commands.rs

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