Add dotenv-load setting (#778)

The `dotenv-load` setting controls whether or not a `.env` file will be
loaded if present. It currently defaults to true.
This commit is contained in:
Casey Rodarmor 2021-03-28 22:38:07 -07:00 committed by GitHub
parent 2e8c58e1cd
commit 18b9799e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 308 additions and 120 deletions

View File

@ -55,9 +55,12 @@ assignment : NAME ':=' expression eol
export : 'export' assignment export : 'export' assignment
setting : 'set' 'export' setting : 'set' 'dotenv-load' boolean?
| 'set' 'export' boolean?
| 'set' 'shell' ':=' '[' string (',' string)* ','? ']' | 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
boolean : ':=' ('true' | 'false')
expression : 'if' condition '{' expression '}' else '{' expression '}' expression : 'if' condition '{' expression '}' else '{' expression '}'
| value '+' expression | value '+' expression
| value | value

View File

@ -388,29 +388,30 @@ foo:
[options="header"] [options="header"]
|================= |=================
| Name | Value | Description | Name | Value | Description
| `export` | | Export all variables as environment variables | `dotenv-load` | `true` or `false` | Load a `.env` file, if present.
| `export` | `true` or `false` | 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.
|================= |=================
==== Shell Boolean settings can be written as:
The `shell` setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected.
```make
# use python3 to execute recipe lines and backticks
set shell := ["python3", "-c"]
# use print to capture result of evaluation
foos := `print("foo" * 4)`
foo:
print("Snake snake snake snake.")
print("{{foos}}")
``` ```
set NAME
```
Which is equivalent to:
```
set NAME := true
```
==== Dotenv Load
If `dotenv-load` is `true`, a `.env` file will be loaded if present. Defaults to `true`.
==== Export ==== Export
The `export` setting causes all Just variables to be exported as environment variables. The `export` setting causes all Just variables to be exported as environment variables. Defaults to `false`.
```make ```make
set export set export
@ -428,6 +429,22 @@ hello
goodbye goodbye
``` ```
==== Shell
The `shell` setting controls the command used to invoke recipe lines and backticks. Shebang recipes are unaffected.
```make
# use python3 to execute recipe lines and backticks
set shell := ["python3", "-c"]
# use print to capture result of evaluation
foos := `print("foo" * 4)`
foo:
print("Snake snake snake snake.")
print("{{foos}}")
```
=== 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

@ -65,13 +65,16 @@ impl<'src> Analyzer<'src> {
for (_, set) in self.sets { for (_, set) in self.sets {
match set.value { match set.value {
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = dotenv_load;
},
Setting::Export(export) => {
settings.export = export;
},
Setting::Shell(shell) => { Setting::Shell(shell) => {
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

@ -15,7 +15,7 @@ impl Display for CompilationError<'_> {
write!(f, "{}", message.prefix())?; write!(f, "{}", message.prefix())?;
match self.kind { match &self.kind {
AliasShadowsRecipe { alias, recipe_line } => { AliasShadowsRecipe { alias, recipe_line } => {
writeln!( writeln!(
f, f,
@ -116,22 +116,23 @@ impl Display for CompilationError<'_> {
"Dependency `{}` got {} {} but takes ", "Dependency `{}` got {} {} but takes ",
dependency, dependency,
found, found,
Count("argument", found), Count("argument", *found),
)?; )?;
if min == max { if min == max {
let expected = min; let expected = min;
writeln!(f, "{} {}", expected, Count("argument", expected))?; writeln!(f, "{} {}", expected, Count("argument", *expected))?;
} else if found < min { } else if found < min {
writeln!(f, "at least {} {}", min, Count("argument", min))?; writeln!(f, "at least {} {}", min, Count("argument", *min))?;
} else { } else {
writeln!(f, "at most {} {}", max, Count("argument", max))?; writeln!(f, "at most {} {}", max, Count("argument", *max))?;
} }
}, },
ExpectedKeyword { expected, found } => writeln!( ExpectedKeyword { expected, found } => writeln!(
f, f,
"Expected keyword `{}` but found identifier `{}`", "Expected keyword {} but found identifier `{}`",
expected, found List::or_ticked(expected),
found
)?, )?,
ParameterShadowsVariable { parameter } => { ParameterShadowsVariable { parameter } => {
writeln!( writeln!(
@ -171,7 +172,7 @@ impl Display for CompilationError<'_> {
"Function `{}` called with {} {} but takes {}", "Function `{}` called with {} {} but takes {}",
function, function,
found, found,
Count("argument", found), Count("argument", *found),
expected expected
)?; )?;
}, },

View File

@ -40,7 +40,7 @@ pub(crate) enum CompilationErrorKind<'src> {
first: usize, first: usize,
}, },
ExpectedKeyword { ExpectedKeyword {
expected: Keyword, expected: Vec<Keyword>,
found: &'src str, found: &'src str,
}, },
ExtraLeadingWhitespace, ExtraLeadingWhitespace,

View File

@ -91,7 +91,7 @@ impl<'src> Justfile<'src> {
} }
let dotenv = if config.load_dotenv { let dotenv = if config.load_dotenv {
load_dotenv(&search.working_directory)? load_dotenv(&search.working_directory, &self.settings)?
} else { } else {
BTreeMap::new() BTreeMap::new()
}; };

View File

@ -6,6 +6,9 @@ pub(crate) enum Keyword {
Alias, Alias,
Else, Else,
Export, Export,
DotenvLoad,
True,
False,
If, If,
Set, Set,
Shell, Shell,

View File

@ -2,10 +2,16 @@ use crate::common::*;
pub(crate) fn load_dotenv( pub(crate) fn load_dotenv(
working_directory: &Path, working_directory: &Path,
settings: &Settings,
) -> RunResult<'static, BTreeMap<String, String>> { ) -> RunResult<'static, BTreeMap<String, String>> {
// `dotenv::from_path_iter` should eventually be un-deprecated, see: // `dotenv::from_path_iter` should eventually be un-deprecated, see:
// https://github.com/dotenv-rs/dotenv/issues/13 // https://github.com/dotenv-rs/dotenv/issues/13
#![allow(deprecated)] #![allow(deprecated)]
if !settings.dotenv_load {
return Ok(BTreeMap::new());
}
for directory in working_directory.ancestors() { for directory in working_directory.ancestors() {
let path = directory.join(".env"); let path = directory.join(".env");

View File

@ -187,17 +187,17 @@ impl<'src> Node<'src> for Set<'src> {
fn tree(&self) -> Tree<'src> { fn tree(&self) -> Tree<'src> {
let mut set = Tree::atom(Keyword::Set.lexeme()); let mut set = Tree::atom(Keyword::Set.lexeme());
set.push_mut(self.name.lexeme()); set.push_mut(self.name.lexeme().replace('-', "_"));
use Setting::*; use Setting::*;
match &self.value { match &self.value {
DotenvLoad(value) | Export(value) => set.push_mut(value.to_string()),
Shell(setting::Shell { command, arguments }) => { Shell(setting::Shell { command, arguments }) => {
set.push_mut(Tree::string(&command.cooked)); set.push_mut(Tree::string(&command.cooked));
for argument in arguments { for argument in arguments {
set.push_mut(Tree::string(&argument.cooked)); set.push_mut(Tree::string(&argument.cooked));
} }
}, },
Export => {},
} }
set set

View File

@ -179,7 +179,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
if expected == found { if expected == found {
Ok(()) Ok(())
} else { } else {
Err(identifier.error(CompilationErrorKind::ExpectedKeyword { expected, found })) Err(identifier.error(CompilationErrorKind::ExpectedKeyword {
expected: vec![expected],
found,
}))
} }
} }
@ -347,6 +350,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
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]) || self.next_are(&[Identifier, Identifier, Eol])
|| self.next_are(&[Identifier, Identifier, Eof])
{ {
items.push(Item::Set(self.parse_set()?)); items.push(Item::Set(self.parse_set()?));
} else { } else {
@ -678,19 +682,50 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(lines) Ok(lines)
} }
/// Parse a boolean setting value
fn parse_set_bool(&mut self) -> CompilationResult<'src, bool> {
if !self.accepted(ColonEquals)? {
return Ok(true);
}
let identifier = self.expect(Identifier)?;
let value = if Keyword::True == identifier.lexeme() {
true
} else if Keyword::False == identifier.lexeme() {
false
} else {
return Err(identifier.error(CompilationErrorKind::ExpectedKeyword {
expected: vec![Keyword::True, Keyword::False],
found: identifier.lexeme(),
}));
};
Ok(value)
}
/// Parse a setting /// Parse a setting
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)?);
let lexeme = name.lexeme();
if name.lexeme() == Keyword::Export.lexeme() { if Keyword::DotenvLoad == lexeme {
let value = self.parse_set_bool()?;
return Ok(Set { return Ok(Set {
value: Setting::Export, value: Setting::DotenvLoad(value),
name,
});
} else if Keyword::Export == lexeme {
let value = self.parse_set_bool()?;
return Ok(Set {
value: Setting::Export(value),
name, name,
}); });
} }
self.presume(ColonEquals)?; self.expect(ColonEquals)?;
if name.lexeme() == Keyword::Shell.lexeme() { if name.lexeme() == Keyword::Shell.lexeme() {
self.expect(BracketL)?; self.expect(BracketL)?;
@ -1541,6 +1576,42 @@ mod tests {
tree: (justfile (recipe a (body ("foo"))) (recipe b)), tree: (justfile (recipe a (body ("foo"))) (recipe b)),
} }
test! {
name: set_export_implicit,
text: "set export",
tree: (justfile (set export true)),
}
test! {
name: set_export_true,
text: "set export := true",
tree: (justfile (set export true)),
}
test! {
name: set_export_false,
text: "set export := false",
tree: (justfile (set export false)),
}
test! {
name: set_dotenv_load_implicit,
text: "set dotenv-load",
tree: (justfile (set dotenv_load true)),
}
test! {
name: set_dotenv_load_true,
text: "set dotenv-load := true",
tree: (justfile (set dotenv_load true)),
}
test! {
name: set_dotenv_load_false,
text: "set dotenv-load := false",
tree: (justfile (set dotenv_load false)),
}
test! { test! {
name: set_shell_no_arguments, name: set_shell_no_arguments,
text: "set shell := ['tclsh']", text: "set shell := ['tclsh']",

View File

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

View File

@ -2,15 +2,17 @@ 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) dotenv_load: bool,
pub(crate) export: bool, pub(crate) export: bool,
pub(crate) shell: Option<setting::Shell<'src>>,
} }
impl<'src> Settings<'src> { impl<'src> Settings<'src> {
pub(crate) fn new() -> Settings<'src> { pub(crate) fn new() -> Settings<'src> {
Settings { Settings {
shell: None, dotenv_load: true,
export: false, export: false,
shell: None,
} }
} }

View File

@ -60,7 +60,7 @@ impl<'src> Token<'src> {
space_column, space_column,
color.prefix(), color.prefix(),
"", "",
space_width, space_width.max(1),
color.suffix() color.suffix()
)?; )?;
}, },

View File

@ -3,8 +3,8 @@ use crate::common::*;
use std::mem; use std::mem;
/// Construct a `Tree` from a symbolic expression literal. This macro, and the /// Construct a `Tree` from a symbolic expression literal. This macro, and the
/// Tree type, are only used in the Parser unit tests, as a concise notation /// Tree type, are only used in the Parser unit tests, providing a concise
/// representing the expected results of parsing a given string. /// notation for representing the expected results of parsing a given string.
macro_rules! tree { macro_rules! tree {
{ {
($($child:tt)*) ($($child:tt)*)

View File

@ -26,3 +26,39 @@ fn dotenv() {
let stdout = str::from_utf8(&output.stdout).unwrap(); let stdout = str::from_utf8(&output.stdout).unwrap();
assert_eq!(stdout, "KEY=SUB\n"); assert_eq!(stdout, "KEY=SUB\n");
} }
test! {
name: set_false,
justfile: r#"
set dotenv-load := false
foo:
if [ -n "${DOTENV_KEY+1}" ]; then echo defined; else echo undefined; fi
"#,
stdout: "undefined\n",
stderr: "if [ -n \"${DOTENV_KEY+1}\" ]; then echo defined; else echo undefined; fi\n",
}
test! {
name: set_implicit,
justfile: r#"
set dotenv-load
foo:
echo $DOTENV_KEY
"#,
stdout: "dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
}
test! {
name: set_true,
justfile: r#"
set dotenv-load := true
foo:
echo $DOTENV_KEY
"#,
stdout: "dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
}

View File

@ -81,7 +81,7 @@ recipe:
} }
test! { test! {
name: setting, name: setting_implicit,
justfile: " justfile: "
set export set export
@ -97,6 +97,37 @@ test! {
stderr: "echo $A\necho $B\necho $C\n", stderr: "echo $A\necho $B\necho $C\n",
} }
test! {
name: setting_true,
justfile: "
set export := true
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_false,
justfile: r#"
set export := false
A := 'hello'
foo:
if [ -n "${A+1}" ]; then echo defined; else echo undefined; fi
"#,
stdout: "undefined\n",
stderr: "if [ -n \"${A+1}\" ]; then echo defined; else echo undefined; fi\n",
}
test! { test! {
name: setting_shebang, name: setting_shebang,
justfile: " justfile: "

View File

@ -65,6 +65,20 @@ test! {
stderr: "echo bar\n", stderr: "echo bar\n",
} }
test! {
name: bad_setting,
justfile: "
set foo
",
stderr: "
error: Expected ':=', but found end of line
|
1 | set foo
| ^
",
status: EXIT_FAILURE,
}
test! { test! {
name: alias_with_dependencies, name: alias_with_dependencies,
justfile: "foo:\n echo foo\nbar: foo\nalias b := bar", justfile: "foo:\n echo foo\nbar: foo\nalias b := bar",
@ -2214,82 +2228,6 @@ test! {
", ",
} }
test! {
name: shell_args,
justfile: "
default:
echo A${foo}A
",
args: ("--shell-arg", "-c"),
stdout: "AA\n",
stderr: "echo A${foo}A\n",
shell: false,
}
test! {
name: shell_override,
justfile: "
set shell := ['foo-bar-baz']
default:
echo hello
",
args: ("--shell", "bash"),
stdout: "hello\n",
stderr: "echo hello\n",
shell: false,
}
test! {
name: shell_arg_override,
justfile: "
set shell := ['foo-bar-baz']
default:
echo hello
",
args: ("--shell-arg", "-cu"),
stdout: "hello\n",
stderr: "echo hello\n",
shell: false,
}
#[cfg(unix)]
test! {
name: set_shell,
justfile: "
set shell := ['echo', '-n']
x := `bar`
foo:
echo {{x}}
echo foo
",
args: (),
stdout: "echo barecho foo",
stderr: "echo bar\necho foo\n",
shell: false,
}
#[cfg(windows)]
test! {
name: set_shell,
justfile: "
set shell := ['echo', '-n']
x := `bar`
foo:
echo {{x}}
echo foo
",
args: (),
stdout: "-n echo -n bar\r\r\n-n echo foo\r\n",
stderr: "echo -n bar\r\necho foo\n",
shell: false,
}
test! { test! {
name: dependency_argument_string, name: dependency_argument_string,
justfile: " justfile: "

View File

@ -98,3 +98,79 @@ fn powershell() {
assert_stdout(&output, stdout); assert_stdout(&output, stdout);
} }
test! {
name: shell_args,
justfile: "
default:
echo A${foo}A
",
args: ("--shell-arg", "-c"),
stdout: "AA\n",
stderr: "echo A${foo}A\n",
shell: false,
}
test! {
name: shell_override,
justfile: "
set shell := ['foo-bar-baz']
default:
echo hello
",
args: ("--shell", "bash"),
stdout: "hello\n",
stderr: "echo hello\n",
shell: false,
}
test! {
name: shell_arg_override,
justfile: "
set shell := ['foo-bar-baz']
default:
echo hello
",
args: ("--shell-arg", "-cu"),
stdout: "hello\n",
stderr: "echo hello\n",
shell: false,
}
#[cfg(unix)]
test! {
name: set_shell,
justfile: "
set shell := ['echo', '-n']
x := `bar`
foo:
echo {{x}}
echo foo
",
args: (),
stdout: "echo barecho foo",
stderr: "echo bar\necho foo\n",
shell: false,
}
#[cfg(windows)]
test! {
name: set_shell,
justfile: "
set shell := ['echo', '-n']
x := `bar`
foo:
echo {{x}}
echo foo
",
args: (),
stdout: "-n echo -n bar\r\r\n-n echo foo\r\n",
stderr: "echo -n bar\r\necho foo\n",
shell: false,
}