summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLaurent Fourrier <ltfourrier@gmail.com>2023-10-12 07:04:46 +0200
committerGitHub <noreply@github.com>2023-10-12 05:04:46 +0000
commit812e1ea3cc8322e527e1d96e9a8c834d99c0fbcb (patch)
treef87f6e2dcac86a80f30a0b7f01c84f8ebf5c6ebf
parentd0c87c8ccd02a0e48c3ffa724c9023b6659c1c24 (diff)
Add `dotenv-filename` and `dotenv-path` settings (#1692)
-rw-r--r--GRAMMAR.md2
-rw-r--r--README.md73
-rwxr-xr-xjustfile4
-rw-r--r--src/keyword.rs2
-rw-r--r--src/load_dotenv.rs25
-rw-r--r--src/node.rs2
-rw-r--r--src/parser.rs64
-rw-r--r--src/setting.rs6
-rw-r--r--src/settings.rs8
-rw-r--r--tests/dotenv.rs84
-rw-r--r--tests/json.rs34
-rw-r--r--tests/misc.rs18
12 files changed, 233 insertions, 89 deletions
diff --git a/GRAMMAR.md b/GRAMMAR.md
index 51a431fb..6a0c40a1 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -60,7 +60,9 @@ assignment : NAME ':=' expression eol
export : 'export' assignment
setting : 'set' 'allow-duplicate-recipes' boolean?
+ | 'set' 'dotenv-filename' ':=' string
| 'set' 'dotenv-load' boolean?
+ | 'set' 'dotenv-path' ':=' string
| 'set' 'export' boolean?
| 'set' 'fallback' boolean?
| 'set' 'ignore-comments' boolean?
diff --git a/README.md b/README.md
index f385d55b..65e7aa4a 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ Yay, all your tests passed!
- Wherever possible, errors are resolved statically. Unknown recipes and circular dependencies are reported before anything runs.
-- `just` [loads `.env` files](#dotenv-integration), making it easy to populate environment variables.
+- `just` [loads `.env` files](#dotenv-settings), making it easy to populate environment variables.
- Recipes can be [listed from the command line](#listing-available-recipes).
@@ -669,7 +669,9 @@ foo:
| Name | Value | Default | Description |
| ------------------------- | ------------------ | ------- |---------------------------------------------------------------------------------------------- |
| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |
+| `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. |
| `dotenv-load` | boolean | `false` | Load a `.env` file, if present. |
+| `dotenv-path` | string | - | Load a `.env` file from a custom path, if present. Overrides `dotenv-filename`. |
| `export` | boolean | `false` | Export all variables as environment variables. |
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
@@ -710,9 +712,41 @@ $ just foo
bar
```
-#### Dotenv Load
+#### Dotenv Settings
-If `dotenv-load` is `true`, a `.env` file will be loaded if present. Defaults to `false`.
+If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load environment variables from a file.
+
+If `dotenv-path` is set, `just` will look for a file at the given path.
+
+Otherwise, `just` looks for a file named `.env` by default, unless `dotenv-filename` set, in which case the value of `dotenv-filename` is used. This file can be located in the same directory as your `justfile` or in a parent directory.
+
+The loaded variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks.
+
+For example, if your `.env` file contains:
+
+```sh
+# a comment, will be ignored
+DATABASE_ADDRESS=localhost:6379
+SERVER_PORT=1337
+```
+
+And your `justfile` contains:
+
+```just
+set dotenv-load
+
+serve:
+ @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
+ ./server --database $DATABASE_ADDRESS --port $SERVER_PORT
+```
+
+`just serve` will output:
+
+```sh
+$ just serve
+Starting server with database localhost:6379 on port 1337…
+./server --database $DATABASE_ADDRESS --port $SERVER_PORT
+```
#### Export
@@ -878,36 +912,6 @@ Available recipes:
test # test stuff
```
-### Dotenv Integration
-
-If [`dotenv-load`](#dotenv-load) is set, `just` will load environment variables from a file named `.env`. This file can be located in the same directory as your `justfile` or in a parent directory. These variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks.
-
-For example, if your `.env` file contains:
-
-```sh
-# a comment, will be ignored
-DATABASE_ADDRESS=localhost:6379
-SERVER_PORT=1337
-```
-
-And your `justfile` contains:
-
-```just
-set dotenv-load
-
-serve:
- @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
- ./server --database $DATABASE_ADDRESS --port $SERVER_PORT
-```
-
-`just serve` will output:
-
-```sh
-$ just serve
-Starting server with database localhost:6379 on port 1337…
-./server --database $DATABASE_ADDRESS --port $SERVER_PORT
-```
-
### Variables and Substitution
Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported:
@@ -1528,9 +1532,6 @@ print_home_folder:
$ just
HOME is '/home/myuser'
```
-#### Loading Environment Variables from a `.env` File
-
-`just` will load environment variables from a `.env` file if [dotenv-load](#dotenv-load) is set. The variables in the file will be available as environment variables to the recipes. See [dotenv-integration](#dotenv-integration) for more information.
#### Setting `just` Variables from Environment Variables
diff --git a/justfile b/justfile
index 52b4ee1f..f25e719c 100755
--- a/justfile
+++ b/justfile
@@ -6,10 +6,6 @@ alias t := test
alias c := check
-bt := '0'
-
-export RUST_BACKTRACE := bt
-
log := "warn"
export JUST_LOG := log
diff --git a/src/keyword.rs b/src/keyword.rs
index bff1b6c2..db2ba951 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -5,7 +5,9 @@ use super::*;
pub(crate) enum Keyword {
Alias,
AllowDuplicateRecipes,
+ DotenvFilename,
DotenvLoad,
+ DotenvPath,
Else,
Export,
Fallback,
diff --git a/src/load_dotenv.rs b/src/load_dotenv.rs
index b1fbe868..1ccb4b9b 100644
--- a/src/load_dotenv.rs
+++ b/src/load_dotenv.rs
@@ -7,25 +7,28 @@ pub(crate) fn load_dotenv(
settings: &Settings,
working_directory: &Path,
) -> RunResult<'static, BTreeMap<String, String>> {
- if !settings.dotenv_load.unwrap_or(false)
- && config.dotenv_filename.is_none()
- && config.dotenv_path.is_none()
- {
+ let dotenv_filename = config
+ .dotenv_filename
+ .as_ref()
+ .or(settings.dotenv_filename.as_ref());
+
+ let dotenv_path = config
+ .dotenv_path
+ .as_ref()
+ .or(settings.dotenv_path.as_ref());
+
+ if !settings.dotenv_load.unwrap_or(false) && dotenv_filename.is_none() && dotenv_path.is_none() {
return Ok(BTreeMap::new());
}
- if let Some(path) = &config.dotenv_path {
+ if let Some(path) = dotenv_path {
return load_from_file(path);
}
- let filename = config
- .dotenv_filename
- .as_deref()
- .unwrap_or(DEFAULT_DOTENV_FILENAME)
- .to_owned();
+ let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str());
for directory in working_directory.ancestors() {
- let path = directory.join(filename.as_str());
+ let path = directory.join(filename);
if path.is_file() {
return load_from_file(&path);
}
diff --git a/src/node.rs b/src/node.rs
index e231c248..924bcf34 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -249,7 +249,7 @@ impl<'src> Node<'src> for Set<'src> {
set.push_mut(Tree::string(&argument.cooked));
}
}
- Setting::Tempdir(value) => {
+ Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
set.push_mut(Tree::string(value));
}
}
diff --git a/src/parser.rs b/src/parser.rs
index 04c4a9d1..81ae3c19 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -780,21 +780,23 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.presume_keyword(Keyword::Set)?;
let name = Name::from_identifier(self.presume(Identifier)?);
let lexeme = name.lexeme();
+ let Some(keyword) = Keyword::from_lexeme(lexeme) else {
+ return Err(name.error(CompileErrorKind::UnknownSetting {
+ setting: name.lexeme(),
+ }));
+ };
- let set_bool: Option<Setting> = match Keyword::from_lexeme(lexeme) {
- Some(kw) => match kw {
- Keyword::AllowDuplicateRecipes => {
- Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
- }
- Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
- Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
- Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
- Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
- Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
- Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
- _ => None,
- },
- None => None,
+ let set_bool = match keyword {
+ Keyword::AllowDuplicateRecipes => {
+ Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
+ }
+ Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
+ Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
+ Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
+ Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
+ Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
+ Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
+ _ => None,
};
if let Some(value) = set_bool {
@@ -803,26 +805,22 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.expect(ColonEquals)?;
- if name.lexeme() == Keyword::Shell.lexeme() {
- Ok(Set {
- value: Setting::Shell(self.parse_shell()?),
- name,
- })
- } else if name.lexeme() == Keyword::WindowsShell.lexeme() {
- Ok(Set {
- value: Setting::WindowsShell(self.parse_shell()?),
- name,
- })
- } else if name.lexeme() == Keyword::Tempdir.lexeme() {
- Ok(Set {
- value: Setting::Tempdir(self.parse_string_literal()?.cooked),
- name,
- })
- } else {
- Err(name.error(CompileErrorKind::UnknownSetting {
- setting: name.lexeme(),
- }))
+ let set_value = match keyword {
+ Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?.cooked)),
+ Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?.cooked)),
+ Keyword::Shell => Some(Setting::Shell(self.parse_shell()?)),
+ Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?.cooked)),
+ Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_shell()?)),
+ _ => None,
+ };
+
+ if let Some(value) = set_value {
+ return Ok(Set { name, value });
}
+
+ Err(name.error(CompileErrorKind::UnknownSetting {
+ setting: name.lexeme(),
+ }))
}
/// Parse a shell setting value
diff --git a/src/setting.rs b/src/setting.rs
index 7278760a..9b2a9fe6 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -3,7 +3,9 @@ use super::*;
#[derive(Debug, Clone)]
pub(crate) enum Setting<'src> {
AllowDuplicateRecipes(bool),
+ DotenvFilename(String),
DotenvLoad(bool),
+ DotenvPath(String),
Export(bool),
Fallback(bool),
IgnoreComments(bool),
@@ -25,8 +27,8 @@ impl<'src> Display for Setting<'src> {
| Setting::PositionalArguments(value)
| Setting::WindowsPowerShell(value) => write!(f, "{value}"),
Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{shell}"),
- Setting::Tempdir(tempdir) => {
- write!(f, "{tempdir:?}")
+ Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
+ write!(f, "{value:?}")
}
}
}
diff --git a/src/settings.rs b/src/settings.rs
index 508cfb02..26765e0f 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -9,7 +9,9 @@ pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool,
+ pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_load: Option<bool>,
+ pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) export: bool,
pub(crate) fallback: bool,
pub(crate) ignore_comments: bool,
@@ -29,9 +31,15 @@ impl<'src> Settings<'src> {
Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => {
settings.allow_duplicate_recipes = allow_duplicate_recipes;
}
+ Setting::DotenvFilename(filename) => {
+ settings.dotenv_filename = Some(filename);
+ }
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = Some(dotenv_load);
}
+ Setting::DotenvPath(path) => {
+ settings.dotenv_path = Some(PathBuf::from(path));
+ }
Setting::Export(export) => {
settings.export = export;
}
diff --git a/tests/dotenv.rs b/tests/dotenv.rs
index a1182f60..3814e892 100644
--- a/tests/dotenv.rs
+++ b/tests/dotenv.rs
@@ -161,3 +161,87 @@ fn path_flag_overwrites_no_load() {
.status(EXIT_SUCCESS)
.run();
}
+
+#[test]
+fn can_set_dotenv_filename_from_justfile() {
+ Test::new()
+ .justfile(
+ r#"
+ set dotenv-filename := ".env.special"
+
+ foo:
+ @echo $NAME
+ "#,
+ )
+ .tree(tree! {
+ ".env.special": "NAME=bar"
+ })
+ .stdout("bar\n")
+ .status(EXIT_SUCCESS)
+ .run();
+}
+
+#[test]
+fn can_set_dotenv_path_from_justfile() {
+ Test::new()
+ .justfile(
+ r#"
+ set dotenv-path:= "subdir/.env"
+
+ foo:
+ @echo $NAME
+ "#,
+ )
+ .tree(tree! {
+ subdir: {
+ ".env": "NAME=bar"
+ }
+ })
+ .stdout("bar\n")
+ .status(EXIT_SUCCESS)
+ .run();
+}
+
+#[test]
+fn program_argument_has_priority_for_dotenv_filename() {
+ Test::new()
+ .justfile(
+ r#"
+ set dotenv-filename := ".env.special"
+
+ foo:
+ @echo $NAME
+ "#,
+ )
+ .tree(tree! {
+ ".env.special": "NAME=bar",
+ ".env.superspecial": "NAME=baz"
+ })
+ .args(["--dotenv-filename", ".env.superspecial"])
+ .stdout("baz\n")
+ .status(EXIT_SUCCESS)
+ .run();
+}
+
+#[test]
+fn program_argument_has_priority_for_dotenv_path() {
+ Test::new()
+ .justfile(
+ r#"
+ set dotenv-path:= "subdir/.env"
+
+ foo:
+ @echo $NAME
+ "#,
+ )
+ .tree(tree! {
+ subdir: {
+ ".env": "NAME=bar",
+ ".env.special": "NAME=baz"
+ }
+ })
+ .args(["--dotenv-path", "subdir/.env.special"])
+ .stdout("baz\n")
+ .status(EXIT_SUCCESS)
+ .run();
+}
diff --git a/tests/json.rs b/tests/json.rs
index 4e4afb1b..39e04b68 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -42,7 +42,9 @@ fn alias() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"positional_arguments": false,
@@ -74,7 +76,9 @@ fn assignment() {
"recipes": {},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -120,7 +124,9 @@ fn body() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -177,7 +183,9 @@ fn dependencies() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -271,7 +279,9 @@ fn dependency_argument() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -329,7 +339,9 @@ fn duplicate_recipes() {
},
"settings": {
"allow_duplicate_recipes": true,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -368,7 +380,9 @@ fn doc_comment() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -394,7 +408,9 @@ fn empty_justfile() {
"recipes": {},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -535,7 +551,9 @@ fn parameters() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -612,7 +630,9 @@ fn priors() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -651,7 +671,9 @@ fn private() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -690,7 +712,9 @@ fn quiet() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -710,6 +734,8 @@ fn settings() {
test(
"
set dotenv-load
+ set dotenv-filename := \"filename\"
+ set dotenv-path := \"path\"
set export
set fallback
set positional-arguments
@@ -738,7 +764,9 @@ fn settings() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": "filename",
"dotenv_load": true,
+ "dotenv_path": "path",
"export": true,
"fallback": true,
"ignore_comments": true,
@@ -783,7 +811,9 @@ fn shebang() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -822,7 +852,9 @@ fn simple() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"ignore_comments": false,
@@ -864,7 +896,9 @@ fn attribute() {
},
"settings": {
"allow_duplicate_recipes": false,
+ "dotenv_filename": null,
"dotenv_load": null,
+ "dotenv_path": null,
"export": false,
"fallback": false,
"positional_arguments": false,
diff --git a/tests/misc.rs b/tests/misc.rs
index 2c64d5d2..7d8ff0f2 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -71,10 +71,24 @@ test! {
set foo
",
stderr: "
- error: Expected ':=', but found end of line
+ error: Unknown setting `foo`
|
1 | set foo
- | ^
+ | ^^^
+ ",
+ status: EXIT_FAILURE,
+}
+
+test! {
+ name: bad_setting_with_keyword_name,
+ justfile: "
+ set if := 'foo'
+ ",
+ stderr: "
+ error: Unknown setting `if`
+ |
+ 1 | set if := 'foo'
+ | ^^
",
status: EXIT_FAILURE,
}