Repository: casey/just
Pull request #773 (2021-03-26 01:28:32)

Prefix parameters with `$` to export to environment


Patch:
diff --git a/README.adoc b/README.adoc
index e32b8645..2aeb392a 100644
--- a/README.adoc
+++ b/README.adoc
@@ -757,6 +757,14 @@ test:
     cargo test
 ```
 
+Parameters prefixed with a `$` will be exported as environment variables:
+
+```make
+test $RUST_BACKTRACE="1":
+    # will print a stack trace if it crashes
+    cargo test
+```
+
 === Recipe Parameters
 
 Recipes may have parameters. Here recipe `build` has a parameter called `target`:
@@ -872,6 +880,13 @@ search QUERY:
     lynx 'https://www.google.com/?q={{QUERY}}'
 ```
 
+Parameters prefixed with a `$` will be exported as environment variables:
+
+```make
+foo $bar:
+  echo $bar
+```
+
 === Running recipes at the end of a recipe
 
 Dependencies of a recipes always run before a recipe starts. That is to say, the dependee always runs before the depender.
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 35bfd08e..39f4d6ac 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -227,7 +227,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
         rest = &rest[1..];
         value
       };
-      scope.bind(false, parameter.name, value);
+      scope.bind(parameter.export, parameter.name, value);
     }
 
     Ok(scope)
diff --git a/src/lexer.rs b/src/lexer.rs
index 5857e1fa..bbec4bb5 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -464,6 +464,7 @@ impl<'src> Lexer<'src> {
     match start {
       '!' => self.lex_bang(),
       '*' => self.lex_single(Asterisk),
+      '$' => self.lex_single(Dollar),
       '@' => self.lex_single(At),
       '[' => self.lex_delimiter(BracketL),
       ']' => self.lex_delimiter(BracketR),
@@ -912,6 +913,7 @@ mod tests {
       Colon => ":",
       ColonEquals => ":=",
       Comma => ",",
+      Dollar => "$",
       Eol => "\n",
       Equals => "=",
       EqualsEquals => "==",
@@ -1048,6 +1050,12 @@ mod tests {
     tokens: (BraceL, BraceL, BraceL, BraceR, BraceR, BraceR),
   }
 
+  test! {
+    name:   dollar,
+    text:   "$",
+    tokens: (Dollar),
+  }
+
   test! {
     name:   export_concatination,
     text:   "export foo = 'foo' + 'bar'",
diff --git a/src/parameter.rs b/src/parameter.rs
index b140f8b5..1806eed4 100644
--- a/src/parameter.rs
+++ b/src/parameter.rs
@@ -9,6 +9,8 @@ pub(crate) struct Parameter<'src> {
   pub(crate) kind:    ParameterKind,
   /// An optional default expression
   pub(crate) default: Option<Expression<'src>>,
+  /// Export parameter as environment variable
+  pub(crate) export:  bool,
 }
 
 impl<'src> Display for Parameter<'src> {
diff --git a/src/parser.rs b/src/parser.rs
index 75c0acea..f6e503d3 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -568,7 +568,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
 
     let mut positional = Vec::new();
 
-    while self.next_is(Identifier) {
+    while self.next_is(Identifier) || self.next_is(Dollar) {
       positional.push(self.parse_parameter(ParameterKind::Singular)?);
     }
 
@@ -620,6 +620,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
 
   /// Parse a recipe parameter
   fn parse_parameter(&mut self, kind: ParameterKind) -> CompilationResult<'src, Parameter<'src>> {
+    let export = self.accepted(Dollar)?;
+
     let name = self.parse_name()?;
 
     let default = if self.accepted(Equals)? {
@@ -632,6 +634,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
       name,
       kind,
       default,
+      export,
     })
   }
 
@@ -1643,7 +1646,10 @@ mod tests {
     line:   0,
     column: 5,
     width:  1,
-    kind:   UnexpectedToken{expected: vec![Asterisk, Colon, Equals, Identifier, Plus], found: Eol},
+    kind:   UnexpectedToken{
+      expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],
+      found:    Eol
+    },
   }
 
   error! {
@@ -1728,7 +1734,7 @@ mod tests {
     line:   0,
     column: 6,
     width:  1,
-    kind:   UnexpectedToken{expected: vec![Identifier], found: Colon},
+    kind:   UnexpectedToken{expected: vec![Dollar, Identifier], found: Colon},
   }
 
   error! {
@@ -1748,7 +1754,10 @@ mod tests {
     line:   0,
     column: 8,
     width:  0,
-    kind:   UnexpectedToken{expected: vec![Asterisk, Colon, Equals, Identifier, Plus], found: Eof},
+    kind:   UnexpectedToken {
+      expected: vec![Asterisk, Colon, Dollar, Equals, Identifier, Plus],
+      found:    Eof
+    },
   }
 
   error! {
diff --git a/src/token_kind.rs b/src/token_kind.rs
index ce225aee..42564d91 100644
--- a/src/token_kind.rs
+++ b/src/token_kind.rs
@@ -15,6 +15,7 @@ pub(crate) enum TokenKind {
   Comma,
   Comment,
   Dedent,
+  Dollar,
   Eof,
   Eol,
   Equals,
@@ -50,6 +51,7 @@ impl Display for TokenKind {
       Comma => "','",
       Comment => "comment",
       Dedent => "dedent",
+      Dollar => "'$'",
       Eof => "end of file",
       Eol => "end of line",
       Equals => "'='",


Test patch:
diff --git a/tests/export.rs b/tests/export.rs
index ab5c3011..f9ad42e5 100644
--- a/tests/export.rs
+++ b/tests/export.rs
@@ -13,6 +13,30 @@ wut:
   stderr:   "echo $FOO $BAR $ABC\n",
 }
 
+test! {
+  name:     parameter,
+  justfile: r#"
+    wut $FOO='a' BAR='b':
+      echo $FOO
+      echo {{BAR}}
+      if [ -n "${BAR+1}" ]; then echo defined; else echo undefined; fi
+  "#,
+  stdout: "a\nb\nundefined\n",
+  stderr: "echo $FOO\necho b\nif [ -n \"${BAR+1}\" ]; then echo defined; else echo undefined; fi\n",
+}
+
+test! {
+  name:     parameter_not_visible_to_backtick,
+  justfile: r#"
+    wut $FOO BAR=`if [ -n "${FOO+1}" ]; then echo defined; else echo undefined; fi`:
+      echo $FOO
+      echo {{BAR}}
+  "#,
+  args:   ("wut", "bar"),
+  stdout: "bar\nundefined\n",
+  stderr: "echo $FOO\necho undefined\n",
+}
+
 test! {
   name:     override_variable,
   justfile: r#"
diff --git a/tests/misc.rs b/tests/misc.rs
index 4280a1a8..012fdc48 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -1422,7 +1422,7 @@ test! {
   justfile: "foo 'bar'",
   args:     ("foo"),
   stdout:   "",
-  stderr:   "error: Expected '*', ':', identifier, or '+', but found raw string
+  stderr:   "error: Expected '*', ':', '$', identifier, or '+', but found raw string
   |
 1 | foo 'bar'
   |     ^^^^^
@@ -2050,12 +2050,12 @@ foo a=\t`echo blaaaaaah:
 test! {
   name:     unknown_start_of_token,
   justfile: "
-assembly_source_files = $(wildcard src/arch/$(arch)/*.s)
+assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
 ",
   stderr:   r#"
     error: Unknown start of token:
       |
-    2 | assembly_source_files = $(wildcard src/arch/$(arch)/*.s)
+    2 | assembly_source_files = %(wildcard src/arch/$(arch)/*.s)
       |                         ^
   "#,
    status:   EXIT_FAILURE,