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,
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 Ok(cmd)
297}
298
299pub(crate) async fn on_preexecute(
300 cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
301) -> Result<(), error::Error> {
302 let full_cmd = format_command_args(&cmd.args);
305 cmd.shell.env_mut().update_or_add(
306 "BASH_COMMAND",
307 variables::ShellValueLiteral::Scalar(full_cmd),
308 |_| Ok(()),
309 env::EnvironmentLookup::Anywhere,
310 env::EnvironmentScope::Global,
311 )?;
312
313 if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
315 let _ = cmd
316 .shell
317 .invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
318 .await?;
319 }
320
321 Ok(())
322}
323
324fn format_command_args(args: &[CommandArg]) -> String {
325 let capacity = args
326 .iter()
327 .map(CommandArg::to_string_len_hint)
328 .sum::<usize>()
329 + args.len().saturating_sub(1);
330 let mut result = String::with_capacity(capacity);
331
332 for (i, arg) in args.iter().enumerate() {
333 if i > 0 {
334 result.push(' ');
335 }
336 let _ = write!(result, "{arg}");
337 }
338
339 result
340}
341
342pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
344 shell: ShellForCommand<'a, SE>,
346
347 pub params: ExecutionParameters,
349
350 pub command_name: String,
352
353 pub args: Vec<CommandArg>,
355
356 pub use_functions: bool,
360
361 pub path_dirs: Option<Vec<PathBuf>>,
364
365 pub process_group_id: Option<i32>,
368
369 pub argv0: Option<String>,
372}
373
374impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
375 pub fn new<I>(
384 shell: ShellForCommand<'a, SE>,
385 params: ExecutionParameters,
386 command_name: String,
387 args: I,
388 ) -> Self
389 where
390 I: IntoIterator<Item = CommandArg>,
391 {
392 Self {
393 shell,
394 params,
395 command_name,
396 args: args.into_iter().collect(),
397 use_functions: true,
398 path_dirs: None,
399 process_group_id: None,
400 argv0: None,
401 }
402 }
403
404 pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
424 let builtin = self.shell.builtins().get(&self.command_name).cloned();
426
427 if self.shell.options().posix_mode
430 && builtin
431 .as_ref()
432 .is_some_and(|r| !r.disabled && r.special_builtin)
433 {
434 #[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
435 let builtin = builtin.unwrap();
436 return self.execute_via_builtin(builtin).await;
437 }
438
439 if self.use_functions {
442 if let Some(func_registration) =
443 self.shell.funcs().get(self.command_name.as_str()).cloned()
444 {
445 return self.execute_via_function(func_registration).await;
446 }
447 }
448
449 if let Some(builtin) = builtin {
452 if !builtin.disabled {
453 return self.execute_via_builtin(builtin).await;
454 }
455 }
456
457 if !sys::fs::contains_path_separator(&self.command_name) {
460 let path = if let Some(path_dirs) = &self.path_dirs {
464 pathsearch::resolve_command(path_dirs, self.command_name.as_str())
465 } else {
466 self.shell
467 .resolve_command_in_path_using_cache(&self.command_name)
468 };
469
470 if let Some(path) = path {
471 self.execute_via_external(&path)
472 } else {
473 let last_arg = Self::take_last_arg(&self.args);
476 self.shell.update_last_arg_variable(last_arg);
477
478 Err(ErrorKind::CommandNotFound(self.command_name).into())
479 }
480 } else {
481 let command_name = sys::fs::resolve_executable_path(
482 PathBuf::from(&self.command_name),
483 self.shell.working_dir(),
484 );
485 self.execute_via_external(command_name.as_path())
486 }
487 }
488
489 fn take_last_arg(args: &[CommandArg]) -> Option<String> {
492 args.last().map(ToString::to_string)
493 }
494
495 async fn execute_via_builtin(
496 self,
497 builtin: builtins::Registration<SE>,
498 ) -> Result<ExecutionSpawnResult, error::Error> {
499 match self.shell {
500 ShellForCommand::OwnedShell { target, .. } => {
501 Ok(Self::execute_via_builtin_in_owned_shell(
502 *target,
503 self.params,
504 builtin,
505 self.command_name,
506 self.args,
507 )
508 .await)
509 }
510 ShellForCommand::ParentShell(..) => {
511 self.execute_via_builtin_in_parent_shell(builtin).await
512 }
513 }
514 }
515
516 fn execute_via_builtin_in_owned_shell(
519 mut shell: Shell<SE>,
520 params: ExecutionParameters,
521 builtin: builtins::Registration<SE>,
522 command_name: String,
523 args: Vec<CommandArg>,
524 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
525 let last_arg = Self::take_last_arg(&args);
526 let task = sys::tasks::spawn_blocking(async move {
529 let cmd_context = ExecutionContext {
530 shell: &mut shell,
531 command_name,
532 params,
533 };
534
535 let result = execute_builtin_command(&builtin, cmd_context, args).await;
536
537 shell.update_last_arg_variable(last_arg);
539
540 result
541 });
542
543 async move { ExecutionSpawnResult::StartedTask(task.await) }
544 }
545
546 async fn execute_via_builtin_in_parent_shell(
547 self,
548 builtin: builtins::Registration<SE>,
549 ) -> Result<ExecutionSpawnResult, error::Error> {
550 let mut shell = self.shell;
551 let last_arg = Self::take_last_arg(&self.args);
552
553 let cmd_context = ExecutionContext {
554 shell: &mut shell,
555 command_name: self.command_name,
556 params: self.params,
557 };
558
559 let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
560
561 shell.update_last_arg_variable(last_arg);
563
564 let result = result?;
565
566 Ok(result.into())
567 }
568
569 async fn execute_via_function(
570 self,
571 func_registration: functions::Registration,
572 ) -> Result<ExecutionSpawnResult, error::Error> {
573 match self.shell {
574 ShellForCommand::OwnedShell { target, .. } => {
575 Ok(Self::execute_via_function_in_owned_shell(
576 *target,
577 self.params,
578 func_registration,
579 self.command_name,
580 self.args,
581 )
582 .await)
583 }
584 ShellForCommand::ParentShell(..) => {
585 self.execute_via_function_in_parent_shell(func_registration)
586 .await
587 }
588 }
589 }
590
591 fn execute_via_function_in_owned_shell(
607 mut shell: Shell<SE>,
608 params: ExecutionParameters,
609 func_registration: functions::Registration,
610 command_name: String,
611 args: Vec<CommandArg>,
612 ) -> impl Future<Output = ExecutionSpawnResult> + use<SE> {
613 let last_arg = Self::take_last_arg(&args);
614 let task = sys::tasks::spawn_blocking(async move {
615 let cmd_context = ExecutionContext {
616 shell: &mut shell,
617 command_name,
618 params,
619 };
620
621 let spawn_result =
622 invoke_shell_function(func_registration, cmd_context, &args[1..]).await;
623
624 shell.update_last_arg_variable(last_arg);
626
627 match spawn_result? {
628 ExecutionSpawnResult::Completed(result) => Ok(result),
629 ExecutionSpawnResult::StartedProcess(_) | ExecutionSpawnResult::StartedTask(_) => {
630 error::unimp("invoke_shell_function returned an unexpected spawn result")
634 }
635 }
636 });
637
638 async move { ExecutionSpawnResult::StartedTask(task.await) }
639 }
640
641 async fn execute_via_function_in_parent_shell(
642 self,
643 func_registration: functions::Registration,
644 ) -> Result<ExecutionSpawnResult, error::Error> {
645 let mut shell = self.shell;
646 let last_arg = Self::take_last_arg(&self.args);
647
648 let cmd_context = ExecutionContext {
649 shell: &mut shell,
650 command_name: self.command_name,
651 params: self.params,
652 };
653
654 let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
656
657 shell.update_last_arg_variable(last_arg);
662
663 result
664 }
665
666 fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
667 let mut shell = self.shell;
668 let last_arg = Self::take_last_arg(&self.args);
669
670 let cmd_context = ExecutionContext {
671 shell: &mut shell,
672 command_name: self.command_name,
673 params: self.params,
674 };
675
676 let resolved_path = path.to_string_lossy();
677 let result = execute_external_command(
678 cmd_context,
679 resolved_path.as_ref(),
680 self.process_group_id,
681 self.argv0.as_deref(),
682 &self.args[1..],
683 );
684
685 shell.update_last_arg_variable(last_arg);
687
688 result
689 }
690}
691
692pub(crate) fn execute_external_command(
693 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
694 executable_path: &str,
695 process_group_id: Option<i32>,
696 argv0_override: Option<&str>,
697 args: &[CommandArg],
698) -> Result<ExecutionSpawnResult, error::Error> {
699 let cmd_args = args
701 .iter()
702 .filter_map(|e| {
703 if let CommandArg::String(s) = e {
704 Some(s)
705 } else {
706 None
707 }
708 })
709 .collect::<Vec<_>>();
710
711 let child_stdin_is_terminal = context
714 .try_fd(openfiles::OpenFiles::STDIN_FD)
715 .is_some_and(|f| f.is_terminal());
716
717 let new_pg = matches!(
719 context.params.process_group_policy,
720 ProcessGroupPolicy::NewProcessGroup
721 );
722
723 let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
728 #[allow(unused_mut, reason = "only mutated on unix platforms")]
729 let mut cmd = compose_std_command(
730 &context,
731 executable_path,
732 argv0,
733 cmd_args.as_slice(),
734 false, )?;
736
737 if new_pg {
739 if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
742 cmd.lead_session();
744 } else {
745 cmd.process_group(0);
747 if child_stdin_is_terminal {
748 cmd.take_foreground();
749 }
750 }
751 } else {
752 if let Some(pgid) = process_group_id {
754 cmd.process_group(pgid);
755 }
756 }
757
758 tracing::debug!(
760 target: trace_categories::COMMANDS,
761 "Spawning: cmd='{} {}'",
762 cmd.get_program().to_string_lossy().to_string(),
763 cmd.get_args()
764 .map(|a| a.to_string_lossy().to_string())
765 .join(" ")
766 );
767
768 match sys::process::spawn(cmd, context.shell.options().kill_external_commands_on_drop) {
769 Ok(child) => {
770 #[expect(clippy::cast_possible_wrap)]
772 let pid = child.id().map(|id| id as i32);
773 let mut actual_pgid = process_group_id;
774 if let Some(pid) = &pid {
775 if new_pg {
776 actual_pgid = Some(*pid);
777 }
778 } else {
779 tracing::warn!("could not retrieve pid for child process");
780 }
781
782 if let (Some(on_spawn), Some(pid)) = (&context.shell.options().spawn.on_spawn, pid) {
783 on_spawn(options::SpawnedProcess {
784 pid,
785 process_group: actual_pgid,
786 });
787 }
788
789 Ok(ExecutionSpawnResult::StartedProcess(
790 processes::ChildProcess::new(child, pid, actual_pgid),
791 ))
792 }
793 Err(spawn_err) => {
794 if context.shell.options().interactive {
795 sys::terminal::move_self_to_foreground()?;
796 }
797
798 Err(spawn_error(context, spawn_err))
799 }
800 }
801}
802
803pub fn missing_cgroup(options: &options::RuntimeOptions) -> Option<std::io::Error> {
810 let cgroup_procs = options.spawn.cgroup_procs.as_ref()?;
811 sys::commands::is_missing(cgroup_procs).then(|| {
812 std::io::Error::new(
813 std::io::ErrorKind::NotFound,
814 std::format!(
815 "cannot join the cgroup: {} is missing",
816 cgroup_procs.to_string_lossy()
817 ),
818 )
819 })
820}
821
822fn spawn_error(
824 context: ExecutionContext<'_, impl extensions::ShellExtensions>,
825 spawn_err: std::io::Error,
826) -> error::Error {
827 if spawn_err.kind() != std::io::ErrorKind::NotFound {
828 return error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err).into();
829 }
830
831 if let Some(missing) = missing_cgroup(context.shell.options()) {
832 error::ErrorKind::FailedToExecuteCommand(context.command_name, missing).into()
833 } else if !context.shell.working_dir().exists() {
834 error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned()).into()
835 } else {
836 error::ErrorKind::CommandNotFound(context.command_name).into()
837 }
838}
839
840async fn execute_builtin_command<SE: extensions::ShellExtensions>(
841 builtin: &builtins::Registration<SE>,
842 context: ExecutionContext<'_, SE>,
843 args: Vec<CommandArg>,
844) -> Result<ExecutionResult, error::Error> {
845 let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
848
849 match (builtin.execute_func)(context, args).await {
850 Ok(result) => Ok(result),
851 Err(e) => {
852 if let Some(io_err) = e.as_io_error() {
855 if io_err.kind() == std::io::ErrorKind::BrokenPipe {
856 return Ok(ExecutionExitCode::from(io_err).into());
857 }
858 }
859
860 Err(if mark_errors_fatal { e.into_fatal() } else { e })
861 }
862 }
863}
864
865pub(crate) async fn invoke_shell_function(
866 function: functions::Registration,
867 mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
868 args: &[CommandArg],
869) -> Result<ExecutionSpawnResult, error::Error> {
870 let ast::FunctionBody(body, redirects) = &function.definition().body;
871
872 if let Some(redirects) = redirects {
874 for redirect in &redirects.0 {
875 interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
876 }
877 }
878
879 let positional_args = args.iter().map(|a| a.to_string());
880
881 let mut shell =
884 context
885 .shell
886 .enter_function(context.command_name.as_str(), &function, positional_args)?;
887
888 let result = body.execute(&mut shell, &context.params).await;
895
896 drop(shell);
898
899 let mut result = result?;
901
902 match result.next_control_flow {
904 ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
905 return error::unimp("break or continue returned from function invocation");
906 }
907 ExecutionControlFlow::ReturnFromFunctionOrScript => {
908 result.next_control_flow = ExecutionControlFlow::Normal;
910 }
911 _ => {}
912 }
913
914 Ok(result.into())
915}
916
917pub(crate) async fn invoke_command_in_subshell_and_get_output(
918 shell: &mut Shell<impl extensions::ShellExtensions>,
919 params: &ExecutionParameters,
920 s: String,
921) -> Result<String, error::Error> {
922 let mut subshell = shell.clone();
924
925 if !shell.options().command_subst_inherits_errexit {
929 subshell.options_mut().exit_on_nonzero_command_exit = false;
930 }
931
932 let mut params = params.clone();
934 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
935
936 let (reader, writer) = sys::pipes::pipe()?;
938 params.set_fd(OpenFiles::STDOUT_FD, writer.into());
939
940 let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
941
942 let cmd_join_handle = sys::tasks::spawn(run_substitution_command(subshell, params, s));
943
944 let output_str = async_reader.read_to_string().await?;
945
946 let run_result = cmd_join_handle.await?;
948 let cmd_result = run_result?;
949
950 shell.set_last_exit_status(cmd_result.exit_code.into());
952
953 Ok(output_str)
957}
958
959async fn run_substitution_command(
960 mut shell: Shell<impl extensions::ShellExtensions>,
961 mut params: ExecutionParameters,
962 command: String,
963) -> Result<ExecutionResult, error::Error> {
964 let parse_result = shell.parse_string(command);
966
967 if let Ok(program) = &parse_result {
971 if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
972 interp::setup_redirect(&mut shell, &mut params, redir).await?;
973 std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
974 return Ok(ExecutionResult::new(0));
975 }
976 }
977
978 let source_info = crate::SourceInfo::from("main");
980
981 shell
983 .run_parsed_result(parse_result, &source_info, ¶ms)
984 .await
985}
986
987fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
990 let [complete] = program.complete_commands.as_slice() else {
992 return None;
993 };
994
995 let ast::CompoundList(items) = complete;
997 let [item] = items.as_slice() else {
998 return None;
999 };
1000
1001 let and_or = &item.0;
1003 if !and_or.additional.is_empty() {
1004 return None;
1005 }
1006
1007 let pipeline = &and_or.first;
1009 if pipeline.bang {
1010 return None;
1011 }
1012
1013 let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
1015 return None;
1016 };
1017
1018 if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
1020 return None;
1021 }
1022
1023 let prefix = simple_cmd.prefix.as_ref()?;
1025 let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
1026 return None;
1027 };
1028
1029 match redir {
1031 ast::IoRedirect::File(
1032 fd,
1033 ast::IoFileRedirectKind::Read,
1034 ast::IoFileRedirectTarget::Filename(..),
1035 ) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
1036 _ => None,
1037 }
1038}