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