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")]
185pub fn compose_std_command<S: AsRef<OsStr>, SE: extensions::ShellExtensions>(
186 context: &ExecutionContext<'_, SE>,
187 command_name: &str,
188 argv0: &str,
189 args: &[S],
190 empty_env: bool,
191) -> Result<std::process::Command, error::Error> {
192 let mut cmd = std::process::Command::new(command_name);
193
194 cmd.arg0(argv0);
197
198 cmd.args(args);
200
201 cmd.current_dir(context.shell.working_dir());
203
204 cmd.env_clear();
206
207 if !empty_env {
209 for (k, v) in context.shell.env().iter_exported() {
210 if v.value().is_set() {
214 cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
215 }
216 }
217 cmd.env("_", command_name);
219 }
220
221 if !empty_env {
223 for (func_name, registration) in context.shell.funcs().iter() {
224 if registration.is_exported() {
225 let var_name = std::format!("BASH_FUNC_{func_name}%%");
226 let value = std::format!("() {}", registration.definition().body);
227 cmd.env(var_name, value);
228 }
229 }
230 }
231
232 match context.try_fd(OpenFiles::STDIN_FD) {
234 Some(OpenFile::Stdin(_)) | None => (),
235 Some(stdin_file) => {
236 let as_stdio: Stdio = stdin_file.try_into()?;
237 cmd.stdin(as_stdio);
238 }
239 }
240
241 match context.try_fd(OpenFiles::STDOUT_FD) {
243 Some(OpenFile::Stdout(_)) | None => (),
244 Some(stdout_file) => {
245 let as_stdio: Stdio = stdout_file.try_into()?;
246 cmd.stdout(as_stdio);
247 }
248 }
249
250 match context.try_fd(OpenFiles::STDERR_FD) {
252 Some(OpenFile::Stderr(_)) | None => {}
253 Some(stderr_file) => {
254 let as_stdio: Stdio = stderr_file.try_into()?;
255 cmd.stderr(as_stdio);
256 }
257 }
258
259 let other_files = context.iter_fds().filter(|(fd, _)| {
261 *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
262 });
263 cmd.inject_fds(other_files)?;
264
265 Ok(cmd)
266}
267
268pub(crate) async fn on_preexecute(
269 cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
270) -> Result<(), error::Error> {
271 let full_cmd = format_command_args(&cmd.args);
274 cmd.shell.env_mut().update_or_add(
275 "BASH_COMMAND",
276 variables::ShellValueLiteral::Scalar(full_cmd),
277 |_| Ok(()),
278 env::EnvironmentLookup::Anywhere,
279 env::EnvironmentScope::Global,
280 )?;
281
282 if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
284 let _ = cmd
285 .shell
286 .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
287 .await?;
288 }
289
290 Ok(())
291}
292
293fn format_command_args(args: &[CommandArg]) -> String {
294 let capacity = args
295 .iter()
296 .map(CommandArg::to_string_len_hint)
297 .sum::<usize>()
298 + args.len().saturating_sub(1);
299 let mut result = String::with_capacity(capacity);
300
301 for (i, arg) in args.iter().enumerate() {
302 if i > 0 {
303 result.push(' ');
304 }
305 let _ = write!(result, "{arg}");
306 }
307
308 result
309}
310
311pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
313 shell: ShellForCommand<'a, SE>,
315
316 pub params: ExecutionParameters,
318
319 pub command_name: String,
321
322 pub args: Vec<CommandArg>,
324
325 pub use_functions: bool,
329
330 pub path_dirs: Option<Vec<PathBuf>>,
333
334 pub process_group_id: Option<i32>,
337
338 pub argv0: Option<String>,
341}
342
343impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
344 pub fn new<I>(
353 shell: ShellForCommand<'a, SE>,
354 params: ExecutionParameters,
355 command_name: String,
356 args: I,
357 ) -> Self
358 where
359 I: IntoIterator<Item = CommandArg>,
360 {
361 Self {
362 shell,
363 params,
364 command_name,
365 args: args.into_iter().collect(),
366 use_functions: true,
367 path_dirs: None,
368 process_group_id: None,
369 argv0: None,
370 }
371 }
372
373 pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
393 let builtin = self.shell.builtins().get(&self.command_name).cloned();
395
396 if self.shell.options().posix_mode
399 && builtin
400 .as_ref()
401 .is_some_and(|r| !r.disabled && r.special_builtin)
402 {
403 #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
404 let builtin = builtin.unwrap();
405 return self.execute_via_builtin(builtin).await;
406 }
407
408 if self.use_functions {
411 if let Some(func_registration) =
412 self.shell.funcs().get(self.command_name.as_str()).cloned()
413 {
414 return self.execute_via_function(func_registration).await;
415 }
416 }
417
418 if let Some(builtin) = builtin {
421 if !builtin.disabled {
422 return self.execute_via_builtin(builtin).await;
423 }
424 }
425
426 if !sys::fs::contains_path_separator(&self.command_name) {
429 let path = if let Some(path_dirs) = &self.path_dirs {
433 pathsearch::resolve_command(path_dirs, self.command_name.as_str())
434 } else {
435 self.shell
436 .resolve_command_in_path_using_cache(&self.command_name)
437 };
438
439 if let Some(path) = path {
440 self.execute_via_external(&path)
441 } else {
442 let last_arg = Self::take_last_arg(&self.args);
445 self.shell.update_last_arg_variable(last_arg);
446
447 Err(ErrorKind::CommandNotFound(self.command_name).into())
448 }
449 } else {
450 let command_name = sys::fs::resolve_executable_path(
451 PathBuf::from(&self.command_name),
452 self.shell.working_dir(),
453 );
454 self.execute_via_external(command_name.as_path())
455 }
456 }
457
458 fn take_last_arg(args: &[CommandArg]) -> Option<String> {
461 args.last().map(ToString::to_string)
462 }
463
464 async fn execute_via_builtin(
465 self,
466 builtin: builtins::Registration<SE>,
467 ) -> Result<ExecutionSpawnResult, error::Error> {
468 match self.shell {
469 ShellForCommand::OwnedShell { target, .. } => {
470 Ok(Self::execute_via_builtin_in_owned_shell(
471 *target,
472 self.params,
473 builtin,
474 self.command_name,
475 self.args,
476 )
477 .await)
478 }
479 ShellForCommand::ParentShell(..) => {
480 self.execute_via_builtin_in_parent_shell(builtin).await
481 }
482 }
483 }
484
485 fn execute_via_builtin_in_owned_shell(
488 mut shell: Shell<SE>,
489 params: ExecutionParameters,
490 builtin: builtins::Registration<SE>,
491 command_name: String,
492 args: Vec<CommandArg>,
493 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
494 let last_arg = Self::take_last_arg(&args);
495 let task = sys::tasks::spawn_blocking(async move {
498 let cmd_context = ExecutionContext {
499 shell: &mut shell,
500 command_name,
501 params,
502 };
503
504 let result = execute_builtin_command(&builtin, cmd_context, args).await;
505
506 shell.update_last_arg_variable(last_arg);
508
509 result
510 });
511
512 async move { ExecutionSpawnResult::StartedTask(task.await) }
513 }
514
515 async fn execute_via_builtin_in_parent_shell(
516 self,
517 builtin: builtins::Registration<SE>,
518 ) -> Result<ExecutionSpawnResult, error::Error> {
519 let mut shell = self.shell;
520 let last_arg = Self::take_last_arg(&self.args);
521
522 let cmd_context = ExecutionContext {
523 shell: &mut shell,
524 command_name: self.command_name,
525 params: self.params,
526 };
527
528 let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
529
530 shell.update_last_arg_variable(last_arg);
532
533 let result = result?;
534
535 Ok(result.into())
536 }
537
538 async fn execute_via_function(
539 self,
540 func_registration: functions::Registration,
541 ) -> Result<ExecutionSpawnResult, error::Error> {
542 match self.shell {
543 ShellForCommand::OwnedShell { target, .. } => {
544 Ok(Self::execute_via_function_in_owned_shell(
545 *target,
546 self.params,
547 func_registration,
548 self.command_name,
549 self.args,
550 )
551 .await)
552 }
553 ShellForCommand::ParentShell(..) => {
554 self.execute_via_function_in_parent_shell(func_registration)
555 .await
556 }
557 }
558 }
559
560 fn execute_via_function_in_owned_shell(
576 mut shell: Shell<SE>,
577 params: ExecutionParameters,
578 func_registration: functions::Registration,
579 command_name: String,
580 args: Vec<CommandArg>,
581 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
582 let last_arg = Self::take_last_arg(&args);
583 let task = sys::tasks::spawn_blocking(async move {
584 let cmd_context = ExecutionContext {
585 shell: &mut shell,
586 command_name,
587 params,
588 };
589
590 let spawn_result =
591 invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
592
593 shell.update_last_arg_variable(last_arg);
595
596 match spawn_result? {
597 ExecutionSpawnResult::Completed(result) => Ok(result),
598 ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
599 error::unimp("invoke_shell_function returned an unexpected spawn result")
603 }
604 }
605 });
606
607 async move { ExecutionSpawnResult::StartedTask(task.await) }
608 }
609
610 async fn execute_via_function_in_parent_shell(
611 self,
612 func_registration: functions::Registration,
613 ) -> Result<ExecutionSpawnResult, error::Error> {
614 let mut shell = self.shell;
615 let last_arg = Self::take_last_arg(&self.args);
616
617 let cmd_context = ExecutionContext {
618 shell: &mut shell,
619 command_name: self.command_name,
620 params: self.params,
621 };
622
623 let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
625
626 shell.update_last_arg_variable(last_arg);
631
632 result
633 }
634
635 fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
636 let mut shell = self.shell;
637 let last_arg = Self::take_last_arg(&self.args);
638
639 let cmd_context = ExecutionContext {
640 shell: &mut shell,
641 command_name: self.command_name,
642 params: self.params,
643 };
644
645 let resolved_path = path.to_string_lossy();
646 let result = execute_external_command(
647 cmd_context,
648 resolved_path.as_ref(),
649 self.process_group_id,
650 self.argv0.as_deref(),
651 &self.args[1..],
652 );
653
654 shell.update_last_arg_variable(last_arg);
656
657 result
658 }
659}
660
661pub(crate) fn execute_external_command(
662 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
663 executable_path: &str,
664 process_group_id: Option<i32>,
665 argv0_override: Option<&str>,
666 args: &[CommandArg],
667) -> Result<ExecutionSpawnResult, error::Error> {
668 let cmd_args = args
670 .iter()
671 .filter_map(|e| {
672 if let CommandArg::String(s) = e {
673 Some(s)
674 } else {
675 None
676 }
677 })
678 .collect::<Vec<_>>();
679
680 let child_stdin_is_terminal = context
683 .try_fd(openfiles::OpenFiles::STDIN_FD)
684 .is_some_and(|f| f.is_terminal());
685
686 let new_pg = matches!(
688 context.params.process_group_policy,
689 ProcessGroupPolicy::NewProcessGroup
690 );
691
692 let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
697 #[allow(unused_mut, reason = "only mutated on unix platforms")]
698 let mut cmd = compose_std_command(
699 &context,
700 executable_path,
701 argv0,
702 cmd_args.as_slice(),
703 false, )?;
705
706 if new_pg {
708 if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
711 cmd.lead_session();
713 } else {
714 cmd.process_group(0);
716 if child_stdin_is_terminal {
717 cmd.take_foreground();
718 }
719 }
720 } else {
721 if let Some(pgid) = process_group_id {
723 cmd.process_group(pgid);
724 }
725 }
726
727 tracing::debug!(
729 target: trace_categories::COMMANDS,
730 "Spawning: cmd='{} {}'",
731 cmd.get_program().to_string_lossy().to_string(),
732 cmd.get_args()
733 .map(|a| a.to_string_lossy().to_string())
734 .join(" ")
735 );
736
737 match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
738 Ok(child) => {
739 #[expect(clippy::cast_possible_wrap)]
741 let pid = child.id().map(|id| id as i32);
742 let mut actual_pgid = process_group_id;
743 if let Some(pid) = &pid {
744 if new_pg {
745 actual_pgid = Some(*pid);
746 }
747 } else {
748 tracing::warn!("could not retrieve pid for child process");
749 }
750
751 Ok(ExecutionSpawnResult::StartedProcess(
752 processes::ChildProcess::new(child, pid, actual_pgid),
753 ))
754 }
755 Err(spawn_err) => {
756 if context.shell.options().interactive {
757 sys::terminal::move_self_to_foreground()?;
758 }
759
760 if spawn_err.kind() == std::io::ErrorKind::NotFound {
761 if !context.shell.working_dir().exists() {
762 Err(
763 error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned())
764 .into(),
765 )
766 } else {
767 Err(error::ErrorKind::CommandNotFound(context.command_name).into())
768 }
769 } else {
770 Err(
771 error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err)
772 .into(),
773 )
774 }
775 }
776 }
777}
778
779async fn execute_builtin_command<SE: extensions::ShellExtensions>(
780 builtin: &builtins::Registration<SE>,
781 context: ExecutionContext<'_, SE>,
782 args: Vec<CommandArg>,
783) -> Result<ExecutionResult, error::Error> {
784 let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
787
788 match (builtin.execute_func)(context, args).await {
789 Ok(result) => Ok(result),
790 Err(e) => {
791 if let Some(io_err) = e.as_io_error() {
794 if io_err.kind() == std::io::ErrorKind::BrokenPipe {
795 return Ok(ExecutionExitCode::from(io_err).into());
796 }
797 }
798
799 Err(if mark_errors_fatal { e.into_fatal() } else { e })
800 }
801 }
802}
803
804pub(crate) async fn invoke_shell_function(
805 function: functions::Registration,
806 mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
807 args: &[CommandArg],
808) -> Result<ExecutionSpawnResult, error::Error> {
809 let ast::FunctionBody(body, redirects) = &function.definition().body;
810
811 if let Some(redirects) = redirects {
813 for redirect in &redirects.0 {
814 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
815 }
816 }
817
818 let positional_args = args.iter().map(|a| a.to_string());
819
820 let mut shell =
823 context
824 .shell
825 .enter_function(context.command_name.as_str(), &function, positional_args)?;
826
827 let result = body.execute(&mut shell, &context.params).await;
834
835 drop(shell);
837
838 let mut result = result?;
840
841 match result.next_control_flow {
843 ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
844 return error::unimp("break or continue returned from function invocation");
845 }
846 ExecutionControlFlow::ReturnFromFunctionOrScript => {
847 result.next_control_flow = ExecutionControlFlow::Normal;
849 }
850 _ => {}
851 }
852
853 Ok(result.into())
854}
855
856pub(crate) async fn invoke_command_in_subshell_and_get_output(
857 shell: &mut Shell<impl extensions::ShellExtensions>,
858 params: &ExecutionParameters,
859 s: String,
860) -> Result<String, error::Error> {
861 let mut subshell = shell.clone();
863
864 if !shell.options().command_subst_inherits_errexit {
868 subshell.options_mut().exit_on_nonzero_command_exit = false;
869 }
870
871 let mut params = params.clone();
873 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
874
875 let (reader, writer) = sys::pipes::pipe()?;
877 params.set_fd(OpenFiles::STDOUT_FD, writer.into());
878
879 let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
880
881 let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
882
883 let output_str = async_reader.read_to_string().await?;
884
885 let run_result = cmd_join_handle.await?;
887 let cmd_result = run_result?;
888
889 shell.set_last_exit_status(cmd_result.exit_code.into());
891
892 Ok(output_str)
896}
897
898async fn run_substitution_command(
899 mut shell: Shell<impl extensions::ShellExtensions>,
900 mut params: ExecutionParameters,
901 command: String,
902) -> Result<ExecutionResult, error::Error> {
903 let parse_result = shell.parse_string(command);
905
906 if let Ok(program) = &parse_result {
910 if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
911 interp::setup_redirect(&mut shell, &mut params, redir).await?;
912 std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
913 return Ok(ExecutionResult::new(0));
914 }
915 }
916
917 let source_info = crate::SourceInfo::from("main");
919
920 shell
922 .run_parsed_result(parse_result, &source_info, ¶ms)
923 .await
924}
925
926fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
929 let [complete] = program.complete_commands.as_slice() else {
931 return None;
932 };
933
934 let ast::CompoundList(items) = complete;
936 let [item] = items.as_slice() else {
937 return None;
938 };
939
940 let and_or = &item.0;
942 if !and_or.additional.is_empty() {
943 return None;
944 }
945
946 let pipeline = &and_or.first;
948 if pipeline.bang {
949 return None;
950 }
951
952 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
954 return None;
955 };
956
957 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
959 return None;
960 }
961
962 let prefix = simple_cmd.prefix.as_ref()?;
964 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
965 return None;
966 };
967
968 match redir {
970 ast::IoRedirect::File(
971 fd,
972 ast::IoFileRedirectKind::Read,
973 ast::IoFileRedirectTarget::Filename(..),
974 ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
975 _ => None,
976 }
977}