Repository: casey/just (rust)

Issue:
Option for .env variables to override existing
I have run into a use case were I would like the variables defined in a `.env` file to override
any already defined variables.

The case is in my nix dotfiles configuration. I would like the option to override the `$USER` env
variable when I build and install the configuration depending on the machine I am installing it on.

Having a setting in the justfile would be helpful

```
# Controls whether the variables defined in a dotenv file will override already defined variables
set dotenv-override := true
```

(name `dotenv-override` in similar ideas in #469)

Pull request:
Add dotenv-override setting
Closes #733

It's maybe too naive? I added some basic tests, but can you think of other edge cases I should test?
I also need to add some documentation I guess.

Patch:
diff --git a/README.md b/README.md
index 908eadf9..2d298a08 100644
--- a/README.md
+++ b/README.md
@@ -989,6 +989,7 @@ foo:
 | `allow-duplicate-variables` | boolean | `false` | Allow variables appearing later in a `justfile` to override earlier variables 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-override` | boolean | `false` | Override existing environment variables with values from the `.env` file. |
 | `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. |
@@ -1060,8 +1061,9 @@ bar
 
 #### Dotenv Settings
 
-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 any of `dotenv-load`, `dotenv-filename`, `dotenv-override`, `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, which
 may be absolute, or relative to the working directory.
@@ -1086,6 +1088,9 @@ It is not an error if an environment file is not found, unless
 The loaded variables are environment variables, not `just` variables, and so
 must be accessed using `$VARIABLE_NAME` in recipes and backticks.
 
+If `dotenv-override` is set, variables from the environment file will override
+existing environment variables.
+
 For example, if your `.env` file contains:
 
 ```console
@@ -4452,7 +4457,9 @@ and checking the program's stdout, stderr, and exit code .
 
 5. Implement the feature.
 
-6. Run `just ci` to make sure that all tests, lints, and checks pass.
+6. Run `just ci` to make sure that all tests, lints, and checks pass. Requires
+   [mdBook](https://github.com/rust-lang/mdBook) and
+   [mdbook-linkcheck](https://github.com/Michael-F-Bryan/mdbook-linkcheck).
 
 7. Open a PR with the new code that is editable by maintainers. PRs often
    require rebasing and minor tweaks. If the PR is not editable by maintainers,
diff --git a/src/keyword.rs b/src/keyword.rs
index 41c9aaa7..7cae3606 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -9,6 +9,7 @@ pub(crate) enum Keyword {
   Assert,
   DotenvFilename,
   DotenvLoad,
+  DotenvOverride,
   DotenvPath,
   DotenvRequired,
   Else,
diff --git a/src/load_dotenv.rs b/src/load_dotenv.rs
index 29b31b24..5988eb79 100644
--- a/src/load_dotenv.rs
+++ b/src/load_dotenv.rs
@@ -16,9 +16,10 @@ pub(crate) fn load_dotenv(
     .or(settings.dotenv_path.as_ref());
 
   if !settings.dotenv_load
+    && !settings.dotenv_override
+    && !settings.dotenv_required
     && dotenv_filename.is_none()
     && dotenv_path.is_none()
-    && !settings.dotenv_required
   {
     return Ok(BTreeMap::new());
   }
@@ -26,7 +27,7 @@ pub(crate) fn load_dotenv(
   if let Some(path) = dotenv_path {
     let path = working_directory.join(path);
     if path.is_file() {
-      return load_from_file(&path);
+      return load_from_file(&path, settings);
     }
   }
 
@@ -35,7 +36,7 @@ pub(crate) fn load_dotenv(
   for directory in working_directory.ancestors() {
     let path = directory.join(filename);
     if path.is_file() {
-      return load_from_file(&path);
+      return load_from_file(&path, settings);
     }
   }
 
@@ -46,12 +47,15 @@ pub(crate) fn load_dotenv(
   }
 }
 
-fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap<String, String>> {
+fn load_from_file(
+  path: &Path,
+  settings: &Settings,
+) -> RunResult<'static, BTreeMap<String, String>> {
   let iter = dotenvy::from_path_iter(path)?;
   let mut dotenv = BTreeMap::new();
   for result in iter {
     let (key, value) = result?;
-    if env::var_os(&key).is_none() {
+    if settings.dotenv_override || env::var_os(&key).is_none() {
       dotenv.insert(key, value);
     }
   }
diff --git a/src/node.rs b/src/node.rs
index 64607824..2a1db705 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -303,6 +303,7 @@ impl<'src> Node<'src> for Set<'src> {
       Setting::AllowDuplicateRecipes(value)
       | Setting::AllowDuplicateVariables(value)
       | Setting::DotenvLoad(value)
+      | Setting::DotenvOverride(value)
       | Setting::DotenvRequired(value)
       | Setting::Export(value)
       | Setting::Fallback(value)
diff --git a/src/parser.rs b/src/parser.rs
index fe00a336..3d0e3848 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1119,6 +1119,7 @@ impl<'run, 'src> Parser<'run, 'src> {
         Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?))
       }
       Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
+      Keyword::DotenvOverride => Some(Setting::DotenvOverride(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()?)),
diff --git a/src/setting.rs b/src/setting.rs
index f187f418..211ba9d3 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -6,6 +6,7 @@ pub(crate) enum Setting<'src> {
   AllowDuplicateVariables(bool),
   DotenvFilename(StringLiteral<'src>),
   DotenvLoad(bool),
+  DotenvOverride(bool),
   DotenvPath(StringLiteral<'src>),
   DotenvRequired(bool),
   Export(bool),
@@ -29,6 +30,7 @@ impl Display for Setting<'_> {
       Self::AllowDuplicateRecipes(value)
       | Self::AllowDuplicateVariables(value)
       | Self::DotenvLoad(value)
+      | Self::DotenvOverride(value)
       | Self::DotenvRequired(value)
       | Self::Export(value)
       | Self::Fallback(value)
diff --git a/src/settings.rs b/src/settings.rs
index 6ab33353..1b352d61 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -11,6 +11,7 @@ pub(crate) struct Settings<'src> {
   pub(crate) allow_duplicate_variables: bool,
   pub(crate) dotenv_filename: Option<String>,
   pub(crate) dotenv_load: bool,
+  pub(crate) dotenv_override: bool,
   pub(crate) dotenv_path: Option<PathBuf>,
   pub(crate) dotenv_required: bool,
   pub(crate) export: bool,
@@ -50,6 +51,9 @@ impl<'src> Settings<'src> {
         Setting::DotenvPath(path) => {
           settings.dotenv_path = Some(PathBuf::from(path.cooked));
         }
+        Setting::DotenvOverride(dotenv_overrride) => {
+          settings.dotenv_override = dotenv_overrride;
+        }
         Setting::DotenvRequired(dotenv_required) => {
           settings.dotenv_required = dotenv_required;
         }


Test patch:
diff --git a/tests/dotenv.rs b/tests/dotenv.rs
index 08c4ed7a..4b45a561 100644
--- a/tests/dotenv.rs
+++ b/tests/dotenv.rs
@@ -362,7 +362,7 @@ fn no_dotenv() {
 }
 
 #[test]
-fn dotenv_env_var_override() {
+fn dotenv_env_var_default_no_override() {
   Test::new()
     .justfile(
       "
@@ -377,6 +377,41 @@ fn dotenv_env_var_override() {
     .run();
 }
 
+#[test]
+fn dotenv_env_var_override() {
+  Test::new()
+    .justfile(
+      "
+        set dotenv-load
+        set dotenv-override := true
+        echo:
+          echo $DOTENV_KEY
+      ",
+    )
+    .write(".env", "DOTENV_KEY=dotenv-value")
+    .env("DOTENV_KEY", "not-the-dotenv-value")
+    .stdout("dotenv-value\n")
+    .stderr("echo $DOTENV_KEY\n")
+    .run();
+}
+
+#[test]
+fn dotenv_env_var_override_no_load() {
+  Test::new()
+    .justfile(
+      "
+        set dotenv-override := true
+        echo:
+          echo $DOTENV_KEY
+      ",
+    )
+    .write(".env", "DOTENV_KEY=dotenv-value")
+    .env("DOTENV_KEY", "not-the-dotenv-value")
+    .stdout("dotenv-value\n")
+    .stderr("echo $DOTENV_KEY\n")
+    .run();
+}
+
 #[test]
 fn dotenv_path_usable_from_subdir() {
   Test::new()
diff --git a/tests/json.rs b/tests/json.rs
index 30f713be..879c6bb2 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -79,6 +79,7 @@ struct Settings<'a> {
   allow_duplicate_variables: bool,
   dotenv_filename: Option<&'a str>,
   dotenv_load: bool,
+  dotenv_override: bool,
   dotenv_path: Option<&'a str>,
   dotenv_required: bool,
   export: bool,