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::unknown_flags::UnknownFlags;
16use crate::warn::Warning;
17use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
18
19fn merge_subcommand_flags(
37 available: &mut BTreeMap<String, Arc<SpecFlag>>,
38 new_flags: BTreeMap<String, Arc<SpecFlag>>,
39 crossing_mount: bool,
40) {
41 available.retain(|_, f| f.global);
43
44 if crossing_mount {
45 for (key, flag) in new_flags {
50 available.insert(key, flag);
51 }
52 return;
53 }
54
55 let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
58 let mut merged_origin: HashMap<usize, usize> = HashMap::new();
62 fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
64 let ptr = Arc::as_ptr(flag) as usize;
65 *merged_origin.get(&ptr).unwrap_or(&ptr)
66 }
67
68 for (key, flag) in new_flags {
73 if flag.global {
74 available.insert(key, flag);
76 continue;
77 }
78
79 let inherited_global = flag.long.iter().find_map(|l| {
88 available
89 .get(&format!("--{l}"))
90 .filter(|f| f.global)
91 .cloned()
92 });
93 if let Some(global_flag) = inherited_global {
94 let global_origin = origin_of(&merged_origin, &global_flag);
104 if available.get(&key).is_some_and(|existing| {
105 existing.global && origin_of(&merged_origin, existing) != global_origin
106 }) {
107 continue;
108 }
109 let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
110 Some(merged) => merged.clone(),
111 None => {
112 let mut merged = (*global_flag).clone();
113 for s in &flag.short {
121 if !merged.short.contains(s) {
122 merged.short.push(*s);
123 }
124 }
125 for l in &flag.long {
126 if !merged.long.contains(l) {
127 merged.long.push(l.clone());
128 }
129 }
130 merged.hidden_short_aliases.retain(|alias| {
134 !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
135 });
136 merged.hidden_aliases.retain(|alias| {
137 !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
138 });
139 for s in &flag.hidden_short_aliases {
140 if !merged.hidden_short_aliases.contains(s) {
141 merged.hidden_short_aliases.push(*s);
142 }
143 }
144 for l in &flag.hidden_aliases {
145 if !merged.hidden_aliases.contains(l) {
146 merged.hidden_aliases.push(l.clone());
147 }
148 }
149 let merged = Arc::new(merged);
150 merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
151 merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
152 for existing in available.values_mut() {
158 if origin_of(&merged_origin, existing) == global_origin {
159 *existing = Arc::clone(&merged);
160 }
161 }
162 merged
163 }
164 };
165 available.insert(key, merged);
166 continue;
167 }
168
169 if available.contains_key(&key) {
173 continue;
174 }
175 available.insert(key, flag);
176 }
177}
178
179fn flag_keys(flag: &SpecFlag) -> Vec<String> {
182 let mut keys: Vec<String> = flag
183 .long
184 .iter()
185 .map(|l| format!("--{l}"))
186 .chain(flag.short.iter().map(|s| format!("-{s}")))
187 .collect();
188 if let Some(negate) = &flag.negate {
189 keys.push(negate.clone());
190 }
191 keys
192}
193
194fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
196 cmd.flags
197 .iter()
198 .flat_map(|f| {
199 let f = Arc::new(f.clone()); flag_keys(&f)
201 .into_iter()
202 .map(|key| (key, Arc::clone(&f)))
203 .collect::<Vec<_>>()
204 })
205 .collect()
206}
207
208fn unique_flags<'a>(
209 flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
210) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
211 let mut seen = HashSet::new();
212 flags
213 .into_iter()
214 .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
215}
216
217pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
230 let Some((root, rest)) = chain.split_first() else {
231 return vec![];
232 };
233 let mut available = gather_flags(root);
234 for cmd in rest {
235 merge_subcommand_flags(&mut available, gather_flags(cmd), false);
236 }
237
238 let mut seen_names = HashSet::new();
249 unique_flags(available.values())
250 .filter(|f| seen_names.insert(f.name.clone()))
251 .cloned()
252 .collect()
253}
254
255fn get_flag_key(word: &str) -> &str {
258 if word.starts_with("--") {
259 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
261 } else if let Some((end, _)) = word.char_indices().nth(2) {
262 &word[0..end]
265 } else {
266 word
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
277#[non_exhaustive]
278pub enum ValueOrigin {
279 DefaultMissing,
284 Env(String),
290 Default,
296 DefaultIf {
300 selector: String,
301 when: Option<String>,
302 },
303}
304
305#[derive(Debug, Clone)]
310#[non_exhaustive]
311pub enum TokenRole {
312 Program,
314 Command { name: String },
316 Flag {
318 flag: Arc<SpecFlag>,
319 spelling: String,
320 negated: bool,
321 },
322 Value {
324 flag: Arc<SpecFlag>,
325 values: Vec<String>,
326 attached: bool,
329 },
330 Arg {
332 arg: Arc<SpecArg>,
333 values: Vec<String>,
334 },
335 Separator,
337 Builtin { spelling: String },
342 ValueTerminator { ends: String },
346 Restart,
350 UnknownFlag { bound_as: Option<Arc<SpecArg>> },
353 Refused { reason: String },
356 External,
358 Unread,
360}
361
362#[derive(Debug, Clone)]
364#[non_exhaustive]
365pub struct TokenBinding {
366 pub index: usize,
368 pub word: String,
369 pub synthesized: bool,
373 pub roles: Vec<TokenRole>,
374}
375
376#[non_exhaustive]
377pub struct ParseOutput {
378 pub cmd: SpecCommand,
379 pub cmds: Vec<SpecCommand>,
380 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
381 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
382 pub tokens: Vec<TokenBinding>,
389 pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
395 pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
398 pub overridden_flags: BTreeMap<String, String>,
404 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
411 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
412 pub errors: Vec<UsageErr>,
413 pub warnings: Vec<Warning>,
417 pub next_arg: Option<Arc<SpecArg>>,
424 pub double_dash_seen: bool,
430 pub external: Option<Vec<String>>,
435}
436
437impl ParseOutput {
438 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
447 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
448 return self.available_flags.clone();
449 };
450 let start = match boundary.checked_sub(1) {
454 Some(prev) if self.cmds[prev].flags_from_mount => prev,
455 _ => boundary,
456 };
457 let mut offered = gather_flags(&self.cmds[start]);
461 for cmd in &self.cmds[start + 1..] {
462 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
463 }
464 offered
465 }
466}
467
468#[derive(Debug, EnumTryAs, Clone)]
469pub enum ParseValue {
470 Bool(bool),
471 String(String),
472 MultiBool(Vec<bool>),
473 MultiString(Vec<String>),
474}
475
476fn collect_deprecations(out: &mut ParseOutput) {
487 for cmd in out.cmds.iter().skip(1) {
488 if cmd.deprecated.is_none()
489 && cmd.deprecated_warn_at.is_none()
490 && cmd.deprecated_remove_at.is_none()
491 {
492 continue;
493 }
494 out.warnings.push(Warning::command(
495 cmd.name.clone(),
496 cmd.deprecated.clone(),
497 cmd.deprecated_warn_at.clone(),
498 cmd.deprecated_remove_at.clone(),
499 ));
500 }
501 for flag in out.flags.keys() {
502 if let Some(warning) = flag_deprecation(flag) {
503 out.warnings.push(warning);
504 }
505 }
506}
507
508fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
510 if flag.deprecated.is_none()
511 && flag.deprecated_warn_at.is_none()
512 && flag.deprecated_remove_at.is_none()
513 {
514 return None;
515 }
516 Some(Warning::flag(
517 flag_spelling(flag),
518 flag.deprecated.clone(),
519 flag.deprecated_warn_at.clone(),
520 flag.deprecated_remove_at.clone(),
521 ))
522}
523
524fn flag_spelling(flag: &SpecFlag) -> String {
527 flag.long
528 .first()
529 .map(|long| format!("--{long}"))
530 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
531 .unwrap_or_else(|| flag.name.clone())
532}
533
534fn flag_current_env(flag: &SpecFlag) -> Option<String> {
536 flag.env
537 .clone()
538 .or_else(|| flag.env_fallback.first().cloned())
539}
540
541fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
542 flag.deprecated_env.iter().any(|declared| declared == name)
543}
544
545fn arg_current_env(arg: &SpecArg) -> Option<String> {
547 arg.env
548 .clone()
549 .or_else(|| arg.env_fallback.first().cloned())
550}
551
552fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
553 arg.deprecated_env.iter().any(|declared| declared == name)
554}
555
556fn first_set_env<'a>(
563 mut names: impl Iterator<Item = &'a str>,
564 get_env: &impl Fn(&str) -> Option<String>,
565) -> Option<(&'a str, String)> {
566 names.find_map(|name| get_env(name).map(|value| (name, value)))
567}
568
569#[non_exhaustive]
589pub struct Parser<'a> {
590 spec: &'a Spec,
591 env: Option<HashMap<String, String>>,
592 mount_outputs: Option<HashMap<String, String>>,
593}
594
595impl<'a> Parser<'a> {
596 pub fn new(spec: &'a Spec) -> Self {
598 Self {
599 spec,
600 env: None,
601 mount_outputs: None,
602 }
603 }
604
605 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
610 self.env = Some(env);
611 self
612 }
613
614 pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
621 self.mount_outputs = Some(outputs);
622 self
623 }
624
625 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
629 let out = self.parse_collecting(input)?;
630 if let Some(err) = out
631 .errors
632 .iter()
633 .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
634 {
635 bail!("{err}");
636 }
637 if !out.errors.is_empty() {
638 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
639 }
640 Ok(out)
641 }
642
643 pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
654 self.parse_collecting(input)
655 }
656
657 pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
672 let mut trace = Trace::new(input);
673 match parse_partial_traced(
674 self.spec,
675 input,
676 self.env.as_ref(),
677 self.mount_outputs.as_ref(),
678 MountTiming::WhenAWordIsUnknown,
679 &mut trace,
680 ) {
681 Ok((out, _)) => Ok(out),
684 Err(_) => Err(trace.tokens),
685 }
686 }
687
688 fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
689 let custom_env = self.env.as_ref();
690 let (mut out, overridden_flags) = parse_partial_with_env(
691 self.spec,
692 input,
693 custom_env,
694 self.mount_outputs.as_ref(),
695 MountTiming::WhenAWordIsUnknown,
696 )?;
697 trace!("{out:?}");
698
699 while try_bind_default_missing(
705 &mut out.flags,
706 &mut out.flag_awaiting_value,
707 custom_env,
708 &mut out.flag_origins,
709 )? {}
710 if let Some(flag) = out.flag_awaiting_value.first() {
711 let token = flag
712 .long
713 .first()
714 .map(|l| format!("--{l}"))
715 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
716 .unwrap_or_else(|| flag.name.clone());
717 let rendered = input.join(" ");
718 let span = rendered
719 .rfind(&token)
720 .map(|at| (at, token.len()))
721 .unwrap_or((0, 0));
722 return Err(UsageErr::InvalidFlag {
723 token,
724 reason: "requires an argument".to_string(),
725 span: span.into(),
726 input: rendered,
727 }
728 .into());
729 }
730
731 collect_deprecations(&mut out);
735
736 let get_env = |key: &str| -> Option<String> {
737 if let Some(env_map) = custom_env {
738 env_map.get(key).cloned()
739 } else {
740 std::env::var(key).ok()
741 }
742 };
743
744 for arg in out.cmd.args.iter() {
749 if out.args.contains_key(arg) {
750 continue;
751 }
752 if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
753 if arg_env_is_deprecated(arg, env_name) {
754 out.warnings
755 .push(Warning::env(env_name, arg_current_env(arg)));
756 }
757 let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
758 validate_choice_values(
759 ChoiceTarget::arg(arg),
760 &values,
761 arg.choices.as_ref(),
762 custom_env,
763 )?;
764 let parsed = if arg.var {
765 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
766 ParseValue::MultiString(values)
767 } else {
768 ParseValue::String(values.into_iter().next().unwrap_or_default())
769 };
770 out.args.insert(Arc::new(arg.clone()), parsed);
771 out.arg_origins
772 .entry(Arc::new(arg.clone()))
773 .or_default()
774 .push(ValueOrigin::Env(env_name.to_string()));
775 continue;
776 }
777 if !arg.default.is_empty() {
778 if arg.var {
780 let values = split_fallback_values(&arg.default, arg.delimiter);
781 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
782 validate_choice_values(
783 ChoiceTarget::arg(arg),
784 &values,
785 arg.choices.as_ref(),
786 custom_env,
787 )?;
788 out.args
790 .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
791 out.arg_origins
792 .entry(Arc::new(arg.clone()))
793 .or_default()
794 .push(ValueOrigin::Default);
795 } else {
796 validate_choice_value(
797 ChoiceTarget::arg(arg),
798 &arg.default[0],
799 arg.choices.as_ref(),
800 custom_env,
801 )?;
802 out.args.insert(
804 Arc::new(arg.clone()),
805 ParseValue::String(arg.default[0].clone()),
806 );
807 out.arg_origins
808 .entry(Arc::new(arg.clone()))
809 .or_default()
810 .push(ValueOrigin::Default);
811 }
812 }
813 }
814
815 let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
820 for flag in &flags {
821 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
822 continue;
823 }
824 if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
825 if let Some(warning) = flag_deprecation(flag) {
829 out.warnings.push(warning);
830 }
831 if flag_env_is_deprecated(flag, env_name) {
832 out.warnings
833 .push(Warning::env(env_name, flag_current_env(flag)));
834 }
835 if let Some(arg) = flag.arg.as_ref() {
836 let values =
837 split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
838 validate_choice_values(
839 ChoiceTarget::option(flag),
840 &values,
841 arg.choices.as_ref(),
842 custom_env,
843 )?;
844 let parsed = if flag.var || arg.var {
845 if flag.var {
846 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
847 }
848 if arg.var {
849 validate_flag_arg_fallback_count(
850 flag,
851 arg,
852 values.len(),
853 &mut out.errors,
854 );
855 }
856 ParseValue::MultiString(values)
857 } else {
858 ParseValue::String(values.into_iter().next().unwrap_or_default())
859 };
860 out.flags.insert(Arc::clone(flag), parsed);
861 } else {
862 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
863 out.flags
864 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
865 }
866 out.flag_origins
867 .entry(Arc::clone(flag))
868 .or_default()
869 .push(ValueOrigin::Env(env_name.to_string()));
870 }
871 }
872 let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
878 for flag in &flags {
879 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
880 continue;
881 }
882 if let Some(condition) = flag.default_if.iter().find(|condition| {
883 default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
884 }) {
885 from_default_if.push((Arc::clone(flag), condition.clone()));
886 }
887 }
888 for (flag, condition) in &from_default_if {
889 bind_flag_fallback(
893 flag,
894 std::slice::from_ref(&condition.value),
895 &mut out,
896 custom_env,
897 ValueOrigin::DefaultIf {
898 selector: condition.selector.clone(),
899 when: condition.when.clone(),
900 },
901 )?;
902 }
903 for flag in &flags {
904 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
905 continue;
906 }
907 if !flag.default.is_empty() {
908 bind_flag_fallback(
909 flag,
910 &flag.default,
911 &mut out,
912 custom_env,
913 ValueOrigin::Default,
914 )?;
915 continue;
916 }
917 if let Some(arg) = flag.arg.as_ref() {
918 if !arg.default.is_empty() {
919 bind_flag_fallback(
920 flag,
921 &arg.default,
922 &mut out,
923 custom_env,
924 ValueOrigin::Default,
925 )?;
926 }
927 }
928 }
929 for (arg, parsed) in &out.args {
934 validate_expression(
935 &arg.name,
936 arg.validate.as_deref(),
937 arg.validate_error.as_deref(),
938 parsed,
939 &mut out.errors,
940 );
941 }
942 for (flag, parsed) in &out.flags {
943 if let Some(arg) = &flag.arg {
944 validate_expression(
945 &flag.name,
946 arg.validate.as_deref(),
947 arg.validate_error.as_deref(),
948 parsed,
949 &mut out.errors,
950 );
951 }
952 }
953 crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
956 Ok(out)
957 }
958}
959
960#[must_use = "parsing result should be used"]
967pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
968 Parser::new(spec).parse(input)
969}
970
971#[must_use = "parsing result should be used"]
975pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
976 parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
977}
978
979pub fn multicall_basename(argv0: &str) -> &str {
982 let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
983 match name.get(name.len().saturating_sub(4)..) {
984 Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
985 _ => name,
986 }
987}
988
989pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
994 let base = multicall_basename(argv0);
995 if !name.is_empty() && base == multicall_basename(name) {
996 return None;
997 }
998 if let Some(bin) = bin {
999 if !bin.is_empty() && base == multicall_basename(bin) {
1000 return None;
1001 }
1002 }
1003 Some(base)
1004}
1005
1006#[derive(Clone, Copy, PartialEq, Eq)]
1015enum MountTiming {
1016 Eager,
1017 WhenAWordIsUnknown,
1018}
1019
1020struct Token {
1030 word: String,
1031 argv: usize,
1037 binding: Option<(Arc<SpecFlag>, usize)>,
1049}
1050
1051impl Token {
1052 fn new(word: String, argv: usize) -> Self {
1053 Self {
1054 word,
1055 argv,
1056 binding: None,
1057 }
1058 }
1059}
1060
1061struct Trace {
1067 tokens: Vec<TokenBinding>,
1068}
1069
1070impl Trace {
1071 fn new(input: &[String]) -> Self {
1072 Self {
1073 tokens: input
1074 .iter()
1075 .enumerate()
1076 .map(|(index, word)| TokenBinding {
1077 index,
1078 word: word.clone(),
1079 synthesized: false,
1080 roles: vec![],
1081 })
1082 .collect(),
1083 }
1084 }
1085
1086 fn record(&mut self, argv: usize, role: TokenRole) {
1087 if let Some(token) = self.tokens.get_mut(argv) {
1088 token.roles.push(role);
1089 }
1090 }
1091
1092 fn note_synthesized(&mut self, argv: usize) {
1094 if let Some(token) = self.tokens.get_mut(argv) {
1095 token.synthesized = true;
1096 }
1097 }
1098
1099 fn close(&mut self, unread: &VecDeque<Token>) {
1101 for token in unread {
1102 self.record(token.argv, TokenRole::Unread);
1103 }
1104 }
1105}
1106
1107fn parse_partial_with_env(
1108 spec: &Spec,
1109 input: &[String],
1110 custom_env: Option<&HashMap<String, String>>,
1111 mount_outputs: Option<&HashMap<String, String>>,
1112 mount_timing: MountTiming,
1113) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1114 let mut trace = Trace::new(input);
1115 parse_partial_traced(
1116 spec,
1117 input,
1118 custom_env,
1119 mount_outputs,
1120 mount_timing,
1121 &mut trace,
1122 )
1123}
1124
1125fn parse_partial_traced(
1132 spec: &Spec,
1133 input: &[String],
1134 custom_env: Option<&HashMap<String, String>>,
1135 mount_outputs: Option<&HashMap<String, String>>,
1136 mount_timing: MountTiming,
1137 trace: &mut Trace,
1138) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1139 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1140 let viewed = spec.for_view(view)?;
1141 return parse_partial_traced(
1142 &viewed,
1143 input,
1144 custom_env,
1145 mount_outputs,
1146 mount_timing,
1147 trace,
1148 );
1149 }
1150 trace!("parse_partial: {input:?}");
1151 let mut input = input
1152 .iter()
1153 .enumerate()
1154 .map(|(argv, word)| Token::new(word.clone(), argv))
1155 .collect::<VecDeque<_>>();
1156 let argv0 = input.pop_front();
1157 if let Some(argv0) = argv0.as_ref() {
1158 trace.record(argv0.argv, TokenRole::Program);
1159 }
1160 if spec.multicall {
1161 if let Some(raw) = argv0 {
1162 if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1163 trace.note_synthesized(raw.argv);
1167 input.push_front(Token::new(applet.to_string(), raw.argv));
1168 }
1169 }
1170 }
1171 let mut command_has_argv = !input.is_empty();
1175
1176 let mut out = ParseOutput {
1177 cmd: spec.cmd.clone(),
1178 cmds: vec![spec.cmd.clone()],
1179 args: IndexMap::new(),
1180 flags: IndexMap::new(),
1181 tokens: vec![],
1182 flag_origins: IndexMap::new(),
1183 arg_origins: IndexMap::new(),
1184 overridden_flags: BTreeMap::new(),
1185 available_flags: gather_flags(&spec.cmd),
1186 flag_awaiting_value: vec![],
1187 errors: vec![],
1188 warnings: vec![],
1189 next_arg: None,
1190 double_dash_seen: false,
1191 external: None,
1192 };
1193 let mut overridden_flags = HashSet::new();
1196 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1201
1202 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1215 let mut command_arg_found = false;
1218 let mut variadic_flag_active = false;
1219 let mut idx = 0;
1220 let mut used_default_subcommand = false;
1223 let mut mounts_resolved = false;
1227 let default_outranks_mounts =
1237 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1238 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1239 {
1240 mounts_resolved = true;
1241 let mut mounted = out.cmd.clone();
1242 mounted.mount(&[], mount_outputs)?;
1243 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1244 if let Some(last) = out.cmds.last_mut() {
1245 *last = mounted.clone();
1246 }
1247 out.cmd = mounted;
1248 }
1249
1250 while idx < input.len() {
1251 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1261 default_accepts_word(&out.cmd, name, &input[idx].word)
1262 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1263 });
1264 if !mounts_resolved
1265 && !out.cmd.mounts.is_empty()
1266 && !default_catches_it
1267 && is_command_word(&input[idx].word)
1268 && !is_negative_number(&input[idx].word)
1269 && out.cmd.find_subcommand(&input[idx].word).is_none()
1270 {
1271 mounts_resolved = true;
1272 let mut mounted = out.cmd.clone();
1273 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1274 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1275 if let Some(last) = out.cmds.last_mut() {
1276 *last = mounted.clone();
1277 }
1278 out.cmd = mounted;
1279 }
1280 if variadic_flag_active
1281 && out.cmd.find_subcommand(&input[idx].word).is_some()
1282 && !out.cmd.subcommand_precedence_over_arg
1283 {
1284 break;
1285 }
1286 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1287 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1288 bail!(
1289 "subcommand '{}' cannot be used with arguments on its parent command",
1290 input[idx].word
1291 );
1292 }
1293 let mut subcommand = subcommand.clone();
1294 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1296 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1299 merge_subcommand_flags(
1300 &mut out.available_flags,
1301 gather_flags(&subcommand),
1302 crossing_mount,
1303 );
1304 let selected = input.remove(idx);
1306 if let Some(selected) = selected {
1307 trace.record(
1308 selected.argv,
1309 TokenRole::Command {
1310 name: subcommand.name.clone(),
1311 },
1312 );
1313 }
1314 command_has_argv = idx < input.len();
1315 out.cmds.push(subcommand.clone());
1316 out.cmd = subcommand.clone();
1317 mounts_resolved = true;
1319 prefix_flags.clear();
1320 command_arg_found = false;
1321 variadic_flag_active = false;
1322 } else if !is_command_word(&input[idx].word)
1325 || declared_numeric_short(&out.available_flags, &input[idx].word)
1326 {
1327 let word = input[idx].word.clone();
1329 let flag_key = get_flag_key(&word);
1330
1331 let is_bundle = word.starts_with("--")
1337 || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1338 if let Some(f) = out
1339 .available_flags
1340 .get(flag_key)
1341 .cloned()
1342 .filter(|_| is_bundle)
1343 {
1344 command_arg_found = true;
1345 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1346 input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1354 let mut forwarded = f.global.then(|| vec![word.clone()]);
1355 idx += 1;
1356
1357 if f.arg.is_some()
1360 && !word.contains('=')
1361 && idx < input.len()
1362 && (!is_flag_like(&input[idx].word)
1363 || (f.arg.as_ref().is_some_and(|arg| arg.allow_negative_numbers)
1364 && is_negative_number(&input[idx].word)))
1365 {
1366 if let Some(words) = forwarded.as_mut() {
1367 words.push(input[idx].word.clone());
1368 }
1369 idx += 1;
1370 }
1371 if let Some(words) = forwarded {
1372 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1373 prefix_flags.push((f, words));
1374 }
1375 } else {
1376 break;
1379 }
1380 } else {
1381 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1382 idx += 1;
1383 continue;
1384 }
1385 if !used_default_subcommand && out.cmds.len() == 1 {
1394 if let Some(default_name) = &spec.default_subcommand {
1395 if let Some(subcommand) = out
1396 .cmd
1397 .find_subcommand(default_name)
1398 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1399 {
1400 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1401 bail!(
1402 "subcommand '{}' cannot be used with arguments on its parent command",
1403 subcommand.name
1404 );
1405 }
1406 let mut subcommand = subcommand.clone();
1407 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1409 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1410 merge_subcommand_flags(
1411 &mut out.available_flags,
1412 gather_flags(&subcommand),
1413 crossing_mount,
1414 );
1415 out.cmds.push(subcommand.clone());
1416 out.cmd = subcommand.clone();
1417 command_has_argv = true;
1418 prefix_flags.clear();
1419 command_arg_found = false;
1420 variadic_flag_active = false;
1421 mounts_resolved = true;
1424 used_default_subcommand = true;
1425 continue;
1430 }
1431 }
1432 }
1433 if out.cmd.external_subcommand {
1438 let rest: Vec<Token> = input.drain(idx..).collect();
1439 for token in &rest {
1440 trace.record(token.argv, TokenRole::External);
1441 }
1442 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1443 break;
1444 }
1445 break;
1447 }
1448 }
1449
1450 let mut next_arg_idx: usize = 0;
1460 let mut enable_flags = true;
1461 let mut grouped_flag = false;
1462 let mut seen_double_dash = false;
1466 let mut double_dash_violations: HashSet<String> = HashSet::new();
1469 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1474
1475 while !input.is_empty() {
1476 let token = input.pop_front().unwrap();
1477 let binding = token.binding;
1479 let argv = token.argv;
1480 let mut w = token.word;
1481 let attached_continuation = grouped_flag;
1486
1487 if let Some(ref restart_token) = out.cmd.restart_token {
1490 if w == *restart_token {
1491 out.args.clear();
1496 out.arg_origins.clear();
1501 trace.record(argv, TokenRole::Restart);
1502 next_arg_idx = 0;
1503 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
1507 }
1508 }
1509
1510 if enable_flags
1519 && w.starts_with('-')
1520 && out.flag_awaiting_value.last().is_some_and(|flag| {
1521 !flag.require_equals
1522 && (flag.allow_hyphen_values()
1523 || (flag
1524 .arg
1525 .as_ref()
1526 .is_some_and(|arg| arg.allow_negative_numbers)
1527 && is_negative_number(&w)))
1528 })
1529 {
1530 let should_return = bind_pending_flag_value(
1533 spec,
1534 &out.cmd,
1535 &mut out.errors,
1536 &mut out.flags,
1537 &mut out.flag_awaiting_value,
1538 &mut w,
1539 &mut input,
1540 custom_env,
1541 trace,
1542 argv,
1543 false,
1545 )?;
1546 if should_return {
1547 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1548 return Ok((out, overridden_flags));
1549 }
1550 continue;
1551 }
1552
1553 if enable_flags
1559 && !attached_continuation
1560 && !out.flag_awaiting_value.is_empty()
1561 && out.flag_awaiting_value.last().is_some_and(|flag| {
1562 (flag.default_missing.is_some() || flag.value_optional)
1563 && (flag.require_equals
1564 || (is_flag_like(&w)
1565 && !flag.allow_hyphen_values()
1566 && !(flag
1567 .arg
1568 .as_ref()
1569 .is_some_and(|arg| arg.allow_negative_numbers)
1570 && is_negative_number(&w))))
1571 })
1572 {
1573 try_bind_default_missing(
1574 &mut out.flags,
1575 &mut out.flag_awaiting_value,
1576 custom_env,
1577 &mut out.flag_origins,
1578 )?;
1579 }
1580
1581 if w == "--" && !seen_double_dash {
1587 enable_flags = false;
1588
1589 let should_preserve = out
1592 .cmd
1593 .args
1594 .get(next_arg_idx)
1595 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1596 .unwrap_or(false);
1597
1598 if should_preserve {
1599 } else {
1602 seen_double_dash = true;
1603 trace.record(argv, TokenRole::Separator);
1604
1605 let target = out.cmd.args.iter().position(|arg| {
1611 arg.double_dash == SpecDoubleDashChoices::Required
1612 && !out.args.contains_key(arg)
1613 });
1614 if let Some(target) = target {
1615 if target > next_arg_idx {
1622 next_arg_idx = target;
1623 }
1624 }
1625 continue;
1626 }
1627 }
1628
1629 if enable_flags && w.starts_with("--") {
1631 grouped_flag = false;
1632 let split = w.split_once('=');
1636 let word = split.map(|(word, _)| word).unwrap_or(&w);
1637 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1638 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1639 let command_level = binding
1640 .as_ref()
1641 .map(|(_, level)| *level)
1642 .unwrap_or(out.cmds.len() - 1);
1643 parsed_flag_spellings
1644 .entry(Arc::as_ptr(f) as usize)
1645 .or_default()
1646 .insert(word.to_string());
1647 trace.record(
1650 argv,
1651 TokenRole::Flag {
1652 flag: Arc::clone(f),
1653 spelling: word.to_string(),
1654 negated: f.negate.as_deref() == Some(word),
1655 },
1656 );
1657 if f.action != crate::SpecFlagAction::Set {
1658 out.errors.push(render_action_err(spec, &out.cmd, f, word));
1659 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1660 return Ok((out, overridden_flags));
1661 }
1662 apply_flag_overrides(
1663 f,
1664 &out.available_flags,
1665 &mut out.flags,
1666 &mut out.flag_awaiting_value,
1667 &mut overridden_flags,
1668 &mut out.overridden_flags,
1669 );
1670 if f.arg.is_some() {
1676 record_scalar_flag_occurrence(
1677 &out.cmds,
1678 f,
1679 command_level,
1680 None,
1681 &mut scalar_occurrences,
1682 &mut out.errors,
1683 );
1684 let f = Arc::clone(f);
1685 out.flag_awaiting_value.push(Arc::clone(&f));
1686 if let Some((_, val)) = split {
1691 let mut val = val.to_string();
1695 let should_return = bind_pending_flag_value(
1696 spec,
1697 &out.cmd,
1698 &mut out.errors,
1699 &mut out.flags,
1700 &mut out.flag_awaiting_value,
1701 &mut val,
1702 &mut input,
1703 custom_env,
1704 trace,
1705 argv,
1706 true,
1709 )?;
1710 if should_return {
1711 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1712 return Ok((out, overridden_flags));
1713 }
1714 }
1715 } else if f.count {
1716 let arr = out
1717 .flags
1718 .entry(Arc::clone(f))
1719 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1720 .try_as_multi_bool_mut()
1721 .unwrap();
1722 arr.push(true);
1723 } else {
1724 let negate = f.negate.clone().unwrap_or_default();
1725 let negated_form = word == negate;
1726 let value = if f.bool_value {
1727 match split.map(|(_, value)| value) {
1728 Some("true") => !negated_form,
1729 Some("false") => negated_form,
1730 Some(value) => {
1731 out.errors.push(UsageErr::InvalidValue {
1732 name: f.name.clone(),
1733 value: value.to_string(),
1734 reason: "expected `true` or `false`".to_string(),
1735 });
1736 continue;
1737 }
1738 None => !negated_form,
1739 }
1740 } else {
1741 !negated_form
1742 };
1743 record_scalar_flag_occurrence(
1748 &out.cmds,
1749 f,
1750 command_level,
1751 Some(!negated_form),
1752 &mut scalar_occurrences,
1753 &mut out.errors,
1754 );
1755 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1756 }
1757 continue;
1758 }
1759 if is_help_arg(spec, &out.cmd, &w) {
1760 out.errors
1761 .push(render_help_err(spec, &out.cmd, w.len() > 2));
1762 trace.record(
1763 argv,
1764 TokenRole::Builtin {
1765 spelling: w.clone(),
1766 },
1767 );
1768 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1769 return Ok((out, overridden_flags));
1770 }
1771 if is_version_arg(spec, &out.cmds, &w) {
1772 out.errors.push(render_version_err(spec, w.len() > 2));
1773 trace.record(
1774 argv,
1775 TokenRole::Builtin {
1776 spelling: w.clone(),
1777 },
1778 );
1779 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1780 return Ok((out, overridden_flags));
1781 }
1782 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1783 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1784 trace.close(&input);
1785 return Err(refused.into());
1786 }
1787 }
1788
1789 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1796 let positional_negative_number = !declared_numeric_short
1797 && is_negative_number(&w)
1798 && out
1799 .cmd
1800 .args
1801 .get(next_arg_idx)
1802 .is_some_and(|arg| arg.allow_negative_numbers);
1803 if enable_flags
1804 && !grouped_flag
1805 && binding.is_none()
1808 && w.starts_with('-')
1809 && w.len() > 1
1810 && is_flag_like(&w)
1811 && !positional_negative_number
1812 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1813 {
1814 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1817 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1818 trace.close(&input);
1819 return Err(refused.into());
1820 }
1821 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
1822 let short = w.chars().nth(1).unwrap();
1823 if let Some(f) = binding
1824 .as_ref()
1825 .map(|(flag, _)| flag)
1826 .or_else(|| out.available_flags.get(&format!("-{short}")))
1827 {
1828 let command_level = binding
1829 .as_ref()
1830 .map(|(_, level)| *level)
1831 .unwrap_or(out.cmds.len() - 1);
1832 if f.action != crate::SpecFlagAction::Set {
1833 out.errors
1834 .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
1835 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1836 return Ok((out, overridden_flags));
1837 }
1838 parsed_flag_spellings
1839 .entry(Arc::as_ptr(f) as usize)
1840 .or_default()
1841 .insert(format!("-{short}"));
1842 trace.record(
1843 argv,
1844 TokenRole::Flag {
1845 flag: Arc::clone(f),
1846 spelling: format!("-{short}"),
1847 negated: false,
1849 },
1850 );
1851 apply_flag_overrides(
1852 f,
1853 &out.available_flags,
1854 &mut out.flags,
1855 &mut out.flag_awaiting_value,
1856 &mut overridden_flags,
1857 &mut out.overridden_flags,
1858 );
1859 let rest = &w[1 + short.len_utf8()..];
1860 if !rest.is_empty() {
1861 input.push_front(Token::new(format!("-{rest}"), argv));
1864 }
1865 grouped_flag = !rest.is_empty();
1869 if f.arg.is_some() {
1870 record_scalar_flag_occurrence(
1871 &out.cmds,
1872 f,
1873 command_level,
1874 None,
1875 &mut scalar_occurrences,
1876 &mut out.errors,
1877 );
1878 out.flag_awaiting_value.push(Arc::clone(f));
1879 } else if f.count {
1880 let arr = out
1881 .flags
1882 .entry(Arc::clone(f))
1883 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1884 .try_as_multi_bool_mut()
1885 .unwrap();
1886 arr.push(true);
1887 } else {
1888 let negate = f.negate.clone().unwrap_or_default();
1889 let value = w != negate;
1890 record_scalar_flag_occurrence(
1891 &out.cmds,
1892 f,
1893 command_level,
1894 Some(value),
1895 &mut scalar_occurrences,
1896 &mut out.errors,
1897 );
1898 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1899 }
1900 continue;
1901 }
1902 if let Some(err) = supplied_short(spec, &out.cmds, short) {
1906 out.errors.push(err);
1907 trace.record(
1908 argv,
1909 TokenRole::Builtin {
1910 spelling: format!("-{short}"),
1911 },
1912 );
1913 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1914 return Ok((out, overridden_flags));
1915 }
1916 if is_help_arg(spec, &out.cmd, &w) {
1917 out.errors
1918 .push(render_help_err(spec, &out.cmd, w.len() > 2));
1919 trace.record(
1920 argv,
1921 TokenRole::Builtin {
1922 spelling: w.clone(),
1923 },
1924 );
1925 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1926 return Ok((out, overridden_flags));
1927 }
1928 if is_version_arg(spec, &out.cmds, &w) {
1929 out.errors.push(render_version_err(spec, w.len() > 2));
1930 trace.record(
1931 argv,
1932 TokenRole::Builtin {
1933 spelling: w.clone(),
1934 },
1935 );
1936 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1937 return Ok((out, overridden_flags));
1938 }
1939 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1940 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1941 trace.close(&input);
1942 return Err(refused.into());
1943 }
1944 if grouped_flag {
1945 grouped_flag = false;
1946 w.remove(0);
1947 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
1951 w.remove(0);
1952 }
1953 }
1954 }
1955
1956 if enable_flags
1967 && !attached_continuation
1968 && !out.flag_awaiting_value.is_empty()
1969 && out
1970 .flag_awaiting_value
1971 .last()
1972 .is_some_and(|flag| flag.require_equals)
1973 {
1974 let flag = out.flag_awaiting_value.last().unwrap();
1975 let token = flag
1976 .long
1977 .first()
1978 .map(|l| format!("--{l}"))
1979 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
1980 .unwrap_or_else(|| flag.name.clone());
1981 out.errors.push(UsageErr::InvalidFlag {
1982 token: token.clone(),
1983 reason: "requires an argument".to_string(),
1984 span: (0, 0).into(),
1985 input: format!("{token} {w}"),
1986 });
1987 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1988 return Ok((out, overridden_flags));
1989 }
1990 if enable_flags && !out.flag_awaiting_value.is_empty() {
1991 let should_return = bind_pending_flag_value(
1994 spec,
1995 &out.cmd,
1996 &mut out.errors,
1997 &mut out.flags,
1998 &mut out.flag_awaiting_value,
1999 &mut w,
2000 &mut input,
2001 custom_env,
2002 trace,
2003 argv,
2004 attached_continuation,
2005 )?;
2006 if should_return {
2007 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2008 return Ok((out, overridden_flags));
2009 }
2010 continue;
2011 }
2012
2013 if out.cmd.allow_missing_positional {
2014 while let Some(current) = out.cmd.args.get(next_arg_idx) {
2015 if current.required || out.args.contains_key(current) {
2016 break;
2017 }
2018 let required_after = out.cmd.args[next_arg_idx + 1..]
2019 .iter()
2020 .filter(|arg| arg.required)
2021 .count();
2022 if required_after == 0 {
2023 break;
2024 }
2025 let remaining_values = 1 + input
2026 .iter()
2027 .filter(|token| !enable_flags || !is_flag_like(&token.word))
2028 .count();
2029 if remaining_values > required_after {
2030 break;
2031 }
2032 next_arg_idx += 1;
2033 }
2034 }
2035
2036 if let Some(arg) = out.cmd.args.get(next_arg_idx) {
2037 if arg.var
2038 && out.args.contains_key(arg)
2039 && arg.value_terminator.as_deref() == Some(w.as_str())
2040 {
2041 trace.record(
2042 argv,
2043 TokenRole::ValueTerminator {
2044 ends: arg.name.clone(),
2045 },
2046 );
2047 next_arg_idx += 1;
2048 continue;
2049 }
2050 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2054 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2055 trace.record(
2056 argv,
2057 TokenRole::Refused {
2058 reason: format!("{} only accepts words after `--`", arg.name),
2059 },
2060 );
2061 continue;
2065 }
2066 let trailing_value =
2071 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2072 let suppress_trailing_delimiter =
2073 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2074 let delimiter = if suppress_trailing_delimiter && trailing_value {
2075 None
2076 } else {
2077 arg.delimiter
2078 };
2079 let parts: Vec<String> = match delimiter {
2080 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2081 None => vec![w.clone()],
2082 };
2083 let mut refused = false;
2084 for part in &parts {
2085 if validate_choices(
2086 spec,
2087 &out.cmd,
2088 &mut out.errors,
2089 ChoiceTarget::arg(arg),
2090 part,
2091 arg.choices.as_ref(),
2092 custom_env,
2093 )? {
2094 refused = true;
2095 break;
2096 }
2097 }
2098 if refused {
2099 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2100 return Ok((out, overridden_flags));
2101 }
2102 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2107 enable_flags = false;
2108 }
2109 let unknown_flag = enable_flags
2114 && !positional_negative_number
2115 && is_flag_like(&w)
2116 && binding.is_none();
2117 trace.record(
2118 argv,
2119 if unknown_flag {
2120 TokenRole::UnknownFlag {
2121 bound_as: Some(Arc::new(arg.clone())),
2122 }
2123 } else {
2124 TokenRole::Arg {
2125 arg: Arc::new(arg.clone()),
2126 values: parts.clone(),
2127 }
2128 },
2129 );
2130 if arg.var {
2131 let arr = out
2132 .args
2133 .entry(Arc::new(arg.clone()))
2134 .or_insert_with(|| ParseValue::MultiString(vec![]))
2135 .try_as_multi_string_mut()
2136 .unwrap();
2137 arr.extend(parts.iter().cloned());
2141 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2142 next_arg_idx += 1;
2143 }
2144 } else {
2145 out.args
2146 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2147 next_arg_idx += 1;
2148 }
2149 continue;
2150 }
2151 if is_help_arg(spec, &out.cmd, &w) {
2152 out.errors
2153 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2154 trace.record(
2155 argv,
2156 TokenRole::Builtin {
2157 spelling: w.clone(),
2158 },
2159 );
2160 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2161 return Ok((out, overridden_flags));
2162 }
2163 if is_version_arg(spec, &out.cmds, &w) {
2164 out.errors.push(render_version_err(spec, w.len() > 2));
2165 trace.record(
2166 argv,
2167 TokenRole::Builtin {
2168 spelling: w.clone(),
2169 },
2170 );
2171 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2172 return Ok((out, overridden_flags));
2173 }
2174 trace.record(
2175 argv,
2176 TokenRole::Refused {
2177 reason: "no declaration takes this word".to_string(),
2178 },
2179 );
2180 trace.close(&input);
2181 bail!("unexpected word: {w}");
2182 }
2183
2184 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2185
2186 let flag_was_parsed =
2191 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2192
2193 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2201 let declared: HashSet<String> = out
2202 .cmd
2203 .flags
2204 .iter()
2205 .filter(|declared| declared.name == flag.name)
2206 .flat_map(flag_keys)
2207 .collect();
2208 let speaks_for_this_flag = declared.iter().any(|spelling| {
2218 out.available_flags
2219 .get(spelling)
2220 .is_some_and(|available| Arc::ptr_eq(available, flag))
2221 });
2222 if speaks_for_this_flag {
2223 declared
2224 } else {
2225 HashSet::new()
2226 }
2227 };
2228
2229 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2237 let child = child_spellings(flag);
2238 let child_exclusive = !child.is_empty()
2239 && out
2240 .cmd
2241 .flags
2242 .iter()
2243 .any(|declared| declared.name == flag.name && declared.exclusive);
2244 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2245 Some(spellings) => (
2246 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2247 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2248 ),
2249 None => (child_exclusive, flag.exclusive && child.is_empty()),
2253 }
2254 };
2255
2256 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2257 let (child, ancestor) = exclusivity_in_play(flag);
2258 child || ancestor
2259 };
2260
2261 let exclusive_present =
2266 unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2267 exclusive_occurrence(flag)
2268 && !overridden_flags.contains(&flag.name)
2269 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2270 });
2271 let requirements_apply = |command_index: usize| {
2272 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2273 };
2274
2275 if out.cmd.arg_required_else_help && !command_has_argv {
2276 out.errors.push(render_help_err(spec, &out.cmd, false));
2277 }
2278
2279 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2285 let mut names: Vec<&str> = out
2286 .cmd
2287 .subcommands
2288 .iter()
2289 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2292 .map(|(name, _)| name.as_str())
2293 .collect();
2294 names.sort_unstable();
2295 out.errors.push(UsageErr::MissingSubcommand(
2296 out.cmd.name.clone(),
2297 names.join(", "),
2298 ));
2299 }
2300
2301 if !exclusive_present {
2304 for arg in out
2305 .cmds
2306 .iter()
2307 .enumerate()
2308 .filter(|(index, _)| requirements_apply(*index))
2309 .flat_map(|(_, cmd)| &cmd.args)
2310 {
2311 if out.args.contains_key(arg) {
2312 continue;
2313 }
2314 if double_dash_violations.contains(&arg.name) {
2316 continue;
2317 }
2318 let required_if = arg.required_if.iter().any(|selector| {
2319 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2320 });
2321 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2322 selector_explicit_has_value(
2323 &condition.selector,
2324 &condition.value,
2325 &out,
2326 &overridden_flags,
2327 custom_env,
2328 )
2329 });
2330 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2331 && arg.required_if_eq_all.iter().all(|condition| {
2332 selector_explicit_has_value(
2333 &condition.selector,
2334 &condition.value,
2335 &out,
2336 &overridden_flags,
2337 custom_env,
2338 )
2339 });
2340 let unless_any = arg.required_unless.iter().any(|selector| {
2341 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2342 });
2343 let unless_all = !arg.required_unless_all.is_empty()
2344 && arg.required_unless_all.iter().all(|selector| {
2345 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2346 });
2347 let required_unless = (!arg.required_unless.is_empty()
2348 || !arg.required_unless_all.is_empty())
2349 && !(unless_any || unless_all);
2350 if (arg.required
2351 || required_if
2352 || required_if_eq
2353 || required_if_eq_all
2354 || required_unless)
2355 && arg.default.is_empty()
2356 {
2357 let has_env = arg
2359 .env
2360 .as_ref()
2361 .is_some_and(|env_var| env_contains(custom_env, env_var));
2362 if !has_env {
2363 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2364 }
2365 }
2366 }
2367 }
2368
2369 for flag in unique_flags(out.available_flags.values()) {
2381 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2382 if !given || overridden_flags.contains(&flag.name) {
2383 continue;
2384 }
2385 for other in &flag.conflicts {
2386 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2387 out.errors.push(UsageErr::InvalidFlag {
2388 token: format!("--{}", flag.name),
2389 reason: format!("conflicts with {other}"),
2390 span: (0, 0).into(),
2391 input: format!("--{} {other}", flag.name),
2392 });
2393 }
2394 }
2395 let owner = out
2406 .cmds
2407 .iter()
2408 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2409 .unwrap_or(out.cmds.len() - 1);
2410 if !exclusive_present && requirements_apply(owner) {
2411 for other in &flag.requires {
2412 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2413 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2414 if other.starts_with('-') {
2415 out.errors.push(UsageErr::MissingFlag(name));
2416 } else {
2417 out.errors.push(UsageErr::MissingArg(name));
2418 }
2419 }
2420 }
2421 for condition in &flag.requires_if {
2422 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2423 && !selector_is_satisfied(
2424 &condition.requires,
2425 &out,
2426 &overridden_flags,
2427 custom_env,
2428 )
2429 {
2430 let name = selector_flag_name(&condition.requires, &out)
2431 .unwrap_or_else(|| condition.requires.clone());
2432 out.errors.push(UsageErr::MissingFlag(name));
2433 }
2434 }
2435 }
2436 }
2437
2438 for (command_index, arg) in out
2441 .cmds
2442 .iter()
2443 .enumerate()
2444 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2445 {
2446 let given = arg_is_explicit(arg, &out, custom_env);
2447 if !given {
2448 continue;
2449 }
2450 for other in &arg.conflicts {
2451 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2452 out.errors.push(UsageErr::InvalidFlag {
2453 token: arg.name.clone(),
2454 reason: format!("conflicts with {other}"),
2455 span: (0, 0).into(),
2456 input: format!("{} {other}", arg.name),
2457 });
2458 }
2459 }
2460 if !exclusive_present && requirements_apply(command_index) {
2461 for other in &arg.requires {
2462 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2463 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2464 if other.starts_with('-') {
2465 out.errors.push(UsageErr::MissingFlag(name));
2466 } else {
2467 out.errors.push(UsageErr::MissingArg(name));
2468 }
2469 }
2470 }
2471 }
2472 }
2473
2474 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2484 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2489 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2490 continue;
2491 }
2492 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2493 .find(|other| {
2494 !Arc::ptr_eq(other, flag)
2495 && !overridden_flags.contains(&other.name)
2496 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2497 })
2498 .map(|other| format!("--{}", other.name));
2499 let other_arg = out.cmd.args.iter().find(|arg| {
2500 out.args.keys().any(|given| given.name == arg.name)
2501 || arg
2502 .env
2503 .as_ref()
2504 .is_some_and(|env| env_contains(custom_env, env))
2505 });
2506 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2512 let selected_subcommand =
2513 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2514 let other = other_flag
2515 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2516 .or(selected_subcommand);
2517 if let Some(other) = other {
2518 out.errors.push(UsageErr::InvalidFlag {
2519 token: format!("--{}", flag.name),
2520 reason: format!("must be given on its own, and {other} was given too"),
2521 span: (0, 0).into(),
2522 input: format!("--{} {other}", flag.name),
2523 });
2524 }
2525 }
2526
2527 let mut group_errors: Vec<UsageErr> = Vec::new();
2536 for (command_index, group) in out
2537 .cmds
2538 .iter()
2539 .enumerate()
2540 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2541 {
2542 let mut given: Vec<&str> = Vec::new();
2548 let mut seen: Vec<String> = Vec::new();
2549 for selector in &group.members {
2550 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2551 continue;
2552 }
2553 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2554 if seen.contains(&name) {
2555 continue;
2556 }
2557 seen.push(name);
2558 given.push(selector.as_str());
2559 }
2560 if !group.multiple && given.len() > 1 {
2561 group_errors.push(UsageErr::InvalidFlag {
2562 token: given[1].to_string(),
2563 reason: format!("cannot be used with {} in group {}", given[0], group.name),
2564 span: (0, 0).into(),
2565 input: format!("{} {}", given[0], given[1]),
2566 });
2567 }
2568 let satisfied = group
2573 .members
2574 .iter()
2575 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2576 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2577 group_errors.push(UsageErr::MissingGroup {
2581 group: group.name.clone(),
2582 members: group.members.join(", "),
2583 });
2584 }
2585 }
2586 out.errors.extend(group_errors);
2587
2588 if !exclusive_present {
2589 for flag in unique_flags(out.available_flags.values()) {
2590 let owner = out
2591 .cmds
2592 .iter()
2593 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2594 .unwrap_or(out.cmds.len() - 1);
2595 if !requirements_apply(owner) {
2596 continue;
2597 }
2598 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2599 continue;
2600 }
2601 let has_default =
2602 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2603 let has_env = flag_has_env(flag, custom_env);
2604 let required_if = flag.required_if.iter().any(|selector| {
2605 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2606 });
2607 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2608 selector_explicit_has_value(
2609 &condition.selector,
2610 &condition.value,
2611 &out,
2612 &overridden_flags,
2613 custom_env,
2614 )
2615 });
2616 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2617 && flag.required_if_eq_all.iter().all(|condition| {
2618 selector_explicit_has_value(
2619 &condition.selector,
2620 &condition.value,
2621 &out,
2622 &overridden_flags,
2623 custom_env,
2624 )
2625 });
2626 let unless_any = flag.required_unless.iter().any(|selector| {
2627 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2628 });
2629 let unless_all = !flag.required_unless_all.is_empty()
2630 && flag.required_unless_all.iter().all(|selector| {
2631 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2632 });
2633 let required_unless = (!flag.required_unless.is_empty()
2634 || !flag.required_unless_all.is_empty())
2635 && !(unless_any || unless_all);
2636 if (flag.required
2637 || required_if
2638 || required_if_eq
2639 || required_if_eq_all
2640 || required_unless)
2641 && !has_default
2642 && !has_env
2643 {
2644 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2645 }
2646 }
2647 }
2648
2649 for (arg, value) in &out.args {
2651 if arg.var {
2652 if let ParseValue::MultiString(values) = value {
2653 if let Some(min) = arg.var_min {
2654 if values.len() < min {
2655 out.errors.push(UsageErr::VarArgTooFew {
2656 name: arg.name.clone(),
2657 min,
2658 got: values.len(),
2659 });
2660 }
2661 }
2662 if let Some(max) = arg.var_max {
2663 if values.len() > max {
2664 out.errors.push(UsageErr::VarArgTooMany {
2665 name: arg.name.clone(),
2666 max,
2667 got: values.len(),
2668 });
2669 }
2670 }
2671 }
2672 }
2673 }
2674
2675 for flag in unique_flags(out.available_flags.values()) {
2679 if flag.var {
2680 let bound = match out.flags.get(flag) {
2681 Some(ParseValue::MultiString(values)) => values.len(),
2682 Some(ParseValue::MultiBool(values)) => values.len(),
2683 Some(_) => 1,
2684 None => 0,
2685 };
2686 let pending = out
2690 .flag_awaiting_value
2691 .iter()
2692 .filter(|pending| {
2693 Arc::ptr_eq(pending, flag)
2694 && (pending.value_optional || pending.default_missing.is_some())
2695 })
2696 .count();
2697 let count = bound + pending;
2698 if count == 0 {
2699 continue;
2700 }
2701 if let Some(min) = flag.var_min {
2702 if count < min {
2703 out.errors.push(UsageErr::VarFlagTooFew {
2704 name: flag.name.clone(),
2705 min,
2706 got: count,
2707 });
2708 }
2709 }
2710 if let Some(max) = flag.var_max {
2711 if count > max {
2712 out.errors.push(UsageErr::VarFlagTooMany {
2713 name: flag.name.clone(),
2714 max,
2715 got: count,
2716 });
2717 }
2718 }
2719 }
2720 }
2721
2722 Ok((out, overridden_flags))
2723}
2724
2725fn validate_expression(
2726 name: &str,
2727 expression: Option<&str>,
2728 message: Option<&str>,
2729 parsed: &ParseValue,
2730 errors: &mut Vec<UsageErr>,
2731) {
2732 let Some(expression) = expression else {
2733 return;
2734 };
2735 #[cfg(not(feature = "validation"))]
2736 let _ = expression;
2737 let values: &[String] = match parsed {
2738 ParseValue::String(value) => std::slice::from_ref(value),
2739 ParseValue::MultiString(values) => values,
2740 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
2741 };
2742 #[cfg(feature = "validation")]
2743 for value in values {
2744 let reason = match usage_validation::validate(expression, value) {
2745 Ok(true) => continue,
2746 Ok(false) => message
2747 .unwrap_or("does not satisfy the validation expression")
2748 .to_string(),
2749 Err(error) => format!("validation expression failed: {error}"),
2750 };
2751 errors.push(UsageErr::InvalidValue {
2752 name: name.to_string(),
2753 value: value.clone(),
2754 reason,
2755 });
2756 break;
2757 }
2758 #[cfg(not(feature = "validation"))]
2759 if let Some(value) = values.first() {
2760 let _ = message;
2761 errors.push(UsageErr::InvalidValue {
2762 name: name.to_string(),
2763 value: value.clone(),
2764 reason: "expression validation requires the `validation` feature".to_string(),
2765 });
2766 }
2767}
2768
2769#[cfg(all(test, not(feature = "validation")))]
2770mod optional_validation_tests {
2771 use crate::{parse, Spec};
2772
2773 #[test]
2774 fn validation_declarations_require_the_opt_in_runtime_feature() {
2775 let spec: Spec = r#"
2776name "ex"
2777bin "ex"
2778arg "<port>" validate="int(value) > 0"
2779 "#
2780 .parse()
2781 .unwrap();
2782 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
2783 assert!(
2784 error
2785 .to_string()
2786 .contains("requires the `validation` feature"),
2787 "{error:?}"
2788 );
2789 }
2790}
2791
2792fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
2793 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
2794}
2795
2796fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
2797 overrider
2798 .overrides
2799 .iter()
2800 .any(|selector| flag_matches_selector(overridden, selector))
2801}
2802
2803fn apply_prefix_flag_overrides(
2804 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
2805 flag: Arc<SpecFlag>,
2806) {
2807 prefix_flags
2808 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
2809}
2810
2811fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
2812 prefix_flags
2813 .iter()
2814 .flat_map(|(_, words)| words.iter().cloned())
2815 .collect()
2816}
2817
2818fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
2819 match custom_env {
2820 Some(env) => env.contains_key(env_var),
2821 None => std::env::var(env_var).is_ok(),
2822 }
2823}
2824
2825fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
2826 flag.env_names()
2827 .any(|env_var| env_contains(custom_env, env_var))
2828}
2829
2830fn fallback_is_true(value: &str) -> bool {
2831 matches!(value, "1" | "true" | "True" | "TRUE")
2832}
2833
2834fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
2835 match delimiter {
2836 Some(delimiter) => values
2837 .iter()
2838 .flat_map(|value| value.split(delimiter).map(str::to_string))
2839 .collect(),
2840 None => values.to_vec(),
2841 }
2842}
2843
2844fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
2845 if let Some(min) = arg.var_min {
2846 if count < min {
2847 errors.push(UsageErr::VarArgTooFew {
2848 name: arg.name.clone(),
2849 min,
2850 got: count,
2851 });
2852 }
2853 }
2854 if let Some(max) = arg.var_max {
2855 if count > max {
2856 errors.push(UsageErr::VarArgTooMany {
2857 name: arg.name.clone(),
2858 max,
2859 got: count,
2860 });
2861 }
2862 }
2863}
2864
2865fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
2866 if let Some(min) = flag.var_min {
2867 if count < min {
2868 errors.push(UsageErr::VarFlagTooFew {
2869 name: flag.name.clone(),
2870 min,
2871 got: count,
2872 });
2873 }
2874 }
2875 if let Some(max) = flag.var_max {
2876 if count > max {
2877 errors.push(UsageErr::VarFlagTooMany {
2878 name: flag.name.clone(),
2879 max,
2880 got: count,
2881 });
2882 }
2883 }
2884}
2885
2886fn validate_flag_arg_fallback_count(
2887 flag: &SpecFlag,
2888 arg: &SpecArg,
2889 count: usize,
2890 errors: &mut Vec<UsageErr>,
2891) {
2892 if let Some(min) = arg.var_min {
2893 if count < min {
2894 errors.push(UsageErr::VarFlagTooFew {
2895 name: flag.name.clone(),
2896 min,
2897 got: count,
2898 });
2899 }
2900 }
2901 if let Some(max) = arg.var_max {
2902 if count > max {
2903 errors.push(UsageErr::VarFlagTooMany {
2904 name: flag.name.clone(),
2905 max,
2906 got: count,
2907 });
2908 }
2909 }
2910}
2911
2912fn bind_flag_fallback(
2915 flag: &Arc<SpecFlag>,
2916 values: &[String],
2917 out: &mut ParseOutput,
2918 custom_env: Option<&HashMap<String, String>>,
2919 origin: ValueOrigin,
2920) -> Result<(), miette::Error> {
2921 if values.is_empty() {
2922 return Ok(());
2923 }
2924 if let Some(arg) = flag.arg.as_ref() {
2925 let values = split_fallback_values(values, arg.delimiter);
2926 if flag.var || arg.var {
2927 if flag.var {
2928 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
2929 }
2930 if arg.var {
2931 validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
2932 }
2933 validate_choice_values(
2934 ChoiceTarget::option(flag),
2935 &values,
2936 arg.choices.as_ref(),
2937 custom_env,
2938 )?;
2939 out.flags
2940 .insert(Arc::clone(flag), ParseValue::MultiString(values));
2941 } else {
2942 let value = values.into_iter().next().unwrap_or_default();
2943 validate_choice_value(
2944 ChoiceTarget::option(flag),
2945 &value,
2946 arg.choices.as_ref(),
2947 custom_env,
2948 )?;
2949 out.flags
2950 .insert(Arc::clone(flag), ParseValue::String(value));
2951 }
2952 } else if flag.var {
2953 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
2954 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
2955 out.flags
2956 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
2957 } else {
2958 out.flags.insert(
2959 Arc::clone(flag),
2960 ParseValue::Bool(fallback_is_true(&values[0])),
2961 );
2962 }
2963 out.flag_origins
2964 .entry(Arc::clone(flag))
2965 .or_default()
2966 .push(origin);
2967 Ok(())
2968}
2969
2970fn default_if_condition_matches(
2971 condition: &crate::SpecDefaultIf,
2972 out: &ParseOutput,
2973 overridden_flags: &HashSet<String>,
2974 custom_env: Option<&HashMap<String, String>>,
2975) -> bool {
2976 match &condition.when {
2977 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
2978 Some(when) => {
2979 let Some(flag) = out
2980 .available_flags
2981 .values()
2982 .chain(out.flags.keys())
2983 .find(|flag| flag_matches_selector(flag, &condition.selector))
2984 else {
2985 return false;
2986 };
2987 if overridden_flags.contains(&flag.name) {
2988 return false;
2989 }
2990 explicit_flag_has_value(flag, when, out, custom_env)
2991 }
2992 }
2993}
2994
2995fn explicit_flag_has_value(
3001 flag: &SpecFlag,
3002 expected: &str,
3003 out: &ParseOutput,
3004 custom_env: Option<&HashMap<String, String>>,
3005) -> bool {
3006 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3007 ParseValue::Bool(value) => value.to_string() == expected,
3008 ParseValue::String(value) => value == expected,
3009 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3010 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3011 });
3012 if out.flags.contains_key(flag) {
3013 return parsed_matches;
3014 }
3015
3016 let value = flag.env_names().find_map(|env| match custom_env {
3017 Some(values) => values.get(env).cloned(),
3018 None => std::env::var(env).ok(),
3019 });
3020 value.is_some_and(
3021 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3022 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3023 None if flag.arg.is_none() => {
3024 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3025 }
3026 None => value == expected,
3027 },
3028 )
3029}
3030
3031fn selector_explicit_has_value(
3032 selector: &str,
3033 expected: &str,
3034 out: &ParseOutput,
3035 overridden_flags: &HashSet<String>,
3036 custom_env: Option<&HashMap<String, String>>,
3037) -> bool {
3038 if let Some(flag) = out
3039 .available_flags
3040 .values()
3041 .chain(out.flags.keys())
3042 .find(|flag| flag_matches_selector(flag, selector))
3043 {
3044 return !overridden_flags.contains(&flag.name)
3045 && explicit_flag_has_value(flag, expected, out, custom_env);
3046 }
3047 let Some(arg) = selector_arg(selector, out) else {
3048 return false;
3049 };
3050 let parsed = out
3051 .args
3052 .iter()
3053 .find(|(given, _)| given.name == arg.name)
3054 .map(|(_, value)| value);
3055 if let Some(value) = parsed {
3056 return match value {
3057 ParseValue::String(value) => value == expected,
3058 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3059 ParseValue::Bool(value) => value.to_string() == expected,
3060 ParseValue::MultiBool(values) => {
3061 values.iter().any(|value| value.to_string() == expected)
3062 }
3063 };
3064 }
3065 let value = arg.env_names().find_map(|env| match custom_env {
3066 Some(values) => values.get(env).cloned(),
3067 None => std::env::var(env).ok(),
3068 });
3069 value.is_some_and(|value| match arg.delimiter {
3070 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3071 None => value == expected,
3072 })
3073}
3074
3075fn selector_is_explicit(
3076 selector: &str,
3077 out: &ParseOutput,
3078 overridden_flags: &HashSet<String>,
3079 custom_env: Option<&HashMap<String, String>>,
3080) -> bool {
3081 let flag_is_explicit = out
3082 .available_flags
3083 .values()
3084 .chain(out.flags.keys())
3085 .any(|flag| {
3086 flag_matches_selector(flag, selector)
3087 && !overridden_flags.contains(&flag.name)
3088 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3089 });
3090 flag_is_explicit
3091 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3092}
3093
3094fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3100 out.available_flags
3101 .values()
3102 .chain(out.flags.keys())
3103 .find(|flag| flag_matches_selector(flag, selector))
3104 .map(|flag| flag.name.clone())
3105 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3106}
3107
3108fn selector_is_satisfied(
3118 selector: &str,
3119 out: &ParseOutput,
3120 overridden_flags: &HashSet<String>,
3121 custom_env: Option<&HashMap<String, String>>,
3122) -> bool {
3123 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3124 return true;
3125 }
3126 let flag_is_satisfied = out
3127 .available_flags
3128 .values()
3129 .chain(out.flags.keys())
3130 .filter(|flag| flag_matches_selector(flag, selector))
3131 .any(|flag| {
3132 !overridden_flags.contains(&flag.name)
3133 && (!flag.default.is_empty()
3134 || flag.arg.iter().any(|a| !a.default.is_empty())
3135 || flag.default_if.iter().any(|condition| {
3136 default_if_condition_matches(condition, out, overridden_flags, custom_env)
3137 }))
3138 });
3139 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3140}
3141
3142fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3143 if selector.starts_with('-') {
3146 return None;
3147 }
3148 out.cmds
3149 .iter()
3150 .flat_map(|cmd| &cmd.args)
3151 .find(|arg| arg.name == selector)
3152}
3153
3154fn arg_is_explicit(
3155 arg: &SpecArg,
3156 out: &ParseOutput,
3157 custom_env: Option<&HashMap<String, String>>,
3158) -> bool {
3159 out.args.keys().any(|given| given.name == arg.name)
3160 || arg
3161 .env
3162 .as_ref()
3163 .is_some_and(|env| env_contains(custom_env, env))
3164}
3165
3166fn apply_flag_overrides(
3167 flag: &Arc<SpecFlag>,
3168 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3169 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3170 pending_flags: &mut Vec<Arc<SpecFlag>>,
3171 overridden_flags: &mut HashSet<String>,
3172 attributed: &mut BTreeMap<String, String>,
3176) {
3177 let overridden_names: HashSet<String> = available_flags
3178 .values()
3179 .chain(parsed_flags.keys())
3180 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3181 .map(|other| other.name.clone())
3182 .collect();
3183
3184 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3185 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3186 for name in &overridden_names {
3187 attributed.insert(name.clone(), flag.name.clone());
3188 }
3189 overridden_flags.extend(overridden_names);
3190 overridden_flags.remove(&flag.name);
3192 attributed.remove(&flag.name);
3193}
3194
3195#[cfg(feature = "docs")]
3196fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3197 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3198}
3199
3200#[cfg(feature = "docs")]
3201fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3202 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3203 if !out.is_empty() {
3204 out.push('\n');
3205 }
3206 out.push_str(&docs::cli::render_help(spec, cmd, true));
3207 let mut children: Vec<_> = cmd
3208 .subcommands
3209 .values()
3210 .filter(|child| !child.hide)
3211 .collect();
3212 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3213 for child in children {
3214 append(out, spec, child);
3215 }
3216 }
3217
3218 let mut out = String::new();
3219 append(&mut out, spec, cmd);
3220 UsageErr::Help(out)
3221}
3222
3223#[cfg(not(feature = "docs"))]
3224fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3225 UsageErr::Help("help".to_string())
3226}
3227
3228#[cfg(not(feature = "docs"))]
3229fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3230 UsageErr::Help("help".to_string())
3231}
3232
3233fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3236 let value = if long {
3237 spec.long_version.as_ref().or(spec.version.as_ref())
3238 } else {
3239 spec.version.as_ref().or(spec.long_version.as_ref())
3240 };
3241 UsageErr::Version(value.cloned().unwrap_or_default())
3242}
3243
3244fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3245 use crate::SpecFlagAction;
3246 match flag.action {
3247 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3248 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3249 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3250 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3251 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3252 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3253 }
3254}
3255
3256#[derive(Copy, Clone)]
3257struct ChoiceTarget<'a> {
3258 kind: &'a str,
3259 name: &'a str,
3260}
3261
3262impl<'a> ChoiceTarget<'a> {
3263 fn arg(arg: &'a SpecArg) -> Self {
3264 Self {
3265 kind: "arg",
3266 name: &arg.name,
3267 }
3268 }
3269
3270 fn option(flag: &'a SpecFlag) -> Self {
3271 Self {
3272 kind: "option",
3273 name: &flag.name,
3274 }
3275 }
3276}
3277
3278fn short_bundle_is_known(
3283 spec: &Spec,
3284 cmds: &[SpecCommand],
3285 available: &BTreeMap<String, Arc<SpecFlag>>,
3286 token: &str,
3287) -> bool {
3288 for c in token.chars().skip(1) {
3289 match available.get(&format!("-{c}")) {
3290 None if supplied_short(spec, cmds, c).is_some() => {}
3295 None => return false,
3296 Some(f) if f.arg.is_some() => return true,
3297 Some(_) => {}
3298 }
3299 }
3300 true
3301}
3302
3303fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3313 let cmd = cmds.last()?;
3314 match letter {
3315 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3316 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3317 _ => None,
3318 }
3319}
3320
3321fn reject_unknown_flag_if_asked(
3327 spec: &Spec,
3328 path: &[SpecCommand],
3329 token: &str,
3330) -> Result<(), UsageErr> {
3331 if !is_flag_like(token) {
3334 return Ok(());
3335 }
3336 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3337 return Ok(());
3338 }
3339 Err(UsageErr::InvalidFlag {
3340 token: token.to_string(),
3341 reason: "no such flag".to_string(),
3342 span: (0, 0).into(),
3343 input: token.to_string(),
3344 })
3345}
3346
3347fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3353 path.iter()
3354 .rev()
3355 .find_map(|cmd| cmd.unknown_flags)
3356 .or(spec.unknown_flags)
3357 .unwrap_or_default()
3358}
3359
3360fn is_flag_like(token: &str) -> bool {
3366 match token.strip_prefix('-') {
3367 None | Some("") => false,
3368 Some(_) => true,
3369 }
3370}
3371
3372fn is_negative_number(token: &str) -> bool {
3373 token.strip_prefix('-').is_some_and(is_number)
3374}
3375
3376fn record_scalar_flag_occurrence(
3377 cmds: &[SpecCommand],
3378 flag: &Arc<SpecFlag>,
3379 command_level: usize,
3380 bool_value: Option<bool>,
3381 occurrences: &mut HashMap<(usize, usize), u8>,
3382 errors: &mut Vec<UsageErr>,
3383) {
3384 let strict = cmds
3385 .get(command_level)
3386 .is_some_and(|cmd| !cmd.args_override_self);
3387 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3388 if !strict || flag.count || collects_values {
3389 return;
3390 }
3391
3392 let bit = match bool_value {
3393 Some(false) if flag.negate.is_some() => 0b10,
3394 _ => 0b01,
3395 };
3396 let key = (Arc::as_ptr(flag) as usize, command_level);
3397 let seen = occurrences.entry(key).or_default();
3398 if *seen & bit != 0 {
3399 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3400 }
3401 *seen |= bit;
3402}
3403
3404fn is_command_word(token: &str) -> bool {
3412 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3413}
3414
3415fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3421 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3422}
3423
3424fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3430 !is_negative_number(token)
3431 || cmd
3432 .find_subcommand(default_name)
3433 .and_then(|default| default.args.first())
3434 .is_some_and(|arg| arg.allow_negative_numbers)
3435}
3436
3437fn is_number(rest: &str) -> bool {
3444 let (mantissa, exponent) = match rest.find(['e', 'E']) {
3445 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3446 None => (rest, None),
3447 };
3448
3449 let mut seen_digit = false;
3450 let mut seen_dot = false;
3451 for c in mantissa.chars() {
3452 match c {
3453 '0'..='9' => seen_digit = true,
3454 '.' if !seen_dot => seen_dot = true,
3455 _ => return false,
3456 }
3457 }
3458 if !seen_digit {
3459 return false;
3460 }
3461
3462 match exponent {
3463 None => true,
3464 Some(exp) => {
3465 let digits = exp
3466 .strip_prefix('+')
3467 .or_else(|| exp.strip_prefix('-'))
3468 .unwrap_or(exp);
3469 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3470 }
3471 }
3472}
3473
3474#[allow(clippy::too_many_arguments)]
3481fn bind_pending_flag_value(
3482 spec: &Spec,
3483 cmd: &SpecCommand,
3484 errors: &mut Vec<UsageErr>,
3485 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3486 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3487 word: &mut String,
3488 input: &mut VecDeque<Token>,
3489 custom_env: Option<&HashMap<String, String>>,
3490 trace: &mut Trace,
3491 argv: usize,
3495 attached: bool,
3496) -> miette::Result<bool> {
3497 let collecting = flag_awaiting_value
3501 .last()
3502 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3503 .cloned()
3504 .map(|flag| {
3505 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3506 (flag, carried)
3507 });
3508 let mut bound = vec![];
3509 let refused = drain_pending_flag_values(
3510 spec,
3511 cmd,
3512 errors,
3513 flags,
3514 flag_awaiting_value,
3515 word,
3516 custom_env,
3517 &mut bound,
3518 )?;
3519 for (flag, values) in bound {
3520 trace.record(
3521 argv,
3522 TokenRole::Value {
3523 flag,
3524 values,
3525 attached,
3526 },
3527 );
3528 }
3529 if refused {
3530 return Ok(true);
3531 }
3532 let Some((flag, carried)) = collecting else {
3533 return Ok(false);
3534 };
3535 collect_variadic_flag_values(
3536 spec,
3537 cmd,
3538 errors,
3539 flags,
3540 flag_awaiting_value,
3541 &flag,
3542 carried,
3543 input,
3544 custom_env,
3545 trace,
3546 )
3547}
3548
3549#[allow(clippy::too_many_arguments)]
3562fn collect_variadic_flag_values(
3563 spec: &Spec,
3564 cmd: &SpecCommand,
3565 errors: &mut Vec<UsageErr>,
3566 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3567 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3568 flag: &Arc<SpecFlag>,
3569 carried: usize,
3570 input: &mut VecDeque<Token>,
3571 custom_env: Option<&HashMap<String, String>>,
3572 trace: &mut Trace,
3573) -> miette::Result<bool> {
3574 let max = flag
3575 .arg
3576 .as_ref()
3577 .and_then(|arg| arg.var_max)
3578 .unwrap_or(usize::MAX);
3579 while flags
3580 .get(flag)
3581 .map(value_count)
3582 .unwrap_or(0)
3583 .saturating_sub(carried)
3584 < max
3585 {
3586 let Some(next) = input.front().map(|token| token.word.as_str()) else {
3587 break;
3588 };
3589 if flag
3590 .arg
3591 .as_ref()
3592 .and_then(|arg| arg.value_terminator.as_deref())
3593 == Some(next)
3594 {
3595 let terminator = input.pop_front().unwrap();
3596 trace.record(
3597 terminator.argv,
3598 TokenRole::ValueTerminator {
3599 ends: flag.name.clone(),
3600 },
3601 );
3602 break;
3603 }
3604 if next == "--"
3607 || (is_flag_like(next)
3608 && !(flag
3609 .arg
3610 .as_ref()
3611 .is_some_and(|arg| arg.allow_negative_numbers)
3612 && is_negative_number(next)))
3613 {
3614 break;
3615 }
3616 let taken = input.pop_front().unwrap();
3617 let argv = taken.argv;
3618 let mut word = taken.word;
3619 flag_awaiting_value.push(Arc::clone(flag));
3620 let mut bound = vec![];
3621 let refused = drain_pending_flag_values(
3622 spec,
3623 cmd,
3624 errors,
3625 flags,
3626 flag_awaiting_value,
3627 &mut word,
3628 custom_env,
3629 &mut bound,
3630 )?;
3631 for (flag, values) in bound {
3632 trace.record(
3635 argv,
3636 TokenRole::Value {
3637 flag,
3638 values,
3639 attached: false,
3640 },
3641 );
3642 }
3643 if refused {
3644 return Ok(true);
3645 }
3646 }
3647 let taken = flags
3656 .get(flag)
3657 .map(value_count)
3658 .unwrap_or(0)
3659 .saturating_sub(carried);
3660 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
3661 if taken < min {
3662 errors.push(UsageErr::VarFlagTooFew {
3663 name: flag.name.clone(),
3664 min,
3665 got: taken,
3666 });
3667 }
3668 }
3669 if taken > max {
3670 errors.push(UsageErr::VarFlagTooMany {
3671 name: flag.name.clone(),
3672 max,
3673 got: taken,
3674 });
3675 }
3676 Ok(false)
3677}
3678
3679fn value_count(value: &ParseValue) -> usize {
3681 match value {
3682 ParseValue::MultiString(values) => values.len(),
3683 ParseValue::MultiBool(values) => values.len(),
3684 _ => 1,
3685 }
3686}
3687
3688fn try_bind_default_missing(
3699 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3700 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3701 custom_env: Option<&HashMap<String, String>>,
3702 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
3703) -> miette::Result<bool> {
3704 let Some(flag) = flag_awaiting_value.last() else {
3705 return Ok(false);
3706 };
3707 let value = match flag.default_missing.clone() {
3708 Some(value) => value,
3709 None if flag.value_optional => {
3710 let flag = flag_awaiting_value.pop().unwrap();
3711 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
3715 origins
3716 .entry(Arc::clone(&flag))
3717 .or_default()
3718 .push(ValueOrigin::DefaultMissing);
3719 if flag.var {
3720 flags
3725 .entry(flag)
3726 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
3727 .try_as_multi_string_mut()
3728 .unwrap()
3729 .push(String::new());
3730 } else if variadic_value {
3731 flags
3734 .entry(flag)
3735 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
3736 } else {
3737 flags.insert(flag, ParseValue::MultiString(Vec::new()));
3741 }
3742 return Ok(true);
3743 }
3744 None => return Ok(false),
3745 };
3746 if let Some(arg) = flag.arg.as_ref() {
3747 validate_choice_value(
3748 ChoiceTarget::option(flag),
3749 &value,
3750 arg.choices.as_ref(),
3751 custom_env,
3752 )?;
3753 }
3754 let flag = flag_awaiting_value.pop().unwrap();
3755 origins
3756 .entry(Arc::clone(&flag))
3757 .or_default()
3758 .push(ValueOrigin::DefaultMissing);
3759 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3760 if collecting {
3761 let arr = flags
3762 .entry(flag)
3763 .or_insert_with(|| ParseValue::MultiString(vec![]))
3764 .try_as_multi_string_mut()
3765 .unwrap();
3766 arr.push(value);
3767 } else {
3768 flags.insert(flag, ParseValue::String(value));
3769 }
3770 Ok(true)
3771}
3772
3773#[allow(clippy::too_many_arguments)]
3778fn drain_pending_flag_values(
3779 spec: &Spec,
3780 cmd: &SpecCommand,
3781 errors: &mut Vec<UsageErr>,
3782 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3783 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3784 word: &mut String,
3785 custom_env: Option<&HashMap<String, String>>,
3786 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3787) -> miette::Result<bool> {
3788 while let Some(flag) = flag_awaiting_value.pop() {
3789 let arg = flag.arg.as_ref().unwrap();
3790 let parts: Vec<String> = match arg.delimiter {
3794 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
3795 None => vec![std::mem::take(word)],
3796 };
3797 for part in &parts {
3798 if validate_choices(
3799 spec,
3800 cmd,
3801 errors,
3802 ChoiceTarget::option(&flag),
3803 part,
3804 arg.choices.as_ref(),
3805 custom_env,
3806 )? {
3807 return Ok(true);
3808 }
3809 }
3810 word.clear();
3811 bound.push((Arc::clone(&flag), parts.clone()));
3812 if flag.var || arg.var {
3815 let arr = flags
3816 .entry(flag)
3817 .or_insert_with(|| ParseValue::MultiString(vec![]))
3818 .try_as_multi_string_mut()
3819 .unwrap();
3820 arr.extend(parts);
3821 } else {
3822 flags.insert(
3825 flag,
3826 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
3827 );
3828 }
3829 }
3830 Ok(false)
3831}
3832
3833fn choice_error(
3834 target: ChoiceTarget<'_>,
3835 value: &str,
3836 choices: Option<&SpecChoices>,
3837 custom_env: Option<&HashMap<String, String>>,
3838) -> Option<String> {
3839 let choices = choices?;
3840 if !choices.strict {
3841 return None;
3842 }
3843 let values = choices.values_with_env(custom_env);
3844 if choices.matches_with_env(value, custom_env) {
3845 return None;
3846 }
3847 if let Some(env) = choices.env() {
3848 if values.is_empty() {
3849 return Some(format!(
3850 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
3851 target.kind, target.name,
3852 ));
3853 }
3854 }
3855 Some(format!(
3856 "Invalid choice for {} {}: {value}, expected one of {}",
3857 target.kind,
3858 target.name,
3859 values.join(", ")
3860 ))
3861}
3862
3863fn validate_choices(
3864 spec: &Spec,
3865 cmd: &SpecCommand,
3866 errors: &mut Vec<UsageErr>,
3867 target: ChoiceTarget<'_>,
3868 value: &str,
3869 choices: Option<&SpecChoices>,
3870 custom_env: Option<&HashMap<String, String>>,
3871) -> miette::Result<bool> {
3872 if is_help_arg(spec, cmd, value)
3873 && choices
3874 .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
3875 {
3876 errors.push(render_help_err(spec, cmd, value.len() > 2));
3877 return Ok(true);
3878 }
3879
3880 if let Some(err) = choice_error(target, value, choices, custom_env) {
3881 bail!("{err}");
3882 }
3883 Ok(false)
3884}
3885
3886fn validate_choice_value(
3887 target: ChoiceTarget<'_>,
3888 value: &str,
3889 choices: Option<&SpecChoices>,
3890 custom_env: Option<&HashMap<String, String>>,
3891) -> miette::Result<()> {
3892 if let Some(err) = choice_error(target, value, choices, custom_env) {
3893 bail!("{err}");
3894 }
3895 Ok(())
3896}
3897
3898fn validate_choice_values(
3899 target: ChoiceTarget<'_>,
3900 values: &[String],
3901 choices: Option<&SpecChoices>,
3902 custom_env: Option<&HashMap<String, String>>,
3903) -> miette::Result<()> {
3904 for value in values {
3905 validate_choice_value(target, value, choices, custom_env)?;
3906 }
3907 Ok(())
3908}
3909
3910fn record_stop(
3917 out: &mut ParseOutput,
3918 next_arg_idx: usize,
3919 seen_double_dash: bool,
3920 trace: &mut Trace,
3921 unread: &VecDeque<Token>,
3922) {
3923 out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
3924 out.double_dash_seen = seen_double_dash;
3925 trace.close(unread);
3926 out.tokens = std::mem::take(&mut trace.tokens);
3927}
3928
3929fn report_double_dash_violation(
3936 arg: &SpecArg,
3937 errors: &mut Vec<UsageErr>,
3938 violations: &mut HashSet<String>,
3939) {
3940 if violations.insert(arg.name.clone()) {
3941 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
3942 }
3943}
3944
3945fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
3960 (spec.version.is_some() || spec.long_version.is_some())
3961 && cmds.len() == 1
3962 && !spec.cmd.disable_version_flag
3963 && (w == "--version" || w == "-V")
3964}
3965
3966fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
3967 spec.disable_help != Some(true)
3968 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
3969 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
3970}
3971
3972impl ParseOutput {
3973 pub fn as_env(&self) -> BTreeMap<String, String> {
3974 let mut env = BTreeMap::new();
3975 for (flag, val) in &self.flags {
3976 let key = format!("usage_{}", flag.name.to_snake_case());
3977 let val = match val {
3978 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
3979 ParseValue::String(s) => s.clone(),
3980 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
3981 ParseValue::MultiString(s) => shell_words::join(s),
3982 };
3983 env.insert(key, val);
3984 }
3985 for (arg, val) in &self.args {
3986 let key = format!("usage_{}", arg.name.to_snake_case());
3987 env.insert(key, val.to_string());
3988 }
3989 env
3990 }
3991}
3992
3993impl Display for ParseValue {
3994 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3995 match self {
3996 ParseValue::Bool(b) => write!(f, "{b}"),
3997 ParseValue::String(s) => write!(f, "{s}"),
3998 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
3999 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
4000 }
4001 }
4002}
4003
4004fn render_token(token: &TokenBinding) -> String {
4006 let roles = token.roles.iter().map(render_role).join(", ");
4007 let synthesized = if token.synthesized { " (read as)" } else { "" };
4008 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4009}
4010
4011fn render_role(role: &TokenRole) -> String {
4012 match role {
4013 TokenRole::Program => "program".to_string(),
4014 TokenRole::Command { name } => format!("subcommand {name}"),
4015 TokenRole::Flag {
4016 flag,
4017 spelling,
4018 negated,
4019 } => {
4020 let negated = if *negated { ", negated" } else { "" };
4021 format!("flag {} as {spelling}{negated}", flag.name)
4022 }
4023 TokenRole::Value {
4024 flag,
4025 values,
4026 attached,
4027 } => {
4028 let attached = if *attached { ", attached" } else { "" };
4029 format!("value of {} = {values:?}{attached}", flag.name)
4030 }
4031 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4032 TokenRole::Separator => "separator".to_string(),
4033 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4034 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4035 TokenRole::Restart => "restart".to_string(),
4036 TokenRole::UnknownFlag { bound_as } => match bound_as {
4037 Some(arg) => format!("unknown flag, bound as {}", arg.name),
4038 None => "unknown flag".to_string(),
4039 },
4040 TokenRole::Refused { reason } => format!("refused: {reason}"),
4041 TokenRole::External => "external".to_string(),
4042 TokenRole::Unread => "unread".to_string(),
4043 }
4044}
4045
4046impl Debug for ParseOutput {
4047 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4048 f.debug_struct("ParseOutput")
4049 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4050 .field(
4051 "args",
4052 &self
4053 .args
4054 .iter()
4055 .map(|(a, w)| format!("{}: {w}", a.name))
4056 .collect_vec(),
4057 )
4058 .field(
4059 "available_flags",
4060 &self
4061 .available_flags
4062 .iter()
4063 .map(|(f, w)| format!("{f}: {w}"))
4064 .collect_vec(),
4065 )
4066 .field(
4067 "flags",
4068 &self
4069 .flags
4070 .iter()
4071 .map(|(f, w)| format!("{}: {w}", f.name))
4072 .collect_vec(),
4073 )
4074 .field("flag_awaiting_value", &self.flag_awaiting_value)
4075 .field("errors", &self.errors)
4076 .field("external", &self.external)
4077 .field(
4081 "tokens",
4082 &self.tokens.iter().map(render_token).collect_vec(),
4083 )
4084 .field(
4085 "origins",
4086 &self
4087 .flag_origins
4088 .iter()
4089 .map(|(f, o)| format!("{}: {o:?}", f.name))
4090 .chain(
4091 self.arg_origins
4092 .iter()
4093 .map(|(a, o)| format!("{}: {o:?}", a.name)),
4094 )
4095 .collect_vec(),
4096 )
4097 .field("overridden_flags", &self.overridden_flags)
4098 .finish()
4099 }
4100}
4101
4102#[cfg(test)]
4103mod tests {
4104 use super::*;
4105 use crate::SpecFlagAction;
4106
4107 fn input(words: &[&str]) -> Vec<String> {
4108 words.iter().map(|word| (*word).to_string()).collect()
4109 }
4110
4111 #[test]
4112 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4113 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4118 .parse()
4119 .unwrap();
4120
4121 for spelling in ["--version", "-V"] {
4122 let err = parse(&spec, &input(&["ex", spelling]))
4123 .expect_err("answering with a version ends the parse");
4124 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4125 }
4126 }
4127
4128 #[test]
4129 fn the_supplied_version_flag_is_the_roots_alone() {
4130 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4133 .parse()
4134 .unwrap();
4135
4136 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4137 assert_eq!(err.to_string(), "unexpected word: --version");
4138 }
4139
4140 #[test]
4141 fn no_declared_version_supplies_nothing() {
4142 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4145
4146 for spelling in ["--version", "-V"] {
4147 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4148 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4149 }
4150 }
4151
4152 #[test]
4153 fn disable_version_flag_removes_the_supplied_spellings() {
4154 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4155 .parse()
4156 .unwrap();
4157
4158 for spelling in ["--version", "-V"] {
4159 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4160 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4161 }
4162 }
4163
4164 #[test]
4165 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4166 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4169 .parse()
4170 .unwrap();
4171
4172 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4173 assert_eq!(out.flags.len(), 1);
4174
4175 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4176 assert_eq!(err.to_string(), "1.2.3");
4177 }
4178
4179 #[test]
4180 fn the_supplied_spellings_split_the_two_version_texts() {
4181 let spec: Spec =
4184 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4185 .parse()
4186 .unwrap();
4187
4188 assert_eq!(
4189 parse(&spec, &input(&["ex", "--version"]))
4190 .unwrap_err()
4191 .to_string(),
4192 "1.2.3 (abcdef)"
4193 );
4194 assert_eq!(
4195 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4196 "1.2.3"
4197 );
4198 }
4199
4200 #[test]
4201 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4202 let spec: Spec =
4207 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4208 .parse()
4209 .unwrap();
4210
4211 for token in ["-vh", "-hv"] {
4212 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4213 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4214 }
4215 for token in ["-vV", "-Vv"] {
4216 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4217 assert_eq!(err.to_string(), "1.2.3", "{token}");
4218 }
4219 }
4220
4221 #[test]
4222 fn a_bundled_help_letter_asks_for_the_short_page() {
4223 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-v --verbose\" help=\"Be loud\" {\n long_help \"Be loud, and say so at length.\"\n}\n"
4226 .parse()
4227 .unwrap();
4228
4229 let short = parse(&spec, &input(&["ex", "-vh"]))
4230 .unwrap_err()
4231 .to_string();
4232 let long = parse(&spec, &input(&["ex", "--help"]))
4233 .unwrap_err()
4234 .to_string();
4235 assert!(short.contains("Be loud"), "{short}");
4236 assert!(!short.contains("at length"), "{short}");
4237 assert!(long.contains("at length"), "{long}");
4238 }
4239
4240 #[test]
4241 fn the_bundled_version_letter_is_the_roots_alone() {
4242 let spec: Spec =
4244 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4245 .parse()
4246 .unwrap();
4247
4248 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4249 assert_eq!(err.to_string(), "unexpected word: -vV");
4250 }
4251
4252 #[test]
4253 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4254 let spec: Spec =
4257 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4258 .parse()
4259 .unwrap();
4260
4261 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4262 assert_eq!(out.flags.len(), 2);
4263 assert!(out
4264 .flags
4265 .iter()
4266 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4267 }
4268
4269 #[test]
4270 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4271 let spec: Spec =
4272 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4273 .parse()
4274 .unwrap();
4275
4276 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4277 assert_eq!(err.to_string(), "unexpected word: -vh");
4278 }
4279
4280 #[test]
4281 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4282 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4285 .parse()
4286 .unwrap();
4287
4288 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4289 assert!(out.flags.is_empty(), "{:?}", out.flags);
4290 assert_eq!(out.args.len(), 1);
4291 }
4292
4293 fn spec_with_arg(arg: SpecArg) -> Spec {
4294 let cmd = SpecCommand::builder().name("test").arg(arg).build();
4295 Spec {
4296 name: "test".to_string(),
4297 bin: "test".to_string(),
4298 cmd,
4299 ..Default::default()
4300 }
4301 }
4302
4303 fn spec_with_flag(flag: SpecFlag) -> Spec {
4304 let cmd = SpecCommand::builder().name("test").flag(flag).build();
4305 Spec {
4306 name: "test".to_string(),
4307 bin: "test".to_string(),
4308 cmd,
4309 ..Default::default()
4310 }
4311 }
4312
4313 fn parse_with_env(
4314 spec: &Spec,
4315 words: &[&str],
4316 env: &[(&str, &str)],
4317 ) -> Result<ParseOutput, miette::Error> {
4318 let env = env
4319 .iter()
4320 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4321 .collect();
4322 Parser::new(spec).with_env(env).parse(&input(words))
4323 }
4324
4325 fn first_string_value(parsed: &ParseOutput) -> &str {
4326 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4327 return value;
4328 }
4329 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4330 return value;
4331 }
4332 panic!("expected first parsed value to be ParseValue::String");
4333 }
4334
4335 #[test]
4336 fn custom_environment_parser_dispatches_executable_views() {
4337 let spec: Spec = r#"
4338bin "ex"
4339view "runner" root="run"
4340cmd "run" {
4341 flag "--token <token>" env="TOKEN"
4342}
4343 "#
4344 .parse()
4345 .unwrap();
4346 let parsed = Parser::new(&spec)
4347 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4348 .parse(&input(&["runner"]))
4349 .unwrap();
4350
4351 assert_eq!(parsed.cmd.name, "runner");
4352 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4353 && matches!(value, ParseValue::String(value) if value == "secret")));
4354 }
4355
4356 #[test]
4357 fn an_executable_view_keeps_the_hosts_version_action() {
4358 let spec: Spec = r#"
4359bin "ex"
4360version "1.2.3"
4361flag "-V --version" action="version"
4362flag "--verbose" global=#true
4363view "runner" root="run" globals=#true
4364cmd "run"
4365 "#
4366 .parse()
4367 .unwrap();
4368
4369 let error = Parser::new(&spec)
4370 .parse(&input(&["runner", "--version"]))
4371 .expect_err("the host version action should answer before view projection");
4372 assert_eq!(error.to_string(), "1.2.3");
4373
4374 let error = Parser::new(&spec)
4375 .parse(&input(&["runner", "--verbose", "--version"]))
4376 .expect_err("the host version action should remain after a carried global");
4377 assert_eq!(error.to_string(), "1.2.3");
4378 }
4379
4380 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4381 let flag = parsed
4382 .flags
4383 .keys()
4384 .find(|flag| flag.name == name)
4385 .unwrap_or_else(|| panic!("expected flag {name}"));
4386 let value = parsed
4387 .flags
4388 .get(flag)
4389 .unwrap_or_else(|| panic!("expected value for flag {name}"));
4390 match value {
4391 ParseValue::String(value) => value,
4392 _ => panic!("expected flag {name} to be ParseValue::String"),
4393 }
4394 }
4395
4396 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4397 let err = result.expect_err("expected parser error");
4398 assert_eq!(format!("{err}"), expected);
4399 }
4400
4401 #[test]
4402 fn a_short_version_action_falls_back_to_the_long_version() {
4403 let flag = SpecFlag::builder()
4404 .short('R')
4405 .action(SpecFlagAction::Version)
4406 .build();
4407 let spec = Spec {
4408 name: "test".to_string(),
4409 bin: "test".to_string(),
4410 long_version: Some("1.2.3\ncommit abc123".to_string()),
4411 ..Default::default()
4412 };
4413 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4414 panic!("expected version action")
4415 };
4416 assert_eq!(version, "1.2.3\ncommit abc123");
4417 }
4418
4419 #[cfg(feature = "unstable_choices_env")]
4420 fn spec_arg_choices_env(key: &str) -> Spec {
4421 spec_with_arg(
4422 SpecArg::builder()
4423 .name("env")
4424 .choices_env(key)
4425 .required(false)
4426 .build(),
4427 )
4428 }
4429
4430 #[cfg(feature = "unstable_choices_env")]
4431 fn spec_flag_choices_env(key: &str) -> Spec {
4432 spec_with_flag(
4433 SpecFlag::builder()
4434 .long("env")
4435 .arg(SpecArg::builder().name("env").choices_env(key).build())
4436 .build(),
4437 )
4438 }
4439
4440 #[test]
4441 fn test_parse() {
4442 let cmd = SpecCommand::builder()
4443 .name("test")
4444 .arg(SpecArg::builder().name("arg").build())
4445 .flag(SpecFlag::builder().long("flag").build())
4446 .build();
4447 let spec = Spec {
4448 name: "test".to_string(),
4449 bin: "test".to_string(),
4450 cmd,
4451 ..Default::default()
4452 };
4453 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4454 let parsed = parse(&spec, &input).unwrap();
4455 assert_eq!(parsed.cmds.len(), 1);
4456 assert_eq!(parsed.cmds[0].name, "test");
4457 assert_eq!(parsed.args.len(), 1);
4458 assert_eq!(parsed.flags.len(), 1);
4459 assert_eq!(parsed.available_flags.len(), 1);
4460 }
4461
4462 #[test]
4463 fn test_flag_overrides_last_occurrence_wins() {
4464 let spec: Spec = r#"
4465flag "--stdin" default=#true
4466flag "--file <file>" overrides="--stdin"
4467 "#
4468 .parse()
4469 .unwrap();
4470
4471 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4472 assert_eq!(file_wins.flags.len(), 1);
4473 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4474 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4475
4476 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
4477 assert_eq!(stdin_wins.flags.len(), 1);
4478 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
4479 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
4480 }
4481
4482 #[test]
4483 fn test_flag_override_clears_pending_value() {
4484 let spec: Spec = r#"
4485flag "--file <file>" overrides="--stdin"
4486flag "--stdin"
4487arg "[input]"
4488 "#
4489 .parse()
4490 .unwrap();
4491
4492 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
4493 assert_eq!(parsed.flags.len(), 1);
4494 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
4495 assert_eq!(first_string_value(&parsed), "input.txt");
4496 }
4497
4498 #[cfg(unix)]
4499 #[test]
4500 fn a_mount_on_the_root_discovers_subcommands() {
4501 let spec: Spec = r#"
4505name "ex"
4506bin "ex"
4507cmd "declared"
4508mount run="echo 'cmd \"discovered\"'"
4509"#
4510 .parse()
4511 .unwrap();
4512
4513 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4514 assert_eq!(out.cmd.name, "discovered");
4515 }
4516
4517 #[test]
4518 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
4519 let spec: Spec = r#"
4520name "ex"
4521bin "ex"
4522mount run="this command must never run"
4523cmd "declared"
4524"#
4525 .parse()
4526 .unwrap();
4527
4528 Parser::new(&spec)
4529 .with_mount_outputs(HashMap::new())
4530 .parse(&input(&["ex", "declared"]))
4531 .expect("a declared command does not resolve the mount");
4532
4533 let error = Parser::new(&spec)
4534 .with_mount_outputs(HashMap::new())
4535 .parse(&input(&["ex", "discovered"]))
4536 .unwrap_err();
4537 assert!(
4538 error
4539 .to_string()
4540 .contains("No injected output was provided for mount command"),
4541 "{error}"
4542 );
4543 }
4544
4545 #[cfg(unix)]
4546 #[test]
4547 fn completion_sees_root_mounted_commands_with_nothing_typed() {
4548 let spec: Spec = r#"
4552name "ex"
4553bin "ex"
4554cmd "declared"
4555mount run="echo 'cmd \"discovered\"'"
4556"#
4557 .parse()
4558 .unwrap();
4559
4560 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4561 assert!(
4562 out.cmd.subcommands.contains_key("discovered"),
4563 "a completion should see mounted commands; got {:?}",
4564 out.cmd.subcommands.keys().collect::<Vec<_>>()
4565 );
4566 }
4567
4568 #[cfg(unix)]
4569 #[test]
4570 fn completion_and_execution_agree_about_discovery() {
4571 let spec: Spec = r#"
4575name "ex"
4576bin "ex"
4577default_subcommand "run"
4578cmd "run" {
4579 arg "<task>"
4580}
4581mount run="exit 1"
4582"#
4583 .parse()
4584 .unwrap();
4585
4586 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4587 assert!(
4588 !out.cmd.subcommands.contains_key("discovered"),
4589 "a completion must not offer what execution will not route"
4590 );
4591
4592 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4593 assert_eq!(out.cmd.name, "run");
4594 }
4595
4596 #[cfg(unix)]
4597 #[test]
4598 fn a_default_subcommand_outranks_discovery() {
4599 let spec: Spec = r#"
4602name "ex"
4603bin "ex"
4604default_subcommand "run"
4605cmd "run" {
4606 arg "<task>"
4607}
4608mount run="exit 1"
4609"#
4610 .parse()
4611 .unwrap();
4612
4613 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4614 assert_eq!(out.cmd.name, "run");
4615 }
4616
4617 #[cfg(unix)]
4618 #[test]
4619 fn a_mount_may_ask_to_outrank_the_default() {
4620 let spec: Spec = r#"
4623name "ex"
4624bin "ex"
4625default_subcommand "run"
4626cmd "run" {
4627 arg "<task>"
4628}
4629mount run="echo 'cmd \"discovered\"'" overrides_default=#true
4630"#
4631 .parse()
4632 .unwrap();
4633
4634 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4635 assert_eq!(out.cmd.name, "discovered");
4636
4637 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4639 assert_eq!(out.cmd.name, "run");
4640 }
4641
4642 #[cfg(unix)]
4643 #[test]
4644 fn a_flag_does_not_run_the_mount() {
4645 let spec: Spec = r#"
4649name "ex"
4650bin "ex"
4651flag "--verbose"
4652cmd "declared"
4653mount run="exit 1"
4654"#
4655 .parse()
4656 .unwrap();
4657
4658 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
4659 assert_eq!(out.cmd.name, "ex");
4660 }
4661
4662 #[cfg(unix)]
4663 #[test]
4664 fn a_declared_subcommand_does_not_run_the_mount() {
4665 let spec: Spec = r#"
4669name "ex"
4670bin "ex"
4671cmd "declared"
4672mount run="exit 1"
4673"#
4674 .parse()
4675 .unwrap();
4676
4677 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
4678 assert_eq!(out.cmd.name, "declared");
4679 }
4680
4681 #[test]
4682 fn a_root_mount_survives_being_written_out() {
4683 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
4684 .parse()
4685 .unwrap();
4686 assert_eq!(spec.cmd.mounts.len(), 1);
4687
4688 let reparsed: Spec = spec.to_string().parse().unwrap();
4689 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
4690 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
4691 }
4692
4693 #[test]
4694 fn test_mount_prefix_applies_flag_overrides() {
4695 let stdin = Arc::new(
4696 SpecFlag::builder()
4697 .name("stdin")
4698 .long("stdin")
4699 .global(true)
4700 .build(),
4701 );
4702 let file = Arc::new(
4703 SpecFlag::builder()
4704 .name("file")
4705 .long("file")
4706 .arg(SpecArg::builder().name("file").build())
4707 .global(true)
4708 .overrides_with(vec!["--stdin".to_string()])
4709 .build(),
4710 );
4711 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
4712
4713 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
4714 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
4715
4716 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
4717 }
4718
4719 #[test]
4720 fn test_flag_override_suppresses_env_value() {
4721 let spec: Spec = r#"
4722flag "--stdin" env="USE_STDIN"
4723flag "--file <file>" overrides="--stdin"
4724 "#
4725 .parse()
4726 .unwrap();
4727
4728 let parsed = parse_with_env(
4729 &spec,
4730 &["test", "--file", "input.txt"],
4731 &[("USE_STDIN", "true")],
4732 )
4733 .unwrap();
4734 assert_eq!(parsed.flags.len(), 1);
4735 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4736 }
4737
4738 #[test]
4739 fn test_flag_override_suppresses_required_check() {
4740 let spec: Spec = r#"
4741flag "--stdin" required=#true
4742flag "--file <file>" overrides="--stdin"
4743 "#
4744 .parse()
4745 .unwrap();
4746
4747 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4748 assert_eq!(parsed.flags.len(), 1);
4749 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4750 }
4751
4752 #[test]
4753 fn test_flag_required_if() {
4754 let spec: Spec = r#"
4755flag "--dir <dir>"
4756flag "--file <file>" required_if="--dir"
4757 "#
4758 .parse()
4759 .unwrap();
4760
4761 parse(&spec, &input(&["test"])).unwrap();
4762 assert_parse_err(
4763 parse(&spec, &input(&["test", "--dir", "src"])),
4764 "Missing required flag: --file <file>",
4765 );
4766 parse(
4767 &spec,
4768 &input(&["test", "--dir", "src", "--file", "input.txt"]),
4769 )
4770 .unwrap();
4771 }
4772
4773 #[test]
4774 fn test_flag_required_unless() {
4775 let spec: Spec = r#"
4776flag "--stdin"
4777flag "--file <file>" required_unless="--stdin"
4778 "#
4779 .parse()
4780 .unwrap();
4781
4782 assert_parse_err(
4783 parse(&spec, &input(&["test"])),
4784 "Missing required flag: --file <file>",
4785 );
4786 parse(&spec, &input(&["test", "--stdin"])).unwrap();
4787 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4788 }
4789
4790 #[test]
4791 fn complete_required_relationship_truth_tables() {
4792 let spec: Spec = r#"
4793name "ex"
4794bin "ex"
4795flag "--mode <mode>"
4796flag "--scope <scope>"
4797flag "--token <token>" {
4798 required_if_eq "--mode" "remote"
4799}
4800flag "--approval <approval>" {
4801 required_if_eq_all "--mode" "remote" "--scope" "global"
4802}
4803flag "--input <input>" {
4804 required_unless "--stdin" "--file"
4805}
4806flag "--checksum <checksum>" {
4807 required_unless_all "--stdin" "--file"
4808}
4809flag "--stdin"
4810flag "--file <file>"
4811arg "[request]" {
4812 requires "--mode" "--scope"
4813}
4814"#
4815 .parse()
4816 .unwrap();
4817 let parse_args = |args: &[&str]| {
4818 parse(
4819 &spec,
4820 &args
4821 .iter()
4822 .map(|arg| (*arg).to_string())
4823 .collect::<Vec<_>>(),
4824 )
4825 };
4826
4827 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
4828 assert!(parse_args(&[
4829 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
4830 ])
4831 .is_err());
4832 parse_args(&[
4833 "ex",
4834 "--mode",
4835 "remote",
4836 "--token",
4837 "secret",
4838 "--scope",
4839 "global",
4840 "--approval",
4841 "yes",
4842 "--stdin",
4843 "--file",
4844 "in",
4845 ])
4846 .unwrap();
4847 parse_args(&[
4848 "ex",
4849 "--mode",
4850 "local",
4851 "--scope",
4852 "project",
4853 "--stdin",
4854 "--checksum",
4855 "sum",
4856 "request.json",
4857 ])
4858 .unwrap();
4859
4860 let reparsed: Spec = spec.to_string().parse().unwrap();
4861 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
4862 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
4863 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
4864 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
4865 }
4866
4867 #[test]
4868 fn test_conditional_requirements_treat_env_as_explicit() {
4869 let spec: Spec = r#"
4870flag "--dir <dir>" env="INPUT_DIR"
4871flag "--stdin" env="USE_STDIN"
4872flag "--file <file>" required_if="--dir" required_unless="--stdin"
4873 "#
4874 .parse()
4875 .unwrap();
4876
4877 assert_parse_err(
4878 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
4879 "Missing required flag: --file <file>",
4880 );
4881 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
4882 }
4883
4884 #[test]
4885 fn test_custom_env_does_not_fall_back_to_process_env() {
4886 assert!(std::env::var("PATH").is_ok());
4887 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
4888
4889 assert_parse_err(
4890 parse_with_env(&spec, &["test"], &[]),
4891 "Missing required flag: --file <file>",
4892 );
4893 }
4894
4895 #[test]
4896 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
4897 let spec: Spec = r#"
4898flag "--dir <dir>" default="src"
4899flag "--file <file>" required_if="--dir"
4900 "#
4901 .parse()
4902 .unwrap();
4903
4904 parse(&spec, &input(&["test"])).unwrap();
4905 }
4906
4907 #[test]
4908 fn test_conditional_requirements_see_overridden_flags_as_absent() {
4909 let spec: Spec = r#"
4910flag "--stdin"
4911flag "--dir <dir>" overrides="--stdin"
4912flag "--file <file>" required_unless="--stdin"
4913 "#
4914 .parse()
4915 .unwrap();
4916
4917 assert_parse_err(
4918 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
4919 "Missing required flag: --file <file>",
4920 );
4921 }
4922
4923 #[test]
4924 fn short_flag_is_one_character_not_one_byte() {
4925 let spec = spec_with_flag(
4929 SpecFlag::builder()
4930 .short('磨')
4931 .long("polish")
4932 .arg(SpecArg::builder().name("opt").build())
4933 .build(),
4934 );
4935 let attached = Parser::new(&spec)
4936 .parse(&input(&["test", "-磨VALUE"]))
4937 .unwrap();
4938 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
4939 let detached = Parser::new(&spec)
4940 .parse(&input(&["test", "-磨", "V"]))
4941 .unwrap();
4942 assert_eq!(flag_string_value(&detached, "polish"), "V");
4943 }
4944
4945 #[test]
4946 fn test_as_env() {
4947 let cmd = SpecCommand::builder()
4948 .name("test")
4949 .arg(SpecArg::builder().name("arg").build())
4950 .flag(SpecFlag::builder().long("flag").build())
4951 .flag(
4952 SpecFlag::builder()
4953 .long("force")
4954 .negate("--no-force")
4955 .build(),
4956 )
4957 .build();
4958 let spec = Spec {
4959 name: "test".to_string(),
4960 bin: "test".to_string(),
4961 cmd,
4962 ..Default::default()
4963 };
4964 let input = vec![
4965 "test".to_string(),
4966 "--flag".to_string(),
4967 "--no-force".to_string(),
4968 ];
4969 let parsed = parse(&spec, &input).unwrap();
4970 let env = parsed.as_env();
4971 assert_eq!(env.len(), 2);
4972 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
4973 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
4974 }
4975
4976 #[test]
4977 fn test_arg_env_var() {
4978 let cmd = SpecCommand::builder()
4979 .name("test")
4980 .arg(
4981 SpecArg::builder()
4982 .name("input")
4983 .env("TEST_ARG_INPUT")
4984 .required(true)
4985 .build(),
4986 )
4987 .build();
4988 let spec = Spec {
4989 name: "test".to_string(),
4990 bin: "test".to_string(),
4991 cmd,
4992 ..Default::default()
4993 };
4994
4995 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
4997
4998 let input = vec!["test".to_string()];
4999 let parsed = parse(&spec, &input).unwrap();
5000
5001 assert_eq!(parsed.args.len(), 1);
5002 let arg = parsed.args.keys().next().unwrap();
5003 assert_eq!(arg.name, "input");
5004 let value = parsed.args.values().next().unwrap();
5005 assert_eq!(value.to_string(), "test_file.txt");
5006
5007 std::env::remove_var("TEST_ARG_INPUT");
5009 }
5010
5011 #[test]
5012 fn test_flag_env_var_with_arg() {
5013 let cmd = SpecCommand::builder()
5014 .name("test")
5015 .flag(
5016 SpecFlag::builder()
5017 .long("output")
5018 .env("TEST_FLAG_OUTPUT")
5019 .arg(SpecArg::builder().name("file").build())
5020 .build(),
5021 )
5022 .build();
5023 let spec = Spec {
5024 name: "test".to_string(),
5025 bin: "test".to_string(),
5026 cmd,
5027 ..Default::default()
5028 };
5029
5030 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5032
5033 let input = vec!["test".to_string()];
5034 let parsed = parse(&spec, &input).unwrap();
5035
5036 assert_eq!(parsed.flags.len(), 1);
5037 let flag = parsed.flags.keys().next().unwrap();
5038 assert_eq!(flag.name, "output");
5039 let value = parsed.flags.values().next().unwrap();
5040 assert_eq!(value.to_string(), "output.txt");
5041
5042 std::env::remove_var("TEST_FLAG_OUTPUT");
5044 }
5045
5046 #[test]
5047 fn test_flag_env_var_boolean() {
5048 let cmd = SpecCommand::builder()
5049 .name("test")
5050 .flag(
5051 SpecFlag::builder()
5052 .long("verbose")
5053 .env("TEST_FLAG_VERBOSE")
5054 .build(),
5055 )
5056 .build();
5057 let spec = Spec {
5058 name: "test".to_string(),
5059 bin: "test".to_string(),
5060 cmd,
5061 ..Default::default()
5062 };
5063
5064 std::env::set_var("TEST_FLAG_VERBOSE", "true");
5066
5067 let input = vec!["test".to_string()];
5068 let parsed = parse(&spec, &input).unwrap();
5069
5070 assert_eq!(parsed.flags.len(), 1);
5071 let flag = parsed.flags.keys().next().unwrap();
5072 assert_eq!(flag.name, "verbose");
5073 let value = parsed.flags.values().next().unwrap();
5074 assert_eq!(value.to_string(), "true");
5075
5076 std::env::remove_var("TEST_FLAG_VERBOSE");
5078 }
5079
5080 #[test]
5081 fn test_env_var_precedence() {
5082 let cmd = SpecCommand::builder()
5084 .name("test")
5085 .arg(
5086 SpecArg::builder()
5087 .name("input")
5088 .env("TEST_PRECEDENCE_INPUT")
5089 .required(true)
5090 .build(),
5091 )
5092 .build();
5093 let spec = Spec {
5094 name: "test".to_string(),
5095 bin: "test".to_string(),
5096 cmd,
5097 ..Default::default()
5098 };
5099
5100 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5102
5103 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5104 let parsed = parse(&spec, &input).unwrap();
5105
5106 assert_eq!(parsed.args.len(), 1);
5107 let value = parsed.args.values().next().unwrap();
5108 assert_eq!(value.to_string(), "cli_file.txt");
5110
5111 std::env::remove_var("TEST_PRECEDENCE_INPUT");
5113 }
5114
5115 #[test]
5116 fn test_flag_var_true_with_single_default() {
5117 let cmd = SpecCommand::builder()
5119 .name("test")
5120 .flag(
5121 SpecFlag::builder()
5122 .long("foo")
5123 .var(true)
5124 .arg(SpecArg::builder().name("foo").build())
5125 .default_value("bar")
5126 .build(),
5127 )
5128 .build();
5129 let spec = Spec {
5130 name: "test".to_string(),
5131 bin: "test".to_string(),
5132 cmd,
5133 ..Default::default()
5134 };
5135
5136 let input = vec!["test".to_string()];
5138 let parsed = parse(&spec, &input).unwrap();
5139
5140 assert_eq!(parsed.flags.len(), 1);
5141 let flag = parsed.flags.keys().next().unwrap();
5142 assert_eq!(flag.name, "foo");
5143 let value = parsed.flags.values().next().unwrap();
5144 match value {
5146 ParseValue::MultiString(v) => {
5147 assert_eq!(v.len(), 1);
5148 assert_eq!(v[0], "bar");
5149 }
5150 _ => panic!("Expected MultiString, got {:?}", value),
5151 }
5152 }
5153
5154 #[test]
5155 fn test_flag_var_true_with_multiple_defaults() {
5156 let cmd = SpecCommand::builder()
5158 .name("test")
5159 .flag(
5160 SpecFlag::builder()
5161 .long("foo")
5162 .var(true)
5163 .arg(SpecArg::builder().name("foo").build())
5164 .default_values(["xyz", "bar"])
5165 .build(),
5166 )
5167 .build();
5168 let spec = Spec {
5169 name: "test".to_string(),
5170 bin: "test".to_string(),
5171 cmd,
5172 ..Default::default()
5173 };
5174
5175 let input = vec!["test".to_string()];
5177 let parsed = parse(&spec, &input).unwrap();
5178
5179 assert_eq!(parsed.flags.len(), 1);
5180 let value = parsed.flags.values().next().unwrap();
5181 match value {
5183 ParseValue::MultiString(v) => {
5184 assert_eq!(v.len(), 2);
5185 assert_eq!(v[0], "xyz");
5186 assert_eq!(v[1], "bar");
5187 }
5188 _ => panic!("Expected MultiString, got {:?}", value),
5189 }
5190 }
5191
5192 #[test]
5193 fn test_flag_var_false_with_default_remains_string() {
5194 let cmd = SpecCommand::builder()
5196 .name("test")
5197 .flag(
5198 SpecFlag::builder()
5199 .long("foo")
5200 .var(false) .arg(SpecArg::builder().name("foo").build())
5202 .default_value("bar")
5203 .build(),
5204 )
5205 .build();
5206 let spec = Spec {
5207 name: "test".to_string(),
5208 bin: "test".to_string(),
5209 cmd,
5210 ..Default::default()
5211 };
5212
5213 let input = vec!["test".to_string()];
5215 let parsed = parse(&spec, &input).unwrap();
5216
5217 assert_eq!(parsed.flags.len(), 1);
5218 let value = parsed.flags.values().next().unwrap();
5219 match value {
5221 ParseValue::String(s) => {
5222 assert_eq!(s, "bar");
5223 }
5224 _ => panic!("Expected String, got {:?}", value),
5225 }
5226 }
5227
5228 #[test]
5229 fn test_arg_var_true_with_single_default() {
5230 let cmd = SpecCommand::builder()
5232 .name("test")
5233 .arg(
5234 SpecArg::builder()
5235 .name("files")
5236 .var(true)
5237 .default_value("default.txt")
5238 .required(false)
5239 .build(),
5240 )
5241 .build();
5242 let spec = Spec {
5243 name: "test".to_string(),
5244 bin: "test".to_string(),
5245 cmd,
5246 ..Default::default()
5247 };
5248
5249 let input = vec!["test".to_string()];
5251 let parsed = parse(&spec, &input).unwrap();
5252
5253 assert_eq!(parsed.args.len(), 1);
5254 let value = parsed.args.values().next().unwrap();
5255 match value {
5257 ParseValue::MultiString(v) => {
5258 assert_eq!(v.len(), 1);
5259 assert_eq!(v[0], "default.txt");
5260 }
5261 _ => panic!("Expected MultiString, got {:?}", value),
5262 }
5263 }
5264
5265 #[test]
5266 fn test_arg_var_true_with_multiple_defaults() {
5267 let cmd = SpecCommand::builder()
5269 .name("test")
5270 .arg(
5271 SpecArg::builder()
5272 .name("files")
5273 .var(true)
5274 .default_values(["file1.txt", "file2.txt"])
5275 .required(false)
5276 .build(),
5277 )
5278 .build();
5279 let spec = Spec {
5280 name: "test".to_string(),
5281 bin: "test".to_string(),
5282 cmd,
5283 ..Default::default()
5284 };
5285
5286 let input = vec!["test".to_string()];
5288 let parsed = parse(&spec, &input).unwrap();
5289
5290 assert_eq!(parsed.args.len(), 1);
5291 let value = parsed.args.values().next().unwrap();
5292 match value {
5294 ParseValue::MultiString(v) => {
5295 assert_eq!(v.len(), 2);
5296 assert_eq!(v[0], "file1.txt");
5297 assert_eq!(v[1], "file2.txt");
5298 }
5299 _ => panic!("Expected MultiString, got {:?}", value),
5300 }
5301 }
5302
5303 #[test]
5304 fn test_arg_var_false_with_default_remains_string() {
5305 let cmd = SpecCommand::builder()
5307 .name("test")
5308 .arg(
5309 SpecArg::builder()
5310 .name("file")
5311 .var(false)
5312 .default_value("default.txt")
5313 .required(false)
5314 .build(),
5315 )
5316 .build();
5317 let spec = Spec {
5318 name: "test".to_string(),
5319 bin: "test".to_string(),
5320 cmd,
5321 ..Default::default()
5322 };
5323
5324 let input = vec!["test".to_string()];
5326 let parsed = parse(&spec, &input).unwrap();
5327
5328 assert_eq!(parsed.args.len(), 1);
5329 let value = parsed.args.values().next().unwrap();
5330 match value {
5332 ParseValue::String(s) => {
5333 assert_eq!(s, "default.txt");
5334 }
5335 _ => panic!("Expected String, got {:?}", value),
5336 }
5337 }
5338
5339 #[test]
5340 fn test_scalar_defaults_validate_only_first_default_choice() {
5341 let specs = [
5342 spec_with_arg(
5343 SpecArg::builder()
5344 .name("env")
5345 .var(false)
5346 .default_values(["dev", "prod"])
5347 .choices(["dev"])
5348 .required(false)
5349 .build(),
5350 ),
5351 spec_with_flag(
5352 SpecFlag::builder()
5353 .long("env")
5354 .arg(
5355 SpecArg::builder()
5356 .name("env")
5357 .default_values(["dev", "prod"])
5358 .choices(["dev"])
5359 .build(),
5360 )
5361 .build(),
5362 ),
5363 ];
5364
5365 for spec in specs {
5366 let parsed = parse(&spec, &input(&["test"])).unwrap();
5367 assert_eq!(first_string_value(&parsed), "dev");
5368 }
5369 }
5370
5371 #[test]
5372 fn a_delimiter_turns_one_word_into_several_values() {
5373 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5374 .parse()
5375 .unwrap();
5376
5377 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5378 let multi = |value: &ParseValue| match value {
5379 ParseValue::MultiString(values) => values.clone(),
5380 other => panic!("expected several values, got {other:?}"),
5381 };
5382 let tags = parsed
5383 .flags
5384 .iter()
5385 .find(|(f, _)| f.name == "tags")
5386 .map(|(_, v)| v)
5387 .unwrap();
5388 assert_eq!(multi(tags), vec!["a", "b", "c"]);
5389 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5390 }
5391
5392 #[test]
5393 fn a_positional_splits_before_its_choices_are_asked() {
5394 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
5398 .parse()
5399 .unwrap();
5400
5401 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5402
5403 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5404 let message = err.to_string();
5405 assert!(message.contains("nowhere"), "{message}");
5406 assert!(
5407 !message.contains("src:nowhere"),
5408 "the bad half should be named, not the whole word: {message}"
5409 );
5410 }
5411
5412 #[test]
5413 fn a_split_value_is_counted_and_judged_as_values() {
5414 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
5418 .parse()
5419 .unwrap();
5420
5421 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5422 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5423 assert!(err.to_string().contains("staging"), "{err}");
5424 assert!(
5425 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5426 "three values should breach var_max=2"
5427 );
5428 }
5429
5430 #[test]
5431 fn a_split_bound_counts_one_occurrence_at_a_time() {
5432 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
5437 .parse()
5438 .unwrap();
5439
5440 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5441 assert!(
5442 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5443 "three values out of one word is still three values"
5444 );
5445 parse(
5448 &spec,
5449 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5450 )
5451 .expect("two per occurrence, twice, is within the bound");
5452 }
5453
5454 #[test]
5455 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5456 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5457 .parse()
5458 .unwrap();
5459
5460 parse(
5461 &spec,
5462 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5463 )
5464 .expect("each occurrence satisfies the bound independently");
5465
5466 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5467 assert!(
5468 error
5469 .to_string()
5470 .contains("requires at least 2 value(s), got 1"),
5471 "{error:?}"
5472 );
5473 }
5474
5475 #[test]
5476 fn an_exclusive_flag_has_to_be_alone() {
5477 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
5478 .parse()
5479 .unwrap();
5480
5481 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
5482
5483 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
5485 assert!(err.to_string().contains("on its own"), "{err}");
5486
5487 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
5490 assert!(err.to_string().contains("on its own"), "{err}");
5491
5492 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
5494 }
5495
5496 #[test]
5497 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
5498 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
5501 .parse()
5502 .unwrap();
5503
5504 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
5505 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
5506 }
5507
5508 #[test]
5509 fn an_exclusive_flag_bypasses_required_siblings() {
5510 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
5511 .parse()
5512 .unwrap();
5513
5514 parse(&spec, &input(&["ex", "--dump"]))
5515 .expect("exclusive is the command's requiredness escape");
5516 assert!(parse(
5517 &spec,
5518 &input(&["ex", "--dump", "--out", "somewhere", "target"])
5519 )
5520 .is_err());
5521 }
5522
5523 #[test]
5524 fn an_environment_value_counts_for_an_exclusive_flag() {
5525 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
5526 .parse()
5527 .unwrap();
5528
5529 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
5530 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
5531 }
5532
5533 #[test]
5534 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
5535 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
5536 .parse()
5537 .unwrap();
5538
5539 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
5540 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
5541 }
5542
5543 #[test]
5544 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
5545 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5546 .parse()
5547 .unwrap();
5548
5549 parse(&spec, &input(&["ex", "run", "--clean"]))
5550 .expect("the child flag is alone within the child command");
5551 assert!(
5552 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5553 "the parent flag still conflicts with selecting the child"
5554 );
5555 }
5556
5557 #[test]
5558 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
5559 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5560 .parse()
5561 .unwrap();
5562
5563 parse(&spec, &input(&["ex", "run", "--clean"]))
5564 .expect("the child-local exclusive flag is alone inside the child command");
5565 assert!(
5566 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5567 "the ancestor spelling still conflicts with selecting the child"
5568 );
5569 }
5570
5571 #[test]
5572 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
5573 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
5574 .parse()
5575 .unwrap();
5576
5577 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
5578 assert!(
5579 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
5580 "the distinct parent declaration is still company despite sharing a name"
5581 );
5582 }
5583
5584 #[test]
5585 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
5586 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5587 .parse()
5588 .unwrap();
5589
5590 parse(&spec, &input(&["ex", "run", "--clean"]))
5591 .expect("the child exclusive flag is valid alone");
5592 assert!(
5593 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5594 "merging with the inherited global must not discard child exclusivity"
5595 );
5596 }
5597
5598 #[test]
5599 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
5600 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5601 .parse()
5602 .unwrap();
5603
5604 parse(&spec, &input(&["ex", "run", "--clean"]))
5605 .expect("the typed long form belongs to the child declaration");
5606 assert!(
5607 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5608 "the inherited short form still belongs to the ancestor"
5609 );
5610 assert!(
5611 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
5612 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
5613 );
5614 }
5615
5616 #[test]
5617 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
5618 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
5619 .parse()
5620 .unwrap();
5621
5622 parse(&spec, &input(&["ex", "run", "--clean"]))
5623 .expect("the child's spelling does not activate the orphan ancestor alias");
5624 assert!(
5625 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5626 "the inherited short alias still belongs to the ancestor exclusive flag"
5627 );
5628 }
5629
5630 #[test]
5631 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
5632 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" negate=\"--no-clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"-c --clean\" global=#true\n}\n"
5633 .parse()
5634 .unwrap();
5635
5636 assert!(
5637 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
5638 "the inherited negated alias still belongs to the ancestor exclusive flag"
5639 );
5640 }
5641
5642 #[test]
5643 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
5644 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\nflag \"-c --config <f>\" global=#true\ncmd \"run\" {\n flag \"-c --clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5649 .parse()
5650 .unwrap();
5651
5652 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
5653 assert!(
5654 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5655 "one unrelated alias collision cannot disown the child from its own flag"
5656 );
5657 }
5658
5659 #[test]
5660 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
5661 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5667 .parse()
5668 .unwrap();
5669 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
5670 .expect("the ancestor owns this occurrence, and it is not exclusive");
5671 assert!(
5672 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5673 "after the subcommand word the child's declaration is in scope, and it is exclusive"
5674 );
5675
5676 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5677 .parse()
5678 .unwrap();
5679 assert!(
5680 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
5681 "the same rule, with an exclusive ancestor: selecting the child is company for it"
5682 );
5683 }
5684
5685 #[test]
5686 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
5687 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
5691 .parse()
5692 .unwrap();
5693
5694 assert!(
5695 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5696 "the orphan ancestor alias is still the ancestor's exclusive flag"
5697 );
5698 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
5699 .expect("the child's own spelling drops the exclusivity the child did not restate");
5700 }
5701
5702 #[test]
5703 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
5704 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5708 .parse()
5709 .unwrap();
5710
5711 assert!(
5712 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
5713 "the child spelling is exclusive whatever it was typed beside"
5714 );
5715 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
5716 .expect("the ancestor's own spelling was never exclusive");
5717 }
5718
5719 #[test]
5720 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
5721 let added: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true env=\"EX_CLEAN\"\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5725 .parse()
5726 .unwrap();
5727
5728 assert!(
5729 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
5730 "the child added exclusivity the environment value has to honor"
5731 );
5732
5733 let dropped: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true env=\"EX_CLEAN\"\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
5734 .parse()
5735 .unwrap();
5736
5737 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
5738 .expect("the child dropped the exclusivity, and the environment value follows it");
5739 }
5740
5741 #[test]
5742 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
5743 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--out <path>\" required=#true\n}\n"
5746 .parse()
5747 .unwrap();
5748
5749 parse(&spec, &input(&["ex", "run", "--clean"]))
5750 .expect("a merged child exclusive flag is still the command's requiredness escape");
5751 }
5752
5753 #[test]
5754 fn a_group_allows_one_member_and_refuses_two() {
5755 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
5756 .parse()
5757 .unwrap();
5758
5759 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
5761 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
5762
5763 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
5764 assert!(err.to_string().contains("group input"), "{err}");
5765 }
5766
5767 #[test]
5768 fn positional_selectors_work_in_conflicts_and_groups() {
5769 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
5770 .parse()
5771 .unwrap();
5772 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
5773 .expect("the flag alone is valid");
5774 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
5775 assert!(parse(
5776 &conflicts,
5777 &input(&["ex", "--from-file", "vars.env", "literal"])
5778 )
5779 .is_err());
5780
5781 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
5782 .parse()
5783 .unwrap();
5784 assert!(parse(
5785 &positional_source,
5786 &input(&["ex", "--from-file", "vars.env", "literal"])
5787 )
5788 .is_err());
5789
5790 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
5791 .parse()
5792 .unwrap();
5793 assert!(parse(&group, &input(&["ex"])).is_err());
5794 parse(&group, &input(&["ex", "target-name"]))
5795 .expect("a positional satisfies a required group");
5796 assert!(parse(
5797 &group,
5798 &input(&["ex", "--file", "input.txt", "target-name"])
5799 )
5800 .is_err());
5801 }
5802
5803 #[test]
5804 fn a_required_group_needs_one_of_its_members() {
5805 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5806 .parse()
5807 .unwrap();
5808
5809 let err = parse(&spec, &input(&["ex"])).unwrap_err();
5810 assert!(err.to_string().contains("--file, --url"), "{err}");
5813 assert!(err.to_string().contains("input"), "{err}");
5814
5815 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
5816 }
5817
5818 #[test]
5819 fn a_multiple_group_only_polices_requiredness() {
5820 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
5823 .parse()
5824 .unwrap();
5825
5826 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
5827 assert!(parse(&spec, &input(&["ex"])).is_err());
5828 }
5829
5830 #[test]
5831 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
5832 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5838 .parse()
5839 .unwrap();
5840
5841 parse(&spec, &input(&["ex"])).expect("the default fills the group");
5842 parse(&spec, &input(&["ex", "--url", "u"]))
5843 .expect("the default must not conflict with the flag the user typed");
5844 }
5845
5846 #[test]
5847 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
5848 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
5851 .parse()
5852 .unwrap();
5853
5854 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
5855 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
5856
5857 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
5859 assert!(err.to_string().contains("group input"), "{err}");
5860 }
5861
5862 #[test]
5863 fn a_group_reads_the_environment_as_given() {
5864 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5867 .parse()
5868 .unwrap();
5869
5870 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
5871 }
5872
5873 #[test]
5874 fn a_requirement_names_the_flag_that_is_missing() {
5875 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
5878 .parse()
5879 .unwrap();
5880
5881 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
5882 assert!(
5883 err.to_string().contains("format"),
5884 "the missing flag should be named: {err}"
5885 );
5886
5887 for words in [
5889 &["ex", "--out", "a.txt", "--format", "json"][..],
5890 &["ex", "--format", "json", "--out", "a.txt"][..],
5891 ] {
5892 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
5893 }
5894
5895 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
5898 }
5899
5900 #[test]
5901 fn a_value_activates_only_its_conditional_requirement() {
5902 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" {\n requires_if \"special.toml\" \"--key\"\n requires_if \"remote.toml\" \"--token\"\n}\nflag \"--key <key>\"\nflag \"--token <token>\"\n"
5903 .parse()
5904 .unwrap();
5905
5906 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
5907 .expect("an unrelated value requires nothing");
5908
5909 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
5910 assert!(key.to_string().contains("key"), "{key}");
5911 parse(
5912 &spec,
5913 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
5914 )
5915 .expect("the matching requirement is satisfied");
5916
5917 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
5918 assert!(token.to_string().contains("token"), "{token}");
5919 }
5920
5921 #[test]
5922 fn conditional_requirements_read_explicit_env_but_not_defaults() {
5923 let from_env: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
5924 .parse()
5925 .unwrap();
5926 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
5927 assert!(err.to_string().contains("key"), "{err}");
5928
5929 let from_default: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" default=\"special.toml\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
5930 .parse()
5931 .unwrap();
5932 parse(&from_default, &input(&["ex"]))
5933 .expect("a default is not an explicit conditional value");
5934 }
5935
5936 #[test]
5937 fn command_line_values_override_env_for_conditional_requirements() {
5938 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
5939 .parse()
5940 .unwrap();
5941
5942 parse_with_env(
5943 &spec,
5944 &["ex", "--config", "ordinary.toml"],
5945 &[("EX_CONFIG", "special.toml")],
5946 )
5947 .expect("the command-line value takes precedence over the environment");
5948 }
5949
5950 #[test]
5951 fn conditional_requirements_normalize_boolean_env_values() {
5952 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
5953 .parse()
5954 .unwrap();
5955
5956 for value in ["1", "true", "True", "TRUE"] {
5957 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
5958 assert!(err.to_string().contains("key"), "{value}: {err}");
5959 }
5960 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
5961 .expect("a false environment value does not activate a true condition");
5962 }
5963
5964 #[test]
5965 fn a_default_satisfies_a_requirement() {
5966 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
5970 .parse()
5971 .unwrap();
5972
5973 parse(&spec, &input(&["ex", "--out", "a.txt"]))
5974 .expect("a defaulted flag is not a missing one");
5975 }
5976
5977 #[test]
5978 fn a_present_flag_binds_a_conditional_default() {
5979 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
5980 .parse()
5981 .unwrap();
5982
5983 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
5984 assert_eq!(
5985 with.as_env().get("usage_bin_names").map(String::as_str),
5986 Some("true")
5987 );
5988
5989 let without = parse(&spec, &input(&["ex"])).unwrap();
5990 assert!(
5991 !without.as_env().contains_key("usage_bin_names"),
5992 "IsPresent does nothing when the selector is absent"
5993 );
5994 }
5995
5996 #[test]
5997 fn an_equals_condition_binds_a_conditional_default() {
5998 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
5999 .parse()
6000 .unwrap();
6001
6002 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6003 assert_eq!(
6004 json.as_env().get("usage_style").map(String::as_str),
6005 Some("pretty")
6006 );
6007 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6008 assert!(!yaml.as_env().contains_key("usage_style"));
6009 }
6010
6011 #[test]
6012 fn an_equals_condition_reads_a_negated_flag() {
6013 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6014 .parse()
6015 .unwrap();
6016
6017 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6018 assert_eq!(
6019 off.as_env().get("usage_pretty").map(String::as_str),
6020 Some("true")
6021 );
6022 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6023 assert!(
6024 !on.as_env().contains_key("usage_pretty"),
6025 "--json is true, so when=false should miss"
6026 );
6027 }
6028
6029 #[test]
6030 fn the_first_matching_conditional_default_wins() {
6031 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--json\" \"compact\"\n default_if \"--pretty\" \"pretty\"\n}\nflag \"--json\"\nflag \"--pretty\"\n"
6032 .parse()
6033 .unwrap();
6034
6035 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6036 assert_eq!(
6037 out.as_env().get("usage_style").map(String::as_str),
6038 Some("compact")
6039 );
6040 }
6041
6042 #[test]
6043 fn argv_and_env_suppress_a_conditional_default() {
6044 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6045 .parse()
6046 .unwrap();
6047
6048 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6049 assert_eq!(
6050 from_env.as_env().get("usage_bin_names").map(String::as_str),
6051 Some("false"),
6052 "the target's environment wins over default_if"
6053 );
6054 }
6055
6056 #[test]
6057 fn a_sibling_env_activates_a_conditional_default() {
6058 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6059 .parse()
6060 .unwrap();
6061
6062 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6063 assert_eq!(
6064 out.as_env().get("usage_bin_names").map(String::as_str),
6065 Some("true")
6066 );
6067 }
6068
6069 #[test]
6070 fn a_default_does_not_activate_a_conditional_default() {
6071 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6075 .parse()
6076 .unwrap();
6077
6078 let out = parse(&spec, &input(&["ex"])).unwrap();
6079 assert_eq!(
6080 out.as_env().get("usage_json").map(String::as_str),
6081 Some("true")
6082 );
6083 assert!(
6084 !out.as_env().contains_key("usage_pretty"),
6085 "a default is not an explicit value for default_if"
6086 );
6087 }
6088
6089 #[test]
6090 fn a_conditional_default_does_not_activate_requires_if() {
6091 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n requires_if \"json\" \"--schema\"\n}\nflag \"--schema <s>\"\nflag \"--json\"\n"
6092 .parse()
6093 .unwrap();
6094
6095 parse(&spec, &input(&["ex", "--json"]))
6096 .expect("a default_if value is not explicit for requires_if");
6097 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6098 }
6099
6100 #[test]
6101 fn a_conditional_default_satisfies_a_requirement() {
6102 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6103 .parse()
6104 .unwrap();
6105
6106 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6107 .expect("default_if fills the required flag");
6108 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6109 }
6110
6111 #[test]
6112 fn an_environment_value_satisfies_a_requirement() {
6113 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6114 .parse()
6115 .unwrap();
6116
6117 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6118 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6119 .expect("the environment supplies it");
6120 }
6121
6122 #[test]
6123 fn a_requirement_is_satisfied_by_a_short_form() {
6124 let spec: Spec =
6128 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6129 .parse()
6130 .unwrap();
6131
6132 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6133
6134 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6135 assert!(err.to_string().contains("key"), "{err}");
6136 }
6137
6138 #[test]
6139 fn conflicting_flags_are_rejected_in_either_order() {
6140 let spec: Spec =
6144 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6145 .parse()
6146 .unwrap();
6147
6148 for words in [
6149 &["ex", "--file", "a.txt", "--stdin"][..],
6150 &["ex", "--stdin", "--file", "a.txt"][..],
6151 ] {
6152 let err = parse(&spec, &input(words)).unwrap_err();
6153 assert!(
6154 err.to_string().contains("conflicts with --stdin"),
6155 "{words:?} should be refused: {err}"
6156 );
6157 }
6158
6159 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6161 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6162 }
6163
6164 #[test]
6165 fn unknown_flags_are_values_by_default() {
6166 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6169 .parse()
6170 .unwrap();
6171 let out = parse(
6172 &spec,
6173 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6174 )
6175 .unwrap();
6176 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6177 assert_eq!(out.args[rest].to_string(), "--wat x");
6178 }
6179
6180 #[test]
6181 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6182 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6183 .parse()
6184 .unwrap();
6185 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6186 .expect("a repeat is a correction by default");
6187 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6188 assert_eq!(out.flags[jobs].to_string(), "2");
6189 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6190 .expect("switches use the same default");
6191
6192 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6193 .parse()
6194 .unwrap();
6195 for words in [
6196 &["ex", "--jobs", "1", "--jobs", "2"][..],
6197 &["ex", "--verbose", "--verbose"][..],
6198 ] {
6199 let err = parse(&strict, &input(words)).unwrap_err();
6200 assert!(
6201 err.to_string().contains("cannot be used multiple times"),
6202 "{err}"
6203 );
6204 }
6205
6206 let reparsed: Spec = strict.to_string().parse().unwrap();
6207 assert!(!reparsed.cmd.args_override_self);
6208 }
6209
6210 #[test]
6211 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6212 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6213 .parse()
6214 .unwrap();
6215
6216 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6217 .expect("opposite forms override each other");
6218 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6219 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6220
6221 for words in [
6222 &["ex", "--color", "--color"][..],
6223 &["ex", "--no-color", "--no-color"][..],
6224 ] {
6225 let err = parse(&spec, &input(words)).unwrap_err();
6226 assert!(err.to_string().contains("cannot be used multiple times"));
6227 }
6228 }
6229
6230 #[test]
6231 fn strict_global_flags_may_repeat_across_command_levels() {
6232 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\" global=#true\nflag \"--jobs <n>\" global=#true\ncmd \"run\" {\n args_override_self #false\n}\n"
6233 .parse()
6234 .unwrap();
6235
6236 let out = parse(
6237 &spec,
6238 &input(&[
6239 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6240 ]),
6241 )
6242 .expect("an inherited global is allowed once at each command level");
6243 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6244 assert_eq!(out.flags[jobs].to_string(), "2");
6245
6246 for words in [
6247 &["ex", "--color", "--color", "run", "--no-color"][..],
6248 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6249 ] {
6250 let err = parse(&spec, &input(words)).unwrap_err();
6251 assert!(err.to_string().contains("cannot be used multiple times"));
6252 }
6253 }
6254
6255 #[test]
6256 fn a_subcommand_can_negate_only_its_parents_requirements() {
6257 let base = r#"name "ex"
6258bin "ex"
6259flag "--config" required=#true
6260flag "--mode" requires="--config"
6261flag "--other"
6262arg "<input>"
6263group "source" "--config" "--other" required=#true
6264cmd "run" { flag "--child" required=#true }
6265"#;
6266 let strict: Spec = base.parse().unwrap();
6267 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6268 let message = err.to_string();
6269 assert!(
6270 message.contains("input") || message.contains("config"),
6271 "{message}"
6272 );
6273
6274 let negated: Spec = base
6275 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6276 .parse()
6277 .unwrap();
6278 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6279 assert!(
6280 err.to_string().contains("child"),
6281 "the selected command keeps its own requirements: {err}"
6282 );
6283
6284 let mut child_optional = negated.clone();
6285 child_optional.cmd.subcommands["run"].flags[0].required = false;
6286 parse(&child_optional, &input(&["ex", "run"]))
6287 .expect("the child selection satisfies all parent requirements");
6288 parse(&child_optional, &input(&["ex", "--mode", "run"]))
6289 .expect("parent requires relationships are negated too");
6290 }
6291
6292 #[test]
6293 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6294 let spec: Spec = r#"name "ex"
6295bin "ex"
6296args_conflicts_with_subcommands #true
6297flag "--verbose"
6298cmd "run"
6299"#
6300 .parse()
6301 .unwrap();
6302
6303 parse(&spec, &input(&["ex", "run"]))
6304 .expect("the subcommand is valid without a parent argument");
6305 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6306 assert!(
6307 err.to_string().contains("cannot be used with arguments"),
6308 "{err}"
6309 );
6310 }
6311
6312 #[test]
6313 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6314 let base = r#"name "ex"
6315bin "ex"
6316flag "--values <value>..."
6317cmd "run"
6318"#;
6319 let plain: Spec = base.parse().unwrap();
6320 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6321 assert_eq!(out.cmd.name, "ex");
6322
6323 let precedence: Spec = base
6324 .replacen(
6325 "bin \"ex\"",
6326 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6327 1,
6328 )
6329 .parse()
6330 .unwrap();
6331 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6332 assert_eq!(out.cmd.name, "run");
6333 }
6334
6335 #[test]
6336 fn a_required_positional_can_follow_an_unfilled_optional_one() {
6337 let base = r#"name "ex"
6338bin "ex"
6339arg "[optional]"
6340arg "<required>"
6341"#;
6342 let plain: Spec = base.parse().unwrap();
6343 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6344 assert!(err.to_string().contains("required"), "{err}");
6345
6346 let enabled: Spec = base
6347 .replacen(
6348 "bin \"ex\"",
6349 "bin \"ex\"\nallow_missing_positional #true",
6350 1,
6351 )
6352 .parse()
6353 .unwrap();
6354 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6355 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6356 let value = &out
6357 .args
6358 .iter()
6359 .find(|(arg, _)| arg.name == "required")
6360 .unwrap()
6361 .1;
6362 assert!(matches!(value, ParseValue::String(value) if value == "value"));
6363 }
6364
6365 #[test]
6366 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6367 let spec: Spec =
6368 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6369 .parse()
6370 .unwrap();
6371 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6372 assert!(
6373 err.to_string().contains("--wat"),
6374 "the message should name the token: {err}"
6375 );
6376
6377 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6380 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6381 assert_eq!(out.args[rest].to_string(), "-1");
6382
6383 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6384 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6385 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6386 }
6387
6388 #[test]
6389 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6390 let spec: Spec = r#"
6391name "ex"
6392bin "ex"
6393unknown_flags "error"
6394flag "-0 --print0" global=#true
6395cmd "run" {
6396 flag "--force"
6397}
6398"#
6399 .parse()
6400 .unwrap();
6401 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6402 assert_eq!(out.cmd.name, "run");
6403 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6404 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6405 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6406 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6407 }
6408
6409 #[test]
6410 fn a_command_may_override_the_cli_wide_setting() {
6411 let spec: Spec = r#"
6413name "ex"
6414bin "ex"
6415unknown_flags "error"
6416cmd "exec" unknown_flags="value" {
6417 arg "[rest]..."
6418}
6419cmd "build" {
6420 arg "[rest]..."
6421}
6422"#
6423 .parse()
6424 .unwrap();
6425
6426 let out = parse(
6427 &spec,
6428 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6429 )
6430 .unwrap();
6431 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6432 assert_eq!(out.args[rest].to_string(), "--wat");
6433
6434 assert!(
6435 parse(
6436 &spec,
6437 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6438 )
6439 .is_err(),
6440 "a command that says nothing inherits the CLI's choice"
6441 );
6442 }
6443
6444 #[test]
6445 fn the_setting_survives_a_round_trip() {
6446 let spec: Spec =
6447 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
6448 .parse()
6449 .unwrap();
6450 let reparsed: Spec = spec.to_string().parse().unwrap();
6451 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
6452 assert_eq!(
6453 reparsed.cmd.subcommands["x"].unknown_flags,
6454 Some(UnknownFlags::Value)
6455 );
6456 }
6457
6458 #[test]
6459 fn test_default_subcommand() {
6460 let run_cmd = SpecCommand::builder()
6462 .name("run")
6463 .arg(SpecArg::builder().name("task").build())
6464 .build();
6465 let mut cmd = SpecCommand::builder().name("test").build();
6466 cmd.subcommands.insert("run".to_string(), run_cmd);
6467
6468 let spec = Spec {
6469 name: "test".to_string(),
6470 bin: "test".to_string(),
6471 cmd,
6472 default_subcommand: Some("run".to_string()),
6473 ..Default::default()
6474 };
6475
6476 let input = vec!["test".to_string(), "mytask".to_string()];
6478 let parsed = parse(&spec, &input).unwrap();
6479
6480 assert_eq!(parsed.cmds.len(), 2);
6482 assert_eq!(parsed.cmds[1].name, "run");
6483
6484 assert_eq!(parsed.args.len(), 1);
6486 let arg = parsed.args.keys().next().unwrap();
6487 assert_eq!(arg.name, "task");
6488 let value = parsed.args.values().next().unwrap();
6489 assert_eq!(value.to_string(), "mytask");
6490 }
6491
6492 #[test]
6493 fn test_default_subcommand_explicit_still_works() {
6494 let run_cmd = SpecCommand::builder()
6496 .name("run")
6497 .arg(SpecArg::builder().name("task").build())
6498 .build();
6499 let other_cmd = SpecCommand::builder()
6500 .name("other")
6501 .arg(SpecArg::builder().name("other_arg").build())
6502 .build();
6503 let mut cmd = SpecCommand::builder().name("test").build();
6504 cmd.subcommands.insert("run".to_string(), run_cmd);
6505 cmd.subcommands.insert("other".to_string(), other_cmd);
6506
6507 let spec = Spec {
6508 name: "test".to_string(),
6509 bin: "test".to_string(),
6510 cmd,
6511 default_subcommand: Some("run".to_string()),
6512 ..Default::default()
6513 };
6514
6515 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
6517 let parsed = parse(&spec, &input).unwrap();
6518
6519 assert_eq!(parsed.cmds.len(), 2);
6521 assert_eq!(parsed.cmds[1].name, "other");
6522 }
6523
6524 #[test]
6525 fn test_default_subcommand_applies_only_at_the_root() {
6526 let mut config_ls = SpecCommand::builder().name("ls").build();
6532 config_ls.args.push(SpecArg::builder().name("what").build());
6533 let mut config_cmd = SpecCommand::builder().name("config").build();
6534 config_cmd.subcommands.insert("ls".to_string(), config_ls);
6535
6536 let mut root_ls = SpecCommand::builder().name("ls").build();
6539 root_ls.args.push(SpecArg::builder().name("what").build());
6540 let mut cmd = SpecCommand::builder().name("ex").build();
6541 cmd.subcommands.insert("ls".to_string(), root_ls);
6542 cmd.subcommands.insert("config".to_string(), config_cmd);
6543
6544 let spec = Spec {
6545 name: "ex".to_string(),
6546 bin: "ex".to_string(),
6547 cmd,
6548 default_subcommand: Some("ls".to_string()),
6549 ..Default::default()
6550 };
6551
6552 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
6556 assert!(
6557 parse(&spec, &input).is_err(),
6558 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
6559 );
6560
6561 let input = vec!["ex".to_string(), "zzz".to_string()];
6563 let parsed = parse(&spec, &input).expect("the root's default applies");
6564 assert_eq!(
6565 parsed
6566 .cmds
6567 .iter()
6568 .map(|c| c.name.as_str())
6569 .collect::<Vec<_>>(),
6570 ["ex", "ls"]
6571 );
6572 assert_eq!(
6573 parsed.args.values().next().map(|v| v.to_string()),
6574 Some("zzz".to_string()),
6575 "and the word binds inside the command it reached"
6576 );
6577 }
6578
6579 #[test]
6580 fn test_default_subcommand_with_nested_subcommands() {
6581 let say_cmd = SpecCommand::builder()
6585 .name("say")
6586 .arg(SpecArg::builder().name("name").build())
6587 .build();
6588 let mut run_cmd = SpecCommand::builder().name("run").build();
6589 run_cmd.subcommands.insert("say".to_string(), say_cmd);
6590
6591 let mut cmd = SpecCommand::builder().name("test").build();
6592 cmd.subcommands.insert("run".to_string(), run_cmd);
6593
6594 let spec = Spec {
6595 name: "test".to_string(),
6596 bin: "test".to_string(),
6597 cmd,
6598 default_subcommand: Some("run".to_string()),
6599 ..Default::default()
6600 };
6601
6602 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
6604 let parsed = parse(&spec, &input).unwrap();
6605
6606 assert_eq!(parsed.cmds.len(), 3);
6608 assert_eq!(parsed.cmds[0].name, "test");
6609 assert_eq!(parsed.cmds[1].name, "run");
6610 assert_eq!(parsed.cmds[2].name, "say");
6611
6612 assert_eq!(parsed.args.len(), 1);
6614 let arg = parsed.args.keys().next().unwrap();
6615 assert_eq!(arg.name, "name");
6616 let value = parsed.args.values().next().unwrap();
6617 assert_eq!(value.to_string(), "hello");
6618 }
6619
6620 fn mounted_global_flag_spec() -> Spec {
6628 let task_cmd = SpecCommand::builder()
6629 .name("sample:run")
6630 .arg(
6631 SpecArg::builder()
6632 .name("profile")
6633 .choices(["alpha", "beta", "gamma"])
6634 .build(),
6635 )
6636 .build();
6637 let mut run_cmd = SpecCommand::builder()
6639 .name("run")
6640 .flag(
6641 SpecFlag::builder()
6642 .name("cd")
6643 .short('C')
6644 .long("cd")
6645 .arg(SpecArg::builder().name("dir").build())
6646 .global(false)
6647 .build(),
6648 )
6649 .build();
6650 run_cmd
6651 .subcommands
6652 .insert("sample:run".to_string(), task_cmd);
6653
6654 let mut cmd = SpecCommand::builder()
6655 .name("test")
6656 .flag(
6657 SpecFlag::builder()
6658 .name("cd")
6659 .short('C')
6660 .long("cd")
6661 .arg(SpecArg::builder().name("dir").build())
6662 .global(true)
6663 .build(),
6664 )
6665 .build();
6666 cmd.subcommands.insert("run".to_string(), run_cmd);
6667
6668 Spec {
6669 name: "test".to_string(),
6670 bin: "test".to_string(),
6671 cmd,
6672 ..Default::default()
6673 }
6674 }
6675
6676 #[test]
6677 fn test_prefix_global_flag_does_not_pollute_choices() {
6678 let spec = mounted_global_flag_spec();
6685
6686 for words in [
6689 &["test", "-C", "/tmp", "run", "sample:run"][..],
6690 &["test", "--cd=/tmp", "run", "sample:run"][..],
6692 ] {
6693 let parsed = parse_partial(&spec, &input(words)).unwrap();
6694 assert_eq!(
6695 parsed
6696 .cmds
6697 .iter()
6698 .map(|c| c.name.as_str())
6699 .collect::<Vec<_>>(),
6700 vec!["test", "run", "sample:run"],
6701 );
6702 assert!(
6704 parsed.args.is_empty(),
6705 "args should be empty, got {:?}",
6706 parsed.args
6707 );
6708
6709 let cd = parsed
6712 .available_flags
6713 .get("--cd")
6714 .expect("--cd should remain available after descending into the subcommand");
6715 assert!(cd.global, "--cd must stay global after descent");
6716 assert!(
6717 parsed.available_flags.get("-C").is_some_and(|f| f.global),
6718 "-C must stay global after descent",
6719 );
6720
6721 assert_eq!(
6725 parsed.as_env().get("usage_cd").map(String::as_str),
6726 Some("/tmp"),
6727 "global flag value must survive in as_env(), got {:?}",
6728 parsed.as_env(),
6729 );
6730 }
6731
6732 let parsed = parse_partial(
6734 &spec,
6735 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
6736 )
6737 .unwrap();
6738 assert_eq!(parsed.args.len(), 1);
6739 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6740
6741 assert_parse_err(
6743 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
6744 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6745 );
6746 }
6747
6748 fn mounted_orphan_short_spec() -> Spec {
6753 let task_cmd = SpecCommand::builder()
6754 .name("sample:run")
6755 .arg(
6756 SpecArg::builder()
6757 .name("profile")
6758 .choices(["alpha", "beta", "gamma"])
6759 .build(),
6760 )
6761 .build();
6762 let mut run_cmd = SpecCommand::builder()
6765 .name("run")
6766 .flag(
6767 SpecFlag::builder()
6768 .name("raw")
6769 .short('r')
6770 .long("raw")
6771 .global(false)
6772 .build(),
6773 )
6774 .flag(
6775 SpecFlag::builder()
6776 .name("force")
6777 .short('f')
6778 .long("force")
6779 .global(false)
6780 .build(),
6781 )
6782 .build();
6783 run_cmd
6784 .subcommands
6785 .insert("sample:run".to_string(), task_cmd);
6786
6787 let mut cmd = SpecCommand::builder()
6789 .name("test")
6790 .flag(
6791 SpecFlag::builder()
6792 .name("raw")
6793 .long("raw")
6794 .global(true)
6795 .build(),
6796 )
6797 .build();
6798 cmd.subcommands.insert("run".to_string(), run_cmd);
6799
6800 Spec {
6801 name: "test".to_string(),
6802 bin: "test".to_string(),
6803 cmd,
6804 ..Default::default()
6805 }
6806 }
6807
6808 #[test]
6809 fn test_orphan_short_alias_survives_merge() {
6810 let spec = mounted_orphan_short_spec();
6816
6817 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
6818 assert_eq!(
6819 parsed
6820 .cmds
6821 .iter()
6822 .map(|c| c.name.as_str())
6823 .collect::<Vec<_>>(),
6824 vec!["test", "run", "sample:run"],
6825 );
6826
6827 assert!(
6830 parsed.available_flags.get("-r").is_some_and(|f| f.global),
6831 "-r must be merged onto the inherited global flag and stay global after descent",
6832 );
6833 assert!(
6834 parsed
6835 .available_flags
6836 .get("--raw")
6837 .is_some_and(|f| f.global),
6838 "--raw must stay global after descent",
6839 );
6840
6841 assert!(
6843 parsed.args.is_empty(),
6844 "args should be empty, got {:?}",
6845 parsed.args
6846 );
6847
6848 assert_eq!(
6850 parsed.as_env().get("usage_raw").map(String::as_str),
6851 Some("true"),
6852 "merged short's value must survive in as_env(), got {:?}",
6853 parsed.as_env(),
6854 );
6855
6856 assert!(
6859 !parsed.available_flags.contains_key("-f"),
6860 "purely-local -f must not be promoted onto a global",
6861 );
6862 assert!(
6863 !parsed.available_flags.contains_key("--force"),
6864 "purely-local --force must not be promoted onto a global",
6865 );
6866
6867 let parsed =
6869 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
6870 assert_eq!(parsed.args.len(), 1);
6871 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6872
6873 assert_parse_err(
6875 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
6876 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6877 );
6878 }
6879
6880 #[test]
6881 fn test_orphan_short_does_not_clobber_unrelated_global() {
6882 let run_cmd = SpecCommand::builder()
6887 .name("run")
6888 .flag(
6889 SpecFlag::builder()
6890 .name("raw")
6891 .short('r')
6892 .long("raw")
6893 .global(false)
6894 .build(),
6895 )
6896 .build();
6897 let mut cmd = SpecCommand::builder()
6898 .name("test")
6899 .flag(
6900 SpecFlag::builder()
6901 .name("raw")
6902 .long("raw")
6903 .global(true)
6904 .build(),
6905 )
6906 .flag(
6907 SpecFlag::builder()
6908 .name("restrict")
6909 .short('r')
6910 .long("restrict")
6911 .global(true)
6912 .build(),
6913 )
6914 .build();
6915 cmd.subcommands.insert("run".to_string(), run_cmd);
6916 let spec = Spec {
6917 name: "test".to_string(),
6918 bin: "test".to_string(),
6919 cmd,
6920 ..Default::default()
6921 };
6922
6923 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
6924 assert_eq!(
6926 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
6927 Some("restrict"),
6928 "-r must remain owned by the unrelated global it already belonged to",
6929 );
6930 assert!(parsed
6932 .available_flags
6933 .get("--raw")
6934 .is_some_and(|f| f.global));
6935 assert!(parsed
6936 .available_flags
6937 .get("--restrict")
6938 .is_some_and(|f| f.global));
6939 }
6940
6941 #[test]
6942 fn test_redeclared_global_aliases_share_one_flag() {
6943 let spec = r#"
6950flag "-y --yes" global=#true effect="write"
6951cmd "run" {
6952 flag "-y --yes --assume-yes"
6953}
6954"#
6955 .parse::<Spec>()
6956 .unwrap();
6957
6958 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
6959
6960 for key in ["-y", "--yes", "--assume-yes"] {
6961 let flag = parsed
6962 .available_flags
6963 .get(key)
6964 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
6965 assert!(flag.global, "{key} must stay global after the descent");
6966 assert_eq!(
6967 flag.long,
6968 vec!["yes".to_string(), "assume-yes".to_string()],
6969 "{key} must resolve to the flag carrying every alias",
6970 );
6971 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
6972 }
6973
6974 assert_eq!(
6976 unique_flags(parsed.available_flags.values()).count(),
6977 1,
6978 "all aliases must point at one flag object, got {:?}",
6979 parsed.available_flags,
6980 );
6981
6982 assert_eq!(
6984 parsed.available_flags["-y"].effect,
6985 Some(crate::SpecCommandEffect::Write),
6986 );
6987 }
6988
6989 #[test]
6990 fn test_redeclared_global_keeps_hidden_alias_metadata() {
6991 let spec = r#"
6992flag "--yes" global=#true {
6993 alias "-q" "--quietly" hide=#true
6994}
6995cmd "run" {
6996 flag "--yes --assume-yes" {
6997 alias "-s" "--secret" hide=#true
6998 }
6999}
7000"#
7001 .parse::<Spec>()
7002 .unwrap();
7003
7004 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7005 let merged = &parsed.available_flags["--yes"];
7006 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7007 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7008 for key in ["-q", "-s", "--quietly", "--secret"] {
7009 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7010 }
7011 }
7012
7013 #[test]
7014 fn test_redeclared_global_can_promote_hidden_aliases() {
7015 let spec = r#"
7016flag "--yes" global=#true {
7017 alias "-q" "--quietly" hide=#true
7018}
7019cmd "run" {
7020 flag "-q --yes --quietly"
7021}
7022"#
7023 .parse::<Spec>()
7024 .unwrap();
7025
7026 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7027 let merged = &parsed.available_flags["--yes"];
7028 assert!(merged.hidden_short_aliases.is_empty());
7029 assert!(merged.hidden_aliases.is_empty());
7030 for key in ["-q", "--quietly"] {
7031 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7032 }
7033 }
7034
7035 #[test]
7036 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7037 let spec = r#"
7043flag "-y --yes --confirm" global=#true
7044cmd "run" {
7045 flag "--yes --assume-yes"
7046}
7047"#
7048 .parse::<Spec>()
7049 .unwrap();
7050
7051 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7052
7053 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7054 let flag = parsed
7055 .available_flags
7056 .get(key)
7057 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7058 assert!(flag.global, "{key} must stay global after the descent");
7059 assert_eq!(
7060 flag.long,
7061 vec![
7062 "yes".to_string(),
7063 "confirm".to_string(),
7064 "assume-yes".to_string()
7065 ],
7066 "{key} must resolve to the flag carrying every alias",
7067 );
7068 }
7069
7070 assert_eq!(
7071 unique_flags(parsed.available_flags.values()).count(),
7072 1,
7073 "all aliases must point at one flag object, got {:?}",
7074 parsed.available_flags,
7075 );
7076 }
7077
7078 fn mounted_task_flag_spec() -> Spec {
7085 let mut task_cmd = SpecCommand::builder()
7086 .name("mytask")
7087 .flag(
7088 SpecFlag::builder()
7089 .name("env")
7090 .long("env")
7091 .arg(
7092 SpecArg::builder()
7093 .name("name")
7094 .choices(["dev", "stage", "prod"])
7095 .build(),
7096 )
7097 .global(false)
7098 .build(),
7099 )
7100 .flag(
7101 SpecFlag::builder()
7102 .name("bump")
7103 .long("bump")
7104 .arg(
7105 SpecArg::builder()
7106 .name("type")
7107 .choices(["auto", "major"])
7108 .build(),
7109 )
7110 .global(false)
7111 .build(),
7112 )
7113 .build();
7114 task_cmd.mounted = true;
7115
7116 let mut run_cmd = SpecCommand::builder()
7117 .name("run")
7118 .flag(
7119 SpecFlag::builder()
7120 .name("force")
7121 .short('f')
7122 .long("force")
7123 .global(false)
7124 .build(),
7125 )
7126 .build();
7127 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7128
7129 let mut cmd = SpecCommand::builder()
7130 .name("test")
7131 .flag(
7132 SpecFlag::builder()
7133 .name("env")
7134 .short('E')
7135 .long("env")
7136 .arg(SpecArg::builder().name("ENV").build())
7137 .global(true)
7138 .build(),
7139 )
7140 .flag(
7141 SpecFlag::builder()
7142 .name("silent")
7143 .long("silent")
7144 .global(true)
7145 .build(),
7146 )
7147 .build();
7148 cmd.subcommands.insert("run".to_string(), run_cmd);
7149
7150 Spec {
7151 name: "test".to_string(),
7152 bin: "test".to_string(),
7153 cmd,
7154 ..Default::default()
7155 }
7156 }
7157
7158 #[test]
7159 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7160 let deep = SpecCommand::builder().name("deep").build();
7166 let mut sub = SpecCommand::builder()
7167 .name("sub")
7168 .flag(
7170 SpecFlag::builder()
7171 .name("cd")
7172 .short('C')
7173 .long("cd")
7174 .arg(SpecArg::builder().name("dir").build())
7175 .global(false)
7176 .build(),
7177 )
7178 .build();
7179 sub.subcommands.insert("deep".to_string(), deep);
7180 let mut task = SpecCommand::builder()
7181 .name("task")
7182 .flag(
7183 SpecFlag::builder()
7184 .name("cd")
7185 .short('C')
7186 .long("cd")
7187 .arg(SpecArg::builder().name("dir").build())
7188 .global(true)
7189 .build(),
7190 )
7191 .build();
7192 task.subcommands.insert("sub".to_string(), sub);
7193 task.mark_mounted();
7194
7195 let mut run_cmd = SpecCommand::builder().name("run").build();
7196 run_cmd.subcommands.insert("task".to_string(), task);
7197 let mut cmd = SpecCommand::builder().name("test").build();
7198 cmd.subcommands.insert("run".to_string(), run_cmd);
7199 let spec = Spec {
7200 name: "test".to_string(),
7201 bin: "test".to_string(),
7202 cmd,
7203 ..Default::default()
7204 };
7205
7206 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7207 assert!(
7208 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7209 "the mounted program's own global must survive descents inside the mounted tree",
7210 );
7211 assert!(
7212 parsed.completion_flags().contains_key("--cd"),
7213 "and must still be offered there: it belongs to the mounted program",
7214 );
7215 assert!(
7216 parsed.completion_flags().contains_key("-C"),
7217 "including the short the nested command re-declared",
7218 );
7219 }
7220
7221 #[test]
7222 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7223 let mut task = SpecCommand::builder()
7228 .name("task")
7229 .flag(
7230 SpecFlag::builder()
7231 .name("bump")
7232 .long("bump")
7233 .global(false)
7234 .build(),
7235 )
7236 .build();
7237 task.mark_mounted();
7238
7239 let mut run_cmd = SpecCommand::builder().name("run").build();
7240 run_cmd.subcommands.insert("task".to_string(), task);
7241 run_cmd.flags = vec![
7243 SpecFlag::builder()
7244 .name("tglobal")
7245 .long("tglobal")
7246 .global(true)
7247 .build(),
7248 SpecFlag::builder()
7249 .name("tlocal")
7250 .long("tlocal")
7251 .global(false)
7252 .build(),
7253 ];
7254 run_cmd.flags_from_mount = true;
7255
7256 let mut cmd = SpecCommand::builder()
7257 .name("test")
7258 .flag(
7259 SpecFlag::builder()
7260 .name("silent")
7261 .long("silent")
7262 .global(true)
7263 .build(),
7264 )
7265 .build();
7266 cmd.subcommands.insert("run".to_string(), run_cmd);
7267 let spec = Spec {
7268 name: "test".to_string(),
7269 bin: "test".to_string(),
7270 cmd,
7271 ..Default::default()
7272 };
7273
7274 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7275 assert_eq!(
7276 parsed.completion_flags().keys().collect::<Vec<_>>(),
7277 vec!["--bump", "--tglobal"],
7278 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7279 `--silent` does not, and the mount's non-global root flag is not inherited",
7280 );
7281 }
7282
7283 #[test]
7284 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7285 let spec = mounted_task_flag_spec();
7290 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7291
7292 assert!(parsed.available_flags.contains_key("--silent"));
7294 assert!(parsed.available_flags.contains_key("-E"));
7295 assert_eq!(
7297 parsed.completion_flags().keys().collect::<Vec<_>>(),
7298 vec!["--bump", "--env"],
7299 "only the mounted command's own flags may be offered",
7300 );
7301
7302 assert!(!parsed.available_flags.contains_key("--force"));
7304 }
7305
7306 #[test]
7307 fn test_mounted_cmd_flag_wins_over_inherited_global() {
7308 let spec = mounted_task_flag_spec();
7311 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7312
7313 let awaiting = parsed
7314 .flag_awaiting_value
7315 .first()
7316 .expect("--env should await a value");
7317 assert_eq!(
7318 awaiting
7319 .arg
7320 .as_ref()
7321 .and_then(|a| a.choices.as_ref())
7322 .map(|c| c.choices.clone()),
7323 Some(vec![
7324 "dev".to_string(),
7325 "stage".to_string(),
7326 "prod".to_string()
7327 ]),
7328 "the mounted command's own --env must win over the inherited global",
7329 );
7330
7331 let parsed =
7334 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7335 assert!(
7336 parsed.args.is_empty(),
7337 "prefix global tokens must not be consumed as positionals, got {:?}",
7338 parsed.args
7339 );
7340 assert_eq!(
7341 parsed.as_env().get("usage_env").map(String::as_str),
7342 Some("anything"),
7343 );
7344 }
7345
7346 #[test]
7347 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7348 let spec = mounted_task_flag_spec();
7353 let parsed = parse_partial(
7354 &spec,
7355 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7356 )
7357 .unwrap();
7358 assert!(
7359 parsed.errors.is_empty(),
7360 "prefix global value must not be validated against the mounted flag: {:?}",
7361 parsed
7362 .errors
7363 .iter()
7364 .map(|e| e.to_string())
7365 .collect::<Vec<_>>(),
7366 );
7367 assert_eq!(
7368 parsed.as_env().get("usage_env").map(String::as_str),
7369 Some("not-a-task-choice"),
7370 );
7371
7372 let parsed = parse_partial(
7374 &spec,
7375 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7376 )
7377 .unwrap();
7378 assert!(parsed.errors.is_empty());
7379 assert_eq!(
7380 parsed.as_env().get("usage_env").map(String::as_str),
7381 Some("not-a-task-choice"),
7382 );
7383
7384 let parsed = parse_partial(
7387 &spec,
7388 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7389 )
7390 .unwrap();
7391 let awaiting = parsed
7392 .flag_awaiting_value
7393 .first()
7394 .expect("--env should await a value");
7395 assert_eq!(
7396 awaiting
7397 .arg
7398 .as_ref()
7399 .and_then(|a| a.choices.as_ref())
7400 .map(|c| c.choices.clone()),
7401 Some(vec![
7402 "dev".to_string(),
7403 "stage".to_string(),
7404 "prod".to_string()
7405 ]),
7406 "the mounted command's --env must own the name after the mounted command",
7407 );
7408 }
7409
7410 #[test]
7411 fn test_non_global_flag_does_not_hide_subcommand() {
7412 let spec = mounted_task_flag_spec();
7416
7417 for words in [
7418 &["test", "run", "--force", "mytask"][..],
7420 &["test", "run", "-f", "mytask"][..],
7421 &["test", "-E", "prod", "run", "--force", "mytask"][..],
7423 ] {
7424 let parsed = parse_partial(&spec, &input(words)).unwrap();
7425 assert_eq!(
7426 parsed
7427 .cmds
7428 .iter()
7429 .map(|c| c.name.as_str())
7430 .collect::<Vec<_>>(),
7431 vec!["test", "run", "mytask"],
7432 "{words:?} should descend into the mounted command",
7433 );
7434 assert!(
7435 parsed.args.is_empty(),
7436 "{words:?} should not consume a positional, got {:?}",
7437 parsed.args,
7438 );
7439 assert_eq!(
7440 parsed.as_env().get("usage_force").map(String::as_str),
7441 Some("true"),
7442 "the non-global flag must still be recorded for {words:?}",
7443 );
7444 }
7445
7446 let mut run_cmd = SpecCommand::builder()
7449 .name("run")
7450 .flag(
7451 SpecFlag::builder()
7452 .name("output")
7453 .short('o')
7454 .long("output")
7455 .arg(SpecArg::builder().name("mode").build())
7456 .global(false)
7457 .build(),
7458 )
7459 .build();
7460 run_cmd.subcommands.insert(
7461 "task".to_string(),
7462 SpecCommand::builder().name("task").build(),
7463 );
7464 let mut cmd = SpecCommand::builder().name("test").build();
7465 cmd.subcommands.insert("run".to_string(), run_cmd);
7466 let spec = Spec {
7467 name: "test".to_string(),
7468 bin: "test".to_string(),
7469 cmd,
7470 ..Default::default()
7471 };
7472
7473 let parsed =
7474 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
7475 assert_eq!(
7476 parsed
7477 .cmds
7478 .iter()
7479 .map(|c| c.name.as_str())
7480 .collect::<Vec<_>>(),
7481 vec!["test", "run", "task"],
7482 );
7483 assert_eq!(
7484 parsed.as_env().get("usage_output").map(String::as_str),
7485 Some("quiet"),
7486 );
7487
7488 assert_parse_err(
7491 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
7492 "unexpected word: --nope",
7493 );
7494 }
7495
7496 #[test]
7497 fn test_non_mounted_subcommand_offers_inherited_globals() {
7498 let mut run_cmd = SpecCommand::builder().name("run").build();
7501 run_cmd.subcommands.insert(
7502 "nested".to_string(),
7503 SpecCommand::builder().name("nested").build(),
7504 );
7505 let mut cmd = SpecCommand::builder()
7506 .name("test")
7507 .flag(
7508 SpecFlag::builder()
7509 .name("silent")
7510 .long("silent")
7511 .global(true)
7512 .build(),
7513 )
7514 .build();
7515 cmd.subcommands.insert("run".to_string(), run_cmd);
7516 let spec = Spec {
7517 name: "test".to_string(),
7518 bin: "test".to_string(),
7519 cmd,
7520 ..Default::default()
7521 };
7522
7523 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
7524 assert_eq!(
7525 parsed.completion_flags().keys().collect::<Vec<_>>(),
7526 parsed.available_flags.keys().collect::<Vec<_>>(),
7527 );
7528 assert!(parsed.completion_flags().contains_key("--silent"));
7529 }
7530
7531 #[test]
7532 fn test_subcommand_alias_collision_keeps_last_owner() {
7533 let run_cmd = SpecCommand::builder()
7537 .name("run")
7538 .flag(
7539 SpecFlag::builder()
7540 .name("alpha")
7541 .short('x')
7542 .long("alpha")
7543 .global(false)
7544 .build(),
7545 )
7546 .flag(
7547 SpecFlag::builder()
7548 .name("beta")
7549 .short('x')
7550 .long("beta")
7551 .global(false)
7552 .build(),
7553 )
7554 .build();
7555 let mut cmd = SpecCommand::builder().name("test").build();
7556 cmd.subcommands.insert("run".to_string(), run_cmd);
7557 let spec = Spec {
7558 name: "test".to_string(),
7559 bin: "test".to_string(),
7560 cmd,
7561 ..Default::default()
7562 };
7563
7564 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7565 assert_eq!(
7567 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
7568 Some("beta"),
7569 "the last-declared flag must keep a shared short alias",
7570 );
7571 assert_eq!(
7573 parsed
7574 .available_flags
7575 .get("--alpha")
7576 .map(|f| f.name.as_str()),
7577 Some("alpha"),
7578 );
7579 assert_eq!(
7580 parsed
7581 .available_flags
7582 .get("--beta")
7583 .map(|f| f.name.as_str()),
7584 Some("beta"),
7585 );
7586 }
7587
7588 #[test]
7589 fn test_default_subcommand_same_name_child() {
7590 let run_task = SpecCommand::builder()
7594 .name("run")
7595 .arg(SpecArg::builder().name("args").build())
7596 .build();
7597 let mut run_cmd = SpecCommand::builder().name("run").build();
7598 run_cmd.subcommands.insert("run".to_string(), run_task);
7599
7600 let mut cmd = SpecCommand::builder().name("test").build();
7601 cmd.subcommands.insert("run".to_string(), run_cmd);
7602
7603 let spec = Spec {
7604 name: "test".to_string(),
7605 bin: "test".to_string(),
7606 cmd,
7607 default_subcommand: Some("run".to_string()),
7608 ..Default::default()
7609 };
7610
7611 let input = vec!["test".to_string(), "run".to_string()];
7613 let parsed = parse(&spec, &input).unwrap();
7614
7615 assert_eq!(parsed.cmds.len(), 2);
7617 assert_eq!(parsed.cmds[0].name, "test");
7618 assert_eq!(parsed.cmds[1].name, "run");
7619
7620 let input = vec![
7622 "test".to_string(),
7623 "run".to_string(),
7624 "run".to_string(),
7625 "hello".to_string(),
7626 ];
7627 let parsed = parse(&spec, &input).unwrap();
7628
7629 assert_eq!(parsed.cmds.len(), 3);
7630 assert_eq!(parsed.cmds[0].name, "test");
7631 assert_eq!(parsed.cmds[1].name, "run");
7632 assert_eq!(parsed.cmds[2].name, "run");
7633 assert_eq!(parsed.args.len(), 1);
7634 let value = parsed.args.values().next().unwrap();
7635 assert_eq!(value.to_string(), "hello");
7636
7637 let mut run_cmd = SpecCommand::builder()
7641 .name("run")
7642 .arg(SpecArg::builder().name("task").build())
7643 .build();
7644 let run_task = SpecCommand::builder().name("run").build();
7645 run_cmd.subcommands.insert("run".to_string(), run_task);
7646
7647 let mut cmd = SpecCommand::builder().name("test").build();
7648 cmd.subcommands.insert("run".to_string(), run_cmd);
7649
7650 let spec = Spec {
7651 name: "test".to_string(),
7652 bin: "test".to_string(),
7653 cmd,
7654 default_subcommand: Some("run".to_string()),
7655 ..Default::default()
7656 };
7657
7658 let input = vec!["test".to_string(), "other".to_string()];
7659 let parsed = parse(&spec, &input).unwrap();
7660
7661 assert_eq!(parsed.cmds.len(), 2);
7664 assert_eq!(parsed.cmds[0].name, "test");
7665 assert_eq!(parsed.cmds[1].name, "run");
7666
7667 assert_eq!(parsed.args.len(), 1);
7669 let value = parsed.args.values().next().unwrap();
7670 assert_eq!(value.to_string(), "other");
7671 }
7672
7673 #[test]
7674 fn test_restart_token() {
7675 let run_cmd = SpecCommand::builder()
7677 .name("run")
7678 .arg(SpecArg::builder().name("task").build())
7679 .restart_token(":::".to_string())
7680 .build();
7681 let mut cmd = SpecCommand::builder().name("test").build();
7682 cmd.subcommands.insert("run".to_string(), run_cmd);
7683
7684 let spec = Spec {
7685 name: "test".to_string(),
7686 bin: "test".to_string(),
7687 cmd,
7688 ..Default::default()
7689 };
7690
7691 let input = vec![
7693 "test".to_string(),
7694 "run".to_string(),
7695 "task1".to_string(),
7696 ":::".to_string(),
7697 "task2".to_string(),
7698 ];
7699 let parsed = parse(&spec, &input).unwrap();
7700
7701 assert_eq!(parsed.args.len(), 1);
7703 let value = parsed.args.values().next().unwrap();
7704 assert_eq!(value.to_string(), "task2");
7705 }
7706
7707 #[test]
7708 fn test_restart_token_multiple() {
7709 let run_cmd = SpecCommand::builder()
7711 .name("run")
7712 .arg(SpecArg::builder().name("task").build())
7713 .restart_token(":::".to_string())
7714 .build();
7715 let mut cmd = SpecCommand::builder().name("test").build();
7716 cmd.subcommands.insert("run".to_string(), run_cmd);
7717
7718 let spec = Spec {
7719 name: "test".to_string(),
7720 bin: "test".to_string(),
7721 cmd,
7722 ..Default::default()
7723 };
7724
7725 let input = vec![
7727 "test".to_string(),
7728 "run".to_string(),
7729 "task1".to_string(),
7730 ":::".to_string(),
7731 "task2".to_string(),
7732 ":::".to_string(),
7733 "task3".to_string(),
7734 ];
7735 let parsed = parse(&spec, &input).unwrap();
7736
7737 assert_eq!(parsed.args.len(), 1);
7739 let value = parsed.args.values().next().unwrap();
7740 assert_eq!(value.to_string(), "task3");
7741 }
7742
7743 #[test]
7744 fn test_restart_token_clears_flag_awaiting_value() {
7745 let run_cmd = SpecCommand::builder()
7747 .name("run")
7748 .arg(SpecArg::builder().name("task").build())
7749 .flag(
7750 SpecFlag::builder()
7751 .name("jobs")
7752 .long("jobs")
7753 .arg(SpecArg::builder().name("count").build())
7754 .build(),
7755 )
7756 .restart_token(":::".to_string())
7757 .build();
7758 let mut cmd = SpecCommand::builder().name("test").build();
7759 cmd.subcommands.insert("run".to_string(), run_cmd);
7760
7761 let spec = Spec {
7762 name: "test".to_string(),
7763 bin: "test".to_string(),
7764 cmd,
7765 ..Default::default()
7766 };
7767
7768 let input = vec![
7770 "test".to_string(),
7771 "run".to_string(),
7772 "task1".to_string(),
7773 "--jobs".to_string(),
7774 ":::".to_string(),
7775 "task2".to_string(),
7776 ];
7777 let parsed = parse(&spec, &input).unwrap();
7778
7779 assert_eq!(parsed.args.len(), 1);
7781 let value = parsed.args.values().next().unwrap();
7782 assert_eq!(value.to_string(), "task2");
7783 assert!(parsed.flag_awaiting_value.is_empty());
7785 }
7786
7787 #[test]
7788 fn test_restart_token_resets_double_dash() {
7789 let run_cmd = SpecCommand::builder()
7791 .name("run")
7792 .arg(SpecArg::builder().name("task").build())
7793 .arg(SpecArg::builder().name("extra_args").var(true).build())
7794 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
7795 .restart_token(":::".to_string())
7796 .build();
7797 let mut cmd = SpecCommand::builder().name("test").build();
7798 cmd.subcommands.insert("run".to_string(), run_cmd);
7799
7800 let spec = Spec {
7801 name: "test".to_string(),
7802 bin: "test".to_string(),
7803 cmd,
7804 ..Default::default()
7805 };
7806
7807 let input = vec![
7809 "test".to_string(),
7810 "run".to_string(),
7811 "task1".to_string(),
7812 "--".to_string(),
7813 "extra".to_string(),
7814 ":::".to_string(),
7815 "--verbose".to_string(),
7816 "task2".to_string(),
7817 ];
7818 let parsed = parse(&spec, &input).unwrap();
7819
7820 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
7822 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7824 let value = parsed.args.get(task_arg).unwrap();
7825 assert_eq!(value.to_string(), "task2");
7826 }
7827
7828 #[test]
7829 fn test_double_dashes_without_preserve() {
7830 let run_cmd = SpecCommand::builder()
7835 .name("run")
7836 .arg(SpecArg::builder().name("args").var(true).build())
7837 .build();
7838 let mut cmd = SpecCommand::builder().name("test").build();
7839 cmd.subcommands.insert("run".to_string(), run_cmd);
7840
7841 let spec = Spec {
7842 name: "test".to_string(),
7843 bin: "test".to_string(),
7844 cmd,
7845 ..Default::default()
7846 };
7847
7848 let input = vec![
7850 "test".to_string(),
7851 "run".to_string(),
7852 "arg1".to_string(),
7853 "--".to_string(),
7854 "arg2".to_string(),
7855 "--".to_string(),
7856 "arg3".to_string(),
7857 ];
7858 let parsed = parse(&spec, &input).unwrap();
7859
7860 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7861 let value = parsed.args.get(args_arg).unwrap();
7862 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
7863 }
7864
7865 #[test]
7866 fn test_double_dashes_with_preserve() {
7867 let run_cmd = SpecCommand::builder()
7869 .name("run")
7870 .arg(
7871 SpecArg::builder()
7872 .name("args")
7873 .var(true)
7874 .double_dash(SpecDoubleDashChoices::Preserve)
7875 .build(),
7876 )
7877 .build();
7878 let mut cmd = SpecCommand::builder().name("test").build();
7879 cmd.subcommands.insert("run".to_string(), run_cmd);
7880
7881 let spec = Spec {
7882 name: "test".to_string(),
7883 bin: "test".to_string(),
7884 cmd,
7885 ..Default::default()
7886 };
7887
7888 let input = vec![
7890 "test".to_string(),
7891 "run".to_string(),
7892 "arg1".to_string(),
7893 "--".to_string(),
7894 "arg2".to_string(),
7895 "--".to_string(),
7896 "arg3".to_string(),
7897 ];
7898 let parsed = parse(&spec, &input).unwrap();
7899
7900 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7901 let value = parsed.args.get(args_arg).unwrap();
7902 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
7903 }
7904
7905 #[test]
7906 fn test_double_dashes_with_preserve_only_dashes() {
7907 let run_cmd = SpecCommand::builder()
7910 .name("run")
7911 .arg(
7912 SpecArg::builder()
7913 .name("args")
7914 .var(true)
7915 .double_dash(SpecDoubleDashChoices::Preserve)
7916 .build(),
7917 )
7918 .build();
7919 let mut cmd = SpecCommand::builder().name("test").build();
7920 cmd.subcommands.insert("run".to_string(), run_cmd);
7921
7922 let spec = Spec {
7923 name: "test".to_string(),
7924 bin: "test".to_string(),
7925 cmd,
7926 ..Default::default()
7927 };
7928
7929 let input = vec![
7931 "test".to_string(),
7932 "run".to_string(),
7933 "--".to_string(),
7934 "--".to_string(),
7935 ];
7936 let parsed = parse(&spec, &input).unwrap();
7937
7938 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7939 let value = parsed.args.get(args_arg).unwrap();
7940 assert_eq!(value.to_string(), "-- --");
7941 }
7942
7943 #[test]
7944 fn test_double_dashes_with_preserve_multiple_args() {
7945 let run_cmd = SpecCommand::builder()
7947 .name("run")
7948 .arg(SpecArg::builder().name("task").build())
7949 .arg(
7950 SpecArg::builder()
7951 .name("extra_args")
7952 .var(true)
7953 .double_dash(SpecDoubleDashChoices::Preserve)
7954 .build(),
7955 )
7956 .build();
7957 let mut cmd = SpecCommand::builder().name("test").build();
7958 cmd.subcommands.insert("run".to_string(), run_cmd);
7959
7960 let spec = Spec {
7961 name: "test".to_string(),
7962 bin: "test".to_string(),
7963 cmd,
7964 ..Default::default()
7965 };
7966
7967 let input = vec![
7970 "test".to_string(),
7971 "run".to_string(),
7972 "task1".to_string(),
7973 "--".to_string(),
7974 "arg1".to_string(),
7975 "--".to_string(),
7976 "--foo".to_string(),
7977 ];
7978 let parsed = parse(&spec, &input).unwrap();
7979
7980 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7981 let task_value = parsed.args.get(task_arg).unwrap();
7982 assert_eq!(task_value.to_string(), "task1");
7983
7984 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
7985 let extra_value = parsed.args.get(extra_arg).unwrap();
7986 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
7987 }
7988
7989 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
7990 let cmd = SpecCommand::builder().name("test").args(args).build();
7991 Spec {
7992 name: "test".to_string(),
7993 bin: "test".to_string(),
7994 cmd,
7995 ..Default::default()
7996 }
7997 }
7998
7999 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8000 let arg = parsed
8001 .args
8002 .keys()
8003 .find(|a| a.name == name)
8004 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8005 parsed.args.get(arg).unwrap().to_string()
8006 }
8007
8008 fn required_arg(name: &str) -> SpecArg {
8009 SpecArg::builder()
8010 .name(name)
8011 .var(true)
8012 .required(false)
8013 .double_dash(SpecDoubleDashChoices::Required)
8014 .build()
8015 }
8016
8017 #[test]
8018 fn test_double_dash_required_reports_error_once_for_variadic() {
8019 let spec = spec_with_args([required_arg("files")]);
8021
8022 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8023
8024 assert!(parsed.args.is_empty());
8025 assert_eq!(parsed.errors.len(), 1);
8026 assert!(
8027 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8028 );
8029 }
8030
8031 #[test]
8032 fn test_double_dash_required_suppresses_missing_arg() {
8033 let spec = spec_with_args([SpecArg::builder()
8035 .name("file")
8036 .required(true)
8037 .double_dash(SpecDoubleDashChoices::Required)
8038 .build()]);
8039
8040 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8041
8042 assert_eq!(parsed.errors.len(), 1);
8043 assert!(matches!(
8044 &parsed.errors[0],
8045 UsageErr::ArgRequiresDoubleDash(_)
8046 ));
8047 assert_eq!(
8049 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8050 Some("file")
8051 );
8052 assert!(!parsed.double_dash_seen);
8053 }
8054
8055 #[test]
8056 fn test_double_dash_routes_to_required_arg() {
8057 let spec = spec_with_args([
8060 SpecArg::builder()
8061 .name("tool")
8062 .var(true)
8063 .required(false)
8064 .build(),
8065 required_arg("command"),
8066 ]);
8067
8068 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8069
8070 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8071 assert_eq!(arg_value(&parsed, "command"), "node app.js");
8072 assert!(parsed.double_dash_seen);
8073 }
8074
8075 #[test]
8076 fn test_double_dash_routes_with_gap_reports_missing_arg() {
8077 let spec = spec_with_args([
8080 SpecArg::builder()
8081 .name("tool")
8082 .var(true)
8083 .required(true)
8084 .build(),
8085 required_arg("command"),
8086 ]);
8087
8088 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8089
8090 assert_eq!(arg_value(&parsed, "command"), "ls");
8091 assert!(parsed.args.keys().all(|a| a.name != "tool"));
8092 assert!(parsed
8093 .errors
8094 .iter()
8095 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8096 }
8097
8098 #[test]
8099 fn test_double_dash_gap_applies_defaults() {
8100 let spec = spec_with_args([
8102 SpecArg::builder()
8103 .name("tool")
8104 .var(true)
8105 .required(false)
8106 .default_value("node@20")
8107 .build(),
8108 required_arg("command"),
8109 ]);
8110
8111 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8112
8113 assert_eq!(arg_value(&parsed, "command"), "ls");
8114 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8115 }
8116
8117 fn spec_with_restart_token_and_required_arg() -> Spec {
8118 let run_cmd = SpecCommand::builder()
8119 .name("run")
8120 .arg(SpecArg::builder().name("task").build())
8121 .arg(required_arg("run_args"))
8122 .restart_token(":::".to_string())
8123 .build();
8124 let mut cmd = SpecCommand::builder().name("test").build();
8125 cmd.subcommands.insert("run".to_string(), run_cmd);
8126 Spec {
8127 name: "test".to_string(),
8128 bin: "test".to_string(),
8129 cmd,
8130 ..Default::default()
8131 }
8132 }
8133
8134 #[test]
8135 fn test_double_dash_required_restart_token_resets_separator() {
8136 let spec = spec_with_restart_token_and_required_arg();
8138
8139 let parsed = parse_partial(
8140 &spec,
8141 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8142 )
8143 .unwrap();
8144
8145 assert_eq!(arg_value(&parsed, "task"), "task2");
8146 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8147 assert_eq!(
8149 parsed
8150 .errors
8151 .iter()
8152 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8153 .count(),
8154 1
8155 );
8156 }
8157
8158 #[test]
8159 fn test_double_dash_required_restart_token_accepts_new_separator() {
8160 let spec = spec_with_restart_token_and_required_arg();
8161
8162 let parsed = parse(
8163 &spec,
8164 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8165 )
8166 .unwrap();
8167
8168 assert_eq!(arg_value(&parsed, "task"), "task2");
8169 assert_eq!(arg_value(&parsed, "run_args"), "c");
8170 }
8171
8172 #[test]
8173 fn test_double_dash_preserve_is_not_a_separator() {
8174 let spec = spec_with_args([
8177 SpecArg::builder()
8178 .name("kept")
8179 .var(true)
8180 .var_max(1)
8181 .required(false)
8182 .double_dash(SpecDoubleDashChoices::Preserve)
8183 .build(),
8184 required_arg("rest"),
8185 ]);
8186
8187 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8188
8189 assert_eq!(arg_value(&parsed, "kept"), "--");
8190 assert!(parsed.args.keys().all(|a| a.name != "rest"));
8191 assert!(!parsed.double_dash_seen);
8192 assert_eq!(parsed.errors.len(), 1);
8193 }
8194
8195 #[test]
8196 fn test_double_dash_required_does_not_bail_in_parse_partial() {
8197 let spec = spec_with_args([required_arg("file")]);
8199
8200 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8201 assert!(parse(&spec, &input(&["test", "x"])).is_err());
8202 }
8203
8204 #[test]
8205 fn test_double_dash_without_required_arg_does_not_move_cursor() {
8206 let spec = spec_with_args([
8208 SpecArg::builder().name("first").required(false).build(),
8209 SpecArg::builder().name("second").required(false).build(),
8210 ]);
8211
8212 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8213
8214 assert_eq!(arg_value(&parsed, "first"), "a");
8215 assert_eq!(arg_value(&parsed, "second"), "b");
8216 assert!(parsed.next_arg.is_none());
8217 }
8218
8219 #[test]
8220 fn test_parser_with_custom_env_for_required_arg() {
8221 let spec = spec_with_arg(
8222 SpecArg::builder()
8223 .name("name")
8224 .env("NAME")
8225 .required(true)
8226 .build(),
8227 );
8228 std::env::remove_var("NAME");
8229
8230 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8231 .expect("parse should succeed with custom env");
8232 assert_eq!(parsed.args.len(), 1);
8233 assert_eq!(first_string_value(&parsed), "john");
8234 }
8235
8236 #[test]
8237 fn test_parser_with_custom_env_for_required_flag() {
8238 let spec = spec_with_flag(
8239 SpecFlag::builder()
8240 .long("name")
8241 .env("NAME")
8242 .required(true)
8243 .arg(SpecArg::builder().name("name").build())
8244 .build(),
8245 );
8246 std::env::remove_var("NAME");
8247
8248 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8249 .expect("parse should succeed with custom env");
8250 assert_eq!(parsed.flags.len(), 1);
8251 assert_eq!(first_string_value(&parsed), "jane");
8252 }
8253
8254 #[test]
8255 fn test_flag_environment_fallbacks_preserve_declaration_order() {
8256 let spec = spec_with_flag(
8257 SpecFlag::builder()
8258 .long("name")
8259 .env("NAME")
8260 .env_fallback("OLD_NAME")
8261 .env_fallback("OLDER_NAME")
8262 .deprecated_env("DEPRECATED_NAME")
8263 .arg(SpecArg::builder().name("name").build())
8264 .build(),
8265 );
8266
8267 let parsed = parse_with_env(
8268 &spec,
8269 &["test"],
8270 &[
8271 ("NAME", "canonical"),
8272 ("OLD_NAME", "fallback"),
8273 ("DEPRECATED_NAME", "deprecated"),
8274 ],
8275 )
8276 .unwrap();
8277 assert_eq!(first_string_value(&parsed), "canonical");
8278
8279 let parsed = parse_with_env(
8280 &spec,
8281 &["test"],
8282 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8283 )
8284 .unwrap();
8285 assert_eq!(first_string_value(&parsed), "old");
8286
8287 let parsed =
8288 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8289 assert_eq!(first_string_value(&parsed), "deprecated");
8290 }
8291
8292 #[test]
8293 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8294 let spec = spec_with_flag(
8295 SpecFlag::builder()
8296 .long("name")
8297 .env("NAME")
8298 .deprecated_env("DEPRECATED_NAME")
8299 .arg(SpecArg::builder().name("name").build())
8300 .build(),
8301 );
8302
8303 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8305 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8306
8307 let parsed =
8308 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8309 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8310 assert_eq!(
8311 parsed.warnings[0].kind,
8312 crate::warn::WarningKind::DeprecatedEnv
8313 );
8314 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8315 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8316 assert_eq!(first_string_value(&parsed), "deprecated");
8318 }
8319
8320 #[test]
8321 fn a_deprecated_flag_reports_only_when_it_was_used() {
8322 let spec = spec_with_flag(
8323 SpecFlag::builder()
8324 .long("output")
8325 .deprecated("use --out")
8326 .deprecated_remove_at("3.0.0")
8327 .arg(SpecArg::builder().name("output").build())
8328 .build(),
8329 );
8330
8331 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8332 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8333
8334 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8335 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8336 assert_eq!(
8337 parsed.warnings[0].kind,
8338 crate::warn::WarningKind::DeprecatedFlag
8339 );
8340 assert_eq!(parsed.warnings[0].name, "--output");
8342 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8343 assert_eq!(
8344 parsed.warnings[0].render(),
8345 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8346 );
8347 }
8348
8349 #[test]
8350 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8351 let flag = SpecFlag::builder()
8352 .long("output")
8353 .deprecated("use --out")
8354 .deprecated_warn_at("9.0.0")
8355 .arg(SpecArg::builder().name("output").build())
8356 .build();
8357 let mut spec = spec_with_flag(flag);
8358 spec.version = Some("2.0.0".to_string());
8359
8360 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8361 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8362
8363 spec.version = Some("9.0.0".to_string());
8365 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8366 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8367 }
8368
8369 #[test]
8370 fn test_parser_with_custom_env_still_fails_when_missing() {
8371 let spec = spec_with_arg(
8372 SpecArg::builder()
8373 .name("name")
8374 .env("NAME")
8375 .required(true)
8376 .build(),
8377 );
8378 std::env::remove_var("NAME");
8379 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8380 }
8381
8382 #[test]
8383 fn test_parser_does_not_treat_env_choice_value_as_help() {
8384 let spec = spec_with_arg(
8385 SpecArg::builder()
8386 .name("env")
8387 .env("CURRENT_ENV")
8388 .choices(["dev", "staging"])
8389 .required(false)
8390 .build(),
8391 );
8392
8393 assert_parse_err(
8394 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8395 "Invalid choice for arg env: --help, expected one of dev, staging",
8396 );
8397 }
8398
8399 #[test]
8400 fn test_parser_does_not_treat_default_choice_value_as_help() {
8401 let spec = spec_with_flag(
8402 SpecFlag::builder()
8403 .long("env")
8404 .arg(
8405 SpecArg::builder()
8406 .name("env")
8407 .choices(["dev", "staging"])
8408 .build(),
8409 )
8410 .default_value("--help")
8411 .build(),
8412 );
8413
8414 assert_parse_err(
8415 parse_with_env(&spec, &["test"], &[]),
8416 "Invalid choice for option env: --help, expected one of dev, staging",
8417 );
8418 }
8419
8420 fn words(of: &[&str]) -> Vec<String> {
8422 of.iter().map(|s| s.to_string()).collect()
8423 }
8424
8425 #[test]
8426 fn a_command_that_needs_a_subcommand_says_so() {
8427 let spec: Spec = r#"
8431name "ex"
8432bin "ex"
8433cmd "gen" subcommand_required=#true {
8434 cmd "two" {}
8435 cmd "one" {}
8436 cmd "secret" hide=#true {}
8437 alias "g"
8438}
8439cmd "open" {
8440 cmd "sub" {}
8441}
8442"#
8443 .parse()
8444 .unwrap();
8445
8446 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
8447 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
8451
8452 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
8454 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
8455
8456 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
8458
8459 parse(&spec, &words(&["ex", "open"])).unwrap();
8462 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
8463 }
8464
8465 #[test]
8466 fn arg_required_else_help_observes_the_selected_commands_argv() {
8467 let spec: Spec = r#"
8468name "ex"
8469bin "ex"
8470flag "--verbose" global=#true
8471cmd "run" arg_required_else_help=#true {
8472 flag "--all"
8473}
8474"#
8475 .parse()
8476 .unwrap();
8477 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
8478
8479 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
8480 assert!(err.to_string().contains("Usage: ex run"), "{err}");
8481
8482 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
8485 assert!(err.to_string().contains("Usage: ex run"), "{err}");
8486
8487 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
8488 }
8489
8490 #[test]
8491 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
8492 let spec: Spec = r#"
8493name "ex"
8494bin "ex"
8495unknown_flags "error"
8496external_subcommand #true
8497cmd "install"
8498flag "-v --verbose" global=#true
8499"#
8500 .parse()
8501 .unwrap();
8502
8503 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
8504 assert_eq!(
8505 parsed.external,
8506 Some(vec!["foo".into(), "--help".into(), "bar".into()])
8507 );
8508 assert!(parsed.flags.is_empty());
8509
8510 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
8512 assert_eq!(parsed.cmd.name, "install");
8513 assert!(parsed.external.is_none());
8514
8515 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
8517 assert_eq!(
8518 parsed.external,
8519 Some(vec!["foo".into(), "--verbose".into()])
8520 );
8521 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
8522
8523 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
8525
8526 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
8530 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
8531 }
8532
8533 #[test]
8534 fn an_external_subcommand_satisfies_subcommand_required() {
8535 let mut spec: Spec = r#"
8536name "ex"
8537bin "ex"
8538external_subcommand #true
8539cmd "install"
8540"#
8541 .parse()
8542 .unwrap();
8543 spec.cmd.subcommand_required = true;
8544
8545 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
8546 assert!(parse(&spec, &input(&["ex"])).is_err());
8547 }
8548
8549 #[test]
8550 fn a_default_subcommand_outranks_an_external_one() {
8551 let spec: Spec = r#"
8552name "ex"
8553bin "ex"
8554default_subcommand "run"
8555external_subcommand #true
8556cmd "run" {
8557 arg "[task]"
8558}
8559"#
8560 .parse()
8561 .unwrap();
8562
8563 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
8564 assert_eq!(parsed.cmd.name, "run");
8565 assert!(parsed.external.is_none());
8566 assert_eq!(first_string_value(&parsed), "build");
8567 }
8568
8569 #[test]
8570 fn multicall_basename_strips_a_path_and_exe() {
8571 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
8572 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
8573 assert_eq!(multicall_basename("LS.EXE"), "LS");
8574 assert_eq!(multicall_basename("busybox"), "busybox");
8575 }
8576
8577 #[test]
8578 fn a_multicall_applet_is_the_first_word() {
8579 let spec: Spec = r#"
8580name "busybox"
8581bin "busybox"
8582multicall #true
8583cmd "ls" {
8584 arg "[ARGS]" var=#true
8585}
8586cmd "cat"
8587"#
8588 .parse()
8589 .unwrap();
8590
8591 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
8593 assert_eq!(parsed.cmd.name, "ls");
8594 match parsed.args.values().next() {
8595 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
8596 other => panic!("expected ARGS to collect -l, got {other:?}"),
8597 }
8598
8599 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
8601 assert_eq!(parsed.cmd.name, "ls");
8602
8603 let mut configured = spec.clone();
8605 configured.name = "BusyBox".to_string();
8606 configured.bin = "/opt/bin/busybox.exe".to_string();
8607 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
8608 assert_eq!(parsed.cmd.name, "ls");
8609
8610 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
8612 assert_eq!(parsed.cmd.name, "ls");
8613
8614 let mut plain = spec.clone();
8616 plain.multicall = false;
8617 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
8618 assert_eq!(parsed.cmd.name, "ls");
8619 }
8620
8621 #[test]
8622 fn a_multicall_unknown_applet_can_be_external() {
8623 let spec: Spec = r#"
8624name "busybox"
8625bin "busybox"
8626multicall #true
8627unknown_flags "error"
8628external_subcommand #true
8629cmd "ls"
8630"#
8631 .parse()
8632 .unwrap();
8633
8634 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
8635 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
8636
8637 let mut closed = spec.clone();
8638 closed.cmd.external_subcommand = false;
8639 assert!(parse(&closed, &input(&["wat"])).is_err());
8640 }
8641
8642 #[cfg(feature = "unstable_choices_env")]
8643 #[test]
8644 fn test_parser_arg_choices_from_custom_env() {
8645 let spec = spec_arg_choices_env("DEPLOY_ENVS");
8646
8647 let parsed =
8648 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
8649 assert_eq!(first_string_value(&parsed), "bar");
8650
8651 assert_parse_err(
8652 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
8653 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
8654 );
8655 assert_parse_err(
8656 parse_with_env(&spec, &["test", "prod"], &[]),
8657 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
8658 );
8659 }
8660
8661 #[cfg(feature = "unstable_choices_env")]
8662 #[test]
8663 fn test_parser_validates_flag_choices_from_custom_env() {
8664 let spec = spec_flag_choices_env("DEPLOY_ENVS");
8665 let parsed = parse_with_env(
8666 &spec,
8667 &["test", "--env", "baz"],
8668 &[("DEPLOY_ENVS", "foo,bar baz")],
8669 )
8670 .unwrap();
8671 assert_eq!(first_string_value(&parsed), "baz");
8672 }
8673
8674 #[cfg(feature = "unstable_choices_env")]
8675 #[test]
8676 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
8677 let arg_env_spec = spec_with_arg(
8678 SpecArg::builder()
8679 .name("env")
8680 .env("CURRENT_ENV")
8681 .choices_env("DEPLOY_ENVS")
8682 .build(),
8683 );
8684 assert_parse_err(
8685 parse_with_env(
8686 &arg_env_spec,
8687 &["test"],
8688 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
8689 ),
8690 "Invalid choice for arg env: prod, expected one of dev, staging",
8691 );
8692
8693 let flag_default_spec = spec_with_flag(
8694 SpecFlag::builder()
8695 .long("env")
8696 .arg(
8697 SpecArg::builder()
8698 .name("env")
8699 .choices_env("DEPLOY_ENVS")
8700 .build(),
8701 )
8702 .default_value("prod")
8703 .build(),
8704 );
8705 assert_parse_err(
8706 parse_with_env(
8707 &flag_default_spec,
8708 &["test"],
8709 &[("DEPLOY_ENVS", "dev,staging")],
8710 ),
8711 "Invalid choice for option env: prod, expected one of dev, staging",
8712 );
8713 }
8714
8715 #[test]
8716 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
8717 let spec: Spec = r#"
8718 flag "-v --verbose" var=#true
8719 arg "[database]" default="myapp_dev"
8720 arg "[args...]"
8721 "#
8722 .parse()
8723 .unwrap();
8724 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8725 .into_iter()
8726 .map(String::from)
8727 .collect();
8728 let parsed = parse(&spec, &input).unwrap();
8729 let env = parsed.as_env();
8730 assert_eq!(env.get("usage_database").unwrap(), "mydb");
8731 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
8732 }
8733
8734 #[test]
8735 fn test_variadic_arg_captures_unknown_flags() {
8736 let cmd = SpecCommand::builder()
8737 .name("test")
8738 .flag(SpecFlag::builder().short('v').long("verbose").build())
8739 .arg(SpecArg::builder().name("database").required(false).build())
8740 .arg(
8741 SpecArg::builder()
8742 .name("args")
8743 .required(false)
8744 .var(true)
8745 .build(),
8746 )
8747 .build();
8748 let spec = Spec {
8749 name: "test".to_string(),
8750 bin: "test".to_string(),
8751 cmd,
8752 ..Default::default()
8753 };
8754
8755 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8757 .into_iter()
8758 .map(String::from)
8759 .collect();
8760 let parsed = parse(&spec, &input).unwrap();
8761 assert_eq!(parsed.args.len(), 2);
8762 let args_val = parsed
8763 .args
8764 .iter()
8765 .find(|(a, _)| a.name == "args")
8766 .unwrap()
8767 .1;
8768 match args_val {
8769 ParseValue::MultiString(v) => {
8770 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8771 }
8772 _ => panic!("Expected MultiString, got {:?}", args_val),
8773 }
8774 }
8775
8776 #[test]
8777 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
8778 let cmd = SpecCommand::builder()
8779 .name("test")
8780 .flag(SpecFlag::builder().short('v').long("verbose").build())
8781 .arg(SpecArg::builder().name("database").required(false).build())
8782 .arg(
8783 SpecArg::builder()
8784 .name("args")
8785 .required(false)
8786 .var(true)
8787 .build(),
8788 )
8789 .build();
8790 let spec = Spec {
8791 name: "test".to_string(),
8792 bin: "test".to_string(),
8793 cmd,
8794 ..Default::default()
8795 };
8796
8797 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
8799 .into_iter()
8800 .map(String::from)
8801 .collect();
8802 let parsed = parse(&spec, &input).unwrap();
8803 assert_eq!(parsed.args.len(), 2);
8804 let args_val = parsed
8805 .args
8806 .iter()
8807 .find(|(a, _)| a.name == "args")
8808 .unwrap()
8809 .1;
8810 match args_val {
8811 ParseValue::MultiString(v) => {
8812 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8813 }
8814 _ => panic!("Expected MultiString, got {:?}", args_val),
8815 }
8816 }
8817
8818 #[test]
8819 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
8820 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
8823
8824 let input: Vec<String> = vec!["test", "--option=3"]
8827 .into_iter()
8828 .map(String::from)
8829 .collect();
8830 let parsed = parse(&spec, &input).unwrap();
8831 let env = parsed.as_env();
8832 assert_eq!(
8833 env.get("usage_other_args").map(String::as_str),
8834 Some("'--option=3'"),
8835 "expected a single --option=3 token, got {:?}",
8836 env.get("usage_other_args"),
8837 );
8838
8839 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
8841 .into_iter()
8842 .map(String::from)
8843 .collect();
8844 let parsed2 = parse(&spec, &input2).unwrap();
8845 let env2 = parsed2.as_env();
8846 assert_eq!(
8847 env2.get("usage_other_args").map(String::as_str),
8848 Some("'--foo=bar' '--baz=qux'"),
8849 "expected two intact tokens, got {:?}",
8850 env2.get("usage_other_args"),
8851 );
8852
8853 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
8855 .into_iter()
8856 .map(String::from)
8857 .collect();
8858 let parsed3 = parse(&spec, &input3).unwrap();
8859 let env3 = parsed3.as_env();
8860 assert_eq!(
8861 env3.get("usage_other_args").map(String::as_str),
8862 Some("positional1 '--option=3' positional2"),
8863 "expected positional args and intact flag token, got {:?}",
8864 env3.get("usage_other_args"),
8865 );
8866 }
8867
8868 #[test]
8869 fn test_allow_hyphen_values_consumes_short_flag_collision() {
8870 let spec = r#"
8871flag "-d --working-dir <DIR>"
8872flag "-a --args <ARGS>" allow_hyphen_values=#true
8873"#
8874 .parse::<Spec>()
8875 .unwrap();
8876
8877 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
8878
8879 assert_eq!(parsed.flags.len(), 1);
8880 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8881 }
8882
8883 #[test]
8884 fn test_allow_hyphen_values_consumes_embedded_long_value() {
8885 let spec = r#"
8886flag "-d --working-dir <DIR>"
8887flag "-a --args <ARGS>" allow_hyphen_values=#true
8888"#
8889 .parse::<Spec>()
8890 .unwrap();
8891
8892 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
8893
8894 assert_eq!(parsed.flags.len(), 1);
8895 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8896 }
8897
8898 #[test]
8899 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
8900 let spec = r#"
8905flag "-a --args <ARGS>" allow_hyphen_values=#true
8906arg "[rest]..."
8907"#
8908 .parse::<Spec>()
8909 .unwrap();
8910
8911 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
8912
8913 assert_eq!(flag_string_value(&parsed, "args"), "--");
8914 let rest = parsed
8915 .args
8916 .values()
8917 .next()
8918 .expect("expected the word after the separator to reach the argument");
8919 assert_eq!(rest.to_string(), "-x");
8920 }
8921
8922 #[test]
8923 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
8924 let spec = r#"
8929flag "-a --args <ARGS>..." allow_hyphen_values=#true
8930"#
8931 .parse::<Spec>()
8932 .unwrap();
8933
8934 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
8935
8936 let flag = parsed
8937 .flags
8938 .keys()
8939 .find(|flag| flag.name == "args")
8940 .expect("expected args flag");
8941 match parsed.flags.get(flag).expect("expected args value") {
8942 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
8943 other => panic!("expected a list of values, got {other:?}"),
8944 }
8945 }
8946
8947 #[test]
8948 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
8949 let spec = r#"
8950flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
8951"#
8952 .parse::<Spec>()
8953 .unwrap();
8954
8955 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
8956
8957 let flag = parsed
8958 .flags
8959 .keys()
8960 .find(|flag| flag.name == "args")
8961 .expect("expected args flag");
8962 let value = parsed.flags.get(flag).expect("expected args value");
8963 match value {
8964 ParseValue::MultiString(values) => {
8965 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
8966 }
8967 _ => panic!("expected MultiString, got {value:?}"),
8968 }
8969 }
8970
8971 #[test]
8972 fn test_require_equals_accepts_attached_and_refuses_detached() {
8973 let spec = r#"
8974flag "--inspect <PORT>" require_equals=#true
8975"#
8976 .parse::<Spec>()
8977 .unwrap();
8978
8979 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
8980 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
8981
8982 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
8983 let msg = format!("{err}");
8984 assert!(
8985 msg.contains("requires an argument") || msg.contains("inspect"),
8986 "detached value must be refused: {msg}"
8987 );
8988 }
8989
8990 #[test]
8991 fn boolean_flags_can_accept_attached_values_when_enabled() {
8992 let spec: Spec = r#"
8993name "ex"
8994bin "ex"
8995flag "--color" negate="--no-color" bool_value=#true
8996arg "[rest]"
8997"#
8998 .parse()
8999 .unwrap();
9000
9001 for (token, expected) in [
9002 ("--color", true),
9003 ("--color=true", true),
9004 ("--color=false", false),
9005 ("--no-color", false),
9006 ("--no-color=false", true),
9007 ] {
9008 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9009 assert!(
9010 matches!(
9011 parsed.flags.get(&spec.cmd.flags[0]),
9012 Some(ParseValue::Bool(value)) if *value == expected
9013 ),
9014 "{token}"
9015 );
9016 }
9017
9018 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9019 assert!(matches!(
9020 parsed.args.get(&spec.cmd.args[0]),
9021 Some(ParseValue::String(value)) if value == "word"
9022 ));
9023 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9024 assert!(err.to_string().contains("expected `true` or `false`"));
9025
9026 let strict: Spec = r#"
9027name "ex"
9028bin "ex"
9029args_override_self #false
9030flag "--color" negate="--no-color" bool_value=#true
9031"#
9032 .parse()
9033 .unwrap();
9034 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9035 let parsed = parse(
9036 &strict,
9037 &input(&["ex", "--color=false", "--no-color=false"]),
9038 )
9039 .unwrap();
9040 assert!(matches!(
9041 parsed.flags.get(&strict.cmd.flags[0]),
9042 Some(ParseValue::Bool(true))
9043 ));
9044 }
9045
9046 #[test]
9047 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9048 let spec = r#"
9049flag "-a --all"
9050flag "-i --inspect <PORT>" require_equals=#true
9051"#
9052 .parse::<Spec>()
9053 .unwrap();
9054
9055 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9056 let msg = format!("{err}");
9057 assert!(
9058 msg.contains("requires an argument") || msg.contains("inspect"),
9059 "bundled short must refuse the following word: {msg}"
9060 );
9061 }
9062
9063 #[test]
9064 fn test_default_missing_binds_when_the_value_is_left_off() {
9065 let spec = r#"
9066flag "-c --color <WHEN>" default_missing="always"
9067flag "-v --verbose"
9068"#
9069 .parse::<Spec>()
9070 .unwrap();
9071
9072 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9073 assert_eq!(flag_string_value(&parsed, "color"), "always");
9074
9075 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9076 assert_eq!(flag_string_value(&parsed, "color"), "never");
9077
9078 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9079 assert_eq!(flag_string_value(&parsed, "color"), "never");
9080
9081 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9082 assert_eq!(flag_string_value(&parsed, "color"), "always");
9083 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9084
9085 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9086 assert_eq!(flag_string_value(&parsed, "color"), "");
9087
9088 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9089 assert_eq!(flag_string_value(&parsed, "color"), "never");
9090
9091 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9092 assert_eq!(flag_string_value(&parsed, "color"), "always");
9093 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9094 }
9095
9096 #[test]
9097 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9098 let spec = r#"
9099flag "--bump [LEVEL]" value_optional=#true
9100flag "--verbose"
9101arg "[FILE]"
9102"#
9103 .parse::<Spec>()
9104 .unwrap();
9105
9106 let absent = parse(&spec, &input(&["test"])).unwrap();
9107 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9108
9109 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9110 let bump = bare
9111 .flags
9112 .iter()
9113 .find(|(flag, _)| flag.name == "bump")
9114 .map(|(_, value)| value)
9115 .unwrap();
9116 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9117 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9118 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9119
9120 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9121 assert_eq!(flag_string_value(&explicit, "bump"), "");
9122
9123 let corrected = parse(
9124 &spec,
9125 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9126 )
9127 .unwrap();
9128 let bump = corrected
9129 .flags
9130 .iter()
9131 .find(|(flag, _)| flag.name == "bump")
9132 .map(|(_, value)| value)
9133 .unwrap();
9134 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9135
9136 let collecting = r#"
9137flag "--tag [TAG]..." value_optional=#true
9138flag "--verbose"
9139"#
9140 .parse::<Spec>()
9141 .unwrap();
9142 let valued = parse(
9143 &collecting,
9144 &input(&["test", "--tag", "one", "two", "--verbose"]),
9145 )
9146 .unwrap();
9147 let tag = valued
9148 .flags
9149 .iter()
9150 .find(|(flag, _)| flag.name == "tag")
9151 .map(|(_, value)| value)
9152 .unwrap();
9153 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9154 }
9155
9156 #[test]
9157 fn test_repeatable_bare_optional_values_count_each_occurrence() {
9158 let spec = r#"
9159flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9160"#
9161 .parse::<Spec>()
9162 .unwrap();
9163
9164 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9165 let tag = parsed
9166 .flags
9167 .iter()
9168 .find(|(flag, _)| flag.name == "tag")
9169 .map(|(_, value)| value)
9170 .unwrap();
9171 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9172
9173 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9174 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9175 }
9176
9177 #[test]
9178 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9179 let spec = r#"
9180flag "--tag [TAG]..." var=#true value_optional=#true
9181flag "--verbose"
9182"#
9183 .parse::<Spec>()
9184 .unwrap();
9185
9186 for argv in [
9187 &["test", "--tag", "one", "two"][..],
9188 &["test", "--tag", "one", "two", "--verbose"][..],
9189 &["test", "--tag", "one", "--tag", "two"][..],
9190 ] {
9191 let parsed = parse(&spec, &input(argv)).unwrap();
9192 let tag = parsed
9193 .flags
9194 .iter()
9195 .find(|(flag, _)| flag.name == "tag")
9196 .map(|(_, value)| value)
9197 .unwrap();
9198 assert!(
9199 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9200 "argv={argv:?}: {tag:?}"
9201 );
9202 }
9203
9204 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9205 let tag = bare
9206 .flags
9207 .iter()
9208 .find(|(flag, _)| flag.name == "tag")
9209 .map(|(_, value)| value)
9210 .unwrap();
9211 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9212 }
9213
9214 #[test]
9215 fn test_default_missing_with_require_equals_refuses_the_following_word() {
9216 let spec = r#"
9217flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9218arg "[rest]"
9219"#
9220 .parse::<Spec>()
9221 .unwrap();
9222
9223 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9224 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9225
9226 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9227 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9228
9229 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9231 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9232 assert_eq!(
9233 parsed
9234 .args
9235 .values()
9236 .next()
9237 .map(|v| v.to_string())
9238 .as_deref(),
9239 Some("80")
9240 );
9241
9242 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9243 assert_eq!(flag_string_value(&parsed, "inspect"), "");
9244 }
9245
9246 #[test]
9247 fn test_default_missing_must_be_a_choice() {
9248 let spec = r#"
9249flag "--color <WHEN>" default_missing="always" {
9250 choices "auto" "always" "never"
9251}
9252"#
9253 .parse::<Spec>()
9254 .unwrap();
9255
9256 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9257 assert_eq!(flag_string_value(&parsed, "color"), "always");
9258
9259 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9260 assert_eq!(flag_string_value(&parsed, "color"), "never");
9261
9262 let spec = r#"
9263flag "--color <WHEN>" default_missing="wat" {
9264 choices "auto" "always" "never"
9265}
9266"#
9267 .parse::<Spec>()
9268 .unwrap();
9269
9270 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9271 let msg = format!("{err}");
9272 assert!(
9273 msg.contains("Invalid choice for option color: wat"),
9274 "missing default has to pass choices the same way a typed value does: {msg}"
9275 );
9276
9277 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9278 let msg = format!("{err}");
9279 assert!(
9280 msg.contains("Invalid choice for option color: wat"),
9281 "an attached value that is not a choice is still refused: {msg}"
9282 );
9283
9284 let spec = r#"
9285flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9286 choices "9229" "80"
9287}
9288arg "[rest]"
9289"#
9290 .parse::<Spec>()
9291 .unwrap();
9292
9293 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9294 let msg = format!("{err}");
9295 assert!(
9296 msg.contains("Invalid choice for option inspect: wat"),
9297 "require_equals still binds the missing string, so the error is the choice: {msg}"
9298 );
9299 }
9300
9301 #[test]
9302 fn test_hyphen_values_still_default_to_short_flag_parsing() {
9303 let spec = r#"
9304flag "-d --working-dir <DIR>"
9305flag "-a --args <ARGS>"
9306"#
9307 .parse::<Spec>()
9308 .unwrap();
9309
9310 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9311
9312 assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
9313 }
9314
9315 mod available_flags {
9318 use super::*;
9319
9320 fn spec() -> Spec {
9321 r#"
9322bin "test"
9323flag "-v --verbose" global=#true
9324flag "--raw" global=#true effect="write"
9325flag "--local-only"
9326cmd "run" {
9327 flag "-r --raw"
9328 flag "-w --watch"
9329 cmd "once"
9330}
9331"#
9332 .parse::<Spec>()
9333 .unwrap()
9334 }
9335
9336 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9337 let mut chain = vec![&spec.cmd];
9338 for segment in path {
9339 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9340 }
9341 chain
9342 }
9343
9344 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9345 let mut names: Vec<_> = available_flags(&chain(spec, path))
9346 .iter()
9347 .map(|f| f.name.clone())
9348 .collect();
9349 names.sort();
9350 names
9351 }
9352
9353 #[test]
9354 fn an_empty_chain_yields_nothing() {
9355 assert!(available_flags(&[]).is_empty());
9356 }
9357
9358 #[test]
9359 fn the_root_gets_its_own_flags() {
9360 let spec = spec();
9361 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9362 }
9363
9364 #[test]
9365 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9366 let spec = spec();
9367 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
9368 }
9369
9370 #[test]
9371 fn a_re_declared_global_is_listed_once() {
9372 let spec = r#"
9375bin "test"
9376flag "-y --yes" global=#true effect="write"
9377cmd "rm" {
9378 flag "-y --yes"
9379}
9380"#
9381 .parse::<Spec>()
9382 .unwrap();
9383 let flags = available_flags(&chain(&spec, &["rm"]));
9384 assert_eq!(flags.len(), 1, "{flags:?}");
9385 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
9386 }
9387
9388 #[test]
9389 fn a_re_declared_global_keeps_the_globals_declaration() {
9390 let spec = spec();
9394 let flags = available_flags(&chain(&spec, &["run"]));
9395 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
9396 assert!(raw.global);
9397 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
9398 assert_eq!(raw.short, ['r']);
9399 }
9400
9401 #[test]
9402 fn it_matches_what_a_parse_accepts() {
9403 let spec = spec();
9406 for path in [vec![], vec!["run"], vec!["run", "once"]] {
9407 let argv = std::iter::once("test".to_string())
9408 .chain(path.iter().map(|s| s.to_string()))
9409 .collect::<Vec<_>>();
9410 let parsed = parse_partial(&spec, &argv).unwrap();
9411
9412 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
9413 .map(|f| f.name.clone())
9414 .collect();
9415 from_parse.sort();
9416 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
9417 }
9418 }
9419 }
9420
9421 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
9426 parsed
9427 .tokens
9428 .iter()
9429 .find(|token| token.index == index)
9430 .unwrap_or_else(|| panic!("no token at {index}"))
9431 .roles
9432 .iter()
9433 .map(render_role)
9434 .collect()
9435 }
9436
9437 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
9438 parsed
9439 .flag_origins
9440 .iter()
9441 .find(|(f, _)| f.name == flag)
9442 .map(|(_, origins)| origins.clone())
9443 .unwrap_or_default()
9444 }
9445
9446 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
9447 let env = env
9448 .iter()
9449 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
9450 .collect();
9451 Parser::new(spec)
9452 .with_env(env)
9453 .explain(&input(words))
9454 .unwrap()
9455 }
9456
9457 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
9458 explain_with_env(spec, words, &[])
9459 }
9460
9461 #[test]
9462 fn an_attached_long_value_is_recorded_on_the_flag_token() {
9463 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9464 .parse()
9465 .unwrap();
9466
9467 let parsed = explain(&spec, &["ex", "--env=prod"]);
9468
9469 assert_eq!(roles(&parsed, 0), ["program"]);
9470 assert_eq!(
9471 roles(&parsed, 1),
9472 ["flag env as --env", "value of env = [\"prod\"], attached"]
9473 );
9474 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
9477 }
9478
9479 #[test]
9480 fn a_detached_long_value_is_recorded_on_its_own_token() {
9481 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9482 .parse()
9483 .unwrap();
9484
9485 let parsed = explain(&spec, &["ex", "--env", "prod"]);
9486
9487 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
9488 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
9489 }
9490
9491 #[test]
9492 fn a_short_bundle_is_attributed_to_the_bundle_token() {
9493 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
9494 .parse()
9495 .unwrap();
9496
9497 let parsed = explain(&spec, &["ex", "-abj8"]);
9498
9499 assert_eq!(
9502 roles(&parsed, 1),
9503 [
9504 "flag a as -a",
9505 "flag b as -b",
9506 "flag j as -j",
9507 "value of j = [\"8\"], attached",
9508 ]
9509 );
9510 assert_eq!(parsed.tokens.len(), 2);
9511 }
9512
9513 #[test]
9514 fn a_delimiter_splits_one_token_into_several_values() {
9515 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
9516 .parse()
9517 .unwrap();
9518
9519 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
9520
9521 assert_eq!(
9522 roles(&parsed, 2),
9523 ["value of tags = [\"a\", \"b\", \"c\"]"],
9524 "the values meant, not the word typed"
9525 );
9526 }
9527
9528 #[test]
9529 fn a_separator_and_the_words_after_it_are_distinguished() {
9530 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
9531 .parse()
9532 .unwrap();
9533
9534 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
9535
9536 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
9537 assert_eq!(roles(&parsed, 2), ["separator"]);
9538 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
9540 }
9541
9542 #[test]
9543 fn a_second_separator_is_data() {
9544 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
9545 .parse()
9546 .unwrap();
9547
9548 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
9549
9550 assert_eq!(roles(&parsed, 1), ["separator"]);
9551 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
9552 }
9553
9554 #[test]
9555 fn an_unknown_flag_says_what_took_it() {
9556 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9557 .parse()
9558 .unwrap();
9559
9560 let parsed = explain(&spec, &["ex", "--wat"]);
9561
9562 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
9565 }
9566
9567 #[test]
9568 fn a_subcommand_word_is_not_a_positional() {
9569 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
9570 .parse()
9571 .unwrap();
9572
9573 let parsed = explain(&spec, &["ex", "build", "a"]);
9574
9575 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
9576 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
9577 }
9578
9579 #[test]
9580 fn a_multicall_applet_is_read_at_argv0() {
9581 let spec: Spec =
9582 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
9583 .parse()
9584 .unwrap();
9585
9586 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
9587
9588 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
9591 assert!(parsed.tokens[0].synthesized);
9592 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
9593 }
9594
9595 #[test]
9596 fn words_the_parse_never_reached_say_so() {
9597 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9598 .parse()
9599 .unwrap();
9600
9601 let parsed = Parser::new(&spec)
9602 .explain(&input(&["ex", "--help", "a"]))
9603 .unwrap();
9604
9605 assert_eq!(roles(&parsed, 2), ["unread"]);
9606 }
9607
9608 #[test]
9609 fn an_env_origin_names_the_variable_that_fired() {
9610 let spec: Spec =
9611 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
9612 .parse()
9613 .unwrap();
9614
9615 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
9616 assert_eq!(
9617 origins(&primary, "token"),
9618 [ValueOrigin::Env("EX_TOKEN".to_string())]
9619 );
9620
9621 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
9624 assert_eq!(
9625 origins(&fallback, "token"),
9626 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
9627 );
9628 }
9629
9630 #[test]
9631 fn a_default_origin_is_recorded_for_flags_and_args() {
9632 let spec: Spec =
9633 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
9634 .parse()
9635 .unwrap();
9636
9637 let parsed = explain(&spec, &["ex"]);
9638
9639 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
9640 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
9641 assert_eq!(arg.name, "src");
9642 assert_eq!(origins, &[ValueOrigin::Default]);
9643 }
9644
9645 #[test]
9646 fn a_default_if_origin_carries_the_condition_that_fired() {
9647 let spec: Spec = r#"
9648name "ex"
9649bin "ex"
9650flag "--profile <p>"
9651flag "--strict" {
9652 default_if "--profile" "prod" "true"
9653}
9654 "#
9655 .parse()
9656 .unwrap();
9657
9658 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
9659
9660 assert_eq!(
9663 origins(&parsed, "strict"),
9664 [ValueOrigin::DefaultIf {
9665 selector: "--profile".to_string(),
9666 when: Some("prod".to_string()),
9667 }]
9668 );
9669 }
9670
9671 #[test]
9672 fn a_bare_optional_value_flag_records_default_missing() {
9673 let spec: Spec =
9674 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
9675 .parse()
9676 .unwrap();
9677
9678 let parsed = explain(&spec, &["ex", "--color", "-v"]);
9679
9680 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
9683 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9684 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
9685 }
9686
9687 #[test]
9688 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
9689 let spec: Spec =
9690 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
9691 .parse()
9692 .unwrap();
9693
9694 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
9695
9696 assert_eq!(
9698 roles(&parsed, 1),
9699 [
9700 "flag color as --color",
9701 "value of color = [\"red\"], attached"
9702 ]
9703 );
9704 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9705 }
9706
9707 #[test]
9708 fn an_override_names_the_flag_that_did_it() {
9709 let spec: Spec =
9710 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
9711 .parse()
9712 .unwrap();
9713
9714 let parsed = explain(&spec, &["ex", "--loud"]);
9715
9716 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
9719 assert!(origins(&parsed, "quiet").is_empty());
9720 }
9721
9722 #[test]
9723 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
9724 let spec: Spec = r#"
9725name "ex"
9726bin "ex"
9727cmd "run" restart_token=":::" {
9728 arg "<task>" default="build"
9729}
9730 "#
9731 .parse()
9732 .unwrap();
9733
9734 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
9735
9736 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
9740 assert_eq!(roles(&parsed, 3), ["restart"]);
9743 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
9744 assert!(parsed.arg_origins.is_empty());
9745 }
9746
9747 #[test]
9748 fn a_value_terminator_says_which_run_it_ended() {
9749 let spec: Spec = r#"
9750name "ex"
9751bin "ex"
9752flag "--exec <cmd>..." value_terminator=";"
9753arg "<src>"
9754 "#
9755 .parse()
9756 .unwrap();
9757
9758 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
9759
9760 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
9761 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
9764 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
9765 }
9766
9767 #[test]
9768 fn an_args_value_terminator_says_which_run_it_ended() {
9769 let spec: Spec = r#"
9770name "ex"
9771bin "ex"
9772arg "<files>..." value_terminator=";"
9773arg "[dest]"
9774 "#
9775 .parse()
9776 .unwrap();
9777
9778 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
9779
9780 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
9781 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
9782 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
9783 }
9784
9785 #[test]
9786 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
9787 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
9788 .parse()
9789 .unwrap();
9790
9791 let parsed = Parser::new(&spec)
9792 .explain(&input(&["ex", "--env=prod"]))
9793 .unwrap();
9794
9795 assert!(Parser::new(&spec)
9798 .parse(&input(&["ex", "--env=prod"]))
9799 .is_err());
9800 assert_eq!(
9801 roles(&parsed, 1),
9802 ["flag env as --env", "value of env = [\"prod\"], attached"]
9803 );
9804 assert!(
9805 parsed.errors.iter().any(|e| e.to_string().contains("src")),
9806 "{:?}",
9807 parsed.errors
9808 );
9809 }
9810
9811 #[test]
9812 fn an_external_subcommand_forwards_whole_tokens() {
9813 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
9814 .parse()
9815 .unwrap();
9816
9817 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
9818
9819 assert_eq!(roles(&parsed, 1), ["external"]);
9820 assert_eq!(roles(&parsed, 2), ["external"]);
9821 }
9822
9823 #[test]
9824 fn a_view_keeps_the_callers_argv_positions() {
9825 let spec: Spec = r#"
9826bin "ex"
9827view "runner" root="run"
9828cmd "run" {
9829 flag "--token <token>"
9830}
9831 "#
9832 .parse()
9833 .unwrap();
9834
9835 let parsed = explain(&spec, &["runner", "--token", "secret"]);
9836
9837 assert_eq!(roles(&parsed, 0), ["program"]);
9840 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
9841 }
9842}