Repository: reubeno/brush
Pull request #416 (2025-03-20 05:27:47)

fix(regex): enable multiline mode for extended test regexes
Fixes extended test regex matching to always enable multiline mode. This ensures that `.` matches `$'\n'`.

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   18.14 μs` | `   18.28 μs` | `    0.14 μs` | `🟠 +0.78%    ` |
| `eval_arithmetic                     ` | `    0.17 μs` | `    0.17 μs` | `   -0.00 μs` | `⚪  Unchanged` |
| `expand_one_string                   ` | `    1.90 μs` | `    1.81 μs` | `   -0.08 μs` | `⚪  Unchanged` |
| `for_loop                            ` | `   23.01 μs` | `   22.45 μs` | `   -0.56 μs` | `🟢 -2.44%    ` |
| `function_call                       ` | `    2.42 μs` | `    2.30 μs` | `   -0.12 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   56.86 μs` | `   56.86 μs` | `    0.00 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `24302.25 μs` | `24285.31 μs` | `  -16.94 μs` | `⚪  Unchanged` |
| `parse_bash_completion               ` | ` 1655.91 μs` | ` 1671.23 μs` | `   15.32 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    1.72 μs` | `    1.77 μs` | `    0.05 μs` | `🟠 +3.15%    ` |
| `run_echo_builtin_command            ` | `   15.43 μs` | `   14.85 μs` | `   -0.57 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 2395.96 μs` | ` 2341.80 μs` | `  -54.16 μs` | `🟢 -2.26%    ` |
| `tokenize_sample_script              ` | `    2.73 μs` | `    2.79 μs` | `    0.06 μs` | `🟠 +2.09%    ` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/extendedtests.rs | 🟠 61.13% | 🟠 61.45% | 🟢 0.32% |
| brush-core/src/patterns.rs | 🟢 98.49% | 🟢 98.48% | 🔴 -0.01% |
| **Overall Coverage** | **🟢 76.05%** | **🟢 76.07%** | **🟢 0.02%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `76.07%`

# Test Summary: bash-completion test suite
| Outcome            | Count                   | Percentage |
| ------------------ | ----------------------: | ---------: |
| ✅ Pass            | 1354            | <span style='color:green'>64.20</span> |
| ❗️ Error           | 222           | <span style='color:red'>10.53</span> |
| ❌ Fail            | 156            | <span style='color:red'>7.40</span> |
| ⏩ Skip            | 361            | 17.12 |
| ❎ Expected Fail   | 15   | 0.71 |
| ✔️ Unexpected Pass | 1 | <span style='color:red'>0.05</span> |
| 📊 Total           | 2109           | 100.00 |

<!-- Sticky Pull Request Comment -->
github-actions[bot]: ## Test Results
    2 files      9 suites   1m 35s ⏱️
  696 tests   696 ✅ 0 💤 0 ❌
1 378 runs  1 378 ✅ 0 💤 0 ❌

Results for commit 25fa9917.

[test-results]:data:application/gzip;base64,H4sIAMWq22cC/13MSw7CIBSF4a00jB0AvdDiZgxeSkJsi+ExMu5dIK22Dr//JOdFrJunSK4dv3QkZpcaVIHJQSfn10pRXJZUN6nkrlvMiP/p4Z4l0W+w2s2nMIXgw1ZCXusn64dx0/55Sr/P5sNl8/ER/bK4VEC4sFopNmi4g2WstyMiNShBIAMwhgmgoDgl7w/mFeLhBgEAAA==


Patch:
diff --git a/brush-core/src/patterns.rs b/brush-core/src/patterns.rs
index ca88a02..220acb7 100644
--- a/brush-core/src/patterns.rs
+++ b/brush-core/src/patterns.rs
@@ -316,12 +316,6 @@ impl Pattern {
             regex_str.push('^');
         }
 
-        if self.multiline {
-            // Set option for multiline matching + set option for allowing '.' pattern to match
-            // newline.
-            regex_str.push_str("(?ms)");
-        }
-
         let mut current_pattern = String::new();
         for piece in &self.pieces {
             match piece {
@@ -365,7 +359,7 @@ impl Pattern {
 
         tracing::debug!(target: trace_categories::PATTERN, "pattern: '{self:?}' => regex: '{regex_str}'");
 
-        let re = regex::compile_regex(regex_str, self.case_insensitive)?;
+        let re = regex::compile_regex(regex_str, self.case_insensitive, self.multiline)?;
         Ok(re)
     }
 
diff --git a/brush-core/src/regex.rs b/brush-core/src/regex.rs
index 8c44ebc..508aaf1 100644
--- a/brush-core/src/regex.rs
+++ b/brush-core/src/regex.rs
@@ -29,6 +29,7 @@ type RegexWord = Vec<RegexPiece>;
 pub struct Regex {
     pieces: RegexWord,
     case_insensitive: bool,
+    multiline: bool,
 }
 
 impl From<RegexWord> for Regex {
@@ -36,6 +37,7 @@ impl From<RegexWord> for Regex {
         Self {
             pieces,
             case_insensitive: false,
+            multiline: false,
         }
     }
 }
@@ -51,6 +53,18 @@ impl Regex {
         self
     }
 
+    /// Enables (or disables) multiline support for this pattern.
+    /// This enables matching across lines as well as enables `.`
+    /// to match newline characters.
+    ///
+    /// # Arguments
+    ///
+    /// * `value` - The new multiline value.
+    pub fn set_multiline(mut self, value: bool) -> Self {
+        self.multiline = value;
+        self
+    }
+
     /// Computes if the regular expression matches the given string.
     ///
     /// # Arguments
@@ -64,7 +78,7 @@ impl Regex {
             .collect();
 
         // TODO: Evaluate how compatible the `fancy_regex` crate is with POSIX EREs.
-        let re = compile_regex(regex_pattern, self.case_insensitive)?;
+        let re = compile_regex(regex_pattern, self.case_insensitive, self.multiline)?;
 
         Ok(re.captures(value)?.map(|captures| {
             captures
@@ -79,10 +93,20 @@ impl Regex {
 pub(crate) fn compile_regex(
     regex_str: String,
     case_insensitive: bool,
+    multiline: bool,
 ) -> Result<fancy_regex::Regex, error::Error> {
     // Handle identified cases where a shell-supported regex isn't supported directly by
     // `fancy_regex` -- specifically, adding missing escape characters.
-    let regex_str = add_missing_escape_chars_to_regex(regex_str.as_str());
+    let mut regex_str = add_missing_escape_chars_to_regex(regex_str.as_str());
+
+    // Handle multiline enablement.
+    if multiline {
+        // The fancy_regex crate internally seems to have flags that can be used
+        // to enable multiline support, but they're not exposed via its
+        // RegexBuilder. We instead just prefix with the right flags.
+        let updated_str = std::format!("(?ms){regex_str}");
+        regex_str = updated_str.into();
+    }
 
     let mut builder = fancy_regex::RegexBuilder::new(regex_str.as_ref());
     builder.case_insensitive(case_insensitive);


Test patch:
diff --git a/brush-core/src/extendedtests.rs b/brush-core/src/extendedtests.rs
index ba10429..3174c33 100644
--- a/brush-core/src/extendedtests.rs
+++ b/brush-core/src/extendedtests.rs
@@ -199,7 +199,9 @@ async fn apply_binary_predicate(
     match op {
         ast::BinaryPredicate::StringMatchesRegex => {
             let s = expansion::basic_expand_word(shell, params, left).await?;
-            let regex = expansion::basic_expand_regex(shell, params, right).await?;
+            let regex = expansion::basic_expand_regex(shell, params, right)
+                .await?
+                .set_multiline(true);
 
             if shell.options.print_commands_and_arguments {
                 shell
diff --git a/brush-shell/tests/cases/extended_tests.yaml b/brush-shell/tests/cases/extended_tests.yaml
index a233ce8..0016570 100644
--- a/brush-shell/tests/cases/extended_tests.yaml
+++ b/brush-shell/tests/cases/extended_tests.yaml
@@ -299,3 +299,7 @@ cases:
       [[ z =~ ^z{2,6}$ ]] && echo "1. Matches"
       [[ zzzz =~ ^z{2,6}$ ]] && echo "2. Matches"
       [[ zzzzzzzzz =~ ^z{2,6}$ ]] && echo "3. Matches"
+
+  - name: "Regex with newline"
+    stdin: |
+      [[ $'\n' =~ . ]] && echo "1. Matches"