Repository: casey/just
Pull request #2083 (2024-05-25 01:32:03)

Don't allow shell-expanded strings in dependency arguments
Fixes, in horrible fashion, #2074.


Patch:
diff --git a/src/parser.rs b/src/parser.rs
index 77c6147b..2843fd85 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -554,9 +554,27 @@ impl<'run, 'src> Parser<'run, 'src> {
     })
   }
 
+  // Check if the next tokens are a shell-expanded string, i.e., `x"foo"`.
+  //
+  // This function skips initial whitespace tokens, but thereafter is
+  // whitespace-sensitive, so `x"foo"` is a shell-expanded string, whereas `x
+  // "foo"` is not.
+  fn next_is_shell_expanded_string(&self) -> bool {
+    let mut tokens = self
+      .tokens
+      .iter()
+      .skip(self.next_token)
+      .skip_while(|token| token.kind == Whitespace);
+
+    tokens
+      .next()
+      .is_some_and(|token| token.kind == Identifier && token.lexeme() == "x")
+      && tokens.next().is_some_and(|token| token.kind == StringToken)
+  }
+
   /// Parse a value, e.g. `(bar)`
   fn parse_value(&mut self) -> CompileResult<'src, Expression<'src>> {
-    if self.next_is(StringToken) || self.next_are(&[Identifier, StringToken]) {
+    if self.next_is(StringToken) || self.next_is_shell_expanded_string() {
       Ok(Expression::StringLiteral {
         string_literal: self.parse_string_literal()?,
       })
@@ -610,7 +628,12 @@ impl<'run, 'src> Parser<'run, 'src> {
   fn parse_string_literal_token(
     &mut self,
   ) -> CompileResult<'src, (Token<'src>, StringLiteral<'src>)> {
-    let expand = self.accepted_keyword(Keyword::X)?;
+    let expand = if self.next_is(Identifier) {
+      self.expect_keyword(Keyword::X)?;
+      true
+    } else {
+      false
+    };
 
     let token = self.expect(StringToken)?;
 
@@ -2362,6 +2385,7 @@ mod tests {
     width:  1,
     kind:   UnexpectedToken {
       expected: vec![
+        Identifier,
         StringToken,
       ],
       found: BracketR,
@@ -2404,6 +2428,7 @@ mod tests {
     kind:   UnexpectedToken {
       expected: vec![
         BracketR,
+        Identifier,
         StringToken,
       ],
       found: Eof,


Test patch:
diff --git a/tests/shell_expansion.rs b/tests/shell_expansion.rs
index f91fd000..c50ce5b4 100644
--- a/tests/shell_expansion.rs
+++ b/tests/shell_expansion.rs
@@ -14,6 +14,27 @@ fn strings_are_shell_expanded() {
     .run();
 }
 
+#[test]
+fn shell_expanded_strings_must_not_have_whitespace() {
+  Test::new()
+    .justfile(
+      "
+        x := x '$JUST_TEST_VARIABLE'
+      ",
+    )
+    .status(1)
+    .stderr(
+      "
+        error: Expected comment, end of file, end of line, '(', '+', or '/', but found string
+         ——▶ justfile:1:8
+          │
+        1 │ x := x '$JUST_TEST_VARIABLE'
+          │        ^^^^^^^^^^^^^^^^^^^^^
+      ",
+    )
+    .run();
+}
+
 #[test]
 fn shell_expanded_error_messages_highlight_string_token() {
   Test::new()
@@ -97,3 +118,42 @@ fn shell_expanded_strings_can_be_used_in_mod_paths() {
     .test_round_trip(false)
     .run();
 }
+
+#[test]
+fn shell_expanded_strings_do_not_conflict_with_dependencies() {
+  Test::new()
+    .justfile(
+      "
+        foo a b:
+          @echo {{a}}{{b}}
+        bar a b: (foo a 'c')
+      ",
+    )
+    .args(["bar", "A", "B"])
+    .stdout("Ac\n")
+    .run();
+
+  Test::new()
+    .justfile(
+      "
+        foo a b:
+          @echo {{a}}{{b}}
+        bar a b: (foo a'c')
+      ",
+    )
+    .args(["bar", "A", "B"])
+    .stdout("Ac\n")
+    .run();
+
+  Test::new()
+    .justfile(
+      "
+        foo a b:
+          @echo {{a}}{{b}}
+        bar x b: (foo x 'c')
+      ",
+    )
+    .args(["bar", "A", "B"])
+    .stdout("Ac\n")
+    .run();
+}