1use 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::{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, pathsearch, processes, sys,
20 trace_categories, traps, variables,
21};
22
23pub enum CommandWaitResult {
25 CommandCompleted(ExecutionResult),
27 CommandStopped(ExecutionResult, processes::ChildProcess),
29}
30
31pub struct ExecutionContext<'a, SE: ShellExtensions = extensions::DefaultShellExtensions> {
33 pub shell: &'a mut Shell<SE>,
35 pub command_name: String,
37 pub params: ExecutionParameters,
39}
40
41impl<SE: ShellExtensions> ExecutionContext<'_, SE> {
42 pub fn stdin(&self) -> impl std::io::Read + 'static {
44 self.params.stdin(self.shell)
45 }
46
47 pub fn stdout(&self) -> impl std::io::Write + 'static {
49 self.params.stdout(self.shell)
50 }
51
52 pub fn stderr(&self) -> impl std::io::Write + 'static {
54 self.params.stderr(self.shell)
55 }
56
57 pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
64 self.params.try_fd(self.shell, fd)
65 }
66
67 pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
69 self.params.iter_fds(self.shell)
70 }
71}
72
73#[derive(Clone, Debug)]
75pub enum CommandArg {
76 String(String),
78 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
129pub enum ShellForCommand<'a, SE: extensions::ShellExtensions> {
131 ParentShell(&'a mut Shell<SE>),
134 OwnedShell {
136 target: Box<Shell<SE>>,
138 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#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
178pub fn compose_std_command<S: AsRef<OsStr>, SE: extensions::ShellExtensions>(
179 context: &ExecutionContext<'_, SE>,
180 command_name: &str,
181 argv0: &str,
182 args: &[S],
183 empty_env: bool,
184) -> Result<std::process::Command, error::Error> {
185 let mut cmd = std::process::Command::new(command_name);
186
187 cmd.arg0(argv0);
190
191 cmd.args(args);
193
194 cmd.current_dir(context.shell.working_dir());
196
197 cmd.env_clear();
199
200 if !empty_env {
202 for (k, v) in context.shell.env().iter_exported() {
203 if v.value().is_set() {
207 cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
208 }
209 }
210 cmd.env("_", command_name);
212 }
213
214 if !empty_env {
216 for (func_name, registration) in context.shell.funcs().iter() {
217 if registration.is_exported() {
218 let var_name = std::format!("BASH_FUNC_{func_name}%%");
219 let value = std::format!("() {}", registration.definition().body);
220 cmd.env(var_name, value);
221 }
222 }
223 }
224
225 match context.try_fd(OpenFiles::STDIN_FD) {
227 Some(OpenFile::Stdin(_)) | None => (),
228 Some(stdin_file) => {
229 let as_stdio: Stdio = stdin_file.try_into()?;
230 cmd.stdin(as_stdio);
231 }
232 }
233
234 match context.try_fd(OpenFiles::STDOUT_FD) {
236 Some(OpenFile::Stdout(_)) | None => (),
237 Some(stdout_file) => {
238 let as_stdio: Stdio = stdout_file.try_into()?;
239 cmd.stdout(as_stdio);
240 }
241 }
242
243 match context.try_fd(OpenFiles::STDERR_FD) {
245 Some(OpenFile::Stderr(_)) | None => {}
246 Some(stderr_file) => {
247 let as_stdio: Stdio = stderr_file.try_into()?;
248 cmd.stderr(as_stdio);
249 }
250 }
251
252 let other_files = context.iter_fds().filter(|(fd, _)| {
254 *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
255 });
256 cmd.inject_fds(other_files)?;
257
258 Ok(cmd)
259}
260
261pub(crate) async fn on_preexecute(
262 cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
263) -> Result<(), error::Error> {
264 let full_cmd = format_command_args(&cmd.args);
267 cmd.shell.env_mut().update_or_add(
268 "BASH_COMMAND",
269 variables::ShellValueLiteral::Scalar(full_cmd),
270 |_| Ok(()),
271 env::EnvironmentLookup::Anywhere,
272 env::EnvironmentScope::Global,
273 )?;
274
275 if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
277 let _ = cmd
278 .shell
279 .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
280 .await?;
281 }
282
283 Ok(())
284}
285
286fn format_command_args(args: &[CommandArg]) -> String {
287 let capacity = args
288 .iter()
289 .map(CommandArg::to_string_len_hint)
290 .sum::<usize>()
291 + args.len().saturating_sub(1);
292 let mut result = String::with_capacity(capacity);
293
294 for (i, arg) in args.iter().enumerate() {
295 if i > 0 {
296 result.push(' ');
297 }
298 let _ = write!(result, "{arg}");
299 }
300
301 result
302}
303
304pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
306 shell: ShellForCommand<'a, SE>,
308
309 pub params: ExecutionParameters,
311
312 pub command_name: String,
314
315 pub args: Vec<CommandArg>,
317
318 pub use_functions: bool,
322
323 pub path_dirs: Option<Vec<PathBuf>>,
326
327 pub process_group_id: Option<i32>,
330
331 pub argv0: Option<String>,
334}
335
336impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
337 pub fn new<I>(
346 shell: ShellForCommand<'a, SE>,
347 params: ExecutionParameters,
348 command_name: String,
349 args: I,
350 ) -> Self
351 where
352 I: IntoIterator<Item = CommandArg>,
353 {
354 Self {
355 shell,
356 params,
357 command_name,
358 args: args.into_iter().collect(),
359 use_functions: true,
360 path_dirs: None,
361 process_group_id: None,
362 argv0: None,
363 }
364 }
365
366 #[allow(
372 clippy::missing_panics_doc,
373 reason = "these unwrap calls should not panic"
374 )]
375 pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
376 let builtin = self.shell.builtins().get(&self.command_name).cloned();
378
379 if self.shell.options().posix_mode
382 && builtin
383 .as_ref()
384 .is_some_and(|r| !r.disabled && r.special_builtin)
385 {
386 #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
387 let builtin = builtin.unwrap();
388 return self.execute_via_builtin(builtin).await;
389 }
390
391 if self.use_functions {
394 if let Some(func_registration) =
395 self.shell.funcs().get(self.command_name.as_str()).cloned()
396 {
397 return self.execute_via_function(func_registration).await;
398 }
399 }
400
401 if let Some(builtin) = builtin {
404 if !builtin.disabled {
405 return self.execute_via_builtin(builtin).await;
406 }
407 }
408
409 if !sys::fs::contains_path_separator(&self.command_name) {
412 let path = if let Some(path_dirs) = &self.path_dirs {
416 pathsearch::resolve_command(path_dirs, self.command_name.as_str())
417 } else {
418 self.shell
419 .resolve_command_in_path_using_cache(&self.command_name)
420 };
421
422 if let Some(path) = path {
423 self.execute_via_external(&path)
424 } else {
425 let last_arg = Self::take_last_arg(&self.args);
428 self.shell.update_last_arg_variable(last_arg);
429
430 Err(ErrorKind::CommandNotFound(self.command_name).into())
431 }
432 } else {
433 let command_name = sys::fs::resolve_executable_path(
434 PathBuf::from(&self.command_name),
435 self.shell.working_dir(),
436 );
437 self.execute_via_external(command_name.as_path())
438 }
439 }
440
441 fn take_last_arg(args: &[CommandArg]) -> Option<String> {
444 args.last().map(ToString::to_string)
445 }
446
447 async fn execute_via_builtin(
448 self,
449 builtin: builtins::Registration<SE>,
450 ) -> Result<ExecutionSpawnResult, error::Error> {
451 match self.shell {
452 ShellForCommand::OwnedShell { target, .. } => {
453 Ok(Self::execute_via_builtin_in_owned_shell(
454 *target,
455 self.params,
456 builtin,
457 self.command_name,
458 self.args,
459 )
460 .await)
461 }
462 ShellForCommand::ParentShell(..) => {
463 self.execute_via_builtin_in_parent_shell(builtin).await
464 }
465 }
466 }
467
468 fn execute_via_builtin_in_owned_shell(
471 mut shell: Shell<SE>,
472 params: ExecutionParameters,
473 builtin: builtins::Registration<SE>,
474 command_name: String,
475 args: Vec<CommandArg>,
476 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
477 let last_arg = Self::take_last_arg(&args);
478 let task = sys::tasks::spawn_blocking(async move {
481 let cmd_context = ExecutionContext {
482 shell: &mut shell,
483 command_name,
484 params,
485 };
486
487 let result = execute_builtin_command(&builtin, cmd_context, args).await;
488
489 shell.update_last_arg_variable(last_arg);
491
492 result
493 });
494
495 async move { ExecutionSpawnResult::StartedTask(task.await) }
496 }
497
498 async fn execute_via_builtin_in_parent_shell(
499 self,
500 builtin: builtins::Registration<SE>,
501 ) -> Result<ExecutionSpawnResult, error::Error> {
502 let mut shell = self.shell;
503 let last_arg = Self::take_last_arg(&self.args);
504
505 let cmd_context = ExecutionContext {
506 shell: &mut shell,
507 command_name: self.command_name,
508 params: self.params,
509 };
510
511 let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
512
513 shell.update_last_arg_variable(last_arg);
515
516 let result = result?;
517
518 Ok(result.into())
519 }
520
521 async fn execute_via_function(
522 self,
523 func_registration: functions::Registration,
524 ) -> Result<ExecutionSpawnResult, error::Error> {
525 match self.shell {
526 ShellForCommand::OwnedShell { target, .. } => {
527 Ok(Self::execute_via_function_in_owned_shell(
528 *target,
529 self.params,
530 func_registration,
531 self.command_name,
532 self.args,
533 )
534 .await)
535 }
536 ShellForCommand::ParentShell(..) => {
537 self.execute_via_function_in_parent_shell(func_registration)
538 .await
539 }
540 }
541 }
542
543 fn execute_via_function_in_owned_shell(
559 mut shell: Shell<SE>,
560 params: ExecutionParameters,
561 func_registration: functions::Registration,
562 command_name: String,
563 args: Vec<CommandArg>,
564 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
565 let last_arg = Self::take_last_arg(&args);
566 let task = sys::tasks::spawn_blocking(async move {
567 let cmd_context = ExecutionContext {
568 shell: &mut shell,
569 command_name,
570 params,
571 };
572
573 let spawn_result =
574 invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
575
576 shell.update_last_arg_variable(last_arg);
578
579 match spawn_result? {
580 ExecutionSpawnResult::Completed(result) => Ok(result),
581 ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
582 error::unimp("invoke_shell_function returned an unexpected spawn result")
586 }
587 }
588 });
589
590 async move { ExecutionSpawnResult::StartedTask(task.await) }
591 }
592
593 async fn execute_via_function_in_parent_shell(
594 self,
595 func_registration: functions::Registration,
596 ) -> Result<ExecutionSpawnResult, error::Error> {
597 let mut shell = self.shell;
598 let last_arg = Self::take_last_arg(&self.args);
599
600 let cmd_context = ExecutionContext {
601 shell: &mut shell,
602 command_name: self.command_name,
603 params: self.params,
604 };
605
606 let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
608
609 shell.update_last_arg_variable(last_arg);
614
615 result
616 }
617
618 fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
619 let mut shell = self.shell;
620 let last_arg = Self::take_last_arg(&self.args);
621
622 let cmd_context = ExecutionContext {
623 shell: &mut shell,
624 command_name: self.command_name,
625 params: self.params,
626 };
627
628 let resolved_path = path.to_string_lossy();
629 let result = execute_external_command(
630 cmd_context,
631 resolved_path.as_ref(),
632 self.process_group_id,
633 self.argv0.as_deref(),
634 &self.args[1..],
635 );
636
637 shell.update_last_arg_variable(last_arg);
639
640 result
641 }
642}
643
644pub(crate) fn execute_external_command(
645 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
646 executable_path: &str,
647 process_group_id: Option<i32>,
648 argv0_override: Option<&str>,
649 args: &[CommandArg],
650) -> Result<ExecutionSpawnResult, error::Error> {
651 let cmd_args = args
653 .iter()
654 .filter_map(|e| {
655 if let CommandArg::String(s) = e {
656 Some(s)
657 } else {
658 None
659 }
660 })
661 .collect::<Vec<_>>();
662
663 let child_stdin_is_terminal = context
666 .try_fd(openfiles::OpenFiles::STDIN_FD)
667 .is_some_and(|f| f.is_terminal());
668
669 let new_pg = matches!(
671 context.params.process_group_policy,
672 ProcessGroupPolicy::NewProcessGroup
673 );
674
675 let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
680 #[allow(unused_mut, reason = "only mutated on unix platforms")]
681 let mut cmd = compose_std_command(
682 &context,
683 executable_path,
684 argv0,
685 cmd_args.as_slice(),
686 false, )?;
688
689 if new_pg {
691 if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
694 cmd.lead_session();
696 } else {
697 cmd.process_group(0);
699 if child_stdin_is_terminal {
700 cmd.take_foreground();
701 }
702 }
703 } else {
704 if let Some(pgid) = process_group_id {
706 cmd.process_group(pgid);
707 }
708 }
709
710 tracing::debug!(
712 target: trace_categories::COMMANDS,
713 "Spawning: cmd='{} {}'",
714 cmd.get_program().to_string_lossy().to_string(),
715 cmd.get_args()
716 .map(|a| a.to_string_lossy().to_string())
717 .join(" ")
718 );
719
720 match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
721 Ok(child) => {
722 #[expect(clippy::cast_possible_wrap)]
724 let pid = child.id().map(|id| id as i32);
725 let mut actual_pgid = process_group_id;
726 if let Some(pid) = &pid {
727 if new_pg {
728 actual_pgid = Some(*pid);
729 }
730 } else {
731 tracing::warn!("could not retrieve pid for child process");
732 }
733
734 Ok(ExecutionSpawnResult::StartedProcess(
735 processes::ChildProcess::new(child, pid, actual_pgid),
736 ))
737 }
738 Err(spawn_err) => {
739 if context.shell.options().interactive {
740 sys::terminal::move_self_to_foreground()?;
741 }
742
743 if spawn_err.kind() == std::io::ErrorKind::NotFound {
744 if !context.shell.working_dir().exists() {
745 Err(
746 error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned())
747 .into(),
748 )
749 } else {
750 Err(error::ErrorKind::CommandNotFound(context.command_name).into())
751 }
752 } else {
753 Err(
754 error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err)
755 .into(),
756 )
757 }
758 }
759 }
760}
761
762async fn execute_builtin_command<SE: extensions::ShellExtensions>(
763 builtin: &builtins::Registration<SE>,
764 context: ExecutionContext<'_, SE>,
765 args: Vec<CommandArg>,
766) -> Result<ExecutionResult, error::Error> {
767 let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
770
771 match (builtin.execute_func)(context, args).await {
772 Ok(result) => Ok(result),
773 Err(e) => {
774 if let Some(io_err) = e.as_io_error() {
777 if io_err.kind() == std::io::ErrorKind::BrokenPipe {
778 return Ok(ExecutionExitCode::from(io_err).into());
779 }
780 }
781
782 Err(if mark_errors_fatal { e.into_fatal() } else { e })
783 }
784 }
785}
786
787pub(crate) async fn invoke_shell_function(
788 function: functions::Registration,
789 mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
790 args: &[CommandArg],
791) -> Result<ExecutionSpawnResult, error::Error> {
792 let ast::FunctionBody(body, redirects) = &function.definition().body;
793
794 if let Some(redirects) = redirects {
796 for redirect in &redirects.0 {
797 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
798 }
799 }
800
801 let positional_args = args.iter().map(|a| a.to_string());
802
803 let mut shell =
806 context
807 .shell
808 .enter_function(context.command_name.as_str(), &function, positional_args)?;
809
810 let result = body.execute(&mut shell, &context.params).await;
817
818 drop(shell);
820
821 let mut result = result?;
823
824 match result.next_control_flow {
826 ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
827 return error::unimp("break or continue returned from function invocation");
828 }
829 ExecutionControlFlow::ReturnFromFunctionOrScript => {
830 result.next_control_flow = ExecutionControlFlow::Normal;
832 }
833 _ => {}
834 }
835
836 Ok(result.into())
837}
838
839pub(crate) async fn invoke_command_in_subshell_and_get_output(
840 shell: &mut Shell<impl extensions::ShellExtensions>,
841 params: &ExecutionParameters,
842 s: String,
843) -> Result<String, error::Error> {
844 let mut subshell = shell.clone();
846
847 if !shell.options().command_subst_inherits_errexit {
851 subshell.options_mut().exit_on_nonzero_command_exit = false;
852 }
853
854 let mut params = params.clone();
856 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
857
858 let (reader, writer) = sys::pipes::pipe()?;
860 params.set_fd(OpenFiles::STDOUT_FD, writer.into());
861
862 let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
863
864 let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
865
866 let output_str = async_reader.read_to_string().await?;
867
868 let run_result = cmd_join_handle.await?;
870 let cmd_result = run_result?;
871
872 shell.set_last_exit_status(cmd_result.exit_code.into());
874
875 Ok(output_str)
879}
880
881async fn run_substitution_command(
882 mut shell: Shell<impl extensions::ShellExtensions>,
883 mut params: ExecutionParameters,
884 command: String,
885) -> Result<ExecutionResult, error::Error> {
886 let parse_result = shell.parse_string(command);
888
889 if let Ok(program) = &parse_result {
893 if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
894 interp::setup_redirect(&mut shell, &mut params, redir).await?;
895 std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
896 return Ok(ExecutionResult::new(0));
897 }
898 }
899
900 let source_info = crate::SourceInfo::from("main");
902
903 shell
905 .run_parsed_result(parse_result, &source_info, ¶ms)
906 .await
907}
908
909fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
912 let [complete] = program.complete_commands.as_slice() else {
914 return None;
915 };
916
917 let ast::CompoundList(items) = complete;
919 let [item] = items.as_slice() else {
920 return None;
921 };
922
923 let and_or = &item.0;
925 if !and_or.additional.is_empty() {
926 return None;
927 }
928
929 let pipeline = &and_or.first;
931 if pipeline.bang {
932 return None;
933 }
934
935 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
937 return None;
938 };
939
940 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
942 return None;
943 }
944
945 let prefix = simple_cmd.prefix.as_ref()?;
947 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
948 return None;
949 };
950
951 match redir {
953 ast::IoRedirect::File(
954 fd,
955 ast::IoFileRedirectKind::Read,
956 ast::IoFileRedirectTarget::Filename(..),
957 ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
958 _ => None,
959 }
960}