From b66a979c083585167e933150cbcde08fa6802e93 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 25 Mar 2021 17:00:32 -0700 Subject: [PATCH] Add `set export` to export all variables as environment variables (#767) Add a setting that exports all variables by default, regardless of whether they use the `export` keyword. This includes assignments as well as parameters. Just does dependency analysis of variable uses, allowing variables to be used out of order in assignments, as long as there are no circular dependencies. However, use of environment variable is not known to Just, so exported variables are only exported to child scopes, to avoid ordering dependencies, since dependency analysis cannot be done. --- GRAMMAR.md | 3 +- README.adoc | 21 ++++++++ src/analyzer.rs | 3 ++ src/command_ext.rs | 14 +++--- src/evaluator.rs | 6 +-- src/justfile.rs | 6 ++- src/node.rs | 1 + src/parser.rs | 12 ++++- src/recipe.rs | 4 +- src/scope.rs | 4 +- src/setting.rs | 1 + src/settings.rs | 8 ++- tests/export.rs | 122 +++++++++++++++++++++++++++++++++++++++++++++ tests/lib.rs | 1 + tests/misc.rs | 58 --------------------- 15 files changed, 186 insertions(+), 78 deletions(-) create mode 100644 tests/export.rs diff --git a/GRAMMAR.md b/GRAMMAR.md index 1d844f3..c8cfa1d 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 09b3da9..e32b864 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 4b502ea..bb392e3 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 1568ae5..f3ae879 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, scope: &Scope); + fn export(&mut self, settings: &Settings, dotenv: &BTreeMap, 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, scope: &Scope) { + fn export(&mut self, settings: &Settings, dotenv: &BTreeMap, 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 6b382be..35bfd08 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 47d0cde..d5371f4 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -221,7 +221,7 @@ impl<'src> Justfile<'src> { search: &'run Search, ran: &mut BTreeSet>, ) -> 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 55476a5..8b9512e 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 d66eb13..75c0ace 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 625e8ba..48ae7e1 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 e7bcf18..d9bee22 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 dc21e9a..5f95d87 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 39bdb2e..44a2082 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>, + pub(crate) shell: Option>, + 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 { diff --git a/tests/export.rs b/tests/export.rs new file mode 100644 index 0000000..ab5c301 --- /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 dbdf149..76ac079 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 989fdf2..4280a1a 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#"