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::{
12 CommandCgroupExt, CommandExt, CommandFdInjectionExt, CommandFgControlExt,
13 CommandOpenFileLimitExt, CommandPidsLimitsExt, CommandProcessLimitExt,
14};
15
16use crate::extensions::{self, ShellExtensions};
17use crate::interp::{self, Execute, ProcessGroupPolicy};
18use crate::openfiles::{self, OpenFile, OpenFiles};
19use crate::results::ExecutionSpawnResult;
20use crate::{
21 ErrorKind, ExecutionControlFlow, ExecutionExitCode, ExecutionParameters, ExecutionResult,
22 Shell, ShellFd, builtins, commands, env, error, escape, functions, options, pathsearch,
23 processes, sys, trace_categories, traps, variables,
24};
25
26pub enum CommandWaitResult {
28 CommandCompleted(ExecutionResult),
30 CommandStopped(ExecutionResult, processes::ChildProcess),
32}
33
34pub struct ExecutionContext<'a, SE: ShellExtensions = extensions::DefaultShellExtensions> {
36 pub shell: &'a mut Shell<SE>,
38 pub command_name: String,
40 pub params: ExecutionParameters,
42}
43
44impl<SE: ShellExtensions> ExecutionContext<'_, SE> {
45 pub fn stdin(&self) -> impl std::io::Read + 'static {
47 self.params.stdin(self.shell)
48 }
49
50 pub fn stdout(&self) -> impl std::io::Write + 'static {
52 self.params.stdout(self.shell)
53 }
54
55 pub fn stderr(&self) -> impl std::io::Write + 'static {
57 self.params.stderr(self.shell)
58 }
59
60 pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
67 self.params.try_fd(self.shell, fd)
68 }
69
70 pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
72 self.params.iter_fds(self.shell)
73 }
74}
75
76#[derive(Clone, Debug)]
78pub enum CommandArg {
79 String(String),
81 Assignment(ast::Assignment),
84}
85
86impl Display for CommandArg {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Self::String(s) => f.write_str(s),
90 Self::Assignment(a) => write!(f, "{a}"),
91 }
92 }
93}
94
95impl From<String> for CommandArg {
96 fn from(s: String) -> Self {
97 Self::String(s)
98 }
99}
100
101impl From<&String> for CommandArg {
102 fn from(value: &String) -> Self {
103 Self::String(value.clone())
104 }
105}
106
107impl CommandArg {
108 const fn to_string_len_hint(&self) -> usize {
109 match self {
110 Self::String(s) => s.len(),
111 Self::Assignment(_) => 0,
112 }
113 }
114
115 pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
116 match self {
117 Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
118 Self::Assignment(a) => {
119 let mut s = a.name.to_string();
120 let op = if a.append { "+=" } else { "=" };
121 s.push_str(op);
122 s.push_str(&escape::quote_if_needed(
123 a.value.to_string().as_str(),
124 escape::QuoteMode::SingleQuote,
125 ));
126 s.into()
127 }
128 }
129 }
130}
131
132pub enum ShellForCommand<'a, SE: extensions::ShellExtensions> {
134 ParentShell(&'a mut Shell<SE>),
137 OwnedShell {
139 target: Box<Shell<SE>>,
141 parent: &'a mut Shell<SE>,
143 },
144}
145
146impl<SE: extensions::ShellExtensions> std::ops::Deref for ShellForCommand<'_, SE> {
147 type Target = Shell<SE>;
148
149 fn deref(&self) -> &Self::Target {
150 match self {
151 ShellForCommand::ParentShell(shell) => shell,
152 ShellForCommand::OwnedShell { target, .. } => target,
153 }
154 }
155}
156
157impl<SE: extensions::ShellExtensions> std::ops::DerefMut for ShellForCommand<'_, SE> {
158 fn deref_mut(&mut self) -> &mut Self::Target {
159 match self {
160 ShellForCommand::ParentShell(shell) => shell,
161 ShellForCommand::OwnedShell { target, .. } => target,
162 }
163 }
164}
165
166#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
192pub fn compose_std_command<S: AsRef<OsStr>, SE: extensions::ShellExtensions>(
193 context: &ExecutionContext<'_, SE>,
194 command_name: &str,
195 argv0: &str,
196 args: &[S],
197 empty_env: bool,
198) -> Result<std::process::Command, error::Error> {
199 let mut cmd = std::process::Command::new(command_name);
200
201 cmd.arg0(argv0);
204
205 cmd.args(args);
207
208 cmd.current_dir(context.shell.working_dir());
210
211 cmd.env_clear();
213
214 if !empty_env {
216 for (k, v) in context.shell.env().iter_exported() {
217 if v.value().is_set() {
221 cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
222 }
223 }
224 cmd.env("_", command_name);
226 }
227
228 if !empty_env {
230 for (func_name, registration) in context.shell.funcs().iter() {
231 if registration.is_exported() {
232 let var_name = std::format!("BASH_FUNC_{func_name}%%");
233 let value = std::format!("() {}", registration.definition().body);
234 cmd.env(var_name, value);
235 }
236 }
237 }
238
239 match context.try_fd(OpenFiles::STDIN_FD) {
241 Some(OpenFile::Stdin(_)) | None => (),
242 Some(stdin_file) => {
243 let as_stdio: Stdio = stdin_file.try_into()?;
244 cmd.stdin(as_stdio);
245 }
246 }
247
248 match context.try_fd(OpenFiles::STDOUT_FD) {
250 Some(OpenFile::Stdout(_)) | None => (),
251 Some(stdout_file) => {
252 let as_stdio: Stdio = stdout_file.try_into()?;
253 cmd.stdout(as_stdio);
254 }
255 }
256
257 match context.try_fd(OpenFiles::STDERR_FD) {
259 Some(OpenFile::Stderr(_)) | None => {}
260 Some(stderr_file) => {
261 let as_stdio: Stdio = stderr_file.try_into()?;
262 cmd.stderr(as_stdio);
263 }
264 }
265
266 if let Some(cgroup_procs) = &context.shell.options().spawn.cgroup_procs {
270 cmd.join_cgroup(std::sync::Arc::clone(cgroup_procs))?;
271 }
272
273 let pids_limits = &context.shell.options().spawn.pids_limits;
279 if !pids_limits.is_empty() {
280 cmd.check_pids_limits(std::sync::Arc::clone(pids_limits))?;
281 }
282
283 let other_files = context.iter_fds().filter(|(fd, _)| {
285 *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
286 });
287 cmd.inject_fds(other_files)?;
288
289 if let Some(limit) = context.shell.options().spawn.open_file_limit {
293 cmd.set_open_file_limit(limit)?;
294 }
295
296 if let Some(limit) = context.shell.options().spawn.process_limit {
299 cmd.set_process_limit(limit)?;
300 }
301
302 Ok(cmd)
303}
304
305pub(crate) async fn on_preexecute(
306 cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
307) -> Result<(), error::Error> {
308 let full_cmd = format_command_args(&cmd.args);
311 cmd.shell.env_mut().update_or_add(
312 "BASH_COMMAND",
313 variables::ShellValueLiteral::Scalar(full_cmd),
314 |_| Ok(()),
315 env::EnvironmentLookup::Anywhere,
316 env::EnvironmentScope::Global,
317 )?;
318
319 if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
321 let _ = cmd
322 .shell
323 .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
324 .await?;
325 }
326
327 Ok(())
328}
329
330fn format_command_args(args: &[CommandArg]) -> String {
331 let capacity = args
332 .iter()
333 .map(CommandArg::to_string_len_hint)
334 .sum::<usize>()
335 + args.len().saturating_sub(1);
336 let mut result = String::with_capacity(capacity);
337
338 for (i, arg) in args.iter().enumerate() {
339 if i > 0 {
340 result.push(' ');
341 }
342 let _ = write!(result, "{arg}");
343 }
344
345 result
346}
347
348pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
350 shell: ShellForCommand<'a, SE>,
352
353 pub params: ExecutionParameters,
355
356 pub command_name: String,
358
359 pub args: Vec<CommandArg>,
361
362 pub use_functions: bool,
366
367 pub path_dirs: Option<Vec<PathBuf>>,
370
371 pub process_group_id: Option<i32>,
374
375 pub argv0: Option<String>,
378}
379
380impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
381 pub fn new<I>(
390 shell: ShellForCommand<'a, SE>,
391 params: ExecutionParameters,
392 command_name: String,
393 args: I,
394 ) -> Self
395 where
396 I: IntoIterator<Item = CommandArg>,
397 {
398 Self {
399 shell,
400 params,
401 command_name,
402 args: args.into_iter().collect(),
403 use_functions: true,
404 path_dirs: None,
405 process_group_id: None,
406 argv0: None,
407 }
408 }
409
410 pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
430 let builtin = self.shell.builtins().get(&self.command_name).cloned();
432
433 if self.shell.options().posix_mode
436 && builtin
437 .as_ref()
438 .is_some_and(|r| !r.disabled && r.special_builtin)
439 {
440 #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
441 let builtin = builtin.unwrap();
442 return self.execute_via_builtin(builtin).await;
443 }
444
445 if self.use_functions {
448 if let Some(func_registration) =
449 self.shell.funcs().get(self.command_name.as_str()).cloned()
450 {
451 return self.execute_via_function(func_registration).await;
452 }
453 }
454
455 if let Some(builtin) = builtin {
458 if !builtin.disabled {
459 return self.execute_via_builtin(builtin).await;
460 }
461 }
462
463 if !sys::fs::contains_path_separator(&self.command_name) {
466 let path = if let Some(path_dirs) = &self.path_dirs {
470 pathsearch::resolve_command(path_dirs, self.command_name.as_str())
471 } else {
472 self.shell
473 .resolve_command_in_path_using_cache(&self.command_name)
474 };
475
476 if let Some(path) = path {
477 self.execute_via_external(&path)
478 } else {
479 let last_arg = Self::take_last_arg(&self.args);
482 self.shell.update_last_arg_variable(last_arg);
483
484 Err(ErrorKind::CommandNotFound(self.command_name).into())
485 }
486 } else {
487 let command_name = sys::fs::resolve_executable_path(
488 PathBuf::from(&self.command_name),
489 self.shell.working_dir(),
490 );
491 self.execute_via_external(command_name.as_path())
492 }
493 }
494
495 fn take_last_arg(args: &[CommandArg]) -> Option<String> {
498 args.last().map(ToString::to_string)
499 }
500
501 async fn execute_via_builtin(
502 self,
503 builtin: builtins::Registration<SE>,
504 ) -> Result<ExecutionSpawnResult, error::Error> {
505 match self.shell {
506 ShellForCommand::OwnedShell { target, .. } => {
507 Ok(Self::execute_via_builtin_in_owned_shell(
508 *target,
509 self.params,
510 builtin,
511 self.command_name,
512 self.args,
513 )
514 .await)
515 }
516 ShellForCommand::ParentShell(..) => {
517 self.execute_via_builtin_in_parent_shell(builtin).await
518 }
519 }
520 }
521
522 fn execute_via_builtin_in_owned_shell(
525 mut shell: Shell<SE>,
526 params: ExecutionParameters,
527 builtin: builtins::Registration<SE>,
528 command_name: String,
529 args: Vec<CommandArg>,
530 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
531 let last_arg = Self::take_last_arg(&args);
532 let task = sys::tasks::spawn_blocking(async move {
535 let cmd_context = ExecutionContext {
536 shell: &mut shell,
537 command_name,
538 params,
539 };
540
541 let result = execute_builtin_command(&builtin, cmd_context, args).await;
542
543 shell.update_last_arg_variable(last_arg);
545
546 result
547 });
548
549 async move { ExecutionSpawnResult::StartedTask(task.await) }
550 }
551
552 async fn execute_via_builtin_in_parent_shell(
553 self,
554 builtin: builtins::Registration<SE>,
555 ) -> Result<ExecutionSpawnResult, error::Error> {
556 let mut shell = self.shell;
557 let last_arg = Self::take_last_arg(&self.args);
558
559 let cmd_context = ExecutionContext {
560 shell: &mut shell,
561 command_name: self.command_name,
562 params: self.params,
563 };
564
565 let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
566
567 shell.update_last_arg_variable(last_arg);
569
570 let result = result?;
571
572 Ok(result.into())
573 }
574
575 async fn execute_via_function(
576 self,
577 func_registration: functions::Registration,
578 ) -> Result<ExecutionSpawnResult, error::Error> {
579 match self.shell {
580 ShellForCommand::OwnedShell { target, .. } => {
581 Ok(Self::execute_via_function_in_owned_shell(
582 *target,
583 self.params,
584 func_registration,
585 self.command_name,
586 self.args,
587 )
588 .await)
589 }
590 ShellForCommand::ParentShell(..) => {
591 self.execute_via_function_in_parent_shell(func_registration)
592 .await
593 }
594 }
595 }
596
597 fn execute_via_function_in_owned_shell(
613 mut shell: Shell<SE>,
614 params: ExecutionParameters,
615 func_registration: functions::Registration,
616 command_name: String,
617 args: Vec<CommandArg>,
618 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
619 let last_arg = Self::take_last_arg(&args);
620 let task = sys::tasks::spawn_blocking(async move {
621 let cmd_context = ExecutionContext {
622 shell: &mut shell,
623 command_name,
624 params,
625 };
626
627 let spawn_result =
628 invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
629
630 shell.update_last_arg_variable(last_arg);
632
633 match spawn_result? {
634 ExecutionSpawnResult::Completed(result) => Ok(result),
635 ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
636 error::unimp("invoke_shell_function returned an unexpected spawn result")
640 }
641 }
642 });
643
644 async move { ExecutionSpawnResult::StartedTask(task.await) }
645 }
646
647 async fn execute_via_function_in_parent_shell(
648 self,
649 func_registration: functions::Registration,
650 ) -> Result<ExecutionSpawnResult, error::Error> {
651 let mut shell = self.shell;
652 let last_arg = Self::take_last_arg(&self.args);
653
654 let cmd_context = ExecutionContext {
655 shell: &mut shell,
656 command_name: self.command_name,
657 params: self.params,
658 };
659
660 let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
662
663 shell.update_last_arg_variable(last_arg);
668
669 result
670 }
671
672 fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
673 let mut shell = self.shell;
674 let last_arg = Self::take_last_arg(&self.args);
675
676 let cmd_context = ExecutionContext {
677 shell: &mut shell,
678 command_name: self.command_name,
679 params: self.params,
680 };
681
682 let resolved_path = path.to_string_lossy();
683 let result = execute_external_command(
684 cmd_context,
685 resolved_path.as_ref(),
686 self.process_group_id,
687 self.argv0.as_deref(),
688 &self.args[1..],
689 );
690
691 shell.update_last_arg_variable(last_arg);
693
694 result
695 }
696}
697
698pub(crate) fn execute_external_command(
699 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
700 executable_path: &str,
701 process_group_id: Option<i32>,
702 argv0_override: Option<&str>,
703 args: &[CommandArg],
704) -> Result<ExecutionSpawnResult, error::Error> {
705 let cmd_args = args
707 .iter()
708 .filter_map(|e| {
709 if let CommandArg::String(s) = e {
710 Some(s)
711 } else {
712 None
713 }
714 })
715 .collect::<Vec<_>>();
716
717 let child_stdin_is_terminal = context
720 .try_fd(openfiles::OpenFiles::STDIN_FD)
721 .is_some_and(|f| f.is_terminal());
722
723 let new_pg = matches!(
725 context.params.process_group_policy,
726 ProcessGroupPolicy::NewProcessGroup
727 );
728
729 let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
734 #[allow(unused_mut, reason = "only mutated on unix platforms")]
735 let mut cmd = compose_std_command(
736 &context,
737 executable_path,
738 argv0,
739 cmd_args.as_slice(),
740 false, )?;
742
743 if new_pg {
745 if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
748 cmd.lead_session();
750 } else {
751 cmd.process_group(0);
753 if child_stdin_is_terminal {
754 cmd.take_foreground();
755 }
756 }
757 } else {
758 if let Some(pgid) = process_group_id {
760 cmd.process_group(pgid);
761 }
762 }
763
764 tracing::debug!(
766 target: trace_categories::COMMANDS,
767 "Spawning: cmd='{} {}'",
768 cmd.get_program().to_string_lossy().to_string(),
769 cmd.get_args()
770 .map(|a| a.to_string_lossy().to_string())
771 .join(" ")
772 );
773
774 let _guard = context
776 .shell
777 .options()
778 .spawn
779 .before_spawn
780 .as_ref()
781 .map(|before_spawn| before_spawn());
782 match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
783 Ok(child) => {
784 #[expect(clippy::cast_possible_wrap)]
786 let pid = child.id().map(|id| id as i32);
787 let mut actual_pgid = process_group_id;
788 if let Some(pid) = &pid {
789 if new_pg {
790 actual_pgid = Some(*pid);
791 }
792 } else {
793 tracing::warn!("could not retrieve pid for child process");
794 }
795
796 if let (Some(on_spawn), Some(pid)) = (&context.shell.options().spawn.on_spawn, pid) {
797 on_spawn(options::SpawnedProcess {
798 pid,
799 process_group: actual_pgid,
800 });
801 }
802
803 Ok(ExecutionSpawnResult::StartedProcess(
804 processes::ChildProcess::new(child, pid, actual_pgid),
805 ))
806 }
807 Err(spawn_err) => {
808 if context.shell.options().interactive {
809 sys::terminal::move_self_to_foreground()?;
810 }
811
812 Err(spawn_error(context, spawn_err))
813 }
814 }
815}
816
817pub fn missing_cgroup(options: &options::RuntimeOptions) -> Option<std::io::Error> {
824 let cgroup_procs = options.spawn.cgroup_procs.as_ref()?;
825 sys::commands::is_missing(cgroup_procs).then(|| {
826 std::io::Error::new(
827 std::io::ErrorKind::NotFound,
828 std::format!(
829 "cannot join the cgroup: {} is missing",
830 cgroup_procs.to_string_lossy()
831 ),
832 )
833 })
834}
835
836fn spawn_error(
838 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
839 spawn_err: std::io::Error,
840) -> error::Error {
841 if spawn_err.kind() != std::io::ErrorKind::NotFound {
842 return error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err).into();
843 }
844
845 if let Some(missing) = missing_cgroup(context.shell.options()) {
846 error::ErrorKind::FailedToExecuteCommand(context.command_name, missing).into()
847 } else if !context.shell.working_dir().exists() {
848 error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned()).into()
849 } else {
850 error::ErrorKind::CommandNotFound(context.command_name).into()
851 }
852}
853
854async fn execute_builtin_command<SE: extensions::ShellExtensions>(
855 builtin: &builtins::Registration<SE>,
856 context: ExecutionContext<'_, SE>,
857 args: Vec<CommandArg>,
858) -> Result<ExecutionResult, error::Error> {
859 let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
862
863 match (builtin.execute_func)(context, args).await {
864 Ok(result) => Ok(result),
865 Err(e) => {
866 if let Some(io_err) = e.as_io_error() {
869 if io_err.kind() == std::io::ErrorKind::BrokenPipe {
870 return Ok(ExecutionExitCode::from(io_err).into());
871 }
872 }
873
874 Err(if mark_errors_fatal { e.into_fatal() } else { e })
875 }
876 }
877}
878
879pub(crate) async fn invoke_shell_function(
880 function: functions::Registration,
881 mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
882 args: &[CommandArg],
883) -> Result<ExecutionSpawnResult, error::Error> {
884 let ast::FunctionBody(body, redirects) = &function.definition().body;
885
886 if let Some(redirects) = redirects {
888 for redirect in &redirects.0 {
889 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
890 }
891 }
892
893 let positional_args = args.iter().map(|a| a.to_string());
894
895 let mut shell =
898 context
899 .shell
900 .enter_function(context.command_name.as_str(), &function, positional_args)?;
901
902 let result = body.execute(&mut shell, &context.params).await;
909
910 drop(shell);
912
913 let mut result = result?;
915
916 match result.next_control_flow {
918 ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
919 return error::unimp("break or continue returned from function invocation");
920 }
921 ExecutionControlFlow::ReturnFromFunctionOrScript => {
922 result.next_control_flow = ExecutionControlFlow::Normal;
924 }
925 _ => {}
926 }
927
928 Ok(result.into())
929}
930
931pub(crate) async fn invoke_command_in_subshell_and_get_output(
932 shell: &mut Shell<impl extensions::ShellExtensions>,
933 params: &ExecutionParameters,
934 s: String,
935) -> Result<String, error::Error> {
936 let mut subshell = shell.clone();
938
939 if !shell.options().command_subst_inherits_errexit {
943 subshell.options_mut().exit_on_nonzero_command_exit = false;
944 }
945
946 let mut params = params.clone();
948 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
949
950 let (reader, writer) = sys::pipes::pipe()?;
952 params.set_fd(OpenFiles::STDOUT_FD, writer.into());
953
954 let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
955
956 let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
957
958 let output_str = async_reader.read_to_string().await?;
959
960 let run_result = cmd_join_handle.await?;
962 let cmd_result = run_result?;
963
964 shell.set_last_exit_status(cmd_result.exit_code.into());
966
967 Ok(output_str)
971}
972
973async fn run_substitution_command(
974 mut shell: Shell<impl extensions::ShellExtensions>,
975 mut params: ExecutionParameters,
976 command: String,
977) -> Result<ExecutionResult, error::Error> {
978 let parse_result = shell.parse_string(command);
980
981 if let Ok(program) = &parse_result {
985 if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
986 interp::setup_redirect(&mut shell, &mut params, redir).await?;
987 std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
988 return Ok(ExecutionResult::new(0));
989 }
990 }
991
992 let source_info = crate::SourceInfo::from("main");
994
995 shell
997 .run_parsed_result(parse_result, &source_info, ¶ms)
998 .await
999}
1000
1001fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
1004 let [complete] = program.complete_commands.as_slice() else {
1006 return None;
1007 };
1008
1009 let ast::CompoundList(items) = complete;
1011 let [item] = items.as_slice() else {
1012 return None;
1013 };
1014
1015 let and_or = &item.0;
1017 if !and_or.additional.is_empty() {
1018 return None;
1019 }
1020
1021 let pipeline = &and_or.first;
1023 if pipeline.bang {
1024 return None;
1025 }
1026
1027 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
1029 return None;
1030 };
1031
1032 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
1034 return None;
1035 }
1036
1037 let prefix = simple_cmd.prefix.as_ref()?;
1039 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
1040 return None;
1041 };
1042
1043 match redir {
1045 ast::IoRedirect::File(
1046 fd,
1047 ast::IoFileRedirectKind::Read,
1048 ast::IoFileRedirectTarget::Filename(..),
1049 ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
1050 _ => None,
1051 }
1052}