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
setting : 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
setting : 'set' 'export'
| 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
expression : 'if' condition '{' expression '}' else '{' expression '}'
| value '+' expression

View File

@ -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`:

View File

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

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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() {

View File

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

View File

@ -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)?;

View File

@ -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) =>

View File

@ -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(),
}
}

View File

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

View File

@ -3,11 +3,15 @@ use crate::common::*;
#[derive(Debug, PartialEq)]
pub(crate) struct Settings<'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 {

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 error_messages;
mod examples;
mod export;
mod init;
mod interrupts;
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! {
name: raw_string,
justfile: r#"