diff --git a/completions/just.bash b/completions/just.bash index 5691709..7a1a2d2 100644 --- a/completions/just.bash +++ b/completions/just.bash @@ -20,7 +20,7 @@ _just() { case "${cmd}" in just) - opts=" -q -u -v -e -l -h -V -f -d -c -s --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show ... " + opts=" -q -u -v -e -l -h -V -f -d -c -s --dry-run --highlight --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... " if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -97,6 +97,14 @@ _just() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --dotenv-filename) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --dotenv-path) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/completions/just.elvish b/completions/just.elvish index 70e2986..4972a56 100644 --- a/completions/just.elvish +++ b/completions/just.elvish @@ -30,6 +30,8 @@ edit:completion:arg-completer[just] = [@words]{ cand --completions 'Print shell completion script for ' cand -s 'Show information about ' cand --show 'Show information about ' + cand --dotenv-filename 'Search for environment file named instead of `.env`' + cand --dotenv-path 'Load environment file at instead of searching for one' cand --dry-run 'Print what just would do without doing it' cand --highlight 'Highlight echoed recipe lines in bold' cand --no-dotenv 'Don''t load `.env` file' diff --git a/completions/just.fish b/completions/just.fish index 48d1da5..5379aea 100644 --- a/completions/just.fish +++ b/completions/just.fish @@ -21,6 +21,8 @@ complete -c just -n "__fish_use_subcommand" -s d -l working-directory -d 'Use ' -r -f -a "zsh bash fish powershell elvish" complete -c just -n "__fish_use_subcommand" -s s -l show -d 'Show information about ' +complete -c just -n "__fish_use_subcommand" -l dotenv-filename -d 'Search for environment file named instead of `.env`' +complete -c just -n "__fish_use_subcommand" -l dotenv-path -d 'Load environment file at instead of searching for one' complete -c just -n "__fish_use_subcommand" -l dry-run -d 'Print what just would do without doing it' complete -c just -n "__fish_use_subcommand" -l highlight -d 'Highlight echoed recipe lines in bold' complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file' diff --git a/completions/just.powershell b/completions/just.powershell index 031d68b..f2be312 100644 --- a/completions/just.powershell +++ b/completions/just.powershell @@ -35,6 +35,8 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for ') [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about ') [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about ') + [CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named instead of `.env`') + [CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load environment file at instead of searching for one') [CompletionResult]::new('--dry-run', 'dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it') [CompletionResult]::new('--highlight', 'highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold') [CompletionResult]::new('--no-dotenv', 'no-dotenv', [CompletionResultType]::ParameterName, 'Don''t load `.env` file') diff --git a/completions/just.zsh b/completions/just.zsh index 25746a8..b504b70 100644 --- a/completions/just.zsh +++ b/completions/just.zsh @@ -31,6 +31,8 @@ _just() { '--completions=[Print shell completion script for ]: :(zsh bash fish powershell elvish)' \ '-s+[Show information about ]: :_just_commands' \ '--show=[Show information about ]: :_just_commands' \ +'(--dotenv-path)--dotenv-filename=[Search for environment file named instead of `.env`]' \ +'--dotenv-path=[Load environment file at instead of searching for one]' \ '(-q --quiet)--dry-run[Print what just would do without doing it]' \ '--highlight[Highlight echoed recipe lines in bold]' \ '--no-dotenv[Don'\''t load `.env` file]' \ diff --git a/src/config.rs b/src/config.rs index 85c8d9d..cc7e441 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,8 @@ pub(crate) struct Config { pub(crate) subcommand: Subcommand, pub(crate) unsorted: bool, pub(crate) unstable: bool, + pub(crate) dotenv_filename: Option, + pub(crate) dotenv_path: Option, pub(crate) verbosity: Verbosity, } @@ -96,6 +98,8 @@ mod arg { pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND"; pub(crate) const UNSORTED: &str = "UNSORTED"; pub(crate) const UNSTABLE: &str = "UNSTABLE"; + pub(crate) const DOTENV_FILENAME: &str = "DOTENV_FILENAME"; + pub(crate) const DOTENV_PATH: &str = "DOTENV_PATH"; pub(crate) const VERBOSE: &str = "VERBOSE"; pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY"; @@ -317,6 +321,19 @@ impl Config { .long("variables") .help("List names of variables"), ) + .arg( + Arg::with_name(arg::DOTENV_FILENAME) + .long("dotenv-filename") + .takes_value(true) + .help("Search for environment file named instead of `.env`") + .conflicts_with(arg::DOTENV_PATH), + ) + .arg( + Arg::with_name(arg::DOTENV_PATH) + .long("dotenv-path") + .help("Load environment file at instead of searching for one") + .takes_value(true), + ) .group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL)) .arg( Arg::with_name(arg::ARGUMENTS) @@ -537,6 +554,8 @@ impl Config { shell_args, shell_present, subcommand, + dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned), + dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from), verbosity, }) } @@ -616,6 +635,12 @@ OPTIONS: --completions Print shell completion script for [possible values: zsh, bash, fish, powershell, elvish] + --dotenv-filename + Search for environment file named instead of + `.env` + --dotenv-path + Load environment file at instead of searching for one + -f, --justfile Use as justfile --list-heading Print before list --list-prefix @@ -883,6 +908,11 @@ ARGS: verbosity: Verbosity::Quiet, } + error! { + name: dotenv_both_filename_and_path, + args: ["--dotenv-filename", "foo", "--dotenv-path", "bar"], + } + test! { name: set_default, args: [], diff --git a/src/error.rs b/src/error.rs index a60b803..94186c2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -426,7 +426,7 @@ impl<'src> ColorDisplay for Error<'src> { )?; }, Dotenv { dotenv_error } => { - write!(f, "Failed to load .env: {}", dotenv_error)?; + write!(f, "Failed to load environment file: {}", dotenv_error)?; }, EditorInvoke { editor, io_error } => { write!( diff --git a/src/load_dotenv.rs b/src/load_dotenv.rs index a91fbc2..c944926 100644 --- a/src/load_dotenv.rs +++ b/src/load_dotenv.rs @@ -1,45 +1,69 @@ use crate::common::*; +const DEFAULT_DOTENV_FILENAME: &str = ".env"; + pub(crate) fn load_dotenv( config: &Config, settings: &Settings, working_directory: &Path, ) -> 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.unwrap_or(true) { + if !settings.dotenv_load.unwrap_or(true) + && config.dotenv_filename.is_none() + && config.dotenv_path.is_none() + { return Ok(BTreeMap::new()); } + if let Some(path) = &config.dotenv_path { + return load_from_file(config, settings, &path); + } + + let filename = config + .dotenv_filename + .as_deref() + .unwrap_or(DEFAULT_DOTENV_FILENAME) + .to_owned(); + for directory in working_directory.ancestors() { - let path = directory.join(".env"); - + let path = directory.join(&filename); if path.is_file() { - if settings.dotenv_load.is_none() - && config.verbosity.loud() - && !std::env::var_os("JUST_SUPPRESS_DOTENV_LOAD_WARNING") - .map(|val| val.as_os_str().to_str() == Some("1")) - .unwrap_or(false) - { - eprintln!( - "{}", - Warning::DotenvLoad.color_display(config.color.stderr()) - ); - } - - let iter = dotenv::from_path_iter(&path)?; - let mut dotenv = BTreeMap::new(); - for result in iter { - let (key, value) = result?; - if env::var_os(&key).is_none() { - dotenv.insert(key, value); - } - } - return Ok(dotenv); + return load_from_file(config, settings, &path); } } Ok(BTreeMap::new()) } + +fn load_from_file( + config: &Config, + settings: &Settings, + path: &Path, +) -> RunResult<'static, BTreeMap> { + // `dotenv::from_path_iter` should eventually be un-deprecated, see: + // https://github.com/dotenv-rs/dotenv/issues/13 + #![allow(deprecated)] + + if config.verbosity.loud() + && settings.dotenv_load.is_none() + && config.dotenv_filename.is_none() + && config.dotenv_path.is_none() + && !std::env::var_os("JUST_SUPPRESS_DOTENV_LOAD_WARNING") + .map(|val| val.as_os_str().to_str() == Some("1")) + .unwrap_or(false) + { + eprintln!( + "{}", + Warning::DotenvLoad.color_display(config.color.stderr()) + ); + } + + let iter = dotenv::from_path_iter(&path)?; + let mut dotenv = BTreeMap::new(); + for result in iter { + let (key, value) = result?; + if env::var_os(&key).is_none() { + dotenv.insert(key, value); + } + } + Ok(dotenv) +} diff --git a/tests/dotenv.rs b/tests/dotenv.rs index 666a04b..6078268 100644 --- a/tests/dotenv.rs +++ b/tests/dotenv.rs @@ -101,3 +101,103 @@ echo $DOTENV_KEY .suppress_dotenv_load_warning(false) .run(); } + +#[test] +fn path_not_found() { + Test::new() + .justfile( + " + foo: + echo $NAME + ", + ) + .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) + .run(); +} + +#[test] +fn path_resolves() { + Test::new() + .justfile( + " + foo: + @echo $NAME + ", + ) + .tree(tree! { + subdir: { + ".env": "NAME=bar" + } + }) + .args(&["--dotenv-path", "subdir/.env"]) + .stdout("bar\n") + .status(EXIT_SUCCESS) + .run(); +} + +#[test] +fn filename_resolves() { + Test::new() + .justfile( + " + foo: + @echo $NAME + ", + ) + .tree(tree! { + ".env.special": "NAME=bar" + }) + .args(&["--dotenv-filename", ".env.special"]) + .stdout("bar\n") + .status(EXIT_SUCCESS) + .run(); +} + +#[test] +fn filename_flag_overwrites_no_load() { + Test::new() + .justfile( + " + set dotenv-load := false + + foo: + @echo $NAME + ", + ) + .tree(tree! { + ".env.special": "NAME=bar" + }) + .args(&["--dotenv-filename", ".env.special"]) + .stdout("bar\n") + .status(EXIT_SUCCESS) + .run(); +} + +#[test] +fn path_flag_overwrites_no_load() { + Test::new() + .justfile( + " + set dotenv-load := false + + foo: + @echo $NAME + ", + ) + .tree(tree! { + subdir: { + ".env": "NAME=bar" + } + }) + .args(&["--dotenv-path", "subdir/.env"]) + .stdout("bar\n") + .status(EXIT_SUCCESS) + .run(); +}