raz_common/
env.rs

1//! Environment variable parsing and manipulation utilities
2
3use crate::error::{CommonError, Result};
4use std::collections::HashMap;
5
6/// Environment variable parser
7pub struct EnvParser;
8
9impl EnvParser {
10    /// Parse a single environment variable assignment (KEY=value)
11    pub fn parse_assignment(input: &str) -> Result<(String, String)> {
12        let (key, value) = input
13            .split_once('=')
14            .ok_or_else(|| CommonError::ShellParse(format!("Invalid env var format: {input}")))?;
15
16        if !is_valid_env_var_name(key) {
17            return Err(CommonError::ShellParse(format!(
18                "Invalid env var name: {key}"
19            )));
20        }
21
22        Ok((key.to_string(), value.to_string()))
23    }
24
25    /// Parse multiple environment variable assignments from a string
26    /// Handles quoted values and escaping
27    pub fn parse_env_string(input: &str) -> Result<HashMap<String, String>> {
28        let mut env_vars = HashMap::new();
29        let parts = shell_words::split(input)
30            .map_err(|e| CommonError::ShellParse(format!("Failed to parse env string: {e}")))?;
31
32        for part in parts {
33            if let Some((key, value)) = part.split_once('=') {
34                if is_valid_env_var_name(key) {
35                    env_vars.insert(key.to_string(), value.to_string());
36                }
37            }
38        }
39
40        Ok(env_vars)
41    }
42
43    /// Parse environment variables from command-line style input
44    /// Separates env vars from other arguments
45    pub fn extract_env_vars(args: &[String]) -> (HashMap<String, String>, Vec<String>) {
46        let mut env_vars = HashMap::new();
47        let mut remaining_args = Vec::new();
48
49        for arg in args {
50            if let Some((key, value)) = arg.split_once('=') {
51                if is_valid_env_var_name(key) {
52                    env_vars.insert(key.to_string(), value.to_string());
53                } else {
54                    remaining_args.push(arg.clone());
55                }
56            } else {
57                remaining_args.push(arg.clone());
58            }
59        }
60
61        (env_vars, remaining_args)
62    }
63
64    /// Merge environment variables, with the second map taking precedence
65    pub fn merge_env_vars(
66        base: HashMap<String, String>,
67        overrides: HashMap<String, String>,
68    ) -> HashMap<String, String> {
69        let mut result = base;
70        result.extend(overrides);
71        result
72    }
73
74    /// Format environment variables for display
75    pub fn format_env_vars(env_vars: &HashMap<String, String>) -> Vec<String> {
76        let mut formatted: Vec<_> = env_vars
77            .iter()
78            .map(|(k, v)| format!("{}={}", k, shell_words::quote(v)))
79            .collect();
80        formatted.sort();
81        formatted
82    }
83}
84
85/// Check if a string is a valid environment variable name
86pub fn is_valid_env_var_name(name: &str) -> bool {
87    !name.is_empty()
88        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
89        && name
90            .chars()
91            .next()
92            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
93}
94
95/// Builder for environment variables
96#[derive(Debug, Default, Clone)]
97pub struct EnvBuilder {
98    vars: HashMap<String, String>,
99}
100
101impl EnvBuilder {
102    /// Create a new environment builder
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    /// Add an environment variable
108    pub fn set(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
109        self.vars.insert(key.into(), value.into());
110        self
111    }
112
113    /// Add multiple environment variables
114    pub fn extend(mut self, vars: HashMap<String, String>) -> Self {
115        self.vars.extend(vars);
116        self
117    }
118
119    /// Remove an environment variable
120    pub fn unset(mut self, key: &str) -> Self {
121        self.vars.remove(key);
122        self
123    }
124
125    /// Build the environment variables
126    pub fn build(self) -> HashMap<String, String> {
127        self.vars
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_parse_assignment() {
137        let (key, value) = EnvParser::parse_assignment("FOO=bar").unwrap();
138        assert_eq!(key, "FOO");
139        assert_eq!(value, "bar");
140
141        let (key, value) = EnvParser::parse_assignment("PATH=/usr/bin:/bin").unwrap();
142        assert_eq!(key, "PATH");
143        assert_eq!(value, "/usr/bin:/bin");
144
145        assert!(EnvParser::parse_assignment("invalid").is_err());
146        assert!(EnvParser::parse_assignment("123=value").is_err());
147    }
148
149    #[test]
150    fn test_extract_env_vars() {
151        let args = vec![
152            "FOO=bar".to_string(),
153            "--flag".to_string(),
154            "VALUE=123".to_string(),
155            "arg".to_string(),
156        ];
157
158        let (env_vars, remaining) = EnvParser::extract_env_vars(&args);
159
160        assert_eq!(env_vars.len(), 2);
161        assert_eq!(env_vars.get("FOO"), Some(&"bar".to_string()));
162        assert_eq!(env_vars.get("VALUE"), Some(&"123".to_string()));
163
164        assert_eq!(remaining, vec!["--flag", "arg"]);
165    }
166
167    #[test]
168    fn test_env_builder() {
169        let env = EnvBuilder::new()
170            .set("FOO", "bar")
171            .set("BAZ", "qux")
172            .unset("BAZ")
173            .build();
174
175        assert_eq!(env.len(), 1);
176        assert_eq!(env.get("FOO"), Some(&"bar".to_string()));
177        assert_eq!(env.get("BAZ"), None);
178    }
179}