agentic_tools_utils/
cli.rs1use anyhow::Result;
7use anyhow::anyhow;
8use std::collections::BTreeSet;
9
10pub fn parse_comma_set(input: &str) -> BTreeSet<String> {
24 input
25 .split(|c: char| c == ',' || c.is_whitespace())
26 .filter(|s| !s.trim().is_empty())
27 .map(|s| s.trim().to_lowercase())
28 .collect()
29}
30
31pub fn bool_from_env(var: &str, default: bool) -> bool {
47 match std::env::var(var) {
48 Ok(v) => match v.trim().to_ascii_lowercase().as_str() {
49 "1" | "true" | "yes" | "on" => true,
50 "0" | "false" | "no" | "off" => false,
51 _ => default,
52 },
53 Err(_) => default,
54 }
55}
56
57pub fn usize_from_env(var: &str, default: usize) -> usize {
71 std::env::var(var)
72 .ok()
73 .and_then(|v| v.trim().parse::<usize>().ok())
74 .unwrap_or(default)
75}
76
77pub fn set_from_env(var: &str) -> Option<BTreeSet<String>> {
89 std::env::var(var)
90 .ok()
91 .map(|s| parse_comma_set(&s))
92 .filter(|s| !s.is_empty())
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct Argv {
100 pub raw: String,
102 pub program: String,
104 pub args: Vec<String>,
106}
107
108pub fn editor_argv() -> Result<Argv> {
135 let visual = std::env::var("VISUAL").ok();
136 let editor = std::env::var("EDITOR").ok();
137
138 let raw = visual
139 .as_deref()
140 .map(str::trim)
141 .filter(|s| !s.is_empty())
142 .or_else(|| editor.as_deref().map(str::trim).filter(|s| !s.is_empty()))
143 .unwrap_or("vi")
144 .to_string();
145
146 let parts =
147 shlex::split(&raw).ok_or_else(|| anyhow!("Invalid $VISUAL/$EDITOR value: {raw}"))?;
148 let (program, args) = parts
149 .split_first()
150 .ok_or_else(|| anyhow!("Empty $VISUAL/$EDITOR value"))?;
151
152 Ok(Argv {
153 raw,
154 program: program.clone(),
155 args: args.to_vec(),
156 })
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn parse_comma_set_basic() {
165 let set = parse_comma_set("foo,bar,baz");
166 assert_eq!(set.len(), 3);
167 assert!(set.contains("foo"));
168 assert!(set.contains("bar"));
169 assert!(set.contains("baz"));
170 }
171
172 #[test]
173 fn parse_comma_set_with_spaces() {
174 let set = parse_comma_set("foo, bar , baz");
175 assert_eq!(set.len(), 3);
176 assert!(set.contains("foo"));
177 assert!(set.contains("bar"));
178 assert!(set.contains("baz"));
179 }
180
181 #[test]
182 fn parse_comma_set_whitespace_separated() {
183 let set = parse_comma_set("foo bar baz");
184 assert_eq!(set.len(), 3);
185 }
186
187 #[test]
188 fn parse_comma_set_mixed_separators() {
189 let set = parse_comma_set("foo, bar baz");
190 assert_eq!(set.len(), 3);
191 }
192
193 #[test]
194 fn parse_comma_set_lowercases() {
195 let set = parse_comma_set("FOO, Bar, BAZ");
196 assert!(set.contains("foo"));
197 assert!(set.contains("bar"));
198 assert!(set.contains("baz"));
199 assert!(!set.contains("FOO"));
200 }
201
202 #[test]
203 fn parse_comma_set_empty() {
204 let set = parse_comma_set("");
205 assert!(set.is_empty());
206 }
207
208 #[test]
209 fn parse_comma_set_only_separators() {
210 let set = parse_comma_set(", , , ");
211 assert!(set.is_empty());
212 }
213
214 #[test]
215 fn parse_comma_set_duplicates_deduplicated() {
216 let set = parse_comma_set("foo, FOO, Foo");
217 assert_eq!(set.len(), 1);
218 assert!(set.contains("foo"));
219 }
220
221 #[test]
222 fn bool_from_env_returns_default_when_unset() {
223 let result = bool_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__", true);
225 assert!(result);
226
227 let result = bool_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__", false);
228 assert!(!result);
229 }
230
231 #[test]
232 fn usize_from_env_returns_default_when_unset() {
233 let result = usize_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__", 42);
234 assert_eq!(result, 42);
235 }
236
237 #[test]
238 fn set_from_env_returns_none_when_unset() {
239 let result = set_from_env("__AGENTIC_TEST_NONEXISTENT_VAR__");
240 assert!(result.is_none());
241 }
242
243 fn argv_from(visual: Option<&str>, editor: Option<&str>) -> super::Result<Argv> {
247 let raw = visual
248 .map(str::trim)
249 .filter(|s| !s.is_empty())
250 .or_else(|| editor.map(str::trim).filter(|s| !s.is_empty()))
251 .unwrap_or("vi")
252 .to_string();
253 let parts = shlex::split(&raw).ok_or_else(|| anyhow::anyhow!("Invalid value: {raw}"))?;
254 let (program, args) = parts
255 .split_first()
256 .ok_or_else(|| anyhow::anyhow!("Empty value"))?;
257 Ok(Argv {
258 raw,
259 program: program.clone(),
260 args: args.to_vec(),
261 })
262 }
263
264 #[test]
265 fn test_editor_code_wait() {
266 let argv = argv_from(None, Some("code --wait")).unwrap();
267 assert_eq!(argv.program, "code");
268 assert_eq!(argv.args, vec!["--wait"]);
269 }
270
271 #[test]
272 fn test_editor_visual_precedence() {
273 let argv = argv_from(Some("nvim"), Some("vim")).unwrap();
274 assert_eq!(argv.program, "nvim");
275 }
276
277 #[test]
278 fn test_editor_whitespace_fallback() {
279 let argv = argv_from(Some(" "), Some(" ")).unwrap();
280 assert_eq!(argv.program, "vi");
281 }
282
283 #[test]
284 fn test_editor_quoted_args() {
285 let argv = argv_from(None, Some(r#"nvim -c "set number""#)).unwrap();
286 assert_eq!(argv.program, "nvim");
287 assert_eq!(argv.args, vec!["-c", "set number"]);
288 }
289
290 #[test]
291 fn test_editor_multiple_args() {
292 let argv = argv_from(None, Some("code --wait --new-window")).unwrap();
293 assert_eq!(argv.program, "code");
294 assert_eq!(argv.args, vec!["--wait", "--new-window"]);
295 }
296
297 #[test]
298 fn test_editor_default_vi() {
299 let argv = argv_from(None, None).unwrap();
300 assert_eq!(argv.program, "vi");
301 assert!(argv.args.is_empty());
302 }
303}