use std::collections::{BTreeMap, HashSet};
use std::process::Command;
pub use std::env::*;
pub fn var_true(key: &str) -> bool {
matches!(var(key), Ok(v) if v == "1" || v == "true")
}
pub fn apply_parsed_env(cmd: &mut Command, env: &BTreeMap<String, String>) {
for (key, val) in env {
cmd.env(key, val);
}
if env.is_empty() {
return;
}
if cfg!(windows) {
let existing = var("WSLENV").ok();
let keys = env.keys().map(String::as_str);
cmd.env("WSLENV", append_to_wslenv(existing.as_deref(), keys));
}
}
fn carries_value_verbatim(entry: &str) -> bool {
match entry.split_once('/') {
None => true,
Some((_, flags)) => !flags.is_empty() && flags.chars().all(|flag| flag == 'u'),
}
}
pub fn append_to_wslenv<'a>(
existing: Option<&str>,
keys: impl IntoIterator<Item = &'a str>,
) -> String {
let mut entries: Vec<&str> = vec![];
let mut names: HashSet<&str> = HashSet::new();
for entry in existing.unwrap_or_default().split(':') {
if entry.is_empty() {
continue;
}
if carries_value_verbatim(entry) {
names.insert(entry.split('/').next().unwrap_or(entry));
}
entries.push(entry);
}
for key in keys {
if key.is_empty() || key.contains(':') || key.contains('/') {
continue;
}
if names.insert(key) {
entries.push(key);
}
}
entries.join(":")
}
pub fn shell_var_name(shell: &str) -> String {
format!("USAGE_SHELL_{}", shell.to_ascii_uppercase())
}
pub fn shell_program_override(
shell: &str,
lookup: impl Fn(&str) -> Option<String>,
) -> Option<String> {
let value = lookup(&shell_var_name(shell))?;
let value = value.trim();
(!value.is_empty()).then(|| value.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn append(existing: Option<&str>, keys: &[&str]) -> String {
append_to_wslenv(existing, keys.iter().copied())
}
#[test]
fn wslenv_adds_keys_in_order() {
assert_eq!(append(None, &["usage_workspace"]), "usage_workspace");
assert_eq!(
append(None, &["usage_workspace", "usage_region"]),
"usage_workspace:usage_region"
);
}
#[test]
fn wslenv_appends_after_existing_entries() {
assert_eq!(append(Some("FOO"), &["usage_a"]), "FOO:usage_a");
}
#[test]
fn wslenv_leaves_existing_flags_untouched() {
assert_eq!(
append(Some("FOO/p:BAR/l"), &["usage_a"]),
"FOO/p:BAR/l:usage_a"
);
}
#[test]
fn wslenv_does_not_repeat_an_existing_name() {
assert_eq!(
append(Some("usage_a"), &["usage_a", "usage_b"]),
"usage_a:usage_b"
);
}
#[test]
fn wslenv_treats_a_direction_only_entry_as_covering_the_name() {
assert_eq!(append(Some("usage_a/u"), &["usage_a"]), "usage_a/u");
}
#[test]
fn wslenv_adds_its_own_entry_beside_one_that_would_lose_the_value() {
for flags in ["/p", "/l", "/w", "/uw"] {
let existing = format!("usage_a{flags}");
assert_eq!(
append(Some(&existing), &["usage_a"]),
format!("{existing}:usage_a"),
"an inherited {existing} must not swallow the value"
);
}
}
#[test]
fn carries_value_verbatim_only_for_bare_names_and_u() {
assert!(carries_value_verbatim("FOO"));
assert!(carries_value_verbatim("FOO/u"));
for entry in [
"FOO/p", "FOO/l", "FOO/w", "FOO/uw", "FOO/wu", "FOO/pu", "FOO/",
] {
assert!(!carries_value_verbatim(entry), "{entry}");
}
}
#[test]
fn wslenv_drops_empty_segments() {
assert_eq!(append(Some(""), &["usage_a"]), "usage_a");
assert_eq!(append(Some("::FOO::"), &["usage_a"]), "FOO:usage_a");
}
#[test]
fn wslenv_with_no_keys_returns_existing() {
assert_eq!(append(Some("FOO"), &[]), "FOO");
assert_eq!(append(None, &[]), "");
}
#[test]
fn wslenv_skips_keys_that_would_corrupt_the_list() {
assert_eq!(append(None, &["ok", "bad:name"]), "ok");
assert_eq!(append(None, &["ok", "bad/p"]), "ok");
assert_eq!(append(None, &["", "ok"]), "ok");
}
#[test]
fn wslenv_dedups_within_the_new_keys() {
assert_eq!(append(None, &["a", "a"]), "a");
}
#[test]
fn wslenv_adds_no_flags() {
assert!(!append(None, &["usage_a"]).contains('/'));
}
#[test]
fn parsed_env_keys_are_safe_for_wslenv() {
let spec: usage::Spec = r#"
arg "<some file>"
flag "--dry-run"
"#
.parse()
.unwrap();
let args = ["test", "x", "--dry-run"].map(String::from);
let env = usage::parse(&spec, &args).unwrap().as_env();
assert!(!env.is_empty());
for key in env.keys() {
assert!(
!key.is_empty() && !key.contains(':') && !key.contains('/'),
"as_env produced a key that cannot go in WSLENV: {key}"
);
}
}
fn from(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> + use<> {
let pairs: Vec<(String, String)> = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
move |key| {
pairs
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.to_string())
}
}
#[test]
fn shell_var_names_cover_every_shell_subcommand() {
assert_eq!(shell_var_name("bash"), "USAGE_SHELL_BASH");
assert_eq!(shell_var_name("zsh"), "USAGE_SHELL_ZSH");
assert_eq!(shell_var_name("fish"), "USAGE_SHELL_FISH");
assert_eq!(shell_var_name("pwsh"), "USAGE_SHELL_PWSH");
}
#[test]
fn unset_means_no_override() {
assert_eq!(shell_program_override("bash", from(&[])), None);
}
#[test]
fn a_blank_value_means_no_override() {
for blank in ["", " ", "\t"] {
assert_eq!(
shell_program_override("bash", from(&[("USAGE_SHELL_BASH", blank)])),
None,
"blank value {blank:?} should read as unset"
);
}
}
#[test]
fn surrounding_whitespace_is_trimmed() {
assert_eq!(
shell_program_override("bash", from(&[("USAGE_SHELL_BASH", " /usr/bin/bash ")])),
Some("/usr/bin/bash".to_string())
);
}
#[test]
fn a_path_with_spaces_survives_intact() {
let path = r"C:\Program Files\Git\bin\bash.exe";
assert_eq!(
shell_program_override("bash", from(&[("USAGE_SHELL_BASH", path)])),
Some(path.to_string())
);
}
#[test]
fn another_shells_variable_is_not_picked_up() {
assert_eq!(
shell_program_override("bash", from(&[("USAGE_SHELL_ZSH", "/bin/zsh")])),
None
);
}
}