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::arg::SpecDoubleDashChoices;
14use crate::{Spec, SpecArg, SpecCommand, SpecFlag};
15
16fn get_flag_key(word: &str) -> &str {
19 if word.starts_with("--") {
20 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
22 } else if word.len() >= 2 {
23 &word[0..2]
25 } else {
26 word
27 }
28}
29
30pub struct ParseOutput {
31 pub cmd: SpecCommand,
32 pub cmds: Vec<SpecCommand>,
33 pub args: IndexMap<SpecArg, ParseValue>,
34 pub flags: IndexMap<SpecFlag, ParseValue>,
35 pub available_flags: BTreeMap<String, SpecFlag>,
36 pub flag_awaiting_value: Vec<SpecFlag>,
37 pub errors: Vec<UsageErr>,
38}
39
40#[derive(Debug, EnumTryAs, Clone)]
41pub enum ParseValue {
42 Bool(bool),
43 String(String),
44 MultiBool(Vec<bool>),
45 MultiString(Vec<String>),
46}
47
48pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
49 let mut out = parse_partial(spec, input)?;
50 trace!("{out:?}");
51
52 for arg in out.cmd.args.iter().skip(out.args.len()) {
54 if let Some(env_var) = arg.env.as_ref() {
55 if let Ok(env_value) = std::env::var(env_var) {
56 out.args.insert(arg.clone(), ParseValue::String(env_value));
57 continue;
58 }
59 }
60 if !arg.default.is_empty() {
61 if arg.var {
63 out.args
65 .insert(arg.clone(), ParseValue::MultiString(arg.default.clone()));
66 } else {
67 out.args
69 .insert(arg.clone(), ParseValue::String(arg.default[0].clone()));
70 }
71 }
72 }
73
74 for flag in out.available_flags.values() {
76 if out.flags.contains_key(flag) {
77 continue;
78 }
79 if let Some(env_var) = flag.env.as_ref() {
80 if let Ok(env_value) = std::env::var(env_var) {
81 if flag.arg.is_some() {
82 out.flags
83 .insert(flag.clone(), ParseValue::String(env_value));
84 } else {
85 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
87 out.flags.insert(flag.clone(), ParseValue::Bool(is_true));
88 }
89 continue;
90 }
91 }
92 if !flag.default.is_empty() {
94 if flag.var {
96 if flag.arg.is_some() {
98 out.flags
99 .insert(flag.clone(), ParseValue::MultiString(flag.default.clone()));
100 } else {
101 let bools: Vec<bool> = flag
103 .default
104 .iter()
105 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
106 .collect();
107 out.flags.insert(flag.clone(), ParseValue::MultiBool(bools));
108 }
109 } else {
110 if flag.arg.is_some() {
112 out.flags
113 .insert(flag.clone(), ParseValue::String(flag.default[0].clone()));
114 } else {
115 let is_true =
117 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
118 out.flags.insert(flag.clone(), ParseValue::Bool(is_true));
119 }
120 }
121 }
122 if let Some(arg) = flag.arg.as_ref() {
124 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
125 if flag.var {
126 out.flags
127 .insert(flag.clone(), ParseValue::MultiString(arg.default.clone()));
128 } else {
129 out.flags
130 .insert(flag.clone(), ParseValue::String(arg.default[0].clone()));
131 }
132 }
133 }
134 }
135 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
136 bail!("{err}");
137 }
138 if !out.errors.is_empty() {
139 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
140 }
141 Ok(out)
142}
143
144pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
145 trace!("parse_partial: {input:?}");
146 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
147 input.pop_front();
148
149 let gather_flags = |cmd: &SpecCommand| {
150 cmd.flags
151 .iter()
152 .flat_map(|f| {
153 let mut flags = f
154 .long
155 .iter()
156 .map(|l| (format!("--{l}"), f.clone()))
157 .chain(f.short.iter().map(|s| (format!("-{s}"), f.clone())))
158 .collect::<Vec<_>>();
159 if let Some(negate) = &f.negate {
160 flags.push((negate.clone(), f.clone()));
161 }
162 flags
163 })
164 .collect()
165 };
166
167 let mut out = ParseOutput {
168 cmd: spec.cmd.clone(),
169 cmds: vec![spec.cmd.clone()],
170 args: IndexMap::new(),
171 flags: IndexMap::new(),
172 available_flags: gather_flags(&spec.cmd),
173 flag_awaiting_value: vec![],
174 errors: vec![],
175 };
176
177 let mut prefix_words: Vec<String> = vec![];
190 let mut idx = 0;
191
192 while idx < input.len() {
193 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
194 let mut subcommand = subcommand.clone();
195 subcommand.mount(&prefix_words)?;
197 out.available_flags.retain(|_, f| f.global);
198 out.available_flags.extend(gather_flags(&subcommand));
199 input.remove(idx);
201 out.cmds.push(subcommand.clone());
202 out.cmd = subcommand.clone();
203 prefix_words.clear();
204 } else if input[idx].starts_with('-') {
207 let word = &input[idx];
209 let flag_key = get_flag_key(word);
210
211 if let Some(f) = out.available_flags.get(flag_key) {
212 if f.global {
214 prefix_words.push(input[idx].clone());
215 idx += 1;
216
217 if f.arg.is_some()
220 && !word.contains('=')
221 && idx < input.len()
222 && !input[idx].starts_with('-')
223 {
224 prefix_words.push(input[idx].clone());
225 idx += 1;
226 }
227 } else {
228 break;
232 }
233 } else {
234 break;
237 }
238 } else {
239 if let Some(default_name) = &spec.default_subcommand {
242 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
243 let mut subcommand = subcommand.clone();
244 subcommand.mount(&prefix_words)?;
246 out.available_flags.retain(|_, f| f.global);
247 out.available_flags.extend(gather_flags(&subcommand));
248 out.cmds.push(subcommand.clone());
249 out.cmd = subcommand.clone();
250 prefix_words.clear();
251 break;
254 }
255 }
256 break;
258 }
259 }
260
261 let mut next_arg = out.cmd.args.first();
266 let mut enable_flags = true;
267 let mut grouped_flag = false;
268
269 while !input.is_empty() {
270 let mut w = input.pop_front().unwrap();
271
272 if let Some(ref restart_token) = out.cmd.restart_token {
275 if w == *restart_token {
276 out.args.clear();
278 next_arg = out.cmd.args.first();
279 out.flag_awaiting_value.clear(); enable_flags = true; continue;
283 }
284 }
285
286 if w == "--" {
287 enable_flags = false;
289
290 let should_preserve = next_arg
293 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
294 .unwrap_or(false);
295
296 if should_preserve {
297 } else {
299 continue;
301 }
302 }
303
304 if enable_flags && w.starts_with("--") {
306 grouped_flag = false;
307 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
308 if !val.is_empty() {
309 input.push_front(val.to_string());
310 }
311 if let Some(f) = out.available_flags.get(word) {
312 if f.arg.is_some() {
313 out.flag_awaiting_value.push(f.clone());
314 } else if f.count {
315 let arr = out
316 .flags
317 .entry(f.clone())
318 .or_insert_with(|| ParseValue::MultiBool(vec![]))
319 .try_as_multi_bool_mut()
320 .unwrap();
321 arr.push(true);
322 } else {
323 let negate = f.negate.clone().unwrap_or_default();
324 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
325 }
326 continue;
327 }
328 if is_help_arg(spec, &w) {
329 out.errors
330 .push(render_help_err(spec, &out.cmd, w.len() > 2));
331 return Ok(out);
332 }
333 }
334
335 if enable_flags && w.starts_with('-') && w.len() > 1 {
337 let short = w.chars().nth(1).unwrap();
338 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
339 if w.len() > 2 {
340 input.push_front(format!("-{}", &w[2..]));
341 grouped_flag = true;
342 }
343 if f.arg.is_some() {
344 out.flag_awaiting_value.push(f.clone());
345 } else if f.count {
346 let arr = out
347 .flags
348 .entry(f.clone())
349 .or_insert_with(|| ParseValue::MultiBool(vec![]))
350 .try_as_multi_bool_mut()
351 .unwrap();
352 arr.push(true);
353 } else {
354 let negate = f.negate.clone().unwrap_or_default();
355 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
356 }
357 continue;
358 }
359 if is_help_arg(spec, &w) {
360 out.errors
361 .push(render_help_err(spec, &out.cmd, w.len() > 2));
362 return Ok(out);
363 }
364 if grouped_flag {
365 grouped_flag = false;
366 w.remove(0);
367 }
368 }
369
370 if !out.flag_awaiting_value.is_empty() {
371 while let Some(flag) = out.flag_awaiting_value.pop() {
372 let arg = flag.arg.as_ref().unwrap();
373 if flag.var {
374 let arr = out
375 .flags
376 .entry(flag)
377 .or_insert_with(|| ParseValue::MultiString(vec![]))
378 .try_as_multi_string_mut()
379 .unwrap();
380 arr.push(w);
381 } else {
382 if let Some(choices) = &arg.choices {
383 if !choices.choices.contains(&w) {
384 if is_help_arg(spec, &w) {
385 out.errors
386 .push(render_help_err(spec, &out.cmd, w.len() > 2));
387 return Ok(out);
388 }
389 bail!(
390 "Invalid choice for option {}: {w}, expected one of {}",
391 flag.name,
392 choices.choices.join(", ")
393 );
394 }
395 }
396 out.flags.insert(flag, ParseValue::String(w));
397 }
398 w = "".to_string();
399 }
400 continue;
401 }
402
403 if let Some(arg) = next_arg {
404 if arg.var {
405 let arr = out
406 .args
407 .entry(arg.clone())
408 .or_insert_with(|| ParseValue::MultiString(vec![]))
409 .try_as_multi_string_mut()
410 .unwrap();
411 arr.push(w);
412 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
413 next_arg = out.cmd.args.get(out.args.len());
414 }
415 } else {
416 if let Some(choices) = &arg.choices {
417 if !choices.choices.contains(&w) {
418 if is_help_arg(spec, &w) {
419 out.errors
420 .push(render_help_err(spec, &out.cmd, w.len() > 2));
421 return Ok(out);
422 }
423 bail!(
424 "Invalid choice for arg {}: {w}, expected one of {}",
425 arg.name,
426 choices.choices.join(", ")
427 );
428 }
429 }
430 out.args.insert(arg.clone(), ParseValue::String(w));
431 next_arg = out.cmd.args.get(out.args.len());
432 }
433 continue;
434 }
435 if is_help_arg(spec, &w) {
436 out.errors
437 .push(render_help_err(spec, &out.cmd, w.len() > 2));
438 return Ok(out);
439 }
440 bail!("unexpected word: {w}");
441 }
442
443 for arg in out.cmd.args.iter().skip(out.args.len()) {
444 if arg.required && arg.default.is_empty() {
445 let has_env = arg
447 .env
448 .as_ref()
449 .map(|e| std::env::var(e).is_ok())
450 .unwrap_or(false);
451 if !has_env {
452 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
453 }
454 }
455 }
456
457 for flag in out.available_flags.values() {
458 if out.flags.contains_key(flag) {
459 continue;
460 }
461 let has_default =
462 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
463 let has_env = flag
464 .env
465 .as_ref()
466 .map(|e| std::env::var(e).is_ok())
467 .unwrap_or(false);
468 if flag.required && !has_default && !has_env {
469 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
470 }
471 }
472
473 for (arg, value) in &out.args {
475 if arg.var {
476 if let ParseValue::MultiString(values) = value {
477 if let Some(min) = arg.var_min {
478 if values.len() < min {
479 out.errors.push(UsageErr::VarArgTooFew {
480 name: arg.name.clone(),
481 min,
482 got: values.len(),
483 });
484 }
485 }
486 if let Some(max) = arg.var_max {
487 if values.len() > max {
488 out.errors.push(UsageErr::VarArgTooMany {
489 name: arg.name.clone(),
490 max,
491 got: values.len(),
492 });
493 }
494 }
495 }
496 }
497 }
498
499 for (flag, value) in &out.flags {
501 if flag.var {
502 let count = match value {
503 ParseValue::MultiString(values) => values.len(),
504 ParseValue::MultiBool(values) => values.len(),
505 _ => continue,
506 };
507 if let Some(min) = flag.var_min {
508 if count < min {
509 out.errors.push(UsageErr::VarFlagTooFew {
510 name: flag.name.clone(),
511 min,
512 got: count,
513 });
514 }
515 }
516 if let Some(max) = flag.var_max {
517 if count > max {
518 out.errors.push(UsageErr::VarFlagTooMany {
519 name: flag.name.clone(),
520 max,
521 got: count,
522 });
523 }
524 }
525 }
526 }
527
528 Ok(out)
529}
530
531#[cfg(feature = "docs")]
532fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
533 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
534}
535
536#[cfg(not(feature = "docs"))]
537fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
538 UsageErr::Help("help".to_string())
539}
540
541fn is_help_arg(spec: &Spec, w: &str) -> bool {
542 spec.disable_help != Some(true)
543 && (w == "--help"
544 || w == "-h"
545 || w == "-?"
546 || (spec.cmd.subcommands.is_empty() && w == "help"))
547}
548
549impl ParseOutput {
550 pub fn as_env(&self) -> BTreeMap<String, String> {
551 let mut env = BTreeMap::new();
552 for (flag, val) in &self.flags {
553 let key = format!("usage_{}", flag.name.to_snake_case());
554 let val = match val {
555 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
556 ParseValue::String(s) => s.clone(),
557 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
558 ParseValue::MultiString(s) => shell_words::join(s),
559 };
560 env.insert(key, val);
561 }
562 for (arg, val) in &self.args {
563 let key = format!("usage_{}", arg.name.to_snake_case());
564 env.insert(key, val.to_string());
565 }
566 env
567 }
568}
569
570impl Display for ParseValue {
571 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
572 match self {
573 ParseValue::Bool(b) => write!(f, "{b}"),
574 ParseValue::String(s) => write!(f, "{s}"),
575 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
576 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
577 }
578 }
579}
580
581impl Debug for ParseOutput {
582 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
583 f.debug_struct("ParseOutput")
584 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
585 .field(
586 "args",
587 &self
588 .args
589 .iter()
590 .map(|(a, w)| format!("{}: {w}", &a.name))
591 .collect_vec(),
592 )
593 .field(
594 "available_flags",
595 &self
596 .available_flags
597 .iter()
598 .map(|(f, w)| format!("{f}: {w}"))
599 .collect_vec(),
600 )
601 .field(
602 "flags",
603 &self
604 .flags
605 .iter()
606 .map(|(f, w)| format!("{}: {w}", &f.name))
607 .collect_vec(),
608 )
609 .field("flag_awaiting_value", &self.flag_awaiting_value)
610 .field("errors", &self.errors)
611 .finish()
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn test_parse() {
621 let cmd = SpecCommand::builder()
622 .name("test")
623 .arg(SpecArg::builder().name("arg").build())
624 .flag(SpecFlag::builder().long("flag").build())
625 .build();
626 let spec = Spec {
627 name: "test".to_string(),
628 bin: "test".to_string(),
629 cmd,
630 ..Default::default()
631 };
632 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
633 let parsed = parse(&spec, &input).unwrap();
634 assert_eq!(parsed.cmds.len(), 1);
635 assert_eq!(parsed.cmds[0].name, "test");
636 assert_eq!(parsed.args.len(), 1);
637 assert_eq!(parsed.flags.len(), 1);
638 assert_eq!(parsed.available_flags.len(), 1);
639 }
640
641 #[test]
642 fn test_as_env() {
643 let cmd = SpecCommand::builder()
644 .name("test")
645 .arg(SpecArg::builder().name("arg").build())
646 .flag(SpecFlag::builder().long("flag").build())
647 .flag(
648 SpecFlag::builder()
649 .long("force")
650 .negate("--no-force")
651 .build(),
652 )
653 .build();
654 let spec = Spec {
655 name: "test".to_string(),
656 bin: "test".to_string(),
657 cmd,
658 ..Default::default()
659 };
660 let input = vec![
661 "test".to_string(),
662 "--flag".to_string(),
663 "--no-force".to_string(),
664 ];
665 let parsed = parse(&spec, &input).unwrap();
666 let env = parsed.as_env();
667 assert_eq!(env.len(), 2);
668 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
669 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
670 }
671
672 #[test]
673 fn test_arg_env_var() {
674 let cmd = SpecCommand::builder()
675 .name("test")
676 .arg(
677 SpecArg::builder()
678 .name("input")
679 .env("TEST_ARG_INPUT")
680 .required(true)
681 .build(),
682 )
683 .build();
684 let spec = Spec {
685 name: "test".to_string(),
686 bin: "test".to_string(),
687 cmd,
688 ..Default::default()
689 };
690
691 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
693
694 let input = vec!["test".to_string()];
695 let parsed = parse(&spec, &input).unwrap();
696
697 assert_eq!(parsed.args.len(), 1);
698 let arg = parsed.args.keys().next().unwrap();
699 assert_eq!(arg.name, "input");
700 let value = parsed.args.values().next().unwrap();
701 assert_eq!(value.to_string(), "test_file.txt");
702
703 std::env::remove_var("TEST_ARG_INPUT");
705 }
706
707 #[test]
708 fn test_flag_env_var_with_arg() {
709 let cmd = SpecCommand::builder()
710 .name("test")
711 .flag(
712 SpecFlag::builder()
713 .long("output")
714 .env("TEST_FLAG_OUTPUT")
715 .arg(SpecArg::builder().name("file").build())
716 .build(),
717 )
718 .build();
719 let spec = Spec {
720 name: "test".to_string(),
721 bin: "test".to_string(),
722 cmd,
723 ..Default::default()
724 };
725
726 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
728
729 let input = vec!["test".to_string()];
730 let parsed = parse(&spec, &input).unwrap();
731
732 assert_eq!(parsed.flags.len(), 1);
733 let flag = parsed.flags.keys().next().unwrap();
734 assert_eq!(flag.name, "output");
735 let value = parsed.flags.values().next().unwrap();
736 assert_eq!(value.to_string(), "output.txt");
737
738 std::env::remove_var("TEST_FLAG_OUTPUT");
740 }
741
742 #[test]
743 fn test_flag_env_var_boolean() {
744 let cmd = SpecCommand::builder()
745 .name("test")
746 .flag(
747 SpecFlag::builder()
748 .long("verbose")
749 .env("TEST_FLAG_VERBOSE")
750 .build(),
751 )
752 .build();
753 let spec = Spec {
754 name: "test".to_string(),
755 bin: "test".to_string(),
756 cmd,
757 ..Default::default()
758 };
759
760 std::env::set_var("TEST_FLAG_VERBOSE", "true");
762
763 let input = vec!["test".to_string()];
764 let parsed = parse(&spec, &input).unwrap();
765
766 assert_eq!(parsed.flags.len(), 1);
767 let flag = parsed.flags.keys().next().unwrap();
768 assert_eq!(flag.name, "verbose");
769 let value = parsed.flags.values().next().unwrap();
770 assert_eq!(value.to_string(), "true");
771
772 std::env::remove_var("TEST_FLAG_VERBOSE");
774 }
775
776 #[test]
777 fn test_env_var_precedence() {
778 let cmd = SpecCommand::builder()
780 .name("test")
781 .arg(
782 SpecArg::builder()
783 .name("input")
784 .env("TEST_PRECEDENCE_INPUT")
785 .required(true)
786 .build(),
787 )
788 .build();
789 let spec = Spec {
790 name: "test".to_string(),
791 bin: "test".to_string(),
792 cmd,
793 ..Default::default()
794 };
795
796 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
798
799 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
800 let parsed = parse(&spec, &input).unwrap();
801
802 assert_eq!(parsed.args.len(), 1);
803 let value = parsed.args.values().next().unwrap();
804 assert_eq!(value.to_string(), "cli_file.txt");
806
807 std::env::remove_var("TEST_PRECEDENCE_INPUT");
809 }
810
811 #[test]
812 fn test_flag_var_true_with_single_default() {
813 let cmd = SpecCommand::builder()
815 .name("test")
816 .flag(
817 SpecFlag::builder()
818 .long("foo")
819 .var(true)
820 .arg(SpecArg::builder().name("foo").build())
821 .default_value("bar")
822 .build(),
823 )
824 .build();
825 let spec = Spec {
826 name: "test".to_string(),
827 bin: "test".to_string(),
828 cmd,
829 ..Default::default()
830 };
831
832 let input = vec!["test".to_string()];
834 let parsed = parse(&spec, &input).unwrap();
835
836 assert_eq!(parsed.flags.len(), 1);
837 let flag = parsed.flags.keys().next().unwrap();
838 assert_eq!(flag.name, "foo");
839 let value = parsed.flags.values().next().unwrap();
840 match value {
842 ParseValue::MultiString(v) => {
843 assert_eq!(v.len(), 1);
844 assert_eq!(v[0], "bar");
845 }
846 _ => panic!("Expected MultiString, got {:?}", value),
847 }
848 }
849
850 #[test]
851 fn test_flag_var_true_with_multiple_defaults() {
852 let cmd = SpecCommand::builder()
854 .name("test")
855 .flag(
856 SpecFlag::builder()
857 .long("foo")
858 .var(true)
859 .arg(SpecArg::builder().name("foo").build())
860 .default_values(["xyz", "bar"])
861 .build(),
862 )
863 .build();
864 let spec = Spec {
865 name: "test".to_string(),
866 bin: "test".to_string(),
867 cmd,
868 ..Default::default()
869 };
870
871 let input = vec!["test".to_string()];
873 let parsed = parse(&spec, &input).unwrap();
874
875 assert_eq!(parsed.flags.len(), 1);
876 let value = parsed.flags.values().next().unwrap();
877 match value {
879 ParseValue::MultiString(v) => {
880 assert_eq!(v.len(), 2);
881 assert_eq!(v[0], "xyz");
882 assert_eq!(v[1], "bar");
883 }
884 _ => panic!("Expected MultiString, got {:?}", value),
885 }
886 }
887
888 #[test]
889 fn test_flag_var_false_with_default_remains_string() {
890 let cmd = SpecCommand::builder()
892 .name("test")
893 .flag(
894 SpecFlag::builder()
895 .long("foo")
896 .var(false) .arg(SpecArg::builder().name("foo").build())
898 .default_value("bar")
899 .build(),
900 )
901 .build();
902 let spec = Spec {
903 name: "test".to_string(),
904 bin: "test".to_string(),
905 cmd,
906 ..Default::default()
907 };
908
909 let input = vec!["test".to_string()];
911 let parsed = parse(&spec, &input).unwrap();
912
913 assert_eq!(parsed.flags.len(), 1);
914 let value = parsed.flags.values().next().unwrap();
915 match value {
917 ParseValue::String(s) => {
918 assert_eq!(s, "bar");
919 }
920 _ => panic!("Expected String, got {:?}", value),
921 }
922 }
923
924 #[test]
925 fn test_arg_var_true_with_single_default() {
926 let cmd = SpecCommand::builder()
928 .name("test")
929 .arg(
930 SpecArg::builder()
931 .name("files")
932 .var(true)
933 .default_value("default.txt")
934 .required(false)
935 .build(),
936 )
937 .build();
938 let spec = Spec {
939 name: "test".to_string(),
940 bin: "test".to_string(),
941 cmd,
942 ..Default::default()
943 };
944
945 let input = vec!["test".to_string()];
947 let parsed = parse(&spec, &input).unwrap();
948
949 assert_eq!(parsed.args.len(), 1);
950 let value = parsed.args.values().next().unwrap();
951 match value {
953 ParseValue::MultiString(v) => {
954 assert_eq!(v.len(), 1);
955 assert_eq!(v[0], "default.txt");
956 }
957 _ => panic!("Expected MultiString, got {:?}", value),
958 }
959 }
960
961 #[test]
962 fn test_arg_var_true_with_multiple_defaults() {
963 let cmd = SpecCommand::builder()
965 .name("test")
966 .arg(
967 SpecArg::builder()
968 .name("files")
969 .var(true)
970 .default_values(["file1.txt", "file2.txt"])
971 .required(false)
972 .build(),
973 )
974 .build();
975 let spec = Spec {
976 name: "test".to_string(),
977 bin: "test".to_string(),
978 cmd,
979 ..Default::default()
980 };
981
982 let input = vec!["test".to_string()];
984 let parsed = parse(&spec, &input).unwrap();
985
986 assert_eq!(parsed.args.len(), 1);
987 let value = parsed.args.values().next().unwrap();
988 match value {
990 ParseValue::MultiString(v) => {
991 assert_eq!(v.len(), 2);
992 assert_eq!(v[0], "file1.txt");
993 assert_eq!(v[1], "file2.txt");
994 }
995 _ => panic!("Expected MultiString, got {:?}", value),
996 }
997 }
998
999 #[test]
1000 fn test_arg_var_false_with_default_remains_string() {
1001 let cmd = SpecCommand::builder()
1003 .name("test")
1004 .arg(
1005 SpecArg::builder()
1006 .name("file")
1007 .var(false)
1008 .default_value("default.txt")
1009 .required(false)
1010 .build(),
1011 )
1012 .build();
1013 let spec = Spec {
1014 name: "test".to_string(),
1015 bin: "test".to_string(),
1016 cmd,
1017 ..Default::default()
1018 };
1019
1020 let input = vec!["test".to_string()];
1022 let parsed = parse(&spec, &input).unwrap();
1023
1024 assert_eq!(parsed.args.len(), 1);
1025 let value = parsed.args.values().next().unwrap();
1026 match value {
1028 ParseValue::String(s) => {
1029 assert_eq!(s, "default.txt");
1030 }
1031 _ => panic!("Expected String, got {:?}", value),
1032 }
1033 }
1034
1035 #[test]
1036 fn test_default_subcommand() {
1037 let run_cmd = SpecCommand::builder()
1039 .name("run")
1040 .arg(SpecArg::builder().name("task").build())
1041 .build();
1042 let mut cmd = SpecCommand::builder().name("test").build();
1043 cmd.subcommands.insert("run".to_string(), run_cmd);
1044
1045 let spec = Spec {
1046 name: "test".to_string(),
1047 bin: "test".to_string(),
1048 cmd,
1049 default_subcommand: Some("run".to_string()),
1050 ..Default::default()
1051 };
1052
1053 let input = vec!["test".to_string(), "mytask".to_string()];
1055 let parsed = parse(&spec, &input).unwrap();
1056
1057 assert_eq!(parsed.cmds.len(), 2);
1059 assert_eq!(parsed.cmds[1].name, "run");
1060
1061 assert_eq!(parsed.args.len(), 1);
1063 let arg = parsed.args.keys().next().unwrap();
1064 assert_eq!(arg.name, "task");
1065 let value = parsed.args.values().next().unwrap();
1066 assert_eq!(value.to_string(), "mytask");
1067 }
1068
1069 #[test]
1070 fn test_default_subcommand_explicit_still_works() {
1071 let run_cmd = SpecCommand::builder()
1073 .name("run")
1074 .arg(SpecArg::builder().name("task").build())
1075 .build();
1076 let other_cmd = SpecCommand::builder()
1077 .name("other")
1078 .arg(SpecArg::builder().name("other_arg").build())
1079 .build();
1080 let mut cmd = SpecCommand::builder().name("test").build();
1081 cmd.subcommands.insert("run".to_string(), run_cmd);
1082 cmd.subcommands.insert("other".to_string(), other_cmd);
1083
1084 let spec = Spec {
1085 name: "test".to_string(),
1086 bin: "test".to_string(),
1087 cmd,
1088 default_subcommand: Some("run".to_string()),
1089 ..Default::default()
1090 };
1091
1092 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1094 let parsed = parse(&spec, &input).unwrap();
1095
1096 assert_eq!(parsed.cmds.len(), 2);
1098 assert_eq!(parsed.cmds[1].name, "other");
1099 }
1100
1101 #[test]
1102 fn test_restart_token() {
1103 let run_cmd = SpecCommand::builder()
1105 .name("run")
1106 .arg(SpecArg::builder().name("task").build())
1107 .restart_token(":::".to_string())
1108 .build();
1109 let mut cmd = SpecCommand::builder().name("test").build();
1110 cmd.subcommands.insert("run".to_string(), run_cmd);
1111
1112 let spec = Spec {
1113 name: "test".to_string(),
1114 bin: "test".to_string(),
1115 cmd,
1116 ..Default::default()
1117 };
1118
1119 let input = vec![
1121 "test".to_string(),
1122 "run".to_string(),
1123 "task1".to_string(),
1124 ":::".to_string(),
1125 "task2".to_string(),
1126 ];
1127 let parsed = parse(&spec, &input).unwrap();
1128
1129 assert_eq!(parsed.args.len(), 1);
1131 let value = parsed.args.values().next().unwrap();
1132 assert_eq!(value.to_string(), "task2");
1133 }
1134
1135 #[test]
1136 fn test_restart_token_multiple() {
1137 let run_cmd = SpecCommand::builder()
1139 .name("run")
1140 .arg(SpecArg::builder().name("task").build())
1141 .restart_token(":::".to_string())
1142 .build();
1143 let mut cmd = SpecCommand::builder().name("test").build();
1144 cmd.subcommands.insert("run".to_string(), run_cmd);
1145
1146 let spec = Spec {
1147 name: "test".to_string(),
1148 bin: "test".to_string(),
1149 cmd,
1150 ..Default::default()
1151 };
1152
1153 let input = vec![
1155 "test".to_string(),
1156 "run".to_string(),
1157 "task1".to_string(),
1158 ":::".to_string(),
1159 "task2".to_string(),
1160 ":::".to_string(),
1161 "task3".to_string(),
1162 ];
1163 let parsed = parse(&spec, &input).unwrap();
1164
1165 assert_eq!(parsed.args.len(), 1);
1167 let value = parsed.args.values().next().unwrap();
1168 assert_eq!(value.to_string(), "task3");
1169 }
1170
1171 #[test]
1172 fn test_restart_token_clears_flag_awaiting_value() {
1173 let run_cmd = SpecCommand::builder()
1175 .name("run")
1176 .arg(SpecArg::builder().name("task").build())
1177 .flag(
1178 SpecFlag::builder()
1179 .name("jobs")
1180 .long("jobs")
1181 .arg(SpecArg::builder().name("count").build())
1182 .build(),
1183 )
1184 .restart_token(":::".to_string())
1185 .build();
1186 let mut cmd = SpecCommand::builder().name("test").build();
1187 cmd.subcommands.insert("run".to_string(), run_cmd);
1188
1189 let spec = Spec {
1190 name: "test".to_string(),
1191 bin: "test".to_string(),
1192 cmd,
1193 ..Default::default()
1194 };
1195
1196 let input = vec![
1198 "test".to_string(),
1199 "run".to_string(),
1200 "task1".to_string(),
1201 "--jobs".to_string(),
1202 ":::".to_string(),
1203 "task2".to_string(),
1204 ];
1205 let parsed = parse(&spec, &input).unwrap();
1206
1207 assert_eq!(parsed.args.len(), 1);
1209 let value = parsed.args.values().next().unwrap();
1210 assert_eq!(value.to_string(), "task2");
1211 assert!(parsed.flag_awaiting_value.is_empty());
1213 }
1214
1215 #[test]
1216 fn test_restart_token_resets_double_dash() {
1217 let run_cmd = SpecCommand::builder()
1219 .name("run")
1220 .arg(SpecArg::builder().name("task").build())
1221 .arg(SpecArg::builder().name("extra_args").var(true).build())
1222 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1223 .restart_token(":::".to_string())
1224 .build();
1225 let mut cmd = SpecCommand::builder().name("test").build();
1226 cmd.subcommands.insert("run".to_string(), run_cmd);
1227
1228 let spec = Spec {
1229 name: "test".to_string(),
1230 bin: "test".to_string(),
1231 cmd,
1232 ..Default::default()
1233 };
1234
1235 let input = vec![
1237 "test".to_string(),
1238 "run".to_string(),
1239 "task1".to_string(),
1240 "--".to_string(),
1241 "extra".to_string(),
1242 ":::".to_string(),
1243 "--verbose".to_string(),
1244 "task2".to_string(),
1245 ];
1246 let parsed = parse(&spec, &input).unwrap();
1247
1248 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1250 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1252 let value = parsed.args.get(task_arg).unwrap();
1253 assert_eq!(value.to_string(), "task2");
1254 }
1255
1256 #[test]
1257 fn test_double_dashes_without_preserve() {
1258 let run_cmd = SpecCommand::builder()
1260 .name("run")
1261 .arg(SpecArg::builder().name("args").var(true).build())
1262 .build();
1263 let mut cmd = SpecCommand::builder().name("test").build();
1264 cmd.subcommands.insert("run".to_string(), run_cmd);
1265
1266 let spec = Spec {
1267 name: "test".to_string(),
1268 bin: "test".to_string(),
1269 cmd,
1270 ..Default::default()
1271 };
1272
1273 let input = vec![
1275 "test".to_string(),
1276 "run".to_string(),
1277 "arg1".to_string(),
1278 "--".to_string(),
1279 "arg2".to_string(),
1280 "--".to_string(),
1281 "arg3".to_string(),
1282 ];
1283 let parsed = parse(&spec, &input).unwrap();
1284
1285 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1286 let value = parsed.args.get(args_arg).unwrap();
1287 assert_eq!(value.to_string(), "arg1 arg2 arg3");
1288 }
1289
1290 #[test]
1291 fn test_double_dashes_with_preserve() {
1292 let run_cmd = SpecCommand::builder()
1294 .name("run")
1295 .arg(
1296 SpecArg::builder()
1297 .name("args")
1298 .var(true)
1299 .double_dash(SpecDoubleDashChoices::Preserve)
1300 .build(),
1301 )
1302 .build();
1303 let mut cmd = SpecCommand::builder().name("test").build();
1304 cmd.subcommands.insert("run".to_string(), run_cmd);
1305
1306 let spec = Spec {
1307 name: "test".to_string(),
1308 bin: "test".to_string(),
1309 cmd,
1310 ..Default::default()
1311 };
1312
1313 let input = vec![
1315 "test".to_string(),
1316 "run".to_string(),
1317 "arg1".to_string(),
1318 "--".to_string(),
1319 "arg2".to_string(),
1320 "--".to_string(),
1321 "arg3".to_string(),
1322 ];
1323 let parsed = parse(&spec, &input).unwrap();
1324
1325 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1326 let value = parsed.args.get(args_arg).unwrap();
1327 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1328 }
1329
1330 #[test]
1331 fn test_double_dashes_with_preserve_only_dashes() {
1332 let run_cmd = SpecCommand::builder()
1335 .name("run")
1336 .arg(
1337 SpecArg::builder()
1338 .name("args")
1339 .var(true)
1340 .double_dash(SpecDoubleDashChoices::Preserve)
1341 .build(),
1342 )
1343 .build();
1344 let mut cmd = SpecCommand::builder().name("test").build();
1345 cmd.subcommands.insert("run".to_string(), run_cmd);
1346
1347 let spec = Spec {
1348 name: "test".to_string(),
1349 bin: "test".to_string(),
1350 cmd,
1351 ..Default::default()
1352 };
1353
1354 let input = vec![
1356 "test".to_string(),
1357 "run".to_string(),
1358 "--".to_string(),
1359 "--".to_string(),
1360 ];
1361 let parsed = parse(&spec, &input).unwrap();
1362
1363 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1364 let value = parsed.args.get(args_arg).unwrap();
1365 assert_eq!(value.to_string(), "-- --");
1366 }
1367
1368 #[test]
1369 fn test_double_dashes_with_preserve_multiple_args() {
1370 let run_cmd = SpecCommand::builder()
1372 .name("run")
1373 .arg(SpecArg::builder().name("task").build())
1374 .arg(
1375 SpecArg::builder()
1376 .name("extra_args")
1377 .var(true)
1378 .double_dash(SpecDoubleDashChoices::Preserve)
1379 .build(),
1380 )
1381 .build();
1382 let mut cmd = SpecCommand::builder().name("test").build();
1383 cmd.subcommands.insert("run".to_string(), run_cmd);
1384
1385 let spec = Spec {
1386 name: "test".to_string(),
1387 bin: "test".to_string(),
1388 cmd,
1389 ..Default::default()
1390 };
1391
1392 let input = vec![
1395 "test".to_string(),
1396 "run".to_string(),
1397 "task1".to_string(),
1398 "--".to_string(),
1399 "arg1".to_string(),
1400 "--".to_string(),
1401 "--foo".to_string(),
1402 ];
1403 let parsed = parse(&spec, &input).unwrap();
1404
1405 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1406 let task_value = parsed.args.get(task_arg).unwrap();
1407 assert_eq!(task_value.to_string(), "task1");
1408
1409 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
1410 let extra_value = parsed.args.get(extra_arg).unwrap();
1411 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
1412 }
1413}