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.
This commit is contained in:
Casey Rodarmor 2021-03-25 17:00:32 -07:00 committed by GitHub
parent 86c2e52dc6
commit b66a979c08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 186 additions and 78 deletions

View File

@ -55,7 +55,8 @@ assignment : NAME ':=' expression eol
export : 'export' assignment export : 'export' assignment
setting : 'set' 'shell' ':=' '[' string (',' string)* ','? ']' setting : 'set' 'export'
| 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
expression : 'if' condition '{' expression '}' else '{' expression '}' expression : 'if' condition '{' expression '}' else '{' expression '}'
| value '+' expression | value '+' expression

View File

@ -388,6 +388,7 @@ foo:
[options="header"] [options="header"]
|================= |=================
| Name | Value | Description | Name | Value | Description
| `export` | | Export all variables as environment variables
|`shell` | `[COMMAND, ARGS...]` | Set the command used to invoke recipes and evaluate backticks. |`shell` | `[COMMAND, ARGS...]` | Set the command used to invoke recipes and evaluate backticks.
|================= |=================
@ -407,6 +408,26 @@ foo:
print("{{foos}}") 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 === Documentation Comments
Comments immediately preceding a recipe will appear in `just --list`: Comments immediately preceding a recipe will appear in `just --list`:

View File

@ -69,6 +69,9 @@ impl<'src> Analyzer<'src> {
assert!(settings.shell.is_none()); assert!(settings.shell.is_none());
settings.shell = Some(shell); settings.shell = Some(shell);
}, },
Setting::Export => {
settings.export = true;
},
} }
} }

View File

@ -1,29 +1,29 @@
use crate::common::*; use crate::common::*;
pub(crate) trait CommandExt { 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 { 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 { for (name, value) in dotenv {
self.env(name, value); self.env(name, value);
} }
if let Some(parent) = scope.parent() { 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() { if let Some(parent) = scope.parent() {
self.export_scope(parent); self.export_scope(settings, parent);
} }
for binding in scope.bindings() { for binding in scope.bindings() {
if binding.export { if settings.export || binding.export {
self.env(binding.name.lexeme(), &binding.value); self.env(binding.name.lexeme(), &binding.value);
} }
} }

View File

@ -143,7 +143,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
cmd.current_dir(&self.search.working_directory); 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()); cmd.stdin(process::Stdio::inherit());
@ -197,14 +197,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
) -> RunResult<'src, Scope<'src, 'run>> { ) -> RunResult<'src, Scope<'src, 'run>> {
let mut evaluator = Evaluator { let mut evaluator = Evaluator {
assignments: None, assignments: None,
scope: Scope::child(scope), scope: scope.child(),
search, search,
settings, settings,
dotenv, dotenv,
config, config,
}; };
let mut scope = Scope::child(scope); let mut scope = scope.child();
let mut rest = arguments; let mut rest = arguments;
for parameter in parameters { for parameter in parameters {

View File

@ -221,7 +221,7 @@ impl<'src> Justfile<'src> {
search: &'run Search, search: &'run Search,
ran: &mut BTreeSet<Vec<String>>, ran: &mut BTreeSet<Vec<String>>,
) -> RunResult<'src, ()> { ) -> RunResult<'src, ()> {
let scope = Evaluator::evaluate_parameters( let outer = Evaluator::evaluate_parameters(
context.config, context.config,
dotenv, dotenv,
&recipe.parameters, &recipe.parameters,
@ -231,6 +231,8 @@ impl<'src> Justfile<'src> {
search, search,
)?; )?;
let scope = outer.child();
let mut evaluator = let mut evaluator =
Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search); 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()]; let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments.iter().cloned() { for argument in arguments.iter().cloned() {

View File

@ -197,6 +197,7 @@ impl<'src> Node<'src> for Set<'src> {
set.push_mut(Tree::string(&argument.cooked)); set.push_mut(Tree::string(&argument.cooked));
} }
}, },
Export => {},
} }
set set

View File

@ -345,7 +345,9 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
items.push(Item::Recipe(self.parse_recipe(doc, false)?)); items.push(Item::Recipe(self.parse_recipe(doc, false)?));
}, },
Some(Keyword::Set) => 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()?)); items.push(Item::Set(self.parse_set()?));
} else { } else {
items.push(Item::Recipe(self.parse_recipe(doc, false)?)); 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>> { fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
self.presume_keyword(Keyword::Set)?; self.presume_keyword(Keyword::Set)?;
let name = Name::from_identifier(self.presume(Identifier)?); let name = Name::from_identifier(self.presume(Identifier)?);
if name.lexeme() == Keyword::Export.lexeme() {
return Ok(Set {
value: Setting::Export,
name,
});
}
self.presume(ColonEquals)?; self.presume(ColonEquals)?;
if name.lexeme() == Keyword::Shell.lexeme() { if name.lexeme() == Keyword::Shell.lexeme() {
self.expect(BracketL)?; self.expect(BracketL)?;

View File

@ -176,7 +176,7 @@ impl<'src, D> Recipe<'src, D> {
output_error, output_error,
})?; })?;
command.export(dotenv, &scope); command.export(context.settings, dotenv, &scope);
// run it! // run it!
match InterruptHandler::guard(|| command.status()) { match InterruptHandler::guard(|| command.status()) {
@ -265,7 +265,7 @@ impl<'src, D> Recipe<'src, D> {
cmd.stdout(Stdio::null()); cmd.stdout(Stdio::null());
} }
cmd.export(dotenv, &scope); cmd.export(context.settings, dotenv, &scope);
match InterruptHandler::guard(|| cmd.status()) { match InterruptHandler::guard(|| cmd.status()) {
Ok(exit_status) => Ok(exit_status) =>

View File

@ -7,9 +7,9 @@ pub(crate) struct Scope<'src: 'run, 'run> {
} }
impl<'src, 'run> Scope<'src, '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 { Scope {
parent: Some(parent), parent: Some(self),
bindings: Table::new(), bindings: Table::new(),
} }
} }

View File

@ -3,6 +3,7 @@ use crate::common::*;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum Setting<'src> { pub(crate) enum Setting<'src> {
Shell(Shell<'src>), Shell(Shell<'src>),
Export,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]

View File

@ -2,12 +2,16 @@ use crate::common::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(crate) struct Settings<'src> { 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> { impl<'src> Settings<'src> {
pub(crate) fn new() -> 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 { pub(crate) fn shell_command(&self, config: &Config) -> Command {

122
tests/export.rs Normal file
View File

@ -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",
}

View File

@ -11,6 +11,7 @@ mod dotenv;
mod edit; mod edit;
mod error_messages; mod error_messages;
mod examples; mod examples;
mod export;
mod init; mod init;
mod interrupts; mod interrupts;
mod invocation_directory; mod invocation_directory;

View File

@ -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! { test! {
name: raw_string, name: raw_string,
justfile: r#" justfile: r#"