brush_core/
commands.rs

1use std::io::Write;
2#[cfg(unix)]
3use std::os::unix::process::CommandExt;
4use std::{borrow::Cow, ffi::OsStr, fmt::Display, process::Stdio, sync::Arc};
5
6use brush_parser::ast;
7#[cfg(unix)]
8use command_fds::{CommandFdExt, FdMapping};
9use itertools::Itertools;
10
11use crate::{
12    ExecutionParameters, ExecutionResult, Shell, builtins, error, escape,
13    interp::{self, Execute, ProcessGroupPolicy},
14    openfiles::{self, OpenFile, OpenFiles},
15    pathsearch, processes, sys, trace_categories,
16};
17
18/// Represents the result of spawning a command.
19pub(crate) enum CommandSpawnResult {
20    /// The child process was spawned.
21    SpawnedProcess(processes::ChildProcess),
22    /// The command immediately exited with the given numeric exit code.
23    ImmediateExit(u8),
24    /// The shell should exit after this command, yielding the given numeric exit code.
25    ExitShell(u8),
26    /// The shell should return from the current function or script, yielding the given numeric
27    /// exit code.
28    ReturnFromFunctionOrScript(u8),
29    /// The shell should break out of the containing loop, identified by the given depth count.
30    BreakLoop(u8),
31    /// The shell should continue the containing loop, identified by the given depth count.
32    ContinueLoop(u8),
33}
34
35impl CommandSpawnResult {
36    // TODO: jobs: remove `no_wait`; it doesn't make any sense
37    #[allow(clippy::too_many_lines)]
38    pub async fn wait(self, no_wait: bool) -> Result<CommandWaitResult, error::Error> {
39        #[allow(clippy::ignored_unit_patterns)]
40        match self {
41            Self::SpawnedProcess(mut child) => {
42                let process_wait_result = if !no_wait {
43                    // Wait for the process to exit or for a relevant signal, whichever happens
44                    // first.
45                    child.wait().await?
46                } else {
47                    processes::ProcessWaitResult::Stopped
48                };
49
50                let command_wait_result = match process_wait_result {
51                    processes::ProcessWaitResult::Completed(output) => {
52                        CommandWaitResult::CommandCompleted(ExecutionResult::from(output))
53                    }
54                    processes::ProcessWaitResult::Stopped => CommandWaitResult::CommandStopped(
55                        ExecutionResult::from(processes::ProcessWaitResult::Stopped),
56                        child,
57                    ),
58                };
59
60                Ok(command_wait_result)
61            }
62            Self::ImmediateExit(exit_code) => Ok(CommandWaitResult::CommandCompleted(
63                ExecutionResult::new(exit_code),
64            )),
65            Self::ExitShell(exit_code) => {
66                Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
67                    exit_code,
68                    exit_shell: true,
69                    ..ExecutionResult::default()
70                }))
71            }
72            Self::ReturnFromFunctionOrScript(exit_code) => {
73                Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
74                    exit_code,
75                    return_from_function_or_script: true,
76                    ..ExecutionResult::default()
77                }))
78            }
79            Self::BreakLoop(count) => Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
80                exit_code: 0,
81                break_loop: Some(count),
82                ..ExecutionResult::default()
83            })),
84            Self::ContinueLoop(count) => Ok(CommandWaitResult::CommandCompleted(ExecutionResult {
85                exit_code: 0,
86                continue_loop: Some(count),
87                ..ExecutionResult::default()
88            })),
89        }
90    }
91}
92
93/// Encapsulates the result of waiting for a command to complete.
94pub(crate) enum CommandWaitResult {
95    /// The command completed.
96    CommandCompleted(ExecutionResult),
97    /// The command was stopped before it completed.
98    CommandStopped(ExecutionResult, processes::ChildProcess),
99}
100
101/// Represents the context for executing a command.
102pub struct ExecutionContext<'a> {
103    /// The shell in which the command is being executed.
104    pub shell: &'a mut Shell,
105    /// The name of the command being executed.    
106    pub command_name: String,
107    /// The parameters for the execution.
108    pub params: ExecutionParameters,
109}
110
111impl ExecutionContext<'_> {
112    /// Returns the standard input file; usable with `write!` et al.
113    pub fn stdin(&self) -> impl std::io::Read + 'static {
114        self.params.stdin()
115    }
116
117    /// Returns the standard output file; usable with `write!` et al.
118    pub fn stdout(&self) -> impl std::io::Write + 'static {
119        self.params.stdout()
120    }
121
122    /// Returns the standard error file; usable with `write!` et al.
123    pub fn stderr(&self) -> impl std::io::Write + 'static {
124        self.params.stderr()
125    }
126
127    pub(crate) const fn should_cmd_lead_own_process_group(&self) -> bool {
128        self.shell.options.interactive
129            && matches!(
130                self.params.process_group_policy,
131                ProcessGroupPolicy::NewProcessGroup
132            )
133    }
134}
135
136/// An argument to a command.
137#[derive(Clone, Debug)]
138pub enum CommandArg {
139    /// A simple string argument.
140    String(String),
141    /// An assignment/declaration; typically treated as a string, but will
142    /// be specially handled by a limited set of built-in commands.
143    Assignment(ast::Assignment),
144}
145
146impl Display for CommandArg {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        match self {
149            Self::String(s) => f.write_str(s),
150            Self::Assignment(a) => write!(f, "{a}"),
151        }
152    }
153}
154
155impl From<String> for CommandArg {
156    fn from(s: String) -> Self {
157        Self::String(s)
158    }
159}
160
161impl From<&String> for CommandArg {
162    fn from(value: &String) -> Self {
163        Self::String(value.clone())
164    }
165}
166
167impl CommandArg {
168    pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
169        match self {
170            Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
171            Self::Assignment(a) => {
172                let mut s = a.name.to_string();
173                let op = if a.append { "+=" } else { "=" };
174                s.push_str(op);
175                s.push_str(&escape::quote_if_needed(
176                    a.value.to_string().as_str(),
177                    escape::QuoteMode::SingleQuote,
178                ));
179                s.into()
180            }
181        }
182    }
183}
184
185#[allow(unused_variables)]
186pub(crate) fn compose_std_command<S: AsRef<OsStr>>(
187    shell: &Shell,
188    command_name: &str,
189    argv0: &str,
190    args: &[S],
191    mut open_files: OpenFiles,
192    empty_env: bool,
193) -> Result<std::process::Command, error::Error> {
194    let mut cmd = std::process::Command::new(command_name);
195
196    // Override argv[0].
197    #[cfg(unix)]
198    cmd.arg0(argv0);
199
200    // Pass through args.
201    cmd.args(args);
202
203    // Use the shell's current working dir.
204    cmd.current_dir(shell.working_dir.as_path());
205
206    // Start with a clear environment.
207    cmd.env_clear();
208
209    // Add in exported variables.
210    if !empty_env {
211        for (k, v) in shell.env.iter_exported() {
212            cmd.env(k.as_str(), v.value().to_cow_str(shell).as_ref());
213        }
214    }
215
216    // Add in exported functions.
217    if !empty_env {
218        for (func_name, registration) in shell.funcs.iter() {
219            if registration.is_exported() {
220                let var_name = std::format!("BASH_FUNC_{func_name}%%");
221                let value = std::format!("() {}", registration.definition.body);
222                cmd.env(var_name, value);
223            }
224        }
225    }
226
227    // Redirect stdin, if applicable.
228    match open_files.files.remove(&0) {
229        Some(OpenFile::Stdin) | None => (),
230        Some(stdin_file) => {
231            let as_stdio: Stdio = stdin_file.into();
232            cmd.stdin(as_stdio);
233        }
234    }
235
236    // Redirect stdout, if applicable.
237    match open_files.files.remove(&1) {
238        Some(OpenFile::Stdout) | None => (),
239        Some(stdout_file) => {
240            let as_stdio: Stdio = stdout_file.into();
241            cmd.stdout(as_stdio);
242        }
243    }
244
245    // Redirect stderr, if applicable.
246    match open_files.files.remove(&2) {
247        Some(OpenFile::Stderr) | None => {}
248        Some(stderr_file) => {
249            let as_stdio: Stdio = stderr_file.into();
250            cmd.stderr(as_stdio);
251        }
252    }
253
254    // Inject any other fds.
255    #[cfg(unix)]
256    {
257        let fd_mappings = open_files
258            .files
259            .into_iter()
260            .map(|(child_fd, open_file)| FdMapping {
261                child_fd: i32::try_from(child_fd).unwrap(),
262                parent_fd: open_file.into_owned_fd().unwrap(),
263            })
264            .collect();
265        cmd.fd_mappings(fd_mappings)
266            .map_err(|_e| error::Error::ChildCreationFailure)?;
267    }
268    #[cfg(not(unix))]
269    {
270        if !open_files.files.is_empty() {
271            return error::unimp("fd redirections on non-Unix platform");
272        }
273    }
274
275    Ok(cmd)
276}
277
278pub(crate) async fn execute(
279    cmd_context: ExecutionContext<'_>,
280    process_group_id: &mut Option<i32>,
281    args: Vec<CommandArg>,
282    use_functions: bool,
283    path_dirs: Option<Vec<String>>,
284) -> Result<CommandSpawnResult, error::Error> {
285    // First see if it's the name of a builtin.
286    let builtin = cmd_context
287        .shell
288        .builtins
289        .get(&cmd_context.command_name)
290        .cloned();
291
292    // If we found a special builtin (that's not disabled), then invoke it.
293    if builtin
294        .as_ref()
295        .is_some_and(|r| !r.disabled && r.special_builtin)
296    {
297        return execute_builtin_command(&builtin.unwrap(), cmd_context, args).await;
298    }
299
300    // Assuming we weren't requested not to do so, check if it's the name of
301    // a shell function.
302    if use_functions {
303        if let Some(func_reg) = cmd_context
304            .shell
305            .funcs
306            .get(cmd_context.command_name.as_str())
307        {
308            // Strip the function name off args.
309            return invoke_shell_function(func_reg.definition.clone(), cmd_context, &args[1..])
310                .await;
311        }
312    }
313
314    // If we found a (non-special) builtin and it's not disabled, then invoke it.
315    if let Some(builtin) = builtin {
316        if !builtin.disabled {
317            return execute_builtin_command(&builtin, cmd_context, args).await;
318        }
319    }
320
321    // We still haven't found a command to invoke. We'll need to look for an external command.
322    if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
323        // All else failed; if we were given path directories to search, try to look through them
324        // for a matching executable. Otherwise, use our default search logic.
325        let path = if let Some(path_dirs) = path_dirs {
326            pathsearch::search_for_executable(
327                path_dirs.iter().map(String::as_str),
328                cmd_context.command_name.as_str(),
329            )
330            .next()
331        } else {
332            cmd_context
333                .shell
334                .find_first_executable_in_path_using_cache(&cmd_context.command_name)
335        };
336
337        if let Some(path) = path {
338            let resolved_path = path.to_string_lossy();
339            execute_external_command(
340                cmd_context,
341                resolved_path.as_ref(),
342                process_group_id,
343                &args[1..],
344            )
345        } else {
346            writeln!(
347                cmd_context.stderr(),
348                "{}: command not found",
349                cmd_context.command_name
350            )?;
351            Ok(CommandSpawnResult::ImmediateExit(127))
352        }
353    } else {
354        let resolved_path = cmd_context.command_name.clone();
355
356        // Strip the command name off args.
357        execute_external_command(
358            cmd_context,
359            resolved_path.as_str(),
360            process_group_id,
361            &args[1..],
362        )
363    }
364}
365
366#[allow(clippy::too_many_lines)]
367#[allow(unused_variables)]
368pub(crate) fn execute_external_command(
369    context: ExecutionContext<'_>,
370    executable_path: &str,
371    process_group_id: &mut Option<i32>,
372    args: &[CommandArg],
373) -> Result<CommandSpawnResult, error::Error> {
374    // Filter out the args; we only want strings.
375    let mut cmd_args = vec![];
376    for arg in args {
377        if let CommandArg::String(s) = arg {
378            cmd_args.push(s);
379        }
380    }
381
382    // Before we lose ownership of the open files, figure out if stdin will be a terminal.
383    #[allow(unused_variables)]
384    let child_stdin_is_terminal = context
385        .params
386        .open_files
387        .stdin()
388        .is_some_and(|f| f.is_term());
389
390    // Figure out if we should be setting up a new process group.
391    let new_pg = context.should_cmd_lead_own_process_group();
392
393    // Save copy of stderr for errors.
394    let mut stderr = context.stderr();
395
396    // Compose the std::process::Command that encapsulates what we want to launch.
397    #[allow(unused_mut)]
398    let mut cmd = compose_std_command(
399        context.shell,
400        executable_path,
401        context.command_name.as_str(),
402        cmd_args.as_slice(),
403        context.params.open_files,
404        false, /* empty environment? */
405    )?;
406
407    // Set up process group state.
408    if new_pg {
409        // We need to set up a new process group.
410        #[cfg(unix)]
411        cmd.process_group(0);
412    } else if let Some(pgid) = process_group_id {
413        // We need to join an established process group.
414        #[cfg(unix)]
415        cmd.process_group(*pgid);
416    }
417
418    // Register some code to run in the forked child process before it execs
419    // the target command.
420    #[cfg(unix)]
421    if new_pg && child_stdin_is_terminal {
422        unsafe {
423            cmd.pre_exec(setup_process_before_exec);
424        }
425    }
426
427    // When tracing is enabled, report.
428    tracing::debug!(
429        target: trace_categories::COMMANDS,
430        "Spawning: cmd='{} {}'",
431        cmd.get_program().to_string_lossy().to_string(),
432        cmd.get_args()
433            .map(|a| a.to_string_lossy().to_string())
434            .join(" ")
435    );
436
437    match sys::process::spawn(cmd) {
438        Ok(child) => {
439            // Retrieve the pid.
440            #[allow(clippy::cast_possible_wrap)]
441            let pid = child.id().map(|id| id as i32);
442            if let Some(pid) = &pid {
443                if new_pg {
444                    *process_group_id = Some(*pid);
445                }
446            } else {
447                tracing::warn!("could not retrieve pid for child process");
448            }
449
450            Ok(CommandSpawnResult::SpawnedProcess(
451                processes::ChildProcess::new(pid, child),
452            ))
453        }
454        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
455            if context.shell.options.interactive {
456                sys::terminal::move_self_to_foreground()?;
457            }
458
459            if !context.shell.working_dir.exists() {
460                // We may have failed because the working directory doesn't exist.
461                writeln!(
462                    stderr,
463                    "{}: working directory does not exist: {}",
464                    context.shell.shell_name.as_ref().unwrap_or(&String::new()),
465                    context.shell.working_dir.display()
466                )?;
467            } else if context.shell.options.sh_mode {
468                writeln!(
469                    stderr,
470                    "{}: {}: {}: not found",
471                    context.shell.shell_name.as_ref().unwrap_or(&String::new()),
472                    context.shell.get_current_input_line_number(),
473                    context.command_name
474                )?;
475            } else {
476                writeln!(stderr, "{}: not found", context.command_name)?;
477            }
478            Ok(CommandSpawnResult::ImmediateExit(127))
479        }
480        Err(e) => {
481            if context.shell.options.interactive {
482                sys::terminal::move_self_to_foreground()?;
483            }
484
485            tracing::error!("{e}");
486            Ok(CommandSpawnResult::ImmediateExit(126))
487        }
488    }
489}
490
491#[cfg(unix)]
492fn setup_process_before_exec() -> Result<(), std::io::Error> {
493    sys::terminal::move_self_to_foreground().map_err(std::io::Error::other)?;
494    Ok(())
495}
496
497async fn execute_builtin_command(
498    builtin: &builtins::Registration,
499    context: ExecutionContext<'_>,
500    args: Vec<CommandArg>,
501) -> Result<CommandSpawnResult, error::Error> {
502    let exit_code = match (builtin.execute_func)(context, args).await {
503        Ok(builtin_result) => match builtin_result.exit_code {
504            builtins::ExitCode::Success => 0,
505            builtins::ExitCode::InvalidUsage => 2,
506            builtins::ExitCode::Unimplemented => 99,
507            builtins::ExitCode::Custom(code) => code,
508            builtins::ExitCode::ExitShell(code) => return Ok(CommandSpawnResult::ExitShell(code)),
509            builtins::ExitCode::ReturnFromFunctionOrScript(code) => {
510                return Ok(CommandSpawnResult::ReturnFromFunctionOrScript(code));
511            }
512            builtins::ExitCode::BreakLoop(count) => {
513                return Ok(CommandSpawnResult::BreakLoop(count));
514            }
515            builtins::ExitCode::ContinueLoop(count) => {
516                return Ok(CommandSpawnResult::ContinueLoop(count));
517            }
518        },
519        Err(e @ error::Error::Unimplemented(..)) => {
520            tracing::warn!(target: trace_categories::UNIMPLEMENTED, "{e}");
521            1
522        }
523        Err(e @ error::Error::UnimplementedAndTracked(..)) => {
524            tracing::warn!(target: trace_categories::UNIMPLEMENTED, "{e}");
525            1
526        }
527        Err(e) => {
528            tracing::error!("{e}");
529            1
530        }
531    };
532
533    Ok(CommandSpawnResult::ImmediateExit(exit_code))
534}
535
536pub(crate) async fn invoke_shell_function(
537    function_definition: Arc<ast::FunctionDefinition>,
538    mut context: ExecutionContext<'_>,
539    args: &[CommandArg],
540) -> Result<CommandSpawnResult, error::Error> {
541    let ast::FunctionBody(body, redirects) = &function_definition.body;
542
543    // Apply any redirects specified at function definition-time.
544    if let Some(redirects) = redirects {
545        for redirect in &redirects.0 {
546            interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
547        }
548    }
549
550    // Temporarily replace positional parameters.
551    let prior_positional_params = std::mem::take(&mut context.shell.positional_parameters);
552    context.shell.positional_parameters = args.iter().map(|a| a.to_string()).collect();
553
554    // Pass through open files.
555    let params = context.params.clone();
556
557    // Note that we're going deeper. Once we do this, we need to make sure we don't bail early
558    // before "exiting" the function.
559    context
560        .shell
561        .enter_function(context.command_name.as_str(), &function_definition)?;
562
563    // Invoke the function.
564    let result = body.execute(context.shell, &params).await;
565
566    // Clean up parameters so any owned files are closed.
567    drop(params);
568
569    // We've come back out, reflect it.
570    context.shell.leave_function()?;
571
572    // Restore positional parameters.
573    context.shell.positional_parameters = prior_positional_params;
574
575    // Get the actual execution result from the body of the function.
576    let result = result?;
577
578    // Report back the exit code, and honor any requests to exit the whole shell.
579    Ok(if result.exit_shell {
580        CommandSpawnResult::ExitShell(result.exit_code)
581    } else {
582        CommandSpawnResult::ImmediateExit(result.exit_code)
583    })
584}
585
586pub(crate) async fn invoke_command_in_subshell_and_get_output(
587    shell: &mut Shell,
588    params: &ExecutionParameters,
589    s: String,
590) -> Result<String, error::Error> {
591    // Instantiate a subshell to run the command in.
592    let mut subshell = shell.clone();
593
594    // Get our own set of parameters we can customize and use.
595    let mut params = params.clone();
596    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
597
598    // Set up pipe so we can read the output.
599    let (reader, writer) = sys::pipes::pipe()?;
600    params
601        .open_files
602        .files
603        .insert(1, openfiles::OpenFile::PipeWriter(writer));
604
605    // Run the command.
606    let result = subshell.run_string(s, &params).await?;
607
608    // Make sure the subshell and params are closed; among other things, this
609    // ensures they're not holding onto the write end of the pipe.
610    drop(subshell);
611    drop(params);
612
613    // Store the status.
614    shell.last_exit_status = result.exit_code;
615
616    // Extract output.
617    let output_str = std::io::read_to_string(reader)?;
618
619    Ok(output_str)
620}