From 18b9799e8d9a2fa8094a03119550abde0e882f1a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 28 Mar 2021 22:38:07 -0700 Subject: [PATCH] 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. --- GRAMMAR.md | 5 +- README.adoc | 47 ++++++++++++------ src/analyzer.rs | 9 ++-- src/compilation_error.rs | 17 +++---- src/compilation_error_kind.rs | 2 +- src/justfile.rs | 2 +- src/keyword.rs | 3 ++ src/load_dotenv.rs | 6 +++ src/node.rs | 4 +- src/parser.rs | 79 ++++++++++++++++++++++++++++-- src/setting.rs | 3 +- src/settings.rs | 10 ++-- src/token.rs | 2 +- src/tree.rs | 4 +- tests/dotenv.rs | 36 ++++++++++++++ tests/export.rs | 33 ++++++++++++- tests/misc.rs | 90 ++++++----------------------------- tests/shell.rs | 76 +++++++++++++++++++++++++++++ 18 files changed, 308 insertions(+), 120 deletions(-) diff --git a/GRAMMAR.md b/GRAMMAR.md index c8cfa1d..01e2530 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -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 diff --git a/README.adoc b/README.adoc index d7e53ba..63abefd 100644 --- a/README.adoc +++ b/README.adoc @@ -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`: diff --git a/src/analyzer.rs b/src/analyzer.rs index bb392e3..2d7f8c8 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -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; - }, } } diff --git a/src/compilation_error.rs b/src/compilation_error.rs index 7947a4e..cf0b4e0 100644 --- a/src/compilation_error.rs +++ b/src/compilation_error.rs @@ -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 )?; }, diff --git a/src/compilation_error_kind.rs b/src/compilation_error_kind.rs index 4e2b2cb..d01a632 100644 --- a/src/compilation_error_kind.rs +++ b/src/compilation_error_kind.rs @@ -40,7 +40,7 @@ pub(crate) enum CompilationErrorKind<'src> { first: usize, }, ExpectedKeyword { - expected: Keyword, + expected: Vec, found: &'src str, }, ExtraLeadingWhitespace, diff --git a/src/justfile.rs b/src/justfile.rs index d5371f4..7b1fcbe 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -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() }; diff --git a/src/keyword.rs b/src/keyword.rs index 4d75305..a3f6314 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -6,6 +6,9 @@ pub(crate) enum Keyword { Alias, Else, Export, + DotenvLoad, + True, + False, If, Set, Shell, diff --git a/src/load_dotenv.rs b/src/load_dotenv.rs index 22cb1ea..d0c1b3d 100644 --- a/src/load_dotenv.rs +++ b/src/load_dotenv.rs @@ -2,10 +2,16 @@ use crate::common::*; pub(crate) fn load_dotenv( working_directory: &Path, + settings: &Settings, ) -> RunResult<'static, BTreeMap> { // `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"); diff --git a/src/node.rs b/src/node.rs index 8b9512e..4e3baaa 100644 --- a/src/node.rs +++ b/src/node.rs @@ -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 diff --git a/src/parser.rs b/src/parser.rs index f6e503d..765eb8a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -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']", diff --git a/src/setting.rs b/src/setting.rs index 5f95d87..082baf1 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -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)] diff --git a/src/settings.rs b/src/settings.rs index 388a673..2037d33 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,15 +2,17 @@ use crate::common::*; #[derive(Debug, PartialEq)] pub(crate) struct Settings<'src> { - pub(crate) shell: Option>, - pub(crate) export: bool, + pub(crate) dotenv_load: bool, + pub(crate) export: bool, + pub(crate) shell: Option>, } impl<'src> Settings<'src> { pub(crate) fn new() -> Settings<'src> { Settings { - shell: None, - export: false, + dotenv_load: true, + export: false, + shell: None, } } diff --git a/src/token.rs b/src/token.rs index 9b42f18..a0ef4de 100644 --- a/src/token.rs +++ b/src/token.rs @@ -60,7 +60,7 @@ impl<'src> Token<'src> { space_column, color.prefix(), "", - space_width, + space_width.max(1), color.suffix() )?; }, diff --git a/src/tree.rs b/src/tree.rs index 0d92960..1e86cb2 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -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)*) diff --git a/tests/dotenv.rs b/tests/dotenv.rs index 288d3dd..50c5b1b 100644 --- a/tests/dotenv.rs +++ b/tests/dotenv.rs @@ -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", +} diff --git a/tests/export.rs b/tests/export.rs index f9ad42e..9852f97 100644 --- a/tests/export.rs +++ b/tests/export.rs @@ -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: " diff --git a/tests/misc.rs b/tests/misc.rs index 2e0db25..18d8236 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -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: " diff --git a/tests/shell.rs b/tests/shell.rs index 179387d..2e526d2 100644 --- a/tests/shell.rs +++ b/tests/shell.rs @@ -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, +}