Repository: Canop/bacon (language: rust)

Issue:
[feature request] Expand env vars in job command
I use env vars as arguments to my executable so I would like support for expanding them before being passed to the executable:

```toml
[jobs.run]
command = ["cargo", "run", "--", "$MY_VAR"]
```

`$MY_VAR` should be expanded before being passed to `cargo run`.


Discussion / hints:
This looks reasonnable, at least when the command is provided as an array.

Now, is there any case when we don't want to interpret an argument starting with a `$` as an env var ?
> Now, is there any case when we don't want to interpret an argument starting with a `$` as an env var ?

I don't think so, but we could support that kind of behavior by escaping the `$` (e.g. `\$MY_VAR`).

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d18a82e..65fd891 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+### next
+- expand env vars in job command unless the job specifies `expand_env_vars = false` - Fix #181
+
 <a name="v2.16.0"></a>
 ### v2.16.0 - 2024/03/30
 - `on_success` triggered with warning or errors depending on `allow_warnings` and `allow_failures` - Fix #179
diff --git a/Cargo.lock b/Cargo.lock
index ef202a1..68ebb69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -125,7 +125,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "bacon"
-version = "2.16.0"
+version = "2.16.1-dev"
 dependencies = [
  "anyhow",
  "cargo_metadata",
diff --git a/Cargo.toml b/Cargo.toml
index 2c5daa9..231f0f1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bacon"
-version = "2.16.0"
+version = "2.16.1-dev"
 authors = ["dystroy <denys.seguret@gmail.com>"]
 repository = "https://github.com/Canop/bacon"
 description = "background rust compiler"
diff --git a/bacon.toml b/bacon.toml
index 7f066a3..5243f54 100644
--- a/bacon.toml
+++ b/bacon.toml
@@ -4,20 +4,22 @@ default_job = "check-all"
 
 [jobs]
 
-[jobs.check-all]
-command = ["cargo", "check", "--all-targets", "--color", "always"]
-need_stdout = false
-watch = ["tests", "benches", "examples"]
-
 [jobs.check]
 command = [
 	"cargo", "check",
 	"--color", "always",
-	"--features", "clipboard",
 ]
 need_stdout = false
 watch = ["benches"]
 
+[jobs.check-all]
+command = [
+	"cargo", "check",
+	"--all-targets",
+	"--color", "always",
+]
+need_stdout = false
+watch = ["tests", "benches", "examples"]
 
 [jobs.win]
 command = [
@@ -25,7 +27,6 @@ command = [
 	"--target", "x86_64-pc-windows-gnu",
 ]
 
-
 [jobs.test]
 command = [
     "cargo", "test", "--color", "always",
diff --git a/src/job.rs b/src/job.rs
index d13c3ae..fb4f12d 100644
--- a/src/job.rs
+++ b/src/job.rs
@@ -7,6 +7,29 @@ use {
 /// One of the possible job that bacon can run
 #[derive(Debug, Clone, Deserialize)]
 pub struct Job {
+    /// Whether to consider that we can have a success
+    /// when we have test failures
+    #[serde(default)]
+    pub allow_failures: bool,
+
+    /// Whether to consider that we can have a success
+    /// when we have warnings. This is especially useful
+    /// for "cargo run" jobs
+    #[serde(default)]
+    pub allow_warnings: bool,
+
+    /// Whether gitignore rules must be applied
+    pub apply_gitignore: Option<bool>,
+
+    /// Whether to wait for the computation to finish before
+    /// to display it on screen
+    ///
+    /// This is true by default. Set it to false if you want
+    /// the previous computation result to be replaced with
+    /// the new one as soon as it starts.
+    #[serde(default = "default_true")]
+    pub background: bool,
+
     /// The tokens making the command to execute (first one
     /// is the executable).
     /// This vector is guaranteed not empty
@@ -22,12 +45,13 @@ pub struct Job {
     #[serde(default = "default_true")]
     pub default_watch: bool,
 
-    /// A list of directories that will be watched if the job
-    /// is run on a package.
-    /// src, examples, tests, and benches are implicitly included
-    /// unless you `set default_watch` to false.
+    /// Env vars to set for this job execution
     #[serde(default)]
-    pub watch: Vec<String>,
+    pub env: HashMap<String, String>,
+
+    /// Whether to expand environment variables in the command
+    #[serde(default = "default_true")]
+    pub expand_env_vars: bool,
 
     /// Whether we need to capture stdout too (stderr is
     /// always captured)
@@ -39,32 +63,12 @@ pub struct Job {
     #[serde(default)]
     pub on_success: Option<Action>,
 
-    /// Whether to consider that we can have a success
-    /// when we have warnings. This is especially useful
-    /// for "cargo run" jobs
-    #[serde(default)]
-    pub allow_warnings: bool,
-
-    /// Whether to consider that we can have a success
-    /// when we have test failures
-    #[serde(default)]
-    pub allow_failures: bool,
-
-    /// Whether gitignore rules must be applied
-    pub apply_gitignore: Option<bool>,
-
-    /// Env vars to set for this job execution
+    /// A list of directories that will be watched if the job
+    /// is run on a package.
+    /// src, examples, tests, and benches are implicitly included
+    /// unless you `set default_watch` to false.
     #[serde(default)]
-    pub env: HashMap<String, String>,
-
-    /// Whether to wait for the computation to finish before
-    /// to display it on screen
-    ///
-    /// This is true by default. Set it to false if you want
-    /// the previous computation result to be replaced with
-    /// the new one as soon as it starts.
-    #[serde(default = "default_true")]
-    pub background: bool,
+    pub watch: Vec<String>,
 }
 
 static DEFAULT_ARGS: &[&str] = &["--color", "always"];
@@ -93,6 +97,7 @@ impl Job {
         Self {
             command,
             default_watch: true,
+            expand_env_vars: true,
             watch: Vec::new(),
             need_stdout: false,
             on_success: None,
diff --git a/src/mission.rs b/src/mission.rs
index c52b155..139b245 100644
--- a/src/mission.rs
+++ b/src/mission.rs
@@ -1,6 +1,7 @@
 use {
     crate::*,
     anyhow::Result,
+    lazy_regex::regex_replace_all,
     notify::{
         RecommendedWatcher,
         RecursiveMode,
@@ -151,7 +152,30 @@ impl<'s> Mission<'s> {
 
     /// build (and doesn't call) the external cargo command
     pub fn get_command(&self) -> Command {
-        let mut tokens = self.job.command.iter();
+        let expanded;
+        let command = if self.job.expand_env_vars {
+            expanded = self
+                .job
+                .command
+                .iter()
+                .map(|token| {
+                    regex_replace_all!(r"\$([A-Z0-9a-z_]+)", token, |whole: &str, name| {
+                        match std::env::var(name) {
+                            Ok(value) => value,
+                            Err(_) => {
+                                warn!("variable {whole} not found in env");
+                                whole.to_string()
+                            }
+                        }
+                    })
+                    .to_string()
+                })
+                .collect();
+            &expanded
+        } else {
+            &self.job.command
+        };
+        let mut tokens = command.iter();
         let mut command = Command::new(
             tokens.next().unwrap(), // implies a check in the job
         );


Test patch: