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

View File

@ -388,29 +388,30 @@ foo:
[options="header"]
|=================
| 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
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
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
set export
@ -428,6 +429,22 @@ hello
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
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 {
match set.value {
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = dotenv_load;
},
Setting::Export(export) => {
settings.export = export;
},
Setting::Shell(shell) => {
assert!(settings.shell.is_none());
settings.shell = Some(shell);
},
Setting::Export => {
settings.export = true;
},
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -179,7 +179,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
if expected == found {
Ok(())
} 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) =>
if self.next_are(&[Identifier, Identifier, ColonEquals])
|| self.next_are(&[Identifier, Identifier, Eol])
|| self.next_are(&[Identifier, Identifier, Eof])
{
items.push(Item::Set(self.parse_set()?));
} else {
@ -678,19 +682,50 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
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
fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
self.presume_keyword(Keyword::Set)?;
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 {
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,
});
}
self.presume(ColonEquals)?;
self.expect(ColonEquals)?;
if name.lexeme() == Keyword::Shell.lexeme() {
self.expect(BracketL)?;
@ -1541,6 +1576,42 @@ mod tests {
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! {
name: set_shell_no_arguments,
text: "set shell := ['tclsh']",

View File

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

View File

@ -2,15 +2,17 @@ use crate::common::*;
#[derive(Debug, PartialEq)]
pub(crate) struct Settings<'src> {
pub(crate) shell: Option<setting::Shell<'src>>,
pub(crate) dotenv_load: bool,
pub(crate) export: bool,
pub(crate) shell: Option<setting::Shell<'src>>,
}
impl<'src> Settings<'src> {
pub(crate) fn new() -> Settings<'src> {
Settings {
shell: None,
dotenv_load: true,
export: false,
shell: None,
}
}

View File

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

View File

@ -3,8 +3,8 @@ use crate::common::*;
use std::mem;
/// 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
/// representing the expected results of parsing a given string.
/// Tree type, are only used in the Parser unit tests, providing a concise
/// notation for representing the expected results of parsing a given string.
macro_rules! tree {
{
($($child:tt)*)

View File

@ -26,3 +26,39 @@ fn dotenv() {
let stdout = str::from_utf8(&output.stdout).unwrap();
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! {
name: setting,
name: setting_implicit,
justfile: "
set export
@ -97,6 +97,37 @@ test! {
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! {
name: setting_shebang,
justfile: "

View File

@ -65,6 +65,20 @@ test! {
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! {
name: alias_with_dependencies,
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! {
name: dependency_argument_string,
justfile: "

View File

@ -98,3 +98,79 @@ fn powershell() {
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,
}