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 w.starts_with('-')
565 && out
566 .flag_awaiting_value
567 .last()
568 .is_some_and(|flag| flag.allow_hyphen_values())
569 {
570 let should_return = drain_pending_flag_values(
571 spec,
572 &out.cmd,
573 &mut out.errors,
574 &mut out.flags,
575 &mut out.flag_awaiting_value,
576 &mut w,
577 custom_env,
578 )?;
579 if should_return {
580 return Ok(out);
581 }
582 continue;
583 }
584
585 if enable_flags && w.starts_with("--") {
587 grouped_flag = false;
588 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
589 if let Some(f) = out.available_flags.get(word) {
590 if !val.is_empty() {
594 input.push_front(val.to_string());
595 }
596 if f.arg.is_some() {
597 out.flag_awaiting_value.push(Arc::clone(f));
598 } else if f.count {
599 let arr = out
600 .flags
601 .entry(Arc::clone(f))
602 .or_insert_with(|| ParseValue::MultiBool(vec![]))
603 .try_as_multi_bool_mut()
604 .unwrap();
605 arr.push(true);
606 } else {
607 let negate = f.negate.clone().unwrap_or_default();
608 out.flags
609 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
610 }
611 continue;
612 }
613 if is_help_arg(spec, &w) {
614 out.errors
615 .push(render_help_err(spec, &out.cmd, w.len() > 2));
616 return Ok(out);
617 }
618 }
619
620 if enable_flags && w.starts_with('-') && w.len() > 1 {
622 let short = w.chars().nth(1).unwrap();
623 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
624 if w.len() > 2 {
625 input.push_front(format!("-{}", &w[2..]));
626 grouped_flag = true;
627 }
628 if f.arg.is_some() {
629 out.flag_awaiting_value.push(Arc::clone(f));
630 } else if f.count {
631 let arr = out
632 .flags
633 .entry(Arc::clone(f))
634 .or_insert_with(|| ParseValue::MultiBool(vec![]))
635 .try_as_multi_bool_mut()
636 .unwrap();
637 arr.push(true);
638 } else {
639 let negate = f.negate.clone().unwrap_or_default();
640 out.flags
641 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
642 }
643 continue;
644 }
645 if is_help_arg(spec, &w) {
646 out.errors
647 .push(render_help_err(spec, &out.cmd, w.len() > 2));
648 return Ok(out);
649 }
650 if grouped_flag {
651 grouped_flag = false;
652 w.remove(0);
653 }
654 }
655
656 if !out.flag_awaiting_value.is_empty() {
657 let should_return = drain_pending_flag_values(
658 spec,
659 &out.cmd,
660 &mut out.errors,
661 &mut out.flags,
662 &mut out.flag_awaiting_value,
663 &mut w,
664 custom_env,
665 )?;
666 if should_return {
667 return Ok(out);
668 }
669 continue;
670 }
671
672 if let Some(arg) = next_arg {
673 if validate_choices(
674 spec,
675 &out.cmd,
676 &mut out.errors,
677 ChoiceTarget::arg(arg),
678 &w,
679 arg.choices.as_ref(),
680 custom_env,
681 )? {
682 return Ok(out);
683 }
684 if arg.var {
685 let arr = out
686 .args
687 .entry(Arc::new(arg.clone()))
688 .or_insert_with(|| ParseValue::MultiString(vec![]))
689 .try_as_multi_string_mut()
690 .unwrap();
691 arr.push(w);
692 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
693 next_arg = out.cmd.args.get(out.args.len());
694 }
695 } else {
696 out.args
697 .insert(Arc::new(arg.clone()), ParseValue::String(w));
698 next_arg = out.cmd.args.get(out.args.len());
699 }
700 continue;
701 }
702 if is_help_arg(spec, &w) {
703 out.errors
704 .push(render_help_err(spec, &out.cmd, w.len() > 2));
705 return Ok(out);
706 }
707 bail!("unexpected word: {w}");
708 }
709
710 for arg in out.cmd.args.iter().skip(out.args.len()) {
711 if arg.required && arg.default.is_empty() {
712 let has_env = arg.env.as_ref().is_some_and(|e| {
714 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
715 || std::env::var(e).is_ok()
716 });
717 if !has_env {
718 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
719 }
720 }
721 }
722
723 for flag in unique_flags(out.available_flags.values()) {
724 if out.flags.contains_key(flag) {
725 continue;
726 }
727 let has_default =
728 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
729 let has_env = flag.env.as_ref().is_some_and(|e| {
731 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
732 });
733 if flag.required && !has_default && !has_env {
734 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
735 }
736 }
737
738 for (arg, value) in &out.args {
740 if arg.var {
741 if let ParseValue::MultiString(values) = value {
742 if let Some(min) = arg.var_min {
743 if values.len() < min {
744 out.errors.push(UsageErr::VarArgTooFew {
745 name: arg.name.clone(),
746 min,
747 got: values.len(),
748 });
749 }
750 }
751 if let Some(max) = arg.var_max {
752 if values.len() > max {
753 out.errors.push(UsageErr::VarArgTooMany {
754 name: arg.name.clone(),
755 max,
756 got: values.len(),
757 });
758 }
759 }
760 }
761 }
762 }
763
764 for (flag, value) in &out.flags {
766 if flag.var {
767 let count = match value {
768 ParseValue::MultiString(values) => values.len(),
769 ParseValue::MultiBool(values) => values.len(),
770 _ => continue,
771 };
772 if let Some(min) = flag.var_min {
773 if count < min {
774 out.errors.push(UsageErr::VarFlagTooFew {
775 name: flag.name.clone(),
776 min,
777 got: count,
778 });
779 }
780 }
781 if let Some(max) = flag.var_max {
782 if count > max {
783 out.errors.push(UsageErr::VarFlagTooMany {
784 name: flag.name.clone(),
785 max,
786 got: count,
787 });
788 }
789 }
790 }
791 }
792
793 Ok(out)
794}
795
796#[cfg(feature = "docs")]
797fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
798 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
799}
800
801#[cfg(not(feature = "docs"))]
802fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
803 UsageErr::Help("help".to_string())
804}
805
806#[derive(Copy, Clone)]
807struct ChoiceTarget<'a> {
808 kind: &'a str,
809 name: &'a str,
810}
811
812impl<'a> ChoiceTarget<'a> {
813 fn arg(arg: &'a SpecArg) -> Self {
814 Self {
815 kind: "arg",
816 name: &arg.name,
817 }
818 }
819
820 fn option(flag: &'a SpecFlag) -> Self {
821 Self {
822 kind: "option",
823 name: &flag.name,
824 }
825 }
826}
827
828fn drain_pending_flag_values(
829 spec: &Spec,
830 cmd: &SpecCommand,
831 errors: &mut Vec<UsageErr>,
832 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
833 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
834 word: &mut String,
835 custom_env: Option<&HashMap<String, String>>,
836) -> miette::Result<bool> {
837 while let Some(flag) = flag_awaiting_value.pop() {
838 let arg = flag.arg.as_ref().unwrap();
839 if validate_choices(
840 spec,
841 cmd,
842 errors,
843 ChoiceTarget::option(&flag),
844 word,
845 arg.choices.as_ref(),
846 custom_env,
847 )? {
848 return Ok(true);
849 }
850 let value = std::mem::take(word);
851 if flag.var {
852 let arr = flags
853 .entry(flag)
854 .or_insert_with(|| ParseValue::MultiString(vec![]))
855 .try_as_multi_string_mut()
856 .unwrap();
857 arr.push(value);
858 } else {
859 flags.insert(flag, ParseValue::String(value));
860 }
861 }
862 Ok(false)
863}
864
865fn choice_error(
866 target: ChoiceTarget<'_>,
867 value: &str,
868 choices: Option<&SpecChoices>,
869 custom_env: Option<&HashMap<String, String>>,
870) -> Option<String> {
871 let choices = choices?;
872 let values = choices.values_with_env(custom_env);
873 if values.iter().any(|choice| choice == value) {
874 return None;
875 }
876 if let Some(env) = choices.env() {
877 if values.is_empty() {
878 return Some(format!(
879 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
880 target.kind, target.name,
881 ));
882 }
883 }
884 Some(format!(
885 "Invalid choice for {} {}: {value}, expected one of {}",
886 target.kind,
887 target.name,
888 values.join(", ")
889 ))
890}
891
892fn validate_choices(
893 spec: &Spec,
894 cmd: &SpecCommand,
895 errors: &mut Vec<UsageErr>,
896 target: ChoiceTarget<'_>,
897 value: &str,
898 choices: Option<&SpecChoices>,
899 custom_env: Option<&HashMap<String, String>>,
900) -> miette::Result<bool> {
901 if is_help_arg(spec, value)
902 && choices.is_some_and(|choices| {
903 !choices
904 .values_with_env(custom_env)
905 .iter()
906 .any(|choice| choice == value)
907 })
908 {
909 errors.push(render_help_err(spec, cmd, value.len() > 2));
910 return Ok(true);
911 }
912
913 if let Some(err) = choice_error(target, value, choices, custom_env) {
914 bail!("{err}");
915 }
916 Ok(false)
917}
918
919fn validate_choice_value(
920 target: ChoiceTarget<'_>,
921 value: &str,
922 choices: Option<&SpecChoices>,
923 custom_env: Option<&HashMap<String, String>>,
924) -> miette::Result<()> {
925 if let Some(err) = choice_error(target, value, choices, custom_env) {
926 bail!("{err}");
927 }
928 Ok(())
929}
930
931fn validate_choice_values(
932 target: ChoiceTarget<'_>,
933 values: &[String],
934 choices: Option<&SpecChoices>,
935 custom_env: Option<&HashMap<String, String>>,
936) -> miette::Result<()> {
937 for value in values {
938 validate_choice_value(target, value, choices, custom_env)?;
939 }
940 Ok(())
941}
942
943fn is_help_arg(spec: &Spec, w: &str) -> bool {
944 spec.disable_help != Some(true)
945 && (w == "--help"
946 || w == "-h"
947 || w == "-?"
948 || (spec.cmd.subcommands.is_empty() && w == "help"))
949}
950
951impl ParseOutput {
952 pub fn as_env(&self) -> BTreeMap<String, String> {
953 let mut env = BTreeMap::new();
954 for (flag, val) in &self.flags {
955 let key = format!("usage_{}", flag.name.to_snake_case());
956 let val = match val {
957 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
958 ParseValue::String(s) => s.clone(),
959 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
960 ParseValue::MultiString(s) => shell_words::join(s),
961 };
962 env.insert(key, val);
963 }
964 for (arg, val) in &self.args {
965 let key = format!("usage_{}", arg.name.to_snake_case());
966 env.insert(key, val.to_string());
967 }
968 env
969 }
970}
971
972impl Display for ParseValue {
973 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
974 match self {
975 ParseValue::Bool(b) => write!(f, "{b}"),
976 ParseValue::String(s) => write!(f, "{s}"),
977 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
978 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
979 }
980 }
981}
982
983impl Debug for ParseOutput {
984 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
985 f.debug_struct("ParseOutput")
986 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
987 .field(
988 "args",
989 &self
990 .args
991 .iter()
992 .map(|(a, w)| format!("{}: {w}", &a.name))
993 .collect_vec(),
994 )
995 .field(
996 "available_flags",
997 &self
998 .available_flags
999 .iter()
1000 .map(|(f, w)| format!("{f}: {w}"))
1001 .collect_vec(),
1002 )
1003 .field(
1004 "flags",
1005 &self
1006 .flags
1007 .iter()
1008 .map(|(f, w)| format!("{}: {w}", &f.name))
1009 .collect_vec(),
1010 )
1011 .field("flag_awaiting_value", &self.flag_awaiting_value)
1012 .field("errors", &self.errors)
1013 .finish()
1014 }
1015}
1016
1017#[cfg(test)]
1018mod tests {
1019 use super::*;
1020
1021 fn input(words: &[&str]) -> Vec<String> {
1022 words.iter().map(|word| (*word).to_string()).collect()
1023 }
1024
1025 fn spec_with_arg(arg: SpecArg) -> Spec {
1026 let cmd = SpecCommand::builder().name("test").arg(arg).build();
1027 Spec {
1028 name: "test".to_string(),
1029 bin: "test".to_string(),
1030 cmd,
1031 ..Default::default()
1032 }
1033 }
1034
1035 fn spec_with_flag(flag: SpecFlag) -> Spec {
1036 let cmd = SpecCommand::builder().name("test").flag(flag).build();
1037 Spec {
1038 name: "test".to_string(),
1039 bin: "test".to_string(),
1040 cmd,
1041 ..Default::default()
1042 }
1043 }
1044
1045 fn parse_with_env(
1046 spec: &Spec,
1047 words: &[&str],
1048 env: &[(&str, &str)],
1049 ) -> Result<ParseOutput, miette::Error> {
1050 let env = env
1051 .iter()
1052 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1053 .collect();
1054 Parser::new(spec).with_env(env).parse(&input(words))
1055 }
1056
1057 fn first_string_value(parsed: &ParseOutput) -> &str {
1058 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1059 return value;
1060 }
1061 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1062 return value;
1063 }
1064 panic!("expected first parsed value to be ParseValue::String");
1065 }
1066
1067 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
1068 let flag = parsed
1069 .flags
1070 .keys()
1071 .find(|flag| flag.name == name)
1072 .unwrap_or_else(|| panic!("expected flag {name}"));
1073 let value = parsed
1074 .flags
1075 .get(flag)
1076 .unwrap_or_else(|| panic!("expected value for flag {name}"));
1077 match value {
1078 ParseValue::String(value) => value,
1079 _ => panic!("expected flag {name} to be ParseValue::String"),
1080 }
1081 }
1082
1083 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1084 let err = result.expect_err("expected parser error");
1085 assert_eq!(format!("{err}"), expected);
1086 }
1087
1088 #[cfg(feature = "unstable_choices_env")]
1089 fn spec_arg_choices_env(key: &str) -> Spec {
1090 spec_with_arg(
1091 SpecArg::builder()
1092 .name("env")
1093 .choices_env(key)
1094 .required(false)
1095 .build(),
1096 )
1097 }
1098
1099 #[cfg(feature = "unstable_choices_env")]
1100 fn spec_flag_choices_env(key: &str) -> Spec {
1101 spec_with_flag(
1102 SpecFlag::builder()
1103 .long("env")
1104 .arg(SpecArg::builder().name("env").choices_env(key).build())
1105 .build(),
1106 )
1107 }
1108
1109 #[test]
1110 fn test_parse() {
1111 let cmd = SpecCommand::builder()
1112 .name("test")
1113 .arg(SpecArg::builder().name("arg").build())
1114 .flag(SpecFlag::builder().long("flag").build())
1115 .build();
1116 let spec = Spec {
1117 name: "test".to_string(),
1118 bin: "test".to_string(),
1119 cmd,
1120 ..Default::default()
1121 };
1122 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1123 let parsed = parse(&spec, &input).unwrap();
1124 assert_eq!(parsed.cmds.len(), 1);
1125 assert_eq!(parsed.cmds[0].name, "test");
1126 assert_eq!(parsed.args.len(), 1);
1127 assert_eq!(parsed.flags.len(), 1);
1128 assert_eq!(parsed.available_flags.len(), 1);
1129 }
1130
1131 #[test]
1132 fn test_as_env() {
1133 let cmd = SpecCommand::builder()
1134 .name("test")
1135 .arg(SpecArg::builder().name("arg").build())
1136 .flag(SpecFlag::builder().long("flag").build())
1137 .flag(
1138 SpecFlag::builder()
1139 .long("force")
1140 .negate("--no-force")
1141 .build(),
1142 )
1143 .build();
1144 let spec = Spec {
1145 name: "test".to_string(),
1146 bin: "test".to_string(),
1147 cmd,
1148 ..Default::default()
1149 };
1150 let input = vec![
1151 "test".to_string(),
1152 "--flag".to_string(),
1153 "--no-force".to_string(),
1154 ];
1155 let parsed = parse(&spec, &input).unwrap();
1156 let env = parsed.as_env();
1157 assert_eq!(env.len(), 2);
1158 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1159 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1160 }
1161
1162 #[test]
1163 fn test_arg_env_var() {
1164 let cmd = SpecCommand::builder()
1165 .name("test")
1166 .arg(
1167 SpecArg::builder()
1168 .name("input")
1169 .env("TEST_ARG_INPUT")
1170 .required(true)
1171 .build(),
1172 )
1173 .build();
1174 let spec = Spec {
1175 name: "test".to_string(),
1176 bin: "test".to_string(),
1177 cmd,
1178 ..Default::default()
1179 };
1180
1181 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1183
1184 let input = vec!["test".to_string()];
1185 let parsed = parse(&spec, &input).unwrap();
1186
1187 assert_eq!(parsed.args.len(), 1);
1188 let arg = parsed.args.keys().next().unwrap();
1189 assert_eq!(arg.name, "input");
1190 let value = parsed.args.values().next().unwrap();
1191 assert_eq!(value.to_string(), "test_file.txt");
1192
1193 std::env::remove_var("TEST_ARG_INPUT");
1195 }
1196
1197 #[test]
1198 fn test_flag_env_var_with_arg() {
1199 let cmd = SpecCommand::builder()
1200 .name("test")
1201 .flag(
1202 SpecFlag::builder()
1203 .long("output")
1204 .env("TEST_FLAG_OUTPUT")
1205 .arg(SpecArg::builder().name("file").build())
1206 .build(),
1207 )
1208 .build();
1209 let spec = Spec {
1210 name: "test".to_string(),
1211 bin: "test".to_string(),
1212 cmd,
1213 ..Default::default()
1214 };
1215
1216 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1218
1219 let input = vec!["test".to_string()];
1220 let parsed = parse(&spec, &input).unwrap();
1221
1222 assert_eq!(parsed.flags.len(), 1);
1223 let flag = parsed.flags.keys().next().unwrap();
1224 assert_eq!(flag.name, "output");
1225 let value = parsed.flags.values().next().unwrap();
1226 assert_eq!(value.to_string(), "output.txt");
1227
1228 std::env::remove_var("TEST_FLAG_OUTPUT");
1230 }
1231
1232 #[test]
1233 fn test_flag_env_var_boolean() {
1234 let cmd = SpecCommand::builder()
1235 .name("test")
1236 .flag(
1237 SpecFlag::builder()
1238 .long("verbose")
1239 .env("TEST_FLAG_VERBOSE")
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 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1252
1253 let input = vec!["test".to_string()];
1254 let parsed = parse(&spec, &input).unwrap();
1255
1256 assert_eq!(parsed.flags.len(), 1);
1257 let flag = parsed.flags.keys().next().unwrap();
1258 assert_eq!(flag.name, "verbose");
1259 let value = parsed.flags.values().next().unwrap();
1260 assert_eq!(value.to_string(), "true");
1261
1262 std::env::remove_var("TEST_FLAG_VERBOSE");
1264 }
1265
1266 #[test]
1267 fn test_env_var_precedence() {
1268 let cmd = SpecCommand::builder()
1270 .name("test")
1271 .arg(
1272 SpecArg::builder()
1273 .name("input")
1274 .env("TEST_PRECEDENCE_INPUT")
1275 .required(true)
1276 .build(),
1277 )
1278 .build();
1279 let spec = Spec {
1280 name: "test".to_string(),
1281 bin: "test".to_string(),
1282 cmd,
1283 ..Default::default()
1284 };
1285
1286 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1288
1289 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1290 let parsed = parse(&spec, &input).unwrap();
1291
1292 assert_eq!(parsed.args.len(), 1);
1293 let value = parsed.args.values().next().unwrap();
1294 assert_eq!(value.to_string(), "cli_file.txt");
1296
1297 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1299 }
1300
1301 #[test]
1302 fn test_flag_var_true_with_single_default() {
1303 let cmd = SpecCommand::builder()
1305 .name("test")
1306 .flag(
1307 SpecFlag::builder()
1308 .long("foo")
1309 .var(true)
1310 .arg(SpecArg::builder().name("foo").build())
1311 .default_value("bar")
1312 .build(),
1313 )
1314 .build();
1315 let spec = Spec {
1316 name: "test".to_string(),
1317 bin: "test".to_string(),
1318 cmd,
1319 ..Default::default()
1320 };
1321
1322 let input = vec!["test".to_string()];
1324 let parsed = parse(&spec, &input).unwrap();
1325
1326 assert_eq!(parsed.flags.len(), 1);
1327 let flag = parsed.flags.keys().next().unwrap();
1328 assert_eq!(flag.name, "foo");
1329 let value = parsed.flags.values().next().unwrap();
1330 match value {
1332 ParseValue::MultiString(v) => {
1333 assert_eq!(v.len(), 1);
1334 assert_eq!(v[0], "bar");
1335 }
1336 _ => panic!("Expected MultiString, got {:?}", value),
1337 }
1338 }
1339
1340 #[test]
1341 fn test_flag_var_true_with_multiple_defaults() {
1342 let cmd = SpecCommand::builder()
1344 .name("test")
1345 .flag(
1346 SpecFlag::builder()
1347 .long("foo")
1348 .var(true)
1349 .arg(SpecArg::builder().name("foo").build())
1350 .default_values(["xyz", "bar"])
1351 .build(),
1352 )
1353 .build();
1354 let spec = Spec {
1355 name: "test".to_string(),
1356 bin: "test".to_string(),
1357 cmd,
1358 ..Default::default()
1359 };
1360
1361 let input = vec!["test".to_string()];
1363 let parsed = parse(&spec, &input).unwrap();
1364
1365 assert_eq!(parsed.flags.len(), 1);
1366 let value = parsed.flags.values().next().unwrap();
1367 match value {
1369 ParseValue::MultiString(v) => {
1370 assert_eq!(v.len(), 2);
1371 assert_eq!(v[0], "xyz");
1372 assert_eq!(v[1], "bar");
1373 }
1374 _ => panic!("Expected MultiString, got {:?}", value),
1375 }
1376 }
1377
1378 #[test]
1379 fn test_flag_var_false_with_default_remains_string() {
1380 let cmd = SpecCommand::builder()
1382 .name("test")
1383 .flag(
1384 SpecFlag::builder()
1385 .long("foo")
1386 .var(false) .arg(SpecArg::builder().name("foo").build())
1388 .default_value("bar")
1389 .build(),
1390 )
1391 .build();
1392 let spec = Spec {
1393 name: "test".to_string(),
1394 bin: "test".to_string(),
1395 cmd,
1396 ..Default::default()
1397 };
1398
1399 let input = vec!["test".to_string()];
1401 let parsed = parse(&spec, &input).unwrap();
1402
1403 assert_eq!(parsed.flags.len(), 1);
1404 let value = parsed.flags.values().next().unwrap();
1405 match value {
1407 ParseValue::String(s) => {
1408 assert_eq!(s, "bar");
1409 }
1410 _ => panic!("Expected String, got {:?}", value),
1411 }
1412 }
1413
1414 #[test]
1415 fn test_arg_var_true_with_single_default() {
1416 let cmd = SpecCommand::builder()
1418 .name("test")
1419 .arg(
1420 SpecArg::builder()
1421 .name("files")
1422 .var(true)
1423 .default_value("default.txt")
1424 .required(false)
1425 .build(),
1426 )
1427 .build();
1428 let spec = Spec {
1429 name: "test".to_string(),
1430 bin: "test".to_string(),
1431 cmd,
1432 ..Default::default()
1433 };
1434
1435 let input = vec!["test".to_string()];
1437 let parsed = parse(&spec, &input).unwrap();
1438
1439 assert_eq!(parsed.args.len(), 1);
1440 let value = parsed.args.values().next().unwrap();
1441 match value {
1443 ParseValue::MultiString(v) => {
1444 assert_eq!(v.len(), 1);
1445 assert_eq!(v[0], "default.txt");
1446 }
1447 _ => panic!("Expected MultiString, got {:?}", value),
1448 }
1449 }
1450
1451 #[test]
1452 fn test_arg_var_true_with_multiple_defaults() {
1453 let cmd = SpecCommand::builder()
1455 .name("test")
1456 .arg(
1457 SpecArg::builder()
1458 .name("files")
1459 .var(true)
1460 .default_values(["file1.txt", "file2.txt"])
1461 .required(false)
1462 .build(),
1463 )
1464 .build();
1465 let spec = Spec {
1466 name: "test".to_string(),
1467 bin: "test".to_string(),
1468 cmd,
1469 ..Default::default()
1470 };
1471
1472 let input = vec!["test".to_string()];
1474 let parsed = parse(&spec, &input).unwrap();
1475
1476 assert_eq!(parsed.args.len(), 1);
1477 let value = parsed.args.values().next().unwrap();
1478 match value {
1480 ParseValue::MultiString(v) => {
1481 assert_eq!(v.len(), 2);
1482 assert_eq!(v[0], "file1.txt");
1483 assert_eq!(v[1], "file2.txt");
1484 }
1485 _ => panic!("Expected MultiString, got {:?}", value),
1486 }
1487 }
1488
1489 #[test]
1490 fn test_arg_var_false_with_default_remains_string() {
1491 let cmd = SpecCommand::builder()
1493 .name("test")
1494 .arg(
1495 SpecArg::builder()
1496 .name("file")
1497 .var(false)
1498 .default_value("default.txt")
1499 .required(false)
1500 .build(),
1501 )
1502 .build();
1503 let spec = Spec {
1504 name: "test".to_string(),
1505 bin: "test".to_string(),
1506 cmd,
1507 ..Default::default()
1508 };
1509
1510 let input = vec!["test".to_string()];
1512 let parsed = parse(&spec, &input).unwrap();
1513
1514 assert_eq!(parsed.args.len(), 1);
1515 let value = parsed.args.values().next().unwrap();
1516 match value {
1518 ParseValue::String(s) => {
1519 assert_eq!(s, "default.txt");
1520 }
1521 _ => panic!("Expected String, got {:?}", value),
1522 }
1523 }
1524
1525 #[test]
1526 fn test_scalar_defaults_validate_only_first_default_choice() {
1527 let specs = [
1528 spec_with_arg(
1529 SpecArg::builder()
1530 .name("env")
1531 .var(false)
1532 .default_values(["dev", "prod"])
1533 .choices(["dev"])
1534 .required(false)
1535 .build(),
1536 ),
1537 spec_with_flag(
1538 SpecFlag::builder()
1539 .long("env")
1540 .arg(
1541 SpecArg::builder()
1542 .name("env")
1543 .default_values(["dev", "prod"])
1544 .choices(["dev"])
1545 .build(),
1546 )
1547 .build(),
1548 ),
1549 ];
1550
1551 for spec in specs {
1552 let parsed = parse(&spec, &input(&["test"])).unwrap();
1553 assert_eq!(first_string_value(&parsed), "dev");
1554 }
1555 }
1556
1557 #[test]
1558 fn test_default_subcommand() {
1559 let run_cmd = SpecCommand::builder()
1561 .name("run")
1562 .arg(SpecArg::builder().name("task").build())
1563 .build();
1564 let mut cmd = SpecCommand::builder().name("test").build();
1565 cmd.subcommands.insert("run".to_string(), run_cmd);
1566
1567 let spec = Spec {
1568 name: "test".to_string(),
1569 bin: "test".to_string(),
1570 cmd,
1571 default_subcommand: Some("run".to_string()),
1572 ..Default::default()
1573 };
1574
1575 let input = vec!["test".to_string(), "mytask".to_string()];
1577 let parsed = parse(&spec, &input).unwrap();
1578
1579 assert_eq!(parsed.cmds.len(), 2);
1581 assert_eq!(parsed.cmds[1].name, "run");
1582
1583 assert_eq!(parsed.args.len(), 1);
1585 let arg = parsed.args.keys().next().unwrap();
1586 assert_eq!(arg.name, "task");
1587 let value = parsed.args.values().next().unwrap();
1588 assert_eq!(value.to_string(), "mytask");
1589 }
1590
1591 #[test]
1592 fn test_default_subcommand_explicit_still_works() {
1593 let run_cmd = SpecCommand::builder()
1595 .name("run")
1596 .arg(SpecArg::builder().name("task").build())
1597 .build();
1598 let other_cmd = SpecCommand::builder()
1599 .name("other")
1600 .arg(SpecArg::builder().name("other_arg").build())
1601 .build();
1602 let mut cmd = SpecCommand::builder().name("test").build();
1603 cmd.subcommands.insert("run".to_string(), run_cmd);
1604 cmd.subcommands.insert("other".to_string(), other_cmd);
1605
1606 let spec = Spec {
1607 name: "test".to_string(),
1608 bin: "test".to_string(),
1609 cmd,
1610 default_subcommand: Some("run".to_string()),
1611 ..Default::default()
1612 };
1613
1614 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1616 let parsed = parse(&spec, &input).unwrap();
1617
1618 assert_eq!(parsed.cmds.len(), 2);
1620 assert_eq!(parsed.cmds[1].name, "other");
1621 }
1622
1623 #[test]
1624 fn test_default_subcommand_with_nested_subcommands() {
1625 let say_cmd = SpecCommand::builder()
1629 .name("say")
1630 .arg(SpecArg::builder().name("name").build())
1631 .build();
1632 let mut run_cmd = SpecCommand::builder().name("run").build();
1633 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1634
1635 let mut cmd = SpecCommand::builder().name("test").build();
1636 cmd.subcommands.insert("run".to_string(), run_cmd);
1637
1638 let spec = Spec {
1639 name: "test".to_string(),
1640 bin: "test".to_string(),
1641 cmd,
1642 default_subcommand: Some("run".to_string()),
1643 ..Default::default()
1644 };
1645
1646 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1648 let parsed = parse(&spec, &input).unwrap();
1649
1650 assert_eq!(parsed.cmds.len(), 3);
1652 assert_eq!(parsed.cmds[0].name, "test");
1653 assert_eq!(parsed.cmds[1].name, "run");
1654 assert_eq!(parsed.cmds[2].name, "say");
1655
1656 assert_eq!(parsed.args.len(), 1);
1658 let arg = parsed.args.keys().next().unwrap();
1659 assert_eq!(arg.name, "name");
1660 let value = parsed.args.values().next().unwrap();
1661 assert_eq!(value.to_string(), "hello");
1662 }
1663
1664 fn mounted_global_flag_spec() -> Spec {
1672 let task_cmd = SpecCommand::builder()
1673 .name("sample:run")
1674 .arg(
1675 SpecArg::builder()
1676 .name("profile")
1677 .choices(["alpha", "beta", "gamma"])
1678 .build(),
1679 )
1680 .build();
1681 let mut run_cmd = SpecCommand::builder()
1683 .name("run")
1684 .flag(
1685 SpecFlag::builder()
1686 .name("cd")
1687 .short('C')
1688 .long("cd")
1689 .arg(SpecArg::builder().name("dir").build())
1690 .global(false)
1691 .build(),
1692 )
1693 .build();
1694 run_cmd
1695 .subcommands
1696 .insert("sample:run".to_string(), task_cmd);
1697
1698 let mut cmd = SpecCommand::builder()
1699 .name("test")
1700 .flag(
1701 SpecFlag::builder()
1702 .name("cd")
1703 .short('C')
1704 .long("cd")
1705 .arg(SpecArg::builder().name("dir").build())
1706 .global(true)
1707 .build(),
1708 )
1709 .build();
1710 cmd.subcommands.insert("run".to_string(), run_cmd);
1711
1712 Spec {
1713 name: "test".to_string(),
1714 bin: "test".to_string(),
1715 cmd,
1716 ..Default::default()
1717 }
1718 }
1719
1720 #[test]
1721 fn test_prefix_global_flag_does_not_pollute_choices() {
1722 let spec = mounted_global_flag_spec();
1729
1730 for words in [
1733 &["test", "-C", "/tmp", "run", "sample:run"][..],
1734 &["test", "--cd=/tmp", "run", "sample:run"][..],
1736 ] {
1737 let parsed = parse_partial(&spec, &input(words)).unwrap();
1738 assert_eq!(
1739 parsed
1740 .cmds
1741 .iter()
1742 .map(|c| c.name.as_str())
1743 .collect::<Vec<_>>(),
1744 vec!["test", "run", "sample:run"],
1745 );
1746 assert!(
1748 parsed.args.is_empty(),
1749 "args should be empty, got {:?}",
1750 parsed.args
1751 );
1752
1753 let cd = parsed
1756 .available_flags
1757 .get("--cd")
1758 .expect("--cd should remain available after descending into the subcommand");
1759 assert!(cd.global, "--cd must stay global after descent");
1760 assert!(
1761 parsed.available_flags.get("-C").is_some_and(|f| f.global),
1762 "-C must stay global after descent",
1763 );
1764
1765 assert_eq!(
1769 parsed.as_env().get("usage_cd").map(String::as_str),
1770 Some("/tmp"),
1771 "global flag value must survive in as_env(), got {:?}",
1772 parsed.as_env(),
1773 );
1774 }
1775
1776 let parsed = parse_partial(
1778 &spec,
1779 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
1780 )
1781 .unwrap();
1782 assert_eq!(parsed.args.len(), 1);
1783 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1784
1785 assert_parse_err(
1787 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
1788 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1789 );
1790 }
1791
1792 fn mounted_orphan_short_spec() -> Spec {
1797 let task_cmd = SpecCommand::builder()
1798 .name("sample:run")
1799 .arg(
1800 SpecArg::builder()
1801 .name("profile")
1802 .choices(["alpha", "beta", "gamma"])
1803 .build(),
1804 )
1805 .build();
1806 let mut run_cmd = SpecCommand::builder()
1809 .name("run")
1810 .flag(
1811 SpecFlag::builder()
1812 .name("raw")
1813 .short('r')
1814 .long("raw")
1815 .global(false)
1816 .build(),
1817 )
1818 .flag(
1819 SpecFlag::builder()
1820 .name("force")
1821 .short('f')
1822 .long("force")
1823 .global(false)
1824 .build(),
1825 )
1826 .build();
1827 run_cmd
1828 .subcommands
1829 .insert("sample:run".to_string(), task_cmd);
1830
1831 let mut cmd = SpecCommand::builder()
1833 .name("test")
1834 .flag(
1835 SpecFlag::builder()
1836 .name("raw")
1837 .long("raw")
1838 .global(true)
1839 .build(),
1840 )
1841 .build();
1842 cmd.subcommands.insert("run".to_string(), run_cmd);
1843
1844 Spec {
1845 name: "test".to_string(),
1846 bin: "test".to_string(),
1847 cmd,
1848 ..Default::default()
1849 }
1850 }
1851
1852 #[test]
1853 fn test_orphan_short_alias_survives_merge() {
1854 let spec = mounted_orphan_short_spec();
1860
1861 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
1862 assert_eq!(
1863 parsed
1864 .cmds
1865 .iter()
1866 .map(|c| c.name.as_str())
1867 .collect::<Vec<_>>(),
1868 vec!["test", "run", "sample:run"],
1869 );
1870
1871 assert!(
1874 parsed.available_flags.get("-r").is_some_and(|f| f.global),
1875 "-r must be merged onto the inherited global flag and stay global after descent",
1876 );
1877 assert!(
1878 parsed
1879 .available_flags
1880 .get("--raw")
1881 .is_some_and(|f| f.global),
1882 "--raw must stay global after descent",
1883 );
1884
1885 assert!(
1887 parsed.args.is_empty(),
1888 "args should be empty, got {:?}",
1889 parsed.args
1890 );
1891
1892 assert_eq!(
1894 parsed.as_env().get("usage_raw").map(String::as_str),
1895 Some("true"),
1896 "merged short's value must survive in as_env(), got {:?}",
1897 parsed.as_env(),
1898 );
1899
1900 assert!(
1903 parsed.available_flags.get("-f").is_none(),
1904 "purely-local -f must not be promoted onto a global",
1905 );
1906 assert!(
1907 parsed.available_flags.get("--force").is_none(),
1908 "purely-local --force must not be promoted onto a global",
1909 );
1910
1911 let parsed =
1913 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
1914 assert_eq!(parsed.args.len(), 1);
1915 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1916
1917 assert_parse_err(
1919 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
1920 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1921 );
1922 }
1923
1924 #[test]
1925 fn test_orphan_short_does_not_clobber_unrelated_global() {
1926 let run_cmd = SpecCommand::builder()
1931 .name("run")
1932 .flag(
1933 SpecFlag::builder()
1934 .name("raw")
1935 .short('r')
1936 .long("raw")
1937 .global(false)
1938 .build(),
1939 )
1940 .build();
1941 let mut cmd = SpecCommand::builder()
1942 .name("test")
1943 .flag(
1944 SpecFlag::builder()
1945 .name("raw")
1946 .long("raw")
1947 .global(true)
1948 .build(),
1949 )
1950 .flag(
1951 SpecFlag::builder()
1952 .name("restrict")
1953 .short('r')
1954 .long("restrict")
1955 .global(true)
1956 .build(),
1957 )
1958 .build();
1959 cmd.subcommands.insert("run".to_string(), run_cmd);
1960 let spec = Spec {
1961 name: "test".to_string(),
1962 bin: "test".to_string(),
1963 cmd,
1964 ..Default::default()
1965 };
1966
1967 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
1968 assert_eq!(
1970 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
1971 Some("restrict"),
1972 "-r must remain owned by the unrelated global it already belonged to",
1973 );
1974 assert!(parsed
1976 .available_flags
1977 .get("--raw")
1978 .is_some_and(|f| f.global));
1979 assert!(parsed
1980 .available_flags
1981 .get("--restrict")
1982 .is_some_and(|f| f.global));
1983 }
1984
1985 #[test]
1986 fn test_subcommand_alias_collision_keeps_last_owner() {
1987 let run_cmd = SpecCommand::builder()
1991 .name("run")
1992 .flag(
1993 SpecFlag::builder()
1994 .name("alpha")
1995 .short('x')
1996 .long("alpha")
1997 .global(false)
1998 .build(),
1999 )
2000 .flag(
2001 SpecFlag::builder()
2002 .name("beta")
2003 .short('x')
2004 .long("beta")
2005 .global(false)
2006 .build(),
2007 )
2008 .build();
2009 let mut cmd = SpecCommand::builder().name("test").build();
2010 cmd.subcommands.insert("run".to_string(), run_cmd);
2011 let spec = Spec {
2012 name: "test".to_string(),
2013 bin: "test".to_string(),
2014 cmd,
2015 ..Default::default()
2016 };
2017
2018 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2019 assert_eq!(
2021 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
2022 Some("beta"),
2023 "the last-declared flag must keep a shared short alias",
2024 );
2025 assert_eq!(
2027 parsed
2028 .available_flags
2029 .get("--alpha")
2030 .map(|f| f.name.as_str()),
2031 Some("alpha"),
2032 );
2033 assert_eq!(
2034 parsed
2035 .available_flags
2036 .get("--beta")
2037 .map(|f| f.name.as_str()),
2038 Some("beta"),
2039 );
2040 }
2041
2042 #[test]
2043 fn test_default_subcommand_same_name_child() {
2044 let run_task = SpecCommand::builder()
2048 .name("run")
2049 .arg(SpecArg::builder().name("args").build())
2050 .build();
2051 let mut run_cmd = SpecCommand::builder().name("run").build();
2052 run_cmd.subcommands.insert("run".to_string(), run_task);
2053
2054 let mut cmd = SpecCommand::builder().name("test").build();
2055 cmd.subcommands.insert("run".to_string(), run_cmd);
2056
2057 let spec = Spec {
2058 name: "test".to_string(),
2059 bin: "test".to_string(),
2060 cmd,
2061 default_subcommand: Some("run".to_string()),
2062 ..Default::default()
2063 };
2064
2065 let input = vec!["test".to_string(), "run".to_string()];
2067 let parsed = parse(&spec, &input).unwrap();
2068
2069 assert_eq!(parsed.cmds.len(), 2);
2071 assert_eq!(parsed.cmds[0].name, "test");
2072 assert_eq!(parsed.cmds[1].name, "run");
2073
2074 let input = vec![
2076 "test".to_string(),
2077 "run".to_string(),
2078 "run".to_string(),
2079 "hello".to_string(),
2080 ];
2081 let parsed = parse(&spec, &input).unwrap();
2082
2083 assert_eq!(parsed.cmds.len(), 3);
2084 assert_eq!(parsed.cmds[0].name, "test");
2085 assert_eq!(parsed.cmds[1].name, "run");
2086 assert_eq!(parsed.cmds[2].name, "run");
2087 assert_eq!(parsed.args.len(), 1);
2088 let value = parsed.args.values().next().unwrap();
2089 assert_eq!(value.to_string(), "hello");
2090
2091 let mut run_cmd = SpecCommand::builder()
2095 .name("run")
2096 .arg(SpecArg::builder().name("task").build())
2097 .build();
2098 let run_task = SpecCommand::builder().name("run").build();
2099 run_cmd.subcommands.insert("run".to_string(), run_task);
2100
2101 let mut cmd = SpecCommand::builder().name("test").build();
2102 cmd.subcommands.insert("run".to_string(), run_cmd);
2103
2104 let spec = Spec {
2105 name: "test".to_string(),
2106 bin: "test".to_string(),
2107 cmd,
2108 default_subcommand: Some("run".to_string()),
2109 ..Default::default()
2110 };
2111
2112 let input = vec!["test".to_string(), "other".to_string()];
2113 let parsed = parse(&spec, &input).unwrap();
2114
2115 assert_eq!(parsed.cmds.len(), 2);
2118 assert_eq!(parsed.cmds[0].name, "test");
2119 assert_eq!(parsed.cmds[1].name, "run");
2120
2121 assert_eq!(parsed.args.len(), 1);
2123 let value = parsed.args.values().next().unwrap();
2124 assert_eq!(value.to_string(), "other");
2125 }
2126
2127 #[test]
2128 fn test_restart_token() {
2129 let run_cmd = SpecCommand::builder()
2131 .name("run")
2132 .arg(SpecArg::builder().name("task").build())
2133 .restart_token(":::".to_string())
2134 .build();
2135 let mut cmd = SpecCommand::builder().name("test").build();
2136 cmd.subcommands.insert("run".to_string(), run_cmd);
2137
2138 let spec = Spec {
2139 name: "test".to_string(),
2140 bin: "test".to_string(),
2141 cmd,
2142 ..Default::default()
2143 };
2144
2145 let input = vec![
2147 "test".to_string(),
2148 "run".to_string(),
2149 "task1".to_string(),
2150 ":::".to_string(),
2151 "task2".to_string(),
2152 ];
2153 let parsed = parse(&spec, &input).unwrap();
2154
2155 assert_eq!(parsed.args.len(), 1);
2157 let value = parsed.args.values().next().unwrap();
2158 assert_eq!(value.to_string(), "task2");
2159 }
2160
2161 #[test]
2162 fn test_restart_token_multiple() {
2163 let run_cmd = SpecCommand::builder()
2165 .name("run")
2166 .arg(SpecArg::builder().name("task").build())
2167 .restart_token(":::".to_string())
2168 .build();
2169 let mut cmd = SpecCommand::builder().name("test").build();
2170 cmd.subcommands.insert("run".to_string(), run_cmd);
2171
2172 let spec = Spec {
2173 name: "test".to_string(),
2174 bin: "test".to_string(),
2175 cmd,
2176 ..Default::default()
2177 };
2178
2179 let input = vec![
2181 "test".to_string(),
2182 "run".to_string(),
2183 "task1".to_string(),
2184 ":::".to_string(),
2185 "task2".to_string(),
2186 ":::".to_string(),
2187 "task3".to_string(),
2188 ];
2189 let parsed = parse(&spec, &input).unwrap();
2190
2191 assert_eq!(parsed.args.len(), 1);
2193 let value = parsed.args.values().next().unwrap();
2194 assert_eq!(value.to_string(), "task3");
2195 }
2196
2197 #[test]
2198 fn test_restart_token_clears_flag_awaiting_value() {
2199 let run_cmd = SpecCommand::builder()
2201 .name("run")
2202 .arg(SpecArg::builder().name("task").build())
2203 .flag(
2204 SpecFlag::builder()
2205 .name("jobs")
2206 .long("jobs")
2207 .arg(SpecArg::builder().name("count").build())
2208 .build(),
2209 )
2210 .restart_token(":::".to_string())
2211 .build();
2212 let mut cmd = SpecCommand::builder().name("test").build();
2213 cmd.subcommands.insert("run".to_string(), run_cmd);
2214
2215 let spec = Spec {
2216 name: "test".to_string(),
2217 bin: "test".to_string(),
2218 cmd,
2219 ..Default::default()
2220 };
2221
2222 let input = vec![
2224 "test".to_string(),
2225 "run".to_string(),
2226 "task1".to_string(),
2227 "--jobs".to_string(),
2228 ":::".to_string(),
2229 "task2".to_string(),
2230 ];
2231 let parsed = parse(&spec, &input).unwrap();
2232
2233 assert_eq!(parsed.args.len(), 1);
2235 let value = parsed.args.values().next().unwrap();
2236 assert_eq!(value.to_string(), "task2");
2237 assert!(parsed.flag_awaiting_value.is_empty());
2239 }
2240
2241 #[test]
2242 fn test_restart_token_resets_double_dash() {
2243 let run_cmd = SpecCommand::builder()
2245 .name("run")
2246 .arg(SpecArg::builder().name("task").build())
2247 .arg(SpecArg::builder().name("extra_args").var(true).build())
2248 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
2249 .restart_token(":::".to_string())
2250 .build();
2251 let mut cmd = SpecCommand::builder().name("test").build();
2252 cmd.subcommands.insert("run".to_string(), run_cmd);
2253
2254 let spec = Spec {
2255 name: "test".to_string(),
2256 bin: "test".to_string(),
2257 cmd,
2258 ..Default::default()
2259 };
2260
2261 let input = vec![
2263 "test".to_string(),
2264 "run".to_string(),
2265 "task1".to_string(),
2266 "--".to_string(),
2267 "extra".to_string(),
2268 ":::".to_string(),
2269 "--verbose".to_string(),
2270 "task2".to_string(),
2271 ];
2272 let parsed = parse(&spec, &input).unwrap();
2273
2274 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
2276 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2278 let value = parsed.args.get(task_arg).unwrap();
2279 assert_eq!(value.to_string(), "task2");
2280 }
2281
2282 #[test]
2283 fn test_double_dashes_without_preserve() {
2284 let run_cmd = SpecCommand::builder()
2286 .name("run")
2287 .arg(SpecArg::builder().name("args").var(true).build())
2288 .build();
2289 let mut cmd = SpecCommand::builder().name("test").build();
2290 cmd.subcommands.insert("run".to_string(), run_cmd);
2291
2292 let spec = Spec {
2293 name: "test".to_string(),
2294 bin: "test".to_string(),
2295 cmd,
2296 ..Default::default()
2297 };
2298
2299 let input = vec![
2301 "test".to_string(),
2302 "run".to_string(),
2303 "arg1".to_string(),
2304 "--".to_string(),
2305 "arg2".to_string(),
2306 "--".to_string(),
2307 "arg3".to_string(),
2308 ];
2309 let parsed = parse(&spec, &input).unwrap();
2310
2311 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2312 let value = parsed.args.get(args_arg).unwrap();
2313 assert_eq!(value.to_string(), "arg1 arg2 arg3");
2314 }
2315
2316 #[test]
2317 fn test_double_dashes_with_preserve() {
2318 let run_cmd = SpecCommand::builder()
2320 .name("run")
2321 .arg(
2322 SpecArg::builder()
2323 .name("args")
2324 .var(true)
2325 .double_dash(SpecDoubleDashChoices::Preserve)
2326 .build(),
2327 )
2328 .build();
2329 let mut cmd = SpecCommand::builder().name("test").build();
2330 cmd.subcommands.insert("run".to_string(), run_cmd);
2331
2332 let spec = Spec {
2333 name: "test".to_string(),
2334 bin: "test".to_string(),
2335 cmd,
2336 ..Default::default()
2337 };
2338
2339 let input = vec![
2341 "test".to_string(),
2342 "run".to_string(),
2343 "arg1".to_string(),
2344 "--".to_string(),
2345 "arg2".to_string(),
2346 "--".to_string(),
2347 "arg3".to_string(),
2348 ];
2349 let parsed = parse(&spec, &input).unwrap();
2350
2351 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2352 let value = parsed.args.get(args_arg).unwrap();
2353 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
2354 }
2355
2356 #[test]
2357 fn test_double_dashes_with_preserve_only_dashes() {
2358 let run_cmd = SpecCommand::builder()
2361 .name("run")
2362 .arg(
2363 SpecArg::builder()
2364 .name("args")
2365 .var(true)
2366 .double_dash(SpecDoubleDashChoices::Preserve)
2367 .build(),
2368 )
2369 .build();
2370 let mut cmd = SpecCommand::builder().name("test").build();
2371 cmd.subcommands.insert("run".to_string(), run_cmd);
2372
2373 let spec = Spec {
2374 name: "test".to_string(),
2375 bin: "test".to_string(),
2376 cmd,
2377 ..Default::default()
2378 };
2379
2380 let input = vec![
2382 "test".to_string(),
2383 "run".to_string(),
2384 "--".to_string(),
2385 "--".to_string(),
2386 ];
2387 let parsed = parse(&spec, &input).unwrap();
2388
2389 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2390 let value = parsed.args.get(args_arg).unwrap();
2391 assert_eq!(value.to_string(), "-- --");
2392 }
2393
2394 #[test]
2395 fn test_double_dashes_with_preserve_multiple_args() {
2396 let run_cmd = SpecCommand::builder()
2398 .name("run")
2399 .arg(SpecArg::builder().name("task").build())
2400 .arg(
2401 SpecArg::builder()
2402 .name("extra_args")
2403 .var(true)
2404 .double_dash(SpecDoubleDashChoices::Preserve)
2405 .build(),
2406 )
2407 .build();
2408 let mut cmd = SpecCommand::builder().name("test").build();
2409 cmd.subcommands.insert("run".to_string(), run_cmd);
2410
2411 let spec = Spec {
2412 name: "test".to_string(),
2413 bin: "test".to_string(),
2414 cmd,
2415 ..Default::default()
2416 };
2417
2418 let input = vec![
2421 "test".to_string(),
2422 "run".to_string(),
2423 "task1".to_string(),
2424 "--".to_string(),
2425 "arg1".to_string(),
2426 "--".to_string(),
2427 "--foo".to_string(),
2428 ];
2429 let parsed = parse(&spec, &input).unwrap();
2430
2431 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2432 let task_value = parsed.args.get(task_arg).unwrap();
2433 assert_eq!(task_value.to_string(), "task1");
2434
2435 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
2436 let extra_value = parsed.args.get(extra_arg).unwrap();
2437 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
2438 }
2439
2440 #[test]
2441 fn test_parser_with_custom_env_for_required_arg() {
2442 let spec = spec_with_arg(
2443 SpecArg::builder()
2444 .name("name")
2445 .env("NAME")
2446 .required(true)
2447 .build(),
2448 );
2449 std::env::remove_var("NAME");
2450
2451 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
2452 .expect("parse should succeed with custom env");
2453 assert_eq!(parsed.args.len(), 1);
2454 assert_eq!(first_string_value(&parsed), "john");
2455 }
2456
2457 #[test]
2458 fn test_parser_with_custom_env_for_required_flag() {
2459 let spec = spec_with_flag(
2460 SpecFlag::builder()
2461 .long("name")
2462 .env("NAME")
2463 .required(true)
2464 .arg(SpecArg::builder().name("name").build())
2465 .build(),
2466 );
2467 std::env::remove_var("NAME");
2468
2469 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
2470 .expect("parse should succeed with custom env");
2471 assert_eq!(parsed.flags.len(), 1);
2472 assert_eq!(first_string_value(&parsed), "jane");
2473 }
2474
2475 #[test]
2476 fn test_parser_with_custom_env_still_fails_when_missing() {
2477 let spec = spec_with_arg(
2478 SpecArg::builder()
2479 .name("name")
2480 .env("NAME")
2481 .required(true)
2482 .build(),
2483 );
2484 std::env::remove_var("NAME");
2485 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
2486 }
2487
2488 #[test]
2489 fn test_parser_does_not_treat_env_choice_value_as_help() {
2490 let spec = spec_with_arg(
2491 SpecArg::builder()
2492 .name("env")
2493 .env("CURRENT_ENV")
2494 .choices(["dev", "staging"])
2495 .required(false)
2496 .build(),
2497 );
2498
2499 assert_parse_err(
2500 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
2501 "Invalid choice for arg env: --help, expected one of dev, staging",
2502 );
2503 }
2504
2505 #[test]
2506 fn test_parser_does_not_treat_default_choice_value_as_help() {
2507 let spec = spec_with_flag(
2508 SpecFlag::builder()
2509 .long("env")
2510 .arg(
2511 SpecArg::builder()
2512 .name("env")
2513 .choices(["dev", "staging"])
2514 .build(),
2515 )
2516 .default_value("--help")
2517 .build(),
2518 );
2519
2520 assert_parse_err(
2521 parse_with_env(&spec, &["test"], &[]),
2522 "Invalid choice for option env: --help, expected one of dev, staging",
2523 );
2524 }
2525
2526 #[cfg(feature = "unstable_choices_env")]
2527 #[test]
2528 fn test_parser_arg_choices_from_custom_env() {
2529 let spec = spec_arg_choices_env("DEPLOY_ENVS");
2530
2531 let parsed =
2532 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
2533 assert_eq!(first_string_value(&parsed), "bar");
2534
2535 assert_parse_err(
2536 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
2537 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
2538 );
2539 assert_parse_err(
2540 parse_with_env(&spec, &["test", "prod"], &[]),
2541 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
2542 );
2543 }
2544
2545 #[cfg(feature = "unstable_choices_env")]
2546 #[test]
2547 fn test_parser_validates_flag_choices_from_custom_env() {
2548 let spec = spec_flag_choices_env("DEPLOY_ENVS");
2549 let parsed = parse_with_env(
2550 &spec,
2551 &["test", "--env", "baz"],
2552 &[("DEPLOY_ENVS", "foo,bar baz")],
2553 )
2554 .unwrap();
2555 assert_eq!(first_string_value(&parsed), "baz");
2556 }
2557
2558 #[cfg(feature = "unstable_choices_env")]
2559 #[test]
2560 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
2561 let arg_env_spec = spec_with_arg(
2562 SpecArg::builder()
2563 .name("env")
2564 .env("CURRENT_ENV")
2565 .choices_env("DEPLOY_ENVS")
2566 .build(),
2567 );
2568 assert_parse_err(
2569 parse_with_env(
2570 &arg_env_spec,
2571 &["test"],
2572 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
2573 ),
2574 "Invalid choice for arg env: prod, expected one of dev, staging",
2575 );
2576
2577 let flag_default_spec = spec_with_flag(
2578 SpecFlag::builder()
2579 .long("env")
2580 .arg(
2581 SpecArg::builder()
2582 .name("env")
2583 .choices_env("DEPLOY_ENVS")
2584 .build(),
2585 )
2586 .default_value("prod")
2587 .build(),
2588 );
2589 assert_parse_err(
2590 parse_with_env(
2591 &flag_default_spec,
2592 &["test"],
2593 &[("DEPLOY_ENVS", "dev,staging")],
2594 ),
2595 "Invalid choice for option env: prod, expected one of dev, staging",
2596 );
2597 }
2598
2599 #[test]
2600 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
2601 let spec: Spec = r#"
2602 flag "-v --verbose" var=#true
2603 arg "[database]" default="myapp_dev"
2604 arg "[args...]"
2605 "#
2606 .parse()
2607 .unwrap();
2608 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2609 .into_iter()
2610 .map(String::from)
2611 .collect();
2612 let parsed = parse(&spec, &input).unwrap();
2613 let env = parsed.as_env();
2614 assert_eq!(env.get("usage_database").unwrap(), "mydb");
2615 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
2616 }
2617
2618 #[test]
2619 fn test_variadic_arg_captures_unknown_flags() {
2620 let cmd = SpecCommand::builder()
2621 .name("test")
2622 .flag(SpecFlag::builder().short('v').long("verbose").build())
2623 .arg(SpecArg::builder().name("database").required(false).build())
2624 .arg(
2625 SpecArg::builder()
2626 .name("args")
2627 .required(false)
2628 .var(true)
2629 .build(),
2630 )
2631 .build();
2632 let spec = Spec {
2633 name: "test".to_string(),
2634 bin: "test".to_string(),
2635 cmd,
2636 ..Default::default()
2637 };
2638
2639 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2641 .into_iter()
2642 .map(String::from)
2643 .collect();
2644 let parsed = parse(&spec, &input).unwrap();
2645 assert_eq!(parsed.args.len(), 2);
2646 let args_val = parsed
2647 .args
2648 .iter()
2649 .find(|(a, _)| a.name == "args")
2650 .unwrap()
2651 .1;
2652 match args_val {
2653 ParseValue::MultiString(v) => {
2654 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2655 }
2656 _ => panic!("Expected MultiString, got {:?}", args_val),
2657 }
2658 }
2659
2660 #[test]
2661 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
2662 let cmd = SpecCommand::builder()
2663 .name("test")
2664 .flag(SpecFlag::builder().short('v').long("verbose").build())
2665 .arg(SpecArg::builder().name("database").required(false).build())
2666 .arg(
2667 SpecArg::builder()
2668 .name("args")
2669 .required(false)
2670 .var(true)
2671 .build(),
2672 )
2673 .build();
2674 let spec = Spec {
2675 name: "test".to_string(),
2676 bin: "test".to_string(),
2677 cmd,
2678 ..Default::default()
2679 };
2680
2681 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
2683 .into_iter()
2684 .map(String::from)
2685 .collect();
2686 let parsed = parse(&spec, &input).unwrap();
2687 assert_eq!(parsed.args.len(), 2);
2688 let args_val = parsed
2689 .args
2690 .iter()
2691 .find(|(a, _)| a.name == "args")
2692 .unwrap()
2693 .1;
2694 match args_val {
2695 ParseValue::MultiString(v) => {
2696 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2697 }
2698 _ => panic!("Expected MultiString, got {:?}", args_val),
2699 }
2700 }
2701
2702 #[test]
2703 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
2704 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
2707
2708 let input: Vec<String> = vec!["test", "--option=3"]
2711 .into_iter()
2712 .map(String::from)
2713 .collect();
2714 let parsed = parse(&spec, &input).unwrap();
2715 let env = parsed.as_env();
2716 assert_eq!(
2717 env.get("usage_other_args").map(String::as_str),
2718 Some("'--option=3'"),
2719 "expected a single --option=3 token, got {:?}",
2720 env.get("usage_other_args"),
2721 );
2722
2723 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
2725 .into_iter()
2726 .map(String::from)
2727 .collect();
2728 let parsed2 = parse(&spec, &input2).unwrap();
2729 let env2 = parsed2.as_env();
2730 assert_eq!(
2731 env2.get("usage_other_args").map(String::as_str),
2732 Some("'--foo=bar' '--baz=qux'"),
2733 "expected two intact tokens, got {:?}",
2734 env2.get("usage_other_args"),
2735 );
2736
2737 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
2739 .into_iter()
2740 .map(String::from)
2741 .collect();
2742 let parsed3 = parse(&spec, &input3).unwrap();
2743 let env3 = parsed3.as_env();
2744 assert_eq!(
2745 env3.get("usage_other_args").map(String::as_str),
2746 Some("positional1 '--option=3' positional2"),
2747 "expected positional args and intact flag token, got {:?}",
2748 env3.get("usage_other_args"),
2749 );
2750 }
2751
2752 #[test]
2753 fn test_allow_hyphen_values_consumes_short_flag_collision() {
2754 let spec = r#"
2755flag "-d --working-dir <DIR>"
2756flag "-a --args <ARGS>" allow_hyphen_values=#true
2757"#
2758 .parse::<Spec>()
2759 .unwrap();
2760
2761 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
2762
2763 assert_eq!(parsed.flags.len(), 1);
2764 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
2765 }
2766
2767 #[test]
2768 fn test_allow_hyphen_values_consumes_embedded_long_value() {
2769 let spec = r#"
2770flag "-d --working-dir <DIR>"
2771flag "-a --args <ARGS>" allow_hyphen_values=#true
2772"#
2773 .parse::<Spec>()
2774 .unwrap();
2775
2776 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
2777
2778 assert_eq!(parsed.flags.len(), 1);
2779 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
2780 }
2781
2782 #[test]
2783 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
2784 let spec = r#"
2785flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
2786"#
2787 .parse::<Spec>()
2788 .unwrap();
2789
2790 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
2791
2792 let flag = parsed
2793 .flags
2794 .keys()
2795 .find(|flag| flag.name == "args")
2796 .expect("expected args flag");
2797 let value = parsed.flags.get(flag).expect("expected args value");
2798 match value {
2799 ParseValue::MultiString(values) => {
2800 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
2801 }
2802 _ => panic!("expected MultiString, got {value:?}"),
2803 }
2804 }
2805
2806 #[test]
2807 fn test_hyphen_values_still_default_to_short_flag_parsing() {
2808 let spec = r#"
2809flag "-d --working-dir <DIR>"
2810flag "-a --args <ARGS>"
2811"#
2812 .parse::<Spec>()
2813 .unwrap();
2814
2815 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
2816
2817 assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
2818 }
2819}