Repository: casey/just
Pull request #767 (2021-03-25 04:37:29)

Add `export` setting to export all variables
Add a setting that exports all variables by default. This includes
assignments as well as parameters.


Patch:
diff --git a/GRAMMAR.md b/GRAMMAR.md
index 1d844f35..c8cfa1d9 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -55,7 +55,8 @@ assignment    : NAME ':=' expression eol
 
 export        : 'export' assignment
 
-setting       : 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
+setting       : 'set' 'export'
+              | 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
 
 expression    : 'if' condition '{' expression '}' else '{' expression '}'
               | value '+' expression
diff --git a/README.adoc b/README.adoc
index 09b3da97..e32b8645 100644
--- a/README.adoc
+++ b/README.adoc
@@ -388,6 +388,7 @@ foo:
 [options="header"]
 |=================
 | Name | Value | Description
+| `export` | | Export all variables as environment variables
 |`shell` | `[COMMAND, ARGS...]` | Set the command used to invoke recipes and evaluate backticks.
 |=================
 
@@ -407,6 +408,26 @@ foo:
   print("{{foos}}")
 ```
 
+==== Export
+
+The `export` setting causes all Just variables to be exported as environment variables.
+
+```make
+set export
+
+a := "hello"
+
+@foo b:
+  echo $a
+  echo $b
+```
+
+```
+$ just foo goodbye
+hello
+goodbye
+```
+
 === Documentation Comments
 
 Comments immediately preceding a recipe will appear in `just --list`:
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 4b502eaa..bb392e3e 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -69,6 +69,9 @@ impl<'src> Analyzer<'src> {
           assert!(settings.shell.is_none());
           settings.shell = Some(shell);
         },
+        Setting::Export => {
+          settings.export = true;
+        },
       }
     }
 
diff --git a/src/command_ext.rs b/src/command_ext.rs
index 1568ae5b..f3ae8798 100644
--- a/src/command_ext.rs
+++ b/src/command_ext.rs
@@ -1,29 +1,29 @@
 use crate::common::*;
 
 pub(crate) trait CommandExt {
-  fn export(&mut self, dotenv: &BTreeMap<String, String>, scope: &Scope);
+  fn export(&mut self, settings: &Settings, dotenv: &BTreeMap<String, String>, scope: &Scope);
 
-  fn export_scope(&mut self, scope: &Scope);
+  fn export_scope(&mut self, settings: &Settings, scope: &Scope);
 }
 
 impl CommandExt for Command {
-  fn export(&mut self, dotenv: &BTreeMap<String, String>, scope: &Scope) {
+  fn export(&mut self, settings: &Settings, dotenv: &BTreeMap<String, String>, scope: &Scope) {
     for (name, value) in dotenv {
       self.env(name, value);
     }
 
     if let Some(parent) = scope.parent() {
-      self.export_scope(parent);
+      self.export_scope(settings, parent);
     }
   }
 
-  fn export_scope(&mut self, scope: &Scope) {
+  fn export_scope(&mut self, settings: &Settings, scope: &Scope) {
     if let Some(parent) = scope.parent() {
-      self.export_scope(parent);
+      self.export_scope(settings, parent);
     }
 
     for binding in scope.bindings() {
-      if binding.export {
+      if settings.export || binding.export {
         self.env(binding.name.lexeme(), &binding.value);
       }
     }
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 6b382be5..35bfd08e 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -143,7 +143,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
 
     cmd.current_dir(&self.search.working_directory);
 
-    cmd.export(self.dotenv, &self.scope);
+    cmd.export(self.settings, self.dotenv, &self.scope);
 
     cmd.stdin(process::Stdio::inherit());
 
@@ -197,14 +197,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
   ) -> RunResult<'src, Scope<'src, 'run>> {
     let mut evaluator = Evaluator {
       assignments: None,
-      scope: Scope::child(scope),
+      scope: scope.child(),
       search,
       settings,
       dotenv,
       config,
     };
 
-    let mut scope = Scope::child(scope);
+    let mut scope = scope.child();
 
     let mut rest = arguments;
     for parameter in parameters {
diff --git a/src/justfile.rs b/src/justfile.rs
index 47d0cde6..d5371f4b 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -221,7 +221,7 @@ impl<'src> Justfile<'src> {
     search: &'run Search,
     ran: &mut BTreeSet<Vec<String>>,
   ) -> RunResult<'src, ()> {
-    let scope = Evaluator::evaluate_parameters(
+    let outer = Evaluator::evaluate_parameters(
       context.config,
       dotenv,
       &recipe.parameters,
@@ -231,6 +231,8 @@ impl<'src> Justfile<'src> {
       search,
     )?;
 
+    let scope = outer.child();
+
     let mut evaluator =
       Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
 
@@ -251,7 +253,7 @@ impl<'src> Justfile<'src> {
       }
     }
 
-    recipe.run(context, dotenv, scope, search)?;
+    recipe.run(context, dotenv, scope.child(), search)?;
 
     let mut invocation = vec![recipe.name().to_owned()];
     for argument in arguments.iter().cloned() {
diff --git a/src/node.rs b/src/node.rs
index 55476a57..8b9512ec 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -197,6 +197,7 @@ impl<'src> Node<'src> for Set<'src> {
           set.push_mut(Tree::string(&argument.cooked));
         }
       },
+      Export => {},
     }
 
     set
diff --git a/src/parser.rs b/src/parser.rs
index d66eb131..75c0acea 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -345,7 +345,9 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
               items.push(Item::Recipe(self.parse_recipe(doc, false)?));
             },
           Some(Keyword::Set) =>
-            if self.next_are(&[Identifier, Identifier, ColonEquals]) {
+            if self.next_are(&[Identifier, Identifier, ColonEquals])
+              || self.next_are(&[Identifier, Identifier, Eol])
+            {
               items.push(Item::Set(self.parse_set()?));
             } else {
               items.push(Item::Recipe(self.parse_recipe(doc, false)?));
@@ -677,6 +679,14 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
   fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
     self.presume_keyword(Keyword::Set)?;
     let name = Name::from_identifier(self.presume(Identifier)?);
+
+    if name.lexeme() == Keyword::Export.lexeme() {
+      return Ok(Set {
+        value: Setting::Export,
+        name,
+      });
+    }
+
     self.presume(ColonEquals)?;
     if name.lexeme() == Keyword::Shell.lexeme() {
       self.expect(BracketL)?;
diff --git a/src/recipe.rs b/src/recipe.rs
index 625e8ba3..48ae7e13 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -176,7 +176,7 @@ impl<'src, D> Recipe<'src, D> {
         output_error,
       })?;
 
-      command.export(dotenv, &scope);
+      command.export(context.settings, dotenv, &scope);
 
       // run it!
       match InterruptHandler::guard(|| command.status()) {
@@ -265,7 +265,7 @@ impl<'src, D> Recipe<'src, D> {
           cmd.stdout(Stdio::null());
         }
 
-        cmd.export(dotenv, &scope);
+        cmd.export(context.settings, dotenv, &scope);
 
         match InterruptHandler::guard(|| cmd.status()) {
           Ok(exit_status) =>
diff --git a/src/scope.rs b/src/scope.rs
index e7bcf18d..d9bee226 100644
--- a/src/scope.rs
+++ b/src/scope.rs
@@ -7,9 +7,9 @@ pub(crate) struct Scope<'src: 'run, 'run> {
 }
 
 impl<'src, 'run> Scope<'src, 'run> {
-  pub(crate) fn child(parent: &'run Scope<'src, 'run>) -> Scope<'src, 'run> {
+  pub(crate) fn child(&'run self) -> Scope<'src, 'run> {
     Scope {
-      parent:   Some(parent),
+      parent:   Some(self),
       bindings: Table::new(),
     }
   }
diff --git a/src/setting.rs b/src/setting.rs
index dc21e9ac..5f95d873 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -3,6 +3,7 @@ use crate::common::*;
 #[derive(Debug)]
 pub(crate) enum Setting<'src> {
   Shell(Shell<'src>),
+  Export,
 }
 
 #[derive(Debug, PartialEq)]
diff --git a/src/settings.rs b/src/settings.rs
index 39bdb2e3..44a2082b 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -2,12 +2,16 @@ use crate::common::*;
 
 #[derive(Debug, PartialEq)]
 pub(crate) struct Settings<'src> {
-  pub(crate) shell: Option<setting::Shell<'src>>,
+  pub(crate) shell:  Option<setting::Shell<'src>>,
+  pub(crate) export: bool,
 }
 
 impl<'src> Settings<'src> {
   pub(crate) fn new() -> Settings<'src> {
-    Settings { shell: None }
+    Settings {
+      shell:  None,
+      export: false,
+    }
   }
 
   pub(crate) fn shell_command(&self, config: &Config) -> Command {


Test patch:
diff --git a/tests/export.rs b/tests/export.rs
new file mode 100644
index 00000000..ab5c3011
--- /dev/null
+++ b/tests/export.rs
@@ -0,0 +1,122 @@
+test! {
+  name:     success,
+  justfile: r#"
+export FOO := "a"
+baz := "c"
+export BAR := "b"
+export ABC := FOO + BAR + baz
+
+wut:
+  echo $FOO $BAR $ABC
+"#,
+  stdout:   "a b abc\n",
+  stderr:   "echo $FOO $BAR $ABC\n",
+}
+
+test! {
+  name:     override_variable,
+  justfile: r#"
+export FOO := "a"
+baz := "c"
+export BAR := "b"
+export ABC := FOO + "-" + BAR + "-" + baz
+
+wut:
+  echo $FOO $BAR $ABC
+"#,
+  args:     ("--set", "BAR", "bye", "FOO=hello"),
+  stdout:   "hello bye hello-bye-c\n",
+  stderr:   "echo $FOO $BAR $ABC\n",
+}
+
+test! {
+  name:     shebang,
+  justfile: r#"
+export FOO := "a"
+baz := "c"
+export BAR := "b"
+export ABC := FOO + BAR + baz
+
+wut:
+  #!/bin/sh
+  echo $FOO $BAR $ABC
+"#,
+  stdout:   "a b abc\n",
+}
+
+test! {
+  name:     recipe_backtick,
+  justfile: r#"
+export EXPORTED_VARIABLE := "A-IS-A"
+
+recipe:
+  echo {{`echo recipe $EXPORTED_VARIABLE`}}
+"#,
+  stdout:   "recipe A-IS-A\n",
+  stderr:   "echo recipe A-IS-A\n",
+}
+
+test! {
+  name: setting,
+  justfile: "
+    set export
+
+    A := 'hello'
+
+    foo B C=`echo $A`:
+      echo $A
+      echo $B
+      echo $C
+  ",
+  args: ("foo", "goodbye"),
+  stdout: "hello\ngoodbye\nhello\n",
+  stderr: "echo $A\necho $B\necho $C\n",
+}
+
+test! {
+  name: setting_shebang,
+  justfile: "
+    set export
+
+    A := 'hello'
+
+    foo B:
+      #!/bin/sh
+      echo $A
+      echo $B
+  ",
+  args: ("foo", "goodbye"),
+  stdout: "hello\ngoodbye\n",
+  stderr: "",
+}
+
+test! {
+  name: setting_override_undefined,
+  justfile: r#"
+    set export
+
+    A := 'hello'
+    B := `if [ -n "${A+1}" ]; then echo defined; else echo undefined; fi`
+
+    foo C='goodbye' D=`if [ -n "${C+1}" ]; then echo defined; else echo undefined; fi`:
+      echo $B
+      echo $D
+  "#,
+  args: ("A=zzz", "foo"),
+  stdout: "undefined\nundefined\n",
+  stderr: "echo $B\necho $D\n",
+}
+
+test! {
+  name: setting_variable_not_visible,
+  justfile: r#"
+    export A := 'hello'
+    export B := `if [ -n "${A+1}" ]; then echo defined; else echo undefined; fi`
+
+    foo:
+      echo $B
+  "#,
+  args: ("A=zzz"),
+  stdout: "undefined\n",
+  stderr: "echo $B\n",
+}
diff --git a/tests/lib.rs b/tests/lib.rs
index dbdf1498..76ac0797 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -11,6 +11,7 @@ mod dotenv;
 mod edit;
 mod error_messages;
 mod examples;
+mod export;
 mod init;
 mod interrupts;
 mod invocation_directory;
diff --git a/tests/misc.rs b/tests/misc.rs
index 989fdf29..4280a1a8 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -574,64 +574,6 @@ hello := "c"
 "#,
 }
 
-test! {
-  name:     export_success,
-  justfile: r#"
-export FOO := "a"
-baz := "c"
-export BAR := "b"
-export ABC := FOO + BAR + baz
-
-wut:
-  echo $FOO $BAR $ABC
-"#,
-  stdout:   "a b abc\n",
-  stderr:   "echo $FOO $BAR $ABC\n",
-}
-
-test! {
-  name:     export_override,
-  justfile: r#"
-export FOO := "a"
-baz := "c"
-export BAR := "b"
-export ABC := FOO + "-" + BAR + "-" + baz
-
-wut:
-  echo $FOO $BAR $ABC
-"#,
-  args:     ("--set", "BAR", "bye", "FOO=hello"),
-  stdout:   "hello bye hello-bye-c\n",
-  stderr:   "echo $FOO $BAR $ABC\n",
-}
-
-test! {
-  name:     export_shebang,
-  justfile: r#"
-export FOO := "a"
-baz := "c"
-export BAR := "b"
-export ABC := FOO + BAR + baz
-
-wut:
-  #!/bin/sh
-  echo $FOO $BAR $ABC
-"#,
-  stdout:   "a b abc\n",
-}
-
-test! {
-  name:     export_recipe_backtick,
-  justfile: r#"
-export EXPORTED_VARIABLE := "A-IS-A"
-
-recipe:
-  echo {{`echo recipe $EXPORTED_VARIABLE`}}
-"#,
-  stdout:   "recipe A-IS-A\n",
-  stderr:   "echo recipe A-IS-A\n",
-}
-
 test! {
   name:     raw_string,
   justfile: r#"