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::{CommandCgroupExt, 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, options, pathsearch,
20 processes, sys, 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")]
188pub fn compose_std_command<S: AsRef<OsStr>, SE: extensions::ShellExtensions>(
189 context: &ExecutionContext<'_, SE>,
190 command_name: &str,
191 argv0: &str,
192 args: &[S],
193 empty_env: bool,
194) -> Result<std::process::Command, error::Error> {
195 let mut cmd = std::process::Command::new(command_name);
196
197 cmd.arg0(argv0);
200
201 cmd.args(args);
203
204 cmd.current_dir(context.shell.working_dir());
206
207 cmd.env_clear();
209
210 if !empty_env {
212 for (k, v) in context.shell.env().iter_exported() {
213 if v.value().is_set() {
217 cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
218 }
219 }
220 cmd.env("_", command_name);
222 }
223
224 if !empty_env {
226 for (func_name, registration) in context.shell.funcs().iter() {
227 if registration.is_exported() {
228 let var_name = std::format!("BASH_FUNC_{func_name}%%");
229 let value = std::format!("() {}", registration.definition().body);
230 cmd.env(var_name, value);
231 }
232 }
233 }
234
235 match context.try_fd(OpenFiles::STDIN_FD) {
237 Some(OpenFile::Stdin(_)) | None => (),
238 Some(stdin_file) => {
239 let as_stdio: Stdio = stdin_file.try_into()?;
240 cmd.stdin(as_stdio);
241 }
242 }
243
244 match context.try_fd(OpenFiles::STDOUT_FD) {
246 Some(OpenFile::Stdout(_)) | None => (),
247 Some(stdout_file) => {
248 let as_stdio: Stdio = stdout_file.try_into()?;
249 cmd.stdout(as_stdio);
250 }
251 }
252
253 match context.try_fd(OpenFiles::STDERR_FD) {
255 Some(OpenFile::Stderr(_)) | None => {}
256 Some(stderr_file) => {
257 let as_stdio: Stdio = stderr_file.try_into()?;
258 cmd.stderr(as_stdio);
259 }
260 }
261
262 let other_files = context.iter_fds().filter(|(fd, _)| {
264 *fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
265 });
266 cmd.inject_fds(other_files)?;
267
268 if let Some(cgroup_procs) = &context.shell.options().spawn.cgroup_procs {
272 cmd.join_cgroup(std::sync::Arc::clone(cgroup_procs))?;
273 }
274
275 Ok(cmd)
276}
277
278pub(crate) async fn on_preexecute(
279 cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
280) -> Result<(), error::Error> {
281 let full_cmd = format_command_args(&cmd.args);
284 cmd.shell.env_mut().update_or_add(
285 "BASH_COMMAND",
286 variables::ShellValueLiteral::Scalar(full_cmd),
287 |_| Ok(()),
288 env::EnvironmentLookup::Anywhere,
289 env::EnvironmentScope::Global,
290 )?;
291
292 if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
294 let _ = cmd
295 .shell
296 .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
297 .await?;
298 }
299
300 Ok(())
301}
302
303fn format_command_args(args: &[CommandArg]) -> String {
304 let capacity = args
305 .iter()
306 .map(CommandArg::to_string_len_hint)
307 .sum::<usize>()
308 + args.len().saturating_sub(1);
309 let mut result = String::with_capacity(capacity);
310
311 for (i, arg) in args.iter().enumerate() {
312 if i > 0 {
313 result.push(' ');
314 }
315 let _ = write!(result, "{arg}");
316 }
317
318 result
319}
320
321pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
323 shell: ShellForCommand<'a, SE>,
325
326 pub params: ExecutionParameters,
328
329 pub command_name: String,
331
332 pub args: Vec<CommandArg>,
334
335 pub use_functions: bool,
339
340 pub path_dirs: Option<Vec<PathBuf>>,
343
344 pub process_group_id: Option<i32>,
347
348 pub argv0: Option<String>,
351}
352
353impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
354 pub fn new<I>(
363 shell: ShellForCommand<'a, SE>,
364 params: ExecutionParameters,
365 command_name: String,
366 args: I,
367 ) -> Self
368 where
369 I: IntoIterator<Item = CommandArg>,
370 {
371 Self {
372 shell,
373 params,
374 command_name,
375 args: args.into_iter().collect(),
376 use_functions: true,
377 path_dirs: None,
378 process_group_id: None,
379 argv0: None,
380 }
381 }
382
383 pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
403 let builtin = self.shell.builtins().get(&self.command_name).cloned();
405
406 if self.shell.options().posix_mode
409 && builtin
410 .as_ref()
411 .is_some_and(|r| !r.disabled && r.special_builtin)
412 {
413 #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
414 let builtin = builtin.unwrap();
415 return self.execute_via_builtin(builtin).await;
416 }
417
418 if self.use_functions {
421 if let Some(func_registration) =
422 self.shell.funcs().get(self.command_name.as_str()).cloned()
423 {
424 return self.execute_via_function(func_registration).await;
425 }
426 }
427
428 if let Some(builtin) = builtin {
431 if !builtin.disabled {
432 return self.execute_via_builtin(builtin).await;
433 }
434 }
435
436 if !sys::fs::contains_path_separator(&self.command_name) {
439 let path = if let Some(path_dirs) = &self.path_dirs {
443 pathsearch::resolve_command(path_dirs, self.command_name.as_str())
444 } else {
445 self.shell
446 .resolve_command_in_path_using_cache(&self.command_name)
447 };
448
449 if let Some(path) = path {
450 self.execute_via_external(&path)
451 } else {
452 let last_arg = Self::take_last_arg(&self.args);
455 self.shell.update_last_arg_variable(last_arg);
456
457 Err(ErrorKind::CommandNotFound(self.command_name).into())
458 }
459 } else {
460 let command_name = sys::fs::resolve_executable_path(
461 PathBuf::from(&self.command_name),
462 self.shell.working_dir(),
463 );
464 self.execute_via_external(command_name.as_path())
465 }
466 }
467
468 fn take_last_arg(args: &[CommandArg]) -> Option<String> {
471 args.last().map(ToString::to_string)
472 }
473
474 async fn execute_via_builtin(
475 self,
476 builtin: builtins::Registration<SE>,
477 ) -> Result<ExecutionSpawnResult, error::Error> {
478 match self.shell {
479 ShellForCommand::OwnedShell { target, .. } => {
480 Ok(Self::execute_via_builtin_in_owned_shell(
481 *target,
482 self.params,
483 builtin,
484 self.command_name,
485 self.args,
486 )
487 .await)
488 }
489 ShellForCommand::ParentShell(..) => {
490 self.execute_via_builtin_in_parent_shell(builtin).await
491 }
492 }
493 }
494
495 fn execute_via_builtin_in_owned_shell(
498 mut shell: Shell<SE>,
499 params: ExecutionParameters,
500 builtin: builtins::Registration<SE>,
501 command_name: String,
502 args: Vec<CommandArg>,
503 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
504 let last_arg = Self::take_last_arg(&args);
505 let task = sys::tasks::spawn_blocking(async move {
508 let cmd_context = ExecutionContext {
509 shell: &mut shell,
510 command_name,
511 params,
512 };
513
514 let result = execute_builtin_command(&builtin, cmd_context, args).await;
515
516 shell.update_last_arg_variable(last_arg);
518
519 result
520 });
521
522 async move { ExecutionSpawnResult::StartedTask(task.await) }
523 }
524
525 async fn execute_via_builtin_in_parent_shell(
526 self,
527 builtin: builtins::Registration<SE>,
528 ) -> Result<ExecutionSpawnResult, error::Error> {
529 let mut shell = self.shell;
530 let last_arg = Self::take_last_arg(&self.args);
531
532 let cmd_context = ExecutionContext {
533 shell: &mut shell,
534 command_name: self.command_name,
535 params: self.params,
536 };
537
538 let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
539
540 shell.update_last_arg_variable(last_arg);
542
543 let result = result?;
544
545 Ok(result.into())
546 }
547
548 async fn execute_via_function(
549 self,
550 func_registration: functions::Registration,
551 ) -> Result<ExecutionSpawnResult, error::Error> {
552 match self.shell {
553 ShellForCommand::OwnedShell { target, .. } => {
554 Ok(Self::execute_via_function_in_owned_shell(
555 *target,
556 self.params,
557 func_registration,
558 self.command_name,
559 self.args,
560 )
561 .await)
562 }
563 ShellForCommand::ParentShell(..) => {
564 self.execute_via_function_in_parent_shell(func_registration)
565 .await
566 }
567 }
568 }
569
570 fn execute_via_function_in_owned_shell(
586 mut shell: Shell<SE>,
587 params: ExecutionParameters,
588 func_registration: functions::Registration,
589 command_name: String,
590 args: Vec<CommandArg>,
591 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
592 let last_arg = Self::take_last_arg(&args);
593 let task = sys::tasks::spawn_blocking(async move {
594 let cmd_context = ExecutionContext {
595 shell: &mut shell,
596 command_name,
597 params,
598 };
599
600 let spawn_result =
601 invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
602
603 shell.update_last_arg_variable(last_arg);
605
606 match spawn_result? {
607 ExecutionSpawnResult::Completed(result) => Ok(result),
608 ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
609 error::unimp("invoke_shell_function returned an unexpected spawn result")
613 }
614 }
615 });
616
617 async move { ExecutionSpawnResult::StartedTask(task.await) }
618 }
619
620 async fn execute_via_function_in_parent_shell(
621 self,
622 func_registration: functions::Registration,
623 ) -> Result<ExecutionSpawnResult, error::Error> {
624 let mut shell = self.shell;
625 let last_arg = Self::take_last_arg(&self.args);
626
627 let cmd_context = ExecutionContext {
628 shell: &mut shell,
629 command_name: self.command_name,
630 params: self.params,
631 };
632
633 let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
635
636 shell.update_last_arg_variable(last_arg);
641
642 result
643 }
644
645 fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
646 let mut shell = self.shell;
647 let last_arg = Self::take_last_arg(&self.args);
648
649 let cmd_context = ExecutionContext {
650 shell: &mut shell,
651 command_name: self.command_name,
652 params: self.params,
653 };
654
655 let resolved_path = path.to_string_lossy();
656 let result = execute_external_command(
657 cmd_context,
658 resolved_path.as_ref(),
659 self.process_group_id,
660 self.argv0.as_deref(),
661 &self.args[1..],
662 );
663
664 shell.update_last_arg_variable(last_arg);
666
667 result
668 }
669}
670
671pub(crate) fn execute_external_command(
672 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
673 executable_path: &str,
674 process_group_id: Option<i32>,
675 argv0_override: Option<&str>,
676 args: &[CommandArg],
677) -> Result<ExecutionSpawnResult, error::Error> {
678 let cmd_args = args
680 .iter()
681 .filter_map(|e| {
682 if let CommandArg::String(s) = e {
683 Some(s)
684 } else {
685 None
686 }
687 })
688 .collect::<Vec<_>>();
689
690 let child_stdin_is_terminal = context
693 .try_fd(openfiles::OpenFiles::STDIN_FD)
694 .is_some_and(|f| f.is_terminal());
695
696 let new_pg = matches!(
698 context.params.process_group_policy,
699 ProcessGroupPolicy::NewProcessGroup
700 );
701
702 let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
707 #[allow(unused_mut, reason = "only mutated on unix platforms")]
708 let mut cmd = compose_std_command(
709 &context,
710 executable_path,
711 argv0,
712 cmd_args.as_slice(),
713 false, )?;
715
716 if new_pg {
718 if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
721 cmd.lead_session();
723 } else {
724 cmd.process_group(0);
726 if child_stdin_is_terminal {
727 cmd.take_foreground();
728 }
729 }
730 } else {
731 if let Some(pgid) = process_group_id {
733 cmd.process_group(pgid);
734 }
735 }
736
737 tracing::debug!(
739 target: trace_categories::COMMANDS,
740 "Spawning: cmd='{} {}'",
741 cmd.get_program().to_string_lossy().to_string(),
742 cmd.get_args()
743 .map(|a| a.to_string_lossy().to_string())
744 .join(" ")
745 );
746
747 match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
748 Ok(child) => {
749 #[expect(clippy::cast_possible_wrap)]
751 let pid = child.id().map(|id| id as i32);
752 let mut actual_pgid = process_group_id;
753 if let Some(pid) = &pid {
754 if new_pg {
755 actual_pgid = Some(*pid);
756 }
757 } else {
758 tracing::warn!("could not retrieve pid for child process");
759 }
760
761 if let (Some(on_spawn), Some(pid)) = (&context.shell.options().spawn.on_spawn, pid) {
762 on_spawn(options::SpawnedProcess {
763 pid,
764 process_group: actual_pgid,
765 });
766 }
767
768 Ok(ExecutionSpawnResult::StartedProcess(
769 processes::ChildProcess::new(child, pid, actual_pgid),
770 ))
771 }
772 Err(spawn_err) => {
773 if context.shell.options().interactive {
774 sys::terminal::move_self_to_foreground()?;
775 }
776
777 Err(spawn_error(context, spawn_err))
778 }
779 }
780}
781
782pub fn missing_cgroup(options: &options::RuntimeOptions) -> Option<std::io::Error> {
789 let cgroup_procs = options.spawn.cgroup_procs.as_ref()?;
790 sys::commands::is_missing(cgroup_procs).then(|| {
791 std::io::Error::new(
792 std::io::ErrorKind::NotFound,
793 std::format!(
794 "cannot join the cgroup: {} is missing",
795 cgroup_procs.to_string_lossy()
796 ),
797 )
798 })
799}
800
801fn spawn_error(
803 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
804 spawn_err: std::io::Error,
805) -> error::Error {
806 if spawn_err.kind() != std::io::ErrorKind::NotFound {
807 return error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err).into();
808 }
809
810 if let Some(missing) = missing_cgroup(context.shell.options()) {
811 error::ErrorKind::FailedToExecuteCommand(context.command_name, missing).into()
812 } else if !context.shell.working_dir().exists() {
813 error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned()).into()
814 } else {
815 error::ErrorKind::CommandNotFound(context.command_name).into()
816 }
817}
818
819async fn execute_builtin_command<SE: extensions::ShellExtensions>(
820 builtin: &builtins::Registration<SE>,
821 context: ExecutionContext<'_, SE>,
822 args: Vec<CommandArg>,
823) -> Result<ExecutionResult, error::Error> {
824 let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
827
828 match (builtin.execute_func)(context, args).await {
829 Ok(result) => Ok(result),
830 Err(e) => {
831 if let Some(io_err) = e.as_io_error() {
834 if io_err.kind() == std::io::ErrorKind::BrokenPipe {
835 return Ok(ExecutionExitCode::from(io_err).into());
836 }
837 }
838
839 Err(if mark_errors_fatal { e.into_fatal() } else { e })
840 }
841 }
842}
843
844pub(crate) async fn invoke_shell_function(
845 function: functions::Registration,
846 mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
847 args: &[CommandArg],
848) -> Result<ExecutionSpawnResult, error::Error> {
849 let ast::FunctionBody(body, redirects) = &function.definition().body;
850
851 if let Some(redirects) = redirects {
853 for redirect in &redirects.0 {
854 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
855 }
856 }
857
858 let positional_args = args.iter().map(|a| a.to_string());
859
860 let mut shell =
863 context
864 .shell
865 .enter_function(context.command_name.as_str(), &function, positional_args)?;
866
867 let result = body.execute(&mut shell, &context.params).await;
874
875 drop(shell);
877
878 let mut result = result?;
880
881 match result.next_control_flow {
883 ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
884 return error::unimp("break or continue returned from function invocation");
885 }
886 ExecutionControlFlow::ReturnFromFunctionOrScript => {
887 result.next_control_flow = ExecutionControlFlow::Normal;
889 }
890 _ => {}
891 }
892
893 Ok(result.into())
894}
895
896pub(crate) async fn invoke_command_in_subshell_and_get_output(
897 shell: &mut Shell<impl extensions::ShellExtensions>,
898 params: &ExecutionParameters,
899 s: String,
900) -> Result<String, error::Error> {
901 let mut subshell = shell.clone();
903
904 if !shell.options().command_subst_inherits_errexit {
908 subshell.options_mut().exit_on_nonzero_command_exit = false;
909 }
910
911 let mut params = params.clone();
913 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
914
915 let (reader, writer) = sys::pipes::pipe()?;
917 params.set_fd(OpenFiles::STDOUT_FD, writer.into());
918
919 let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
920
921 let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
922
923 let output_str = async_reader.read_to_string().await?;
924
925 let run_result = cmd_join_handle.await?;
927 let cmd_result = run_result?;
928
929 shell.set_last_exit_status(cmd_result.exit_code.into());
931
932 Ok(output_str)
936}
937
938async fn run_substitution_command(
939 mut shell: Shell<impl extensions::ShellExtensions>,
940 mut params: ExecutionParameters,
941 command: String,
942) -> Result<ExecutionResult, error::Error> {
943 let parse_result = shell.parse_string(command);
945
946 if let Ok(program) = &parse_result {
950 if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
951 interp::setup_redirect(&mut shell, &mut params, redir).await?;
952 std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
953 return Ok(ExecutionResult::new(0));
954 }
955 }
956
957 let source_info = crate::SourceInfo::from("main");
959
960 shell
962 .run_parsed_result(parse_result, &source_info, ¶ms)
963 .await
964}
965
966fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
969 let [complete] = program.complete_commands.as_slice() else {
971 return None;
972 };
973
974 let ast::CompoundList(items) = complete;
976 let [item] = items.as_slice() else {
977 return None;
978 };
979
980 let and_or = &item.0;
982 if !and_or.additional.is_empty() {
983 return None;
984 }
985
986 let pipeline = &and_or.first;
988 if pipeline.bang {
989 return None;
990 }
991
992 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
994 return None;
995 };
996
997 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
999 return None;
1000 }
1001
1002 let prefix = simple_cmd.prefix.as_ref()?;
1004 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
1005 return None;
1006 };
1007
1008 match redir {
1010 ast::IoRedirect::File(
1011 fd,
1012 ast::IoFileRedirectKind::Read,
1013 ast::IoFileRedirectTarget::Filename(..),
1014 ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
1015 _ => None,
1016 }
1017}