Repository: reubeno/brush (rust)

Issue:
prompt: Backtick operator in PS1 not handled
In #470, @osiris reported issues using their [minimal-bash-prompt](https://gitlab.com/osiux/minimal-bash-prompt/-/blob/master/minimal-bash-prompt?ref_type=heads) with `brush`. 

It appears that the backtick’d invocation of the `prompt` shell function is one of the problems.

We should add a compat test case to this to our test suite and correct the gap in behavior. 

Pull request:
fix(expansion): fix issues parsing backquoted commands nested in single/double-quoted strings
The issue tracked in #488 was broader than just prompts--and found multiple bugs:

* We were missing some test coverage for a broader form of prompt-expanding.
* We only correctly parsed backquoted commands in containing double-quoted strings if the former started strictly at the start of the latter.
* Ditto for single-quoted strings.

The fixes entail correcting the "stop" conditions for the grammar rules tracking the bodies of the quoted strings.

There's still an unresolved issue of `@P`-expansion needing to word-expanding the prompt-expanded result. That will need to be resolved in a separate PR.

Resolves #488 

Patch:
diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs
index 5e36db8..f74e93a 100644
--- a/brush-core/src/expansion.rs
+++ b/brush-core/src/expansion.rs
@@ -391,8 +391,7 @@ impl<'a> WordExpander<'a> {
 
     /// Apply tilde-expansion, parameter expansion, command substitution, and arithmetic expansion.
     pub async fn basic_expand_to_str(&mut self, word: &str) -> Result<String, error::Error> {
-        let expanded = String::from(self.basic_expand(word).await?);
-        Ok(expanded)
+        Ok(String::from(self.basic_expand(word).await?))
     }
 
     #[allow(clippy::ref_option)]
diff --git a/brush-parser/src/word.rs b/brush-parser/src/word.rs
index c392eac..1cc3d92 100644
--- a/brush-parser/src/word.rs
+++ b/brush-parser/src/word.rs
@@ -656,7 +656,7 @@ peg::parser! {
         rule unquoted_literal_text_piece<T>(stop_condition: rule<T>, in_command: bool) =
             is_true(in_command) extglob_pattern() /
             is_true(in_command) subshell_command() /
-            !stop_condition() !normal_escape_sequence() [^'$' | '\'' | '\"'] {}
+            !stop_condition() !normal_escape_sequence() [^'$' | '\'' | '\"' | '`'] {}
 
         rule is_true(value: bool) = &[_] {? if value { Ok(()) } else { Err("not true") } }
 
@@ -673,7 +673,7 @@ peg::parser! {
             s:double_quote_body_text() { WordPiece::Text(s.to_owned()) }
 
         rule double_quote_body_text() -> &'input str =
-            $((!double_quoted_escape_sequence() [^'$' | '\"'])+)
+            $((!double_quoted_escape_sequence() [^'$' | '\"' | '`'])+)
 
         rule normal_escape_sequence() -> WordPiece =
             s:$("\\" [c]) { WordPiece::EscapeSequence(s.to_owned()) }
@@ -944,6 +944,32 @@ mod tests {
         Ok(())
     }
 
+    #[test]
+    fn parse_backquoted_command() -> Result<()> {
+        let parsed = super::parse("`echo hi`", &ParserOptions::default())?;
+        assert_matches!(
+            &parsed[..],
+            [WordPieceWithSource { piece: WordPiece::BackquotedCommandSubstitution(s), .. }] if s == "echo hi"
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn parse_backquoted_command_in_double_quotes() -> Result<()> {
+        let parsed = super::parse(r#""`echo hi`""#, &ParserOptions::default())?;
+        assert_matches!(
+            &parsed[..],
+            [WordPieceWithSource { piece: WordPiece::DoubleQuotedSequence(inner), .. }]
+                if matches!(
+                    &inner[..],
+                    [WordPieceWithSource { piece: WordPiece::BackquotedCommandSubstitution(s), .. }] if s == "echo hi"
+                )
+        );
+
+        Ok(())
+    }
+
     #[test]
     fn parse_extglob_with_embedded_parameter() -> Result<()> {
         let parsed = super::parse("+([$var])", &ParserOptions::default())?;


Test patch:
diff --git a/brush-shell/tests/cases/prompt.yaml b/brush-shell/tests/cases/prompt.yaml
index a8e8752..2e2a5cc 100644
--- a/brush-shell/tests/cases/prompt.yaml
+++ b/brush-shell/tests/cases/prompt.yaml
@@ -105,3 +105,21 @@ cases:
       prompt='\#'
       expanded=${prompt@P}
       echo "${expanded}"
+
+  - name: "Command substitution in prompt"
+    known_failure: true # @P expansion doesn't word-expand after prompt expanding
+    stdin: |
+      prompt='$(echo Hello)'
+      echo "Without spaces: ${prompt@P}"
+
+      prompt=' $(echo Hello) '
+      echo "With spaces: ${prompt@P}"
+
+  - name: "Backquoted command substitution in prompt"
+    known_failure: true # @P expansion doesn't word-expand after prompt expanding
+    stdin: |
+      prompt='`echo Hello`'
+      echo "Without spaces: ${prompt@P}"
+
+      prompt=' `echo Hello` '
+      echo "With spaces: ${prompt@P}"
diff --git a/brush-shell/tests/cases/word_expansion.yaml b/brush-shell/tests/cases/word_expansion.yaml
index 998f8a8..7edfb6d 100644
--- a/brush-shell/tests/cases/word_expansion.yaml
+++ b/brush-shell/tests/cases/word_expansion.yaml
@@ -80,6 +80,12 @@ cases:
     stdin: |
       echo `echo \`echo hi\``
 
+  - name: "Backtick command substitution in double quotes"
+    stdin: |
+      echo "`echo First line`"
+      echo " `echo Second line` "
+      echo "Third`echo line`here"
+
   - name: "String length"
     stdin: |
       x="abc"