Skip to main content

shrub_core/
interp.rs

1use std::borrow::Cow;
2use std::collections::VecDeque;
3use std::io::Write;
4use std::path::{Path, PathBuf};
5
6use itertools::Itertools;
7use shrub_parser::ast::{self, CommandPrefixOrSuffixItem};
8
9use crate::arithmetic::{self, ExpandAndEvaluate};
10use crate::commands::{self, CommandArg};
11use crate::env::{EnvironmentLookup, EnvironmentScope, valid_variable_name};
12use crate::openfiles::{OpenFile, OpenFiles};
13use crate::results::{
14    ExecutionExitCode, ExecutionResult, ExecutionSpawnResult, ExecutionWaitResult,
15};
16use crate::shell::Shell;
17use crate::variables::{
18    ArrayLiteral, ShellValue, ShellValueLiteral, ShellValueUnsetType, ShellVariable,
19};
20use crate::{
21    ShellFd, error, expansion, extendedtests, extensions, ioutils, jobs, openfiles, sys, timing,
22};
23
24/// Encapsulates the context of execution in a command pipeline.
25struct PipelineExecutionContext<'a, SE: extensions::ShellExtensions> {
26    /// The shell in which the command should be executed.
27    shell: commands::ShellForCommand<'a, SE>,
28    /// Process group ID for spawned processes.
29    process_group_id: Option<i32>,
30}
31
32/// Parameters for execution.
33#[derive(Clone, Default)]
34pub struct ExecutionParameters {
35    /// The open files tracked by the current context.
36    open_files: openfiles::OpenFiles,
37    /// Policy for how to manage spawned external processes.
38    pub process_group_policy: ProcessGroupPolicy,
39    /// Whether `errexit` (exit on error) behavior should be
40    /// suppressed in this execution context. Defaults to `false`.
41    pub suppress_errexit: bool,
42}
43
44impl ExecutionParameters {
45    /// Returns the standard input file; usable with `write!` et al.
46    ///
47    /// # Arguments
48    ///
49    /// * `shell` - The shell context.
50    pub fn stdin(
51        &self,
52        shell: &Shell<impl extensions::ShellExtensions>,
53    ) -> impl std::io::Read + 'static {
54        self.try_stdin(shell).unwrap_or_else(|| {
55            ioutils::FailingReaderWriter::new("standard input not available").into()
56        })
57    }
58
59    /// Tries to retrieve the standard input file. Returns `None` if not set.
60    ///
61    /// # Arguments
62    ///
63    /// * `shell` - The shell context.
64    pub fn try_stdin(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Option<OpenFile> {
65        self.try_fd(shell, openfiles::OpenFiles::STDIN_FD)
66    }
67
68    /// Returns the standard output file; usable with `write!` et al. In the
69    /// event that no such file is available, returns a valid implementation
70    /// of `std::io::Write` that fails all I/O requests.
71    ///
72    ///
73    /// # Arguments
74    ///
75    /// * `shell` - The shell context.
76    pub fn stdout(
77        &self,
78        shell: &Shell<impl extensions::ShellExtensions>,
79    ) -> impl std::io::Write + 'static {
80        self.try_stdout(shell).unwrap_or_else(|| {
81            ioutils::FailingReaderWriter::new("standard output not available").into()
82        })
83    }
84
85    /// Tries to retrieve the standard output file. Returns `None` if not set.
86    ///
87    /// # Arguments
88    ///
89    /// * `shell` - The shell context.
90    pub fn try_stdout(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Option<OpenFile> {
91        self.try_fd(shell, openfiles::OpenFiles::STDOUT_FD)
92    }
93
94    /// Returns the standard error file; usable with `write!` et al. In the
95    /// event that no such file is available, returns a valid implementation
96    /// of `std::io::Write` that fails all I/O requests.
97    ///
98    /// # Arguments
99    ///
100    /// * `shell` - The shell context.
101    pub fn stderr(
102        &self,
103        shell: &Shell<impl extensions::ShellExtensions>,
104    ) -> impl std::io::Write + 'static {
105        self.try_stderr(shell).unwrap_or_else(|| {
106            ioutils::FailingReaderWriter::new("standard error not available").into()
107        })
108    }
109
110    /// Tries to retrieve the standard error file. Returns `None` if not set.
111    ///
112    /// # Arguments
113    ///
114    /// * `shell` - The shell context.
115    pub fn try_stderr(&self, shell: &Shell<impl extensions::ShellExtensions>) -> Option<OpenFile> {
116        self.try_fd(shell, openfiles::OpenFiles::STDERR_FD)
117    }
118
119    /// Returns the file descriptor with the given number. Returns `None`
120    /// if the file descriptor is not open.
121    ///
122    /// # Arguments
123    ///
124    /// * `shell` - The shell context.
125    /// * `fd` - The file descriptor number to retrieve.
126    pub fn try_fd(
127        &self,
128        shell: &Shell<impl extensions::ShellExtensions>,
129        fd: ShellFd,
130    ) -> 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
136                // fallback to what's represented in the shell's
137                // open files.
138                shell.persistent_open_files().try_fd(fd).cloned()
139            }
140        }
141    }
142
143    /// Sets the given file descriptor to the provided open file.
144    ///
145    /// # Arguments
146    ///
147    /// * `fd` - The file descriptor number to set.
148    /// * `file` - The open file to set.
149    pub fn set_fd(&mut self, fd: ShellFd, file: openfiles::OpenFile) {
150        self.open_files.set_fd(fd, file);
151    }
152
153    /// Iterates over all open file descriptors in this context.
154    ///
155    /// # Arguments
156    ///
157    /// * `shell` - The shell context.
158    pub fn iter_fds(
159        &self,
160        shell: &Shell<impl extensions::ShellExtensions>,
161    ) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
162        let our_fds = self.open_files.iter_fds();
163        let shell_fds = shell
164            .persistent_open_files()
165            .iter_fds()
166            .filter(|(fd, _)| !self.open_files.contains_fd(*fd));
167
168        #[allow(clippy::needless_collect)]
169        let all_fds: Vec<_> = our_fds
170            .chain(shell_fds)
171            .map(|(fd, file)| (fd, file.clone()))
172            .collect();
173
174        all_fds.into_iter()
175    }
176}
177
178#[derive(Clone, Debug, Default)]
179/// Policy for how to manage spawned external processes.
180pub enum ProcessGroupPolicy {
181    /// Place the process in a new process group.
182    #[default]
183    NewProcessGroup,
184    /// Place the process in the same process group as its parent.
185    SameProcessGroup,
186}
187
188#[async_trait::async_trait]
189pub trait Execute {
190    async fn execute(
191        &self,
192        shell: &mut Shell<impl extensions::ShellExtensions>,
193        params: &ExecutionParameters,
194    ) -> Result<ExecutionResult, error::Error>;
195}
196
197#[async_trait::async_trait]
198trait ExecuteInPipeline<SE: extensions::ShellExtensions> {
199    async fn execute_in_pipeline(
200        &self,
201        context: PipelineExecutionContext<'_, SE>,
202        params: ExecutionParameters,
203    ) -> Result<ExecutionSpawnResult, error::Error>;
204}
205
206#[async_trait::async_trait]
207impl Execute for ast::Program {
208    async fn execute(
209        &self,
210        shell: &mut Shell<impl extensions::ShellExtensions>,
211        params: &ExecutionParameters,
212    ) -> Result<ExecutionResult, error::Error> {
213        let mut result = ExecutionResult::success();
214
215        for command in &self.complete_commands {
216            // Execute the command and handle any errors without immediately
217            // propagating them. This allows interactive shells to
218            // continue executing subsequent commands even after
219            // errors.
220            match command.execute(shell, params).await {
221                Ok(exec_result) => result = exec_result,
222                Err(err) => {
223                    // Display the error and convert to an execution result.
224                    let _ = shell.display_error(&mut params.stderr(shell), &err);
225                    result = err.into_result(shell);
226                }
227            }
228
229            // Update status
230            shell.set_last_exit_status(result.exit_code.into());
231
232            // Check if we should stop executing subsequent commands
233            if !result.is_normal_flow() {
234                break;
235            }
236        }
237
238        Ok(result)
239    }
240}
241
242#[async_trait::async_trait]
243impl Execute for ast::CompoundList {
244    async fn execute(
245        &self,
246        shell: &mut Shell<impl extensions::ShellExtensions>,
247        params: &ExecutionParameters,
248    ) -> Result<ExecutionResult, error::Error> {
249        let mut result = ExecutionResult::success();
250
251        for ast::CompoundListItem(ao_list, sep) in &self.0 {
252            let run_async = matches!(sep, ast::SeparatorOperator::Async);
253
254            if run_async {
255                let job = spawn_async_ao_list_in_task(ao_list, shell, params);
256                let job_formatted = job.to_pid_style_string();
257
258                if shell.options().interactive && !shell.is_subshell() {
259                    writeln!(params.stderr(shell), "{job_formatted}")?;
260                }
261
262                result = ExecutionResult::success();
263            } else {
264                result = ao_list.execute(shell, params).await?;
265
266                // Update status
267                shell.set_last_exit_status(result.exit_code.into());
268            }
269
270            if !result.is_normal_flow() {
271                break;
272            }
273        }
274
275        Ok(result)
276    }
277}
278
279fn spawn_async_ao_list_in_task<'a, SE: extensions::ShellExtensions>(
280    ao_list: &ast::AndOrList,
281    shell: &'a mut Shell<SE>,
282    params: &ExecutionParameters,
283) -> &'a jobs::Job {
284    // Clone the inputs.
285    let mut cloned_shell = shell.clone();
286    let mut cloned_params = params.clone();
287    let cloned_ao_list = ao_list.clone();
288
289    // Mark the child shell as not interactive; we don't want it messing with
290    // the terminal too much.
291    cloned_shell.options_mut().interactive = false;
292
293    // Redirect stdin to null, per spec.
294    if let Ok(null) = openfiles::null() {
295        cloned_params.set_fd(openfiles::OpenFiles::STDIN_FD, null);
296    }
297
298    let join_handle = sys::tasks::spawn(async move {
299        cloned_ao_list
300            .execute(&mut cloned_shell, &cloned_params)
301            .await
302    });
303
304    shell.jobs_mut().add_as_current(jobs::Job::new(
305        [jobs::JobTask::Internal(join_handle)],
306        ao_list.to_string(),
307        jobs::JobState::Running,
308    ))
309}
310
311#[async_trait::async_trait]
312impl Execute for ast::AndOrList {
313    async fn execute(
314        &self,
315        shell: &mut Shell<impl extensions::ShellExtensions>,
316        params: &ExecutionParameters,
317    ) -> Result<ExecutionResult, error::Error> {
318        let has_operators = !self.additional.is_empty();
319
320        // For the first command, suppress errexit if there are more commands
321        // after it
322        let mut first_params = params.clone();
323        if has_operators {
324            first_params.suppress_errexit = true;
325        }
326
327        let mut result = self.first.execute(shell, &first_params).await?;
328
329        for (index, next_ao) in self.additional.iter().enumerate() {
330            // Check for non-normal control flow.
331            if !result.is_normal_flow() {
332                break;
333            }
334
335            let (is_and, pipeline) = match next_ao {
336                ast::AndOr::And(p) => (true, p),
337                ast::AndOr::Or(p) => (false, p),
338            };
339
340            // If we short-circuit, then we don't break out of the whole loop
341            // but we skip evaluating the current pipeline. We'll then continue
342            // on and possibly evaluate a subsequent one (depending on the
343            // operator before it).
344            if is_and {
345                if !result.is_success() {
346                    continue;
347                }
348            } else if result.is_success() {
349                continue;
350            }
351
352            // For the last command in the chain, use original params (errexit
353            // not suppressed) For earlier commands, suppress
354            // errexit
355            let mut params = params.clone();
356
357            let is_last = index == self.additional.len() - 1;
358            if !is_last {
359                params.suppress_errexit = true;
360            }
361
362            result = pipeline.execute(shell, &params).await?;
363        }
364
365        Ok(result)
366    }
367}
368
369#[async_trait::async_trait]
370impl Execute for ast::Pipeline {
371    async fn execute(
372        &self,
373        shell: &mut Shell<impl extensions::ShellExtensions>,
374        params: &ExecutionParameters,
375    ) -> Result<ExecutionResult, error::Error> {
376        // Capture current timing if so requested.
377        let stopwatch = self
378            .timed
379            .is_some()
380            .then(timing::start_timing)
381            .transpose()?;
382
383        let mut params = params.clone();
384
385        // If this pipeline is negated, suppress errexit for commands within it
386        if self.bang {
387            params.suppress_errexit = true;
388        }
389
390        // Spawn all the processes required for the pipeline, connecting
391        // outputs/inputs with pipes as needed.
392        let spawn_results = spawn_pipeline_processes(self, shell, &params).await?;
393
394        // Wait for the processes. This also has a side effect of updating
395        // pipeline status.
396        let mut result =
397            wait_for_pipeline_processes_and_update_status(self, spawn_results, shell, &params)
398                .await?;
399
400        // Invert the exit code if requested.
401        if self.bang {
402            result.exit_code = ExecutionExitCode::from(if result.is_success() { 1 } else { 0 });
403        }
404
405        // Update exit status.
406        shell.set_last_exit_status(result.exit_code.into());
407
408        // Fire the ERR trap if the pipeline failed in a non-conditional
409        // context. We reuse `suppress_errexit` here because bash
410        // suppresses the ERR trap in exactly the same contexts it
411        // suppresses errexit (conditionals, `!`-prefixed pipelines,
412        // etc.).
413        if !result.is_success() && !params.suppress_errexit && !self.bang {
414            if shell.traps().handles(crate::traps::TrapSignal::Err) {
415                shell
416                    .invoke_trap_handler(crate::traps::TrapSignal::Err, &params)
417                    .await?;
418            }
419        }
420
421        // Apply errexit if not suppressed (and not negated)
422        if !params.suppress_errexit && !self.bang {
423            shell.apply_errexit_if_enabled(&mut result);
424        }
425
426        // If requested, report timing.
427        if let (Some(timed), Some(stopwatch)) = (&self.timed, &stopwatch)
428            && let Some(mut stderr) = params.try_fd(shell, openfiles::OpenFiles::STDERR_FD)
429        {
430            let timing = stopwatch.stop()?;
431            if timed.is_posix_output() {
432                std::write!(
433                    stderr,
434                    "real {}\nuser {}\nsys {}\n",
435                    timing::format_duration_posixly(&timing.wall),
436                    timing::format_duration_posixly(&timing.user),
437                    timing::format_duration_posixly(&timing.system),
438                )?;
439            } else {
440                std::write!(
441                    stderr,
442                    "\nreal\t{}\nuser\t{}\nsys\t{}\n",
443                    timing::format_duration_non_posixly(&timing.wall),
444                    timing::format_duration_non_posixly(&timing.user),
445                    timing::format_duration_non_posixly(&timing.system),
446                )?;
447            }
448        }
449
450        Ok(result)
451    }
452}
453
454async fn spawn_pipeline_processes(
455    pipeline: &ast::Pipeline,
456    shell: &mut Shell<impl extensions::ShellExtensions>,
457    params: &ExecutionParameters,
458) -> Result<VecDeque<ExecutionSpawnResult>, error::Error> {
459    let pipeline_len = pipeline.seq.len();
460    let mut pipe_readers = vec![];
461    let mut pipe_writers = vec![];
462    let mut spawn_results = VecDeque::new();
463    let mut process_group_id: Option<i32> = None;
464
465    // Create pipes to use between commands, but only bother doing so if there's
466    // more than one command.
467    if pipeline_len > 1 {
468        pipe_readers.reserve_exact(pipeline_len - 1);
469        pipe_writers.reserve_exact(pipeline_len - 1);
470
471        for _ in 0..(pipeline_len - 1) {
472            let (reader, writer) = sys::pipes::pipe()?;
473            pipe_readers.push(Some(reader.into()));
474            pipe_writers.push(Some(writer.into()));
475        }
476        // Push `None` to the readers; it will be popped off by the *first*
477        // command, which will mean that command gets its stdin from the
478        // execution parameters' current stdin.
479        pipe_readers.push(None);
480    }
481
482    for (current_pipeline_index, command) in pipeline.seq.iter().enumerate() {
483        //
484        // We run a command directly in the current shell if either of the
485        // following is true:
486        //     * There's only one command in the pipeline.
487        //     * This is the *last* command in the pipeline, the lastpipe option
488        //       is enabled, and job monitoring is disabled.
489        // Otherwise, we spawn a separate subshell for each command in the
490        // pipeline.
491        //
492
493        let run_in_current_shell = pipeline_len == 1
494            || (current_pipeline_index == pipeline_len - 1
495                && shell.options().run_last_pipeline_cmd_in_current_shell
496                && !shell.options().enable_job_control);
497
498        // Set up parameters appropriate for this command.
499        let mut cmd_params = params.clone();
500
501        // Install pipes.
502        if let Some(Some(reader)) = pipe_readers.pop() {
503            cmd_params.open_files.set_fd(OpenFiles::STDIN_FD, reader);
504        }
505        if let Some(Some(writer)) = pipe_writers.pop() {
506            cmd_params.open_files.set_fd(OpenFiles::STDOUT_FD, writer);
507        }
508
509        let pipeline_context = if !run_in_current_shell {
510            // Make sure that all commands in the pipeline are in the same
511            // process group.
512            if current_pipeline_index > 0 {
513                cmd_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
514            }
515
516            PipelineExecutionContext {
517                shell: commands::ShellForCommand::OwnedShell {
518                    target: Box::new(shell.clone()),
519                    parent: shell,
520                },
521                process_group_id,
522            }
523        } else {
524            PipelineExecutionContext {
525                shell: commands::ShellForCommand::ParentShell(shell),
526                process_group_id,
527            }
528        };
529
530        let spawn_result = command
531            .execute_in_pipeline(pipeline_context, cmd_params)
532            .await?;
533
534        // Update the process group ID if something was spawned.
535        if let ExecutionSpawnResult::StartedProcess(child) = &spawn_result {
536            if process_group_id.is_none() {
537                process_group_id = child.pgid();
538            }
539        }
540
541        spawn_results.push_back(spawn_result);
542    }
543
544    Ok(spawn_results)
545}
546
547async fn wait_for_pipeline_processes_and_update_status(
548    pipeline: &ast::Pipeline,
549    mut process_spawn_results: VecDeque<ExecutionSpawnResult>,
550    shell: &mut Shell<impl extensions::ShellExtensions>,
551    params: &ExecutionParameters,
552) -> Result<ExecutionResult, error::Error> {
553    let mut result = ExecutionResult::success();
554    let mut stopped_children = vec![];
555    let mut last_failure_exit_code: Option<ExecutionExitCode> = None;
556
557    // Clear our the pipeline status so we can start filling it out.
558    shell.last_pipeline_statuses_mut().clear();
559
560    while let Some(child) = process_spawn_results.pop_front() {
561        let wait_result = if !stopped_children.is_empty() {
562            child.poll().await?
563        } else {
564            child.wait().await?
565        };
566
567        match wait_result {
568            ExecutionWaitResult::Completed(current_result) => {
569                result = current_result;
570                shell.set_last_exit_status(result.exit_code.into());
571                shell
572                    .last_pipeline_statuses_mut()
573                    .push(result.exit_code.into());
574
575                // Track the last failure for pipefail option
576                if !result.is_success() {
577                    last_failure_exit_code = Some(result.exit_code);
578                }
579            }
580            ExecutionWaitResult::Stopped(child) => {
581                result = ExecutionResult::stopped();
582                shell.set_last_exit_status(result.exit_code.into());
583                shell
584                    .last_pipeline_statuses_mut()
585                    .push(result.exit_code.into());
586
587                stopped_children.push(jobs::JobTask::External(child));
588            }
589        }
590    }
591
592    // Apply pipefail semantics if enabled
593    if shell.options().return_last_failure_from_pipeline {
594        if let Some(failure_exit_code) = last_failure_exit_code {
595            result.exit_code = failure_exit_code;
596        }
597    }
598
599    if shell.options().interactive {
600        sys::terminal::move_self_to_foreground()?;
601    }
602
603    // If there were stopped jobs, then encapsulate the pipeline as a managed
604    // job and hand it off to the job manager.
605    if !stopped_children.is_empty() {
606        let job = shell.jobs_mut().add_as_current(jobs::Job::new(
607            stopped_children,
608            pipeline.to_string(),
609            jobs::JobState::Stopped,
610        ));
611
612        let formatted = job.to_string();
613
614        // N.B. We use the '\r' to overwrite any ^Z output.
615        writeln!(params.stderr(shell), "\r{formatted}")?;
616    }
617
618    Ok(result)
619}
620
621#[async_trait::async_trait]
622impl<SE: extensions::ShellExtensions> ExecuteInPipeline<SE> for ast::Command {
623    async fn execute_in_pipeline(
624        &self,
625        mut pipeline_context: PipelineExecutionContext<'_, SE>,
626        mut params: ExecutionParameters,
627    ) -> Result<ExecutionSpawnResult, error::Error> {
628        if pipeline_context.shell.options().do_not_execute_commands {
629            return Ok(ExecutionSpawnResult::Completed(ExecutionResult::success()));
630        }
631
632        // Updates the shell with information about the currently executing
633        // command.
634        pipeline_context.shell.set_current_cmd(self);
635
636        match self {
637            Self::Simple(simple) => simple.execute_in_pipeline(pipeline_context, params).await,
638            Self::Compound(compound, redirects) => {
639                // Set up any additional redirects.
640                if let Some(redirects) = redirects {
641                    for redirect in &redirects.0 {
642                        setup_redirect(&mut pipeline_context.shell, &mut params, redirect).await?;
643                    }
644                }
645
646                match pipeline_context.shell {
647                    // An owned shell is a discarded clone, so this stage can run asynchronously.
648                    // That lets the pipeline start readers before writers fill the pipe.
649                    commands::ShellForCommand::OwnedShell { target, .. } => {
650                        let (mut shell, compound) = (*target, compound.clone());
651                        Ok(ExecutionSpawnResult::StartedTask(
652                            sys::tasks::spawn_blocking(async move {
653                                compound.execute(&mut shell, &params).await
654                            })
655                            .await,
656                        ))
657                    }
658                    commands::ShellForCommand::ParentShell(shell) => {
659                        Ok(compound.execute(shell, &params).await?.into())
660                    }
661                }
662            }
663            Self::Function(func) => Ok(func
664                .execute(&mut pipeline_context.shell, &params)
665                .await?
666                .into()),
667        }
668    }
669}
670
671enum WhileOrUntil {
672    While,
673    Until,
674}
675
676#[async_trait::async_trait]
677impl Execute for ast::CompoundCommand {
678    async fn execute(
679        &self,
680        shell: &mut Shell<impl extensions::ShellExtensions>,
681        params: &ExecutionParameters,
682    ) -> Result<ExecutionResult, error::Error> {
683        match self {
684            Self::BraceGroup(ast::BraceGroupCommand { list, .. }) => {
685                list.execute(shell, params).await
686            }
687            Self::Subshell(ast::SubshellCommand { list, .. }) => {
688                // Clone off a new subshell, and run the body of the subshell
689                // there. TODO(source-info): Do we need to reset
690                // the line number?
691                let mut subshell = shell.clone();
692
693                // Handle errors within the subshell context to prevent fatal
694                // errors from propagating to the parent shell.
695                let subshell_result = match list.execute(&mut subshell, params).await {
696                    Ok(result) => result,
697                    Err(error) => {
698                        // Display the error to stderr, but prevent fatal error
699                        // propagation
700                        let mut stderr = params.stderr(shell);
701                        let _ = shell.display_error(&mut stderr, &error);
702
703                        // Convert error to result in subshell context
704                        error.into_result(&subshell)
705                    }
706                };
707
708                // Preserve the subshell's exit code, but don't honor any of its
709                // requests to exit the shell, break out of
710                // loops, etc.
711                Ok(ExecutionResult::from(subshell_result.exit_code))
712            }
713            Self::ForClause(f) => f.execute(shell, params).await,
714            Self::CaseClause(c) => c.execute(shell, params).await,
715            Self::IfClause(i) => i.execute(shell, params).await,
716            Self::WhileClause(w) => (WhileOrUntil::While, w).execute(shell, params).await,
717            Self::UntilClause(u) => (WhileOrUntil::Until, u).execute(shell, params).await,
718            Self::Arithmetic(a) => a.execute(shell, params).await,
719            Self::ArithmeticForClause(a) => a.execute(shell, params).await,
720            Self::Coprocess(c) => c.execute(shell, params).await,
721            Self::ExtendedTest(e) => {
722                let result =
723                    if extendedtests::eval_extended_test_expr(&e.expr, shell, params).await? {
724                        0
725                    } else {
726                        1
727                    };
728                Ok(ExecutionResult::new(result))
729            }
730        }
731    }
732}
733
734#[async_trait::async_trait]
735impl Execute for ast::CoprocessCommand {
736    async fn execute(
737        &self,
738        shell: &mut Shell<impl extensions::ShellExtensions>,
739        params: &ExecutionParameters,
740    ) -> Result<ExecutionResult, error::Error> {
741        if shell.options().do_not_execute_commands {
742            return Ok(ExecutionResult::success());
743        }
744
745        // Resolve the name of the variable that will receive the coprocess's
746        // file descriptors.
747        let name = self
748            .name
749            .as_ref()
750            .map_or(Cow::Borrowed("COPROC"), |w| Cow::Owned(w.to_string()));
751
752        if !valid_variable_name(&name) {
753            writeln!(
754                params.stderr(shell),
755                "coproc {name}: not a valid identifier"
756            )?;
757            return Ok(ExecutionExitCode::GeneralError.into());
758        }
759
760        // The coprocess leaves the shell keeping two more descriptors.
761        shell.check_kept_descriptors(shell.open_files().count_above_stderr() + 2)?;
762
763        // Set up the pipes that we'll use to communicate with the coprocess.
764        let (stdin_reader, stdin_writer) = sys::pipes::pipe()?;
765        let (stdout_reader, stdout_writer) = sys::pipes::pipe()?;
766
767        // Allocate new fds in the (parent) shell for the read end of the
768        // coprocess's stdout and the write end of the coprocess's
769        // stdin.
770        let stdout_fd = shell.open_files_mut().add(stdout_reader.into())?;
771        let stdin_fd = shell.open_files_mut().add(stdin_writer.into())?;
772
773        // Crete a subshell that the coprocess will own and run in.
774        let mut child_shell = shell.clone();
775        child_shell.options_mut().interactive = false;
776
777        // Setup redirection for the coprocess's shell's stdin/stdout.
778        let mut child_params = params.clone();
779        child_params
780            .open_files
781            .set_fd(OpenFiles::STDIN_FD, stdin_reader.into());
782        child_params
783            .open_files
784            .set_fd(OpenFiles::STDOUT_FD, stdout_writer.into());
785
786        let body = self.body.clone();
787        let join_handle = sys::tasks::spawn(async move {
788            let pipeline_context = PipelineExecutionContext {
789                shell: commands::ShellForCommand::ParentShell(&mut child_shell),
790                process_group_id: None,
791            };
792            let spawn_result = body
793                .execute_in_pipeline(pipeline_context, child_params)
794                .await?;
795            match spawn_result.wait().await? {
796                ExecutionWaitResult::Completed(result) => Ok(result),
797                ExecutionWaitResult::Stopped(_) => Ok(ExecutionResult::stopped()),
798            }
799        });
800
801        let job = shell.jobs_mut().add_as_current(jobs::Job::new(
802            [jobs::JobTask::Internal(join_handle)],
803            format!("coproc {name}"),
804            jobs::JobState::Running,
805        ));
806        let job_id = job.id;
807
808        // Fill out the fd variable.
809        let arr_value = ShellValue::from(vec![stdout_fd.to_string(), stdin_fd.to_string()]);
810        shell
811            .env_mut()
812            .set_global(name.clone(), ShellVariable::new(arr_value))?;
813
814        // Set the job ID for the coprocess in a separate variable with the _PID
815        // suffix.
816        let pid_name = format!("{name}_PID");
817        shell
818            .env_mut()
819            .set_global(pid_name, ShellVariable::new(job_id.to_string()))?;
820
821        Ok(ExecutionResult::success())
822    }
823}
824
825#[async_trait::async_trait]
826impl Execute for ast::ForClauseCommand {
827    async fn execute(
828        &self,
829        shell: &mut Shell<impl extensions::ShellExtensions>,
830        params: &ExecutionParameters,
831    ) -> Result<ExecutionResult, error::Error> {
832        let mut result = ExecutionResult::success();
833
834        // If we were given explicit words to iterate over, then expand them
835        // all, with splitting enabled.
836        let expanded_values = if let Some(unexpanded_values) = &self.values {
837            expand_words(shell, params, unexpanded_values).await?
838        } else {
839            // Otherwise, we use the current positional parameters.
840            shell.current_shell_args().to_vec()
841        };
842
843        for value in expanded_values {
844            if shell.options().print_commands_and_arguments {
845                if let Some(unexpanded_values) = &self.values {
846                    shell
847                        .trace_command(
848                            params,
849                            std::format!(
850                                "for {} in {}",
851                                self.variable_name,
852                                unexpanded_values.iter().join(" ")
853                            ),
854                        )
855                        .await;
856                } else {
857                    shell
858                        .trace_command(params, std::format!("for {}", self.variable_name))
859                        .await;
860                }
861            }
862
863            // Update the variable.
864            shell.env_mut().update_or_add(
865                &self.variable_name,
866                ShellValueLiteral::Scalar(value),
867                |_| Ok(()),
868                EnvironmentLookup::Anywhere,
869                EnvironmentScope::Global,
870            )?;
871
872            result = self.body.list.execute(shell, params).await?;
873            if result.is_return_or_exit() {
874                break;
875            }
876
877            let is_break = result.is_break();
878
879            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
880
881            if is_break || result.is_continue() {
882                break;
883            }
884        }
885
886        shell.set_last_exit_status(result.exit_code.into());
887        Ok(result)
888    }
889}
890
891#[async_trait::async_trait]
892impl Execute for ast::CaseClauseCommand {
893    async fn execute(
894        &self,
895        shell: &mut Shell<impl extensions::ShellExtensions>,
896        params: &ExecutionParameters,
897    ) -> Result<ExecutionResult, error::Error> {
898        // N.B. One would think it makes sense to trace the expanded value being
899        // switched on, but that's not it.
900        if shell.options().print_commands_and_arguments {
901            shell
902                .trace_command(params, std::format!("case {} in", self.value))
903                .await;
904        }
905
906        let expanded_value = expansion::basic_expand_word(shell, params, &self.value).await?;
907        let mut result: ExecutionResult = ExecutionResult::success();
908        let mut force_execute_next_case = false;
909
910        for case in &self.cases {
911            if force_execute_next_case {
912                force_execute_next_case = false;
913            } else {
914                let mut matches = false;
915                for pattern in &case.patterns {
916                    let expanded_pattern = expansion::basic_expand_pattern(shell, params, pattern)
917                        .await?
918                        .set_extended_globbing(shell.options().extended_globbing)
919                        .set_case_insensitive(shell.options().case_insensitive_conditionals);
920
921                    if expanded_pattern.exactly_matches(expanded_value.as_str())? {
922                        matches = true;
923                        break;
924                    }
925                }
926
927                if !matches {
928                    continue;
929                }
930            }
931
932            result = if let Some(case_cmd) = &case.cmd {
933                case_cmd.execute(shell, params).await?
934            } else {
935                ExecutionResult::success()
936            };
937
938            // Check for early return (return/exit) or loop control flow
939            // (break/continue)
940            if !result.is_normal_flow() {
941                break;
942            }
943
944            match case.post_action {
945                ast::CaseItemPostAction::ExitCase => break,
946                ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
947                    force_execute_next_case = true;
948                }
949                ast::CaseItemPostAction::ContinueEvaluatingCases => (),
950            }
951        }
952
953        shell.set_last_exit_status(result.exit_code.into());
954
955        Ok(result)
956    }
957}
958
959#[async_trait::async_trait]
960impl Execute for ast::IfClauseCommand {
961    async fn execute(
962        &self,
963        shell: &mut Shell<impl extensions::ShellExtensions>,
964        params: &ExecutionParameters,
965    ) -> Result<ExecutionResult, error::Error> {
966        // Execute condition with errexit suppressed
967        let mut condition_params = params.clone();
968        condition_params.suppress_errexit = true;
969        let condition = self.condition.execute(shell, &condition_params).await?;
970
971        // Check if the condition itself resulted in non-normal control flow.
972        if !condition.is_normal_flow() {
973            return Ok(condition);
974        }
975
976        if condition.is_success() {
977            return self.then.execute(shell, params).await;
978        }
979
980        if let Some(elses) = &self.elses {
981            for else_clause in elses {
982                match &else_clause.condition {
983                    Some(else_condition) => {
984                        let else_condition_result =
985                            else_condition.execute(shell, &condition_params).await?;
986
987                        // Check if the elif condition caused non-normal control
988                        // flow.
989                        if !else_condition_result.is_normal_flow() {
990                            return Ok(else_condition_result);
991                        }
992
993                        if else_condition_result.is_success() {
994                            return else_clause.body.execute(shell, params).await;
995                        }
996                    }
997                    None => {
998                        return else_clause.body.execute(shell, params).await;
999                    }
1000                }
1001            }
1002        }
1003
1004        // If we got down here, then no branch was taken; we make sure to
1005        // reset the last exit status to success and then return success.
1006        let result = ExecutionResult::success();
1007        shell.set_last_exit_status(result.exit_code.into());
1008
1009        Ok(result)
1010    }
1011}
1012
1013#[async_trait::async_trait]
1014impl Execute for (WhileOrUntil, &ast::WhileOrUntilClauseCommand) {
1015    async fn execute(
1016        &self,
1017        shell: &mut Shell<impl extensions::ShellExtensions>,
1018        params: &ExecutionParameters,
1019    ) -> Result<ExecutionResult, error::Error> {
1020        let is_while = match self.0 {
1021            WhileOrUntil::While => true,
1022            WhileOrUntil::Until => false,
1023        };
1024        let test_condition = &self.1.0;
1025        let body = &self.1.1;
1026
1027        let mut result = ExecutionResult::success();
1028
1029        // Execute loop condition with errexit suppressed
1030        let mut condition_params = params.clone();
1031        condition_params.suppress_errexit = true;
1032
1033        loop {
1034            let condition_result = test_condition.execute(shell, &condition_params).await?;
1035
1036            // Update status for condition
1037            shell.set_last_exit_status(condition_result.exit_code.into());
1038
1039            if !condition_result.is_normal_flow() {
1040                result = condition_result;
1041
1042                // If the condition has break/continue, the while/until loop
1043                // itself consumes one level. We need to
1044                // decrement the level before returning.
1045                result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
1046                break;
1047            }
1048
1049            if condition_result.is_success() != is_while {
1050                break;
1051            }
1052
1053            result = body.list.execute(shell, params).await?;
1054            if result.is_return_or_exit() {
1055                break;
1056            }
1057
1058            let is_break = result.is_break();
1059
1060            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
1061
1062            if is_break || result.is_continue() {
1063                break;
1064            }
1065        }
1066
1067        shell.set_last_exit_status(result.exit_code.into());
1068        Ok(result)
1069    }
1070}
1071
1072#[async_trait::async_trait]
1073impl Execute for ast::ArithmeticCommand {
1074    async fn execute(
1075        &self,
1076        shell: &mut Shell<impl extensions::ShellExtensions>,
1077        params: &ExecutionParameters,
1078    ) -> Result<ExecutionResult, error::Error> {
1079        let value = self.expr.eval(shell, params, true).await?;
1080        let result = if value != 0 {
1081            ExecutionResult::success()
1082        } else {
1083            ExecutionResult::general_error()
1084        };
1085
1086        shell.set_last_exit_status(result.exit_code.into());
1087
1088        Ok(result)
1089    }
1090}
1091
1092#[async_trait::async_trait]
1093impl Execute for ast::ArithmeticForClauseCommand {
1094    async fn execute(
1095        &self,
1096        shell: &mut Shell<impl extensions::ShellExtensions>,
1097        params: &ExecutionParameters,
1098    ) -> Result<ExecutionResult, error::Error> {
1099        let mut result = ExecutionResult::success();
1100        if let Some(initializer) = &self.initializer {
1101            initializer.eval(shell, params, true).await?;
1102        }
1103
1104        loop {
1105            if let Some(condition) = &self.condition {
1106                // An empty condition (e.g., `for (( ; ; ))`) means "always
1107                // true".
1108                if !condition.value.is_empty() && condition.eval(shell, params, true).await? == 0 {
1109                    break;
1110                }
1111            }
1112
1113            result = self.body.list.execute(shell, params).await?;
1114            if result.is_return_or_exit() {
1115                break;
1116            }
1117
1118            let is_break = result.is_break();
1119
1120            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
1121
1122            if is_break || result.is_continue() {
1123                break;
1124            }
1125
1126            if let Some(updater) = &self.updater {
1127                updater.eval(shell, params, true).await?;
1128            }
1129        }
1130
1131        shell.set_last_exit_status(result.exit_code.into());
1132        Ok(result)
1133    }
1134}
1135
1136#[async_trait::async_trait]
1137impl Execute for ast::FunctionDefinition {
1138    async fn execute(
1139        &self,
1140        shell: &mut Shell<impl extensions::ShellExtensions>,
1141        _params: &ExecutionParameters,
1142    ) -> Result<ExecutionResult, error::Error> {
1143        let func_name = self.fname.value.clone();
1144
1145        // In POSIX mode, function names can't shadow special builtins.
1146        if shell.options().posix_mode
1147            && shell
1148                .builtins()
1149                .get(&func_name)
1150                .is_some_and(|r| r.special_builtin)
1151        {
1152            return Err(
1153                error::Error::from(error::ErrorKind::FunctionNameShadowsSpecialBuiltin {
1154                    name: func_name,
1155                })
1156                .into_fatal(),
1157            );
1158        }
1159
1160        // The function definition's source context should be the same as the
1161        // current frame so we directly pass that through.
1162        let source_info = shell
1163            .call_stack()
1164            .current_frame()
1165            .map_or_else(crate::SourceInfo::default, |frame| {
1166                frame.adjusted_source_info()
1167            });
1168        shell.define_func(func_name, self.clone(), &source_info);
1169
1170        let result = ExecutionResult::success();
1171        shell.set_last_exit_status(result.exit_code.into());
1172
1173        Ok(result)
1174    }
1175}
1176
1177#[async_trait::async_trait]
1178#[allow(clippy::too_many_lines)]
1179impl<SE: extensions::ShellExtensions> ExecuteInPipeline<SE> for ast::SimpleCommand {
1180    async fn execute_in_pipeline(
1181        &self,
1182        mut context: PipelineExecutionContext<'_, SE>,
1183        mut params: ExecutionParameters,
1184    ) -> Result<ExecutionSpawnResult, error::Error> {
1185        let prefix_iter = self.prefix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
1186        let suffix_iter = self.suffix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
1187        let cmd_name_items = self
1188            .word_or_name
1189            .as_ref()
1190            .map(|won| CommandPrefixOrSuffixItem::Word(won.clone()));
1191
1192        let mut assignments = vec![];
1193        let mut args: Vec<CommandArg> = vec![];
1194        let mut command_takes_assignments = false;
1195
1196        // Capture the status change count before expansion, so we can detect
1197        // if expansion (e.g., command substitution) set an exit status.
1198        let status_change_count_before_expansion = context.shell.last_exit_status_change_count();
1199
1200        for item in prefix_iter.chain(cmd_name_items.iter()).chain(suffix_iter) {
1201            match item {
1202                CommandPrefixOrSuffixItem::IoRedirect(redirect) => {
1203                    if let Err(e) = setup_redirect(&mut context.shell, &mut params, redirect).await
1204                    {
1205                        writeln!(params.stderr(&context.shell), "error: {e}")?;
1206                        return Ok(ExecutionResult::general_error().into());
1207                    }
1208                }
1209                CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell_command) => {
1210                    let (installed_fd_num, substitution_file) = setup_process_substitution(
1211                        &context.shell,
1212                        &params,
1213                        kind,
1214                        subshell_command,
1215                    )?;
1216
1217                    params
1218                        .open_files
1219                        .set_fd(installed_fd_num, substitution_file);
1220
1221                    args.push(CommandArg::String(std::format!(
1222                        "/dev/fd/{installed_fd_num}"
1223                    )));
1224                }
1225                CommandPrefixOrSuffixItem::AssignmentWord(assignment, word) => {
1226                    if args.is_empty() {
1227                        // If we haven't yet seen any arguments, then this must
1228                        // be a proper
1229                        // scoped assignment. Add it to the list we're
1230                        // accumulating.
1231                        assignments.push(assignment);
1232                    } else {
1233                        if command_takes_assignments {
1234                            // This looks like an assignment, and the command
1235                            // being invoked is a
1236                            // well-known builtin that takes arguments that need
1237                            // to function like
1238                            // assignments (but which are processed by the
1239                            // builtin).
1240                            let expanded =
1241                                expand_assignment(&mut context.shell, &params, assignment).await?;
1242                            args.push(CommandArg::Assignment(expanded));
1243                        } else {
1244                            // This *looks* like an assignment, but it's really
1245                            // a string we should
1246                            // fully treat as a regular looking
1247                            // argument.
1248                            let mut next_args = expansion::full_expand_and_split_word(
1249                                &mut context.shell,
1250                                &params,
1251                                word,
1252                            )
1253                            .await?
1254                            .into_iter()
1255                            .map(CommandArg::String)
1256                            .collect();
1257                            args.append(&mut next_args);
1258                        }
1259                    }
1260                }
1261                CommandPrefixOrSuffixItem::Word(arg) => {
1262                    let mut next_args =
1263                        expansion::full_expand_and_split_word(&mut context.shell, &params, arg)
1264                            .await?;
1265
1266                    if args.is_empty() {
1267                        if let Some(cmd_name) = next_args.first() {
1268                            // Aliases are only expanded when `expand_aliases`
1269                            // is enabled; it's
1270                            // enabled by default for interactive shells.
1271                            if context.shell.options().expand_aliases
1272                                && let Some(alias_value) =
1273                                    context.shell.aliases().get(cmd_name.as_str())
1274                            {
1275                                //
1276                                // TODO(#57): This is a total hack; aliases are
1277                                // supposed to be
1278                                // handled much earlier in the process.
1279                                //
1280                                // N.B. Tokenizing first releases our borrow of
1281                                // the shell's aliases,
1282                                // so we can take a mutable borrow of the shell
1283                                // to expand the words.
1284                                let alias_words = tokenize_alias_body(&context.shell, alias_value);
1285                                let mut alias_pieces =
1286                                    expand_words(&mut context.shell, &params, alias_words).await?;
1287
1288                                next_args.remove(0);
1289                                alias_pieces.append(&mut next_args);
1290
1291                                next_args = alias_pieces;
1292                            }
1293
1294                            // Check if we're going to be invoking a special
1295                            // declaration builtin.
1296                            // That will change how we parse and process args.
1297                            // (An alias with an
1298                            // empty body leaves us with no words at all.)
1299                            if let Some(first_arg) = next_args.first()
1300                                && context
1301                                    .shell
1302                                    .builtins()
1303                                    .get(first_arg.as_str())
1304                                    .is_some_and(|r| !r.disabled && r.declaration_builtin)
1305                            {
1306                                command_takes_assignments = true;
1307                            }
1308                        }
1309                    }
1310
1311                    let mut next_args = next_args.into_iter().map(CommandArg::String).collect();
1312                    args.append(&mut next_args);
1313                }
1314            }
1315        }
1316
1317        // If we have a command, then execute it.
1318        if let Some(CommandArg::String(cmd_name)) = args.first() {
1319            let cmd_name = cmd_name.clone();
1320            let mut stderr = params.stderr(&context.shell);
1321
1322            let (owned_shell, parent_shell) = match context.shell {
1323                commands::ShellForCommand::ParentShell(shell) => (None, shell),
1324                commands::ShellForCommand::OwnedShell { target, parent } => (Some(target), parent),
1325            };
1326
1327            let shell = if let Some(owned_shell) = owned_shell {
1328                commands::ShellForCommand::OwnedShell {
1329                    target: owned_shell,
1330                    parent: parent_shell,
1331                }
1332            } else {
1333                commands::ShellForCommand::ParentShell(parent_shell)
1334            };
1335
1336            let context = PipelineExecutionContext {
1337                shell,
1338                process_group_id: context.process_group_id,
1339            };
1340
1341            match execute_command(context, params, cmd_name, &assignments, args).await {
1342                Ok(result) => Ok(result),
1343                Err(err) => {
1344                    let _ = parent_shell.display_error(&mut stderr, &err);
1345
1346                    let result = err.into_result(parent_shell);
1347                    Ok(result.into())
1348                }
1349            }
1350        } else {
1351            // No command to run; assignments must be applied to this shell.
1352            for assignment in assignments {
1353                // Apply the assignment. Don't mark as fatal - let errors be
1354                // handled at the program level so multiple
1355                // complete_commands can execute independently.
1356                apply_assignment(
1357                    assignment,
1358                    &mut context.shell,
1359                    &params,
1360                    false,
1361                    None,
1362                    EnvironmentScope::Global,
1363                )
1364                .await?;
1365            }
1366
1367            // Assignment-only statements clear $_ (set to empty string).
1368            // This matches bash behavior where assignments don't have a "last
1369            // argument".
1370            context.shell.update_last_arg_variable(None);
1371
1372            // We need to set the last exit status to indicate assignment
1373            // success, but only if there was no status set during
1374            // expansion. We use the status count captured before
1375            // expansion to detect if command substitution (or other
1376            // expansion) set an exit status.
1377            if status_change_count_before_expansion == context.shell.last_exit_status_change_count()
1378            {
1379                context.shell.set_last_exit_status(0);
1380            }
1381
1382            // Return the last exit status we have; in some cases, an expansion
1383            // might result in a non-zero exit status stored in the shell.
1384            Ok(ExecutionResult::new(context.shell.last_exit_status()).into())
1385        }
1386    }
1387}
1388
1389async fn execute_command(
1390    context: PipelineExecutionContext<'_, impl extensions::ShellExtensions>,
1391    params: ExecutionParameters,
1392    cmd_name: String,
1393    assignments: &[&ast::Assignment],
1394    args: Vec<CommandArg>,
1395) -> Result<ExecutionSpawnResult, error::Error> {
1396    // Run the command in a new ephemeral environment scope, for its variable
1397    // assignments.
1398    let mut scoped;
1399    let mut shell = match context.shell {
1400        commands::ShellForCommand::ParentShell(shell) => {
1401            // The scope is left however the command ends, even if it's
1402            // cancelled.
1403            scoped = shell.enter_scope(EnvironmentScope::Command);
1404            commands::ShellForCommand::ParentShell(&mut scoped)
1405        }
1406        commands::ShellForCommand::OwnedShell { mut target, parent } => {
1407            // The command's shell is discarded after it runs, scope and all.
1408            target.env_mut().push_scope(EnvironmentScope::Command);
1409            commands::ShellForCommand::OwnedShell { target, parent }
1410        }
1411    };
1412
1413    for assignment in assignments {
1414        // Ensure it's tagged as exported and created in the command scope.
1415        apply_assignment(
1416            assignment,
1417            &mut shell,
1418            &params,
1419            true,
1420            Some(EnvironmentScope::Command),
1421            EnvironmentScope::Command,
1422        )
1423        .await?;
1424    }
1425
1426    if shell.options().print_commands_and_arguments {
1427        shell
1428            .trace_command(
1429                &params,
1430                args.iter().map(|arg| arg.quote_for_tracing()).join(" "),
1431            )
1432            .await;
1433    }
1434
1435    // Construct the command struct.
1436    let mut cmd = commands::SimpleCommand::new(shell, params, cmd_name, args);
1437    cmd.process_group_id = context.process_group_id;
1438
1439    // Run through any pre-execution hooks as best effort.
1440    let _ = commands::on_preexecute(&mut cmd).await;
1441
1442    // Execute
1443    // TODO(jobs): do we need to move self back to foreground on error here?
1444    cmd.execute().await
1445}
1446
1447/// Tokenizes the body of an alias into the unexpanded words that should replace
1448/// the aliased command name.
1449///
1450/// This only handles alias bodies that amount to a simple sequence of words;
1451/// bodies containing operators (pipes, redirections, `&&`, ...) and recursive
1452/// expansion of chained aliases require alias substitution to happen in the
1453/// tokenizer instead (see issue #57).
1454fn tokenize_alias_body(
1455    shell: &Shell<impl extensions::ShellExtensions>,
1456    alias_value: &str,
1457) -> Vec<String> {
1458    // Tokenize the body the same way the shell would tokenize any other input
1459    // it's given.
1460    let options = shell.parser_options().tokenizer_options();
1461
1462    // If we can't tokenize the body, fall back to naively splitting it on
1463    // whitespace.
1464    shrub_parser::tokenize_str_with_options(alias_value, &options).map_or_else(
1465        |_| {
1466            alias_value
1467                .split_ascii_whitespace()
1468                .map(str::to_owned)
1469                .collect()
1470        },
1471        |tokens| tokens.iter().map(|t| t.to_str().to_owned()).collect(),
1472    )
1473}
1474
1475/// Expands the given words, with splitting enabled, yielding the fields they
1476/// expand to.
1477async fn expand_words(
1478    shell: &mut Shell<impl extensions::ShellExtensions>,
1479    params: &ExecutionParameters,
1480    words: impl IntoIterator<Item = impl AsRef<str>>,
1481) -> Result<Vec<String>, error::Error> {
1482    // N.B. Expansion needs `&mut shell`, so the words have to be expanded in
1483    // sequence.
1484    let mut fields = vec![];
1485    for word in words {
1486        fields.extend(expansion::full_expand_and_split_word(shell, params, word).await?);
1487    }
1488    Ok(fields)
1489}
1490
1491async fn expand_assignment(
1492    shell: &mut Shell<impl extensions::ShellExtensions>,
1493    params: &ExecutionParameters,
1494    assignment: &ast::Assignment,
1495) -> Result<ast::Assignment, error::Error> {
1496    let value = expand_assignment_value(shell, params, &assignment.value).await?;
1497    Ok(ast::Assignment {
1498        name: basic_expand_assignment_name(shell, params, &assignment.name).await?,
1499        value,
1500        append: assignment.append,
1501        loc: assignment.loc.clone(),
1502    })
1503}
1504
1505async fn basic_expand_assignment_name(
1506    shell: &mut Shell<impl extensions::ShellExtensions>,
1507    params: &ExecutionParameters,
1508    name: &ast::AssignmentName,
1509) -> Result<ast::AssignmentName, error::Error> {
1510    match name {
1511        ast::AssignmentName::VariableName(name) => {
1512            let expanded = expansion::basic_expand_word(shell, params, name).await?;
1513            Ok(ast::AssignmentName::VariableName(expanded))
1514        }
1515        ast::AssignmentName::ArrayElementName(name, index) => {
1516            let expanded_name = expansion::basic_expand_word(shell, params, name).await?;
1517            let expanded_index = expansion::basic_expand_word(shell, params, index).await?;
1518            Ok(ast::AssignmentName::ArrayElementName(
1519                expanded_name,
1520                expanded_index,
1521            ))
1522        }
1523    }
1524}
1525
1526async fn expand_assignment_value(
1527    shell: &mut Shell<impl extensions::ShellExtensions>,
1528    params: &ExecutionParameters,
1529    value: &ast::AssignmentValue,
1530) -> Result<ast::AssignmentValue, error::Error> {
1531    let expanded = match value {
1532        ast::AssignmentValue::Scalar(s) => {
1533            let expanded_word = expansion::basic_expand_assignment_word(shell, params, s).await?;
1534            ast::AssignmentValue::Scalar(ast::Word::from(expanded_word))
1535        }
1536        ast::AssignmentValue::Array(arr) => {
1537            let mut expanded_values = vec![];
1538            for (key, value) in arr {
1539                if let Some(k) = key {
1540                    let expanded_key = expansion::basic_expand_assignment_word(shell, params, k)
1541                        .await?
1542                        .into();
1543                    let expanded_value =
1544                        expansion::basic_expand_assignment_word(shell, params, value)
1545                            .await?
1546                            .into();
1547                    expanded_values.push((Some(expanded_key), expanded_value));
1548                } else {
1549                    // Array elements are treated as regular words, not
1550                    // assignments
1551                    let split_expanded_value =
1552                        expansion::full_expand_and_split_word(shell, params, value).await?;
1553                    for expanded_value in split_expanded_value {
1554                        expanded_values.push((None, expanded_value.into()));
1555                    }
1556                }
1557            }
1558
1559            ast::AssignmentValue::Array(expanded_values)
1560        }
1561    };
1562
1563    Ok(expanded)
1564}
1565
1566#[expect(clippy::too_many_lines)]
1567async fn apply_assignment(
1568    assignment: &ast::Assignment,
1569    shell: &mut Shell<impl extensions::ShellExtensions>,
1570    params: &ExecutionParameters,
1571    mut export: bool,
1572    required_scope: Option<EnvironmentScope>,
1573    creation_scope: EnvironmentScope,
1574) -> Result<(), error::Error> {
1575    // Figure out if we are trying to assign to a variable or assign to an
1576    // element of an existing array.
1577    let mut array_index;
1578    let variable_name = match &assignment.name {
1579        ast::AssignmentName::VariableName(name) => {
1580            array_index = None;
1581            name
1582        }
1583        ast::AssignmentName::ArrayElementName(name, index) => {
1584            let expanded = expansion::basic_expand_word(shell, params, index).await?;
1585            array_index = Some(expanded);
1586            name
1587        }
1588    };
1589
1590    // Expand the values.
1591    let new_value = match &assignment.value {
1592        ast::AssignmentValue::Scalar(unexpanded_value) => {
1593            let value =
1594                expansion::basic_expand_assignment_word(shell, params, unexpanded_value).await?;
1595            ShellValueLiteral::Scalar(value)
1596        }
1597        ast::AssignmentValue::Array(unexpanded_values) => {
1598            let mut elements = vec![];
1599            for (unexpanded_key, unexpanded_value) in unexpanded_values {
1600                let key = match unexpanded_key {
1601                    Some(unexpanded_key) => Some(
1602                        expansion::basic_expand_assignment_word(shell, params, unexpanded_key)
1603                            .await?,
1604                    ),
1605                    None => None,
1606                };
1607
1608                if key.is_some() {
1609                    let value =
1610                        expansion::basic_expand_assignment_word(shell, params, unexpanded_value)
1611                            .await?;
1612                    elements.push((key, value));
1613                } else {
1614                    // Array elements are treated as regular words, not
1615                    // assignments
1616                    let values =
1617                        expansion::full_expand_and_split_word(shell, params, unexpanded_value)
1618                            .await?;
1619                    for value in values {
1620                        elements.push((None, value));
1621                    }
1622                }
1623            }
1624            ShellValueLiteral::Array(ArrayLiteral(elements))
1625        }
1626    };
1627
1628    if shell.options().print_commands_and_arguments {
1629        let op = if assignment.append { "+=" } else { "=" };
1630        shell
1631            .trace_command(params, std::format!("{}{op}{new_value}", assignment.name))
1632            .await;
1633    }
1634
1635    // See if we need to eval an array index.
1636    if let Some(idx) = &array_index {
1637        // An array subscript is arithmetically evaluated unless the target is
1638        // an associative array (in which case the subscript is used as
1639        // a literal key). A scalar or unset/untyped variable becomes an
1640        // indexed array, so its subscript still needs to be evaluated.
1641        let will_be_indexed_array =
1642            if let Some((_, existing_value)) = shell.env().get(variable_name) {
1643                !matches!(
1644                    existing_value.value(),
1645                    ShellValue::AssociativeArray(_)
1646                        | ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
1647                )
1648            } else {
1649                true
1650            };
1651
1652        if will_be_indexed_array {
1653            array_index = Some(
1654                arithmetic::expand_and_eval(shell, params, idx.as_str(), false)
1655                    .await?
1656                    .to_string(),
1657            );
1658        }
1659    }
1660
1661    // Read option before taking mutable borrow on env.
1662    let export_variables_on_modification = shell.options().export_variables_on_modification;
1663
1664    // Like bash, a variable for one command can't override a readonly one:
1665    // report that and skip this assignment. The command still runs.
1666    if required_scope.is_some()
1667        && shell
1668            .env()
1669            .get(variable_name.as_str())
1670            .is_some_and(|(_, var)| var.is_readonly())
1671    {
1672        let err = error::Error::from(error::ErrorKind::ReadonlyVariable);
1673        let _ = shell.display_error(&mut params.stderr(shell), &err);
1674        return Ok(());
1675    }
1676
1677    // See if we can find an existing value associated with the variable. A
1678    // variable for one command (e.g. `FOO=1 cmd`) can only be one already
1679    // set for that same command, in its own scope -- not one set for a
1680    // command it was called from.
1681    let lookup = required_scope.map_or(
1682        EnvironmentLookup::Anywhere,
1683        EnvironmentLookup::OnlyInCurrent,
1684    );
1685    let existing_value = shell
1686        .env_mut()
1687        .get_mut_using_policy(variable_name.as_str(), lookup);
1688    if let Some(existing_value) = existing_value {
1689        if let Some(array_index) = array_index {
1690            match new_value {
1691                ShellValueLiteral::Scalar(s) => {
1692                    existing_value.assign_at_index(array_index, s, assignment.append)?;
1693                }
1694                ShellValueLiteral::Array(_) => {
1695                    return error::unimp("replacing an array item with an array");
1696                }
1697            }
1698        } else {
1699            if !export
1700                && export_variables_on_modification
1701                && !matches!(new_value, ShellValueLiteral::Array(_))
1702            {
1703                export = true;
1704            }
1705
1706            existing_value.assign(new_value, assignment.append)?;
1707        }
1708
1709        if export {
1710            existing_value.export();
1711        }
1712
1713        // That's it!
1714        return Ok(());
1715    }
1716
1717    // If we fell down here, then we need to add it.
1718    let new_value = if let Some(array_index) = array_index {
1719        match new_value {
1720            ShellValueLiteral::Scalar(s) => {
1721                ShellValue::indexed_array_from_literals(ArrayLiteral(vec![(Some(array_index), s)]))
1722            }
1723            ShellValueLiteral::Array(_) => {
1724                return error::unimp("cannot assign list to array member");
1725            }
1726        }
1727    } else {
1728        match new_value {
1729            ShellValueLiteral::Scalar(s) => {
1730                export = export || shell.options().export_variables_on_modification;
1731
1732                // A new variable for a command (e.g. `FOO+=x cmd`) appends to
1733                // the value the variable has where it's
1734                // visible, as that variable would (e.g. adding, for
1735                // an integer); the result is a plain string.
1736                let s = match shell.env().get(variable_name.as_str()) {
1737                    Some((_, visible)) if assignment.append => {
1738                        let mut appended = visible.clone();
1739                        appended.assign(ShellValueLiteral::Scalar(s), true)?;
1740                        appended.value().to_cow_str(shell).into_owned()
1741                    }
1742                    _ => s,
1743                };
1744                ShellValue::String(s)
1745            }
1746            ShellValueLiteral::Array(values) => ShellValue::indexed_array_from_literals(values),
1747        }
1748    };
1749
1750    let mut new_var = ShellVariable::new(new_value);
1751
1752    if export {
1753        new_var.export();
1754    }
1755
1756    shell.env_mut().add(variable_name, new_var, creation_scope)
1757}
1758
1759#[expect(clippy::too_many_lines)]
1760pub(crate) async fn setup_redirect(
1761    shell: &mut Shell<impl extensions::ShellExtensions>,
1762    params: &'_ mut ExecutionParameters,
1763    redirect: &ast::IoRedirect,
1764) -> Result<(), error::Error> {
1765    match redirect {
1766        ast::IoRedirect::OutputAndError(f, append) => {
1767            let mut expanded_fields =
1768                expansion::full_expand_and_split_word(shell, params, f).await?;
1769            if expanded_fields.len() != 1 {
1770                return Err(error::ErrorKind::InvalidRedirection.into());
1771            }
1772
1773            let expanded_file_path = expanded_fields.remove(0);
1774            setup_redirect_output_and_error_to(shell, params, &expanded_file_path, *append)?;
1775        }
1776
1777        ast::IoRedirect::File(specified_fd_num, kind, target) => {
1778            match target {
1779                ast::IoFileRedirectTarget::Filename(f) => {
1780                    let mut options = std::fs::File::options();
1781
1782                    let mut expanded_fields =
1783                        expansion::full_expand_and_split_word(shell, params, f).await?;
1784
1785                    if expanded_fields.len() != 1 {
1786                        return Err(error::ErrorKind::InvalidRedirection.into());
1787                    }
1788
1789                    let expanded_file_path: PathBuf =
1790                        shell.absolute_path(Path::new(expanded_fields.remove(0).as_str()));
1791
1792                    let default_fd_if_unspecified = get_default_fd_for_redirect_kind(kind);
1793                    match kind {
1794                        ast::IoFileRedirectKind::Read => {
1795                            options.read(true);
1796                        }
1797                        ast::IoFileRedirectKind::Write => {
1798                            if shell
1799                                .options()
1800                                .disallow_overwriting_regular_files_via_output_redirection
1801                            {
1802                                // First check to see if the path points to an
1803                                // existing regular
1804                                // file.
1805                                if !expanded_file_path.is_file() {
1806                                    options.create(true);
1807                                } else {
1808                                    options.create_new(true);
1809                                }
1810                                options.write(true);
1811                            } else {
1812                                options.create(true);
1813                                options.write(true);
1814                                options.truncate(true);
1815                            }
1816                        }
1817                        ast::IoFileRedirectKind::Append => {
1818                            options.create(true);
1819                            options.append(true);
1820                        }
1821                        ast::IoFileRedirectKind::ReadAndWrite => {
1822                            options.create(true);
1823                            options.read(true);
1824                            options.write(true);
1825                        }
1826                        ast::IoFileRedirectKind::Clobber => {
1827                            options.create(true);
1828                            options.write(true);
1829                            options.truncate(true);
1830                        }
1831                        ast::IoFileRedirectKind::DuplicateInput => {
1832                            options.read(true);
1833                        }
1834                        ast::IoFileRedirectKind::DuplicateOutput => {
1835                            options.create(true);
1836                            options.write(true);
1837                        }
1838                    }
1839
1840                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1841
1842                    let opened_file = shell
1843                        .open_file(&options, &expanded_file_path, params)
1844                        .map_err(|err| {
1845                            error::ErrorKind::RedirectionFailure(
1846                                expanded_file_path.to_string_lossy().to_string(),
1847                                err.to_string(),
1848                            )
1849                        })?;
1850
1851                    params.open_files.set_fd(fd_num, opened_file);
1852                }
1853
1854                ast::IoFileRedirectTarget::Fd(fd) => {
1855                    let default_fd_if_unspecified = match kind {
1856                        ast::IoFileRedirectKind::DuplicateInput => 0,
1857                        ast::IoFileRedirectKind::DuplicateOutput => 1,
1858                        _ => {
1859                            return error::unimp("unexpected redirect kind");
1860                        }
1861                    };
1862
1863                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1864
1865                    if let Some(target_file) = params.try_fd(shell, *fd) {
1866                        params.open_files.set_fd(fd_num, target_file);
1867                    } else {
1868                        return Err(error::ErrorKind::BadFileDescriptor(*fd).into());
1869                    }
1870                }
1871
1872                ast::IoFileRedirectTarget::Duplicate(word) => {
1873                    let default_fd_if_unspecified = match kind {
1874                        ast::IoFileRedirectKind::DuplicateInput => 0,
1875                        ast::IoFileRedirectKind::DuplicateOutput => 1,
1876                        _ => {
1877                            return error::unimp("unexpected redirect kind");
1878                        }
1879                    };
1880
1881                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1882
1883                    let mut expanded_fields =
1884                        expansion::full_expand_and_split_word(shell, params, word).await?;
1885
1886                    if expanded_fields.len() != 1 {
1887                        return Err(error::ErrorKind::InvalidRedirection.into());
1888                    }
1889
1890                    let mut expanded = expanded_fields.remove(0);
1891
1892                    let dash = if expanded.ends_with('-') {
1893                        expanded.pop();
1894                        true
1895                    } else {
1896                        false
1897                    };
1898
1899                    if expanded.is_empty() {
1900                        // Nothing to do
1901                    } else if expanded.chars().all(|c: char| c.is_ascii_digit()) {
1902                        let source_fd_num = expanded
1903                            .parse::<ShellFd>()
1904                            .map_err(|_| error::ErrorKind::InvalidRedirection)?;
1905
1906                        // Reference the same open file as the source fd (shared
1907                        // handle; no OS-level duplication).
1908                        let Some(target_file) = params.try_fd(shell, source_fd_num) else {
1909                            return Err(error::ErrorKind::BadFileDescriptor(source_fd_num).into());
1910                        };
1911
1912                        params.open_files.set_fd(fd_num, target_file);
1913                    } else if fd_num == 1 && !dash {
1914                        // Special case for compatibility: redirect stdout and
1915                        // stderr to the file
1916                        // given by `expanded`.
1917                        setup_redirect_output_and_error_to(
1918                            shell, params, &expanded, false, /* append? */
1919                        )?;
1920                    } else {
1921                        return Err(error::ErrorKind::InvalidRedirection.into());
1922                    }
1923
1924                    if dash {
1925                        // Close the specified fd. Ignore it if it's not valid.
1926                        params.open_files.remove_fd(fd_num);
1927                    }
1928                }
1929
1930                ast::IoFileRedirectTarget::ProcessSubstitution(substitution_kind, subshell_cmd) => {
1931                    match kind {
1932                        ast::IoFileRedirectKind::Read
1933                        | ast::IoFileRedirectKind::Write
1934                        | ast::IoFileRedirectKind::Append
1935                        | ast::IoFileRedirectKind::ReadAndWrite
1936                        | ast::IoFileRedirectKind::Clobber => {
1937                            let (substitution_fd, substitution_file) = setup_process_substitution(
1938                                shell,
1939                                params,
1940                                substitution_kind,
1941                                subshell_cmd,
1942                            )?;
1943
1944                            let target_file = substitution_file.clone();
1945                            params.open_files.set_fd(substitution_fd, substitution_file);
1946
1947                            let fd_num = specified_fd_num
1948                                .unwrap_or_else(|| get_default_fd_for_redirect_kind(kind));
1949
1950                            params.open_files.set_fd(fd_num, target_file);
1951                        }
1952                        _ => return error::unimp("invalid process substitution"),
1953                    }
1954                }
1955            }
1956        }
1957
1958        ast::IoRedirect::HereDocument(fd_num, io_here) => {
1959            // If not specified, default to stdin (fd 0).
1960            let fd_num = fd_num.unwrap_or(0);
1961
1962            // Expand if required.
1963            let io_here_doc = if io_here.requires_expansion {
1964                expansion::basic_expand_heredoc_word(shell, params, &io_here.doc).await?
1965            } else {
1966                io_here.doc.flatten()
1967            };
1968
1969            let f = setup_open_file_with_contents(io_here_doc.as_str())?;
1970
1971            params.open_files.set_fd(fd_num, f);
1972        }
1973
1974        ast::IoRedirect::HereString(fd_num, word) => {
1975            // If not specified, default to stdin (fd 0).
1976            let fd_num = fd_num.unwrap_or(0);
1977
1978            let mut expanded_word = expansion::basic_expand_word(shell, params, word).await?;
1979            expanded_word.push('\n');
1980
1981            let f = setup_open_file_with_contents(expanded_word.as_str())?;
1982
1983            params.open_files.set_fd(fd_num, f);
1984        }
1985    }
1986
1987    Ok(())
1988}
1989
1990/// Sets up redirection of both stdout and stderr to the same file, given by
1991/// `file_path`.
1992///
1993/// # Arguments
1994///
1995/// * `shell` - The shell instance.
1996/// * `params` - The execution parameters to modify.
1997/// * `file_path` - The path to the file to redirect output and error to.
1998/// * `append` - Whether to append. If `false`, the file will be truncated.
1999fn setup_redirect_output_and_error_to(
2000    shell: &Shell<impl extensions::ShellExtensions>,
2001    params: &mut ExecutionParameters,
2002    file_path: &str,
2003    append: bool,
2004) -> Result<(), error::Error> {
2005    let abs_file_path: PathBuf = shell.absolute_path(Path::new(file_path));
2006
2007    let mut file_options = std::fs::File::options();
2008    file_options
2009        .create(true)
2010        .write(true)
2011        .truncate(!append)
2012        .append(append);
2013
2014    let stdout_file = shell
2015        .open_file(&file_options, &abs_file_path, params)
2016        .map_err(|err| {
2017            error::ErrorKind::RedirectionFailure(
2018                abs_file_path.to_string_lossy().to_string(),
2019                err.to_string(),
2020            )
2021        })?;
2022
2023    let stderr_file = stdout_file.clone();
2024
2025    params.open_files.set_fd(OpenFiles::STDOUT_FD, stdout_file);
2026    params.open_files.set_fd(OpenFiles::STDERR_FD, stderr_file);
2027
2028    Ok(())
2029}
2030
2031const fn get_default_fd_for_redirect_kind(kind: &ast::IoFileRedirectKind) -> ShellFd {
2032    match kind {
2033        ast::IoFileRedirectKind::Read => 0,
2034        ast::IoFileRedirectKind::Write => 1,
2035        ast::IoFileRedirectKind::Append => 1,
2036        ast::IoFileRedirectKind::ReadAndWrite => 0,
2037        ast::IoFileRedirectKind::Clobber => 1,
2038        ast::IoFileRedirectKind::DuplicateInput => 0,
2039        ast::IoFileRedirectKind::DuplicateOutput => 1,
2040    }
2041}
2042
2043fn setup_process_substitution(
2044    shell: &Shell<impl extensions::ShellExtensions>,
2045    params: &ExecutionParameters,
2046    kind: &ast::ProcessSubstitutionKind,
2047    subshell_cmd: &ast::SubshellCommand,
2048) -> Result<(ShellFd, OpenFile), error::Error> {
2049    // TODO(execute): Don't execute synchronously!
2050    // Execute in a subshell.
2051    let mut subshell = shell.clone();
2052
2053    // Set up execution parameters for the child execution.
2054    let mut child_params = params.clone();
2055    child_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
2056
2057    // Set up pipe so we can connect to the command.
2058    let (reader, writer) = sys::pipes::pipe()?;
2059    let (reader, writer) = (reader.into(), writer.into());
2060
2061    let target_file = match kind {
2062        ast::ProcessSubstitutionKind::Read => {
2063            child_params.open_files.set_fd(OpenFiles::STDOUT_FD, writer);
2064            reader
2065        }
2066        ast::ProcessSubstitutionKind::Write => {
2067            child_params.open_files.set_fd(OpenFiles::STDIN_FD, reader);
2068            writer
2069        }
2070    };
2071
2072    // Asynchronously spawn off the subshell; we intentionally don't block on
2073    // its completion.
2074    let subshell_cmd = subshell_cmd.to_owned();
2075    sys::tasks::spawn(async move {
2076        // Intentionally ignore the result of the subshell command.
2077        let _ = subshell_cmd
2078            .list
2079            .execute(&mut subshell, &child_params)
2080            .await;
2081    });
2082
2083    // Starting at 63 (a.k.a. 64-1)--and decrementing--look for an
2084    // available fd.
2085    let mut candidate_fd_num = 63;
2086    while params.open_files.contains_fd(candidate_fd_num) {
2087        candidate_fd_num -= 1;
2088        if candidate_fd_num == 0 {
2089            return error::unimp("no available file descriptors");
2090        }
2091    }
2092
2093    Ok((candidate_fd_num, target_file))
2094}
2095
2096fn setup_open_file_with_contents(contents: &str) -> Result<OpenFile, error::Error> {
2097    let (reader, mut writer) = sys::pipes::pipe()?;
2098
2099    let bytes = contents.as_bytes();
2100
2101    // ast-grep-ignore: platform-cfg-outside-sys
2102    #[cfg(any(target_os = "linux", target_os = "android"))]
2103    {
2104        use std::os::fd::AsFd as _;
2105
2106        let len = i32::try_from(bytes.len())
2107            .map_err(|_err| error::Error::from(error::ErrorKind::TooMuchData))?;
2108        nix::fcntl::fcntl(reader.as_fd(), nix::fcntl::FcntlArg::F_SETPIPE_SZ(len))?;
2109    }
2110
2111    writer.write_all(bytes)?;
2112    drop(writer);
2113
2114    Ok(reader.into())
2115}
2116
2117// Sizes depend on the platform's types, so they're only checked where they were
2118// measured.
2119#[cfg(test)]
2120#[cfg(target_os = "linux")] // ast-grep-ignore: platform-cfg-outside-sys
2121#[allow(clippy::panic_in_result_fn, reason = "assertions in a fallible test")]
2122mod tests {
2123    use super::*;
2124    use crate::{ProfileLoadBehavior, RcLoadBehavior};
2125
2126    /// Every simple command allocates these futures, so keep them small. A
2127    /// rarely taken branch can grow one by a kilobyte if it holds a `Shell`
2128    /// across an `.await`, and clippy's `large_futures` can't see futures
2129    /// whose layout depends on `SE`. The limits leave room for compiler
2130    /// updates; if one is exceeded, find out why before raising it.
2131    #[tokio::test]
2132    async fn futures_for_a_simple_command_stay_small() -> anyhow::Result<()> {
2133        let mut shell = Shell::builder()
2134            .profile(ProfileLoadBehavior::Skip)
2135            .rc(RcLoadBehavior::Skip)
2136            .build()
2137            .await?;
2138        let params = shell.default_exec_params();
2139
2140        let program = shell.parse_string("true")?;
2141        let list = &program.complete_commands[0];
2142        let and_or = &list.0[0].0;
2143        let pipeline = &and_or.first;
2144        let command = &pipeline.seq[0];
2145        let ast::Command::Simple(simple) = command else {
2146            anyhow::bail!("expected a simple command");
2147        };
2148
2149        let mut sizes = vec![];
2150        let context = PipelineExecutionContext {
2151            shell: commands::ShellForCommand::ParentShell(&mut shell),
2152            process_group_id: None,
2153        };
2154        let future = simple.execute_in_pipeline(context, params.clone());
2155        sizes.push((
2156            "SimpleCommand::execute_in_pipeline",
2157            size_of_val(&*future),
2158            3072,
2159        ));
2160        drop(future);
2161
2162        let context = PipelineExecutionContext {
2163            shell: commands::ShellForCommand::ParentShell(&mut shell),
2164            process_group_id: None,
2165        };
2166        let future = command.execute_in_pipeline(context, params.clone());
2167        sizes.push(("Command::execute_in_pipeline", size_of_val(&*future), 768));
2168        drop(future);
2169
2170        let future = pipeline.execute(&mut shell, &params);
2171        sizes.push(("Pipeline::execute", size_of_val(&*future), 768));
2172        drop(future);
2173
2174        let future = and_or.execute(&mut shell, &params);
2175        sizes.push(("AndOrList::execute", size_of_val(&*future), 256));
2176        drop(future);
2177
2178        let future = list.execute(&mut shell, &params);
2179        sizes.push(("CompoundList::execute", size_of_val(&*future), 128));
2180        drop(future);
2181
2182        for (name, size, limit) in sizes {
2183            assert!(
2184                size <= limit,
2185                "{name}'s future takes {size} bytes; the limit is {limit}"
2186            );
2187        }
2188
2189        Ok(())
2190    }
2191}