1use 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
27pub enum CommandWaitResult {
29 CommandCompleted(ExecutionResult),
31 CommandStopped(ExecutionResult, processes::ChildProcess),
33}
34
35pub struct ExecutionContext<'a, SE: ShellExtensions = extensions::DefaultShellExtensions> {
37 pub shell: &'a mut Shell<SE>,
39 pub command_name: String,
41 pub params: ExecutionParameters,
43}
44
45impl<SE: ShellExtensions> ExecutionContext<'_, SE> {
46 pub fn stdin(&self) -> impl std::io::Read + 'static {
48 self.params.stdin(self.shell)
49 }
50
51 pub fn stdout(&self) -> impl std::io::Write + 'static {
53 self.params.stdout(self.shell)
54 }
55
56 pub fn stderr(&self) -> impl std::io::Write + 'static {
58 self.params.stderr(self.shell)
59 }
60
61 pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
68 self.params.try_fd(self.shell, fd)
69 }
70
71 pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
73 self.params.iter_fds(self.shell)
74 }
75}
76
77#[derive(Clone, Debug)]
79pub enum CommandArg {
80 String(String),
82 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
126pub enum ShellForCommand<'a, SE: extensions::ShellExtensions> {
128 ParentShell(&'a mut Shell<SE>),
131 OwnedShell {
133 target: Box<Shell<SE>>,
135 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#[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 cmd.arg0(argv0);
185
186 cmd.args(args);
188
189 cmd.current_dir(context.shell.working_dir());
191
192 cmd.env_clear();
194
195 if !empty_env {
197 for (k, v) in context.shell.env().iter_exported() {
198 if v.value().is_set() {
202 cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
203 }
204 }
205 cmd.env("_", command_name);
207 }
208
209 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 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 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 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 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 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 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
281pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
283 shell: ShellForCommand<'a, SE>,
285
286 pub params: ExecutionParameters,
288
289 pub command_name: String,
291
292 pub args: Vec<CommandArg>,
294
295 pub use_functions: bool,
298
299 pub path_dirs: Option<Vec<PathBuf>>,
302
303 pub process_group_id: Option<i32>,
306
307 pub argv0: Option<String>,
310
311 #[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 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 #[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 let builtin = self.shell.builtins().get(&self.command_name).cloned();
358
359 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 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 let Some(builtin) = builtin {
384 if !builtin.disabled {
385 return self.execute_via_builtin(builtin).await;
386 }
387 }
388
389 if !sys::fs::contains_path_separator(&self.command_name) {
391 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 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 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 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 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 let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
516
517 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 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 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 let child_stdin_is_terminal = context
581 .try_fd(openfiles::OpenFiles::STDIN_FD)
582 .is_some_and(|f| f.is_terminal());
583
584 let new_pg = matches!(
586 context.params.process_group_policy,
587 ProcessGroupPolicy::NewProcessGroup
588 );
589
590 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, )?;
602
603 if new_pg {
605 if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
607 cmd.lead_session();
609 } else {
610 cmd.process_group(0);
612 if child_stdin_is_terminal {
613 cmd.take_foreground();
614 }
615 }
616 } else {
617 if let Some(pgid) = process_group_id {
619 cmd.process_group(pgid);
620 }
621 }
622
623 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 #[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 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 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 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 let params = context.params.clone();
716
717 context.shell.enter_function(
720 context.command_name.as_str(),
721 &function,
722 positional_args,
723 &context.params,
724 )?;
725
726 let result = body.execute(context.shell, ¶ms).await;
728
729 drop(params);
731
732 context.shell.leave_function()?;
734
735 let mut result = result?;
737
738 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 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 let mut subshell = shell.clone();
760
761 if !shell.options().command_subst_inherits_errexit {
764 subshell.options_mut().exit_on_nonzero_command_exit = false;
765 }
766
767 let mut params = params.clone();
769 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
770
771 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 let run_result = cmd_join_handle.await?;
783 let cmd_result = run_result?;
784
785 shell.set_last_exit_status(cmd_result.exit_code.into());
787
788 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 let parse_result = shell.parse_string(command);
801
802 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 let source_info = crate::SourceInfo::from("main");
815
816 shell
818 .run_parsed_result(parse_result, &source_info, ¶ms)
819 .await
820}
821
822fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
825 let [complete] = program.complete_commands.as_slice() else {
827 return None;
828 };
829
830 let ast::CompoundList(items) = complete;
832 let [item] = items.as_slice() else {
833 return None;
834 };
835
836 let and_or = &item.0;
838 if !and_or.additional.is_empty() {
839 return None;
840 }
841
842 let pipeline = &and_or.first;
844 if pipeline.bang {
845 return None;
846 }
847
848 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
850 return None;
851 };
852
853 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
855 return None;
856 }
857
858 let prefix = simple_cmd.prefix.as_ref()?;
860 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
861 return None;
862 };
863
864 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}