1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, HashMap, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use std::sync::Arc;
9use strum::EnumTryAs;
10
11#[cfg(feature = "docs")]
12use crate::docs;
13use crate::error::UsageErr;
14use crate::spec::arg::SpecDoubleDashChoices;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17fn merge_subcommand_flags(
28 available: &mut BTreeMap<String, Arc<SpecFlag>>,
29 new_flags: BTreeMap<String, Arc<SpecFlag>>,
30) {
31 available.retain(|_, f| f.global);
33
34 let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
37
38 for (key, flag) in new_flags {
43 if flag.global {
44 available.insert(key, flag);
46 continue;
47 }
48
49 let inherited_global = flag.long.iter().find_map(|l| {
58 available
59 .get(&format!("--{l}"))
60 .filter(|f| f.global)
61 .cloned()
62 });
63 if let Some(global_flag) = inherited_global {
64 if available
69 .get(&key)
70 .is_some_and(|existing| existing.global && !Arc::ptr_eq(existing, &global_flag))
71 {
72 continue;
73 }
74 let merged = merged_cache
75 .entry(Arc::as_ptr(&flag) as usize)
76 .or_insert_with(|| {
77 let mut merged = (*global_flag).clone();
78 for s in &flag.short {
79 if !merged.short.contains(s) {
80 merged.short.push(*s);
81 }
82 }
83 for l in &flag.long {
84 if !merged.long.contains(l) {
85 merged.long.push(l.clone());
86 }
87 }
88 Arc::new(merged)
89 })
90 .clone();
91 available.insert(key, merged);
92 continue;
93 }
94
95 if available.contains_key(&key) {
99 continue;
100 }
101 available.insert(key, flag);
102 }
103}
104
105fn flag_keys(flag: &SpecFlag) -> Vec<String> {
108 let mut keys: Vec<String> = flag
109 .long
110 .iter()
111 .map(|l| format!("--{l}"))
112 .chain(flag.short.iter().map(|s| format!("-{s}")))
113 .collect();
114 if let Some(negate) = &flag.negate {
115 keys.push(negate.clone());
116 }
117 keys
118}
119
120fn get_flag_key(word: &str) -> &str {
123 if word.starts_with("--") {
124 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
126 } else if word.len() >= 2 {
127 &word[0..2]
129 } else {
130 word
131 }
132}
133
134pub struct ParseOutput {
135 pub cmd: SpecCommand,
136 pub cmds: Vec<SpecCommand>,
137 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
138 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
139 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
140 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
141 pub errors: Vec<UsageErr>,
142}
143
144#[derive(Debug, EnumTryAs, Clone)]
145pub enum ParseValue {
146 Bool(bool),
147 String(String),
148 MultiBool(Vec<bool>),
149 MultiString(Vec<String>),
150}
151
152#[non_exhaustive]
172pub struct Parser<'a> {
173 spec: &'a Spec,
174 env: Option<HashMap<String, String>>,
175}
176
177impl<'a> Parser<'a> {
178 pub fn new(spec: &'a Spec) -> Self {
180 Self { spec, env: None }
181 }
182
183 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
188 self.env = Some(env);
189 self
190 }
191
192 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
196 let custom_env = self.env.as_ref();
197 let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
198 trace!("{out:?}");
199
200 let get_env = |key: &str| -> Option<String> {
201 if let Some(env_map) = custom_env {
202 env_map.get(key).cloned()
203 } else {
204 std::env::var(key).ok()
205 }
206 };
207
208 for arg in out.cmd.args.iter().skip(out.args.len()) {
210 if let Some(env_var) = arg.env.as_ref() {
211 if let Some(env_value) = get_env(env_var) {
212 validate_choice_value(
213 ChoiceTarget::arg(arg),
214 &env_value,
215 arg.choices.as_ref(),
216 custom_env,
217 )?;
218 out.args
219 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
220 continue;
221 }
222 }
223 if !arg.default.is_empty() {
224 if arg.var {
226 validate_choice_values(
227 ChoiceTarget::arg(arg),
228 &arg.default,
229 arg.choices.as_ref(),
230 custom_env,
231 )?;
232 out.args.insert(
234 Arc::new(arg.clone()),
235 ParseValue::MultiString(arg.default.clone()),
236 );
237 } else {
238 validate_choice_value(
239 ChoiceTarget::arg(arg),
240 &arg.default[0],
241 arg.choices.as_ref(),
242 custom_env,
243 )?;
244 out.args.insert(
246 Arc::new(arg.clone()),
247 ParseValue::String(arg.default[0].clone()),
248 );
249 }
250 }
251 }
252
253 for flag in out.available_flags.values() {
255 if out.flags.contains_key(flag) {
256 continue;
257 }
258 if let Some(env_var) = flag.env.as_ref() {
259 if let Some(env_value) = get_env(env_var) {
260 if let Some(arg) = flag.arg.as_ref() {
261 validate_choice_value(
262 ChoiceTarget::option(flag),
263 &env_value,
264 arg.choices.as_ref(),
265 custom_env,
266 )?;
267 out.flags
268 .insert(Arc::clone(flag), ParseValue::String(env_value));
269 } else {
270 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
272 out.flags
273 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
274 }
275 continue;
276 }
277 }
278 if !flag.default.is_empty() {
280 if flag.var {
282 if let Some(arg) = flag.arg.as_ref() {
284 validate_choice_values(
285 ChoiceTarget::option(flag),
286 &flag.default,
287 arg.choices.as_ref(),
288 custom_env,
289 )?;
290 out.flags.insert(
291 Arc::clone(flag),
292 ParseValue::MultiString(flag.default.clone()),
293 );
294 } else {
295 let bools: Vec<bool> = flag
297 .default
298 .iter()
299 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
300 .collect();
301 out.flags
302 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
303 }
304 } else {
305 if let Some(arg) = flag.arg.as_ref() {
307 validate_choice_value(
308 ChoiceTarget::option(flag),
309 &flag.default[0],
310 arg.choices.as_ref(),
311 custom_env,
312 )?;
313 out.flags.insert(
314 Arc::clone(flag),
315 ParseValue::String(flag.default[0].clone()),
316 );
317 } else {
318 let is_true =
320 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
321 out.flags
322 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
323 }
324 }
325 }
326 if let Some(arg) = flag.arg.as_ref() {
328 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
329 if flag.var {
330 validate_choice_values(
331 ChoiceTarget::option(flag),
332 &arg.default,
333 arg.choices.as_ref(),
334 custom_env,
335 )?;
336 out.flags.insert(
337 Arc::clone(flag),
338 ParseValue::MultiString(arg.default.clone()),
339 );
340 } else {
341 validate_choice_value(
342 ChoiceTarget::option(flag),
343 &arg.default[0],
344 arg.choices.as_ref(),
345 custom_env,
346 )?;
347 out.flags
348 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
349 }
350 }
351 }
352 }
353 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
354 bail!("{err}");
355 }
356 if !out.errors.is_empty() {
357 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
358 }
359 Ok(out)
360 }
361}
362
363#[must_use = "parsing result should be used"]
370pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
371 Parser::new(spec).parse(input)
372}
373
374#[must_use = "parsing result should be used"]
378pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
379 parse_partial_with_env(spec, input, None)
380}
381
382fn parse_partial_with_env(
384 spec: &Spec,
385 input: &[String],
386 custom_env: Option<&HashMap<String, String>>,
387) -> Result<ParseOutput, miette::Error> {
388 trace!("parse_partial: {input:?}");
389 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
390 input.pop_front();
391
392 let gather_flags = |cmd: &SpecCommand| {
393 cmd.flags
394 .iter()
395 .flat_map(|f| {
396 let f = Arc::new(f.clone()); flag_keys(&f)
398 .into_iter()
399 .map(|key| (key, Arc::clone(&f)))
400 .collect::<Vec<_>>()
401 })
402 .collect()
403 };
404
405 let mut out = ParseOutput {
406 cmd: spec.cmd.clone(),
407 cmds: vec![spec.cmd.clone()],
408 args: IndexMap::new(),
409 flags: IndexMap::new(),
410 available_flags: gather_flags(&spec.cmd),
411 flag_awaiting_value: vec![],
412 errors: vec![],
413 };
414
415 let mut prefix_words: Vec<String> = vec![];
428 let mut idx = 0;
429 let mut used_default_subcommand = false;
432
433 while idx < input.len() {
434 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
435 let mut subcommand = subcommand.clone();
436 subcommand.mount(&prefix_words)?;
438 merge_subcommand_flags(&mut out.available_flags, gather_flags(&subcommand));
439 input.remove(idx);
441 out.cmds.push(subcommand.clone());
442 out.cmd = subcommand.clone();
443 prefix_words.clear();
444 } else if input[idx].starts_with('-') {
447 let word = &input[idx];
449 let flag_key = get_flag_key(word);
450
451 if let Some(f) = out.available_flags.get(flag_key) {
452 if f.global {
461 prefix_words.push(input[idx].clone());
462 idx += 1;
463
464 if f.arg.is_some()
467 && !word.contains('=')
468 && idx < input.len()
469 && !input[idx].starts_with('-')
470 {
471 prefix_words.push(input[idx].clone());
472 idx += 1;
473 }
474 } else {
475 break;
479 }
480 } else {
481 break;
484 }
485 } else {
486 if !used_default_subcommand {
489 if let Some(default_name) = &spec.default_subcommand {
490 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
491 let mut subcommand = subcommand.clone();
492 subcommand.mount(&prefix_words)?;
494 merge_subcommand_flags(&mut out.available_flags, gather_flags(&subcommand));
495 out.cmds.push(subcommand.clone());
496 out.cmd = subcommand.clone();
497 prefix_words.clear();
498 used_default_subcommand = true;
499 continue;
504 }
505 }
506 }
507 break;
509 }
510 }
511
512 let mut next_arg = out.cmd.args.first();
517 let mut enable_flags = true;
518 let mut grouped_flag = false;
519
520 while !input.is_empty() {
521 let mut w = input.pop_front().unwrap();
522
523 if let Some(ref restart_token) = out.cmd.restart_token {
526 if w == *restart_token {
527 out.args.clear();
529 next_arg = out.cmd.args.first();
530 out.flag_awaiting_value.clear(); enable_flags = true; continue;
534 }
535 }
536
537 if w == "--" {
538 enable_flags = false;
540
541 let should_preserve = next_arg
544 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
545 .unwrap_or(false);
546
547 if should_preserve {
548 } else {
550 continue;
552 }
553 }
554
555 if enable_flags && w.starts_with("--") {
557 grouped_flag = false;
558 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
559 if !val.is_empty() {
560 input.push_front(val.to_string());
561 }
562 if let Some(f) = out.available_flags.get(word) {
563 if f.arg.is_some() {
564 out.flag_awaiting_value.push(Arc::clone(f));
565 } else if f.count {
566 let arr = out
567 .flags
568 .entry(Arc::clone(f))
569 .or_insert_with(|| ParseValue::MultiBool(vec![]))
570 .try_as_multi_bool_mut()
571 .unwrap();
572 arr.push(true);
573 } else {
574 let negate = f.negate.clone().unwrap_or_default();
575 out.flags
576 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
577 }
578 continue;
579 }
580 if is_help_arg(spec, &w) {
581 out.errors
582 .push(render_help_err(spec, &out.cmd, w.len() > 2));
583 return Ok(out);
584 }
585 }
586
587 if enable_flags && w.starts_with('-') && w.len() > 1 {
589 let short = w.chars().nth(1).unwrap();
590 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
591 if w.len() > 2 {
592 input.push_front(format!("-{}", &w[2..]));
593 grouped_flag = true;
594 }
595 if f.arg.is_some() {
596 out.flag_awaiting_value.push(Arc::clone(f));
597 } else if f.count {
598 let arr = out
599 .flags
600 .entry(Arc::clone(f))
601 .or_insert_with(|| ParseValue::MultiBool(vec![]))
602 .try_as_multi_bool_mut()
603 .unwrap();
604 arr.push(true);
605 } else {
606 let negate = f.negate.clone().unwrap_or_default();
607 out.flags
608 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
609 }
610 continue;
611 }
612 if is_help_arg(spec, &w) {
613 out.errors
614 .push(render_help_err(spec, &out.cmd, w.len() > 2));
615 return Ok(out);
616 }
617 if grouped_flag {
618 grouped_flag = false;
619 w.remove(0);
620 }
621 }
622
623 if !out.flag_awaiting_value.is_empty() {
624 while let Some(flag) = out.flag_awaiting_value.pop() {
625 let arg = flag.arg.as_ref().unwrap();
626 if validate_choices(
627 spec,
628 &out.cmd,
629 &mut out.errors,
630 ChoiceTarget::option(&flag),
631 &w,
632 arg.choices.as_ref(),
633 custom_env,
634 )? {
635 return Ok(out);
636 }
637 if flag.var {
638 let arr = out
639 .flags
640 .entry(flag)
641 .or_insert_with(|| ParseValue::MultiString(vec![]))
642 .try_as_multi_string_mut()
643 .unwrap();
644 arr.push(w);
645 } else {
646 out.flags.insert(flag, ParseValue::String(w));
647 }
648 w = "".to_string();
649 }
650 continue;
651 }
652
653 if let Some(arg) = next_arg {
654 if validate_choices(
655 spec,
656 &out.cmd,
657 &mut out.errors,
658 ChoiceTarget::arg(arg),
659 &w,
660 arg.choices.as_ref(),
661 custom_env,
662 )? {
663 return Ok(out);
664 }
665 if arg.var {
666 let arr = out
667 .args
668 .entry(Arc::new(arg.clone()))
669 .or_insert_with(|| ParseValue::MultiString(vec![]))
670 .try_as_multi_string_mut()
671 .unwrap();
672 arr.push(w);
673 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
674 next_arg = out.cmd.args.get(out.args.len());
675 }
676 } else {
677 out.args
678 .insert(Arc::new(arg.clone()), ParseValue::String(w));
679 next_arg = out.cmd.args.get(out.args.len());
680 }
681 continue;
682 }
683 if is_help_arg(spec, &w) {
684 out.errors
685 .push(render_help_err(spec, &out.cmd, w.len() > 2));
686 return Ok(out);
687 }
688 bail!("unexpected word: {w}");
689 }
690
691 for arg in out.cmd.args.iter().skip(out.args.len()) {
692 if arg.required && arg.default.is_empty() {
693 let has_env = arg.env.as_ref().is_some_and(|e| {
695 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
696 || std::env::var(e).is_ok()
697 });
698 if !has_env {
699 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
700 }
701 }
702 }
703
704 for flag in out.available_flags.values() {
705 if out.flags.contains_key(flag) {
706 continue;
707 }
708 let has_default =
709 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
710 let has_env = flag.env.as_ref().is_some_and(|e| {
712 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
713 });
714 if flag.required && !has_default && !has_env {
715 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
716 }
717 }
718
719 for (arg, value) in &out.args {
721 if arg.var {
722 if let ParseValue::MultiString(values) = value {
723 if let Some(min) = arg.var_min {
724 if values.len() < min {
725 out.errors.push(UsageErr::VarArgTooFew {
726 name: arg.name.clone(),
727 min,
728 got: values.len(),
729 });
730 }
731 }
732 if let Some(max) = arg.var_max {
733 if values.len() > max {
734 out.errors.push(UsageErr::VarArgTooMany {
735 name: arg.name.clone(),
736 max,
737 got: values.len(),
738 });
739 }
740 }
741 }
742 }
743 }
744
745 for (flag, value) in &out.flags {
747 if flag.var {
748 let count = match value {
749 ParseValue::MultiString(values) => values.len(),
750 ParseValue::MultiBool(values) => values.len(),
751 _ => continue,
752 };
753 if let Some(min) = flag.var_min {
754 if count < min {
755 out.errors.push(UsageErr::VarFlagTooFew {
756 name: flag.name.clone(),
757 min,
758 got: count,
759 });
760 }
761 }
762 if let Some(max) = flag.var_max {
763 if count > max {
764 out.errors.push(UsageErr::VarFlagTooMany {
765 name: flag.name.clone(),
766 max,
767 got: count,
768 });
769 }
770 }
771 }
772 }
773
774 Ok(out)
775}
776
777#[cfg(feature = "docs")]
778fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
779 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
780}
781
782#[cfg(not(feature = "docs"))]
783fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
784 UsageErr::Help("help".to_string())
785}
786
787#[derive(Copy, Clone)]
788struct ChoiceTarget<'a> {
789 kind: &'a str,
790 name: &'a str,
791}
792
793impl<'a> ChoiceTarget<'a> {
794 fn arg(arg: &'a SpecArg) -> Self {
795 Self {
796 kind: "arg",
797 name: &arg.name,
798 }
799 }
800
801 fn option(flag: &'a SpecFlag) -> Self {
802 Self {
803 kind: "option",
804 name: &flag.name,
805 }
806 }
807}
808
809fn choice_error(
810 target: ChoiceTarget<'_>,
811 value: &str,
812 choices: Option<&SpecChoices>,
813 custom_env: Option<&HashMap<String, String>>,
814) -> Option<String> {
815 let choices = choices?;
816 let values = choices.values_with_env(custom_env);
817 if values.iter().any(|choice| choice == value) {
818 return None;
819 }
820 if let Some(env) = choices.env() {
821 if values.is_empty() {
822 return Some(format!(
823 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
824 target.kind, target.name,
825 ));
826 }
827 }
828 Some(format!(
829 "Invalid choice for {} {}: {value}, expected one of {}",
830 target.kind,
831 target.name,
832 values.join(", ")
833 ))
834}
835
836fn validate_choices(
837 spec: &Spec,
838 cmd: &SpecCommand,
839 errors: &mut Vec<UsageErr>,
840 target: ChoiceTarget<'_>,
841 value: &str,
842 choices: Option<&SpecChoices>,
843 custom_env: Option<&HashMap<String, String>>,
844) -> miette::Result<bool> {
845 if is_help_arg(spec, value)
846 && choices.is_some_and(|choices| {
847 !choices
848 .values_with_env(custom_env)
849 .iter()
850 .any(|choice| choice == value)
851 })
852 {
853 errors.push(render_help_err(spec, cmd, value.len() > 2));
854 return Ok(true);
855 }
856
857 if let Some(err) = choice_error(target, value, choices, custom_env) {
858 bail!("{err}");
859 }
860 Ok(false)
861}
862
863fn validate_choice_value(
864 target: ChoiceTarget<'_>,
865 value: &str,
866 choices: Option<&SpecChoices>,
867 custom_env: Option<&HashMap<String, String>>,
868) -> miette::Result<()> {
869 if let Some(err) = choice_error(target, value, choices, custom_env) {
870 bail!("{err}");
871 }
872 Ok(())
873}
874
875fn validate_choice_values(
876 target: ChoiceTarget<'_>,
877 values: &[String],
878 choices: Option<&SpecChoices>,
879 custom_env: Option<&HashMap<String, String>>,
880) -> miette::Result<()> {
881 for value in values {
882 validate_choice_value(target, value, choices, custom_env)?;
883 }
884 Ok(())
885}
886
887fn is_help_arg(spec: &Spec, w: &str) -> bool {
888 spec.disable_help != Some(true)
889 && (w == "--help"
890 || w == "-h"
891 || w == "-?"
892 || (spec.cmd.subcommands.is_empty() && w == "help"))
893}
894
895impl ParseOutput {
896 pub fn as_env(&self) -> BTreeMap<String, String> {
897 let mut env = BTreeMap::new();
898 for (flag, val) in &self.flags {
899 let key = format!("usage_{}", flag.name.to_snake_case());
900 let val = match val {
901 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
902 ParseValue::String(s) => s.clone(),
903 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
904 ParseValue::MultiString(s) => shell_words::join(s),
905 };
906 env.insert(key, val);
907 }
908 for (arg, val) in &self.args {
909 let key = format!("usage_{}", arg.name.to_snake_case());
910 env.insert(key, val.to_string());
911 }
912 env
913 }
914}
915
916impl Display for ParseValue {
917 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
918 match self {
919 ParseValue::Bool(b) => write!(f, "{b}"),
920 ParseValue::String(s) => write!(f, "{s}"),
921 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
922 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
923 }
924 }
925}
926
927impl Debug for ParseOutput {
928 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
929 f.debug_struct("ParseOutput")
930 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
931 .field(
932 "args",
933 &self
934 .args
935 .iter()
936 .map(|(a, w)| format!("{}: {w}", &a.name))
937 .collect_vec(),
938 )
939 .field(
940 "available_flags",
941 &self
942 .available_flags
943 .iter()
944 .map(|(f, w)| format!("{f}: {w}"))
945 .collect_vec(),
946 )
947 .field(
948 "flags",
949 &self
950 .flags
951 .iter()
952 .map(|(f, w)| format!("{}: {w}", &f.name))
953 .collect_vec(),
954 )
955 .field("flag_awaiting_value", &self.flag_awaiting_value)
956 .field("errors", &self.errors)
957 .finish()
958 }
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964
965 fn input(words: &[&str]) -> Vec<String> {
966 words.iter().map(|word| (*word).to_string()).collect()
967 }
968
969 fn spec_with_arg(arg: SpecArg) -> Spec {
970 let cmd = SpecCommand::builder().name("test").arg(arg).build();
971 Spec {
972 name: "test".to_string(),
973 bin: "test".to_string(),
974 cmd,
975 ..Default::default()
976 }
977 }
978
979 fn spec_with_flag(flag: SpecFlag) -> Spec {
980 let cmd = SpecCommand::builder().name("test").flag(flag).build();
981 Spec {
982 name: "test".to_string(),
983 bin: "test".to_string(),
984 cmd,
985 ..Default::default()
986 }
987 }
988
989 fn parse_with_env(
990 spec: &Spec,
991 words: &[&str],
992 env: &[(&str, &str)],
993 ) -> Result<ParseOutput, miette::Error> {
994 let env = env
995 .iter()
996 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
997 .collect();
998 Parser::new(spec).with_env(env).parse(&input(words))
999 }
1000
1001 fn first_string_value(parsed: &ParseOutput) -> &str {
1002 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1003 return value;
1004 }
1005 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1006 return value;
1007 }
1008 panic!("expected first parsed value to be ParseValue::String");
1009 }
1010
1011 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1012 let err = result.expect_err("expected parser error");
1013 assert_eq!(format!("{err}"), expected);
1014 }
1015
1016 #[cfg(feature = "unstable_choices_env")]
1017 fn spec_arg_choices_env(key: &str) -> Spec {
1018 spec_with_arg(
1019 SpecArg::builder()
1020 .name("env")
1021 .choices_env(key)
1022 .required(false)
1023 .build(),
1024 )
1025 }
1026
1027 #[cfg(feature = "unstable_choices_env")]
1028 fn spec_flag_choices_env(key: &str) -> Spec {
1029 spec_with_flag(
1030 SpecFlag::builder()
1031 .long("env")
1032 .arg(SpecArg::builder().name("env").choices_env(key).build())
1033 .build(),
1034 )
1035 }
1036
1037 #[test]
1038 fn test_parse() {
1039 let cmd = SpecCommand::builder()
1040 .name("test")
1041 .arg(SpecArg::builder().name("arg").build())
1042 .flag(SpecFlag::builder().long("flag").build())
1043 .build();
1044 let spec = Spec {
1045 name: "test".to_string(),
1046 bin: "test".to_string(),
1047 cmd,
1048 ..Default::default()
1049 };
1050 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1051 let parsed = parse(&spec, &input).unwrap();
1052 assert_eq!(parsed.cmds.len(), 1);
1053 assert_eq!(parsed.cmds[0].name, "test");
1054 assert_eq!(parsed.args.len(), 1);
1055 assert_eq!(parsed.flags.len(), 1);
1056 assert_eq!(parsed.available_flags.len(), 1);
1057 }
1058
1059 #[test]
1060 fn test_as_env() {
1061 let cmd = SpecCommand::builder()
1062 .name("test")
1063 .arg(SpecArg::builder().name("arg").build())
1064 .flag(SpecFlag::builder().long("flag").build())
1065 .flag(
1066 SpecFlag::builder()
1067 .long("force")
1068 .negate("--no-force")
1069 .build(),
1070 )
1071 .build();
1072 let spec = Spec {
1073 name: "test".to_string(),
1074 bin: "test".to_string(),
1075 cmd,
1076 ..Default::default()
1077 };
1078 let input = vec![
1079 "test".to_string(),
1080 "--flag".to_string(),
1081 "--no-force".to_string(),
1082 ];
1083 let parsed = parse(&spec, &input).unwrap();
1084 let env = parsed.as_env();
1085 assert_eq!(env.len(), 2);
1086 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1087 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1088 }
1089
1090 #[test]
1091 fn test_arg_env_var() {
1092 let cmd = SpecCommand::builder()
1093 .name("test")
1094 .arg(
1095 SpecArg::builder()
1096 .name("input")
1097 .env("TEST_ARG_INPUT")
1098 .required(true)
1099 .build(),
1100 )
1101 .build();
1102 let spec = Spec {
1103 name: "test".to_string(),
1104 bin: "test".to_string(),
1105 cmd,
1106 ..Default::default()
1107 };
1108
1109 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1111
1112 let input = vec!["test".to_string()];
1113 let parsed = parse(&spec, &input).unwrap();
1114
1115 assert_eq!(parsed.args.len(), 1);
1116 let arg = parsed.args.keys().next().unwrap();
1117 assert_eq!(arg.name, "input");
1118 let value = parsed.args.values().next().unwrap();
1119 assert_eq!(value.to_string(), "test_file.txt");
1120
1121 std::env::remove_var("TEST_ARG_INPUT");
1123 }
1124
1125 #[test]
1126 fn test_flag_env_var_with_arg() {
1127 let cmd = SpecCommand::builder()
1128 .name("test")
1129 .flag(
1130 SpecFlag::builder()
1131 .long("output")
1132 .env("TEST_FLAG_OUTPUT")
1133 .arg(SpecArg::builder().name("file").build())
1134 .build(),
1135 )
1136 .build();
1137 let spec = Spec {
1138 name: "test".to_string(),
1139 bin: "test".to_string(),
1140 cmd,
1141 ..Default::default()
1142 };
1143
1144 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1146
1147 let input = vec!["test".to_string()];
1148 let parsed = parse(&spec, &input).unwrap();
1149
1150 assert_eq!(parsed.flags.len(), 1);
1151 let flag = parsed.flags.keys().next().unwrap();
1152 assert_eq!(flag.name, "output");
1153 let value = parsed.flags.values().next().unwrap();
1154 assert_eq!(value.to_string(), "output.txt");
1155
1156 std::env::remove_var("TEST_FLAG_OUTPUT");
1158 }
1159
1160 #[test]
1161 fn test_flag_env_var_boolean() {
1162 let cmd = SpecCommand::builder()
1163 .name("test")
1164 .flag(
1165 SpecFlag::builder()
1166 .long("verbose")
1167 .env("TEST_FLAG_VERBOSE")
1168 .build(),
1169 )
1170 .build();
1171 let spec = Spec {
1172 name: "test".to_string(),
1173 bin: "test".to_string(),
1174 cmd,
1175 ..Default::default()
1176 };
1177
1178 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1180
1181 let input = vec!["test".to_string()];
1182 let parsed = parse(&spec, &input).unwrap();
1183
1184 assert_eq!(parsed.flags.len(), 1);
1185 let flag = parsed.flags.keys().next().unwrap();
1186 assert_eq!(flag.name, "verbose");
1187 let value = parsed.flags.values().next().unwrap();
1188 assert_eq!(value.to_string(), "true");
1189
1190 std::env::remove_var("TEST_FLAG_VERBOSE");
1192 }
1193
1194 #[test]
1195 fn test_env_var_precedence() {
1196 let cmd = SpecCommand::builder()
1198 .name("test")
1199 .arg(
1200 SpecArg::builder()
1201 .name("input")
1202 .env("TEST_PRECEDENCE_INPUT")
1203 .required(true)
1204 .build(),
1205 )
1206 .build();
1207 let spec = Spec {
1208 name: "test".to_string(),
1209 bin: "test".to_string(),
1210 cmd,
1211 ..Default::default()
1212 };
1213
1214 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1216
1217 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1218 let parsed = parse(&spec, &input).unwrap();
1219
1220 assert_eq!(parsed.args.len(), 1);
1221 let value = parsed.args.values().next().unwrap();
1222 assert_eq!(value.to_string(), "cli_file.txt");
1224
1225 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1227 }
1228
1229 #[test]
1230 fn test_flag_var_true_with_single_default() {
1231 let cmd = SpecCommand::builder()
1233 .name("test")
1234 .flag(
1235 SpecFlag::builder()
1236 .long("foo")
1237 .var(true)
1238 .arg(SpecArg::builder().name("foo").build())
1239 .default_value("bar")
1240 .build(),
1241 )
1242 .build();
1243 let spec = Spec {
1244 name: "test".to_string(),
1245 bin: "test".to_string(),
1246 cmd,
1247 ..Default::default()
1248 };
1249
1250 let input = vec!["test".to_string()];
1252 let parsed = parse(&spec, &input).unwrap();
1253
1254 assert_eq!(parsed.flags.len(), 1);
1255 let flag = parsed.flags.keys().next().unwrap();
1256 assert_eq!(flag.name, "foo");
1257 let value = parsed.flags.values().next().unwrap();
1258 match value {
1260 ParseValue::MultiString(v) => {
1261 assert_eq!(v.len(), 1);
1262 assert_eq!(v[0], "bar");
1263 }
1264 _ => panic!("Expected MultiString, got {:?}", value),
1265 }
1266 }
1267
1268 #[test]
1269 fn test_flag_var_true_with_multiple_defaults() {
1270 let cmd = SpecCommand::builder()
1272 .name("test")
1273 .flag(
1274 SpecFlag::builder()
1275 .long("foo")
1276 .var(true)
1277 .arg(SpecArg::builder().name("foo").build())
1278 .default_values(["xyz", "bar"])
1279 .build(),
1280 )
1281 .build();
1282 let spec = Spec {
1283 name: "test".to_string(),
1284 bin: "test".to_string(),
1285 cmd,
1286 ..Default::default()
1287 };
1288
1289 let input = vec!["test".to_string()];
1291 let parsed = parse(&spec, &input).unwrap();
1292
1293 assert_eq!(parsed.flags.len(), 1);
1294 let value = parsed.flags.values().next().unwrap();
1295 match value {
1297 ParseValue::MultiString(v) => {
1298 assert_eq!(v.len(), 2);
1299 assert_eq!(v[0], "xyz");
1300 assert_eq!(v[1], "bar");
1301 }
1302 _ => panic!("Expected MultiString, got {:?}", value),
1303 }
1304 }
1305
1306 #[test]
1307 fn test_flag_var_false_with_default_remains_string() {
1308 let cmd = SpecCommand::builder()
1310 .name("test")
1311 .flag(
1312 SpecFlag::builder()
1313 .long("foo")
1314 .var(false) .arg(SpecArg::builder().name("foo").build())
1316 .default_value("bar")
1317 .build(),
1318 )
1319 .build();
1320 let spec = Spec {
1321 name: "test".to_string(),
1322 bin: "test".to_string(),
1323 cmd,
1324 ..Default::default()
1325 };
1326
1327 let input = vec!["test".to_string()];
1329 let parsed = parse(&spec, &input).unwrap();
1330
1331 assert_eq!(parsed.flags.len(), 1);
1332 let value = parsed.flags.values().next().unwrap();
1333 match value {
1335 ParseValue::String(s) => {
1336 assert_eq!(s, "bar");
1337 }
1338 _ => panic!("Expected String, got {:?}", value),
1339 }
1340 }
1341
1342 #[test]
1343 fn test_arg_var_true_with_single_default() {
1344 let cmd = SpecCommand::builder()
1346 .name("test")
1347 .arg(
1348 SpecArg::builder()
1349 .name("files")
1350 .var(true)
1351 .default_value("default.txt")
1352 .required(false)
1353 .build(),
1354 )
1355 .build();
1356 let spec = Spec {
1357 name: "test".to_string(),
1358 bin: "test".to_string(),
1359 cmd,
1360 ..Default::default()
1361 };
1362
1363 let input = vec!["test".to_string()];
1365 let parsed = parse(&spec, &input).unwrap();
1366
1367 assert_eq!(parsed.args.len(), 1);
1368 let value = parsed.args.values().next().unwrap();
1369 match value {
1371 ParseValue::MultiString(v) => {
1372 assert_eq!(v.len(), 1);
1373 assert_eq!(v[0], "default.txt");
1374 }
1375 _ => panic!("Expected MultiString, got {:?}", value),
1376 }
1377 }
1378
1379 #[test]
1380 fn test_arg_var_true_with_multiple_defaults() {
1381 let cmd = SpecCommand::builder()
1383 .name("test")
1384 .arg(
1385 SpecArg::builder()
1386 .name("files")
1387 .var(true)
1388 .default_values(["file1.txt", "file2.txt"])
1389 .required(false)
1390 .build(),
1391 )
1392 .build();
1393 let spec = Spec {
1394 name: "test".to_string(),
1395 bin: "test".to_string(),
1396 cmd,
1397 ..Default::default()
1398 };
1399
1400 let input = vec!["test".to_string()];
1402 let parsed = parse(&spec, &input).unwrap();
1403
1404 assert_eq!(parsed.args.len(), 1);
1405 let value = parsed.args.values().next().unwrap();
1406 match value {
1408 ParseValue::MultiString(v) => {
1409 assert_eq!(v.len(), 2);
1410 assert_eq!(v[0], "file1.txt");
1411 assert_eq!(v[1], "file2.txt");
1412 }
1413 _ => panic!("Expected MultiString, got {:?}", value),
1414 }
1415 }
1416
1417 #[test]
1418 fn test_arg_var_false_with_default_remains_string() {
1419 let cmd = SpecCommand::builder()
1421 .name("test")
1422 .arg(
1423 SpecArg::builder()
1424 .name("file")
1425 .var(false)
1426 .default_value("default.txt")
1427 .required(false)
1428 .build(),
1429 )
1430 .build();
1431 let spec = Spec {
1432 name: "test".to_string(),
1433 bin: "test".to_string(),
1434 cmd,
1435 ..Default::default()
1436 };
1437
1438 let input = vec!["test".to_string()];
1440 let parsed = parse(&spec, &input).unwrap();
1441
1442 assert_eq!(parsed.args.len(), 1);
1443 let value = parsed.args.values().next().unwrap();
1444 match value {
1446 ParseValue::String(s) => {
1447 assert_eq!(s, "default.txt");
1448 }
1449 _ => panic!("Expected String, got {:?}", value),
1450 }
1451 }
1452
1453 #[test]
1454 fn test_scalar_defaults_validate_only_first_default_choice() {
1455 let specs = [
1456 spec_with_arg(
1457 SpecArg::builder()
1458 .name("env")
1459 .var(false)
1460 .default_values(["dev", "prod"])
1461 .choices(["dev"])
1462 .required(false)
1463 .build(),
1464 ),
1465 spec_with_flag(
1466 SpecFlag::builder()
1467 .long("env")
1468 .arg(
1469 SpecArg::builder()
1470 .name("env")
1471 .default_values(["dev", "prod"])
1472 .choices(["dev"])
1473 .build(),
1474 )
1475 .build(),
1476 ),
1477 ];
1478
1479 for spec in specs {
1480 let parsed = parse(&spec, &input(&["test"])).unwrap();
1481 assert_eq!(first_string_value(&parsed), "dev");
1482 }
1483 }
1484
1485 #[test]
1486 fn test_default_subcommand() {
1487 let run_cmd = SpecCommand::builder()
1489 .name("run")
1490 .arg(SpecArg::builder().name("task").build())
1491 .build();
1492 let mut cmd = SpecCommand::builder().name("test").build();
1493 cmd.subcommands.insert("run".to_string(), run_cmd);
1494
1495 let spec = Spec {
1496 name: "test".to_string(),
1497 bin: "test".to_string(),
1498 cmd,
1499 default_subcommand: Some("run".to_string()),
1500 ..Default::default()
1501 };
1502
1503 let input = vec!["test".to_string(), "mytask".to_string()];
1505 let parsed = parse(&spec, &input).unwrap();
1506
1507 assert_eq!(parsed.cmds.len(), 2);
1509 assert_eq!(parsed.cmds[1].name, "run");
1510
1511 assert_eq!(parsed.args.len(), 1);
1513 let arg = parsed.args.keys().next().unwrap();
1514 assert_eq!(arg.name, "task");
1515 let value = parsed.args.values().next().unwrap();
1516 assert_eq!(value.to_string(), "mytask");
1517 }
1518
1519 #[test]
1520 fn test_default_subcommand_explicit_still_works() {
1521 let run_cmd = SpecCommand::builder()
1523 .name("run")
1524 .arg(SpecArg::builder().name("task").build())
1525 .build();
1526 let other_cmd = SpecCommand::builder()
1527 .name("other")
1528 .arg(SpecArg::builder().name("other_arg").build())
1529 .build();
1530 let mut cmd = SpecCommand::builder().name("test").build();
1531 cmd.subcommands.insert("run".to_string(), run_cmd);
1532 cmd.subcommands.insert("other".to_string(), other_cmd);
1533
1534 let spec = Spec {
1535 name: "test".to_string(),
1536 bin: "test".to_string(),
1537 cmd,
1538 default_subcommand: Some("run".to_string()),
1539 ..Default::default()
1540 };
1541
1542 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1544 let parsed = parse(&spec, &input).unwrap();
1545
1546 assert_eq!(parsed.cmds.len(), 2);
1548 assert_eq!(parsed.cmds[1].name, "other");
1549 }
1550
1551 #[test]
1552 fn test_default_subcommand_with_nested_subcommands() {
1553 let say_cmd = SpecCommand::builder()
1557 .name("say")
1558 .arg(SpecArg::builder().name("name").build())
1559 .build();
1560 let mut run_cmd = SpecCommand::builder().name("run").build();
1561 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1562
1563 let mut cmd = SpecCommand::builder().name("test").build();
1564 cmd.subcommands.insert("run".to_string(), run_cmd);
1565
1566 let spec = Spec {
1567 name: "test".to_string(),
1568 bin: "test".to_string(),
1569 cmd,
1570 default_subcommand: Some("run".to_string()),
1571 ..Default::default()
1572 };
1573
1574 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1576 let parsed = parse(&spec, &input).unwrap();
1577
1578 assert_eq!(parsed.cmds.len(), 3);
1580 assert_eq!(parsed.cmds[0].name, "test");
1581 assert_eq!(parsed.cmds[1].name, "run");
1582 assert_eq!(parsed.cmds[2].name, "say");
1583
1584 assert_eq!(parsed.args.len(), 1);
1586 let arg = parsed.args.keys().next().unwrap();
1587 assert_eq!(arg.name, "name");
1588 let value = parsed.args.values().next().unwrap();
1589 assert_eq!(value.to_string(), "hello");
1590 }
1591
1592 fn mounted_global_flag_spec() -> Spec {
1600 let task_cmd = SpecCommand::builder()
1601 .name("sample:run")
1602 .arg(
1603 SpecArg::builder()
1604 .name("profile")
1605 .choices(["alpha", "beta", "gamma"])
1606 .build(),
1607 )
1608 .build();
1609 let mut run_cmd = SpecCommand::builder()
1611 .name("run")
1612 .flag(
1613 SpecFlag::builder()
1614 .name("cd")
1615 .short('C')
1616 .long("cd")
1617 .arg(SpecArg::builder().name("dir").build())
1618 .global(false)
1619 .build(),
1620 )
1621 .build();
1622 run_cmd
1623 .subcommands
1624 .insert("sample:run".to_string(), task_cmd);
1625
1626 let mut cmd = SpecCommand::builder()
1627 .name("test")
1628 .flag(
1629 SpecFlag::builder()
1630 .name("cd")
1631 .short('C')
1632 .long("cd")
1633 .arg(SpecArg::builder().name("dir").build())
1634 .global(true)
1635 .build(),
1636 )
1637 .build();
1638 cmd.subcommands.insert("run".to_string(), run_cmd);
1639
1640 Spec {
1641 name: "test".to_string(),
1642 bin: "test".to_string(),
1643 cmd,
1644 ..Default::default()
1645 }
1646 }
1647
1648 #[test]
1649 fn test_prefix_global_flag_does_not_pollute_choices() {
1650 let spec = mounted_global_flag_spec();
1657
1658 for words in [
1661 &["test", "-C", "/tmp", "run", "sample:run"][..],
1662 &["test", "--cd=/tmp", "run", "sample:run"][..],
1664 ] {
1665 let parsed = parse_partial(&spec, &input(words)).unwrap();
1666 assert_eq!(
1667 parsed
1668 .cmds
1669 .iter()
1670 .map(|c| c.name.as_str())
1671 .collect::<Vec<_>>(),
1672 vec!["test", "run", "sample:run"],
1673 );
1674 assert!(
1676 parsed.args.is_empty(),
1677 "args should be empty, got {:?}",
1678 parsed.args
1679 );
1680
1681 let cd = parsed
1684 .available_flags
1685 .get("--cd")
1686 .expect("--cd should remain available after descending into the subcommand");
1687 assert!(cd.global, "--cd must stay global after descent");
1688 assert!(
1689 parsed.available_flags.get("-C").is_some_and(|f| f.global),
1690 "-C must stay global after descent",
1691 );
1692
1693 assert_eq!(
1697 parsed.as_env().get("usage_cd").map(String::as_str),
1698 Some("/tmp"),
1699 "global flag value must survive in as_env(), got {:?}",
1700 parsed.as_env(),
1701 );
1702 }
1703
1704 let parsed = parse_partial(
1706 &spec,
1707 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
1708 )
1709 .unwrap();
1710 assert_eq!(parsed.args.len(), 1);
1711 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1712
1713 assert_parse_err(
1715 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
1716 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1717 );
1718 }
1719
1720 fn mounted_orphan_short_spec() -> Spec {
1725 let task_cmd = SpecCommand::builder()
1726 .name("sample:run")
1727 .arg(
1728 SpecArg::builder()
1729 .name("profile")
1730 .choices(["alpha", "beta", "gamma"])
1731 .build(),
1732 )
1733 .build();
1734 let mut run_cmd = SpecCommand::builder()
1737 .name("run")
1738 .flag(
1739 SpecFlag::builder()
1740 .name("raw")
1741 .short('r')
1742 .long("raw")
1743 .global(false)
1744 .build(),
1745 )
1746 .flag(
1747 SpecFlag::builder()
1748 .name("force")
1749 .short('f')
1750 .long("force")
1751 .global(false)
1752 .build(),
1753 )
1754 .build();
1755 run_cmd
1756 .subcommands
1757 .insert("sample:run".to_string(), task_cmd);
1758
1759 let mut cmd = SpecCommand::builder()
1761 .name("test")
1762 .flag(
1763 SpecFlag::builder()
1764 .name("raw")
1765 .long("raw")
1766 .global(true)
1767 .build(),
1768 )
1769 .build();
1770 cmd.subcommands.insert("run".to_string(), run_cmd);
1771
1772 Spec {
1773 name: "test".to_string(),
1774 bin: "test".to_string(),
1775 cmd,
1776 ..Default::default()
1777 }
1778 }
1779
1780 #[test]
1781 fn test_orphan_short_alias_survives_merge() {
1782 let spec = mounted_orphan_short_spec();
1788
1789 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
1790 assert_eq!(
1791 parsed
1792 .cmds
1793 .iter()
1794 .map(|c| c.name.as_str())
1795 .collect::<Vec<_>>(),
1796 vec!["test", "run", "sample:run"],
1797 );
1798
1799 assert!(
1802 parsed.available_flags.get("-r").is_some_and(|f| f.global),
1803 "-r must be merged onto the inherited global flag and stay global after descent",
1804 );
1805 assert!(
1806 parsed
1807 .available_flags
1808 .get("--raw")
1809 .is_some_and(|f| f.global),
1810 "--raw must stay global after descent",
1811 );
1812
1813 assert!(
1815 parsed.args.is_empty(),
1816 "args should be empty, got {:?}",
1817 parsed.args
1818 );
1819
1820 assert_eq!(
1822 parsed.as_env().get("usage_raw").map(String::as_str),
1823 Some("true"),
1824 "merged short's value must survive in as_env(), got {:?}",
1825 parsed.as_env(),
1826 );
1827
1828 assert!(
1831 parsed.available_flags.get("-f").is_none(),
1832 "purely-local -f must not be promoted onto a global",
1833 );
1834 assert!(
1835 parsed.available_flags.get("--force").is_none(),
1836 "purely-local --force must not be promoted onto a global",
1837 );
1838
1839 let parsed =
1841 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
1842 assert_eq!(parsed.args.len(), 1);
1843 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1844
1845 assert_parse_err(
1847 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
1848 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1849 );
1850 }
1851
1852 #[test]
1853 fn test_orphan_short_does_not_clobber_unrelated_global() {
1854 let run_cmd = SpecCommand::builder()
1859 .name("run")
1860 .flag(
1861 SpecFlag::builder()
1862 .name("raw")
1863 .short('r')
1864 .long("raw")
1865 .global(false)
1866 .build(),
1867 )
1868 .build();
1869 let mut cmd = SpecCommand::builder()
1870 .name("test")
1871 .flag(
1872 SpecFlag::builder()
1873 .name("raw")
1874 .long("raw")
1875 .global(true)
1876 .build(),
1877 )
1878 .flag(
1879 SpecFlag::builder()
1880 .name("restrict")
1881 .short('r')
1882 .long("restrict")
1883 .global(true)
1884 .build(),
1885 )
1886 .build();
1887 cmd.subcommands.insert("run".to_string(), run_cmd);
1888 let spec = Spec {
1889 name: "test".to_string(),
1890 bin: "test".to_string(),
1891 cmd,
1892 ..Default::default()
1893 };
1894
1895 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
1896 assert_eq!(
1898 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
1899 Some("restrict"),
1900 "-r must remain owned by the unrelated global it already belonged to",
1901 );
1902 assert!(parsed
1904 .available_flags
1905 .get("--raw")
1906 .is_some_and(|f| f.global));
1907 assert!(parsed
1908 .available_flags
1909 .get("--restrict")
1910 .is_some_and(|f| f.global));
1911 }
1912
1913 #[test]
1914 fn test_subcommand_alias_collision_keeps_last_owner() {
1915 let run_cmd = SpecCommand::builder()
1919 .name("run")
1920 .flag(
1921 SpecFlag::builder()
1922 .name("alpha")
1923 .short('x')
1924 .long("alpha")
1925 .global(false)
1926 .build(),
1927 )
1928 .flag(
1929 SpecFlag::builder()
1930 .name("beta")
1931 .short('x')
1932 .long("beta")
1933 .global(false)
1934 .build(),
1935 )
1936 .build();
1937 let mut cmd = SpecCommand::builder().name("test").build();
1938 cmd.subcommands.insert("run".to_string(), run_cmd);
1939 let spec = Spec {
1940 name: "test".to_string(),
1941 bin: "test".to_string(),
1942 cmd,
1943 ..Default::default()
1944 };
1945
1946 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
1947 assert_eq!(
1949 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
1950 Some("beta"),
1951 "the last-declared flag must keep a shared short alias",
1952 );
1953 assert_eq!(
1955 parsed
1956 .available_flags
1957 .get("--alpha")
1958 .map(|f| f.name.as_str()),
1959 Some("alpha"),
1960 );
1961 assert_eq!(
1962 parsed
1963 .available_flags
1964 .get("--beta")
1965 .map(|f| f.name.as_str()),
1966 Some("beta"),
1967 );
1968 }
1969
1970 #[test]
1971 fn test_default_subcommand_same_name_child() {
1972 let run_task = SpecCommand::builder()
1976 .name("run")
1977 .arg(SpecArg::builder().name("args").build())
1978 .build();
1979 let mut run_cmd = SpecCommand::builder().name("run").build();
1980 run_cmd.subcommands.insert("run".to_string(), run_task);
1981
1982 let mut cmd = SpecCommand::builder().name("test").build();
1983 cmd.subcommands.insert("run".to_string(), run_cmd);
1984
1985 let spec = Spec {
1986 name: "test".to_string(),
1987 bin: "test".to_string(),
1988 cmd,
1989 default_subcommand: Some("run".to_string()),
1990 ..Default::default()
1991 };
1992
1993 let input = vec!["test".to_string(), "run".to_string()];
1995 let parsed = parse(&spec, &input).unwrap();
1996
1997 assert_eq!(parsed.cmds.len(), 2);
1999 assert_eq!(parsed.cmds[0].name, "test");
2000 assert_eq!(parsed.cmds[1].name, "run");
2001
2002 let input = vec![
2004 "test".to_string(),
2005 "run".to_string(),
2006 "run".to_string(),
2007 "hello".to_string(),
2008 ];
2009 let parsed = parse(&spec, &input).unwrap();
2010
2011 assert_eq!(parsed.cmds.len(), 3);
2012 assert_eq!(parsed.cmds[0].name, "test");
2013 assert_eq!(parsed.cmds[1].name, "run");
2014 assert_eq!(parsed.cmds[2].name, "run");
2015 assert_eq!(parsed.args.len(), 1);
2016 let value = parsed.args.values().next().unwrap();
2017 assert_eq!(value.to_string(), "hello");
2018
2019 let mut run_cmd = SpecCommand::builder()
2023 .name("run")
2024 .arg(SpecArg::builder().name("task").build())
2025 .build();
2026 let run_task = SpecCommand::builder().name("run").build();
2027 run_cmd.subcommands.insert("run".to_string(), run_task);
2028
2029 let mut cmd = SpecCommand::builder().name("test").build();
2030 cmd.subcommands.insert("run".to_string(), run_cmd);
2031
2032 let spec = Spec {
2033 name: "test".to_string(),
2034 bin: "test".to_string(),
2035 cmd,
2036 default_subcommand: Some("run".to_string()),
2037 ..Default::default()
2038 };
2039
2040 let input = vec!["test".to_string(), "other".to_string()];
2041 let parsed = parse(&spec, &input).unwrap();
2042
2043 assert_eq!(parsed.cmds.len(), 2);
2046 assert_eq!(parsed.cmds[0].name, "test");
2047 assert_eq!(parsed.cmds[1].name, "run");
2048
2049 assert_eq!(parsed.args.len(), 1);
2051 let value = parsed.args.values().next().unwrap();
2052 assert_eq!(value.to_string(), "other");
2053 }
2054
2055 #[test]
2056 fn test_restart_token() {
2057 let run_cmd = SpecCommand::builder()
2059 .name("run")
2060 .arg(SpecArg::builder().name("task").build())
2061 .restart_token(":::".to_string())
2062 .build();
2063 let mut cmd = SpecCommand::builder().name("test").build();
2064 cmd.subcommands.insert("run".to_string(), run_cmd);
2065
2066 let spec = Spec {
2067 name: "test".to_string(),
2068 bin: "test".to_string(),
2069 cmd,
2070 ..Default::default()
2071 };
2072
2073 let input = vec![
2075 "test".to_string(),
2076 "run".to_string(),
2077 "task1".to_string(),
2078 ":::".to_string(),
2079 "task2".to_string(),
2080 ];
2081 let parsed = parse(&spec, &input).unwrap();
2082
2083 assert_eq!(parsed.args.len(), 1);
2085 let value = parsed.args.values().next().unwrap();
2086 assert_eq!(value.to_string(), "task2");
2087 }
2088
2089 #[test]
2090 fn test_restart_token_multiple() {
2091 let run_cmd = SpecCommand::builder()
2093 .name("run")
2094 .arg(SpecArg::builder().name("task").build())
2095 .restart_token(":::".to_string())
2096 .build();
2097 let mut cmd = SpecCommand::builder().name("test").build();
2098 cmd.subcommands.insert("run".to_string(), run_cmd);
2099
2100 let spec = Spec {
2101 name: "test".to_string(),
2102 bin: "test".to_string(),
2103 cmd,
2104 ..Default::default()
2105 };
2106
2107 let input = vec![
2109 "test".to_string(),
2110 "run".to_string(),
2111 "task1".to_string(),
2112 ":::".to_string(),
2113 "task2".to_string(),
2114 ":::".to_string(),
2115 "task3".to_string(),
2116 ];
2117 let parsed = parse(&spec, &input).unwrap();
2118
2119 assert_eq!(parsed.args.len(), 1);
2121 let value = parsed.args.values().next().unwrap();
2122 assert_eq!(value.to_string(), "task3");
2123 }
2124
2125 #[test]
2126 fn test_restart_token_clears_flag_awaiting_value() {
2127 let run_cmd = SpecCommand::builder()
2129 .name("run")
2130 .arg(SpecArg::builder().name("task").build())
2131 .flag(
2132 SpecFlag::builder()
2133 .name("jobs")
2134 .long("jobs")
2135 .arg(SpecArg::builder().name("count").build())
2136 .build(),
2137 )
2138 .restart_token(":::".to_string())
2139 .build();
2140 let mut cmd = SpecCommand::builder().name("test").build();
2141 cmd.subcommands.insert("run".to_string(), run_cmd);
2142
2143 let spec = Spec {
2144 name: "test".to_string(),
2145 bin: "test".to_string(),
2146 cmd,
2147 ..Default::default()
2148 };
2149
2150 let input = vec![
2152 "test".to_string(),
2153 "run".to_string(),
2154 "task1".to_string(),
2155 "--jobs".to_string(),
2156 ":::".to_string(),
2157 "task2".to_string(),
2158 ];
2159 let parsed = parse(&spec, &input).unwrap();
2160
2161 assert_eq!(parsed.args.len(), 1);
2163 let value = parsed.args.values().next().unwrap();
2164 assert_eq!(value.to_string(), "task2");
2165 assert!(parsed.flag_awaiting_value.is_empty());
2167 }
2168
2169 #[test]
2170 fn test_restart_token_resets_double_dash() {
2171 let run_cmd = SpecCommand::builder()
2173 .name("run")
2174 .arg(SpecArg::builder().name("task").build())
2175 .arg(SpecArg::builder().name("extra_args").var(true).build())
2176 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
2177 .restart_token(":::".to_string())
2178 .build();
2179 let mut cmd = SpecCommand::builder().name("test").build();
2180 cmd.subcommands.insert("run".to_string(), run_cmd);
2181
2182 let spec = Spec {
2183 name: "test".to_string(),
2184 bin: "test".to_string(),
2185 cmd,
2186 ..Default::default()
2187 };
2188
2189 let input = vec![
2191 "test".to_string(),
2192 "run".to_string(),
2193 "task1".to_string(),
2194 "--".to_string(),
2195 "extra".to_string(),
2196 ":::".to_string(),
2197 "--verbose".to_string(),
2198 "task2".to_string(),
2199 ];
2200 let parsed = parse(&spec, &input).unwrap();
2201
2202 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
2204 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2206 let value = parsed.args.get(task_arg).unwrap();
2207 assert_eq!(value.to_string(), "task2");
2208 }
2209
2210 #[test]
2211 fn test_double_dashes_without_preserve() {
2212 let run_cmd = SpecCommand::builder()
2214 .name("run")
2215 .arg(SpecArg::builder().name("args").var(true).build())
2216 .build();
2217 let mut cmd = SpecCommand::builder().name("test").build();
2218 cmd.subcommands.insert("run".to_string(), run_cmd);
2219
2220 let spec = Spec {
2221 name: "test".to_string(),
2222 bin: "test".to_string(),
2223 cmd,
2224 ..Default::default()
2225 };
2226
2227 let input = vec![
2229 "test".to_string(),
2230 "run".to_string(),
2231 "arg1".to_string(),
2232 "--".to_string(),
2233 "arg2".to_string(),
2234 "--".to_string(),
2235 "arg3".to_string(),
2236 ];
2237 let parsed = parse(&spec, &input).unwrap();
2238
2239 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2240 let value = parsed.args.get(args_arg).unwrap();
2241 assert_eq!(value.to_string(), "arg1 arg2 arg3");
2242 }
2243
2244 #[test]
2245 fn test_double_dashes_with_preserve() {
2246 let run_cmd = SpecCommand::builder()
2248 .name("run")
2249 .arg(
2250 SpecArg::builder()
2251 .name("args")
2252 .var(true)
2253 .double_dash(SpecDoubleDashChoices::Preserve)
2254 .build(),
2255 )
2256 .build();
2257 let mut cmd = SpecCommand::builder().name("test").build();
2258 cmd.subcommands.insert("run".to_string(), run_cmd);
2259
2260 let spec = Spec {
2261 name: "test".to_string(),
2262 bin: "test".to_string(),
2263 cmd,
2264 ..Default::default()
2265 };
2266
2267 let input = vec![
2269 "test".to_string(),
2270 "run".to_string(),
2271 "arg1".to_string(),
2272 "--".to_string(),
2273 "arg2".to_string(),
2274 "--".to_string(),
2275 "arg3".to_string(),
2276 ];
2277 let parsed = parse(&spec, &input).unwrap();
2278
2279 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2280 let value = parsed.args.get(args_arg).unwrap();
2281 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
2282 }
2283
2284 #[test]
2285 fn test_double_dashes_with_preserve_only_dashes() {
2286 let run_cmd = SpecCommand::builder()
2289 .name("run")
2290 .arg(
2291 SpecArg::builder()
2292 .name("args")
2293 .var(true)
2294 .double_dash(SpecDoubleDashChoices::Preserve)
2295 .build(),
2296 )
2297 .build();
2298 let mut cmd = SpecCommand::builder().name("test").build();
2299 cmd.subcommands.insert("run".to_string(), run_cmd);
2300
2301 let spec = Spec {
2302 name: "test".to_string(),
2303 bin: "test".to_string(),
2304 cmd,
2305 ..Default::default()
2306 };
2307
2308 let input = vec![
2310 "test".to_string(),
2311 "run".to_string(),
2312 "--".to_string(),
2313 "--".to_string(),
2314 ];
2315 let parsed = parse(&spec, &input).unwrap();
2316
2317 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2318 let value = parsed.args.get(args_arg).unwrap();
2319 assert_eq!(value.to_string(), "-- --");
2320 }
2321
2322 #[test]
2323 fn test_double_dashes_with_preserve_multiple_args() {
2324 let run_cmd = SpecCommand::builder()
2326 .name("run")
2327 .arg(SpecArg::builder().name("task").build())
2328 .arg(
2329 SpecArg::builder()
2330 .name("extra_args")
2331 .var(true)
2332 .double_dash(SpecDoubleDashChoices::Preserve)
2333 .build(),
2334 )
2335 .build();
2336 let mut cmd = SpecCommand::builder().name("test").build();
2337 cmd.subcommands.insert("run".to_string(), run_cmd);
2338
2339 let spec = Spec {
2340 name: "test".to_string(),
2341 bin: "test".to_string(),
2342 cmd,
2343 ..Default::default()
2344 };
2345
2346 let input = vec![
2349 "test".to_string(),
2350 "run".to_string(),
2351 "task1".to_string(),
2352 "--".to_string(),
2353 "arg1".to_string(),
2354 "--".to_string(),
2355 "--foo".to_string(),
2356 ];
2357 let parsed = parse(&spec, &input).unwrap();
2358
2359 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2360 let task_value = parsed.args.get(task_arg).unwrap();
2361 assert_eq!(task_value.to_string(), "task1");
2362
2363 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
2364 let extra_value = parsed.args.get(extra_arg).unwrap();
2365 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
2366 }
2367
2368 #[test]
2369 fn test_parser_with_custom_env_for_required_arg() {
2370 let spec = spec_with_arg(
2371 SpecArg::builder()
2372 .name("name")
2373 .env("NAME")
2374 .required(true)
2375 .build(),
2376 );
2377 std::env::remove_var("NAME");
2378
2379 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
2380 .expect("parse should succeed with custom env");
2381 assert_eq!(parsed.args.len(), 1);
2382 assert_eq!(first_string_value(&parsed), "john");
2383 }
2384
2385 #[test]
2386 fn test_parser_with_custom_env_for_required_flag() {
2387 let spec = spec_with_flag(
2388 SpecFlag::builder()
2389 .long("name")
2390 .env("NAME")
2391 .required(true)
2392 .arg(SpecArg::builder().name("name").build())
2393 .build(),
2394 );
2395 std::env::remove_var("NAME");
2396
2397 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
2398 .expect("parse should succeed with custom env");
2399 assert_eq!(parsed.flags.len(), 1);
2400 assert_eq!(first_string_value(&parsed), "jane");
2401 }
2402
2403 #[test]
2404 fn test_parser_with_custom_env_still_fails_when_missing() {
2405 let spec = spec_with_arg(
2406 SpecArg::builder()
2407 .name("name")
2408 .env("NAME")
2409 .required(true)
2410 .build(),
2411 );
2412 std::env::remove_var("NAME");
2413 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
2414 }
2415
2416 #[test]
2417 fn test_parser_does_not_treat_env_choice_value_as_help() {
2418 let spec = spec_with_arg(
2419 SpecArg::builder()
2420 .name("env")
2421 .env("CURRENT_ENV")
2422 .choices(["dev", "staging"])
2423 .required(false)
2424 .build(),
2425 );
2426
2427 assert_parse_err(
2428 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
2429 "Invalid choice for arg env: --help, expected one of dev, staging",
2430 );
2431 }
2432
2433 #[test]
2434 fn test_parser_does_not_treat_default_choice_value_as_help() {
2435 let spec = spec_with_flag(
2436 SpecFlag::builder()
2437 .long("env")
2438 .arg(
2439 SpecArg::builder()
2440 .name("env")
2441 .choices(["dev", "staging"])
2442 .build(),
2443 )
2444 .default_value("--help")
2445 .build(),
2446 );
2447
2448 assert_parse_err(
2449 parse_with_env(&spec, &["test"], &[]),
2450 "Invalid choice for option env: --help, expected one of dev, staging",
2451 );
2452 }
2453
2454 #[cfg(feature = "unstable_choices_env")]
2455 #[test]
2456 fn test_parser_arg_choices_from_custom_env() {
2457 let spec = spec_arg_choices_env("DEPLOY_ENVS");
2458
2459 let parsed =
2460 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
2461 assert_eq!(first_string_value(&parsed), "bar");
2462
2463 assert_parse_err(
2464 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
2465 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
2466 );
2467 assert_parse_err(
2468 parse_with_env(&spec, &["test", "prod"], &[]),
2469 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
2470 );
2471 }
2472
2473 #[cfg(feature = "unstable_choices_env")]
2474 #[test]
2475 fn test_parser_validates_flag_choices_from_custom_env() {
2476 let spec = spec_flag_choices_env("DEPLOY_ENVS");
2477 let parsed = parse_with_env(
2478 &spec,
2479 &["test", "--env", "baz"],
2480 &[("DEPLOY_ENVS", "foo,bar baz")],
2481 )
2482 .unwrap();
2483 assert_eq!(first_string_value(&parsed), "baz");
2484 }
2485
2486 #[cfg(feature = "unstable_choices_env")]
2487 #[test]
2488 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
2489 let arg_env_spec = spec_with_arg(
2490 SpecArg::builder()
2491 .name("env")
2492 .env("CURRENT_ENV")
2493 .choices_env("DEPLOY_ENVS")
2494 .build(),
2495 );
2496 assert_parse_err(
2497 parse_with_env(
2498 &arg_env_spec,
2499 &["test"],
2500 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
2501 ),
2502 "Invalid choice for arg env: prod, expected one of dev, staging",
2503 );
2504
2505 let flag_default_spec = spec_with_flag(
2506 SpecFlag::builder()
2507 .long("env")
2508 .arg(
2509 SpecArg::builder()
2510 .name("env")
2511 .choices_env("DEPLOY_ENVS")
2512 .build(),
2513 )
2514 .default_value("prod")
2515 .build(),
2516 );
2517 assert_parse_err(
2518 parse_with_env(
2519 &flag_default_spec,
2520 &["test"],
2521 &[("DEPLOY_ENVS", "dev,staging")],
2522 ),
2523 "Invalid choice for option env: prod, expected one of dev, staging",
2524 );
2525 }
2526
2527 #[test]
2528 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
2529 let spec: Spec = r#"
2530 flag "-v --verbose" var=#true
2531 arg "[database]" default="myapp_dev"
2532 arg "[args...]"
2533 "#
2534 .parse()
2535 .unwrap();
2536 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2537 .into_iter()
2538 .map(String::from)
2539 .collect();
2540 let parsed = parse(&spec, &input).unwrap();
2541 let env = parsed.as_env();
2542 assert_eq!(env.get("usage_database").unwrap(), "mydb");
2543 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
2544 }
2545
2546 #[test]
2547 fn test_variadic_arg_captures_unknown_flags() {
2548 let cmd = SpecCommand::builder()
2549 .name("test")
2550 .flag(SpecFlag::builder().short('v').long("verbose").build())
2551 .arg(SpecArg::builder().name("database").required(false).build())
2552 .arg(
2553 SpecArg::builder()
2554 .name("args")
2555 .required(false)
2556 .var(true)
2557 .build(),
2558 )
2559 .build();
2560 let spec = Spec {
2561 name: "test".to_string(),
2562 bin: "test".to_string(),
2563 cmd,
2564 ..Default::default()
2565 };
2566
2567 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2569 .into_iter()
2570 .map(String::from)
2571 .collect();
2572 let parsed = parse(&spec, &input).unwrap();
2573 assert_eq!(parsed.args.len(), 2);
2574 let args_val = parsed
2575 .args
2576 .iter()
2577 .find(|(a, _)| a.name == "args")
2578 .unwrap()
2579 .1;
2580 match args_val {
2581 ParseValue::MultiString(v) => {
2582 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2583 }
2584 _ => panic!("Expected MultiString, got {:?}", args_val),
2585 }
2586 }
2587
2588 #[test]
2589 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
2590 let cmd = SpecCommand::builder()
2591 .name("test")
2592 .flag(SpecFlag::builder().short('v').long("verbose").build())
2593 .arg(SpecArg::builder().name("database").required(false).build())
2594 .arg(
2595 SpecArg::builder()
2596 .name("args")
2597 .required(false)
2598 .var(true)
2599 .build(),
2600 )
2601 .build();
2602 let spec = Spec {
2603 name: "test".to_string(),
2604 bin: "test".to_string(),
2605 cmd,
2606 ..Default::default()
2607 };
2608
2609 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
2611 .into_iter()
2612 .map(String::from)
2613 .collect();
2614 let parsed = parse(&spec, &input).unwrap();
2615 assert_eq!(parsed.args.len(), 2);
2616 let args_val = parsed
2617 .args
2618 .iter()
2619 .find(|(a, _)| a.name == "args")
2620 .unwrap()
2621 .1;
2622 match args_val {
2623 ParseValue::MultiString(v) => {
2624 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2625 }
2626 _ => panic!("Expected MultiString, got {:?}", args_val),
2627 }
2628 }
2629}