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        // Set up the pipes that we'll use to communicate with the coprocess.
761        let (stdin_reader, stdin_writer) = sys::pipes::pipe()?;
762        let (stdout_reader, stdout_writer) = sys::pipes::pipe()?;
763
764        // Allocate new fds in the (parent) shell for the read end of the
765        // coprocess's stdout and the write end of the coprocess's
766        // stdin.
767        let stdout_fd = shell.open_files_mut().add(stdout_reader.into())?;
768        let stdin_fd = shell.open_files_mut().add(stdin_writer.into())?;
769
770        // Crete a subshell that the coprocess will own and run in.
771        let mut child_shell = shell.clone();
772        child_shell.options_mut().interactive = false;
773
774        // Setup redirection for the coprocess's shell's stdin/stdout.
775        let mut child_params = params.clone();
776        child_params
777            .open_files
778            .set_fd(OpenFiles::STDIN_FD, stdin_reader.into());
779        child_params
780            .open_files
781            .set_fd(OpenFiles::STDOUT_FD, stdout_writer.into());
782
783        let body = self.body.clone();
784        let join_handle = sys::tasks::spawn(async move {
785            let pipeline_context = PipelineExecutionContext {
786                shell: commands::ShellForCommand::ParentShell(&mut child_shell),
787                process_group_id: None,
788            };
789            let spawn_result = body
790                .execute_in_pipeline(pipeline_context, child_params)
791                .await?;
792            match spawn_result.wait().await? {
793                ExecutionWaitResult::Completed(result) => Ok(result),
794                ExecutionWaitResult::Stopped(_) => Ok(ExecutionResult::stopped()),
795            }
796        });
797
798        let job = shell.jobs_mut().add_as_current(jobs::Job::new(
799            [jobs::JobTask::Internal(join_handle)],
800            format!("coproc {name}"),
801            jobs::JobState::Running,
802        ));
803        let job_id = job.id;
804
805        // Fill out the fd variable.
806        let arr_value = ShellValue::from(vec![stdout_fd.to_string(), stdin_fd.to_string()]);
807        shell
808            .env_mut()
809            .set_global(name.clone(), ShellVariable::new(arr_value))?;
810
811        // Set the job ID for the coprocess in a separate variable with the _PID
812        // suffix.
813        let pid_name = format!("{name}_PID");
814        shell
815            .env_mut()
816            .set_global(pid_name, ShellVariable::new(job_id.to_string()))?;
817
818        Ok(ExecutionResult::success())
819    }
820}
821
822#[async_trait::async_trait]
823impl Execute for ast::ForClauseCommand {
824    async fn execute(
825        &self,
826        shell: &mut Shell<impl extensions::ShellExtensions>,
827        params: &ExecutionParameters,
828    ) -> Result<ExecutionResult, error::Error> {
829        let mut result = ExecutionResult::success();
830
831        // If we were given explicit words to iterate over, then expand them
832        // all, with splitting enabled.
833        let expanded_values = if let Some(unexpanded_values) = &self.values {
834            expand_words(shell, params, unexpanded_values).await?
835        } else {
836            // Otherwise, we use the current positional parameters.
837            shell.current_shell_args().to_vec()
838        };
839
840        for value in expanded_values {
841            if shell.options().print_commands_and_arguments {
842                if let Some(unexpanded_values) = &self.values {
843                    shell
844                        .trace_command(
845                            params,
846                            std::format!(
847                                "for {} in {}",
848                                self.variable_name,
849                                unexpanded_values.iter().join(" ")
850                            ),
851                        )
852                        .await;
853                } else {
854                    shell
855                        .trace_command(params, std::format!("for {}", self.variable_name))
856                        .await;
857                }
858            }
859
860            // Update the variable.
861            shell.env_mut().update_or_add(
862                &self.variable_name,
863                ShellValueLiteral::Scalar(value),
864                |_| Ok(()),
865                EnvironmentLookup::Anywhere,
866                EnvironmentScope::Global,
867            )?;
868
869            result = self.body.list.execute(shell, params).await?;
870            if result.is_return_or_exit() {
871                break;
872            }
873
874            let is_break = result.is_break();
875
876            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
877
878            if is_break || result.is_continue() {
879                break;
880            }
881        }
882
883        shell.set_last_exit_status(result.exit_code.into());
884        Ok(result)
885    }
886}
887
888#[async_trait::async_trait]
889impl Execute for ast::CaseClauseCommand {
890    async fn execute(
891        &self,
892        shell: &mut Shell<impl extensions::ShellExtensions>,
893        params: &ExecutionParameters,
894    ) -> Result<ExecutionResult, error::Error> {
895        // N.B. One would think it makes sense to trace the expanded value being
896        // switched on, but that's not it.
897        if shell.options().print_commands_and_arguments {
898            shell
899                .trace_command(params, std::format!("case {} in", self.value))
900                .await;
901        }
902
903        let expanded_value = expansion::basic_expand_word(shell, params, &self.value).await?;
904        let mut result: ExecutionResult = ExecutionResult::success();
905        let mut force_execute_next_case = false;
906
907        for case in &self.cases {
908            if force_execute_next_case {
909                force_execute_next_case = false;
910            } else {
911                let mut matches = false;
912                for pattern in &case.patterns {
913                    let expanded_pattern = expansion::basic_expand_pattern(shell, params, pattern)
914                        .await?
915                        .set_extended_globbing(shell.options().extended_globbing)
916                        .set_case_insensitive(shell.options().case_insensitive_conditionals);
917
918                    if expanded_pattern.exactly_matches(expanded_value.as_str())? {
919                        matches = true;
920                        break;
921                    }
922                }
923
924                if !matches {
925                    continue;
926                }
927            }
928
929            result = if let Some(case_cmd) = &case.cmd {
930                case_cmd.execute(shell, params).await?
931            } else {
932                ExecutionResult::success()
933            };
934
935            // Check for early return (return/exit) or loop control flow
936            // (break/continue)
937            if !result.is_normal_flow() {
938                break;
939            }
940
941            match case.post_action {
942                ast::CaseItemPostAction::ExitCase => break,
943                ast::CaseItemPostAction::UnconditionallyExecuteNextCaseItem => {
944                    force_execute_next_case = true;
945                }
946                ast::CaseItemPostAction::ContinueEvaluatingCases => (),
947            }
948        }
949
950        shell.set_last_exit_status(result.exit_code.into());
951
952        Ok(result)
953    }
954}
955
956#[async_trait::async_trait]
957impl Execute for ast::IfClauseCommand {
958    async fn execute(
959        &self,
960        shell: &mut Shell<impl extensions::ShellExtensions>,
961        params: &ExecutionParameters,
962    ) -> Result<ExecutionResult, error::Error> {
963        // Execute condition with errexit suppressed
964        let mut condition_params = params.clone();
965        condition_params.suppress_errexit = true;
966        let condition = self.condition.execute(shell, &condition_params).await?;
967
968        // Check if the condition itself resulted in non-normal control flow.
969        if !condition.is_normal_flow() {
970            return Ok(condition);
971        }
972
973        if condition.is_success() {
974            return self.then.execute(shell, params).await;
975        }
976
977        if let Some(elses) = &self.elses {
978            for else_clause in elses {
979                match &else_clause.condition {
980                    Some(else_condition) => {
981                        let else_condition_result =
982                            else_condition.execute(shell, &condition_params).await?;
983
984                        // Check if the elif condition caused non-normal control
985                        // flow.
986                        if !else_condition_result.is_normal_flow() {
987                            return Ok(else_condition_result);
988                        }
989
990                        if else_condition_result.is_success() {
991                            return else_clause.body.execute(shell, params).await;
992                        }
993                    }
994                    None => {
995                        return else_clause.body.execute(shell, params).await;
996                    }
997                }
998            }
999        }
1000
1001        // If we got down here, then no branch was taken; we make sure to
1002        // reset the last exit status to success and then return success.
1003        let result = ExecutionResult::success();
1004        shell.set_last_exit_status(result.exit_code.into());
1005
1006        Ok(result)
1007    }
1008}
1009
1010#[async_trait::async_trait]
1011impl Execute for (WhileOrUntil, &ast::WhileOrUntilClauseCommand) {
1012    async fn execute(
1013        &self,
1014        shell: &mut Shell<impl extensions::ShellExtensions>,
1015        params: &ExecutionParameters,
1016    ) -> Result<ExecutionResult, error::Error> {
1017        let is_while = match self.0 {
1018            WhileOrUntil::While => true,
1019            WhileOrUntil::Until => false,
1020        };
1021        let test_condition = &self.1.0;
1022        let body = &self.1.1;
1023
1024        let mut result = ExecutionResult::success();
1025
1026        // Execute loop condition with errexit suppressed
1027        let mut condition_params = params.clone();
1028        condition_params.suppress_errexit = true;
1029
1030        loop {
1031            let condition_result = test_condition.execute(shell, &condition_params).await?;
1032
1033            // Update status for condition
1034            shell.set_last_exit_status(condition_result.exit_code.into());
1035
1036            if !condition_result.is_normal_flow() {
1037                result = condition_result;
1038
1039                // If the condition has break/continue, the while/until loop
1040                // itself consumes one level. We need to
1041                // decrement the level before returning.
1042                result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
1043                break;
1044            }
1045
1046            if condition_result.is_success() != is_while {
1047                break;
1048            }
1049
1050            result = body.list.execute(shell, params).await?;
1051            if result.is_return_or_exit() {
1052                break;
1053            }
1054
1055            let is_break = result.is_break();
1056
1057            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
1058
1059            if is_break || result.is_continue() {
1060                break;
1061            }
1062        }
1063
1064        shell.set_last_exit_status(result.exit_code.into());
1065        Ok(result)
1066    }
1067}
1068
1069#[async_trait::async_trait]
1070impl Execute for ast::ArithmeticCommand {
1071    async fn execute(
1072        &self,
1073        shell: &mut Shell<impl extensions::ShellExtensions>,
1074        params: &ExecutionParameters,
1075    ) -> Result<ExecutionResult, error::Error> {
1076        let value = self.expr.eval(shell, params, true).await?;
1077        let result = if value != 0 {
1078            ExecutionResult::success()
1079        } else {
1080            ExecutionResult::general_error()
1081        };
1082
1083        shell.set_last_exit_status(result.exit_code.into());
1084
1085        Ok(result)
1086    }
1087}
1088
1089#[async_trait::async_trait]
1090impl Execute for ast::ArithmeticForClauseCommand {
1091    async fn execute(
1092        &self,
1093        shell: &mut Shell<impl extensions::ShellExtensions>,
1094        params: &ExecutionParameters,
1095    ) -> Result<ExecutionResult, error::Error> {
1096        let mut result = ExecutionResult::success();
1097        if let Some(initializer) = &self.initializer {
1098            initializer.eval(shell, params, true).await?;
1099        }
1100
1101        loop {
1102            if let Some(condition) = &self.condition {
1103                // An empty condition (e.g., `for (( ; ; ))`) means "always
1104                // true".
1105                if !condition.value.is_empty() && condition.eval(shell, params, true).await? == 0 {
1106                    break;
1107                }
1108            }
1109
1110            result = self.body.list.execute(shell, params).await?;
1111            if result.is_return_or_exit() {
1112                break;
1113            }
1114
1115            let is_break = result.is_break();
1116
1117            result.next_control_flow = result.next_control_flow.try_decrement_loop_levels();
1118
1119            if is_break || result.is_continue() {
1120                break;
1121            }
1122
1123            if let Some(updater) = &self.updater {
1124                updater.eval(shell, params, true).await?;
1125            }
1126        }
1127
1128        shell.set_last_exit_status(result.exit_code.into());
1129        Ok(result)
1130    }
1131}
1132
1133#[async_trait::async_trait]
1134impl Execute for ast::FunctionDefinition {
1135    async fn execute(
1136        &self,
1137        shell: &mut Shell<impl extensions::ShellExtensions>,
1138        _params: &ExecutionParameters,
1139    ) -> Result<ExecutionResult, error::Error> {
1140        let func_name = self.fname.value.clone();
1141
1142        // In POSIX mode, function names can't shadow special builtins.
1143        if shell.options().posix_mode
1144            && shell
1145                .builtins()
1146                .get(&func_name)
1147                .is_some_and(|r| r.special_builtin)
1148        {
1149            return Err(
1150                error::Error::from(error::ErrorKind::FunctionNameShadowsSpecialBuiltin {
1151                    name: func_name,
1152                })
1153                .into_fatal(),
1154            );
1155        }
1156
1157        // The function definition's source context should be the same as the
1158        // current frame so we directly pass that through.
1159        let source_info = shell
1160            .call_stack()
1161            .current_frame()
1162            .map_or_else(crate::SourceInfo::default, |frame| {
1163                frame.adjusted_source_info()
1164            });
1165        shell.define_func(func_name, self.clone(), &source_info);
1166
1167        let result = ExecutionResult::success();
1168        shell.set_last_exit_status(result.exit_code.into());
1169
1170        Ok(result)
1171    }
1172}
1173
1174#[async_trait::async_trait]
1175#[allow(clippy::too_many_lines)]
1176impl<SE: extensions::ShellExtensions> ExecuteInPipeline<SE> for ast::SimpleCommand {
1177    async fn execute_in_pipeline(
1178        &self,
1179        mut context: PipelineExecutionContext<'_, SE>,
1180        mut params: ExecutionParameters,
1181    ) -> Result<ExecutionSpawnResult, error::Error> {
1182        let prefix_iter = self.prefix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
1183        let suffix_iter = self.suffix.as_ref().map(|s| s.0.iter()).unwrap_or_default();
1184        let cmd_name_items = self
1185            .word_or_name
1186            .as_ref()
1187            .map(|won| CommandPrefixOrSuffixItem::Word(won.clone()));
1188
1189        let mut assignments = vec![];
1190        let mut args: Vec<CommandArg> = vec![];
1191        let mut command_takes_assignments = false;
1192
1193        // Capture the status change count before expansion, so we can detect
1194        // if expansion (e.g., command substitution) set an exit status.
1195        let status_change_count_before_expansion = context.shell.last_exit_status_change_count();
1196
1197        for item in prefix_iter.chain(cmd_name_items.iter()).chain(suffix_iter) {
1198            match item {
1199                CommandPrefixOrSuffixItem::IoRedirect(redirect) => {
1200                    if let Err(e) = setup_redirect(&mut context.shell, &mut params, redirect).await
1201                    {
1202                        writeln!(params.stderr(&context.shell), "error: {e}")?;
1203                        return Ok(ExecutionResult::general_error().into());
1204                    }
1205                }
1206                CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell_command) => {
1207                    let (installed_fd_num, substitution_file) = setup_process_substitution(
1208                        &context.shell,
1209                        &params,
1210                        kind,
1211                        subshell_command,
1212                    )?;
1213
1214                    params
1215                        .open_files
1216                        .set_fd(installed_fd_num, substitution_file);
1217
1218                    args.push(CommandArg::String(std::format!(
1219                        "/dev/fd/{installed_fd_num}"
1220                    )));
1221                }
1222                CommandPrefixOrSuffixItem::AssignmentWord(assignment, word) => {
1223                    if args.is_empty() {
1224                        // If we haven't yet seen any arguments, then this must
1225                        // be a proper
1226                        // scoped assignment. Add it to the list we're
1227                        // accumulating.
1228                        assignments.push(assignment);
1229                    } else {
1230                        if command_takes_assignments {
1231                            // This looks like an assignment, and the command
1232                            // being invoked is a
1233                            // well-known builtin that takes arguments that need
1234                            // to function like
1235                            // assignments (but which are processed by the
1236                            // builtin).
1237                            let expanded =
1238                                expand_assignment(&mut context.shell, &params, assignment).await?;
1239                            args.push(CommandArg::Assignment(expanded));
1240                        } else {
1241                            // This *looks* like an assignment, but it's really
1242                            // a string we should
1243                            // fully treat as a regular looking
1244                            // argument.
1245                            let mut next_args = expansion::full_expand_and_split_word(
1246                                &mut context.shell,
1247                                &params,
1248                                word,
1249                            )
1250                            .await?
1251                            .into_iter()
1252                            .map(CommandArg::String)
1253                            .collect();
1254                            args.append(&mut next_args);
1255                        }
1256                    }
1257                }
1258                CommandPrefixOrSuffixItem::Word(arg) => {
1259                    let mut next_args =
1260                        expansion::full_expand_and_split_word(&mut context.shell, &params, arg)
1261                            .await?;
1262
1263                    if args.is_empty() {
1264                        if let Some(cmd_name) = next_args.first() {
1265                            // Aliases are only expanded when `expand_aliases`
1266                            // is enabled; it's
1267                            // enabled by default for interactive shells.
1268                            if context.shell.options().expand_aliases
1269                                && let Some(alias_value) =
1270                                    context.shell.aliases().get(cmd_name.as_str())
1271                            {
1272                                //
1273                                // TODO(#57): This is a total hack; aliases are
1274                                // supposed to be
1275                                // handled much earlier in the process.
1276                                //
1277                                // N.B. Tokenizing first releases our borrow of
1278                                // the shell's aliases,
1279                                // so we can take a mutable borrow of the shell
1280                                // to expand the words.
1281                                let alias_words = tokenize_alias_body(&context.shell, alias_value);
1282                                let mut alias_pieces =
1283                                    expand_words(&mut context.shell, &params, alias_words).await?;
1284
1285                                next_args.remove(0);
1286                                alias_pieces.append(&mut next_args);
1287
1288                                next_args = alias_pieces;
1289                            }
1290
1291                            // Check if we're going to be invoking a special
1292                            // declaration builtin.
1293                            // That will change how we parse and process args.
1294                            // (An alias with an
1295                            // empty body leaves us with no words at all.)
1296                            if let Some(first_arg) = next_args.first()
1297                                && context
1298                                    .shell
1299                                    .builtins()
1300                                    .get(first_arg.as_str())
1301                                    .is_some_and(|r| !r.disabled && r.declaration_builtin)
1302                            {
1303                                command_takes_assignments = true;
1304                            }
1305                        }
1306                    }
1307
1308                    let mut next_args = next_args.into_iter().map(CommandArg::String).collect();
1309                    args.append(&mut next_args);
1310                }
1311            }
1312        }
1313
1314        // If we have a command, then execute it.
1315        if let Some(CommandArg::String(cmd_name)) = args.first() {
1316            let cmd_name = cmd_name.clone();
1317            let mut stderr = params.stderr(&context.shell);
1318
1319            let (owned_shell, parent_shell) = match context.shell {
1320                commands::ShellForCommand::ParentShell(shell) => (None, shell),
1321                commands::ShellForCommand::OwnedShell { target, parent } => (Some(target), parent),
1322            };
1323
1324            let shell = if let Some(owned_shell) = owned_shell {
1325                commands::ShellForCommand::OwnedShell {
1326                    target: owned_shell,
1327                    parent: parent_shell,
1328                }
1329            } else {
1330                commands::ShellForCommand::ParentShell(parent_shell)
1331            };
1332
1333            let context = PipelineExecutionContext {
1334                shell,
1335                process_group_id: context.process_group_id,
1336            };
1337
1338            match execute_command(context, params, cmd_name, &assignments, args).await {
1339                Ok(result) => Ok(result),
1340                Err(err) => {
1341                    let _ = parent_shell.display_error(&mut stderr, &err);
1342
1343                    let result = err.into_result(parent_shell);
1344                    Ok(result.into())
1345                }
1346            }
1347        } else {
1348            // No command to run; assignments must be applied to this shell.
1349            for assignment in assignments {
1350                // Apply the assignment. Don't mark as fatal - let errors be
1351                // handled at the program level so multiple
1352                // complete_commands can execute independently.
1353                apply_assignment(
1354                    assignment,
1355                    &mut context.shell,
1356                    &params,
1357                    false,
1358                    None,
1359                    EnvironmentScope::Global,
1360                )
1361                .await?;
1362            }
1363
1364            // Assignment-only statements clear $_ (set to empty string).
1365            // This matches bash behavior where assignments don't have a "last
1366            // argument".
1367            context.shell.update_last_arg_variable(None);
1368
1369            // We need to set the last exit status to indicate assignment
1370            // success, but only if there was no status set during
1371            // expansion. We use the status count captured before
1372            // expansion to detect if command substitution (or other
1373            // expansion) set an exit status.
1374            if status_change_count_before_expansion == context.shell.last_exit_status_change_count()
1375            {
1376                context.shell.set_last_exit_status(0);
1377            }
1378
1379            // Return the last exit status we have; in some cases, an expansion
1380            // might result in a non-zero exit status stored in the shell.
1381            Ok(ExecutionResult::new(context.shell.last_exit_status()).into())
1382        }
1383    }
1384}
1385
1386async fn execute_command(
1387    context: PipelineExecutionContext<'_, impl extensions::ShellExtensions>,
1388    params: ExecutionParameters,
1389    cmd_name: String,
1390    assignments: &[&ast::Assignment],
1391    args: Vec<CommandArg>,
1392) -> Result<ExecutionSpawnResult, error::Error> {
1393    // Run the command in a new ephemeral environment scope, for its variable
1394    // assignments.
1395    let mut scoped;
1396    let mut shell = match context.shell {
1397        commands::ShellForCommand::ParentShell(shell) => {
1398            // The scope is left however the command ends, even if it's
1399            // cancelled.
1400            scoped = shell.enter_scope(EnvironmentScope::Command);
1401            commands::ShellForCommand::ParentShell(&mut scoped)
1402        }
1403        commands::ShellForCommand::OwnedShell { mut target, parent } => {
1404            // The command's shell is discarded after it runs, scope and all.
1405            target.env_mut().push_scope(EnvironmentScope::Command);
1406            commands::ShellForCommand::OwnedShell { target, parent }
1407        }
1408    };
1409
1410    for assignment in assignments {
1411        // Ensure it's tagged as exported and created in the command scope.
1412        apply_assignment(
1413            assignment,
1414            &mut shell,
1415            &params,
1416            true,
1417            Some(EnvironmentScope::Command),
1418            EnvironmentScope::Command,
1419        )
1420        .await?;
1421    }
1422
1423    if shell.options().print_commands_and_arguments {
1424        shell
1425            .trace_command(
1426                &params,
1427                args.iter().map(|arg| arg.quote_for_tracing()).join(" "),
1428            )
1429            .await;
1430    }
1431
1432    // Construct the command struct.
1433    let mut cmd = commands::SimpleCommand::new(shell, params, cmd_name, args);
1434    cmd.process_group_id = context.process_group_id;
1435
1436    // Run through any pre-execution hooks as best effort.
1437    let _ = commands::on_preexecute(&mut cmd).await;
1438
1439    // Execute
1440    // TODO(jobs): do we need to move self back to foreground on error here?
1441    cmd.execute().await
1442}
1443
1444/// Tokenizes the body of an alias into the unexpanded words that should replace
1445/// the aliased command name.
1446///
1447/// This only handles alias bodies that amount to a simple sequence of words;
1448/// bodies containing operators (pipes, redirections, `&&`, ...) and recursive
1449/// expansion of chained aliases require alias substitution to happen in the
1450/// tokenizer instead (see issue #57).
1451fn tokenize_alias_body(
1452    shell: &Shell<impl extensions::ShellExtensions>,
1453    alias_value: &str,
1454) -> Vec<String> {
1455    // Tokenize the body the same way the shell would tokenize any other input
1456    // it's given.
1457    let options = shell.parser_options().tokenizer_options();
1458
1459    // If we can't tokenize the body, fall back to naively splitting it on
1460    // whitespace.
1461    shrub_parser::tokenize_str_with_options(alias_value, &options).map_or_else(
1462        |_| {
1463            alias_value
1464                .split_ascii_whitespace()
1465                .map(str::to_owned)
1466                .collect()
1467        },
1468        |tokens| tokens.iter().map(|t| t.to_str().to_owned()).collect(),
1469    )
1470}
1471
1472/// Expands the given words, with splitting enabled, yielding the fields they
1473/// expand to.
1474async fn expand_words(
1475    shell: &mut Shell<impl extensions::ShellExtensions>,
1476    params: &ExecutionParameters,
1477    words: impl IntoIterator<Item = impl AsRef<str>>,
1478) -> Result<Vec<String>, error::Error> {
1479    // N.B. Expansion needs `&mut shell`, so the words have to be expanded in
1480    // sequence.
1481    let mut fields = vec![];
1482    for word in words {
1483        fields.extend(expansion::full_expand_and_split_word(shell, params, word).await?);
1484    }
1485    Ok(fields)
1486}
1487
1488async fn expand_assignment(
1489    shell: &mut Shell<impl extensions::ShellExtensions>,
1490    params: &ExecutionParameters,
1491    assignment: &ast::Assignment,
1492) -> Result<ast::Assignment, error::Error> {
1493    let value = expand_assignment_value(shell, params, &assignment.value).await?;
1494    Ok(ast::Assignment {
1495        name: basic_expand_assignment_name(shell, params, &assignment.name).await?,
1496        value,
1497        append: assignment.append,
1498        loc: assignment.loc.clone(),
1499    })
1500}
1501
1502async fn basic_expand_assignment_name(
1503    shell: &mut Shell<impl extensions::ShellExtensions>,
1504    params: &ExecutionParameters,
1505    name: &ast::AssignmentName,
1506) -> Result<ast::AssignmentName, error::Error> {
1507    match name {
1508        ast::AssignmentName::VariableName(name) => {
1509            let expanded = expansion::basic_expand_word(shell, params, name).await?;
1510            Ok(ast::AssignmentName::VariableName(expanded))
1511        }
1512        ast::AssignmentName::ArrayElementName(name, index) => {
1513            let expanded_name = expansion::basic_expand_word(shell, params, name).await?;
1514            let expanded_index = expansion::basic_expand_word(shell, params, index).await?;
1515            Ok(ast::AssignmentName::ArrayElementName(
1516                expanded_name,
1517                expanded_index,
1518            ))
1519        }
1520    }
1521}
1522
1523async fn expand_assignment_value(
1524    shell: &mut Shell<impl extensions::ShellExtensions>,
1525    params: &ExecutionParameters,
1526    value: &ast::AssignmentValue,
1527) -> Result<ast::AssignmentValue, error::Error> {
1528    let expanded = match value {
1529        ast::AssignmentValue::Scalar(s) => {
1530            let expanded_word = expansion::basic_expand_assignment_word(shell, params, s).await?;
1531            ast::AssignmentValue::Scalar(ast::Word::from(expanded_word))
1532        }
1533        ast::AssignmentValue::Array(arr) => {
1534            let mut expanded_values = vec![];
1535            for (key, value) in arr {
1536                if let Some(k) = key {
1537                    let expanded_key = expansion::basic_expand_assignment_word(shell, params, k)
1538                        .await?
1539                        .into();
1540                    let expanded_value =
1541                        expansion::basic_expand_assignment_word(shell, params, value)
1542                            .await?
1543                            .into();
1544                    expanded_values.push((Some(expanded_key), expanded_value));
1545                } else {
1546                    // Array elements are treated as regular words, not
1547                    // assignments
1548                    let split_expanded_value =
1549                        expansion::full_expand_and_split_word(shell, params, value).await?;
1550                    for expanded_value in split_expanded_value {
1551                        expanded_values.push((None, expanded_value.into()));
1552                    }
1553                }
1554            }
1555
1556            ast::AssignmentValue::Array(expanded_values)
1557        }
1558    };
1559
1560    Ok(expanded)
1561}
1562
1563#[expect(clippy::too_many_lines)]
1564async fn apply_assignment(
1565    assignment: &ast::Assignment,
1566    shell: &mut Shell<impl extensions::ShellExtensions>,
1567    params: &ExecutionParameters,
1568    mut export: bool,
1569    required_scope: Option<EnvironmentScope>,
1570    creation_scope: EnvironmentScope,
1571) -> Result<(), error::Error> {
1572    // Figure out if we are trying to assign to a variable or assign to an
1573    // element of an existing array.
1574    let mut array_index;
1575    let variable_name = match &assignment.name {
1576        ast::AssignmentName::VariableName(name) => {
1577            array_index = None;
1578            name
1579        }
1580        ast::AssignmentName::ArrayElementName(name, index) => {
1581            let expanded = expansion::basic_expand_word(shell, params, index).await?;
1582            array_index = Some(expanded);
1583            name
1584        }
1585    };
1586
1587    // Expand the values.
1588    let new_value = match &assignment.value {
1589        ast::AssignmentValue::Scalar(unexpanded_value) => {
1590            let value =
1591                expansion::basic_expand_assignment_word(shell, params, unexpanded_value).await?;
1592            ShellValueLiteral::Scalar(value)
1593        }
1594        ast::AssignmentValue::Array(unexpanded_values) => {
1595            let mut elements = vec![];
1596            for (unexpanded_key, unexpanded_value) in unexpanded_values {
1597                let key = match unexpanded_key {
1598                    Some(unexpanded_key) => Some(
1599                        expansion::basic_expand_assignment_word(shell, params, unexpanded_key)
1600                            .await?,
1601                    ),
1602                    None => None,
1603                };
1604
1605                if key.is_some() {
1606                    let value =
1607                        expansion::basic_expand_assignment_word(shell, params, unexpanded_value)
1608                            .await?;
1609                    elements.push((key, value));
1610                } else {
1611                    // Array elements are treated as regular words, not
1612                    // assignments
1613                    let values =
1614                        expansion::full_expand_and_split_word(shell, params, unexpanded_value)
1615                            .await?;
1616                    for value in values {
1617                        elements.push((None, value));
1618                    }
1619                }
1620            }
1621            ShellValueLiteral::Array(ArrayLiteral(elements))
1622        }
1623    };
1624
1625    if shell.options().print_commands_and_arguments {
1626        let op = if assignment.append { "+=" } else { "=" };
1627        shell
1628            .trace_command(params, std::format!("{}{op}{new_value}", assignment.name))
1629            .await;
1630    }
1631
1632    // See if we need to eval an array index.
1633    if let Some(idx) = &array_index {
1634        // An array subscript is arithmetically evaluated unless the target is
1635        // an associative array (in which case the subscript is used as
1636        // a literal key). A scalar or unset/untyped variable becomes an
1637        // indexed array, so its subscript still needs to be evaluated.
1638        let will_be_indexed_array =
1639            if let Some((_, existing_value)) = shell.env().get(variable_name) {
1640                !matches!(
1641                    existing_value.value(),
1642                    ShellValue::AssociativeArray(_)
1643                        | ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
1644                )
1645            } else {
1646                true
1647            };
1648
1649        if will_be_indexed_array {
1650            array_index = Some(
1651                arithmetic::expand_and_eval(shell, params, idx.as_str(), false)
1652                    .await?
1653                    .to_string(),
1654            );
1655        }
1656    }
1657
1658    // Read option before taking mutable borrow on env.
1659    let export_variables_on_modification = shell.options().export_variables_on_modification;
1660
1661    // Like bash, a variable for one command can't override a readonly one:
1662    // report that and skip this assignment. The command still runs.
1663    if required_scope.is_some()
1664        && shell
1665            .env()
1666            .get(variable_name.as_str())
1667            .is_some_and(|(_, var)| var.is_readonly())
1668    {
1669        let err = error::Error::from(error::ErrorKind::ReadonlyVariable);
1670        let _ = shell.display_error(&mut params.stderr(shell), &err);
1671        return Ok(());
1672    }
1673
1674    // See if we can find an existing value associated with the variable. A
1675    // variable for one command (e.g. `FOO=1 cmd`) can only be one already
1676    // set for that same command, in its own scope -- not one set for a
1677    // command it was called from.
1678    let lookup = required_scope.map_or(
1679        EnvironmentLookup::Anywhere,
1680        EnvironmentLookup::OnlyInCurrent,
1681    );
1682    let existing_value = shell
1683        .env_mut()
1684        .get_mut_using_policy(variable_name.as_str(), lookup);
1685    if let Some(existing_value) = existing_value {
1686        if let Some(array_index) = array_index {
1687            match new_value {
1688                ShellValueLiteral::Scalar(s) => {
1689                    existing_value.assign_at_index(array_index, s, assignment.append)?;
1690                }
1691                ShellValueLiteral::Array(_) => {
1692                    return error::unimp("replacing an array item with an array");
1693                }
1694            }
1695        } else {
1696            if !export
1697                && export_variables_on_modification
1698                && !matches!(new_value, ShellValueLiteral::Array(_))
1699            {
1700                export = true;
1701            }
1702
1703            existing_value.assign(new_value, assignment.append)?;
1704        }
1705
1706        if export {
1707            existing_value.export();
1708        }
1709
1710        // That's it!
1711        return Ok(());
1712    }
1713
1714    // If we fell down here, then we need to add it.
1715    let new_value = if let Some(array_index) = array_index {
1716        match new_value {
1717            ShellValueLiteral::Scalar(s) => {
1718                ShellValue::indexed_array_from_literals(ArrayLiteral(vec![(Some(array_index), s)]))
1719            }
1720            ShellValueLiteral::Array(_) => {
1721                return error::unimp("cannot assign list to array member");
1722            }
1723        }
1724    } else {
1725        match new_value {
1726            ShellValueLiteral::Scalar(s) => {
1727                export = export || shell.options().export_variables_on_modification;
1728
1729                // A new variable for a command (e.g. `FOO+=x cmd`) appends to
1730                // the value the variable has where it's
1731                // visible, as that variable would (e.g. adding, for
1732                // an integer); the result is a plain string.
1733                let s = match shell.env().get(variable_name.as_str()) {
1734                    Some((_, visible)) if assignment.append => {
1735                        let mut appended = visible.clone();
1736                        appended.assign(ShellValueLiteral::Scalar(s), true)?;
1737                        appended.value().to_cow_str(shell).into_owned()
1738                    }
1739                    _ => s,
1740                };
1741                ShellValue::String(s)
1742            }
1743            ShellValueLiteral::Array(values) => ShellValue::indexed_array_from_literals(values),
1744        }
1745    };
1746
1747    let mut new_var = ShellVariable::new(new_value);
1748
1749    if export {
1750        new_var.export();
1751    }
1752
1753    shell.env_mut().add(variable_name, new_var, creation_scope)
1754}
1755
1756#[expect(clippy::too_many_lines)]
1757pub(crate) async fn setup_redirect(
1758    shell: &mut Shell<impl extensions::ShellExtensions>,
1759    params: &'_ mut ExecutionParameters,
1760    redirect: &ast::IoRedirect,
1761) -> Result<(), error::Error> {
1762    match redirect {
1763        ast::IoRedirect::OutputAndError(f, append) => {
1764            let mut expanded_fields =
1765                expansion::full_expand_and_split_word(shell, params, f).await?;
1766            if expanded_fields.len() != 1 {
1767                return Err(error::ErrorKind::InvalidRedirection.into());
1768            }
1769
1770            let expanded_file_path = expanded_fields.remove(0);
1771            setup_redirect_output_and_error_to(shell, params, &expanded_file_path, *append)?;
1772        }
1773
1774        ast::IoRedirect::File(specified_fd_num, kind, target) => {
1775            match target {
1776                ast::IoFileRedirectTarget::Filename(f) => {
1777                    let mut options = std::fs::File::options();
1778
1779                    let mut expanded_fields =
1780                        expansion::full_expand_and_split_word(shell, params, f).await?;
1781
1782                    if expanded_fields.len() != 1 {
1783                        return Err(error::ErrorKind::InvalidRedirection.into());
1784                    }
1785
1786                    let expanded_file_path: PathBuf =
1787                        shell.absolute_path(Path::new(expanded_fields.remove(0).as_str()));
1788
1789                    let default_fd_if_unspecified = get_default_fd_for_redirect_kind(kind);
1790                    match kind {
1791                        ast::IoFileRedirectKind::Read => {
1792                            options.read(true);
1793                        }
1794                        ast::IoFileRedirectKind::Write => {
1795                            if shell
1796                                .options()
1797                                .disallow_overwriting_regular_files_via_output_redirection
1798                            {
1799                                // First check to see if the path points to an
1800                                // existing regular
1801                                // file.
1802                                if !expanded_file_path.is_file() {
1803                                    options.create(true);
1804                                } else {
1805                                    options.create_new(true);
1806                                }
1807                                options.write(true);
1808                            } else {
1809                                options.create(true);
1810                                options.write(true);
1811                                options.truncate(true);
1812                            }
1813                        }
1814                        ast::IoFileRedirectKind::Append => {
1815                            options.create(true);
1816                            options.append(true);
1817                        }
1818                        ast::IoFileRedirectKind::ReadAndWrite => {
1819                            options.create(true);
1820                            options.read(true);
1821                            options.write(true);
1822                        }
1823                        ast::IoFileRedirectKind::Clobber => {
1824                            options.create(true);
1825                            options.write(true);
1826                            options.truncate(true);
1827                        }
1828                        ast::IoFileRedirectKind::DuplicateInput => {
1829                            options.read(true);
1830                        }
1831                        ast::IoFileRedirectKind::DuplicateOutput => {
1832                            options.create(true);
1833                            options.write(true);
1834                        }
1835                    }
1836
1837                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1838
1839                    let opened_file = shell
1840                        .open_file(&options, &expanded_file_path, params)
1841                        .map_err(|err| {
1842                            error::ErrorKind::RedirectionFailure(
1843                                expanded_file_path.to_string_lossy().to_string(),
1844                                err.to_string(),
1845                            )
1846                        })?;
1847
1848                    params.open_files.set_fd(fd_num, opened_file);
1849                }
1850
1851                ast::IoFileRedirectTarget::Fd(fd) => {
1852                    let default_fd_if_unspecified = match kind {
1853                        ast::IoFileRedirectKind::DuplicateInput => 0,
1854                        ast::IoFileRedirectKind::DuplicateOutput => 1,
1855                        _ => {
1856                            return error::unimp("unexpected redirect kind");
1857                        }
1858                    };
1859
1860                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1861
1862                    if let Some(target_file) = params.try_fd(shell, *fd) {
1863                        params.open_files.set_fd(fd_num, target_file);
1864                    } else {
1865                        return Err(error::ErrorKind::BadFileDescriptor(*fd).into());
1866                    }
1867                }
1868
1869                ast::IoFileRedirectTarget::Duplicate(word) => {
1870                    let default_fd_if_unspecified = match kind {
1871                        ast::IoFileRedirectKind::DuplicateInput => 0,
1872                        ast::IoFileRedirectKind::DuplicateOutput => 1,
1873                        _ => {
1874                            return error::unimp("unexpected redirect kind");
1875                        }
1876                    };
1877
1878                    let fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
1879
1880                    let mut expanded_fields =
1881                        expansion::full_expand_and_split_word(shell, params, word).await?;
1882
1883                    if expanded_fields.len() != 1 {
1884                        return Err(error::ErrorKind::InvalidRedirection.into());
1885                    }
1886
1887                    let mut expanded = expanded_fields.remove(0);
1888
1889                    let dash = if expanded.ends_with('-') {
1890                        expanded.pop();
1891                        true
1892                    } else {
1893                        false
1894                    };
1895
1896                    if expanded.is_empty() {
1897                        // Nothing to do
1898                    } else if expanded.chars().all(|c: char| c.is_ascii_digit()) {
1899                        let source_fd_num = expanded
1900                            .parse::<ShellFd>()
1901                            .map_err(|_| error::ErrorKind::InvalidRedirection)?;
1902
1903                        // Reference the same open file as the source fd (shared
1904                        // handle; no OS-level duplication).
1905                        let Some(target_file) = params.try_fd(shell, source_fd_num) else {
1906                            return Err(error::ErrorKind::BadFileDescriptor(source_fd_num).into());
1907                        };
1908
1909                        params.open_files.set_fd(fd_num, target_file);
1910                    } else if fd_num == 1 && !dash {
1911                        // Special case for compatibility: redirect stdout and
1912                        // stderr to the file
1913                        // given by `expanded`.
1914                        setup_redirect_output_and_error_to(
1915                            shell, params, &expanded, false, /* append? */
1916                        )?;
1917                    } else {
1918                        return Err(error::ErrorKind::InvalidRedirection.into());
1919                    }
1920
1921                    if dash {
1922                        // Close the specified fd. Ignore it if it's not valid.
1923                        params.open_files.remove_fd(fd_num);
1924                    }
1925                }
1926
1927                ast::IoFileRedirectTarget::ProcessSubstitution(substitution_kind, subshell_cmd) => {
1928                    match kind {
1929                        ast::IoFileRedirectKind::Read
1930                        | ast::IoFileRedirectKind::Write
1931                        | ast::IoFileRedirectKind::Append
1932                        | ast::IoFileRedirectKind::ReadAndWrite
1933                        | ast::IoFileRedirectKind::Clobber => {
1934                            let (substitution_fd, substitution_file) = setup_process_substitution(
1935                                shell,
1936                                params,
1937                                substitution_kind,
1938                                subshell_cmd,
1939                            )?;
1940
1941                            let target_file = substitution_file.clone();
1942                            params.open_files.set_fd(substitution_fd, substitution_file);
1943
1944                            let fd_num = specified_fd_num
1945                                .unwrap_or_else(|| get_default_fd_for_redirect_kind(kind));
1946
1947                            params.open_files.set_fd(fd_num, target_file);
1948                        }
1949                        _ => return error::unimp("invalid process substitution"),
1950                    }
1951                }
1952            }
1953        }
1954
1955        ast::IoRedirect::HereDocument(fd_num, io_here) => {
1956            // If not specified, default to stdin (fd 0).
1957            let fd_num = fd_num.unwrap_or(0);
1958
1959            // Expand if required.
1960            let io_here_doc = if io_here.requires_expansion {
1961                expansion::basic_expand_heredoc_word(shell, params, &io_here.doc).await?
1962            } else {
1963                io_here.doc.flatten()
1964            };
1965
1966            let f = setup_open_file_with_contents(io_here_doc.as_str())?;
1967
1968            params.open_files.set_fd(fd_num, f);
1969        }
1970
1971        ast::IoRedirect::HereString(fd_num, word) => {
1972            // If not specified, default to stdin (fd 0).
1973            let fd_num = fd_num.unwrap_or(0);
1974
1975            let mut expanded_word = expansion::basic_expand_word(shell, params, word).await?;
1976            expanded_word.push('\n');
1977
1978            let f = setup_open_file_with_contents(expanded_word.as_str())?;
1979
1980            params.open_files.set_fd(fd_num, f);
1981        }
1982    }
1983
1984    Ok(())
1985}
1986
1987/// Sets up redirection of both stdout and stderr to the same file, given by
1988/// `file_path`.
1989///
1990/// # Arguments
1991///
1992/// * `shell` - The shell instance.
1993/// * `params` - The execution parameters to modify.
1994/// * `file_path` - The path to the file to redirect output and error to.
1995/// * `append` - Whether to append. If `false`, the file will be truncated.
1996fn setup_redirect_output_and_error_to(
1997    shell: &Shell<impl extensions::ShellExtensions>,
1998    params: &mut ExecutionParameters,
1999    file_path: &str,
2000    append: bool,
2001) -> Result<(), error::Error> {
2002    let abs_file_path: PathBuf = shell.absolute_path(Path::new(file_path));
2003
2004    let mut file_options = std::fs::File::options();
2005    file_options
2006        .create(true)
2007        .write(true)
2008        .truncate(!append)
2009        .append(append);
2010
2011    let stdout_file = shell
2012        .open_file(&file_options, &abs_file_path, params)
2013        .map_err(|err| {
2014            error::ErrorKind::RedirectionFailure(
2015                abs_file_path.to_string_lossy().to_string(),
2016                err.to_string(),
2017            )
2018        })?;
2019
2020    let stderr_file = stdout_file.clone();
2021
2022    params.open_files.set_fd(OpenFiles::STDOUT_FD, stdout_file);
2023    params.open_files.set_fd(OpenFiles::STDERR_FD, stderr_file);
2024
2025    Ok(())
2026}
2027
2028const fn get_default_fd_for_redirect_kind(kind: &ast::IoFileRedirectKind) -> ShellFd {
2029    match kind {
2030        ast::IoFileRedirectKind::Read => 0,
2031        ast::IoFileRedirectKind::Write => 1,
2032        ast::IoFileRedirectKind::Append => 1,
2033        ast::IoFileRedirectKind::ReadAndWrite => 0,
2034        ast::IoFileRedirectKind::Clobber => 1,
2035        ast::IoFileRedirectKind::DuplicateInput => 0,
2036        ast::IoFileRedirectKind::DuplicateOutput => 1,
2037    }
2038}
2039
2040fn setup_process_substitution(
2041    shell: &Shell<impl extensions::ShellExtensions>,
2042    params: &ExecutionParameters,
2043    kind: &ast::ProcessSubstitutionKind,
2044    subshell_cmd: &ast::SubshellCommand,
2045) -> Result<(ShellFd, OpenFile), error::Error> {
2046    // TODO(execute): Don't execute synchronously!
2047    // Execute in a subshell.
2048    let mut subshell = shell.clone();
2049
2050    // Set up execution parameters for the child execution.
2051    let mut child_params = params.clone();
2052    child_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
2053
2054    // Set up pipe so we can connect to the command.
2055    let (reader, writer) = sys::pipes::pipe()?;
2056    let (reader, writer) = (reader.into(), writer.into());
2057
2058    let target_file = match kind {
2059        ast::ProcessSubstitutionKind::Read => {
2060            child_params.open_files.set_fd(OpenFiles::STDOUT_FD, writer);
2061            reader
2062        }
2063        ast::ProcessSubstitutionKind::Write => {
2064            child_params.open_files.set_fd(OpenFiles::STDIN_FD, reader);
2065            writer
2066        }
2067    };
2068
2069    // Asynchronously spawn off the subshell; we intentionally don't block on
2070    // its completion.
2071    let subshell_cmd = subshell_cmd.to_owned();
2072    sys::tasks::spawn(async move {
2073        // Intentionally ignore the result of the subshell command.
2074        let _ = subshell_cmd
2075            .list
2076            .execute(&mut subshell, &child_params)
2077            .await;
2078    });
2079
2080    // Starting at 63 (a.k.a. 64-1)--and decrementing--look for an
2081    // available fd.
2082    let mut candidate_fd_num = 63;
2083    while params.open_files.contains_fd(candidate_fd_num) {
2084        candidate_fd_num -= 1;
2085        if candidate_fd_num == 0 {
2086            return error::unimp("no available file descriptors");
2087        }
2088    }
2089
2090    Ok((candidate_fd_num, target_file))
2091}
2092
2093fn setup_open_file_with_contents(contents: &str) -> Result<OpenFile, error::Error> {
2094    let (reader, mut writer) = sys::pipes::pipe()?;
2095
2096    let bytes = contents.as_bytes();
2097
2098    // ast-grep-ignore: platform-cfg-outside-sys
2099    #[cfg(any(target_os = "linux", target_os = "android"))]
2100    {
2101        use std::os::fd::AsFd as _;
2102
2103        let len = i32::try_from(bytes.len())
2104            .map_err(|_err| error::Error::from(error::ErrorKind::TooMuchData))?;
2105        nix::fcntl::fcntl(reader.as_fd(), nix::fcntl::FcntlArg::F_SETPIPE_SZ(len))?;
2106    }
2107
2108    writer.write_all(bytes)?;
2109    drop(writer);
2110
2111    Ok(reader.into())
2112}
2113
2114// Sizes depend on the platform's types, so they're only checked where they were
2115// measured.
2116#[cfg(test)]
2117#[cfg(target_os = "linux")] // ast-grep-ignore: platform-cfg-outside-sys
2118#[allow(clippy::panic_in_result_fn, reason = "assertions in a fallible test")]
2119mod tests {
2120    use super::*;
2121    use crate::{ProfileLoadBehavior, RcLoadBehavior};
2122
2123    /// Every simple command allocates these futures, so keep them small. A
2124    /// rarely taken branch can grow one by a kilobyte if it holds a `Shell`
2125    /// across an `.await`, and clippy's `large_futures` can't see futures
2126    /// whose layout depends on `SE`. The limits leave room for compiler
2127    /// updates; if one is exceeded, find out why before raising it.
2128    #[tokio::test]
2129    async fn futures_for_a_simple_command_stay_small() -> anyhow::Result<()> {
2130        let mut shell = Shell::builder()
2131            .profile(ProfileLoadBehavior::Skip)
2132            .rc(RcLoadBehavior::Skip)
2133            .build()
2134            .await?;
2135        let params = shell.default_exec_params();
2136
2137        let program = shell.parse_string("true")?;
2138        let list = &program.complete_commands[0];
2139        let and_or = &list.0[0].0;
2140        let pipeline = &and_or.first;
2141        let command = &pipeline.seq[0];
2142        let ast::Command::Simple(simple) = command else {
2143            anyhow::bail!("expected a simple command");
2144        };
2145
2146        let mut sizes = vec![];
2147        let context = PipelineExecutionContext {
2148            shell: commands::ShellForCommand::ParentShell(&mut shell),
2149            process_group_id: None,
2150        };
2151        let future = simple.execute_in_pipeline(context, params.clone());
2152        sizes.push((
2153            "SimpleCommand::execute_in_pipeline",
2154            size_of_val(&*future),
2155            3072,
2156        ));
2157        drop(future);
2158
2159        let context = PipelineExecutionContext {
2160            shell: commands::ShellForCommand::ParentShell(&mut shell),
2161            process_group_id: None,
2162        };
2163        let future = command.execute_in_pipeline(context, params.clone());
2164        sizes.push(("Command::execute_in_pipeline", size_of_val(&*future), 768));
2165        drop(future);
2166
2167        let future = pipeline.execute(&mut shell, &params);
2168        sizes.push(("Pipeline::execute", size_of_val(&*future), 768));
2169        drop(future);
2170
2171        let future = and_or.execute(&mut shell, &params);
2172        sizes.push(("AndOrList::execute", size_of_val(&*future), 256));
2173        drop(future);
2174
2175        let future = list.execute(&mut shell, &params);
2176        sizes.push(("CompoundList::execute", size_of_val(&*future), 128));
2177        drop(future);
2178
2179        for (name, size, limit) in sizes {
2180            assert!(
2181                size <= limit,
2182                "{name}'s future takes {size} bytes; the limit is {limit}"
2183            );
2184        }
2185
2186        Ok(())
2187    }
2188}