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 !arg.default.is_empty() {
60 if arg.var {
62 out.args
64 .insert(arg.clone(), ParseValue::MultiString(arg.default.clone()));
65 } else {
66 out.args
68 .insert(arg.clone(), ParseValue::String(arg.default[0].clone()));
69 }
70 }
71 }
72
73 for flag in out.available_flags.values() {
75 if out.flags.contains_key(flag) {
76 continue;
77 }
78 if let Some(env_var) = flag.env.as_ref() {
79 if let Ok(env_value) = std::env::var(env_var) {
80 if flag.arg.is_some() {
81 out.flags
82 .insert(flag.clone(), ParseValue::String(env_value));
83 } else {
84 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
86 out.flags.insert(flag.clone(), ParseValue::Bool(is_true));
87 }
88 continue;
89 }
90 }
91 if !flag.default.is_empty() {
93 if flag.var {
95 if flag.arg.is_some() {
97 out.flags
98 .insert(flag.clone(), ParseValue::MultiString(flag.default.clone()));
99 } else {
100 let bools: Vec<bool> = flag
102 .default
103 .iter()
104 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
105 .collect();
106 out.flags.insert(flag.clone(), ParseValue::MultiBool(bools));
107 }
108 } else {
109 if flag.arg.is_some() {
111 out.flags
112 .insert(flag.clone(), ParseValue::String(flag.default[0].clone()));
113 } else {
114 let is_true =
116 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
117 out.flags.insert(flag.clone(), ParseValue::Bool(is_true));
118 }
119 }
120 }
121 if let Some(arg) = flag.arg.as_ref() {
123 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
124 if flag.var {
125 out.flags
126 .insert(flag.clone(), ParseValue::MultiString(arg.default.clone()));
127 } else {
128 out.flags
129 .insert(flag.clone(), ParseValue::String(arg.default[0].clone()));
130 }
131 }
132 }
133 }
134 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
135 bail!("{err}");
136 }
137 if !out.errors.is_empty() {
138 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
139 }
140 Ok(out)
141}
142
143pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
144 trace!("parse_partial: {input:?}");
145 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
146 input.pop_front();
147
148 let gather_flags = |cmd: &SpecCommand| {
149 cmd.flags
150 .iter()
151 .flat_map(|f| {
152 let mut flags = f
153 .long
154 .iter()
155 .map(|l| (format!("--{l}"), f.clone()))
156 .chain(f.short.iter().map(|s| (format!("-{s}"), f.clone())))
157 .collect::<Vec<_>>();
158 if let Some(negate) = &f.negate {
159 flags.push((negate.clone(), f.clone()));
160 }
161 flags
162 })
163 .collect()
164 };
165
166 let mut out = ParseOutput {
167 cmd: spec.cmd.clone(),
168 cmds: vec![spec.cmd.clone()],
169 args: IndexMap::new(),
170 flags: IndexMap::new(),
171 available_flags: gather_flags(&spec.cmd),
172 flag_awaiting_value: vec![],
173 errors: vec![],
174 };
175
176 let mut prefix_words: Vec<String> = vec![];
189 let mut idx = 0;
190
191 while idx < input.len() {
192 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
193 let mut subcommand = subcommand.clone();
194 subcommand.mount(&prefix_words)?;
196 out.available_flags.retain(|_, f| f.global);
197 out.available_flags.extend(gather_flags(&subcommand));
198 input.remove(idx);
200 out.cmds.push(subcommand.clone());
201 out.cmd = subcommand.clone();
202 prefix_words.clear();
203 } else if input[idx].starts_with('-') {
206 let word = &input[idx];
208 let flag_key = get_flag_key(word);
209
210 if let Some(f) = out.available_flags.get(flag_key) {
211 if f.global {
213 prefix_words.push(input[idx].clone());
214 idx += 1;
215
216 if f.arg.is_some()
219 && !word.contains('=')
220 && idx < input.len()
221 && !input[idx].starts_with('-')
222 {
223 prefix_words.push(input[idx].clone());
224 idx += 1;
225 }
226 } else {
227 break;
231 }
232 } else {
233 break;
236 }
237 } else {
238 if let Some(default_name) = &spec.default_subcommand {
241 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
242 let mut subcommand = subcommand.clone();
243 subcommand.mount(&prefix_words)?;
245 out.available_flags.retain(|_, f| f.global);
246 out.available_flags.extend(gather_flags(&subcommand));
247 out.cmds.push(subcommand.clone());
248 out.cmd = subcommand.clone();
249 prefix_words.clear();
250 break;
253 }
254 }
255 break;
257 }
258 }
259
260 let mut next_arg = out.cmd.args.first();
265 let mut enable_flags = true;
266 let mut grouped_flag = false;
267
268 while !input.is_empty() {
269 let mut w = input.pop_front().unwrap();
270
271 if let Some(ref restart_token) = out.cmd.restart_token {
274 if w == *restart_token {
275 out.args.clear();
277 next_arg = out.cmd.args.first();
278 out.flag_awaiting_value.clear(); enable_flags = true; continue;
282 }
283 }
284
285 if w == "--" {
286 enable_flags = false;
287 continue;
288 }
289
290 if enable_flags && w.starts_with("--") {
292 grouped_flag = false;
293 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
294 if !val.is_empty() {
295 input.push_front(val.to_string());
296 }
297 if let Some(f) = out.available_flags.get(word) {
298 if f.arg.is_some() {
299 out.flag_awaiting_value.push(f.clone());
300 } else if f.count {
301 let arr = out
302 .flags
303 .entry(f.clone())
304 .or_insert_with(|| ParseValue::MultiBool(vec![]))
305 .try_as_multi_bool_mut()
306 .unwrap();
307 arr.push(true);
308 } else {
309 let negate = f.negate.clone().unwrap_or_default();
310 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
311 }
312 continue;
313 }
314 if is_help_arg(spec, &w) {
315 out.errors
316 .push(render_help_err(spec, &out.cmd, w.len() > 2));
317 return Ok(out);
318 }
319 }
320
321 if enable_flags && w.starts_with('-') && w.len() > 1 {
323 let short = w.chars().nth(1).unwrap();
324 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
325 if w.len() > 2 {
326 input.push_front(format!("-{}", &w[2..]));
327 grouped_flag = true;
328 }
329 if f.arg.is_some() {
330 out.flag_awaiting_value.push(f.clone());
331 } else if f.count {
332 let arr = out
333 .flags
334 .entry(f.clone())
335 .or_insert_with(|| ParseValue::MultiBool(vec![]))
336 .try_as_multi_bool_mut()
337 .unwrap();
338 arr.push(true);
339 } else {
340 let negate = f.negate.clone().unwrap_or_default();
341 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
342 }
343 continue;
344 }
345 if is_help_arg(spec, &w) {
346 out.errors
347 .push(render_help_err(spec, &out.cmd, w.len() > 2));
348 return Ok(out);
349 }
350 if grouped_flag {
351 grouped_flag = false;
352 w.remove(0);
353 }
354 }
355
356 if !out.flag_awaiting_value.is_empty() {
357 while let Some(flag) = out.flag_awaiting_value.pop() {
358 let arg = flag.arg.as_ref().unwrap();
359 if flag.var {
360 let arr = out
361 .flags
362 .entry(flag)
363 .or_insert_with(|| ParseValue::MultiString(vec![]))
364 .try_as_multi_string_mut()
365 .unwrap();
366 arr.push(w);
367 } else {
368 if let Some(choices) = &arg.choices {
369 if !choices.choices.contains(&w) {
370 if is_help_arg(spec, &w) {
371 out.errors
372 .push(render_help_err(spec, &out.cmd, w.len() > 2));
373 return Ok(out);
374 }
375 bail!(
376 "Invalid choice for option {}: {w}, expected one of {}",
377 flag.name,
378 choices.choices.join(", ")
379 );
380 }
381 }
382 out.flags.insert(flag, ParseValue::String(w));
383 }
384 w = "".to_string();
385 }
386 continue;
387 }
388
389 if let Some(arg) = next_arg {
390 if arg.var {
391 let arr = out
392 .args
393 .entry(arg.clone())
394 .or_insert_with(|| ParseValue::MultiString(vec![]))
395 .try_as_multi_string_mut()
396 .unwrap();
397 arr.push(w);
398 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
399 next_arg = out.cmd.args.get(out.args.len());
400 }
401 } else {
402 if let Some(choices) = &arg.choices {
403 if !choices.choices.contains(&w) {
404 if is_help_arg(spec, &w) {
405 out.errors
406 .push(render_help_err(spec, &out.cmd, w.len() > 2));
407 return Ok(out);
408 }
409 bail!(
410 "Invalid choice for arg {}: {w}, expected one of {}",
411 arg.name,
412 choices.choices.join(", ")
413 );
414 }
415 }
416 out.args.insert(arg.clone(), ParseValue::String(w));
417 next_arg = out.cmd.args.get(out.args.len());
418 }
419 continue;
420 }
421 if is_help_arg(spec, &w) {
422 out.errors
423 .push(render_help_err(spec, &out.cmd, w.len() > 2));
424 return Ok(out);
425 }
426 bail!("unexpected word: {w}");
427 }
428
429 for arg in out.cmd.args.iter().skip(out.args.len()) {
430 if arg.required && arg.default.is_empty() {
431 let has_env = arg
433 .env
434 .as_ref()
435 .map(|e| std::env::var(e).is_ok())
436 .unwrap_or(false);
437 if !has_env {
438 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
439 }
440 }
441 }
442
443 for flag in out.available_flags.values() {
444 if out.flags.contains_key(flag) {
445 continue;
446 }
447 let has_default =
448 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
449 let has_env = flag
450 .env
451 .as_ref()
452 .map(|e| std::env::var(e).is_ok())
453 .unwrap_or(false);
454 if flag.required && !has_default && !has_env {
455 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
456 }
457 }
458
459 for (arg, value) in &out.args {
461 if arg.var {
462 if let ParseValue::MultiString(values) = value {
463 if let Some(min) = arg.var_min {
464 if values.len() < min {
465 out.errors.push(UsageErr::VarArgTooFew {
466 name: arg.name.clone(),
467 min,
468 got: values.len(),
469 });
470 }
471 }
472 if let Some(max) = arg.var_max {
473 if values.len() > max {
474 out.errors.push(UsageErr::VarArgTooMany {
475 name: arg.name.clone(),
476 max,
477 got: values.len(),
478 });
479 }
480 }
481 }
482 }
483 }
484
485 for (flag, value) in &out.flags {
487 if flag.var {
488 let count = match value {
489 ParseValue::MultiString(values) => values.len(),
490 ParseValue::MultiBool(values) => values.len(),
491 _ => continue,
492 };
493 if let Some(min) = flag.var_min {
494 if count < min {
495 out.errors.push(UsageErr::VarFlagTooFew {
496 name: flag.name.clone(),
497 min,
498 got: count,
499 });
500 }
501 }
502 if let Some(max) = flag.var_max {
503 if count > max {
504 out.errors.push(UsageErr::VarFlagTooMany {
505 name: flag.name.clone(),
506 max,
507 got: count,
508 });
509 }
510 }
511 }
512 }
513
514 Ok(out)
515}
516
517#[cfg(feature = "docs")]
518fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
519 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
520}
521
522#[cfg(not(feature = "docs"))]
523fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
524 UsageErr::Help("help".to_string())
525}
526
527fn is_help_arg(spec: &Spec, w: &str) -> bool {
528 spec.disable_help != Some(true)
529 && (w == "--help"
530 || w == "-h"
531 || w == "-?"
532 || (spec.cmd.subcommands.is_empty() && w == "help"))
533}
534
535impl ParseOutput {
536 pub fn as_env(&self) -> BTreeMap<String, String> {
537 let mut env = BTreeMap::new();
538 for (flag, val) in &self.flags {
539 let key = format!("usage_{}", flag.name.to_snake_case());
540 let val = match val {
541 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
542 ParseValue::String(s) => s.clone(),
543 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
544 ParseValue::MultiString(s) => shell_words::join(s),
545 };
546 env.insert(key, val);
547 }
548 for (arg, val) in &self.args {
549 let key = format!("usage_{}", arg.name.to_snake_case());
550 env.insert(key, val.to_string());
551 }
552 env
553 }
554}
555
556impl Display for ParseValue {
557 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
558 match self {
559 ParseValue::Bool(b) => write!(f, "{b}"),
560 ParseValue::String(s) => write!(f, "{s}"),
561 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
562 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
563 }
564 }
565}
566
567impl Debug for ParseOutput {
568 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
569 f.debug_struct("ParseOutput")
570 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
571 .field(
572 "args",
573 &self
574 .args
575 .iter()
576 .map(|(a, w)| format!("{}: {w}", &a.name))
577 .collect_vec(),
578 )
579 .field(
580 "available_flags",
581 &self
582 .available_flags
583 .iter()
584 .map(|(f, w)| format!("{f}: {w}"))
585 .collect_vec(),
586 )
587 .field(
588 "flags",
589 &self
590 .flags
591 .iter()
592 .map(|(f, w)| format!("{}: {w}", &f.name))
593 .collect_vec(),
594 )
595 .field("flag_awaiting_value", &self.flag_awaiting_value)
596 .field("errors", &self.errors)
597 .finish()
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_parse() {
607 let cmd = SpecCommand::builder()
608 .name("test")
609 .arg(SpecArg::builder().name("arg").build())
610 .flag(SpecFlag::builder().long("flag").build())
611 .build();
612 let spec = Spec {
613 name: "test".to_string(),
614 bin: "test".to_string(),
615 cmd,
616 ..Default::default()
617 };
618 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
619 let parsed = parse(&spec, &input).unwrap();
620 assert_eq!(parsed.cmds.len(), 1);
621 assert_eq!(parsed.cmds[0].name, "test");
622 assert_eq!(parsed.args.len(), 1);
623 assert_eq!(parsed.flags.len(), 1);
624 assert_eq!(parsed.available_flags.len(), 1);
625 }
626
627 #[test]
628 fn test_as_env() {
629 let cmd = SpecCommand::builder()
630 .name("test")
631 .arg(SpecArg::builder().name("arg").build())
632 .flag(SpecFlag::builder().long("flag").build())
633 .flag(
634 SpecFlag::builder()
635 .long("force")
636 .negate("--no-force")
637 .build(),
638 )
639 .build();
640 let spec = Spec {
641 name: "test".to_string(),
642 bin: "test".to_string(),
643 cmd,
644 ..Default::default()
645 };
646 let input = vec![
647 "test".to_string(),
648 "--flag".to_string(),
649 "--no-force".to_string(),
650 ];
651 let parsed = parse(&spec, &input).unwrap();
652 let env = parsed.as_env();
653 assert_eq!(env.len(), 2);
654 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
655 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
656 }
657
658 #[test]
659 fn test_arg_env_var() {
660 let cmd = SpecCommand::builder()
661 .name("test")
662 .arg(
663 SpecArg::builder()
664 .name("input")
665 .env("TEST_ARG_INPUT")
666 .required(true)
667 .build(),
668 )
669 .build();
670 let spec = Spec {
671 name: "test".to_string(),
672 bin: "test".to_string(),
673 cmd,
674 ..Default::default()
675 };
676
677 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
679
680 let input = vec!["test".to_string()];
681 let parsed = parse(&spec, &input).unwrap();
682
683 assert_eq!(parsed.args.len(), 1);
684 let arg = parsed.args.keys().next().unwrap();
685 assert_eq!(arg.name, "input");
686 let value = parsed.args.values().next().unwrap();
687 assert_eq!(value.to_string(), "test_file.txt");
688
689 std::env::remove_var("TEST_ARG_INPUT");
691 }
692
693 #[test]
694 fn test_flag_env_var_with_arg() {
695 let cmd = SpecCommand::builder()
696 .name("test")
697 .flag(
698 SpecFlag::builder()
699 .long("output")
700 .env("TEST_FLAG_OUTPUT")
701 .arg(SpecArg::builder().name("file").build())
702 .build(),
703 )
704 .build();
705 let spec = Spec {
706 name: "test".to_string(),
707 bin: "test".to_string(),
708 cmd,
709 ..Default::default()
710 };
711
712 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
714
715 let input = vec!["test".to_string()];
716 let parsed = parse(&spec, &input).unwrap();
717
718 assert_eq!(parsed.flags.len(), 1);
719 let flag = parsed.flags.keys().next().unwrap();
720 assert_eq!(flag.name, "output");
721 let value = parsed.flags.values().next().unwrap();
722 assert_eq!(value.to_string(), "output.txt");
723
724 std::env::remove_var("TEST_FLAG_OUTPUT");
726 }
727
728 #[test]
729 fn test_flag_env_var_boolean() {
730 let cmd = SpecCommand::builder()
731 .name("test")
732 .flag(
733 SpecFlag::builder()
734 .long("verbose")
735 .env("TEST_FLAG_VERBOSE")
736 .build(),
737 )
738 .build();
739 let spec = Spec {
740 name: "test".to_string(),
741 bin: "test".to_string(),
742 cmd,
743 ..Default::default()
744 };
745
746 std::env::set_var("TEST_FLAG_VERBOSE", "true");
748
749 let input = vec!["test".to_string()];
750 let parsed = parse(&spec, &input).unwrap();
751
752 assert_eq!(parsed.flags.len(), 1);
753 let flag = parsed.flags.keys().next().unwrap();
754 assert_eq!(flag.name, "verbose");
755 let value = parsed.flags.values().next().unwrap();
756 assert_eq!(value.to_string(), "true");
757
758 std::env::remove_var("TEST_FLAG_VERBOSE");
760 }
761
762 #[test]
763 fn test_env_var_precedence() {
764 let cmd = SpecCommand::builder()
766 .name("test")
767 .arg(
768 SpecArg::builder()
769 .name("input")
770 .env("TEST_PRECEDENCE_INPUT")
771 .required(true)
772 .build(),
773 )
774 .build();
775 let spec = Spec {
776 name: "test".to_string(),
777 bin: "test".to_string(),
778 cmd,
779 ..Default::default()
780 };
781
782 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
784
785 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
786 let parsed = parse(&spec, &input).unwrap();
787
788 assert_eq!(parsed.args.len(), 1);
789 let value = parsed.args.values().next().unwrap();
790 assert_eq!(value.to_string(), "cli_file.txt");
792
793 std::env::remove_var("TEST_PRECEDENCE_INPUT");
795 }
796
797 #[test]
798 fn test_flag_var_true_with_single_default() {
799 let cmd = SpecCommand::builder()
801 .name("test")
802 .flag(
803 SpecFlag::builder()
804 .long("foo")
805 .var(true)
806 .arg(SpecArg::builder().name("foo").build())
807 .default_value("bar")
808 .build(),
809 )
810 .build();
811 let spec = Spec {
812 name: "test".to_string(),
813 bin: "test".to_string(),
814 cmd,
815 ..Default::default()
816 };
817
818 let input = vec!["test".to_string()];
820 let parsed = parse(&spec, &input).unwrap();
821
822 assert_eq!(parsed.flags.len(), 1);
823 let flag = parsed.flags.keys().next().unwrap();
824 assert_eq!(flag.name, "foo");
825 let value = parsed.flags.values().next().unwrap();
826 match value {
828 ParseValue::MultiString(v) => {
829 assert_eq!(v.len(), 1);
830 assert_eq!(v[0], "bar");
831 }
832 _ => panic!("Expected MultiString, got {:?}", value),
833 }
834 }
835
836 #[test]
837 fn test_flag_var_true_with_multiple_defaults() {
838 let cmd = SpecCommand::builder()
840 .name("test")
841 .flag(
842 SpecFlag::builder()
843 .long("foo")
844 .var(true)
845 .arg(SpecArg::builder().name("foo").build())
846 .default_values(["xyz", "bar"])
847 .build(),
848 )
849 .build();
850 let spec = Spec {
851 name: "test".to_string(),
852 bin: "test".to_string(),
853 cmd,
854 ..Default::default()
855 };
856
857 let input = vec!["test".to_string()];
859 let parsed = parse(&spec, &input).unwrap();
860
861 assert_eq!(parsed.flags.len(), 1);
862 let value = parsed.flags.values().next().unwrap();
863 match value {
865 ParseValue::MultiString(v) => {
866 assert_eq!(v.len(), 2);
867 assert_eq!(v[0], "xyz");
868 assert_eq!(v[1], "bar");
869 }
870 _ => panic!("Expected MultiString, got {:?}", value),
871 }
872 }
873
874 #[test]
875 fn test_flag_var_false_with_default_remains_string() {
876 let cmd = SpecCommand::builder()
878 .name("test")
879 .flag(
880 SpecFlag::builder()
881 .long("foo")
882 .var(false) .arg(SpecArg::builder().name("foo").build())
884 .default_value("bar")
885 .build(),
886 )
887 .build();
888 let spec = Spec {
889 name: "test".to_string(),
890 bin: "test".to_string(),
891 cmd,
892 ..Default::default()
893 };
894
895 let input = vec!["test".to_string()];
897 let parsed = parse(&spec, &input).unwrap();
898
899 assert_eq!(parsed.flags.len(), 1);
900 let value = parsed.flags.values().next().unwrap();
901 match value {
903 ParseValue::String(s) => {
904 assert_eq!(s, "bar");
905 }
906 _ => panic!("Expected String, got {:?}", value),
907 }
908 }
909
910 #[test]
911 fn test_arg_var_true_with_single_default() {
912 let cmd = SpecCommand::builder()
914 .name("test")
915 .arg(
916 SpecArg::builder()
917 .name("files")
918 .var(true)
919 .default_value("default.txt")
920 .required(false)
921 .build(),
922 )
923 .build();
924 let spec = Spec {
925 name: "test".to_string(),
926 bin: "test".to_string(),
927 cmd,
928 ..Default::default()
929 };
930
931 let input = vec!["test".to_string()];
933 let parsed = parse(&spec, &input).unwrap();
934
935 assert_eq!(parsed.args.len(), 1);
936 let value = parsed.args.values().next().unwrap();
937 match value {
939 ParseValue::MultiString(v) => {
940 assert_eq!(v.len(), 1);
941 assert_eq!(v[0], "default.txt");
942 }
943 _ => panic!("Expected MultiString, got {:?}", value),
944 }
945 }
946
947 #[test]
948 fn test_arg_var_true_with_multiple_defaults() {
949 let cmd = SpecCommand::builder()
951 .name("test")
952 .arg(
953 SpecArg::builder()
954 .name("files")
955 .var(true)
956 .default_values(["file1.txt", "file2.txt"])
957 .required(false)
958 .build(),
959 )
960 .build();
961 let spec = Spec {
962 name: "test".to_string(),
963 bin: "test".to_string(),
964 cmd,
965 ..Default::default()
966 };
967
968 let input = vec!["test".to_string()];
970 let parsed = parse(&spec, &input).unwrap();
971
972 assert_eq!(parsed.args.len(), 1);
973 let value = parsed.args.values().next().unwrap();
974 match value {
976 ParseValue::MultiString(v) => {
977 assert_eq!(v.len(), 2);
978 assert_eq!(v[0], "file1.txt");
979 assert_eq!(v[1], "file2.txt");
980 }
981 _ => panic!("Expected MultiString, got {:?}", value),
982 }
983 }
984
985 #[test]
986 fn test_arg_var_false_with_default_remains_string() {
987 let cmd = SpecCommand::builder()
989 .name("test")
990 .arg(
991 SpecArg::builder()
992 .name("file")
993 .var(false)
994 .default_value("default.txt")
995 .required(false)
996 .build(),
997 )
998 .build();
999 let spec = Spec {
1000 name: "test".to_string(),
1001 bin: "test".to_string(),
1002 cmd,
1003 ..Default::default()
1004 };
1005
1006 let input = vec!["test".to_string()];
1008 let parsed = parse(&spec, &input).unwrap();
1009
1010 assert_eq!(parsed.args.len(), 1);
1011 let value = parsed.args.values().next().unwrap();
1012 match value {
1014 ParseValue::String(s) => {
1015 assert_eq!(s, "default.txt");
1016 }
1017 _ => panic!("Expected String, got {:?}", value),
1018 }
1019 }
1020
1021 #[test]
1022 fn test_default_subcommand() {
1023 let run_cmd = SpecCommand::builder()
1025 .name("run")
1026 .arg(SpecArg::builder().name("task").build())
1027 .build();
1028 let mut cmd = SpecCommand::builder().name("test").build();
1029 cmd.subcommands.insert("run".to_string(), run_cmd);
1030
1031 let spec = Spec {
1032 name: "test".to_string(),
1033 bin: "test".to_string(),
1034 cmd,
1035 default_subcommand: Some("run".to_string()),
1036 ..Default::default()
1037 };
1038
1039 let input = vec!["test".to_string(), "mytask".to_string()];
1041 let parsed = parse(&spec, &input).unwrap();
1042
1043 assert_eq!(parsed.cmds.len(), 2);
1045 assert_eq!(parsed.cmds[1].name, "run");
1046
1047 assert_eq!(parsed.args.len(), 1);
1049 let arg = parsed.args.keys().next().unwrap();
1050 assert_eq!(arg.name, "task");
1051 let value = parsed.args.values().next().unwrap();
1052 assert_eq!(value.to_string(), "mytask");
1053 }
1054
1055 #[test]
1056 fn test_default_subcommand_explicit_still_works() {
1057 let run_cmd = SpecCommand::builder()
1059 .name("run")
1060 .arg(SpecArg::builder().name("task").build())
1061 .build();
1062 let other_cmd = SpecCommand::builder()
1063 .name("other")
1064 .arg(SpecArg::builder().name("other_arg").build())
1065 .build();
1066 let mut cmd = SpecCommand::builder().name("test").build();
1067 cmd.subcommands.insert("run".to_string(), run_cmd);
1068 cmd.subcommands.insert("other".to_string(), other_cmd);
1069
1070 let spec = Spec {
1071 name: "test".to_string(),
1072 bin: "test".to_string(),
1073 cmd,
1074 default_subcommand: Some("run".to_string()),
1075 ..Default::default()
1076 };
1077
1078 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1080 let parsed = parse(&spec, &input).unwrap();
1081
1082 assert_eq!(parsed.cmds.len(), 2);
1084 assert_eq!(parsed.cmds[1].name, "other");
1085 }
1086
1087 #[test]
1088 fn test_restart_token() {
1089 let run_cmd = SpecCommand::builder()
1091 .name("run")
1092 .arg(SpecArg::builder().name("task").build())
1093 .restart_token(":::".to_string())
1094 .build();
1095 let mut cmd = SpecCommand::builder().name("test").build();
1096 cmd.subcommands.insert("run".to_string(), run_cmd);
1097
1098 let spec = Spec {
1099 name: "test".to_string(),
1100 bin: "test".to_string(),
1101 cmd,
1102 ..Default::default()
1103 };
1104
1105 let input = vec![
1107 "test".to_string(),
1108 "run".to_string(),
1109 "task1".to_string(),
1110 ":::".to_string(),
1111 "task2".to_string(),
1112 ];
1113 let parsed = parse(&spec, &input).unwrap();
1114
1115 assert_eq!(parsed.args.len(), 1);
1117 let value = parsed.args.values().next().unwrap();
1118 assert_eq!(value.to_string(), "task2");
1119 }
1120
1121 #[test]
1122 fn test_restart_token_multiple() {
1123 let run_cmd = SpecCommand::builder()
1125 .name("run")
1126 .arg(SpecArg::builder().name("task").build())
1127 .restart_token(":::".to_string())
1128 .build();
1129 let mut cmd = SpecCommand::builder().name("test").build();
1130 cmd.subcommands.insert("run".to_string(), run_cmd);
1131
1132 let spec = Spec {
1133 name: "test".to_string(),
1134 bin: "test".to_string(),
1135 cmd,
1136 ..Default::default()
1137 };
1138
1139 let input = vec![
1141 "test".to_string(),
1142 "run".to_string(),
1143 "task1".to_string(),
1144 ":::".to_string(),
1145 "task2".to_string(),
1146 ":::".to_string(),
1147 "task3".to_string(),
1148 ];
1149 let parsed = parse(&spec, &input).unwrap();
1150
1151 assert_eq!(parsed.args.len(), 1);
1153 let value = parsed.args.values().next().unwrap();
1154 assert_eq!(value.to_string(), "task3");
1155 }
1156
1157 #[test]
1158 fn test_restart_token_clears_flag_awaiting_value() {
1159 let run_cmd = SpecCommand::builder()
1161 .name("run")
1162 .arg(SpecArg::builder().name("task").build())
1163 .flag(
1164 SpecFlag::builder()
1165 .name("jobs")
1166 .long("jobs")
1167 .arg(SpecArg::builder().name("count").build())
1168 .build(),
1169 )
1170 .restart_token(":::".to_string())
1171 .build();
1172 let mut cmd = SpecCommand::builder().name("test").build();
1173 cmd.subcommands.insert("run".to_string(), run_cmd);
1174
1175 let spec = Spec {
1176 name: "test".to_string(),
1177 bin: "test".to_string(),
1178 cmd,
1179 ..Default::default()
1180 };
1181
1182 let input = vec![
1184 "test".to_string(),
1185 "run".to_string(),
1186 "task1".to_string(),
1187 "--jobs".to_string(),
1188 ":::".to_string(),
1189 "task2".to_string(),
1190 ];
1191 let parsed = parse(&spec, &input).unwrap();
1192
1193 assert_eq!(parsed.args.len(), 1);
1195 let value = parsed.args.values().next().unwrap();
1196 assert_eq!(value.to_string(), "task2");
1197 assert!(parsed.flag_awaiting_value.is_empty());
1199 }
1200
1201 #[test]
1202 fn test_restart_token_resets_double_dash() {
1203 let run_cmd = SpecCommand::builder()
1205 .name("run")
1206 .arg(SpecArg::builder().name("task").build())
1207 .arg(SpecArg::builder().name("extra_args").var(true).build())
1208 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1209 .restart_token(":::".to_string())
1210 .build();
1211 let mut cmd = SpecCommand::builder().name("test").build();
1212 cmd.subcommands.insert("run".to_string(), run_cmd);
1213
1214 let spec = Spec {
1215 name: "test".to_string(),
1216 bin: "test".to_string(),
1217 cmd,
1218 ..Default::default()
1219 };
1220
1221 let input = vec![
1223 "test".to_string(),
1224 "run".to_string(),
1225 "task1".to_string(),
1226 "--".to_string(),
1227 "extra".to_string(),
1228 ":::".to_string(),
1229 "--verbose".to_string(),
1230 "task2".to_string(),
1231 ];
1232 let parsed = parse(&spec, &input).unwrap();
1233
1234 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1236 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1238 let value = parsed.args.get(task_arg).unwrap();
1239 assert_eq!(value.to_string(), "task2");
1240 }
1241}