Repository: reubeno/brush (rust)

Issue:
Gettext enabled strings do not split correctly
`test.sh`:
```bash
# Fails
echo $"Hello"
# Works
echo $'Hello'
```

```
$ ./target/debug/brush test.sh
$Hello
Hello
```

Note that I'm not asking for gettext support, just that it should print `Hello` instead of `$Hello`.

Pull request description:
feat(parser): add gettext enabled quotes
> [!Note]
> This doesn't translate gettext strings, but it will allow it to be used with it's base string (for now).

---

Closes #442.

Patch:
diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs
index 1c61967..5e7f5e0 100644
--- a/brush-core/src/expansion.rs
+++ b/brush-core/src/expansion.rs
@@ -642,7 +642,8 @@ impl<'a> WordExpander<'a> {
                     String::from_utf8_lossy(expanded.as_slice()).into_owned(),
                 ))
             }
-            brush_parser::word::WordPiece::DoubleQuotedSequence(pieces) => {
+            brush_parser::word::WordPiece::DoubleQuotedSequence(pieces)
+            | brush_parser::word::WordPiece::GettextDoubleQuotedSequence(pieces) => {
                 let mut fields: Vec<WordField> = vec![];
 
                 let pieces_is_empty = pieces.is_empty();
diff --git a/brush-interactive/src/reedline/highlighter.rs b/brush-interactive/src/reedline/highlighter.rs
index 7f38187..df2091e 100644
--- a/brush-interactive/src/reedline/highlighter.rs
+++ b/brush-interactive/src/reedline/highlighter.rs
@@ -181,7 +181,8 @@ impl<'a> StyledInputLine<'a> {
                     global_offset + word_piece.end_index,
                 );
             }
-            brush_parser::word::WordPiece::DoubleQuotedSequence(subpieces) => {
+            brush_parser::word::WordPiece::DoubleQuotedSequence(subpieces)
+            | brush_parser::word::WordPiece::GettextDoubleQuotedSequence(subpieces) => {
                 self.set_next_missing_style(styles::quoted());
                 for subpiece in subpieces {
                     self.style_and_append_word_piece(subpiece, styles::quoted(), global_offset);
diff --git a/brush-parser/src/word.rs b/brush-parser/src/word.rs
index bbad620..54dc7ce 100644
--- a/brush-parser/src/word.rs
+++ b/brush-parser/src/word.rs
@@ -37,6 +37,8 @@ pub enum WordPiece {
     AnsiCQuotedText(String),
     /// A sequence of pieces that are embedded in double quotes.
     DoubleQuotedSequence(Vec<WordPieceWithSource>),
+    /// Gettext enabled variant of [`WordPiece::DoubleQuotedSequence`].
+    GettextDoubleQuotedSequence(Vec<WordPieceWithSource>),
     /// A tilde prefix.
     TildePrefix(String),
     /// A parameter expansion.
@@ -620,10 +622,18 @@ peg::parser! {
             }
 
         rule word_piece<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
+            // Rules that match quoted text.
+            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
+            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
+            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
+            s:gettext_double_quoted_sequence() { WordPiece::GettextDoubleQuotedSequence(s) } /
+            // Rules that match pieces starting with a dollar sign ('$').
             arithmetic_expansion() /
             command_substitution() /
             parameter_expansion() /
-            unquoted_text(<stop_condition()>, in_command)
+            // Rules that match unquoted text that doesn't start with an unescaped dollar sign.
+            normal_escape_sequence() /
+            unquoted_literal_text(<stop_condition()>, in_command)
 
         rule double_quoted_word_piece() -> WordPiece =
             arithmetic_expansion() /
@@ -632,16 +642,12 @@ peg::parser! {
             double_quoted_escape_sequence() /
             double_quoted_text()
 
-        rule unquoted_text<T>(stop_condition: rule<T>, in_command: bool) -> WordPiece =
-            s:double_quoted_sequence() { WordPiece::DoubleQuotedSequence(s) } /
-            s:single_quoted_literal_text() { WordPiece::SingleQuotedText(s.to_owned()) } /
-            s:ansi_c_quoted_text() { WordPiece::AnsiCQuotedText(s.to_owned()) } /
-            normal_escape_sequence() /
-            unquoted_literal_text(<stop_condition()>, in_command)
-
         rule double_quoted_sequence() -> Vec<WordPieceWithSource> =
             "\"" i:double_quoted_sequence_inner()* "\"" { i }
 
+        rule gettext_double_quoted_sequence() -> Vec<WordPieceWithSource> =
+            "$\"" i:double_quoted_sequence_inner()* "\"" { i }
+
         rule double_quoted_sequence_inner() -> WordPieceWithSource =
             start_index:position!() piece:double_quoted_word_piece() end_index:position!() {
                 WordPieceWithSource {
@@ -921,6 +927,24 @@ mod tests {
         })
     }
 
+    #[test]
+    fn parse_ansi_c_quoted_text() -> Result<()> {
+        assert_ron_snapshot!(test_parse(r"$'hi\nthere\t'")?);
+        Ok(())
+    }
+
+    #[test]
+    fn parse_double_quoted_text() -> Result<()> {
+        assert_ron_snapshot!(test_parse(r#""a ${b} c""#)?);
+        Ok(())
+    }
+
+    #[test]
+    fn parse_gettext_double_quoted_text() -> Result<()> {
+        assert_ron_snapshot!(test_parse(r#"$"a ${b} c""#)?);
+        Ok(())
+    }
+
     #[test]
     fn parse_command_substitution() -> Result<()> {
         super::expansion_parser::command_piece("echo", &ParserOptions::default())?;


Test patch:
diff --git a/brush-parser/src/snapshots/brush_parser__word__tests__parse_ansi_c_quoted_text.snap b/brush-parser/src/snapshots/brush_parser__word__tests__parse_ansi_c_quoted_text.snap
new file mode 100644
index 0000000..0082b31
--- /dev/null
+++ b/brush-parser/src/snapshots/brush_parser__word__tests__parse_ansi_c_quoted_text.snap
@@ -0,0 +1,14 @@
+---
+source: brush-parser/src/word.rs
+expression: "test_parse(r\"$'hi\\nthere\\t'\")?"
+---
+ParseTestResults(
+  input: "$\'hi\\nthere\\t\'",
+  result: [
+    WordPieceWithSource(
+      piece: AnsiCQuotedText("hi\\nthere\\t"),
+      start_index: 0,
+      end_index: 14,
+    ),
+  ],
+)
diff --git a/brush-parser/src/snapshots/brush_parser__word__tests__parse_double_quoted_text.snap b/brush-parser/src/snapshots/brush_parser__word__tests__parse_double_quoted_text.snap
new file mode 100644
index 0000000..e9a846b
--- /dev/null
+++ b/brush-parser/src/snapshots/brush_parser__word__tests__parse_double_quoted_text.snap
@@ -0,0 +1,33 @@
+---
+source: brush-parser/src/word.rs
+expression: "test_parse(r#\"\"a ${b} c\"\"#)?"
+---
+ParseTestResults(
+  input: "\"a ${b} c\"",
+  result: [
+    WordPieceWithSource(
+      piece: DoubleQuotedSequence([
+        WordPieceWithSource(
+          piece: Text("a "),
+          start_index: 1,
+          end_index: 3,
+        ),
+        WordPieceWithSource(
+          piece: ParameterExpansion(Parameter(
+            parameter: Named("b"),
+            indirect: false,
+          )),
+          start_index: 3,
+          end_index: 7,
+        ),
+        WordPieceWithSource(
+          piece: Text(" c"),
+          start_index: 7,
+          end_index: 9,
+        ),
+      ]),
+      start_index: 0,
+      end_index: 10,
+    ),
+  ],
+)
diff --git a/brush-parser/src/snapshots/brush_parser__word__tests__parse_gettext_double_quoted_text.snap b/brush-parser/src/snapshots/brush_parser__word__tests__parse_gettext_double_quoted_text.snap
new file mode 100644
index 0000000..0d3f6ce
--- /dev/null
+++ b/brush-parser/src/snapshots/brush_parser__word__tests__parse_gettext_double_quoted_text.snap
@@ -0,0 +1,33 @@
+---
+source: brush-parser/src/word.rs
+expression: "test_parse(r#\"$\"a ${b} c\"\"#)?"
+---
+ParseTestResults(
+  input: "$\"a ${b} c\"",
+  result: [
+    WordPieceWithSource(
+      piece: GettextDoubleQuotedSequence([
+        WordPieceWithSource(
+          piece: Text("a "),
+          start_index: 2,
+          end_index: 4,
+        ),
+        WordPieceWithSource(
+          piece: ParameterExpansion(Parameter(
+            parameter: Named("b"),
+            indirect: false,
+          )),
+          start_index: 4,
+          end_index: 8,
+        ),
+        WordPieceWithSource(
+          piece: Text(" c"),
+          start_index: 8,
+          end_index: 10,
+        ),
+      ]),
+      start_index: 0,
+      end_index: 11,
+    ),
+  ],
+)
diff --git a/brush-shell/tests/cases/word_expansion.yaml b/brush-shell/tests/cases/word_expansion.yaml
index e385bca..6adaa9c 100644
--- a/brush-shell/tests/cases/word_expansion.yaml
+++ b/brush-shell/tests/cases/word_expansion.yaml
@@ -42,6 +42,11 @@ cases:
       echo "ANSI-C quoted len: ${#ansi_c_quoted}"
       echo -n $'\n' | hexdump -C
 
+  - name: "gettext style quotes"
+    stdin: |
+      quoted=$"Hello, world"
+      echo "Content: [${quoted}]"
+
   - name: "Command substitution"
     stdin: |
       var="value"