1pub mod env_source;
62
63use crate::env_source::EnvSource;
64
65pub fn expand_env_vars(input: &str) -> String {
66 let vars = std::env::vars();
67 expand_env_recursive(input, &vars)
68}
69
70pub fn expand_env_recursive(input: &str, env: &impl EnvSource) -> String {
71 const MAX_EXPAND_DEPTH: usize = 8;
72 fn inner(s: &str, env: &impl EnvSource, depth: usize) -> String {
73 if depth >= MAX_EXPAND_DEPTH {
74 return s.to_string();
75 }
76 let expanded = expand_env(s, env);
77 if expanded.contains('$') && expanded != s {
78 inner(&expanded, env, depth + 1)
79 } else {
80 expanded
81 }
82 }
83 inner(input, env, 0)
84}
85
86pub fn expand_env(input: &str, env: &impl EnvSource) -> String {
98 let mut result = String::new();
99 let mut chars = input.chars().peekable();
100
101 while let Some(c) = chars.next() {
102 if c == '$' {
103 match chars.peek() {
104 Some('$') => {
105 chars.next(); result.push('$');
107 }
108 Some('{') => {
109 chars.next(); let mut key = String::new();
111 let mut default = None;
112 let mut in_default = false;
113 while let Some(&ch) = chars.peek() {
114 if ch == '}' {
115 chars.next(); break;
117 } else if ch == ':' && chars.clone().nth(1) == Some('-') {
118 chars.next();
119 chars.next(); in_default = true;
121 } else {
122 if in_default {
123 default.get_or_insert(String::new()).push(ch);
124 } else {
125 key.push(ch);
126 }
127 chars.next();
128 }
129 }
130 let val = env.get(&key).or(default.as_ref().cloned()).unwrap_or_default();
131 result.push_str(&val);
132 }
133 Some(ch) if ch.is_alphanumeric() || *ch == '_' => {
134 let mut key = String::new();
135 while let Some(&ch) = chars.peek() {
136 if ch.is_alphanumeric() || ch == '_' {
137 key.push(ch);
138 chars.next();
139 } else {
140 break;
141 }
142 }
143 let val = env.get(&key).unwrap_or_default();
144 result.push_str(&val);
145 }
146 _ => {
147 result.push('$');
148 }
149 }
150 } else {
151 result.push(c);
152 }
153 }
154
155 result
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::env_source::{EnvSourceChain, FnEnvSource};
162 use std::collections::{BTreeMap, HashMap};
163
164 #[test]
165 fn test_expand_basic() {
166 let mut env = HashMap::new();
167 env.insert("FOO".into(), "bar".into());
168 assert_eq!(expand_env("$FOO/bin", &env), "bar/bin");
169 }
170
171 #[test]
172 fn test_expand_brace() {
173 let mut env = HashMap::new();
174 env.insert("FOO".into(), "bar".into());
175 assert_eq!(expand_env("${FOO}/lib", &env), "bar/lib");
176 }
177
178 #[test]
179 fn test_expand_with_default() {
180 let env = HashMap::new();
181 assert_eq!(expand_env("${FOO:-baz}/bin", &env), "baz/bin");
182 }
183
184 #[test]
185 fn test_unterminated_brace() {
186 let mut env = HashMap::new();
187 env.insert("FOO".into(), "bar".into());
188 assert_eq!(expand_env("${FOO", &env), "bar");
189 }
190
191 #[test]
192 fn test_mixed_vars_and_defaults() {
193 let mut env = HashMap::new();
194 env.insert("X".into(), "123".into());
195 env.insert("Y".into(), "abc".into());
196 assert_eq!(expand_env("$X/${Y:-zzz}/$Z", &env), "123/abc/");
197 }
198
199 #[test]
200 fn test_literal_dollar_sign() {
201 let env = HashMap::new();
202 assert_eq!(expand_env("Price is $$100", &env), "Price is $100");
203 }
204
205 #[test]
206 fn test_non_alphanumeric_after_dollar() {
207 let env = HashMap::new();
208 assert_eq!(expand_env("Hello $!", &env), "Hello $!");
209 }
210
211 #[test]
212 fn test_multiple_variables() {
213 let mut env = HashMap::new();
214 env.insert("A".into(), "1".into());
215 env.insert("B".into(), "2".into());
216 env.insert("C".into(), "3".into());
217 assert_eq!(expand_env("$A-$B-${C:-0}", &env), "1-2-3");
218 }
219
220 #[test]
221 fn test_empty_input() {
222 let env = HashMap::new();
223 assert_eq!(expand_env("", &env), "");
224 }
225
226 #[test]
227 fn test_default_value_with_special_chars() {
228 let env = HashMap::new();
229 assert_eq!(expand_env("${MISSING:-/usr/local/bin}", &env), "/usr/local/bin");
230 }
231
232 #[test]
233 fn test_no_substitution() {
234 let env = HashMap::new();
235 assert_eq!(expand_env("just a string", &env), "just a string");
236 }
237
238 #[test]
239 fn test_env_source_btree_map() {
240 let mut env = BTreeMap::new();
241 env.insert("FOO".into(), "baz".into());
242 assert_eq!(expand_env("$FOO", &env), "baz");
243 }
244
245 #[test]
246 fn test_env_source_slice() {
247 let env: &[(&str, &str)] = &[("FOO", "baz")];
248 assert_eq!(expand_env("prefix_$FOO", &env), "prefix_baz");
249 }
250
251 #[test]
252 fn test_env_source_fn_adapter() {
253 let env_fn = FnEnvSource(|key: &str| if key == "FOO" { Some("baz".into()) } else { None });
254 assert_eq!(expand_env("abc$FOO", &env_fn), "abcbaz");
255 }
256
257 #[test]
258 fn test_chain_env_source() {
259 let env1 = [("FOO", "a")];
260 let mut env2 = HashMap::new();
261 env2.insert("BAR".into(), "b".into());
262 let chain = EnvSourceChain {
263 primary: &env1[..],
264 fallback: &env2,
265 };
266 assert_eq!(expand_env("$FOO:$BAR:$BAZ", &chain), "a:b:");
267 }
268
269 #[test]
270 fn test_recursive_expand() {
271 let mut env = HashMap::new();
272 env.insert("FOO".into(), "$BAR".into());
273 env.insert("BAR".into(), "hello".into());
274 assert_eq!(expand_env_recursive("$FOO world", &env), "hello world");
275 }
276
277 #[test]
278 fn test_recursive_multi_layer() {
279 let mut env = HashMap::new();
280 env.insert("A".into(), "$B".into());
281 env.insert("B".into(), "$C".into());
282 env.insert("C".into(), "$D".into());
283 env.insert("D".into(), "42".into());
284 assert_eq!(expand_env_recursive("A=$A, B=$B, C=$C, D=$D", &env), "A=42, B=42, C=42, D=42");
285 }
286
287 #[test]
288 fn test_recursive_prevent_infinite() {
289 let mut env = HashMap::new();
290 env.insert("LOOP".into(), "$LOOP".into());
291 let res = expand_env_recursive("start:$LOOP:end", &env);
292 assert!(res.contains("$LOOP"));
294 }
295}