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#"