Add set dotenv-required to require an environment file (#2116)

This commit is contained in:
Casey Rodarmor 2024-05-30 18:12:07 -05:00 committed by GitHub
parent d38c1add13
commit f2201d8684
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 276 additions and 190 deletions

View File

@ -812,6 +812,7 @@ foo:
| `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 and error if not present. Overrides `dotenv-filename`. |
| `dotenv-required` | boolean | `false` | Error if a `.env` file isn't found. |
| `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 `#`. |
@ -877,17 +878,25 @@ bar
#### Dotenv Settings
If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load
environment variables from a file.
If any of `dotenv-load`, `dotenv-filename`, `dotenv-path`, or `dotenv-required`
are set, `just` will try to load environment variables from a file.
If `dotenv-path` is set, `just` will look for a file at the given path. It is
an error if a dotenv file is not found at `dotenv-path`, but not an error if a
dotenv file is not found with `dotenv-filename`.
If `dotenv-path` is set, `just` will look for a file at the given path, which
may be absolute, or relative to the working directory.
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.
If `dotenv-filename` is set `just` will look for a file at the given path,
relative to the working directory and each of its ancestors.
If `dotenv-filename` is not set, but `dotenv-load` or `dotenv-required` are
set, just will look for a file named `.env`, relative to the working directory
and each of its ancestors.
`dotenv-filename` and `dotenv-path` and similar, but `dotenv-path` is only
checked relative to the working directory, whereas `dotenv-filename` is checked
relative to the working directory and each of its ancestors.
It is not an error if an environment file is not found, unless
`dotenv-required` is set.
The loaded variables are environment variables, not `just` variables, and so
must be accessed using `$VARIABLE_NAME` in recipes and backticks.

View File

@ -79,6 +79,7 @@ pub(crate) enum Error<'src> {
Dotenv {
dotenv_error: dotenvy::Error,
},
DotenvRequired,
DumpJson {
serde_json_error: serde_json::Error,
},
@ -347,6 +348,9 @@ impl<'src> ColorDisplay for Error<'src> {
Dotenv { dotenv_error } => {
write!(f, "Failed to load environment file: {dotenv_error}")?;
}
DotenvRequired => {
write!(f, "Dotenv file not found")?;
}
DumpJson { serde_json_error } => {
write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?;
}

View File

@ -10,6 +10,7 @@ pub(crate) enum Keyword {
DotenvFilename,
DotenvLoad,
DotenvPath,
DotenvRequired,
Else,
Export,
Fallback,

View File

@ -1,7 +1,5 @@
use super::*;
const DEFAULT_DOTENV_FILENAME: &str = ".env";
pub(crate) fn load_dotenv(
config: &Config,
settings: &Settings,
@ -17,16 +15,21 @@ pub(crate) fn load_dotenv(
.as_ref()
.or(settings.dotenv_path.as_ref());
if !settings.dotenv_load.unwrap_or_default() && dotenv_filename.is_none() && dotenv_path.is_none()
if !settings.dotenv_load
&& dotenv_filename.is_none()
&& dotenv_path.is_none()
&& !settings.dotenv_required
{
return Ok(BTreeMap::new());
}
if let Some(path) = dotenv_path {
if path.is_file() {
return load_from_file(&working_directory.join(path));
}
}
let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str());
let filename = dotenv_filename.map_or(".env", |s| s.as_str());
for directory in working_directory.ancestors() {
let path = directory.join(filename);
@ -35,8 +38,12 @@ pub(crate) fn load_dotenv(
}
}
if settings.dotenv_required {
Err(Error::DotenvRequired)
} else {
Ok(BTreeMap::new())
}
}
fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap<String, String>> {
let iter = dotenvy::from_path_iter(path)?;

View File

@ -284,6 +284,7 @@ impl<'src> Node<'src> for Set<'src> {
Setting::AllowDuplicateRecipes(value)
| Setting::AllowDuplicateVariables(value)
| Setting::DotenvLoad(value)
| Setting::DotenvRequired(value)
| Setting::Export(value)
| Setting::Fallback(value)
| Setting::PositionalArguments(value)

View File

@ -917,6 +917,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?))
}
Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
Keyword::DotenvRequired => Some(Setting::DotenvRequired(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()?)),

View File

@ -7,6 +7,7 @@ pub(crate) enum Setting<'src> {
DotenvFilename(String),
DotenvLoad(bool),
DotenvPath(String),
DotenvRequired(bool),
Export(bool),
Fallback(bool),
IgnoreComments(bool),
@ -24,6 +25,7 @@ impl<'src> Display for Setting<'src> {
Self::AllowDuplicateRecipes(value)
| Self::AllowDuplicateVariables(value)
| Self::DotenvLoad(value)
| Self::DotenvRequired(value)
| Self::Export(value)
| Self::Fallback(value)
| Self::IgnoreComments(value)

View File

@ -10,8 +10,9 @@ pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool,
pub(crate) allow_duplicate_variables: bool,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_load: Option<bool>,
pub(crate) dotenv_load: bool,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) dotenv_required: bool,
pub(crate) export: bool,
pub(crate) fallback: bool,
pub(crate) ignore_comments: bool,
@ -39,11 +40,14 @@ impl<'src> Settings<'src> {
settings.dotenv_filename = Some(filename);
}
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = Some(dotenv_load);
settings.dotenv_load = dotenv_load;
}
Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path));
}
Setting::DotenvRequired(dotenv_required) => {
settings.dotenv_required = dotenv_required;
}
Setting::Export(export) => {
settings.export = export;
}

View File

@ -47,16 +47,21 @@ test! {
status: 2,
}
test! {
name: env_is_loaded,
justfile: "
#[test]
fn env_is_loaded() {
Test::new()
.justfile(
"
set dotenv-load
x:
echo XYZ
",
args: ("--command", "sh", "-c", "printf $DOTENV_KEY"),
stdout: "dotenv-value",
)
.args(["--command", "sh", "-c", "printf $DOTENV_KEY"])
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value")
.run();
}
test! {

View File

@ -12,40 +12,54 @@ fn dotenv() {
.run();
}
test! {
name: set_false,
justfile: r#"
#[test]
fn set_false() {
Test::new()
.justfile(
r#"
set dotenv-load := false
foo:
@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",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("undefined\n")
.run();
}
test! {
name: set_implicit,
justfile: r#"
#[test]
fn set_implicit() {
Test::new()
.justfile(
"
set dotenv-load
foo:
echo $DOTENV_KEY
"#,
stdout: "dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}
test! {
name: set_true,
justfile: r#"
#[test]
fn set_true() {
Test::new()
.justfile(
"
set dotenv-load := true
foo:
echo $DOTENV_KEY
"#,
stdout: "dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}
#[test]
@ -57,28 +71,24 @@ fn no_warning() {
echo ${DOTENV_KEY:-unset}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("unset\n")
.stderr("echo ${DOTENV_KEY:-unset}\n")
.run();
}
#[test]
fn path_not_found() {
fn dotenv_required() {
Test::new()
.justfile(
"
set dotenv-required
foo:
echo $JUST_TEST_VARIABLE
",
)
.args(["--dotenv-path", ".env.prod"])
.stderr(if cfg!(windows) {
"error: Failed to load environment file: The system cannot find the file specified. (os \
error 2)\n"
} else {
"error: Failed to load environment file: No such file or directory (os error 2)\n"
})
.status(EXIT_FAILURE)
.stderr("error: Dotenv file not found\n")
.status(1)
.run();
}
@ -227,12 +237,12 @@ fn program_argument_has_priority_for_dotenv_filename() {
fn program_argument_has_priority_for_dotenv_path() {
Test::new()
.justfile(
r#"
set dotenv-path := "subdir/.env"
"
set dotenv-path := 'subdir/.env'
foo:
@echo $JUST_TEST_VARIABLE
"#,
",
)
.tree(tree! {
subdir: {
@ -257,8 +267,111 @@ fn dotenv_path_is_relative_to_working_directory() {
@echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.tree(tree! { subdir: { } })
.current_dir("subdir")
.stdout("dotenv-value\n")
.run();
}
#[test]
fn dotenv_variable_in_recipe() {
Test::new()
.justfile(
"
set dotenv-load
echo:
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}
#[test]
fn dotenv_variable_in_backtick() {
Test::new()
.justfile(
"
set dotenv-load
X:=`echo $DOTENV_KEY`
echo:
echo {{X}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo dotenv-value\n")
.run();
}
#[test]
fn dotenv_variable_in_function_in_recipe() {
Test::new()
.justfile(
"
set dotenv-load
echo:
echo {{env_var_or_default('DOTENV_KEY', 'foo')}}
echo {{env_var('DOTENV_KEY')}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\ndotenv-value\n")
.stderr("echo dotenv-value\necho dotenv-value\n")
.run();
}
#[test]
fn dotenv_variable_in_function_in_backtick() {
Test::new()
.justfile(
"
set dotenv-load
X:=env_var_or_default('DOTENV_KEY', 'foo')
Y:=env_var('DOTENV_KEY')
echo:
echo {{X}}
echo {{Y}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\ndotenv-value\n")
.stderr("echo dotenv-value\necho dotenv-value\n")
.run();
}
#[test]
fn no_dotenv() {
Test::new()
.justfile(
"
X:=env_var_or_default('DOTENV_KEY', 'DEFAULT')
echo:
echo {{X}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.arg("--no-dotenv")
.stdout("DEFAULT\n")
.stderr("echo DEFAULT\n")
.run();
}
#[test]
fn dotenv_env_var_override() {
Test::new()
.justfile(
"
echo:
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.env("DOTENV_KEY", "not-the-dotenv-value")
.stdout("not-the-dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}

View File

@ -46,8 +46,9 @@ fn alias() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"positional_arguments": false,
@ -84,8 +85,9 @@ fn assignment() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -136,8 +138,9 @@ fn body() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -200,8 +203,9 @@ fn dependencies() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -302,8 +306,9 @@ fn dependency_argument() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -366,8 +371,9 @@ fn duplicate_recipes() {
"allow_duplicate_recipes": true,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -408,8 +414,9 @@ fn duplicate_variables() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": true,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -453,8 +460,9 @@ fn doc_comment() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -484,8 +492,9 @@ fn empty_justfile() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -636,8 +645,9 @@ fn parameters() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -721,8 +731,9 @@ fn priors() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -766,8 +777,9 @@ fn private() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -811,8 +823,9 @@ fn quiet() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -870,6 +883,7 @@ fn settings() {
"dotenv_filename": "filename",
"dotenv_load": true,
"dotenv_path": "path",
"dotenv_required": false,
"export": true,
"fallback": true,
"ignore_comments": true,
@ -919,8 +933,9 @@ fn shebang() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -964,8 +979,9 @@ fn simple() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"ignore_comments": false,
@ -1012,8 +1028,9 @@ fn attribute() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"positional_arguments": false,
@ -1073,8 +1090,9 @@ fn module() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"positional_arguments": false,
@ -1093,8 +1111,9 @@ fn module() {
"allow_duplicate_recipes": false,
"allow_duplicate_variables": false,
"dotenv_filename": null,
"dotenv_load": null,
"dotenv_load": false,
"dotenv_path": null,
"dotenv_required": false,
"export": false,
"fallback": false,
"positional_arguments": false,

View File

@ -1589,84 +1589,6 @@ echo:
stderr: "echo 1\n",
}
test! {
name: dotenv_variable_in_recipe,
justfile: "
#
set dotenv-load
echo:
echo $DOTENV_KEY
",
stdout: "dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
}
test! {
name: dotenv_variable_in_backtick,
justfile: "
#
set dotenv-load
X:=`echo $DOTENV_KEY`
echo:
echo {{X}}
",
stdout: "dotenv-value\n",
stderr: "echo dotenv-value\n",
}
test! {
name: dotenv_variable_in_function_in_recipe,
justfile: "
#
set dotenv-load
echo:
echo {{env_var_or_default('DOTENV_KEY', 'foo')}}
echo {{env_var('DOTENV_KEY')}}
",
stdout: "dotenv-value\ndotenv-value\n",
stderr: "echo dotenv-value\necho dotenv-value\n",
}
test! {
name: dotenv_variable_in_function_in_backtick,
justfile: "
#
set dotenv-load
X:=env_var_or_default('DOTENV_KEY', 'foo')
Y:=env_var('DOTENV_KEY')
echo:
echo {{X}}
echo {{Y}}
",
stdout: "dotenv-value\ndotenv-value\n",
stderr: "echo dotenv-value\necho dotenv-value\n",
}
test! {
name: no_dotenv,
justfile: "
#
X:=env_var_or_default('DOTENV_KEY', 'DEFAULT')
echo:
echo {{X}}
",
args: ("--no-dotenv"),
stdout: "DEFAULT\n",
stderr: "echo DEFAULT\n",
}
test! {
name: dotenv_env_var_override,
justfile: "
#
echo:
echo $DOTENV_KEY
",
env: {"DOTENV_KEY": "not-the-dotenv-value",},
stdout: "not-the-dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
}
test! {
name: invalid_escape_sequence_message,
justfile: r#"

View File

@ -515,7 +515,6 @@ fn missing_optional_modules_do_not_conflict() {
#[test]
fn root_dotenv_is_available_to_submodules() {
Test::new()
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
.justfile(
"
set dotenv-load
@ -523,10 +522,10 @@ fn root_dotenv_is_available_to_submodules() {
mod foo
",
)
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
.write(".env", "DOTENV_KEY=dotenv-value")
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.args(["--unstable", "foo", "foo"])
.stdout("dotenv-value\n")
.run();
}
@ -534,10 +533,6 @@ fn root_dotenv_is_available_to_submodules() {
#[test]
fn dotenv_settings_in_submodule_are_ignored() {
Test::new()
.write(
"foo.just",
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
)
.justfile(
"
set dotenv-load
@ -545,10 +540,13 @@ fn dotenv_settings_in_submodule_are_ignored() {
mod foo
",
)
.write(
"foo.just",
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.test_round_trip(false)
.arg("--unstable")
.arg("foo")
.arg("foo")
.args(["--unstable", "foo", "foo"])
.stdout("dotenv-value\n")
.run();
}

View File

@ -82,6 +82,7 @@ fn shell_expanded_strings_can_be_used_in_settings() {
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.env("JUST_TEST_VARIABLE", ".env")
.stdout("dotenv-value\n")
.run();

View File

@ -201,9 +201,8 @@ impl Test {
} else {
self.stdout.clone()
};
let stderr = unindent(&self.stderr);
fs::write(self.tempdir.path().join(".env"), "DOTENV_KEY=dotenv-value").unwrap();
let stderr = unindent(&self.stderr);
let mut command = Command::new(executable_path("just"));