1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use strum::EnumTryAs;
9
10#[cfg(feature = "docs")]
11use crate::docs;
12use crate::error::UsageErr;
13use crate::{Spec, SpecArg, SpecCommand, SpecFlag};
14
15pub struct ParseOutput {
16 pub cmd: SpecCommand,
17 pub cmds: Vec<SpecCommand>,
18 pub args: IndexMap<SpecArg, ParseValue>,
19 pub flags: IndexMap<SpecFlag, ParseValue>,
20 pub available_flags: BTreeMap<String, SpecFlag>,
21 pub flag_awaiting_value: Vec<SpecFlag>,
22 pub errors: Vec<UsageErr>,
23}
24
25#[derive(Debug, EnumTryAs, Clone)]
26pub enum ParseValue {
27 Bool(bool),
28 String(String),
29 MultiBool(Vec<bool>),
30 MultiString(Vec<String>),
31}
32
33pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
34 let mut out = parse_partial(spec, input)?;
35 trace!("{out:?}");
36
37 for arg in out.cmd.args.iter().skip(out.args.len()) {
39 if let Some(env_var) = arg.env.as_ref() {
40 if let Ok(env_value) = std::env::var(env_var) {
41 out.args.insert(arg.clone(), ParseValue::String(env_value));
42 continue;
43 }
44 }
45 if let Some(default) = arg.default.as_ref() {
46 out.args
47 .insert(arg.clone(), ParseValue::String(default.clone()));
48 }
49 }
50
51 for flag in out.available_flags.values() {
53 if out.flags.contains_key(flag) {
54 continue;
55 }
56 if let Some(env_var) = flag.env.as_ref() {
57 if let Ok(env_value) = std::env::var(env_var) {
58 if flag.arg.is_some() {
59 out.flags
60 .insert(flag.clone(), ParseValue::String(env_value));
61 } else {
62 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
64 out.flags.insert(flag.clone(), ParseValue::Bool(is_true));
65 }
66 continue;
67 }
68 }
69 if let Some(default) = flag.default.as_ref() {
70 out.flags
71 .insert(flag.clone(), ParseValue::String(default.clone()));
72 }
73 if let Some(Some(default)) = flag.arg.as_ref().map(|a| &a.default) {
74 out.flags
75 .insert(flag.clone(), ParseValue::String(default.clone()));
76 }
77 }
78 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
79 bail!("{err}");
80 }
81 if !out.errors.is_empty() {
82 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
83 }
84 Ok(out)
85}
86
87pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
88 trace!("parse_partial: {input:?}");
89 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
90 input.pop_front();
91
92 let gather_flags = |cmd: &SpecCommand| {
93 cmd.flags
94 .iter()
95 .flat_map(|f| {
96 let mut flags = f
97 .long
98 .iter()
99 .map(|l| (format!("--{l}"), f.clone()))
100 .chain(f.short.iter().map(|s| (format!("-{s}"), f.clone())))
101 .collect::<Vec<_>>();
102 if let Some(negate) = &f.negate {
103 flags.push((negate.clone(), f.clone()));
104 }
105 flags
106 })
107 .collect()
108 };
109
110 let mut out = ParseOutput {
111 cmd: spec.cmd.clone(),
112 cmds: vec![spec.cmd.clone()],
113 args: IndexMap::new(),
114 flags: IndexMap::new(),
115 available_flags: gather_flags(&spec.cmd),
116 flag_awaiting_value: vec![],
117 errors: vec![],
118 };
119
120 while !input.is_empty() {
121 if let Some(subcommand) = out.cmd.find_subcommand(&input[0]) {
122 let mut subcommand = subcommand.clone();
123 subcommand.mount()?;
124 out.available_flags.retain(|_, f| f.global);
125 out.available_flags.extend(gather_flags(&subcommand));
126 input.pop_front();
127 out.cmds.push(subcommand.clone());
128 out.cmd = subcommand.clone();
129 } else {
130 break;
131 }
132 }
133
134 let mut next_arg = out.cmd.args.first();
135 let mut enable_flags = true;
136 let mut grouped_flag = false;
137
138 while !input.is_empty() {
139 let mut w = input.pop_front().unwrap();
140
141 if w == "--" {
142 enable_flags = false;
143 continue;
144 }
145
146 if enable_flags && w.starts_with("--") {
148 grouped_flag = false;
149 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
150 if !val.is_empty() {
151 input.push_front(val.to_string());
152 }
153 if let Some(f) = out.available_flags.get(word) {
154 if f.arg.is_some() {
155 out.flag_awaiting_value.push(f.clone());
156 } else if f.var {
157 let arr = out
158 .flags
159 .entry(f.clone())
160 .or_insert_with(|| ParseValue::MultiBool(vec![]))
161 .try_as_multi_bool_mut()
162 .unwrap();
163 arr.push(true);
164 } else {
165 let negate = f.negate.clone().unwrap_or_default();
166 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
167 }
168 continue;
169 }
170 if is_help_arg(spec, &w) {
171 out.errors
172 .push(render_help_err(spec, &out.cmd, w.len() > 2));
173 return Ok(out);
174 }
175 }
176
177 if enable_flags && w.starts_with('-') && w.len() > 1 {
179 let short = w.chars().nth(1).unwrap();
180 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
181 if w.len() > 2 {
182 input.push_front(format!("-{}", &w[2..]));
183 grouped_flag = true;
184 }
185 if f.arg.is_some() {
186 out.flag_awaiting_value.push(f.clone());
187 } else if f.var {
188 let arr = out
189 .flags
190 .entry(f.clone())
191 .or_insert_with(|| ParseValue::MultiBool(vec![]))
192 .try_as_multi_bool_mut()
193 .unwrap();
194 arr.push(true);
195 } else {
196 let negate = f.negate.clone().unwrap_or_default();
197 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
198 }
199 continue;
200 }
201 if is_help_arg(spec, &w) {
202 out.errors
203 .push(render_help_err(spec, &out.cmd, w.len() > 2));
204 return Ok(out);
205 }
206 if grouped_flag {
207 grouped_flag = false;
208 w.remove(0);
209 }
210 }
211
212 if !out.flag_awaiting_value.is_empty() {
213 while let Some(flag) = out.flag_awaiting_value.pop() {
214 let arg = flag.arg.as_ref().unwrap();
215 if flag.var {
216 let arr = out
217 .flags
218 .entry(flag)
219 .or_insert_with(|| ParseValue::MultiString(vec![]))
220 .try_as_multi_string_mut()
221 .unwrap();
222 arr.push(w);
223 } else {
224 if let Some(choices) = &arg.choices {
225 if !choices.choices.contains(&w) {
226 if is_help_arg(spec, &w) {
227 out.errors
228 .push(render_help_err(spec, &out.cmd, w.len() > 2));
229 return Ok(out);
230 }
231 bail!(
232 "Invalid choice for option {}: {w}, expected one of {}",
233 flag.name,
234 choices.choices.join(", ")
235 );
236 }
237 }
238 out.flags.insert(flag, ParseValue::String(w));
239 }
240 w = "".to_string();
241 }
242 continue;
243 }
244
245 if let Some(arg) = next_arg {
246 if arg.var {
247 let arr = out
248 .args
249 .entry(arg.clone())
250 .or_insert_with(|| ParseValue::MultiString(vec![]))
251 .try_as_multi_string_mut()
252 .unwrap();
253 arr.push(w);
254 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
255 next_arg = out.cmd.args.get(out.args.len());
256 }
257 } else {
258 if let Some(choices) = &arg.choices {
259 if !choices.choices.contains(&w) {
260 if is_help_arg(spec, &w) {
261 out.errors
262 .push(render_help_err(spec, &out.cmd, w.len() > 2));
263 return Ok(out);
264 }
265 bail!(
266 "Invalid choice for arg {}: {w}, expected one of {}",
267 arg.name,
268 choices.choices.join(", ")
269 );
270 }
271 }
272 out.args.insert(arg.clone(), ParseValue::String(w));
273 next_arg = out.cmd.args.get(out.args.len());
274 }
275 continue;
276 }
277 if is_help_arg(spec, &w) {
278 out.errors
279 .push(render_help_err(spec, &out.cmd, w.len() > 2));
280 return Ok(out);
281 }
282 bail!("unexpected word: {w}");
283 }
284
285 for arg in out.cmd.args.iter().skip(out.args.len()) {
286 if arg.required && arg.default.is_none() {
287 let has_env = arg
289 .env
290 .as_ref()
291 .map(|e| std::env::var(e).is_ok())
292 .unwrap_or(false);
293 if !has_env {
294 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
295 }
296 }
297 }
298
299 for flag in out.available_flags.values() {
300 if out.flags.contains_key(flag) {
301 continue;
302 }
303 let has_default = flag.default.is_some() || flag.arg.iter().any(|a| a.default.is_some());
304 let has_env = flag
305 .env
306 .as_ref()
307 .map(|e| std::env::var(e).is_ok())
308 .unwrap_or(false);
309 if flag.required && !has_default && !has_env {
310 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
311 }
312 }
313
314 Ok(out)
315}
316
317#[cfg(feature = "docs")]
318fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
319 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
320}
321
322#[cfg(not(feature = "docs"))]
323fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
324 UsageErr::Help("help".to_string())
325}
326
327fn is_help_arg(spec: &Spec, w: &str) -> bool {
328 spec.disable_help != Some(true)
329 && (w == "--help"
330 || w == "-h"
331 || w == "-?"
332 || (spec.cmd.subcommands.is_empty() && w == "help"))
333}
334
335impl ParseOutput {
336 pub fn as_env(&self) -> BTreeMap<String, String> {
337 let mut env = BTreeMap::new();
338 for (flag, val) in &self.flags {
339 let key = format!("usage_{}", flag.name.to_snake_case());
340 let val = match val {
341 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
342 ParseValue::String(s) => s.clone(),
343 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
344 ParseValue::MultiString(s) => shell_words::join(s),
345 };
346 env.insert(key, val);
347 }
348 for (arg, val) in &self.args {
349 let key = format!("usage_{}", arg.name.to_snake_case());
350 env.insert(key, val.to_string());
351 }
352 env
353 }
354}
355
356impl Display for ParseValue {
357 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358 match self {
359 ParseValue::Bool(b) => write!(f, "{b}"),
360 ParseValue::String(s) => write!(f, "{s}"),
361 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
362 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
363 }
364 }
365}
366
367impl Debug for ParseOutput {
368 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
369 f.debug_struct("ParseOutput")
370 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
371 .field(
372 "args",
373 &self
374 .args
375 .iter()
376 .map(|(a, w)| format!("{}: {w}", &a.name))
377 .collect_vec(),
378 )
379 .field(
380 "available_flags",
381 &self
382 .available_flags
383 .iter()
384 .map(|(f, w)| format!("{f}: {w}"))
385 .collect_vec(),
386 )
387 .field(
388 "flags",
389 &self
390 .flags
391 .iter()
392 .map(|(f, w)| format!("{}: {w}", &f.name))
393 .collect_vec(),
394 )
395 .field("flag_awaiting_value", &self.flag_awaiting_value)
396 .field("errors", &self.errors)
397 .finish()
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_parse() {
407 let mut cmd = SpecCommand::default();
408 cmd.name = "test".to_string();
409 cmd.args = vec![SpecArg {
410 name: "arg".to_string(),
411 ..Default::default()
412 }];
413 cmd.flags = vec![SpecFlag {
414 name: "flag".to_string(),
415 long: vec!["flag".to_string()],
416 ..Default::default()
417 }];
418 let spec = Spec {
419 name: "test".to_string(),
420 bin: "test".to_string(),
421 cmd,
422 ..Default::default()
423 };
424 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
425 let parsed = parse(&spec, &input).unwrap();
426 assert_eq!(parsed.cmds.len(), 1);
427 assert_eq!(parsed.cmds[0].name, "test");
428 assert_eq!(parsed.args.len(), 1);
429 assert_eq!(parsed.flags.len(), 1);
430 assert_eq!(parsed.available_flags.len(), 1);
431 }
432
433 #[test]
434 fn test_as_env() {
435 let mut cmd = SpecCommand::default();
436 cmd.name = "test".to_string();
437 cmd.args = vec![SpecArg {
438 name: "arg".to_string(),
439 ..Default::default()
440 }];
441 cmd.flags = vec![
442 SpecFlag {
443 name: "flag".to_string(),
444 long: vec!["flag".to_string()],
445 ..Default::default()
446 },
447 SpecFlag {
448 name: "force".to_string(),
449 long: vec!["force".to_string()],
450 negate: Some("--no-force".to_string()),
451 ..Default::default()
452 },
453 ];
454 let spec = Spec {
455 name: "test".to_string(),
456 bin: "test".to_string(),
457 cmd,
458 ..Default::default()
459 };
460 let input = vec![
461 "test".to_string(),
462 "--flag".to_string(),
463 "--no-force".to_string(),
464 ];
465 let parsed = parse(&spec, &input).unwrap();
466 let env = parsed.as_env();
467 assert_eq!(env.len(), 2);
468 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
469 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
470 }
471
472 #[test]
473 fn test_arg_env_var() {
474 let mut cmd = SpecCommand::default();
475 cmd.name = "test".to_string();
476 cmd.args = vec![SpecArg {
477 name: "input".to_string(),
478 env: Some("TEST_ARG_INPUT".to_string()),
479 required: true,
480 ..Default::default()
481 }];
482 let spec = Spec {
483 name: "test".to_string(),
484 bin: "test".to_string(),
485 cmd,
486 ..Default::default()
487 };
488
489 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
491
492 let input = vec!["test".to_string()];
493 let parsed = parse(&spec, &input).unwrap();
494
495 assert_eq!(parsed.args.len(), 1);
496 let arg = parsed.args.keys().next().unwrap();
497 assert_eq!(arg.name, "input");
498 let value = parsed.args.values().next().unwrap();
499 assert_eq!(value.to_string(), "test_file.txt");
500
501 std::env::remove_var("TEST_ARG_INPUT");
503 }
504
505 #[test]
506 fn test_flag_env_var_with_arg() {
507 let mut cmd = SpecCommand::default();
508 cmd.name = "test".to_string();
509 cmd.flags = vec![SpecFlag {
510 name: "output".to_string(),
511 long: vec!["output".to_string()],
512 env: Some("TEST_FLAG_OUTPUT".to_string()),
513 arg: Some(SpecArg {
514 name: "file".to_string(),
515 ..Default::default()
516 }),
517 ..Default::default()
518 }];
519 let spec = Spec {
520 name: "test".to_string(),
521 bin: "test".to_string(),
522 cmd,
523 ..Default::default()
524 };
525
526 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
528
529 let input = vec!["test".to_string()];
530 let parsed = parse(&spec, &input).unwrap();
531
532 assert_eq!(parsed.flags.len(), 1);
533 let flag = parsed.flags.keys().next().unwrap();
534 assert_eq!(flag.name, "output");
535 let value = parsed.flags.values().next().unwrap();
536 assert_eq!(value.to_string(), "output.txt");
537
538 std::env::remove_var("TEST_FLAG_OUTPUT");
540 }
541
542 #[test]
543 fn test_flag_env_var_boolean() {
544 let mut cmd = SpecCommand::default();
545 cmd.name = "test".to_string();
546 cmd.flags = vec![SpecFlag {
547 name: "verbose".to_string(),
548 long: vec!["verbose".to_string()],
549 env: Some("TEST_FLAG_VERBOSE".to_string()),
550 ..Default::default()
551 }];
552 let spec = Spec {
553 name: "test".to_string(),
554 bin: "test".to_string(),
555 cmd,
556 ..Default::default()
557 };
558
559 std::env::set_var("TEST_FLAG_VERBOSE", "true");
561
562 let input = vec!["test".to_string()];
563 let parsed = parse(&spec, &input).unwrap();
564
565 assert_eq!(parsed.flags.len(), 1);
566 let flag = parsed.flags.keys().next().unwrap();
567 assert_eq!(flag.name, "verbose");
568 let value = parsed.flags.values().next().unwrap();
569 assert_eq!(value.to_string(), "true");
570
571 std::env::remove_var("TEST_FLAG_VERBOSE");
573 }
574
575 #[test]
576 fn test_env_var_precedence() {
577 let mut cmd = SpecCommand::default();
579 cmd.name = "test".to_string();
580 cmd.args = vec![SpecArg {
581 name: "input".to_string(),
582 env: Some("TEST_PRECEDENCE_INPUT".to_string()),
583 required: true,
584 ..Default::default()
585 }];
586 let spec = Spec {
587 name: "test".to_string(),
588 bin: "test".to_string(),
589 cmd,
590 ..Default::default()
591 };
592
593 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
595
596 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
597 let parsed = parse(&spec, &input).unwrap();
598
599 assert_eq!(parsed.args.len(), 1);
600 let value = parsed.args.values().next().unwrap();
601 assert_eq!(value.to_string(), "cli_file.txt");
603
604 std::env::remove_var("TEST_PRECEDENCE_INPUT");
606 }
607}