brush_core/
interp.rs

1use brush_parser::ast::{self, CommandPrefixOrSuffixItem};
2use itertools::Itertools;
3use std::collections::VecDeque;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use sys::commands::ExitStatusExt;
7
8use crate::arithmetic::{self, ExpandAndEvaluate};
9use crate::commands::{self, CommandArg};
10use crate::env::{EnvironmentLookup, EnvironmentScope};
11use crate::openfiles::{OpenFile, OpenFiles};
12use crate::results::{
13    self, ExecutionExitCode, ExecutionResult, ExecutionSpawnResult, ExecutionWaitResult,
14};
15use crate::shell::Shell;
16use crate::variables::{
17    ArrayLiteral, ShellValue, ShellValueLiteral, ShellValueUnsetType, ShellVariable,
18};
19use crate::{ShellFd, error, expansion, extendedtests, jobs, openfiles, processes, sys, timing};
20
21impl From<processes::ProcessWaitResult> for results::ExecutionResult {
22    fn from(wait_result: processes::ProcessWaitResult) -> Self {
23        match wait_result {
24            processes::ProcessWaitResult::Completed(output) => output.into(),
25            processes::ProcessWaitResult::Stopped => Self::stopped(),
26        }
27    }
28}
29
30impl From<std::process::Output> for results::ExecutionResult {
31    fn from(output: std::process::Output) -> Self {
32        if let Some(code) = output.status.code() {
33            #[expect(clippy::cast_sign_loss)]
34            return Self::new((code & 0xFF) as u8);
35        }
36
37        if let Some(signal) = output.status.signal() {
38            #[expect(clippy::cast_sign_loss)]
39            return Self::new((signal & 0xFF) as u8 + 128);
40        }
41
42        tracing::error!("unhandled process exit");
43        Self::new(127)
44    }
45}
46
47/// Encapsulates the context of execution in a command pipeline.
48struct PipelineExecutionContext<'a> {
49    /// The shell in which the command is being executed.
50    shell: &'a mut Shell,
51
52    current_pipeline_index: usize,
53    pipeline_len: usize,
54    output_pipes: &'a mut Vec<std::io::PipeReader>,
55
56    process_group_id: Option<i32>,
57}
58
59/// Parameters for execution.
60#[derive(Clone, Default)]
61pub struct ExecutionParameters {
62    /// The open files tracked by the current context.
63    open_files: openfiles::OpenFiles,
64    /// Policy for how to manage spawned external processes.
65    pub process_group_policy: ProcessGroupPolicy,
66}
67
68impl ExecutionParameters {
69    /// Returns the standard input file; usable with `write!` et al.
70    ///
71    /// # Arguments
72    ///
73    /// * `shell` - The shell context.
74    pub fn stdin(&self, shell: &Shell) -> impl std::io::Read + 'static {
75        self.try_stdin(shell).unwrap()
76    }
77
78    /// Tries to retrieve the standard input file. Returns `None` if not set.
79    ///
80    /// # Arguments
81    ///
82    /// * `shell` - The shell context.
83    pub fn try_stdin(&self, shell: &Shell) -> Option<OpenFile> {
84        self.try_fd(shell, openfiles::OpenFiles::STDIN_FD)
85    }
86
87    /// Returns the standard output file; usable with `write!` et al.
88    ///
89    /// # Arguments
90    ///
91    /// * `shell` - The shell context.
92    pub fn stdout(&self, shell: &Shell) -> impl std::io::Write + 'static {
93        self.try_stdout(shell).unwrap()
94    }
95
96    /// Tries to retrieve the standard output file. Returns `None` if not set.
97    ///
98    /// # Arguments
99    ///
100    /// * `shell` - The shell context.
101    pub fn try_stdout(&self, shell: &Shell) -> Option<OpenFile> {
102        self.try_fd(shell, openfiles::OpenFiles::STDOUT_FD)
103    }
104
105    /// Returns the standard error file; usable with `write!` et al.
106    ///
107    /// # Arguments
108    ///
109    /// * `shell` - The shell context.
110    pub fn stderr(&self, shell: &Shell) -> impl std::io::Write + 'static {
111        self.try_stderr(shell).unwrap()
112    }
113
114    /// Tries to retrieve the standard error file. Returns `None` if not set.
115    ///
116    /// # Arguments
117    ///
118    /// * `shell` - The shell context.
119    pub fn try_stderr(&self, shell: &Shell) -> Option<OpenFile> {
120        self.try_fd(shell, openfiles::OpenFiles::STDERR_FD)
121    }
122
123    /// Returns the file descriptor with the given number. Returns `None`
124    /// if the file descriptor is not open.
125    ///
126    /// # Arguments
127    ///
128    /// * `shell` - The shell context.
129    /// * `fd` - The file descriptor number to retrieve.
130    pub fn try_fd(&self, shell: &Shell, fd: ShellFd) -> Option<openfiles::OpenFile> {
131        match self.open_files.fd_entry(fd) {
132            openfiles::OpenFileEntry::Open(f) => Some(f.clone()),
133            openfiles::OpenFileEntry::NotPresent => None,
134            openfiles::OpenFileEntry::NotSpecified => {
135                // We didn't have this fd specified one way or the other; we fallback
136                // to what's represented in the shell's open files.
137                shell.persistent_open_files().try_fd(fd).cloned()
138            }
139        }
140    }
141
142    /// Sets the given file descriptor to the provided open file.
143    ///
144    /// # Arguments
145    ///
146    /// * `fd` - The file descriptor number to set.
147    /// * `file` - The open file to set.
148    pub fn set_fd(&mut self, fd: ShellFd, file: openfiles::OpenFile) {
149        self.open_files.set_fd(fd, file);
150    }
151
152    /// Iterates over all open file descriptors in this context.
153    ///
154    /// # Arguments
155    ///
156    /// * `shell` - The shell context.
157    pub fn iter_fds(&self, shell: &Shell) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
158        let our_fds = self.open_files.iter_fds();
159        let shell_fds = shell
160            .persistent_open_files()
161            .iter_fds()
162            .filter(|(fd, _)| !self.open_files.contains_fd(*fd));
163
164        #[allow(clippy::needless_collect)]
165        let all_fds: Vec<_> = our_fds
166            .chain(shell_fds)
167            .map(|(fd, file)| (fd, file.clone()))
168            .collect();
169
170        all_fds.into_iter()
171    }
172}
173
174#[derive(Clone, Debug, Default)]
175/// Policy for how to manage spawned external processes.
176pub enum ProcessGroupPolicy {
177    /// Place the process in a new process group.
178    #[default]
179    NewProcessGroup,
180    /// Place the process in the same process group as its parent.
181    SameProcessGroup,
182}
183
184#[async_trait::async_trait]
185pub trait Execute {
186    async fn execute(
187        &self,
188        shell: &mut Shell,
189        params: &ExecutionParameters,
190    ) -> Result<ExecutionResult, error::Error>;
191}
192
193#[async_trait::async_trait]
194trait ExecuteInPipeline {
195    async fn execute_in_pipeline(
196        &self,
197        context: &mut PipelineExecutionContext<'_>,
198        params: ExecutionParameters,
199    ) -> Result<ExecutionSpawnResult, error::Error>;
200}
201
202#[async_trait::async_trait]
203impl Execute for ast::Program {
204    async fn execute(
205        &self,
206        shell: &mut Shell,
207        params: &ExecutionParameters,
208    ) -> Result<ExecutionResult, error::Error> {
209        let mut result = ExecutionResult::success();
210
211        for command in &self.complete_commands {
212            result = command.execute(shell, params).await?;
213            if !result.is_normal_flow() {
214                break;
215            }
216        }
217
218        *shell.last_exit_status_mut() = result.exit_code.into();
219        Ok(result)
220    }
221}
222
223#[async_trait::async_trait]
224impl Execute for ast::CompoundList {
225    async fn execute(
226        &self,
227        shell: &mut Shell,
228        params: &ExecutionParameters,
229    ) -> Result<ExecutionResult, error::Error> {
230        let mut result = ExecutionResult::success();
231
232        for ast::CompoundListItem(ao_list, sep) in &self.0 {
233            let run_async = matches!(sep, ast::SeparatorOperator::Async);
234
235            if run_async {
236                // TODO: Reenable launching in child process?
237                // let job = spawn_ao_list_in_child(ao_list, shell, params).await?;
238
239                let job = spawn_ao_list_in_task(ao_list, shell, params);
240                let job_formatted = job.to_pid_style_string();
241
242                if shell.options.interactive && !shell.is_subshell() {
243                    writeln!(params.stderr(shell), "{job_formatted}")?;
244                }
245
246                result = ExecutionResult::success();
247            } else {
248                result = ao_list.execute(shell, params).await?;
249            }
250
251            if !result.is_normal_flow() {
252                break;
253            }
254        }
255
256        *shell.last_exit_status_mut() = result.exit_code.into();
257        Ok(result)
258    }
259}
260
261fn spawn_ao_list_in_task<'a>(
262    ao_list: &ast::AndOrList,
263    shell: &'a mut Shell,
264    params: &ExecutionParameters,
265) -> &'a jobs::Job {
266    // Clone the inputs.
267    let mut cloned_shell = shell.clone();
268    let cloned_params = params.clone();
269    let cloned_ao_list = ao_list.clone();
270
271    // Mark the child shell as not interactive; we don't want it messing with the terminal too much.
272    cloned_shell.options.interactive = false;
273
274    let join_handle = tokio::spawn(async move {
275        cloned_ao_list
276            .execute(&mut cloned_shell, &cloned_params)
277            .await
278    });
279
280    shell.jobs.add_as_current(jobs::Job::new(
281        [jobs::JobTask::Internal(join_handle)],
282        ao_list.to_string(),
283        jobs::JobState::Running,
284    ))
285}
286
287#[async_trait::async_trait]
288impl Execute for ast::AndOrList {
289    async fn execute(
290        &self,
291        shell: &mut Shell,
292        params: &ExecutionParameters,
293    ) -> Result<ExecutionResult, error::Error> {
294        let mut result = self.first.execute(shell, params).await?;
295
296        for next_ao in &self.additional {
297            // Check for non-normal control flow.
298            if !result.is_normal_flow() {
299                break;
300            }
301
302            let (is_and, pipeline) = match next_ao {
303                ast::AndOr::And(p) => (true, p),
304                ast::AndOr::Or(p) => (false, p),
305            };
306
307            // If we short-circuit, then we don't break out of the whole loop
308            // but we skip evaluating the current pipeline. We'll then continue
309            // on and possibly evaluate a subsequent one (depending on the
310            // operator before it).
311            if is_and {
312                if !result.is_success() {
313                    continue;
314                }
315            } else if result.is_success() {
316                continue;
317            }
318
319            result = pipeline.execute(shell, params).await?;
320        }
321
322        Ok(result)
323    }
324}
325
326#[async_trait::async_trait]
327impl Execute for ast::Pipeline {
328    async fn execute(
329        &self,
330        shell: &mut Shell,
331        params: &ExecutionParameters,
332    ) -> Result<ExecutionResult, error::Error> {
333        // Capture current timing if so requested.
334        let stopwatch = self
335            .timed
336            .is_some()
337            .then(timing::start_timing)
338            .transpose()?;
339
340        // Spawn all the processes required for the pipeline, connecting outputs/inputs with pipes
341        // as needed.
342        let spawn_results = spawn_pipeline_processes(self, shell, params).await?;
343
344        // Wait for the processes. This also has a side effect of updating pipeline status.
345        let mut result =
346            wait_for_pipeline_processes_and_update_status(self, spawn_results, shell, params)
347                .await?;
348
349        // Invert the exit code if requested.
350        if self.bang {
351            result.exit_code = ExecutionExitCode::from(if result.is_success() { 1 } else { 0 });
352        }
353
354        // Update statuses.
355        *shell.last_exit_status_mut() = result.exit_code.into();
356
357        // If requested, report timing.
358        if let Some(timed) = &self.timed {
359            if let Some(mut stderr) = params.try_fd(shell, openfiles::OpenFiles::STDERR_FD) {
360                let timing = stopwatch.unwrap().stop()?;
361
362                if timed.is_posix_output() {
363                    std::write!(
364                        stderr,
365                        "real {}\nuser {}\nsys {}\n",
366                        timing::format_duration_posixly(&timing.wall),
367                        timing::format_duration_posixly(&timing.user),
368                        timing::format_duration_posixly(&timing.system),
369                    )?;
370                } else {
371                    std::write!(
372                        stderr,
373                        "\nreal\t{}\nuser\t{}\nsys\t{}\n",
374                        timing::format_duration_non_posixly(&timing.wall),
375                        timing::format_duration_non_posixly(&timing.user),
376                        timing::format_duration_non_posixly(&timing.system),
377                    )?;
378                }
379            }
380        }
381
382        Ok(result)
383    }
384}
385
386async fn spawn_pipeline_processes(
387    pipeline: &ast::Pipeline,
388    shell: &mut Shell,
389    params: &ExecutionParameters,
390) -> Result<VecDeque<ExecutionSpawnResult>, error::Error> {
391    let pipeline_len = pipeline.seq.len();
392    let mut output_pipes = vec![];
393    let mut spawn_results = VecDeque::new();
394    let mut process_group_id: Option<i32> = None;
395
396    for (current_pipeline_index, command) in pipeline.seq.iter().enumerate() {
397        //
398        // We run a command directly in the current shell if either of the following is true:
399        //     * There's only one command in the pipeline.
400        //     * This is the *last* command in the pipeline, the lastpipe option is enabled, and job
401        //       monitoring is disabled.
402        // Otherwise, we spawn a separate subshell for each command in the pipeline.
403        //
404
405        let run_in_current_shell = pipeline_len == 1
406            || (current_pipeline_index == pipeline_len - 1
407                && shell.options.run_last_pipeline_cmd_in_current_shell
408                && !shell.options.enable_job_control);
409
410        if !run_in_current_shell {
411            let mut subshell = shell.clone();
412            let mut pipeline_context = PipelineExecutionContext {
413                shell: &mut subshell,
414                current_pipeline_index,
415                pipeline_len,
416                output_pipes: &mut output_pipes,
417                process_group_id,
418            };
419
420            let mut cmd_params = params.clone();
421
422            // Make sure that all commands in the pipeline are in the same process group.
423            if current_pipeline_index > 0 {
424                cmd_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
425            }
426
427            spawn_results.push_back(
428                command
429                    .execute_in_pipeline(&mut pipeline_context, cmd_params)
430                    .await?,
431            );
432            process_group_id = pipeline_context.process_group_id;
433        } else {
434            let mut pipeline_context = PipelineExecutionContext {
435                shell,
436                current_pipeline_index,
437                pipeline_len,
438                output_pipes: &mut output_pipes,
439                process_group_id,
440            };
441
442            spawn_results.push_back(
443                command
444                    .execute_in_pipeline(&mut pipeline_context, params.clone())
445                    .await?,
446            );
447            process_group_id = pipeline_context.process_group_id;
448        }
449    }
450
451    Ok(spawn_results)
452}
453
454async fn wait_for_pipeline_processes_and_update_status(
455    pipeline: &ast::Pipeline,
456    mut process_spawn_results: VecDeque<ExecutionSpawnResult>,
457    shell: &mut Shell,
458    params: &ExecutionParameters,
459) -> Result<ExecutionResult, error::Error> {
460    let mut result = ExecutionResult::success();
461    let mut stopped_children = vec![];
462
463    // Clear our the pipeline status so we can start filling it out.
464    shell.last_pipeline_statuses.clear();
465
466    while let Some(child) = process_spawn_results.pop_front() {
467        match child.wait(!stopped_children.is_empty()).await? {
468            ExecutionWaitResult::Completed(current_result) => {
469                result = current_result;
470                *shell.last_exit_status_mut() = result.exit_code.into();
471                shell.last_pipeline_statuses.push(result.exit_code.into());
472            }
473            ExecutionWaitResult::Stopped(child) => {
474                result = ExecutionResult::stopped();
475                *shell.last_exit_status_mut() = result.exit_code.into();
476                shell.last_pipeline_statuses.push(result.exit_code.into());
477
478                stopped_children.push(jobs::JobTask::External(child));
479            }
480        }
481    }
482
483    if shell.options.interactive {
484        sys::terminal::move_self_to_foreground()?;
485    }
486
487    // If there were stopped jobs, then encapsulate the pipeline as a managed job and hand it
488    // off to the job manager.
489    if !stopped_children.is_empty() {
490        let job = shell.jobs.add_as_current(jobs::Job::new(
491            stopped_children,
492            pipeline.to_string(),
493            jobs::JobState::Stopped,
494        ));
495
496        let formatted = job.to_string();
497
498        // N.B. We use the '\r' to overwrite any ^Z output.
499        writeln!(params.stderr(shell), "\r{formatted}")?;
500    }
501
502    Ok(result)
503}
504
505#[async_trait::async_trait]
506impl ExecuteInPipeline for ast::Command {
507    async fn execute_in_pipeline(
508        &self,
509        pipeline_context: &mut PipelineExecutionContext<'_>,
510        mut params: ExecutionParameters,
511    ) -> Result<ExecutionSpawnResult, error::Error> {
512        if pipeline_context.shell.options.do_not_execute_commands {
513            return Ok(ExecutionSpawnResult::Completed(ExecutionResult::success()));
514        }
515
516        match self {
517            Self::Simple(simple) => simple.execute_in_pipeline(pipeline_context, params).await,
518            Self::Compound(compound, redirects) => {
519                // Set up pipelining.
520                setup_pipeline_redirection(&mut params.open_files, pipeline_context)?;
521
522                // Set up any additional redirects.
523                if let Some(redirects) = redirects {
524                    for redirect in &redirects.0 {
525                        setup_redirect(pipeline_context.shell, &mut params, redirect).await?;
526                    }
527                }
528
529                Ok(compound
530                    .execute(pipeline_context.shell, &params)
531                    .await?
532                    .into())
533            }
534            Self::Function(func) => Ok(func.execute(pipeline_context.shell, &params).await?.into()),
535            Self::ExtendedTest(e) => {
536                let result = if extendedtests::eval_extended_test_expr(
537                    &e.expr,
538                    pipeline_context.shell,
539                    &params,
540                )
541                .await?
542                {
543                    0
544                } else {
545                    1
546                };
547                Ok(ExecutionResult::new(result).into())
548            }
549        }
550    }
551}
552
553enum WhileOrUntil {
554    While,
555    Until,
556}
557
558#[async_trait::async_trait]
559impl Execute for ast::CompoundCommand {
560    async fn execute(
561        &self,
562        shell: &mut Shell,
563        params: &ExecutionParameters,
564    ) -> Result<ExecutionResult, error::Error> {
565        match self {
566            Self::BraceGroup(ast::BraceGroupCommand { list, .. }) => {
567                list.execute(shell, params).await
568            }
569            Self::Subshell(ast::SubshellCommand { list, .. }) => {
570                // Clone off a new subshell, and run the body of the subshell there.
571                let mut subshell = shell.clone();
572                let subshell_result = list.execute(&mut subshell, params).await?;
573
574                // Preserve the subshell's exit code, but don't honor any of its requests to exit
575                // the shell, break out of loops, etc.
576                Ok(ExecutionResult::from(subshell_result.exit_code))
577            }
578            Self::ForClause(f) => f.execute(shell, params).await,
579            Self::CaseClause(c) => c.execute(shell, params).await,
580            Self::IfClause(i) => i.execute(shell, params).await,
581            Self::WhileClause(w) => (WhileOrUntil::While, w).execute(shell, params).await,
582            Self::UntilClause(u) => (WhileOrUntil::Until, u).execute(shell, params).await,
583            Self::Arithmetic(a) => a.execute(shell, params).await,
584            Self::ArithmeticForClause(a) => a.execute(shell, params).await,
585        }
586    }
587}
588
589#[async_trait::async_trait]
590impl Execute for ast::ForClauseCommand {
591    async fn execute(
592        &self,
593        shell: &mut Shell,
594        params: &ExecutionParameters,
595    ) -> Result<ExecutionResult, error::Error> {
596        let mut result = ExecutionResult::success();
597
598        // If we were given explicit words to iterate over, then expand them all, with splitting
599        // enabled.
600        let mut expanded_values = vec![];
601        if let Some(unexpanded_values) = &self.values {
602            for value in unexpanded_values {
603                let mut expanded =
604                    expansion::full_expand_and_split_word(shell, params, value).await?;
605                expanded_values.append(&mut expanded);
606            }
607        } else {
608            // Otherwise, we use the current positional parameters.
609            expanded_values.extend_from_slice(&shell.positional_parameters);
610        }
611
612        for value in expanded_values {
613            if shell.options.print_commands_and_arguments {
614                if let Some(unexpanded_values) = &self.values {
615                    shell
616                        .trace_command(
617                            params,
618                            std::format!(
619                                "for {} in {}",
620                                self.variable_name,
621                                unexpanded_values.iter().join(" ")
622                            ),
623                        )
624                        .await?;
625                } else {
626                    shell
627                        .trace_command(params, std::format!("for {}", self.variable_name,))
628                        .await?;
629                }
630            }
631
632            // Update the variable.
633            shell.env.update_or_add(
634                &self.variable_name,
635                ShellValueLiteral::Scalar(value),
636                |_| Ok(()),
637                EnvironmentLookup::Anywhere,
638                EnvironmentScope::Global,
639            )?;
640
641            result = self.body.list.execute(shell, params).await?;
642            if result.is_return_or_exit() {
643                break;
644            }
645
646            let is_break = result.is_break();
647
648            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
649
650            if is_break || result.is_continue() {
651                break;
652            }
653        }
654
655        *shell.last_exit_status_mut() = result.exit_code.into();
656        Ok(result)
657    }
658}
659
660#[async_trait::async_trait]
661impl Execute for ast::CaseClauseCommand {
662    async fn execute(
663        &self,
664        shell: &mut Shell,
665        params: &ExecutionParameters,
666    ) -> Result<ExecutionResult, error::Error> {
667        // N.B. One would think it makes sense to trace the expanded value being switched
668        // on, but that's not it.
669        if shell.options.print_commands_and_arguments {
670            shell
671                .trace_command(params, std::format!("case {} in", &self.value))
672                .await?;
673        }
674
675        let expanded_value = expansion::basic_expand_word(shell, params, &self.value).await?;
676        let mut result: ExecutionResult = ExecutionResult::success();
677        let mut force_execute_next_case = false;
678
679        for case in &self.cases {
680            if force_execute_next_case {
681                force_execute_next_case = false;
682            } else {
683                let mut matches = false;
684                for pattern in &case.patterns {
685                    let expanded_pattern = expansion::basic_expand_pattern(shell, params, pattern)
686                        .await?
687                        .set_extended_globbing(shell.options.extended_globbing)
688                        .set_case_insensitive(shell.options.case_insensitive_conditionals);
689
690                    if expanded_pattern.exactly_matches(expanded_value.as_str())? {
691                        matches = true;
692                        break;
693                    }
694                }
695
696                if !matches {
697                    continue;
698                }
699            }
700
701            result = if let Some(case_cmd) = &case.cmd {
702                case_cmd.execute(shell, params).await?
703            } else {
704                ExecutionResult::success()
705            };
706
707            // Check for early return (return/exit) or loop control flow (break/continue)
708            if !result.is_normal_flow() {
709                break;
710            }
711
712            match case.post_action {
713                ast::CaseItemPostAction::ExitCase => break,
714                ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
715                    force_execute_next_case = true;
716                }
717                ast::CaseItemPostAction::ContinueEvaluatingCases => (),
718            }
719        }
720
721        *shell.last_exit_status_mut() = result.exit_code.into();
722
723        Ok(result)
724    }
725}
726
727#[async_trait::async_trait]
728impl Execute for ast::IfClauseCommand {
729    async fn execute(
730        &self,
731        shell: &mut Shell,
732        params: &ExecutionParameters,
733    ) -> Result<ExecutionResult, error::Error> {
734        let condition = self.condition.execute(shell, params).await?;
735
736        // Check if the condition itself resulted in non-normal control flow.
737        if !condition.is_normal_flow() {
738            return Ok(condition);
739        }
740
741        if condition.is_success() {
742            return self.then.execute(shell, params).await;
743        }
744
745        if let Some(elses) = &self.elses {
746            for else_clause in elses {
747                match &else_clause.condition {
748                    Some(else_condition) => {
749                        let else_condition_result = else_condition.execute(shell, params).await?;
750
751                        // Check if the elif condition caused non-normal control flow.
752                        if !else_condition_result.is_normal_flow() {
753                            return Ok(else_condition_result);
754                        }
755
756                        if else_condition_result.is_success() {
757                            return else_clause.body.execute(shell, params).await;
758                        }
759                    }
760                    None => {
761                        return else_clause.body.execute(shell, params).await;
762                    }
763                }
764            }
765        }
766
767        let result = ExecutionResult::success();
768        *shell.last_exit_status_mut() = result.exit_code.into();
769
770        Ok(result)
771    }
772}
773
774#[async_trait::async_trait]
775impl Execute for (WhileOrUntil, &ast::WhileOrUntilClauseCommand) {
776    async fn execute(
777        &self,
778        shell: &mut Shell,
779        params: &ExecutionParameters,
780    ) -> Result<ExecutionResult, error::Error> {
781        let is_while = match self.0 {
782            WhileOrUntil::While => true,
783            WhileOrUntil::Until => false,
784        };
785        let test_condition = &self.1.0;
786        let body = &self.1.1;
787
788        let mut result = ExecutionResult::success();
789
790        loop {
791            let condition_result = test_condition.execute(shell, params).await?;
792            if !condition_result.is_normal_flow() {
793                result = condition_result;
794
795                // If the condition has break/continue, the while/until loop itself
796                // consumes one level. We need to decrement the level before returning.
797                result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
798                break;
799            }
800
801            if condition_result.is_success() != is_while {
802                break;
803            }
804
805            result = body.list.execute(shell, params).await?;
806            if result.is_return_or_exit() {
807                break;
808            }
809
810            let is_break = result.is_break();
811
812            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
813
814            if is_break || result.is_continue() {
815                break;
816            }
817        }
818
819        Ok(result)
820    }
821}
822
823#[async_trait::async_trait]
824impl Execute for ast::ArithmeticCommand {
825    async fn execute(
826        &self,
827        shell: &mut Shell,
828        params: &ExecutionParameters,
829    ) -> Result<ExecutionResult, error::Error> {
830        let value = self.expr.eval(shell, params, true).await?;
831        let result = if value != 0 {
832            ExecutionResult::success()
833        } else {
834            ExecutionResult::general_error()
835        };
836
837        *shell.last_exit_status_mut() = result.exit_code.into();
838
839        Ok(result)
840    }
841}
842
843#[async_trait::async_trait]
844impl Execute for ast::ArithmeticForClauseCommand {
845    async fn execute(
846        &self,
847        shell: &mut Shell,
848        params: &ExecutionParameters,
849    ) -> Result<ExecutionResult, error::Error> {
850        let mut result = ExecutionResult::success();
851        if let Some(initializer) = &self.initializer {
852            initializer.eval(shell, params, true).await?;
853        }
854
855        loop {
856            if let Some(condition) = &self.condition {
857                if condition.eval(shell, params, true).await? == 0 {
858                    break;
859                }
860            }
861
862            result = self.body.list.execute(shell, params).await?;
863            if result.is_return_or_exit() {
864                break;
865            }
866
867            let is_break = result.is_break();
868
869            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
870
871            if is_break || result.is_continue() {
872                break;
873            }
874
875            if let Some(updater) = &self.updater {
876                updater.eval(shell, params, true).await?;
877            }
878        }
879
880        *shell.last_exit_status_mut() = result.exit_code.into();
881        Ok(result)
882    }
883}
884
885#[async_trait::async_trait]
886impl Execute for ast::FunctionDefinition {
887    async fn execute(
888        &self,
889        shell: &mut Shell,
890        _params: &ExecutionParameters,
891    ) -> Result<ExecutionResult, error::Error> {
892        shell.define_func(self.fname.value.clone(), self.clone());
893
894        let result = ExecutionResult::success();
895        *shell.last_exit_status_mut() = result.exit_code.into();
896
897        Ok(result)
898    }
899}
900
901#[async_trait::async_trait]
902#[allow(clippy::too_many_lines)]
903impl ExecuteInPipeline for ast::SimpleCommand {
904    async fn execute_in_pipeline(
905        &self,
906        context: &mut PipelineExecutionContext<'_>,
907        mut params: ExecutionParameters,
908    ) -> Result<ExecutionSpawnResult, error::Error> {
909        let prefix_iter = self.prefix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
910        let suffix_iter = self.suffix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
911        let cmd_name_items = self
912            .word_or_name
913            .as_ref()
914            .map(|won| CommandPrefixOrSuffixItem::Word(won.clone()));
915
916        // Set up pipelining.
917        setup_pipeline_redirection(&mut params.open_files, context)?;
918
919        let mut assignments = vec![];
920        let mut args: Vec<CommandArg> = vec![];
921        let mut command_takes_assignments = false;
922
923        for item in prefix_iter.chain(cmd_name_items.iter()).chain(suffix_iter) {
924            match item {
925                CommandPrefixOrSuffixItem::IoRedirect(redirect) => {
926                    if let Err(e) = setup_redirect(context.shell, &mut params, redirect).await {
927                        writeln!(params.stderr(context.shell), "error: {e}")?;
928                        return Ok(ExecutionResult::general_error().into());
929                    }
930                }
931                CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell_command) => {
932                    let (installed_fd_num, substitution_file) =
933                        setup_process_substitution(context.shell, &params, kind, subshell_command)?;
934
935                    params
936                        .open_files
937                        .set_fd(installed_fd_num, substitution_file);
938
939                    args.push(CommandArg::String(std::format!(
940                        "/dev/fd/{installed_fd_num}"
941                    )));
942                }
943                CommandPrefixOrSuffixItem::AssignmentWord(assignment, word) => {
944                    if args.is_empty() {
945                        // If we haven't yet seen any arguments, then this must be a proper
946                        // scoped assignment. Add it to the list we're accumulating.
947                        assignments.push(assignment);
948                    } else {
949                        if command_takes_assignments {
950                            // This looks like an assignment, and the command being invoked is a
951                            // well-known builtin that takes arguments that need to function like
952                            // assignments (but which are processed by the builtin).
953                            let expanded =
954                                expand_assignment(context.shell, &params, assignment).await?;
955                            args.push(CommandArg::Assignment(expanded));
956                        } else {
957                            // This *looks* like an assignment, but it's really a string we should
958                            // fully treat as a regular looking
959                            // argument.
960                            let mut next_args =
961                                expansion::full_expand_and_split_word(context.shell, &params, word)
962                                    .await?
963                                    .into_iter()
964                                    .map(CommandArg::String)
965                                    .collect();
966                            args.append(&mut next_args);
967                        }
968                    }
969                }
970                CommandPrefixOrSuffixItem::Word(arg) => {
971                    let mut next_args =
972                        expansion::full_expand_and_split_word(context.shell, &params, arg).await?;
973
974                    if args.is_empty() {
975                        if let Some(cmd_name) = next_args.first() {
976                            if let Some(alias_value) = context.shell.aliases.get(cmd_name.as_str())
977                            {
978                                //
979                                // TODO(#57): This is a total hack; aliases are supposed to be
980                                // handled much earlier in the process.
981                                //
982                                let mut alias_pieces: Vec<_> = alias_value
983                                    .split_ascii_whitespace()
984                                    .map(|i| i.to_owned())
985                                    .collect();
986
987                                next_args.remove(0);
988                                alias_pieces.append(&mut next_args);
989
990                                next_args = alias_pieces;
991                            }
992
993                            let first_arg = next_args[0].as_str();
994
995                            // Check if we're going to be invoking a special declaration builtin.
996                            // That will change how we parse and process args.
997                            if context
998                                .shell
999                                .builtins()
1000                                .get(first_arg)
1001                                .is_some_and(|r| !r.disabled && r.declaration_builtin)
1002                            {
1003                                command_takes_assignments = true;
1004                            }
1005                        }
1006                    }
1007
1008                    let mut next_args = next_args.into_iter().map(CommandArg::String).collect();
1009                    args.append(&mut next_args);
1010                }
1011            }
1012        }
1013
1014        // If we have a command, then execute it.
1015        if let Some(CommandArg::String(cmd_name)) = args.first().cloned() {
1016            let mut stderr = params.stderr(context.shell);
1017
1018            match execute_command(context, params, cmd_name, assignments, args).await {
1019                Ok(result) => Ok(result),
1020                Err(err) => {
1021                    let _ = context.shell.display_error(&mut stderr, &err).await;
1022                    let exit_code = ExecutionExitCode::from(&err);
1023                    Ok(ExecutionResult::from(exit_code).into())
1024                }
1025            }
1026        } else {
1027            // Reset last status.
1028            *context.shell.last_exit_status_mut() = 0;
1029
1030            // No command to run; assignments must be applied to this shell.
1031            for assignment in assignments {
1032                apply_assignment(
1033                    assignment,
1034                    context.shell,
1035                    &params,
1036                    false,
1037                    None,
1038                    EnvironmentScope::Global,
1039                )
1040                .await?;
1041            }
1042
1043            // Return the last exit status we have; in some cases, an expansion
1044            // might result in a non-zero exit status stored in the shell.
1045            Ok(ExecutionResult::new(context.shell.last_result()).into())
1046        }
1047    }
1048}
1049
1050async fn execute_command(
1051    context: &mut PipelineExecutionContext<'_>,
1052    params: ExecutionParameters,
1053    cmd_name: String,
1054    assignments: Vec<&ast::Assignment>,
1055    args: Vec<CommandArg>,
1056) -> Result<ExecutionSpawnResult, error::Error> {
1057    // Push a new ephemeral environment scope for the duration of the command. We'll
1058    // set command-scoped variable assignments after doing so, and revert them before
1059    // returning.
1060    context.shell.env.push_scope(EnvironmentScope::Command);
1061    for assignment in &assignments {
1062        // Ensure it's tagged as exported and created in the command scope.
1063        apply_assignment(
1064            assignment,
1065            context.shell,
1066            &params,
1067            true,
1068            Some(EnvironmentScope::Command),
1069            EnvironmentScope::Command,
1070        )
1071        .await?;
1072    }
1073
1074    if context.shell.options.print_commands_and_arguments {
1075        context
1076            .shell
1077            .trace_command(
1078                &params,
1079                args.iter().map(|arg| arg.quote_for_tracing()).join(" "),
1080            )
1081            .await?;
1082    }
1083
1084    let mut cmd_context = commands::ExecutionContext {
1085        shell: context.shell,
1086        command_name: cmd_name,
1087        params,
1088    };
1089
1090    // Run through any pre-execution hooks.
1091    commands::on_preexecute(&mut cmd_context, args.as_slice()).await?;
1092
1093    // Execute.
1094    let execution_result = commands::execute(
1095        cmd_context,
1096        &mut context.process_group_id,
1097        args,
1098        true, /* use functions? */
1099        None,
1100    )
1101    .await;
1102
1103    // Pop off that ephemeral environment scope.
1104    // TODO: jobs: do we need to move self back to foreground on error here?
1105    context.shell.env.pop_scope(EnvironmentScope::Command)?;
1106
1107    execution_result
1108}
1109
1110async fn expand_assignment(
1111    shell: &mut Shell,
1112    params: &ExecutionParameters,
1113    assignment: &ast::Assignment,
1114) -> Result<ast::Assignment, error::Error> {
1115    let value = expand_assignment_value(shell, params, &assignment.value).await?;
1116    Ok(ast::Assignment {
1117        name: basic_expand_assignment_name(shell, params, &assignment.name).await?,
1118        value,
1119        append: assignment.append,
1120        loc: assignment.loc.clone(),
1121    })
1122}
1123
1124async fn basic_expand_assignment_name(
1125    shell: &mut Shell,
1126    params: &ExecutionParameters,
1127    name: &ast::AssignmentName,
1128) -> Result<ast::AssignmentName, error::Error> {
1129    match name {
1130        ast::AssignmentName::VariableName(name) => {
1131            let expanded = expansion::basic_expand_str(shell, params, name).await?;
1132            Ok(ast::AssignmentName::VariableName(expanded))
1133        }
1134        ast::AssignmentName::ArrayElementName(name, index) => {
1135            let expanded_name = expansion::basic_expand_str(shell, params, name).await?;
1136            let expanded_index = expansion::basic_expand_str(shell, params, index).await?;
1137            Ok(ast::AssignmentName::ArrayElementName(
1138                expanded_name,
1139                expanded_index,
1140            ))
1141        }
1142    }
1143}
1144
1145async fn expand_assignment_value(
1146    shell: &mut Shell,
1147    params: &ExecutionParameters,
1148    value: &ast::AssignmentValue,
1149) -> Result<ast::AssignmentValue, error::Error> {
1150    let expanded = match value {
1151        ast::AssignmentValue::Scalar(s) => {
1152            let expanded_word = expansion::basic_expand_word(shell, params, s).await?;
1153            ast::AssignmentValue::Scalar(ast::Word::from(expanded_word))
1154        }
1155        ast::AssignmentValue::Array(arr) => {
1156            let mut expanded_values = vec![];
1157            for (key, value) in arr {
1158                if let Some(k) = key {
1159                    let expanded_key = expansion::basic_expand_word(shell, params, k).await?.into();
1160                    let expanded_value = expansion::basic_expand_word(shell, params, value)
1161                        .await?
1162                        .into();
1163                    expanded_values.push((Some(expanded_key), expanded_value));
1164                } else {
1165                    let split_expanded_value =
1166                        expansion::full_expand_and_split_word(shell, params, value).await?;
1167                    for expanded_value in split_expanded_value {
1168                        expanded_values.push((None, expanded_value.into()));
1169                    }
1170                }
1171            }
1172
1173            ast::AssignmentValue::Array(expanded_values)
1174        }
1175    };
1176
1177    Ok(expanded)
1178}
1179
1180#[expect(clippy::too_many_lines)]
1181async fn apply_assignment(
1182    assignment: &ast::Assignment,
1183    shell: &mut Shell,
1184    params: &ExecutionParameters,
1185    mut export: bool,
1186    required_scope: Option<EnvironmentScope>,
1187    creation_scope: EnvironmentScope,
1188) -> Result<(), error::Error> {
1189    // Figure out if we are trying to assign to a variable or assign to an element of an existing
1190    // array.
1191    let mut array_index;
1192    let variable_name = match &assignment.name {
1193        ast::AssignmentName::VariableName(name) => {
1194            array_index = None;
1195            name
1196        }
1197        ast::AssignmentName::ArrayElementName(name, index) => {
1198            let expanded = expansion::basic_expand_str(shell, params, index).await?;
1199            array_index = Some(expanded);
1200            name
1201        }
1202    };
1203
1204    // Expand the values.
1205    let new_value = match &assignment.value {
1206        ast::AssignmentValue::Scalar(unexpanded_value) => {
1207            let value = expansion::basic_expand_word(shell, params, unexpanded_value).await?;
1208            ShellValueLiteral::Scalar(value)
1209        }
1210        ast::AssignmentValue::Array(unexpanded_values) => {
1211            let mut elements = vec![];
1212            for (unexpanded_key, unexpanded_value) in unexpanded_values {
1213                let key = match unexpanded_key {
1214                    Some(unexpanded_key) => {
1215                        Some(expansion::basic_expand_word(shell, params, unexpanded_key).await?)
1216                    }
1217                    None => None,
1218                };
1219
1220                if key.is_some() {
1221                    let value =
1222                        expansion::basic_expand_word(shell, params, unexpanded_value).await?;
1223                    elements.push((key, value));
1224                } else {
1225                    let values =
1226                        expansion::full_expand_and_split_word(shell, params, unexpanded_value)
1227                            .await?;
1228                    for value in values {
1229                        elements.push((None, value));
1230                    }
1231                }
1232            }
1233            ShellValueLiteral::Array(ArrayLiteral(elements))
1234        }
1235    };
1236
1237    if shell.options.print_commands_and_arguments {
1238        let op = if assignment.append { "+=" } else { "=" };
1239        shell
1240            .trace_command(params, std::format!("{}{op}{new_value}", assignment.name))
1241            .await?;
1242    }
1243
1244    // See if we need to eval an array index.
1245    if let Some(idx) = &array_index {
1246        let will_be_indexed_array = if let Some((_, existing_value)) = shell.env.get(variable_name)
1247        {
1248            matches!(
1249                existing_value.value(),
1250                ShellValue::IndexedArray(_) | ShellValue::Unset(ShellValueUnsetType::IndexedArray)
1251            )
1252        } else {
1253            true
1254        };
1255
1256        if will_be_indexed_array {
1257            array_index = Some(
1258                arithmetic::expand_and_eval(shell, params, idx.as_str(), false)
1259                    .await?
1260                    .to_string(),
1261            );
1262        }
1263    }
1264
1265    // See if we can find an existing value associated with the variable.
1266    if let Some((existing_value_scope, existing_value)) = shell.env.get_mut(variable_name.as_str())
1267    {
1268        if required_scope.is_none() || Some(existing_value_scope) == required_scope {
1269            if let Some(array_index) = array_index {
1270                match new_value {
1271                    ShellValueLiteral::Scalar(s) => {
1272                        existing_value.assign_at_index(array_index, s, assignment.append)?;
1273                    }
1274                    ShellValueLiteral::Array(_) => {
1275                        return error::unimp("replacing an array item with an array");
1276                    }
1277                }
1278            } else {
1279                if !export
1280                    && shell.options.export_variables_on_modification
1281                    && !matches!(new_value, ShellValueLiteral::Array(_))
1282                {
1283                    export = true;
1284                }
1285
1286                existing_value.assign(new_value, assignment.append)?;
1287            }
1288
1289            if export {
1290                existing_value.export();
1291            }
1292
1293            // That's it!
1294            return Ok(());
1295        }
1296    }
1297
1298    // If we fell down here, then we need to add it.
1299    let new_value = if let Some(array_index) = array_index {
1300        match new_value {
1301            ShellValueLiteral::Scalar(s) => {
1302                ShellValue::indexed_array_from_literals(ArrayLiteral(vec![(Some(array_index), s)]))
1303            }
1304            ShellValueLiteral::Array(_) => {
1305                return error::unimp("cannot assign list to array member");
1306            }
1307        }
1308    } else {
1309        match new_value {
1310            ShellValueLiteral::Scalar(s) => {
1311                export = export || shell.options.export_variables_on_modification;
1312                ShellValue::String(s)
1313            }
1314            ShellValueLiteral::Array(values) => ShellValue::indexed_array_from_literals(values),
1315        }
1316    };
1317
1318    let mut new_var = ShellVariable::new(new_value);
1319
1320    if export {
1321        new_var.export();
1322    }
1323
1324    shell.env.add(variable_name, new_var, creation_scope)
1325}
1326
1327fn setup_pipeline_redirection(
1328    open_files: &mut OpenFiles,
1329    context: &mut PipelineExecutionContext<'_>,
1330) -> Result<(), error::Error> {
1331    if context.current_pipeline_index > 0 {
1332        // Find the stdout from the preceding process.
1333        if let Some(preceding_output_reader) = context.output_pipes.pop() {
1334            // Set up stdin of this process to take stdout of the preceding process.
1335            open_files.set_fd(OpenFiles::STDIN_FD, preceding_output_reader.into());
1336        } else {
1337            open_files.set_fd(OpenFiles::STDIN_FD, openfiles::null()?);
1338        }
1339    }
1340
1341    // If this is a non-last command in a multi-command, then we need to arrange to redirect output
1342    // to a pipe that we can read later.
1343    if context.pipeline_len > 1 && context.current_pipeline_index < context.pipeline_len - 1 {
1344        // Set up stdout of this process to go to stdin of the succeeding process.
1345        let (reader, writer) = std::io::pipe()?;
1346        context.output_pipes.push(reader);
1347        open_files.set_fd(OpenFiles::STDOUT_FD, writer.into());
1348    }
1349
1350    Ok(())
1351}
1352
1353#[expect(clippy::too_many_lines)]
1354pub(crate) async fn setup_redirect(
1355    shell: &mut Shell,
1356    params: &'_ mut ExecutionParameters,
1357    redirect: &ast::IoRedirect,
1358) -> Result<(), error::Error> {
1359    match redirect {
1360        ast::IoRedirect::OutputAndError(f, append) => {
1361            let mut expanded_fields =
1362                expansion::full_expand_and_split_word(shell, params, f).await?;
1363            if expanded_fields.len() != 1 {
1364                return Err(error::ErrorKind::InvalidRedirection.into());
1365            }
1366
1367            let expanded_file_path: PathBuf =
1368                shell.absolute_path(Path::new(expanded_fields.remove(0).as_str()));
1369
1370            let mut file_options = std::fs::File::options();
1371            file_options
1372                .create(true)
1373                .write(true)
1374                .truncate(!*append)
1375                .append(*append);
1376
1377            let stdout_file = shell
1378                .open_file(&file_options, &expanded_file_path, params)
1379                .map_err(|err| {
1380                    error::ErrorKind::RedirectionFailure(
1381                        expanded_file_path.to_string_lossy().to_string(),
1382                        err.to_string(),
1383                    )
1384                })?;
1385
1386            let stderr_file = stdout_file.try_clone()?;
1387
1388            params.open_files.set_fd(OpenFiles::STDOUT_FD, stdout_file);
1389            params.open_files.set_fd(OpenFiles::STDERR_FD, stderr_file);
1390        }
1391
1392        ast::IoRedirect::File(specified_fd_num, kind, target) => {
1393            match target {
1394                ast::IoFileRedirectTarget::Filename(f) => {
1395                    let mut options = std::fs::File::options();
1396
1397                    let mut expanded_fields =
1398                        expansion::full_expand_and_split_word(shell, params, f).await?;
1399
1400                    if expanded_fields.len() != 1 {
1401                        return Err(error::ErrorKind::InvalidRedirection.into());
1402                    }
1403
1404                    let expanded_file_path: PathBuf =
1405                        shell.absolute_path(Path::new(expanded_fields.remove(0).as_str()));
1406
1407                    let default_fd_if_unspecified = get_default_fd_for_redirect_kind(kind);
1408                    match kind {
1409                        ast::IoFileRedirectKind::Read => {
1410                            options.read(true);
1411                        }
1412                        ast::IoFileRedirectKind::Write => {
1413                            if shell
1414                                .options
1415                                .disallow_overwriting_regular_files_via_output_redirection
1416                            {
1417                                // First check to see if the path points to an existing regular
1418                                // file.
1419                                if !expanded_file_path.is_file() {
1420                                    options.create(true);
1421                                } else {
1422                                    options.create_new(true);
1423                                }
1424                                options.write(true);
1425                            } else {
1426                                options.create(true);
1427                                options.write(true);
1428                                options.truncate(true);
1429                            }
1430                        }
1431                        ast::IoFileRedirectKind::Append => {
1432                            options.create(true);
1433                            options.append(true);
1434                        }
1435                        ast::IoFileRedirectKind::ReadAndWrite => {
1436                            options.create(true);
1437                            options.read(true);
1438                            options.write(true);
1439                        }
1440                        ast::IoFileRedirectKind::Clobber => {
1441                            options.create(true);
1442                            options.write(true);
1443                            options.truncate(true);
1444                        }
1445                        ast::IoFileRedirectKind::DuplicateInput => {
1446                            options.read(true);
1447                        }
1448                        ast::IoFileRedirectKind::DuplicateOutput => {
1449                            options.create(true);
1450                            options.write(true);
1451                        }
1452                    }
1453
1454                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1455
1456                    let opened_file = shell
1457                        .open_file(&options, &expanded_file_path, params)
1458                        .map_err(|err| {
1459                            error::ErrorKind::RedirectionFailure(
1460                                expanded_file_path.to_string_lossy().to_string(),
1461                                err.to_string(),
1462                            )
1463                        })?;
1464
1465                    params.open_files.set_fd(fd_num, opened_file);
1466                }
1467
1468                ast::IoFileRedirectTarget::Fd(fd) => {
1469                    let default_fd_if_unspecified = match kind {
1470                        ast::IoFileRedirectKind::DuplicateInput => 0,
1471                        ast::IoFileRedirectKind::DuplicateOutput => 1,
1472                        _ => {
1473                            return error::unimp("unexpected redirect kind");
1474                        }
1475                    };
1476
1477                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1478
1479                    if let Some(f) = params.try_fd(shell, *fd) {
1480                        let target_file = f.try_clone()?;
1481
1482                        params.open_files.set_fd(fd_num, target_file);
1483                    } else {
1484                        return Err(error::ErrorKind::BadFileDescriptor(*fd).into());
1485                    }
1486                }
1487
1488                ast::IoFileRedirectTarget::Duplicate(word) => {
1489                    let default_fd_if_unspecified = match kind {
1490                        ast::IoFileRedirectKind::DuplicateInput => 0,
1491                        ast::IoFileRedirectKind::DuplicateOutput => 1,
1492                        _ => {
1493                            return error::unimp("unexpected redirect kind");
1494                        }
1495                    };
1496
1497                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1498
1499                    let mut expanded_fields =
1500                        expansion::full_expand_and_split_word(shell, params, word).await?;
1501
1502                    if expanded_fields.len() != 1 {
1503                        return Err(error::ErrorKind::InvalidRedirection.into());
1504                    }
1505
1506                    let mut expanded = expanded_fields.remove(0);
1507
1508                    let dash = if expanded.ends_with('-') {
1509                        expanded.pop();
1510                        true
1511                    } else {
1512                        false
1513                    };
1514
1515                    if expanded.is_empty() {
1516                        // Nothing to do
1517                    } else if expanded.chars().all(|c: char| c.is_ascii_digit()) {
1518                        let source_fd_num = expanded
1519                            .parse::<ShellFd>()
1520                            .map_err(|_| error::ErrorKind::InvalidRedirection)?;
1521
1522                        // Duplicate the fd.
1523                        let target_file = if let Some(f) = params.try_fd(shell, source_fd_num) {
1524                            f.try_clone()?
1525                        } else {
1526                            return Err(error::ErrorKind::BadFileDescriptor(source_fd_num).into());
1527                        };
1528
1529                        params.open_files.set_fd(fd_num, target_file);
1530                    } else {
1531                        return Err(error::ErrorKind::InvalidRedirection.into());
1532                    }
1533
1534                    if dash {
1535                        // Close the specified fd. Ignore it if it's not valid.
1536                        params.open_files.remove_fd(fd_num);
1537                    }
1538                }
1539
1540                ast::IoFileRedirectTarget::ProcessSubstitution(substitution_kind, subshell_cmd) => {
1541                    match kind {
1542                        ast::IoFileRedirectKind::Read
1543                        | ast::IoFileRedirectKind::Write
1544                        | ast::IoFileRedirectKind::Append
1545                        | ast::IoFileRedirectKind::ReadAndWrite
1546                        | ast::IoFileRedirectKind::Clobber => {
1547                            let (substitution_fd, substitution_file) = setup_process_substitution(
1548                                shell,
1549                                params,
1550                                substitution_kind,
1551                                subshell_cmd,
1552                            )?;
1553
1554                            let target_file = substitution_file.try_clone()?;
1555                            params.open_files.set_fd(substitution_fd, substitution_file);
1556
1557                            let fd_num = specified_fd_num
1558                                .unwrap_or_else(|| get_default_fd_for_redirect_kind(kind));
1559
1560                            params.open_files.set_fd(fd_num, target_file);
1561                        }
1562                        _ => return error::unimp("invalid process substitution"),
1563                    }
1564                }
1565            }
1566        }
1567
1568        ast::IoRedirect::HereDocument(fd_num, io_here) => {
1569            // If not specified, default to stdin (fd 0).
1570            let fd_num = fd_num.unwrap_or(0);
1571
1572            // Expand if required.
1573            let io_here_doc = if io_here.requires_expansion {
1574                expansion::basic_expand_word(shell, params, &io_here.doc).await?
1575            } else {
1576                io_here.doc.flatten()
1577            };
1578
1579            let f = setup_open_file_with_contents(io_here_doc.as_str())?;
1580
1581            params.open_files.set_fd(fd_num, f);
1582        }
1583
1584        ast::IoRedirect::HereString(fd_num, word) => {
1585            // If not specified, default to stdin (fd 0).
1586            let fd_num = fd_num.unwrap_or(0);
1587
1588            let mut expanded_word = expansion::basic_expand_word(shell, params, word).await?;
1589            expanded_word.push('\n');
1590
1591            let f = setup_open_file_with_contents(expanded_word.as_str())?;
1592
1593            params.open_files.set_fd(fd_num, f);
1594        }
1595    }
1596
1597    Ok(())
1598}
1599
1600const fn get_default_fd_for_redirect_kind(kind: &ast::IoFileRedirectKind) -> ShellFd {
1601    match kind {
1602        ast::IoFileRedirectKind::Read => 0,
1603        ast::IoFileRedirectKind::Write => 1,
1604        ast::IoFileRedirectKind::Append => 1,
1605        ast::IoFileRedirectKind::ReadAndWrite => 0,
1606        ast::IoFileRedirectKind::Clobber => 1,
1607        ast::IoFileRedirectKind::DuplicateInput => 0,
1608        ast::IoFileRedirectKind::DuplicateOutput => 1,
1609    }
1610}
1611
1612fn setup_process_substitution(
1613    shell: &Shell,
1614    params: &ExecutionParameters,
1615    kind: &ast::ProcessSubstitutionKind,
1616    subshell_cmd: &ast::SubshellCommand,
1617) -> Result<(ShellFd, OpenFile), error::Error> {
1618    // TODO: Don't execute synchronously!
1619    // Execute in a subshell.
1620    let mut subshell = shell.clone();
1621
1622    // Set up execution parameters for the child execution.
1623    let mut child_params = params.clone();
1624    child_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
1625
1626    // Set up pipe so we can connect to the command.
1627    let (reader, writer) = std::io::pipe()?;
1628    let (reader, writer) = (reader.into(), writer.into());
1629
1630    let target_file = match kind {
1631        ast::ProcessSubstitutionKind::Read => {
1632            child_params.open_files.set_fd(OpenFiles::STDOUT_FD, writer);
1633            reader
1634        }
1635        ast::ProcessSubstitutionKind::Write => {
1636            child_params.open_files.set_fd(OpenFiles::STDIN_FD, reader);
1637            writer
1638        }
1639    };
1640
1641    // Asynchronously spawn off the subshell; we intentionally don't block on its
1642    // completion.
1643    let subshell_cmd = subshell_cmd.to_owned();
1644    tokio::spawn(async move {
1645        // Intentionally ignore the result of the subshell command.
1646        let _ = subshell_cmd
1647            .list
1648            .execute(&mut subshell, &child_params)
1649            .await;
1650    });
1651
1652    // Starting at 63 (a.k.a. 64-1)--and decrementing--look for an
1653    // available fd.
1654    let mut candidate_fd_num = 63;
1655    while params.open_files.contains_fd(candidate_fd_num) {
1656        candidate_fd_num -= 1;
1657        if candidate_fd_num == 0 {
1658            return error::unimp("no available file descriptors");
1659        }
1660    }
1661
1662    Ok((candidate_fd_num, target_file))
1663}
1664
1665fn setup_open_file_with_contents(contents: &str) -> Result<OpenFile, error::Error> {
1666    let (reader, mut writer) = std::io::pipe()?;
1667
1668    let bytes = contents.as_bytes();
1669
1670    #[cfg(target_os = "linux")]
1671    {
1672        use std::os::fd::AsFd as _;
1673
1674        let len = i32::try_from(bytes.len())?;
1675        nix::fcntl::fcntl(reader.as_fd(), nix::fcntl::FcntlArg::F_SETPIPE_SZ(len))?;
1676    }
1677
1678    writer.write_all(bytes)?;
1679    drop(writer);
1680
1681    Ok(reader.into())
1682}