Add dotenv-filename and dotenv-path settings (#1692)

This commit is contained in:
Laurent Fourrier 2023-10-12 07:04:46 +02:00 committed by GitHub
parent d0c87c8ccd
commit 812e1ea3cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 233 additions and 89 deletions

View File

@ -60,7 +60,9 @@ assignment : NAME ':=' expression eol
export : 'export' assignment export : 'export' assignment
setting : 'set' 'allow-duplicate-recipes' boolean? setting : 'set' 'allow-duplicate-recipes' boolean?
| 'set' 'dotenv-filename' ':=' string
| 'set' 'dotenv-load' boolean? | 'set' 'dotenv-load' boolean?
| 'set' 'dotenv-path' ':=' string
| 'set' 'export' boolean? | 'set' 'export' boolean?
| 'set' 'fallback' boolean? | 'set' 'fallback' boolean?
| 'set' 'ignore-comments' boolean? | 'set' 'ignore-comments' boolean?

View File

@ -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. - 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). - Recipes can be [listed from the command line](#listing-available-recipes).
@ -669,7 +669,9 @@ foo:
| Name | Value | Default | Description | | Name | Value | Default | Description |
| ------------------------- | ------------------ | ------- |---------------------------------------------------------------------------------------------- | | ------------------------- | ------------------ | ------- |---------------------------------------------------------------------------------------------- |
| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. | | `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-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. | | `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. | | `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 `#`. | | `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
@ -710,9 +712,41 @@ $ just foo
bar 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 #### Export
@ -878,36 +912,6 @@ Available recipes:
test # test stuff 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 and Substitution
Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported: Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported:
@ -1528,9 +1532,6 @@ print_home_folder:
$ just $ just
HOME is '/home/myuser' 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 #### Setting `just` Variables from Environment Variables

View File

@ -6,10 +6,6 @@ alias t := test
alias c := check alias c := check
bt := '0'
export RUST_BACKTRACE := bt
log := "warn" log := "warn"
export JUST_LOG := log export JUST_LOG := log

View File

@ -5,7 +5,9 @@ use super::*;
pub(crate) enum Keyword { pub(crate) enum Keyword {
Alias, Alias,
AllowDuplicateRecipes, AllowDuplicateRecipes,
DotenvFilename,
DotenvLoad, DotenvLoad,
DotenvPath,
Else, Else,
Export, Export,
Fallback, Fallback,

View File

@ -7,25 +7,28 @@ pub(crate) fn load_dotenv(
settings: &Settings, settings: &Settings,
working_directory: &Path, working_directory: &Path,
) -> RunResult<'static, BTreeMap<String, String>> { ) -> RunResult<'static, BTreeMap<String, String>> {
if !settings.dotenv_load.unwrap_or(false) let dotenv_filename = config
&& config.dotenv_filename.is_none() .dotenv_filename
&& config.dotenv_path.is_none() .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()); return Ok(BTreeMap::new());
} }
if let Some(path) = &config.dotenv_path { if let Some(path) = dotenv_path {
return load_from_file(path); return load_from_file(path);
} }
let filename = config let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str());
.dotenv_filename
.as_deref()
.unwrap_or(DEFAULT_DOTENV_FILENAME)
.to_owned();
for directory in working_directory.ancestors() { for directory in working_directory.ancestors() {
let path = directory.join(filename.as_str()); let path = directory.join(filename);
if path.is_file() { if path.is_file() {
return load_from_file(&path); return load_from_file(&path);
} }

View File

@ -249,7 +249,7 @@ impl<'src> Node<'src> for Set<'src> {
set.push_mut(Tree::string(&argument.cooked)); 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)); set.push_mut(Tree::string(value));
} }
} }

View File

@ -780,9 +780,13 @@ impl<'tokens, 'src> Parser<'tokens, '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(); 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) { let set_bool = match keyword {
Some(kw) => match kw {
Keyword::AllowDuplicateRecipes => { Keyword::AllowDuplicateRecipes => {
Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?)) Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
} }
@ -793,8 +797,6 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)), Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)), Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
_ => None, _ => None,
},
None => None,
}; };
if let Some(value) = set_bool { if let Some(value) = set_bool {
@ -803,27 +805,23 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.expect(ColonEquals)?; self.expect(ColonEquals)?;
if name.lexeme() == Keyword::Shell.lexeme() { let set_value = match keyword {
Ok(Set { Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?.cooked)),
value: Setting::Shell(self.parse_shell()?), Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?.cooked)),
name, Keyword::Shell => Some(Setting::Shell(self.parse_shell()?)),
}) Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?.cooked)),
} else if name.lexeme() == Keyword::WindowsShell.lexeme() { Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_shell()?)),
Ok(Set { _ => None,
value: Setting::WindowsShell(self.parse_shell()?), };
name,
}) if let Some(value) = set_value {
} else if name.lexeme() == Keyword::Tempdir.lexeme() { return Ok(Set { name, value });
Ok(Set { }
value: Setting::Tempdir(self.parse_string_literal()?.cooked),
name,
})
} else {
Err(name.error(CompileErrorKind::UnknownSetting { Err(name.error(CompileErrorKind::UnknownSetting {
setting: name.lexeme(), setting: name.lexeme(),
})) }))
} }
}
/// Parse a shell setting value /// Parse a shell setting value
fn parse_shell(&mut self) -> CompileResult<'src, Shell<'src>> { fn parse_shell(&mut self) -> CompileResult<'src, Shell<'src>> {

View File

@ -3,7 +3,9 @@ use super::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) enum Setting<'src> { pub(crate) enum Setting<'src> {
AllowDuplicateRecipes(bool), AllowDuplicateRecipes(bool),
DotenvFilename(String),
DotenvLoad(bool), DotenvLoad(bool),
DotenvPath(String),
Export(bool), Export(bool),
Fallback(bool), Fallback(bool),
IgnoreComments(bool), IgnoreComments(bool),
@ -25,8 +27,8 @@ impl<'src> Display for Setting<'src> {
| Setting::PositionalArguments(value) | Setting::PositionalArguments(value)
| Setting::WindowsPowerShell(value) => write!(f, "{value}"), | Setting::WindowsPowerShell(value) => write!(f, "{value}"),
Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{shell}"), Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{shell}"),
Setting::Tempdir(tempdir) => { Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
write!(f, "{tempdir:?}") write!(f, "{value:?}")
} }
} }
} }

View File

@ -9,7 +9,9 @@ pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub(crate) struct Settings<'src> { pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool, pub(crate) allow_duplicate_recipes: bool,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_load: Option<bool>, pub(crate) dotenv_load: Option<bool>,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) export: bool, pub(crate) export: bool,
pub(crate) fallback: bool, pub(crate) fallback: bool,
pub(crate) ignore_comments: bool, pub(crate) ignore_comments: bool,
@ -29,9 +31,15 @@ impl<'src> Settings<'src> {
Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => { Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => {
settings.allow_duplicate_recipes = allow_duplicate_recipes; settings.allow_duplicate_recipes = allow_duplicate_recipes;
} }
Setting::DotenvFilename(filename) => {
settings.dotenv_filename = Some(filename);
}
Setting::DotenvLoad(dotenv_load) => { Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = Some(dotenv_load); settings.dotenv_load = Some(dotenv_load);
} }
Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path));
}
Setting::Export(export) => { Setting::Export(export) => {
settings.export = export; settings.export = export;
} }

View File

@ -161,3 +161,87 @@ fn path_flag_overwrites_no_load() {
.status(EXIT_SUCCESS) .status(EXIT_SUCCESS)
.run(); .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();
}

View File

@ -42,7 +42,9 @@ fn alias() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"positional_arguments": false, "positional_arguments": false,
@ -74,7 +76,9 @@ fn assignment() {
"recipes": {}, "recipes": {},
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -120,7 +124,9 @@ fn body() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -177,7 +183,9 @@ fn dependencies() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -271,7 +279,9 @@ fn dependency_argument() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -329,7 +339,9 @@ fn duplicate_recipes() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": true, "allow_duplicate_recipes": true,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -368,7 +380,9 @@ fn doc_comment() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -394,7 +408,9 @@ fn empty_justfile() {
"recipes": {}, "recipes": {},
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -535,7 +551,9 @@ fn parameters() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -612,7 +630,9 @@ fn priors() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -651,7 +671,9 @@ fn private() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -690,7 +712,9 @@ fn quiet() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -710,6 +734,8 @@ fn settings() {
test( test(
" "
set dotenv-load set dotenv-load
set dotenv-filename := \"filename\"
set dotenv-path := \"path\"
set export set export
set fallback set fallback
set positional-arguments set positional-arguments
@ -738,7 +764,9 @@ fn settings() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": "filename",
"dotenv_load": true, "dotenv_load": true,
"dotenv_path": "path",
"export": true, "export": true,
"fallback": true, "fallback": true,
"ignore_comments": true, "ignore_comments": true,
@ -783,7 +811,9 @@ fn shebang() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -822,7 +852,9 @@ fn simple() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -864,7 +896,9 @@ fn attribute() {
}, },
"settings": { "settings": {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"dotenv_filename": null,
"dotenv_load": null, "dotenv_load": null,
"dotenv_path": null,
"export": false, "export": false,
"fallback": false, "fallback": false,
"positional_arguments": false, "positional_arguments": false,

View File

@ -71,10 +71,24 @@ test! {
set foo set foo
", ",
stderr: " stderr: "
error: Expected ':=', but found end of line error: Unknown setting `foo`
| |
1 | set 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, status: EXIT_FAILURE,
} }