Skip to content

Commit

Permalink
new: Improve env substitution with flags.
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj committed Sep 6, 2024
1 parent 673d833 commit 27b1b74
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 13 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

#### 🚀 Updates

- Updated environment variable substitution to support different outputs when a variable is missing,
based on a trailing flag syntax.
- `$FOO` or `${FOO}` - If variable is missing, keeps the original syntax (current default).
- `$FOO?` or `${FOO?}` - If variable is missing, replaces with an empty string.
- `$FOO!` or `${FOO!}` - Ignores variable substitution and preserves the syntax (without !).

#### ⚙️ Internal

- Updated Rust to v1.81.
Expand Down
10 changes: 8 additions & 2 deletions crates/config/src/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ macro_rules! pattern {
pattern!(ENV_VAR, "\\$([A-Z0-9_]+)"); // $ENV_VAR
pattern!(ENV_VAR_DISTINCT, "^\\$([A-Z0-9_]+)$"); // $ENV_VAR
pattern!(ENV_VAR_GLOB_DISTINCT, "^\\$([A-Z0-9_*]+)$"); // $ENV_*
pattern!(ENV_VAR_SUBSTITUTE, "\\$(?:\\{([A-Z0-9_]+)\\}|([A-Z0-9_]+))"); // $ENV_VAR, ${ENV_VAR}
pattern!(ENV_VAR_SUBSTITUTE_STRICT, "\\$\\{([A-Z0-9_]+)\\}"); // ${ENV_VAR}
pattern!(
ENV_VAR_SUBSTITUTE,
"(?:\\$\\{(?P<name1>[A-Z0-9_]+)(?P<flag1>[!?]{1})?\\})|(?:\\$(?P<name2>[A-Z0-9_]+)(?P<flag2>[!?]{1})?)"
); // $ENV_VAR, ${ENV_VAR}
pattern!(
ENV_VAR_SUBSTITUTE_STRICT,
"\\$\\{(?P<name>[A-Z0-9_]+)(?P<flag>[!?]{1})?\\}"
); // ${ENV_VAR}

// Task tokens

Expand Down
79 changes: 68 additions & 11 deletions crates/project-expander/src/expander_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,87 @@ pub fn substitute_env_var(
patterns::ENV_VAR_SUBSTITUTE.replace_all(
value,
|caps: &patterns::Captures| {
// First with wrapping {} then without: ${FOO} -> $FOO
let name = caps.get(1).or_else(|| caps.get(2)).unwrap().as_str();
let Some(name) = caps.name("name1")
.or_else(|| caps.name("name2"))
.map(|cap| cap.as_str())
else {
return String::new();
};

let flag = caps.name("flag1").or_else(|| caps.name("flag2")).map(|cap| cap.as_str());

// If the variable is referencing itself, don't pull
// from the local map, and instead only pull from the
// system environment. Otherwise we hit recursion!
let maybe_var = if !base_name.is_empty() && base_name == name {
env::var(name).ok()
} else {
env_map.get(name).cloned().or_else(|| env::var(name).ok())
let get_replacement_value = || {
if !base_name.is_empty() && base_name == name {
env::var(name).ok()
} else {
env_map.get(name).cloned().or_else(|| env::var(name).ok())
}
};

match maybe_var {
Some(var) => var,
None => {
match flag {
// Don't substitute
Some("!") => {
format!("${name}")
},
// Substitute with empty string when missing
Some("?") =>{
debug!(
"Task value `{}` contains the environment variable ${}, but this variable is not set. Replacing with an empty value.",
value,
name
);

get_replacement_value().unwrap_or_default()
},
// Substitute with self when missing
_ => {
debug!(
"Task value `{}` contains the environment variable ${}, but this variable is not set. Not substituting and keeping as-is.",
"Task value `{}` contains the environment variable ${}, but this variable is not set. Not substituting and keeping as-is. Append with ? or ! to change outcome.",
value,
name
);

caps.get(0).unwrap().as_str().to_owned()
get_replacement_value()
.unwrap_or_else(|| caps.get(0).unwrap().as_str().to_owned())
}
}
})
.to_string()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn handles_flags_when_missing() {
let envs = FxHashMap::default();

assert_eq!(substitute_env_var("", "$KEY", &envs), "$KEY");
assert_eq!(substitute_env_var("", "${KEY}", &envs), "${KEY}");

assert_eq!(substitute_env_var("", "$KEY!", &envs), "$KEY");
assert_eq!(substitute_env_var("", "${KEY!}", &envs), "$KEY");

assert_eq!(substitute_env_var("", "$KEY?", &envs), "");
assert_eq!(substitute_env_var("", "${KEY?}", &envs), "");
}

#[test]
fn handles_flags_when_not_missing() {
let mut envs = FxHashMap::default();
envs.insert("KEY".to_owned(), "value".to_owned());

assert_eq!(substitute_env_var("", "$KEY", &envs), "value");
assert_eq!(substitute_env_var("", "${KEY}", &envs), "value");

assert_eq!(substitute_env_var("", "$KEY!", &envs), "$KEY");
assert_eq!(substitute_env_var("", "${KEY!}", &envs), "$KEY");

assert_eq!(substitute_env_var("", "$KEY?", &envs), "value");
assert_eq!(substitute_env_var("", "${KEY?}", &envs), "value");
}
}

0 comments on commit 27b1b74

Please sign in to comment.