Repository: reubeno/brush
Pull request #374 (2025-02-02 04:14:09)

fix: propagate execution parameters more thoroughly
Address cases where command substitution environments don't correctly inherit the open files of the containing command.

_Caveat: I'm not thrilled about how much code needed to be touched, but this at least *fixes* a known defect. Hopefully we can clean it up subsequently._

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   17.98 μs` | `   18.20 μs` | `    0.22 μs` | `⚪  Unchanged` |
| `eval_arithmetic                     ` | `    0.17 μs` | `    0.17 μs` | `    0.00 μs` | `⚪  Unchanged` |
| `expand_one_string                   ` | `    1.62 μs` | `    1.80 μs` | `    0.18 μs` | `🟠 +11.02%   ` |
| `for_loop                            ` | `   22.17 μs` | `   21.68 μs` | `   -0.49 μs` | `⚪  Unchanged` |
| `function_call                       ` | `    2.36 μs` | `    2.30 μs` | `   -0.06 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   57.41 μs` | `   57.16 μs` | `   -0.24 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `21709.54 μs` | `21435.61 μs` | ` -273.93 μs` | `⚪  Unchanged` |
| `parse_bash_completion               ` | ` 1663.78 μs` | ` 1662.84 μs` | `   -0.94 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    1.75 μs` | `    1.76 μs` | `    0.01 μs` | `⚪  Unchanged` |
| `run_echo_builtin_command            ` | `   15.38 μs` | `   15.39 μs` | `    0.01 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 2399.87 μs` | ` 1829.34 μs` | ` -570.52 μs` | `🟢 -23.77%   ` |
| `tokenize_sample_script              ` | `    2.79 μs` | `    2.91 μs` | `    0.12 μs` | `🟠 +4.26%    ` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/arithmetic.rs | 🟢 98.48% | 🟢 98.52% | 🟢 0.04% |
| brush-core/src/builtins/printf.rs | 🟢 93.88% | 🟢 94.55% | 🟢 0.67% |
| brush-core/src/commands.rs | 🟢 87.14% | 🟢 87.21% | 🟢 0.07% |
| brush-core/src/completion.rs | 🟠 63.43% | 🟠 63.41% | 🔴 -0.02% |
| brush-core/src/expansion.rs | 🟢 96.1% | 🟢 96.14% | 🟢 0.04% |
| brush-core/src/extendedtests.rs | 🟠 64.61% | 🟠 66.36% | 🟢 1.75% |
| brush-core/src/interp.rs | 🟢 91.48% | 🟢 91.6% | 🟢 0.12% |
| brush-core/src/jobs.rs | 🔴 37.07% | 🔴 43.1% | 🟢 6.03% |
| brush-core/src/options.rs | 🟢 92.39% | 🟢 93.48% | 🟢 1.09% |
| brush-core/src/shell.rs | 🟢 86.07% | 🟢 85.95% | 🔴 -0.12% |
| **Overall Coverage** | **🟢 77.81%** | **🟢 77.97%** | **🟢 0.16%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `77.97%`
<!-- Sticky Pull Request Comment -->
github-actions[bot]: ## Test Results
    2 files      9 suites   1m 37s ⏱️
  668 tests   668 ✅ 0 💤 0 ❌
1 322 runs  1 322 ✅ 0 💤 0 ❌

Results for commit f99d0c0a.

[test-results]:data:application/gzip;base64,H4sIAOzznmcC/13MSw7CIBSF4a00jB1Q5HXdjKG3kBDbYiiMjHsXSKutw+8/yXkR5ye7klvHLh1Zs08NUDDmaJIPS6UqLkuqm5R6133NiP/p4Z8l0W9wxk+nYGMMcSsxL/WzvzK2af88pd9n8+Gy+fiIYZ59KiAOYKRIjUDO+WClAq6pQeEGrYBpEEz0jKIk7w+VMUnKBgEAAA==


Patch:
diff --git a/brush-core/benches/shell.rs b/brush-core/benches/shell.rs
index ed29401..83b133c 100644
--- a/brush-core/benches/shell.rs
+++ b/brush-core/benches/shell.rs
@@ -26,7 +26,8 @@ mod unix {
     }
 
     async fn expand_string(shell: &mut brush_core::Shell, s: &str) {
-        let _ = shell.basic_expand_string(s).await.unwrap();
+        let params = shell.default_exec_params();
+        let _ = shell.basic_expand_string(&params, s).await.unwrap();
     }
 
     fn eval_arithmetic_expr(shell: &mut brush_core::Shell, expr: &str) {
diff --git a/brush-core/src/arithmetic.rs b/brush-core/src/arithmetic.rs
index 0dfe413..ccd0852 100644
--- a/brush-core/src/arithmetic.rs
+++ b/brush-core/src/arithmetic.rs
@@ -1,6 +1,6 @@
 use std::borrow::Cow;
 
-use crate::{env, expansion, variables, Shell};
+use crate::{env, expansion, variables, ExecutionParameters, Shell};
 use brush_parser::ast;
 
 /// Represents an error that occurs during evaluation of an arithmetic expression.
@@ -47,12 +47,22 @@ pub trait ExpandAndEvaluate {
     ///
     /// * `shell` - The shell to use for evaluation.
     /// * `trace_if_needed` - Whether to trace the evaluation.
-    async fn eval(&self, shell: &mut Shell, trace_if_needed: bool) -> Result<i64, EvalError>;
+    async fn eval(
+        &self,
+        shell: &mut Shell,
+        params: &ExecutionParameters,
+        trace_if_needed: bool,
+    ) -> Result<i64, EvalError>;
 }
 
 impl ExpandAndEvaluate for ast::UnexpandedArithmeticExpr {
-    async fn eval(&self, shell: &mut Shell, trace_if_needed: bool) -> Result<i64, EvalError> {
-        expand_and_eval(shell, self.value.as_str(), trace_if_needed).await
+    async fn eval(
+        &self,
+        shell: &mut Shell,
+        params: &ExecutionParameters,
+        trace_if_needed: bool,
+    ) -> Result<i64, EvalError> {
+        expand_and_eval(shell, params, self.value.as_str(), trace_if_needed).await
     }
 }
 
@@ -65,11 +75,12 @@ impl ExpandAndEvaluate for ast::UnexpandedArithmeticExpr {
 /// * `trace_if_needed` - Whether to trace the evaluation.
 pub(crate) async fn expand_and_eval(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     expr: &str,
     trace_if_needed: bool,
 ) -> Result<i64, EvalError> {
     // Per documentation, first shell-expand it.
-    let expanded_self = expansion::basic_expand_str_without_tilde(shell, expr)
+    let expanded_self = expansion::basic_expand_str_without_tilde(shell, params, expr)
         .await
         .map_err(|_e| EvalError::FailedToExpandExpression(expr.to_owned()))?;
 
diff --git a/brush-core/src/builtins/printf.rs b/brush-core/src/builtins/printf.rs
index 50b6012..b370af1 100644
--- a/brush-core/src/builtins/printf.rs
+++ b/brush-core/src/builtins/printf.rs
@@ -24,7 +24,13 @@ impl builtins::Command for PrintfCommand {
         let result = self.evaluate(&context)?;
 
         if let Some(variable_name) = &self.output_variable {
-            expansion::assign_to_named_parameter(context.shell, variable_name, result).await?;
+            expansion::assign_to_named_parameter(
+                context.shell,
+                &context.params,
+                variable_name,
+                result,
+            )
+            .await?;
         } else {
             write!(context.stdout(), "{result}")?;
             context.stdout().flush()?;
diff --git a/brush-core/src/commands.rs b/brush-core/src/commands.rs
index 46b4907..9b56fc1 100644
--- a/brush-core/src/commands.rs
+++ b/brush-core/src/commands.rs
@@ -521,7 +521,7 @@ pub(crate) async fn invoke_shell_function(
     // Apply any redirects specified at function definition-time.
     if let Some(redirects) = redirects {
         for redirect in &redirects.0 {
-            interp::setup_redirect(&mut context.params.open_files, context.shell, redirect).await?;
+            interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
         }
     }
 
@@ -563,21 +563,23 @@ pub(crate) async fn invoke_shell_function(
 
 pub(crate) async fn invoke_command_in_subshell_and_get_output(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     s: String,
 ) -> Result<String, error::Error> {
     // Instantiate a subshell to run the command in.
     let mut subshell = shell.clone();
 
+    // Get our own set of parameters we can customize and use.
+    let mut params = params.clone();
+    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
+
     // Set up pipe so we can read the output.
     let (reader, writer) = sys::pipes::pipe()?;
-    subshell
+    params
         .open_files
         .files
         .insert(1, openfiles::OpenFile::PipeWriter(writer));
 
-    let mut params = subshell.default_exec_params();
-    params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
-
     // Run the command.
     let result = subshell.run_string(s, &params).await?;
 
diff --git a/brush-core/src/completion.rs b/brush-core/src/completion.rs
index 8c3d7cc..030ea66 100644
--- a/brush-core/src/completion.rs
+++ b/brush-core/src/completion.rs
@@ -244,7 +244,9 @@ impl Spec {
         // Generate completions based on any provided actions (and on words).
         let mut candidates = self.generate_action_completions(shell, context).await?;
         if let Some(word_list) = &self.word_list {
-            let words = crate::expansion::full_expand_and_split_str(shell, word_list).await?;
+            let params = shell.default_exec_params();
+            let words =
+                crate::expansion::full_expand_and_split_str(shell, &params, word_list).await?;
             for word in words {
                 if word.starts_with(context.token_to_complete) {
                     candidates.insert(word);
@@ -607,8 +609,10 @@ impl Spec {
         }
 
         // Run the command.
+        let params = shell.default_exec_params();
         let output =
-            commands::invoke_command_in_subshell_and_get_output(&mut shell, command_line).await?;
+            commands::invoke_command_in_subshell_and_get_output(&mut shell, &params, command_line)
+                .await?;
 
         // Split results.
         let mut candidates = IndexSet::new();
@@ -1052,8 +1056,9 @@ async fn get_file_completions(
 ) -> IndexSet<String> {
     // Basic-expand the token-to-be-completed; it won't have been expanded to this point.
     let mut throwaway_shell = shell.clone();
+    let params = throwaway_shell.default_exec_params();
     let expanded_token = throwaway_shell
-        .basic_expand_string(token_to_complete)
+        .basic_expand_string(&params, token_to_complete)
         .await
         .unwrap_or_else(|_err| token_to_complete.to_owned());
 
diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs
index 6424f71..a24b32f 100644
--- a/brush-core/src/expansion.rs
+++ b/brush-core/src/expansion.rs
@@ -20,6 +20,7 @@ use crate::trace_categories;
 use crate::variables::ShellValueUnsetType;
 use crate::variables::ShellVariable;
 use crate::variables::{self, ShellValue};
+use crate::ExecutionParameters;
 
 #[derive(Debug)]
 struct Expansion {
@@ -294,17 +295,19 @@ enum ParameterState {
 
 pub(crate) async fn basic_expand_pattern(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     word: &ast::Word,
 ) -> Result<patterns::Pattern, error::Error> {
-    let mut expander = WordExpander::new(shell);
+    let mut expander = WordExpander::new(shell, params);
     expander.basic_expand_pattern(&word.flatten()).await
 }
 
 pub(crate) async fn basic_expand_regex(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     word: &ast::Word,
 ) -> Result<crate::regex::Regex, error::Error> {
-    let mut expander = WordExpander::new(shell);
+    let mut expander = WordExpander::new(shell, params);
 
     // Brace expansion does not appear to be used in regexes.
     expander.force_disable_brace_expansion = true;
@@ -314,63 +317,73 @@ pub(crate) async fn basic_expand_regex(
 
 pub(crate) async fn basic_expand_word(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     word: &ast::Word,
 ) -> Result<String, error::Error> {
-    basic_expand_str(shell, word.flatten().as_str()).await
+    basic_expand_str(shell, params, word.flatten().as_str()).await
 }
 
-pub(crate) async fn basic_expand_str(shell: &mut Shell, s: &str) -> Result<String, error::Error> {
-    let mut expander = WordExpander::new(shell);
+pub(crate) async fn basic_expand_str(
+    shell: &mut Shell,
+    params: &ExecutionParameters,
+    s: &str,
+) -> Result<String, error::Error> {
+    let mut expander = WordExpander::new(shell, params);
     expander.basic_expand_to_str(s).await
 }
 
 pub(crate) async fn basic_expand_str_without_tilde(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     s: &str,
 ) -> Result<String, error::Error> {
-    let mut expander = WordExpander::new(shell);
+    let mut expander = WordExpander::new(shell, params);
     expander.parser_options.tilde_expansion = false;
     expander.basic_expand_to_str(s).await
 }
 
 pub(crate) async fn full_expand_and_split_word(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     word: &ast::Word,
 ) -> Result<Vec<String>, error::Error> {
-    full_expand_and_split_str(shell, word.flatten().as_str()).await
+    full_expand_and_split_str(shell, params, word.flatten().as_str()).await
 }
 
 pub(crate) async fn full_expand_and_split_str(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     s: &str,
 ) -> Result<Vec<String>, error::Error> {
-    let mut expander = WordExpander::new(shell);
+    let mut expander = WordExpander::new(shell, params);
     expander.full_expand_with_splitting(s).await
 }
 
 pub(crate) async fn assign_to_named_parameter(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     name: &str,
     value: String,
 ) -> Result<(), error::Error> {
     let parser_options = shell.parser_options();
-    let mut expander = WordExpander::new(shell);
+    let mut expander = WordExpander::new(shell, params);
     let parameter = brush_parser::word::parse_parameter(name, &parser_options)?;
     expander.assign_to_parameter(&parameter, value).await
 }
 
 struct WordExpander<'a> {
     shell: &'a mut Shell,
+    params: &'a ExecutionParameters,
     parser_options: brush_parser::ParserOptions,
     force_disable_brace_expansion: bool,
 }
 
 impl<'a> WordExpander<'a> {
-    pub fn new(shell: &'a mut Shell) -> Self {
+    pub fn new(shell: &'a mut Shell, params: &'a ExecutionParameters) -> Self {
         let parser_options = shell.parser_options();
-
         Self {
             shell,
+            params,
             parser_options,
             force_disable_brace_expansion: false,
         }
@@ -710,7 +723,8 @@ impl<'a> WordExpander<'a> {
             brush_parser::word::WordPiece::BackquotedCommandSubstitution(s)
             | brush_parser::word::WordPiece::CommandSubstitution(s) => {
                 let output_str =
-                    commands::invoke_command_in_subshell_and_get_output(self.shell, s).await?;
+                    commands::invoke_command_in_subshell_and_get_output(self.shell, self.params, s)
+                        .await?;
 
                 // We trim trailing newlines, per spec.
                 let trimmed = output_str.trim_end_matches('\n');
@@ -923,7 +937,7 @@ impl<'a> WordExpander<'a> {
                     );
                 }
 
-                let expanded_offset = offset.eval(self.shell, false).await?;
+                let expanded_offset = offset.eval(self.shell, self.params, false).await?;
                 let expanded_offset = if expanded_offset < 0 {
                     0
                 } else {
@@ -934,7 +948,7 @@ impl<'a> WordExpander<'a> {
                 let expanded_offset = min(expanded_offset, expanded_parameter_len);
 
                 let end_offset = if let Some(length) = length {
-                    let mut expanded_length = length.eval(self.shell, false).await?;
+                    let mut expanded_length = length.eval(self.shell, self.params, false).await?;
                     if expanded_length < 0 {
                         let param_length: i64 = i64::try_from(expanded_parameter_len)?;
                         expanded_length += param_length;
@@ -1357,7 +1371,7 @@ impl<'a> WordExpander<'a> {
         let index_to_use = if for_set_associative_array {
             self.basic_expand_to_str(index).await?
         } else {
-            arithmetic::expand_and_eval(self.shell, index, false)
+            arithmetic::expand_and_eval(self.shell, self.params, index, false)
                 .await?
                 .to_string()
         };
@@ -1416,7 +1430,7 @@ impl<'a> WordExpander<'a> {
         &mut self,
         expr: brush_parser::ast::UnexpandedArithmeticExpr,
     ) -> Result<String, error::Error> {
-        let value = expr.eval(self.shell, false).await?;
+        let value = expr.eval(self.shell, self.params, false).await?;
         Ok(value.to_string())
     }
 
@@ -1691,33 +1705,34 @@ mod tests {
     async fn test_full_expansion() -> Result<()> {
         let options = crate::shell::CreateOptions::default();
         let mut shell = crate::shell::Shell::new(&options).await?;
+        let params = shell.default_exec_params();
 
         assert_eq!(
-            full_expand_and_split_str(&mut shell, "\"\"").await?,
+            full_expand_and_split_str(&mut shell, &params, "\"\"").await?,
             vec![""]
         );
         assert_eq!(
-            full_expand_and_split_str(&mut shell, "a b").await?,
+            full_expand_and_split_str(&mut shell, &params, "a b").await?,
             vec!["a", "b"]
         );
         assert_eq!(
-            full_expand_and_split_str(&mut shell, "ab").await?,
+            full_expand_and_split_str(&mut shell, &params, "ab").await?,
             vec!["ab"]
         );
         assert_eq!(
-            full_expand_and_split_str(&mut shell, r#""a b""#).await?,
+            full_expand_and_split_str(&mut shell, &params, r#""a b""#).await?,
             vec!["a b"]
         );
         assert_eq!(
-            full_expand_and_split_str(&mut shell, "").await?,
+            full_expand_and_split_str(&mut shell, &params, "").await?,
             Vec::<String>::new()
         );
         assert_eq!(
-            full_expand_and_split_str(&mut shell, "$@").await?,
+            full_expand_and_split_str(&mut shell, &params, "$@").await?,
             Vec::<String>::new()
         );
         assert_eq!(
-            full_expand_and_split_str(&mut shell, "$*").await?,
+            full_expand_and_split_str(&mut shell, &params, "$*").await?,
             Vec::<String>::new()
         );
 
@@ -1728,7 +1743,8 @@ mod tests {
     async fn test_brace_expansion() -> Result<()> {
         let options = crate::shell::CreateOptions::default();
         let mut shell = crate::shell::Shell::new(&options).await?;
-        let expander = WordExpander::new(&mut shell);
+        let params = shell.default_exec_params();
+        let expander = WordExpander::new(&mut shell, &params);
 
         assert_eq!(expander.brace_expand_if_needed("abc")?, ["abc"]);
         assert_eq!(expander.brace_expand_if_needed("a{,b}d")?, ["ad", "abd"]);
@@ -1751,7 +1767,8 @@ mod tests {
     async fn test_field_splitting() -> Result<()> {
         let options = crate::shell::CreateOptions::default();
         let mut shell = crate::shell::Shell::new(&options).await?;
-        let expander = WordExpander::new(&mut shell);
+        let params = shell.default_exec_params();
+        let expander = WordExpander::new(&mut shell, &params);
 
         let expansion = Expansion {
             fields: vec![
diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs
index cb8a243..9885fe8 100644
--- a/brush-core/src/interp.rs
+++ b/brush-core/src/interp.rs
@@ -482,8 +482,7 @@ impl ExecuteInPipeline for ast::Command {
                 // Set up any additional redirects.
                 if let Some(redirects) = redirects {
                     for redirect in &redirects.0 {
-                        setup_redirect(&mut params.open_files, pipeline_context.shell, redirect)
-                            .await?;
+                        setup_redirect(pipeline_context.shell, &mut params, redirect).await?;
                     }
                 }
 
@@ -508,7 +507,9 @@ impl ExecuteInPipeline for ast::Command {
             }
             ast::Command::ExtendedTest(e) => {
                 let result =
-                    if extendedtests::eval_extended_test_expr(e, pipeline_context.shell).await? {
+                    if extendedtests::eval_extended_test_expr(e, pipeline_context.shell, &params)
+                        .await?
+                    {
                         0
                     } else {
                         1
@@ -573,7 +574,8 @@ impl Execute for ast::ForClauseCommand {
         let mut expanded_values = vec![];
         if let Some(unexpanded_values) = &self.values {
             for value in unexpanded_values {
-                let mut expanded = expansion::full_expand_and_split_word(shell, value).await?;
+                let mut expanded =
+                    expansion::full_expand_and_split_word(shell, params, value).await?;
                 expanded_values.append(&mut expanded);
             }
         } else {
@@ -644,7 +646,7 @@ impl Execute for ast::CaseClauseCommand {
             shell.trace_command(std::format!("case {} in", &self.value))?;
         }
 
-        let expanded_value = expansion::basic_expand_word(shell, &self.value).await?;
+        let expanded_value = expansion::basic_expand_word(shell, params, &self.value).await?;
         let mut result: ExecutionResult = ExecutionResult::success();
         let mut force_execute_next_case = false;
 
@@ -654,7 +656,7 @@ impl Execute for ast::CaseClauseCommand {
             } else {
                 let mut matches = false;
                 for pattern in &case.patterns {
-                    let expanded_pattern = expansion::basic_expand_pattern(shell, pattern)
+                    let expanded_pattern = expansion::basic_expand_pattern(shell, params, pattern)
                         .await?
                         .set_extended_globbing(shell.options.extended_globbing)
                         .set_case_insensitive(shell.options.case_insensitive_conditionals);
@@ -790,9 +792,9 @@ impl Execute for ast::ArithmeticCommand {
     async fn execute(
         &self,
         shell: &mut Shell,
-        _params: &ExecutionParameters,
+        params: &ExecutionParameters,
     ) -> Result<ExecutionResult, error::Error> {
-        let value = self.expr.eval(shell, true).await?;
+        let value = self.expr.eval(shell, params, true).await?;
         let result = if value != 0 {
             ExecutionResult::success()
         } else {
@@ -814,12 +816,12 @@ impl Execute for ast::ArithmeticForClauseCommand {
     ) -> Result<ExecutionResult, error::Error> {
         let mut result = ExecutionResult::success();
         if let Some(initializer) = &self.initializer {
-            initializer.eval(shell, true).await?;
+            initializer.eval(shell, params, true).await?;
         }
 
         loop {
             if let Some(condition) = &self.condition {
-                if condition.eval(shell, true).await? == 0 {
+                if condition.eval(shell, params, true).await? == 0 {
                     break;
                 }
             }
@@ -847,7 +849,7 @@ impl Execute for ast::ArithmeticForClauseCommand {
             }
 
             if let Some(updater) = &self.updater {
-                updater.eval(shell, true).await?;
+                updater.eval(shell, params, true).await?;
             }
         }
 
@@ -908,7 +910,7 @@ impl ExecuteInPipeline for ast::SimpleCommand {
         {
             match item {
                 CommandPrefixOrSuffixItem::IoRedirect(redirect) => {
-                    if setup_redirect(&mut params.open_files, context.shell, redirect)
+                    if setup_redirect(context.shell, &mut params, redirect)
                         .await?
                         .is_none()
                     {
@@ -918,8 +920,8 @@ impl ExecuteInPipeline for ast::SimpleCommand {
                 }
                 CommandPrefixOrSuffixItem::ProcessSubstitution(kind, subshell_command) => {
                     let (installed_fd_num, substitution_file) = setup_process_substitution(
-                        &mut params.open_files,
                         context.shell,
+                        &mut params,
                         kind,
                         subshell_command,
                     )?;
@@ -943,14 +945,15 @@ impl ExecuteInPipeline for ast::SimpleCommand {
                             // This looks like an assignment, and the command being invoked is a
                             // well-known builtin that takes arguments that need to function like
                             // assignments (but which are processed by the builtin).
-                            let expanded = expand_assignment(context.shell, assignment).await?;
+                            let expanded =
+                                expand_assignment(context.shell, &params, assignment).await?;
                             args.push(CommandArg::Assignment(expanded));
                         } else {
                             // This *looks* like an assignment, but it's really a string we should
                             // fully treat as a regular looking
                             // argument.
                             let mut next_args =
-                                expansion::full_expand_and_split_word(context.shell, word)
+                                expansion::full_expand_and_split_word(context.shell, &params, word)
                                     .await?
                                     .into_iter()
                                     .map(CommandArg::String)
@@ -961,7 +964,7 @@ impl ExecuteInPipeline for ast::SimpleCommand {
                 }
                 CommandPrefixOrSuffixItem::Word(arg) => {
                     let mut next_args =
-                        expansion::full_expand_and_split_word(context.shell, arg).await?;
+                        expansion::full_expand_and_split_word(context.shell, &params, arg).await?;
 
                     if args.is_empty() {
                         if let Some(cmd_name) = next_args.first() {
@@ -1013,6 +1016,7 @@ impl ExecuteInPipeline for ast::SimpleCommand {
                 apply_assignment(
                     assignment,
                     context.shell,
+                    &params,
                     true,
                     Some(EnvironmentScope::Command),
                     EnvironmentScope::Command,
@@ -1095,6 +1099,7 @@ impl ExecuteInPipeline for ast::SimpleCommand {
                 apply_assignment(
                     assignment,
                     context.shell,
+                    &params,
                     false,
                     None,
                     EnvironmentScope::Global,
@@ -1113,11 +1118,12 @@ impl ExecuteInPipeline for ast::SimpleCommand {
 
 async fn expand_assignment(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     assignment: &ast::Assignment,
 ) -> Result<ast::Assignment, error::Error> {
-    let value = expand_assignment_value(shell, &assignment.value).await?;
+    let value = expand_assignment_value(shell, params, &assignment.value).await?;
     Ok(ast::Assignment {
-        name: basic_expand_assignment_name(shell, &assignment.name).await?,
+        name: basic_expand_assignment_name(shell, params, &assignment.name).await?,
         value,
         append: assignment.append,
     })
@@ -1125,16 +1131,17 @@ async fn expand_assignment(
 
 async fn basic_expand_assignment_name(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     name: &ast::AssignmentName,
 ) -> Result<ast::AssignmentName, error::Error> {
     match name {
         ast::AssignmentName::VariableName(name) => {
-            let expanded = expansion::basic_expand_str(shell, name).await?;
+            let expanded = expansion::basic_expand_str(shell, params, name).await?;
             Ok(ast::AssignmentName::VariableName(expanded))
         }
         ast::AssignmentName::ArrayElementName(name, index) => {
-            let expanded_name = expansion::basic_expand_str(shell, name).await?;
-            let expanded_index = expansion::basic_expand_str(shell, index).await?;
+            let expanded_name = expansion::basic_expand_str(shell, params, name).await?;
+            let expanded_index = expansion::basic_expand_str(shell, params, index).await?;
             Ok(ast::AssignmentName::ArrayElementName(
                 expanded_name,
                 expanded_index,
@@ -1145,11 +1152,12 @@ async fn basic_expand_assignment_name(
 
 async fn expand_assignment_value(
     shell: &mut Shell,
+    params: &ExecutionParameters,
     value: &ast::AssignmentValue,
 ) -> Result<ast::AssignmentValue, error::Error> {
     let expanded = match value {
         ast::AssignmentValue::Scalar(s) => {
-            let expanded_word = expansion::basic_expand_word(shell, s).await?;
+            let expanded_word = expansion::basic_expand_word(shell, params, s).await?;
             ast::AssignmentValue::Scalar(ast::Word {
                 value: expanded_word,
             })
@@ -1158,12 +1166,14 @@ async fn expand_assignment_value(
             let mut expanded_values = vec![];
             for (key, value) in arr {
                 if let Some(k) = key {
-                    let expanded_key = expansion::basic_expand_word(shell, k).await?.into();
-                    let expanded_value = expansion::basic_expand_word(shell, value).await?.into();
+                    let expanded_key = expansion::basic_expand_word(shell, params, k).await?.into();
+                    let expanded_value = expansion::basic_expand_word(shell, params, value)
+                        .await?
+                        .into();
                     expanded_values.push((Some(expanded_key), expanded_value));
                 } else {
                     let split_expanded_value =
-                        expansion::full_expand_and_split_word(shell, value).await?;
+                        expansion::full_expand_and_split_word(shell, params, value).await?;
                     for expanded_value in split_expanded_value {
                         expanded_values.push((None, expanded_value.into()));
                     }
@@ -1181,6 +1191,7 @@ async fn expand_assignment_value(
 async fn apply_assignment(
     assignment: &ast::Assignment,
     shell: &mut Shell,
+    params: &ExecutionParameters,
     mut export: bool,
     required_scope: Option<EnvironmentScope>,
     creation_scope: EnvironmentScope,
@@ -1194,7 +1205,7 @@ async fn apply_assignment(
             name
         }
         ast::AssignmentName::ArrayElementName(name, index) => {
-            let expanded = expansion::basic_expand_str(shell, index).await?;
+            let expanded = expansion::basic_expand_str(shell, params, index).await?;
             array_index = Some(expanded);
             name
         }
@@ -1203,7 +1214,7 @@ async fn apply_assignment(
     // Expand the values.
     let new_value = match &assignment.value {
         ast::AssignmentValue::Scalar(unexpanded_value) => {
-            let value = expansion::basic_expand_word(shell, unexpanded_value).await?;
+            let value = expansion::basic_expand_word(shell, params, unexpanded_value).await?;
             ShellValueLiteral::Scalar(value)
         }
         ast::AssignmentValue::Array(unexpanded_values) => {
@@ -1211,17 +1222,19 @@ async fn apply_assignment(
             for (unexpanded_key, unexpanded_value) in unexpanded_values {
                 let key = match unexpanded_key {
                     Some(unexpanded_key) => {
-                        Some(expansion::basic_expand_word(shell, unexpanded_key).await?)
+                        Some(expansion::basic_expand_word(shell, params, unexpanded_key).await?)
                     }
                     None => None,
                 };
 
                 if key.is_some() {
-                    let value = expansion::basic_expand_word(shell, unexpanded_value).await?;
+                    let value =
+                        expansion::basic_expand_word(shell, params, unexpanded_value).await?;
                     elements.push((key, value));
                 } else {
                     let values =
-                        expansion::full_expand_and_split_word(shell, unexpanded_value).await?;
+                        expansion::full_expand_and_split_word(shell, params, unexpanded_value)
+                            .await?;
                     for value in values {
                         elements.push((None, value));
                     }
@@ -1250,7 +1263,7 @@ async fn apply_assignment(
 
         if will_be_indexed_array {
             array_index = Some(
-                arithmetic::expand_and_eval(shell, idx.as_str(), false)
+                arithmetic::expand_and_eval(shell, params, idx.as_str(), false)
                     .await?
                     .to_string(),
             );
@@ -1349,13 +1362,14 @@ fn setup_pipeline_redirection(
 
 #[allow(clippy::too_many_lines)]
 pub(crate) async fn setup_redirect(
-    open_files: &'_ mut OpenFiles,
     shell: &mut Shell,
+    params: &'_ mut ExecutionParameters,
     redirect: &ast::IoRedirect,
 ) -> Result<Option<u32>, error::Error> {
     match redirect {
         ast::IoRedirect::OutputAndError(f, append) => {
-            let mut expanded_fields = expansion::full_expand_and_split_word(shell, f).await?;
+            let mut expanded_fields =
+                expansion::full_expand_and_split_word(shell, params, f).await?;
             if expanded_fields.len() != 1 {
                 return Err(error::Error::InvalidRedirection);
             }
@@ -1379,8 +1393,8 @@ pub(crate) async fn setup_redirect(
             let stdout_file = OpenFile::File(opened_file);
             let stderr_file = stdout_file.try_dup()?;
 
-            open_files.files.insert(1, stdout_file);
-            open_files.files.insert(2, stderr_file);
+            params.open_files.files.insert(1, stdout_file);
+            params.open_files.files.insert(2, stderr_file);
 
             Ok(Some(1))
         }
@@ -1392,7 +1406,7 @@ pub(crate) async fn setup_redirect(
                     let mut options = std::fs::File::options();
 
                     let mut expanded_fields =
-                        expansion::full_expand_and_split_word(shell, f).await?;
+                        expansion::full_expand_and_split_word(shell, params, f).await?;
 
                     if expanded_fields.len() != 1 {
                         return Err(error::Error::InvalidRedirection);
@@ -1470,7 +1484,7 @@ pub(crate) async fn setup_redirect(
 
                     fd_num = specified_fd_num.unwrap_or(default_fd_if_unspecified);
 
-                    if let Some(f) = open_files.files.get(fd) {
+                    if let Some(f) = params.open_files.files.get(fd) {
                         target_file = f.try_dup()?;
                     } else {
                         tracing::error!("{}: Bad file descriptor", fd);
@@ -1485,14 +1499,17 @@ pub(crate) async fn setup_redirect(
                         | ast::IoFileRedirectKind::ReadAndWrite
                         | ast::IoFileRedirectKind::Clobber => {
                             let (substitution_fd, substitution_file) = setup_process_substitution(
-                                open_files,
                                 shell,
+                                params,
                                 substitution_kind,
                                 subshell_cmd,
                             )?;
 
                             target_file = substitution_file.try_dup()?;
-                            open_files.files.insert(substitution_fd, substitution_file);
+                            params
+                                .open_files
+                                .files
+                                .insert(substitution_fd, substitution_file);
 
                             fd_num = specified_fd_num
                                 .unwrap_or_else(|| get_default_fd_for_redirect_kind(kind));
@@ -1502,7 +1519,7 @@ pub(crate) async fn setup_redirect(
                 }
             }
 
-            open_files.files.insert(fd_num, target_file);
+            params.open_files.files.insert(fd_num, target_file);
             Ok(Some(fd_num))
         }
         ast::IoRedirect::HereDocument(fd_num, io_here) => {
@@ -1511,26 +1528,26 @@ pub(crate) async fn setup_redirect(
 
             // Expand if required.
             let io_here_doc = if io_here.requires_expansion {
-                expansion::basic_expand_word(shell, &io_here.doc).await?
+                expansion::basic_expand_word(shell, params, &io_here.doc).await?
             } else {
                 io_here.doc.flatten()
             };
 
             let f = setup_open_file_with_contents(io_here_doc.as_str())?;
 
-            open_files.files.insert(fd_num, f);
+            params.open_files.files.insert(fd_num, f);
             Ok(Some(fd_num))
         }
         ast::IoRedirect::HereString(fd_num, word) => {
             // If not specified, default to stdin (fd 0).
             let fd_num = fd_num.unwrap_or(0);
 
-            let mut expanded_word = expansion::basic_expand_word(shell, word).await?;
+            let mut expanded_word = expansion::basic_expand_word(shell, params, word).await?;
             expanded_word.push('\n');
 
             let f = setup_open_file_with_contents(expanded_word.as_str())?;
 
-            open_files.files.insert(fd_num, f);
+            params.open_files.files.insert(fd_num, f);
             Ok(Some(fd_num))
         }
     }
@@ -1549,8 +1566,8 @@ fn get_default_fd_for_redirect_kind(kind: &ast::IoFileRedirectKind) -> u32 {
 }
 
 fn setup_process_substitution(
-    open_files: &mut OpenFiles,
     shell: &mut Shell,
+    params: &mut ExecutionParameters,
     kind: &ast::ProcessSubstitutionKind,
     subshell_cmd: &ast::SubshellCommand,
 ) -> Result<(u32, OpenFile), error::Error> {
@@ -1558,19 +1575,23 @@ fn setup_process_substitution(
     // Execute in a subshell.
     let mut subshell = shell.clone();
 
+    // Set up execution parameters for the child execution.
+    let mut child_params = params.clone();
+    child_params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
+
     // Set up pipe so we can connect to the command.
     let (reader, writer) = sys::pipes::pipe()?;
 
     let target_file = match kind {
         ast::ProcessSubstitutionKind::Read => {
-            subshell
+            child_params
                 .open_files
                 .files
                 .insert(1, openfiles::OpenFile::PipeWriter(writer));
             OpenFile::PipeReader(reader)
         }
         ast::ProcessSubstitutionKind::Write => {
-            subshell
+            child_params
                 .open_files
                 .files
                 .insert(0, openfiles::OpenFile::PipeReader(reader));
@@ -1578,23 +1599,18 @@ fn setup_process_substitution(
         }
     };
 
-    let exec_params = ExecutionParameters {
-        open_files: subshell.open_files.clone(),
-        process_group_policy: ProcessGroupPolicy::SameProcessGroup,
-    };
-
     // Asynchronously spawn off the subshell; we intentionally don't block on its
     // completion.
     let subshell_cmd = subshell_cmd.to_owned();
     tokio::spawn(async move {
         // Intentionally ignore the result of the subshell command.
-        let _ = subshell_cmd.0.execute(&mut subshell, &exec_params).await;
+        let _ = subshell_cmd.0.execute(&mut subshell, &child_params).await;
     });
 
     // Starting at 63 (a.k.a. 64-1)--and decrementing--look for an
     // available fd.
     let mut candidate_fd_num = 63;
-    while open_files.files.contains_key(&candidate_fd_num) {
+    while params.open_files.files.contains_key(&candidate_fd_num) {
         candidate_fd_num -= 1;
         if candidate_fd_num == 0 {
             return error::unimp("no available file descriptors");
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index 655c00a..8cdac2c 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -33,7 +33,7 @@ pub struct Shell {
     /// Trap handler configuration for the shell.
     pub traps: traps::TrapHandlerConfig,
     /// Manages files opened and accessible via redirection operators.
-    pub open_files: openfiles::OpenFiles,
+    open_files: openfiles::OpenFiles,
     /// The current working directory.
     pub working_dir: PathBuf,
     /// The shell environment, containing shell variables.
@@ -942,9 +942,10 @@ impl Shell {
     /// * `s` - The string to expand.
     pub async fn basic_expand_string<S: AsRef<str>>(
         &mut self,
+        params: &ExecutionParameters,
         s: S,
     ) -> Result<String, error::Error> {
-        let result = expansion::basic_expand_str(self, s.as_ref()).await?;
+        let result = expansion::basic_expand_str(self, params, s.as_ref()).await?;
         Ok(result)
     }
 
@@ -956,9 +957,10 @@ impl Shell {
     /// * `s` - The string to expand and split.
     pub async fn full_expand_and_split_string<S: AsRef<str>>(
         &mut self,
+        params: &ExecutionParameters,
         s: S,
     ) -> Result<Vec<String>, error::Error> {
-        let result = expansion::full_expand_and_split_str(self, s.as_ref()).await?;
+        let result = expansion::full_expand_and_split_str(self, params, s.as_ref()).await?;
         Ok(result)
     }
 
@@ -981,13 +983,9 @@ impl Shell {
         script_path: &Path,
         args: I,
     ) -> Result<ExecutionResult, error::Error> {
-        self.parse_and_execute_script_file(
-            script_path,
-            args,
-            &self.default_exec_params(),
-            ScriptCallType::Executed,
-        )
-        .await
+        let params = self.default_exec_params();
+        self.parse_and_execute_script_file(script_path, args, &params, ScriptCallType::Executed)
+            .await
     }
 
     async fn run_parsed_result(
@@ -1116,7 +1114,8 @@ impl Shell {
         let formatted_prompt = prompt::expand_prompt(self, prompt_spec.into_owned())?;
 
         // Now expand.
-        expansion::basic_expand_str(self, &formatted_prompt).await
+        let params = self.default_exec_params();
+        expansion::basic_expand_str(self, &params, &formatted_prompt).await
     }
 
     /// Returns the exit status of the last command executed in this shell.


Test patch:
diff --git a/brush-core/src/builtins/test.rs b/brush-core/src/builtins/test.rs
index 532552c..cb0d4e1 100644
--- a/brush-core/src/builtins/test.rs
+++ b/brush-core/src/builtins/test.rs
@@ -1,7 +1,7 @@
 use clap::Parser;
 use std::io::Write;
 
-use crate::{builtins, commands, error, tests, Shell};
+use crate::{builtins, commands, error, tests, ExecutionParameters, Shell};
 
 /// Evaluate test expression.
 #[derive(Parser)]
@@ -30,7 +30,7 @@ impl builtins::Command for TestCommand {
             args = &args[0..args.len() - 1];
         }
 
-        if execute_test(context.shell, args)? {
+        if execute_test(context.shell, &context.params, args)? {
             Ok(builtins::ExitCode::Success)
         } else {
             Ok(builtins::ExitCode::Custom(1))
@@ -38,8 +38,12 @@ impl builtins::Command for TestCommand {
     }
 }
 
-fn execute_test(shell: &mut Shell, args: &[String]) -> Result<bool, error::Error> {
+fn execute_test(
+    shell: &mut Shell,
+    params: &ExecutionParameters,
+    args: &[String],
+) -> Result<bool, error::Error> {
     let test_command =
         brush_parser::test_command::parse(args).map_err(error::Error::TestCommandParseError)?;
-    tests::eval_test_expr(&test_command, shell)
+    tests::eval_test_expr(&test_command, shell, params)
 }
diff --git a/brush-core/src/extendedtests.rs b/brush-core/src/extendedtests.rs
index 1c18071..cc54e2b 100644
--- a/brush-core/src/extendedtests.rs
+++ b/brush-core/src/extendedtests.rs
@@ -8,37 +8,40 @@ use crate::{
         users,
     },
     variables::{self, ArrayLiteral},
-    Shell,
+    ExecutionParameters, Shell,
 };
 
 #[async_recursion::async_recursion]
 pub(crate) async fn eval_extended_test_expr(
     expr: &ast::ExtendedTestExpr,
     shell: &mut Shell,
+    params: &ExecutionParameters,
 ) -> Result<bool, error::Error> {
     #[allow(clippy::single_match_else)]
     match expr {
         ast::ExtendedTestExpr::UnaryTest(op, operand) => {
-            apply_unary_predicate(op, operand, shell).await
+            apply_unary_predicate(op, operand, shell, params).await
         }
         ast::ExtendedTestExpr::BinaryTest(op, left, right) => {
-            apply_binary_predicate(op, left, right, shell).await
+            apply_binary_predicate(op, left, right, shell, params).await
         }
         ast::ExtendedTestExpr::And(left, right) => {
-            let result = eval_extended_test_expr(left, shell).await?
-                && eval_extended_test_expr(right, shell).await?;
+            let result = eval_extended_test_expr(left, shell, params).await?
+                && eval_extended_test_expr(right, shell, params).await?;
             Ok(result)
         }
         ast::ExtendedTestExpr::Or(left, right) => {
-            let result = eval_extended_test_expr(left, shell).await?
-                || eval_extended_test_expr(right, shell).await?;
+            let result = eval_extended_test_expr(left, shell, params).await?
+                || eval_extended_test_expr(right, shell, params).await?;
             Ok(result)
         }
         ast::ExtendedTestExpr::Not(expr) => {
-            let result = !eval_extended_test_expr(expr, shell).await?;
+            let result = !eval_extended_test_expr(expr, shell, params).await?;
             Ok(result)
         }
-        ast::ExtendedTestExpr::Parenthesized(expr) => eval_extended_test_expr(expr, shell).await,
+        ast::ExtendedTestExpr::Parenthesized(expr) => {
+            eval_extended_test_expr(expr, shell, params).await
+        }
     }
 }
 
@@ -46,8 +49,9 @@ async fn apply_unary_predicate(
     op: &ast::UnaryPredicate,
     operand: &ast::Word,
     shell: &mut Shell,
+    params: &ExecutionParameters,
 ) -> Result<bool, error::Error> {
-    let expanded_operand = expansion::basic_expand_word(shell, operand).await?;
+    let expanded_operand = expansion::basic_expand_word(shell, params, operand).await?;
 
     if shell.options.print_commands_and_arguments {
         shell.trace_command(std::format!(
@@ -56,7 +60,7 @@ async fn apply_unary_predicate(
         ))?;
     }
 
-    apply_unary_predicate_to_str(op, expanded_operand.as_str(), shell)
+    apply_unary_predicate_to_str(op, expanded_operand.as_str(), shell, params)
 }
 
 #[allow(clippy::too_many_lines)]
@@ -64,6 +68,7 @@ pub(crate) fn apply_unary_predicate_to_str(
     op: &ast::UnaryPredicate,
     operand: &str,
     shell: &mut Shell,
+    params: &ExecutionParameters,
 ) -> Result<bool, error::Error> {
     #[allow(clippy::match_single_binding)]
     match op {
@@ -119,7 +124,7 @@ pub(crate) fn apply_unary_predicate_to_str(
         }
         ast::UnaryPredicate::FdIsOpenTerminal => {
             if let Ok(fd) = operand.parse::<u32>() {
-                if let Some(open_file) = shell.open_files.files.get(&fd) {
+                if let Some(open_file) = params.open_files.files.get(&fd) {
                     Ok(open_file.is_term())
                 } else {
                     Ok(false)
@@ -186,12 +191,13 @@ async fn apply_binary_predicate(
     left: &ast::Word,
     right: &ast::Word,
     shell: &mut Shell,
+    params: &ExecutionParameters,
 ) -> Result<bool, error::Error> {
     #[allow(clippy::single_match_else)]
     match op {
         ast::BinaryPredicate::StringMatchesRegex => {
-            let s = expansion::basic_expand_word(shell, left).await?;
-            let regex = expansion::basic_expand_regex(shell, right).await?;
+            let s = expansion::basic_expand_word(shell, params, left).await?;
+            let regex = expansion::basic_expand_regex(shell, params, right).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {s} {op} {right} ]]"))?;
@@ -227,8 +233,8 @@ async fn apply_binary_predicate(
             Ok(matches)
         }
         ast::BinaryPredicate::StringExactlyMatchesString => {
-            let left = expansion::basic_expand_word(shell, left).await?;
-            let right = expansion::basic_expand_word(shell, right).await?;
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -237,8 +243,8 @@ async fn apply_binary_predicate(
             Ok(left == right)
         }
         ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
-            let left = expansion::basic_expand_word(shell, left).await?;
-            let right = expansion::basic_expand_word(shell, right).await?;
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -247,8 +253,8 @@ async fn apply_binary_predicate(
             Ok(left != right)
         }
         ast::BinaryPredicate::StringContainsSubstring => {
-            let s = expansion::basic_expand_word(shell, left).await?;
-            let substring = expansion::basic_expand_word(shell, right).await?;
+            let s = expansion::basic_expand_word(shell, params, left).await?;
+            let substring = expansion::basic_expand_word(shell, params, right).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {s} {op} {substring} ]]"))?;
@@ -266,8 +272,8 @@ async fn apply_binary_predicate(
             "extended test binary predicate LeftFileIsOlderOrDoesNotExistWhenRightDoes",
         ),
         ast::BinaryPredicate::LeftSortsBeforeRight => {
-            let left = expansion::basic_expand_word(shell, left).await?;
-            let right = expansion::basic_expand_word(shell, right).await?;
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -277,8 +283,8 @@ async fn apply_binary_predicate(
             Ok(left < right)
         }
         ast::BinaryPredicate::LeftSortsAfterRight => {
-            let left = expansion::basic_expand_word(shell, left).await?;
-            let right = expansion::basic_expand_word(shell, right).await?;
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -288,8 +294,10 @@ async fn apply_binary_predicate(
             Ok(left > right)
         }
         ast::BinaryPredicate::ArithmeticEqualTo => {
-            let left = arithmetic::expand_and_eval(shell, left.value.as_str(), false).await?;
-            let right = arithmetic::expand_and_eval(shell, right.value.as_str(), false).await?;
+            let left =
+                arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
+            let right =
+                arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -298,8 +306,10 @@ async fn apply_binary_predicate(
             Ok(left == right)
         }
         ast::BinaryPredicate::ArithmeticNotEqualTo => {
-            let left = arithmetic::expand_and_eval(shell, left.value.as_str(), false).await?;
-            let right = arithmetic::expand_and_eval(shell, right.value.as_str(), false).await?;
+            let left =
+                arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
+            let right =
+                arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -308,8 +318,10 @@ async fn apply_binary_predicate(
             Ok(left != right)
         }
         ast::BinaryPredicate::ArithmeticLessThan => {
-            let left = arithmetic::expand_and_eval(shell, left.value.as_str(), false).await?;
-            let right = arithmetic::expand_and_eval(shell, right.value.as_str(), false).await?;
+            let left =
+                arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
+            let right =
+                arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -318,8 +330,10 @@ async fn apply_binary_predicate(
             Ok(left < right)
         }
         ast::BinaryPredicate::ArithmeticLessThanOrEqualTo => {
-            let left = arithmetic::expand_and_eval(shell, left.value.as_str(), false).await?;
-            let right = arithmetic::expand_and_eval(shell, right.value.as_str(), false).await?;
+            let left =
+                arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
+            let right =
+                arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -328,8 +342,10 @@ async fn apply_binary_predicate(
             Ok(left <= right)
         }
         ast::BinaryPredicate::ArithmeticGreaterThan => {
-            let left = arithmetic::expand_and_eval(shell, left.value.as_str(), false).await?;
-            let right = arithmetic::expand_and_eval(shell, right.value.as_str(), false).await?;
+            let left =
+                arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
+            let right =
+                arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -338,8 +354,10 @@ async fn apply_binary_predicate(
             Ok(left > right)
         }
         ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo => {
-            let left = arithmetic::expand_and_eval(shell, left.value.as_str(), false).await?;
-            let right = arithmetic::expand_and_eval(shell, right.value.as_str(), false).await?;
+            let left =
+                arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
+            let right =
+                arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
 
             if shell.options.print_commands_and_arguments {
                 shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
@@ -352,14 +370,14 @@ async fn apply_binary_predicate(
         // operand (treated as a shell pattern).
         // TODO: implement case-insensitive matching if relevant via shopt options (nocasematch).
         ast::BinaryPredicate::StringExactlyMatchesPattern => {
-            let s = expansion::basic_expand_word(shell, left).await?;
-            let pattern = expansion::basic_expand_pattern(shell, right)
+            let s = expansion::basic_expand_word(shell, params, left).await?;
+            let pattern = expansion::basic_expand_pattern(shell, params, right)
                 .await?
                 .set_extended_globbing(shell.options.extended_globbing)
                 .set_case_insensitive(shell.options.case_insensitive_conditionals);
 
             if shell.options.print_commands_and_arguments {
-                let expanded_right = expansion::basic_expand_word(shell, right).await?;
+                let expanded_right = expansion::basic_expand_word(shell, params, right).await?;
                 let escaped_right = escape::quote_if_needed(
                     expanded_right.as_str(),
                     escape::QuoteMode::BackslashEscape,
@@ -370,14 +388,14 @@ async fn apply_binary_predicate(
             pattern.exactly_matches(s.as_str())
         }
         ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
-            let s = expansion::basic_expand_word(shell, left).await?;
-            let pattern = expansion::basic_expand_pattern(shell, right)
+            let s = expansion::basic_expand_word(shell, params, left).await?;
+            let pattern = expansion::basic_expand_pattern(shell, params, right)
                 .await?
                 .set_extended_globbing(shell.options.extended_globbing)
                 .set_case_insensitive(shell.options.case_insensitive_conditionals);
 
             if shell.options.print_commands_and_arguments {
-                let expanded_right = expansion::basic_expand_word(shell, right).await?;
+                let expanded_right = expansion::basic_expand_word(shell, params, right).await?;
                 let escaped_right = escape::quote_if_needed(
                     expanded_right.as_str(),
                     escape::QuoteMode::BackslashEscape,
diff --git a/brush-core/src/tests.rs b/brush-core/src/tests.rs
index ec2f9ad..adf5ada 100644
--- a/brush-core/src/tests.rs
+++ b/brush-core/src/tests.rs
@@ -1,22 +1,23 @@
-use crate::{error, extendedtests, Shell};
+use crate::{error, extendedtests, ExecutionParameters, Shell};
 
 pub(crate) fn eval_test_expr(
     expr: &brush_parser::ast::TestExpr,
     shell: &mut Shell,
+    params: &ExecutionParameters,
 ) -> Result<bool, error::Error> {
     match expr {
         brush_parser::ast::TestExpr::False => Ok(false),
         brush_parser::ast::TestExpr::Literal(s) => Ok(!s.is_empty()),
         brush_parser::ast::TestExpr::And(left, right) => {
-            Ok(eval_test_expr(left, shell)? && eval_test_expr(right, shell)?)
+            Ok(eval_test_expr(left, shell, params)? && eval_test_expr(right, shell, params)?)
         }
         brush_parser::ast::TestExpr::Or(left, right) => {
-            Ok(eval_test_expr(left, shell)? || eval_test_expr(right, shell)?)
+            Ok(eval_test_expr(left, shell, params)? || eval_test_expr(right, shell, params)?)
         }
-        brush_parser::ast::TestExpr::Not(expr) => Ok(!eval_test_expr(expr, shell)?),
-        brush_parser::ast::TestExpr::Parenthesized(expr) => eval_test_expr(expr, shell),
+        brush_parser::ast::TestExpr::Not(expr) => Ok(!eval_test_expr(expr, shell, params)?),
+        brush_parser::ast::TestExpr::Parenthesized(expr) => eval_test_expr(expr, shell, params),
         brush_parser::ast::TestExpr::UnaryTest(op, operand) => {
-            extendedtests::apply_unary_predicate_to_str(op, operand, shell)
+            extendedtests::apply_unary_predicate_to_str(op, operand, shell, params)
         }
         brush_parser::ast::TestExpr::BinaryTest(op, left, right) => {
             extendedtests::apply_binary_predicate_to_strs(op, left.as_str(), right.as_str(), shell)
diff --git a/brush-shell/tests/cases/redirection.yaml b/brush-shell/tests/cases/redirection.yaml
index 5fa2d97..89707c1 100644
--- a/brush-shell/tests/cases/redirection.yaml
+++ b/brush-shell/tests/cases/redirection.yaml
@@ -101,13 +101,11 @@ cases:
       echo $(echo hi >&3) 3>output.txt
 
   - name: "Redirection in command substitution in braces to non-standard fd"
-    known_failure: true # https://github.com/reubeno/brush/issues/358
     ignore_stderr: true
     stdin: |
       { : $(echo hi >&3); } 3>output.txt
 
   - name: "Redirection in command substitution in subshell to non-standard fd"
-    known_failure: true # https://github.com/reubeno/brush/issues/358
     ignore_stderr: true
     stdin: |
       ( : $(echo hi >&3); ) 3>output.txt