brush_core/
interp.rs

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