Skip to main content

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