Skip to main content

clash_brush_core/
interp.rs

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