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 break;
241 }
242 }
243
244 let mut next_arg = out.cmd.args.first();
249 let mut enable_flags = true;
250 let mut grouped_flag = false;
251
252 while !input.is_empty() {
253 let mut w = input.pop_front().unwrap();
254
255 if w == "--" {
256 enable_flags = false;
257 continue;
258 }
259
260 if enable_flags && w.starts_with("--") {
262 grouped_flag = false;
263 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
264 if !val.is_empty() {
265 input.push_front(val.to_string());
266 }
267 if let Some(f) = out.available_flags.get(word) {
268 if f.arg.is_some() {
269 out.flag_awaiting_value.push(f.clone());
270 } else if f.count {
271 let arr = out
272 .flags
273 .entry(f.clone())
274 .or_insert_with(|| ParseValue::MultiBool(vec![]))
275 .try_as_multi_bool_mut()
276 .unwrap();
277 arr.push(true);
278 } else {
279 let negate = f.negate.clone().unwrap_or_default();
280 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
281 }
282 continue;
283 }
284 if is_help_arg(spec, &w) {
285 out.errors
286 .push(render_help_err(spec, &out.cmd, w.len() > 2));
287 return Ok(out);
288 }
289 }
290
291 if enable_flags && w.starts_with('-') && w.len() > 1 {
293 let short = w.chars().nth(1).unwrap();
294 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
295 if w.len() > 2 {
296 input.push_front(format!("-{}", &w[2..]));
297 grouped_flag = true;
298 }
299 if f.arg.is_some() {
300 out.flag_awaiting_value.push(f.clone());
301 } else if f.count {
302 let arr = out
303 .flags
304 .entry(f.clone())
305 .or_insert_with(|| ParseValue::MultiBool(vec![]))
306 .try_as_multi_bool_mut()
307 .unwrap();
308 arr.push(true);
309 } else {
310 let negate = f.negate.clone().unwrap_or_default();
311 out.flags.insert(f.clone(), ParseValue::Bool(w != negate));
312 }
313 continue;
314 }
315 if is_help_arg(spec, &w) {
316 out.errors
317 .push(render_help_err(spec, &out.cmd, w.len() > 2));
318 return Ok(out);
319 }
320 if grouped_flag {
321 grouped_flag = false;
322 w.remove(0);
323 }
324 }
325
326 if !out.flag_awaiting_value.is_empty() {
327 while let Some(flag) = out.flag_awaiting_value.pop() {
328 let arg = flag.arg.as_ref().unwrap();
329 if flag.var {
330 let arr = out
331 .flags
332 .entry(flag)
333 .or_insert_with(|| ParseValue::MultiString(vec![]))
334 .try_as_multi_string_mut()
335 .unwrap();
336 arr.push(w);
337 } else {
338 if let Some(choices) = &arg.choices {
339 if !choices.choices.contains(&w) {
340 if is_help_arg(spec, &w) {
341 out.errors
342 .push(render_help_err(spec, &out.cmd, w.len() > 2));
343 return Ok(out);
344 }
345 bail!(
346 "Invalid choice for option {}: {w}, expected one of {}",
347 flag.name,
348 choices.choices.join(", ")
349 );
350 }
351 }
352 out.flags.insert(flag, ParseValue::String(w));
353 }
354 w = "".to_string();
355 }
356 continue;
357 }
358
359 if let Some(arg) = next_arg {
360 if arg.var {
361 let arr = out
362 .args
363 .entry(arg.clone())
364 .or_insert_with(|| ParseValue::MultiString(vec![]))
365 .try_as_multi_string_mut()
366 .unwrap();
367 arr.push(w);
368 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
369 next_arg = out.cmd.args.get(out.args.len());
370 }
371 } else {
372 if let Some(choices) = &arg.choices {
373 if !choices.choices.contains(&w) {
374 if is_help_arg(spec, &w) {
375 out.errors
376 .push(render_help_err(spec, &out.cmd, w.len() > 2));
377 return Ok(out);
378 }
379 bail!(
380 "Invalid choice for arg {}: {w}, expected one of {}",
381 arg.name,
382 choices.choices.join(", ")
383 );
384 }
385 }
386 out.args.insert(arg.clone(), ParseValue::String(w));
387 next_arg = out.cmd.args.get(out.args.len());
388 }
389 continue;
390 }
391 if is_help_arg(spec, &w) {
392 out.errors
393 .push(render_help_err(spec, &out.cmd, w.len() > 2));
394 return Ok(out);
395 }
396 bail!("unexpected word: {w}");
397 }
398
399 for arg in out.cmd.args.iter().skip(out.args.len()) {
400 if arg.required && arg.default.is_empty() {
401 let has_env = arg
403 .env
404 .as_ref()
405 .map(|e| std::env::var(e).is_ok())
406 .unwrap_or(false);
407 if !has_env {
408 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
409 }
410 }
411 }
412
413 for flag in out.available_flags.values() {
414 if out.flags.contains_key(flag) {
415 continue;
416 }
417 let has_default =
418 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
419 let has_env = flag
420 .env
421 .as_ref()
422 .map(|e| std::env::var(e).is_ok())
423 .unwrap_or(false);
424 if flag.required && !has_default && !has_env {
425 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
426 }
427 }
428
429 Ok(out)
430}
431
432#[cfg(feature = "docs")]
433fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
434 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
435}
436
437#[cfg(not(feature = "docs"))]
438fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
439 UsageErr::Help("help".to_string())
440}
441
442fn is_help_arg(spec: &Spec, w: &str) -> bool {
443 spec.disable_help != Some(true)
444 && (w == "--help"
445 || w == "-h"
446 || w == "-?"
447 || (spec.cmd.subcommands.is_empty() && w == "help"))
448}
449
450impl ParseOutput {
451 pub fn as_env(&self) -> BTreeMap<String, String> {
452 let mut env = BTreeMap::new();
453 for (flag, val) in &self.flags {
454 let key = format!("usage_{}", flag.name.to_snake_case());
455 let val = match val {
456 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
457 ParseValue::String(s) => s.clone(),
458 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
459 ParseValue::MultiString(s) => shell_words::join(s),
460 };
461 env.insert(key, val);
462 }
463 for (arg, val) in &self.args {
464 let key = format!("usage_{}", arg.name.to_snake_case());
465 env.insert(key, val.to_string());
466 }
467 env
468 }
469}
470
471impl Display for ParseValue {
472 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
473 match self {
474 ParseValue::Bool(b) => write!(f, "{b}"),
475 ParseValue::String(s) => write!(f, "{s}"),
476 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
477 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
478 }
479 }
480}
481
482impl Debug for ParseOutput {
483 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
484 f.debug_struct("ParseOutput")
485 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
486 .field(
487 "args",
488 &self
489 .args
490 .iter()
491 .map(|(a, w)| format!("{}: {w}", &a.name))
492 .collect_vec(),
493 )
494 .field(
495 "available_flags",
496 &self
497 .available_flags
498 .iter()
499 .map(|(f, w)| format!("{f}: {w}"))
500 .collect_vec(),
501 )
502 .field(
503 "flags",
504 &self
505 .flags
506 .iter()
507 .map(|(f, w)| format!("{}: {w}", &f.name))
508 .collect_vec(),
509 )
510 .field("flag_awaiting_value", &self.flag_awaiting_value)
511 .field("errors", &self.errors)
512 .finish()
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
521 fn test_parse() {
522 let mut cmd = SpecCommand::default();
523 cmd.name = "test".to_string();
524 cmd.args = vec![SpecArg {
525 name: "arg".to_string(),
526 ..Default::default()
527 }];
528 cmd.flags = vec![SpecFlag {
529 name: "flag".to_string(),
530 long: vec!["flag".to_string()],
531 ..Default::default()
532 }];
533 let spec = Spec {
534 name: "test".to_string(),
535 bin: "test".to_string(),
536 cmd,
537 ..Default::default()
538 };
539 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
540 let parsed = parse(&spec, &input).unwrap();
541 assert_eq!(parsed.cmds.len(), 1);
542 assert_eq!(parsed.cmds[0].name, "test");
543 assert_eq!(parsed.args.len(), 1);
544 assert_eq!(parsed.flags.len(), 1);
545 assert_eq!(parsed.available_flags.len(), 1);
546 }
547
548 #[test]
549 fn test_as_env() {
550 let mut cmd = SpecCommand::default();
551 cmd.name = "test".to_string();
552 cmd.args = vec![SpecArg {
553 name: "arg".to_string(),
554 ..Default::default()
555 }];
556 cmd.flags = vec![
557 SpecFlag {
558 name: "flag".to_string(),
559 long: vec!["flag".to_string()],
560 ..Default::default()
561 },
562 SpecFlag {
563 name: "force".to_string(),
564 long: vec!["force".to_string()],
565 negate: Some("--no-force".to_string()),
566 ..Default::default()
567 },
568 ];
569 let spec = Spec {
570 name: "test".to_string(),
571 bin: "test".to_string(),
572 cmd,
573 ..Default::default()
574 };
575 let input = vec![
576 "test".to_string(),
577 "--flag".to_string(),
578 "--no-force".to_string(),
579 ];
580 let parsed = parse(&spec, &input).unwrap();
581 let env = parsed.as_env();
582 assert_eq!(env.len(), 2);
583 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
584 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
585 }
586
587 #[test]
588 fn test_arg_env_var() {
589 let mut cmd = SpecCommand::default();
590 cmd.name = "test".to_string();
591 cmd.args = vec![SpecArg {
592 name: "input".to_string(),
593 env: Some("TEST_ARG_INPUT".to_string()),
594 required: true,
595 ..Default::default()
596 }];
597 let spec = Spec {
598 name: "test".to_string(),
599 bin: "test".to_string(),
600 cmd,
601 ..Default::default()
602 };
603
604 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
606
607 let input = vec!["test".to_string()];
608 let parsed = parse(&spec, &input).unwrap();
609
610 assert_eq!(parsed.args.len(), 1);
611 let arg = parsed.args.keys().next().unwrap();
612 assert_eq!(arg.name, "input");
613 let value = parsed.args.values().next().unwrap();
614 assert_eq!(value.to_string(), "test_file.txt");
615
616 std::env::remove_var("TEST_ARG_INPUT");
618 }
619
620 #[test]
621 fn test_flag_env_var_with_arg() {
622 let mut cmd = SpecCommand::default();
623 cmd.name = "test".to_string();
624 cmd.flags = vec![SpecFlag {
625 name: "output".to_string(),
626 long: vec!["output".to_string()],
627 env: Some("TEST_FLAG_OUTPUT".to_string()),
628 arg: Some(SpecArg {
629 name: "file".to_string(),
630 ..Default::default()
631 }),
632 ..Default::default()
633 }];
634 let spec = Spec {
635 name: "test".to_string(),
636 bin: "test".to_string(),
637 cmd,
638 ..Default::default()
639 };
640
641 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
643
644 let input = vec!["test".to_string()];
645 let parsed = parse(&spec, &input).unwrap();
646
647 assert_eq!(parsed.flags.len(), 1);
648 let flag = parsed.flags.keys().next().unwrap();
649 assert_eq!(flag.name, "output");
650 let value = parsed.flags.values().next().unwrap();
651 assert_eq!(value.to_string(), "output.txt");
652
653 std::env::remove_var("TEST_FLAG_OUTPUT");
655 }
656
657 #[test]
658 fn test_flag_env_var_boolean() {
659 let mut cmd = SpecCommand::default();
660 cmd.name = "test".to_string();
661 cmd.flags = vec![SpecFlag {
662 name: "verbose".to_string(),
663 long: vec!["verbose".to_string()],
664 env: Some("TEST_FLAG_VERBOSE".to_string()),
665 ..Default::default()
666 }];
667 let spec = Spec {
668 name: "test".to_string(),
669 bin: "test".to_string(),
670 cmd,
671 ..Default::default()
672 };
673
674 std::env::set_var("TEST_FLAG_VERBOSE", "true");
676
677 let input = vec!["test".to_string()];
678 let parsed = parse(&spec, &input).unwrap();
679
680 assert_eq!(parsed.flags.len(), 1);
681 let flag = parsed.flags.keys().next().unwrap();
682 assert_eq!(flag.name, "verbose");
683 let value = parsed.flags.values().next().unwrap();
684 assert_eq!(value.to_string(), "true");
685
686 std::env::remove_var("TEST_FLAG_VERBOSE");
688 }
689
690 #[test]
691 fn test_env_var_precedence() {
692 let mut cmd = SpecCommand::default();
694 cmd.name = "test".to_string();
695 cmd.args = vec![SpecArg {
696 name: "input".to_string(),
697 env: Some("TEST_PRECEDENCE_INPUT".to_string()),
698 required: true,
699 ..Default::default()
700 }];
701 let spec = Spec {
702 name: "test".to_string(),
703 bin: "test".to_string(),
704 cmd,
705 ..Default::default()
706 };
707
708 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
710
711 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
712 let parsed = parse(&spec, &input).unwrap();
713
714 assert_eq!(parsed.args.len(), 1);
715 let value = parsed.args.values().next().unwrap();
716 assert_eq!(value.to_string(), "cli_file.txt");
718
719 std::env::remove_var("TEST_PRECEDENCE_INPUT");
721 }
722
723 #[test]
724 fn test_flag_var_true_with_single_default() {
725 let mut cmd = SpecCommand::default();
727 cmd.name = "test".to_string();
728 cmd.flags = vec![SpecFlag {
729 name: "foo".to_string(),
730 long: vec!["foo".to_string()],
731 var: true,
732 arg: Some(SpecArg {
733 name: "foo".to_string(),
734 ..Default::default()
735 }),
736 default: vec!["bar".to_string()],
737 ..Default::default()
738 }];
739 let spec = Spec {
740 name: "test".to_string(),
741 bin: "test".to_string(),
742 cmd,
743 ..Default::default()
744 };
745
746 let input = vec!["test".to_string()];
748 let parsed = parse(&spec, &input).unwrap();
749
750 assert_eq!(parsed.flags.len(), 1);
751 let flag = parsed.flags.keys().next().unwrap();
752 assert_eq!(flag.name, "foo");
753 let value = parsed.flags.values().next().unwrap();
754 match value {
756 ParseValue::MultiString(v) => {
757 assert_eq!(v.len(), 1);
758 assert_eq!(v[0], "bar");
759 }
760 _ => panic!("Expected MultiString, got {:?}", value),
761 }
762 }
763
764 #[test]
765 fn test_flag_var_true_with_multiple_defaults() {
766 let mut cmd = SpecCommand::default();
768 cmd.name = "test".to_string();
769 cmd.flags = vec![SpecFlag {
770 name: "foo".to_string(),
771 long: vec!["foo".to_string()],
772 var: true,
773 arg: Some(SpecArg {
774 name: "foo".to_string(),
775 ..Default::default()
776 }),
777 default: vec!["xyz".to_string(), "bar".to_string()],
778 ..Default::default()
779 }];
780 let spec = Spec {
781 name: "test".to_string(),
782 bin: "test".to_string(),
783 cmd,
784 ..Default::default()
785 };
786
787 let input = vec!["test".to_string()];
789 let parsed = parse(&spec, &input).unwrap();
790
791 assert_eq!(parsed.flags.len(), 1);
792 let value = parsed.flags.values().next().unwrap();
793 match value {
795 ParseValue::MultiString(v) => {
796 assert_eq!(v.len(), 2);
797 assert_eq!(v[0], "xyz");
798 assert_eq!(v[1], "bar");
799 }
800 _ => panic!("Expected MultiString, got {:?}", value),
801 }
802 }
803
804 #[test]
805 fn test_flag_var_false_with_default_remains_string() {
806 let mut cmd = SpecCommand::default();
808 cmd.name = "test".to_string();
809 cmd.flags = vec![SpecFlag {
810 name: "foo".to_string(),
811 long: vec!["foo".to_string()],
812 var: false, arg: Some(SpecArg {
814 name: "foo".to_string(),
815 ..Default::default()
816 }),
817 default: vec!["bar".to_string()],
818 ..Default::default()
819 }];
820 let spec = Spec {
821 name: "test".to_string(),
822 bin: "test".to_string(),
823 cmd,
824 ..Default::default()
825 };
826
827 let input = vec!["test".to_string()];
829 let parsed = parse(&spec, &input).unwrap();
830
831 assert_eq!(parsed.flags.len(), 1);
832 let value = parsed.flags.values().next().unwrap();
833 match value {
835 ParseValue::String(s) => {
836 assert_eq!(s, "bar");
837 }
838 _ => panic!("Expected String, got {:?}", value),
839 }
840 }
841
842 #[test]
843 fn test_arg_var_true_with_single_default() {
844 let mut cmd = SpecCommand::default();
846 cmd.name = "test".to_string();
847 cmd.args = vec![SpecArg {
848 name: "files".to_string(),
849 var: true,
850 default: vec!["default.txt".to_string()],
851 required: false,
852 ..Default::default()
853 }];
854 let spec = Spec {
855 name: "test".to_string(),
856 bin: "test".to_string(),
857 cmd,
858 ..Default::default()
859 };
860
861 let input = vec!["test".to_string()];
863 let parsed = parse(&spec, &input).unwrap();
864
865 assert_eq!(parsed.args.len(), 1);
866 let value = parsed.args.values().next().unwrap();
867 match value {
869 ParseValue::MultiString(v) => {
870 assert_eq!(v.len(), 1);
871 assert_eq!(v[0], "default.txt");
872 }
873 _ => panic!("Expected MultiString, got {:?}", value),
874 }
875 }
876
877 #[test]
878 fn test_arg_var_true_with_multiple_defaults() {
879 let mut cmd = SpecCommand::default();
881 cmd.name = "test".to_string();
882 cmd.args = vec![SpecArg {
883 name: "files".to_string(),
884 var: true,
885 default: vec!["file1.txt".to_string(), "file2.txt".to_string()],
886 required: false,
887 ..Default::default()
888 }];
889 let spec = Spec {
890 name: "test".to_string(),
891 bin: "test".to_string(),
892 cmd,
893 ..Default::default()
894 };
895
896 let input = vec!["test".to_string()];
898 let parsed = parse(&spec, &input).unwrap();
899
900 assert_eq!(parsed.args.len(), 1);
901 let value = parsed.args.values().next().unwrap();
902 match value {
904 ParseValue::MultiString(v) => {
905 assert_eq!(v.len(), 2);
906 assert_eq!(v[0], "file1.txt");
907 assert_eq!(v[1], "file2.txt");
908 }
909 _ => panic!("Expected MultiString, got {:?}", value),
910 }
911 }
912
913 #[test]
914 fn test_arg_var_false_with_default_remains_string() {
915 let mut cmd = SpecCommand::default();
917 cmd.name = "test".to_string();
918 cmd.args = vec![SpecArg {
919 name: "file".to_string(),
920 var: false,
921 default: vec!["default.txt".to_string()],
922 required: false,
923 ..Default::default()
924 }];
925 let spec = Spec {
926 name: "test".to_string(),
927 bin: "test".to_string(),
928 cmd,
929 ..Default::default()
930 };
931
932 let input = vec!["test".to_string()];
934 let parsed = parse(&spec, &input).unwrap();
935
936 assert_eq!(parsed.args.len(), 1);
937 let value = parsed.args.values().next().unwrap();
938 match value {
940 ParseValue::String(s) => {
941 assert_eq!(s, "default.txt");
942 }
943 _ => panic!("Expected String, got {:?}", value),
944 }
945 }
946}