expand_env_vars/
lib.rs

1//! A cross-platform environment variable expander that supports Unix-style (`$VAR`, `${VAR}`)
2//! and Windows-style (`%VAR%`) syntax.
3
4use std::env;
5
6use regex::Regex;
7
8use std::fmt;
9
10/// Custom error type for environment variable expansion.
11#[derive(Debug)]
12pub enum EnvExpansionError {
13    MissingVar(String),
14}
15
16impl fmt::Display for EnvExpansionError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            EnvExpansionError::MissingVar(var) => {
20                write!(f, "Missing environment variable: {}", var)
21            }
22        }
23    }
24}
25
26impl std::error::Error for EnvExpansionError {}
27
28/// Expands environment variable placeholders in a string with actual environment values.
29///
30/// - On **Unix**, supports `$VAR` and `${VAR}`.
31/// - On **Windows**, supports `%VAR%`.
32///
33/// # Errors
34///
35/// Currently, missing variables are replaced with an empty string.
36/// A stricter mode can be implemented later to return an error for missing variables.
37///
38pub fn expand_env_vars(input: &str) -> Result<String, EnvExpansionError> {
39    #[cfg(unix)]
40    {
41        let unix_re = Regex::new(r"\$(\w+)|\$\{(\w+)\}").unwrap();
42        let result = unix_re.replace_all(input, |caps: &regex::Captures| {
43            let var_name = caps
44                .get(1)
45                .or_else(|| caps.get(2))
46                .map(|m| m.as_str())
47                .unwrap_or("");
48            env::var(var_name).unwrap_or_default()
49        });
50        Ok(result.into_owned())
51    }
52
53    #[cfg(windows)]
54    {
55        let windows_re = Regex::new(r"%(\w+)%").unwrap();
56        let result = windows_re.replace_all(input, |caps: &regex::Captures| {
57            let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
58            env::var(var_name).unwrap_or_default()
59        });
60        result.into_owned()
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_single_var_unix() {
70        unsafe {
71            std::env::set_var("USER", "alice");
72        }
73        let input = "Hello $USER!";
74        let output = expand_env_vars(input).unwrap();
75        assert_eq!(output, "Hello alice!");
76    }
77
78    #[test]
79    fn test_braced_var_unix() {
80        unsafe {
81            std::env::set_var("HOME", "/home/alice");
82        }
83        let input = "Path: ${HOME}/code";
84        let output = expand_env_vars(input).unwrap();
85        assert_eq!(output, "Path: /home/alice/code");
86    }
87
88    #[test]
89    fn test_multiple_vars_unix() {
90        unsafe {
91            std::env::set_var("USER", "bob");
92            std::env::set_var("SHELL", "/bin/bash");
93        }
94        let input = "$USER uses $SHELL";
95        let output = expand_env_vars(input).unwrap();
96        assert_eq!(output, "bob uses /bin/bash");
97    }
98
99    #[test]
100    fn test_missing_var_unix() {
101        unsafe {
102            std::env::remove_var("DOES_NOT_EXIST");
103        }
104        let input = "This is $DOES_NOT_EXIST";
105        let output = expand_env_vars(input).unwrap();
106        assert_eq!(output, "This is ");
107    }
108
109    #[cfg(windows)]
110    #[test]
111    fn test_single_var_windows() {
112        unsafe {
113            std::env::set_var("USERNAME", "charlie");
114        }
115        let input = "User: %USERNAME%";
116        let output = expand_env_vars(input).unwrap();
117        assert_eq!(output, "User: charlie");
118    }
119
120    #[cfg(windows)]
121    #[test]
122    fn test_multiple_vars_windows() {
123        unsafe {
124            std::env::set_var("USERNAME", "charlie");
125            std::env::set_var("APPDATA", "C:\\Users\\charlie\\AppData");
126        }
127        let input = "%USERNAME%'s config: %APPDATA%";
128        let output = expand_env_vars(input).unwrap();
129        assert_eq!(output, "charlie's config: C:\\Users\\charlie\\AppData");
130    }
131
132    #[cfg(windows)]
133    #[test]
134    fn test_missing_var_windows() {
135        unsafe {
136            std::env::remove_var("DOES_NOT_EXIST");
137        }
138        let input = "Value: %DOES_NOT_EXIST%";
139        let output = expand_env_vars(input).unwrap();
140        assert_eq!(output, "Value: ");
141    }
142}