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