1use crate::error::{CommonError, Result};
4use std::collections::HashMap;
5
6pub struct EnvParser;
8
9impl EnvParser {
10 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 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 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 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 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
85pub 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#[derive(Debug, Default, Clone)]
97pub struct EnvBuilder {
98 vars: HashMap<String, String>,
99}
100
101impl EnvBuilder {
102 pub fn new() -> Self {
104 Self::default()
105 }
106
107 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 pub fn extend(mut self, vars: HashMap<String, String>) -> Self {
115 self.vars.extend(vars);
116 self
117 }
118
119 pub fn unset(mut self, key: &str) -> Self {
121 self.vars.remove(key);
122 self
123 }
124
125 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}