rush_var/
lib.rs

1//! # rush-var —— Bash风格环境变量插值库
2//!
3//! 支持 `$VAR`、`${VAR}`、`${VAR:-default}`、`$$`(字面$)等语法,支持递归插值,适配多种环境变量源:
4//!
5//! - [`HashMap<String, String>`], [`BTreeMap`], 切片对 (`&[(&str, &str)]`)
6//! - 自定义闭包 [`FnEnvSource`](例如连接数据库、远程环境服务等)
7//! - 组合多个源 [`EnvSourceChain`],优先从主源读取,回退到备用源
8//!
9//! ## 基础用法
10//!
11//! ```rust
12//! use rush_var::expand_env;
13//! let env = [ ("FOO", "bar") ];
14//! assert_eq!(expand_env("Hello $FOO!", &env), "Hello bar!");
15//! assert_eq!(expand_env("path=${BAR:-/usr/local}/bin", &env), "path=/usr/local/bin");
16//! ```
17//!
18//! ## 使用闭包作为环境源
19//!
20//! ```rust
21//! use rush_var::env_source::FnEnvSource;
22//! use rush_var::expand_env;
23//! let env = FnEnvSource(|k: &str| if k == "USER" { Some("alice".to_string()) } else { None });
24//! assert_eq!(expand_env("hi_$USER", &env), "hi_alice");
25//! ```
26//!
27//! ## 组合多个变量源(优先主源,后备源)
28//!
29//! ```rust
30//! use rush_var::env_source::EnvSourceChain;
31//! use rush_var::expand_env;
32//! let main = [ ("A", "x") ];
33//! let mut fallback = std::collections::HashMap::new();
34//! fallback.insert("B".to_string(), "y".to_string());
35//! let chain = EnvSourceChain { primary: &main[..], fallback: &fallback };
36//! assert_eq!(expand_env("$A,$B", &chain), "x,y");
37//! ```
38//!
39//! ## 递归插值
40//!
41//! ```rust
42//! use std::collections::HashMap;
43//! use rush_var::expand_env_recursive;
44//!
45//! let mut env = HashMap::new();
46//! env.insert("A".into(), "$B".into());
47//! env.insert("B".into(), "123".into());
48//! assert_eq!(expand_env_recursive("val=$A", &env), "val=123");
49//! ```
50//!
51//! 最大递归深度限制为 8 层,以防止无限循环。
52//!
53//! ## 用于 std::env::vars()
54//!
55//! ```rust
56//! use rush_var::expand_env_vars;
57//! unsafe { std::env::set_var("FOO", "hello"); }
58//! assert_eq!(expand_env_vars("$FOO world"), "hello world");
59//! ```
60
61pub 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
86/// Bash 风格环境变量插值主函数。
87///
88/// 支持 $VAR、${VAR}、${VAR:-default}、$$(字面$),适配多种环境变量源。
89///
90/// # 用法示例
91/// ```rust
92/// use rush_var::expand_env;
93/// let env = [ ("FOO", "bar") ];
94/// assert_eq!(expand_env("$FOO/bin", &env), "bar/bin");
95/// assert_eq!(expand_env("${BAR:-default}/lib", &env), "default/lib");
96/// ```
97pub 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(); // consume second $
106                    result.push('$');
107                }
108                Some('{') => {
109                    chars.next(); // consume '{'
110                    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(); // consume '}'
116                            break;
117                        } else if ch == ':' && chars.clone().nth(1) == Some('-') {
118                            chars.next();
119                            chars.next(); // consume :-
120                            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        // 最多递归8次,最后返回原样
293        assert!(res.contains("$LOOP"));
294    }
295}