1use crate::miette::{self, bail};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::fmt::{Debug, Display, Formatter};
7use std::sync::Arc;
8
9#[cfg(feature = "cli-help")]
10use crate::docs;
11use crate::error::UsageErr;
12use crate::spec::arg::SpecDoubleDashChoices;
13use crate::spec::unknown_flags::UnknownFlags;
14use crate::warn::Warning;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17fn merge_subcommand_flags(
35 available: &mut BTreeMap<String, Arc<SpecFlag>>,
36 new_flags: BTreeMap<String, Arc<SpecFlag>>,
37 crossing_mount: bool,
38) {
39 available.retain(|_, f| f.global);
41
42 if crossing_mount {
43 for (key, flag) in new_flags {
48 available.insert(key, flag);
49 }
50 return;
51 }
52
53 let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56 let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60 fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62 let ptr = Arc::as_ptr(flag) as usize;
63 *merged_origin.get(&ptr).unwrap_or(&ptr)
64 }
65
66 for (key, flag) in new_flags {
71 if flag.global {
72 available.insert(key, flag);
74 continue;
75 }
76
77 let inherited_global = flag.long.iter().find_map(|l| {
86 available
87 .get(&format!("--{l}"))
88 .filter(|f| f.global)
89 .cloned()
90 });
91 if let Some(global_flag) = inherited_global {
92 let global_origin = origin_of(&merged_origin, &global_flag);
102 if available.get(&key).is_some_and(|existing| {
103 existing.global && origin_of(&merged_origin, existing) != global_origin
104 }) {
105 continue;
106 }
107 let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108 Some(merged) => merged.clone(),
109 None => {
110 let mut merged = (*global_flag).clone();
111 for s in &flag.short {
119 if !merged.short.contains(s) {
120 merged.short.push(*s);
121 }
122 }
123 for l in &flag.long {
124 if !merged.long.contains(l) {
125 merged.long.push(l.clone());
126 }
127 }
128 merged.hidden_short_aliases.retain(|alias| {
132 !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
133 });
134 merged.hidden_aliases.retain(|alias| {
135 !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
136 });
137 for s in &flag.hidden_short_aliases {
138 if !merged.hidden_short_aliases.contains(s) {
139 merged.hidden_short_aliases.push(*s);
140 }
141 }
142 for l in &flag.hidden_aliases {
143 if !merged.hidden_aliases.contains(l) {
144 merged.hidden_aliases.push(l.clone());
145 }
146 }
147 let merged = Arc::new(merged);
148 merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
149 merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
150 for existing in available.values_mut() {
156 if origin_of(&merged_origin, existing) == global_origin {
157 *existing = Arc::clone(&merged);
158 }
159 }
160 merged
161 }
162 };
163 available.insert(key, merged);
164 continue;
165 }
166
167 if available.contains_key(&key) {
171 continue;
172 }
173 available.insert(key, flag);
174 }
175}
176
177fn flag_keys(flag: &SpecFlag) -> Vec<String> {
180 let mut keys: Vec<String> = flag
181 .long
182 .iter()
183 .map(|l| format!("--{l}"))
184 .chain(flag.short.iter().map(|s| format!("-{s}")))
185 .collect();
186 if let Some(negate) = &flag.negate {
187 keys.push(negate.clone());
188 }
189 keys
190}
191
192fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
194 cmd.flags
195 .iter()
196 .chain(cmd.clause.iter().flat_map(|clause| &clause.flags))
197 .flat_map(|f| {
198 let f = Arc::new(f.clone()); flag_keys(&f)
200 .into_iter()
201 .map(|key| (key, Arc::clone(&f)))
202 .collect::<Vec<_>>()
203 })
204 .collect()
205}
206
207fn unique_flags<'a>(
208 flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
209) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
210 let mut seen = HashSet::new();
211 flags
212 .into_iter()
213 .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
214}
215
216pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
229 let Some((root, rest)) = chain.split_first() else {
230 return vec![];
231 };
232 let mut available = gather_flags(root);
233 for cmd in rest {
234 merge_subcommand_flags(&mut available, gather_flags(cmd), false);
235 }
236
237 let mut seen_names = HashSet::new();
248 unique_flags(available.values())
249 .filter(|f| seen_names.insert(f.name.clone()))
250 .cloned()
251 .collect()
252}
253
254fn get_flag_key(word: &str) -> &str {
257 if word.starts_with("--") {
258 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
260 } else if let Some((end, _)) = word.char_indices().nth(2) {
261 &word[0..end]
264 } else {
265 word
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
276#[non_exhaustive]
277pub enum ValueOrigin {
278 DefaultMissing,
283 Env(String),
289 Default,
295 DefaultIf {
299 selector: String,
300 when: Option<String>,
301 },
302}
303
304#[derive(Debug, Clone)]
309#[non_exhaustive]
310pub enum TokenRole {
311 Program,
313 Command { name: String },
315 Flag {
317 flag: Arc<SpecFlag>,
318 spelling: String,
319 negated: bool,
320 },
321 Value {
323 flag: Arc<SpecFlag>,
324 values: Vec<String>,
325 attached: bool,
328 },
329 Arg {
331 arg: Arc<SpecArg>,
332 values: Vec<String>,
333 },
334 Separator,
336 Builtin { spelling: String },
341 ValueTerminator { ends: String },
345 Restart,
349 UnknownFlag { bound_as: Option<Arc<SpecArg>> },
352 Refused { reason: String },
355 External,
357 Unread,
359 Sigil {
361 arg: Arc<SpecArg>,
362 sigil: String,
363 values: Vec<String>,
364 },
365 ClauseSeparator { name: String },
367}
368
369#[derive(Debug, Clone)]
371#[non_exhaustive]
372pub struct TokenBinding {
373 pub index: usize,
375 pub word: String,
376 pub synthesized: bool,
380 pub roles: Vec<TokenRole>,
381}
382
383#[non_exhaustive]
384pub struct ParseOutput {
385 pub cmd: SpecCommand,
386 pub cmds: Vec<SpecCommand>,
387 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
388 pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
390 pub clause_flags: IndexMap<String, Vec<IndexMap<Arc<SpecFlag>, ParseValue>>>,
392 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
393 pub tokens: Vec<TokenBinding>,
400 pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
406 pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
409 pub overridden_flags: BTreeMap<String, String>,
415 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
422 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
423 pub errors: Vec<UsageErr>,
424 pub warnings: Vec<Warning>,
428 pub next_arg: Option<Arc<SpecArg>>,
435 pub double_dash_seen: bool,
441 pub external: Option<Vec<String>>,
446}
447
448impl ParseOutput {
449 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
458 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
459 return self.available_flags.clone();
460 };
461 let start = match boundary.checked_sub(1) {
465 Some(prev) if self.cmds[prev].flags_from_mount => prev,
466 _ => boundary,
467 };
468 let mut offered = gather_flags(&self.cmds[start]);
472 for cmd in &self.cmds[start + 1..] {
473 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
474 }
475 offered
476 }
477}
478
479#[derive(Debug, Clone)]
480pub enum ParseValue {
481 Bool(bool),
482 String(String),
483 MultiBool(Vec<bool>),
484 MultiString(Vec<String>),
485}
486
487impl ParseValue {
488 pub fn try_as_bool(self) -> Option<bool> {
489 match self {
490 Self::Bool(value) => Some(value),
491 _ => None,
492 }
493 }
494
495 pub const fn try_as_bool_ref(&self) -> Option<&bool> {
496 match self {
497 Self::Bool(value) => Some(value),
498 _ => None,
499 }
500 }
501
502 pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
503 match self {
504 Self::Bool(value) => Some(value),
505 _ => None,
506 }
507 }
508
509 pub fn try_as_string(self) -> Option<String> {
510 match self {
511 Self::String(value) => Some(value),
512 _ => None,
513 }
514 }
515
516 pub const fn try_as_string_ref(&self) -> Option<&String> {
517 match self {
518 Self::String(value) => Some(value),
519 _ => None,
520 }
521 }
522
523 pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
524 match self {
525 Self::String(value) => Some(value),
526 _ => None,
527 }
528 }
529
530 pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
531 match self {
532 Self::MultiBool(value) => Some(value),
533 _ => None,
534 }
535 }
536
537 pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
538 match self {
539 Self::MultiBool(value) => Some(value),
540 _ => None,
541 }
542 }
543
544 pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
545 match self {
546 Self::MultiBool(value) => Some(value),
547 _ => None,
548 }
549 }
550
551 pub fn try_as_multi_string(self) -> Option<Vec<String>> {
552 match self {
553 Self::MultiString(value) => Some(value),
554 _ => None,
555 }
556 }
557
558 pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
559 match self {
560 Self::MultiString(value) => Some(value),
561 _ => None,
562 }
563 }
564
565 pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
566 match self {
567 Self::MultiString(value) => Some(value),
568 _ => None,
569 }
570 }
571}
572
573fn collect_deprecations(out: &mut ParseOutput) {
584 for cmd in out.cmds.iter().skip(1) {
585 if cmd.deprecated.is_none()
586 && cmd.deprecated_warn_at.is_none()
587 && cmd.deprecated_remove_at.is_none()
588 {
589 continue;
590 }
591 out.warnings.push(Warning::command(
592 cmd.name.clone(),
593 cmd.deprecated.clone(),
594 cmd.deprecated_warn_at.clone(),
595 cmd.deprecated_remove_at.clone(),
596 ));
597 }
598 for flag in out.flags.keys() {
599 if let Some(warning) = flag_deprecation(flag) {
600 out.warnings.push(warning);
601 }
602 }
603}
604
605fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
607 if flag.deprecated.is_none()
608 && flag.deprecated_warn_at.is_none()
609 && flag.deprecated_remove_at.is_none()
610 {
611 return None;
612 }
613 Some(Warning::flag(
614 flag_spelling(flag),
615 flag.deprecated.clone(),
616 flag.deprecated_warn_at.clone(),
617 flag.deprecated_remove_at.clone(),
618 ))
619}
620
621fn flag_spelling(flag: &SpecFlag) -> String {
624 flag.long
625 .first()
626 .map(|long| format!("--{long}"))
627 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
628 .unwrap_or_else(|| flag.name.clone())
629}
630
631fn flag_current_env(flag: &SpecFlag) -> Option<String> {
633 flag.env
634 .clone()
635 .or_else(|| flag.env_fallback.first().cloned())
636}
637
638fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
639 flag.deprecated_env.iter().any(|declared| declared == name)
640}
641
642fn arg_current_env(arg: &SpecArg) -> Option<String> {
644 arg.env
645 .clone()
646 .or_else(|| arg.env_fallback.first().cloned())
647}
648
649fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
650 arg.deprecated_env.iter().any(|declared| declared == name)
651}
652
653fn first_set_env<'a>(
660 mut names: impl Iterator<Item = &'a str>,
661 get_env: &impl Fn(&str) -> Option<String>,
662) -> Option<(&'a str, String)> {
663 names.find_map(|name| get_env(name).map(|value| (name, value)))
664}
665
666#[non_exhaustive]
686pub struct Parser<'a> {
687 spec: &'a Spec,
688 env: Option<HashMap<String, String>>,
689 mount_outputs: Option<HashMap<String, String>>,
690}
691
692impl<'a> Parser<'a> {
693 pub fn new(spec: &'a Spec) -> Self {
695 Self {
696 spec,
697 env: None,
698 mount_outputs: None,
699 }
700 }
701
702 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
707 self.env = Some(env);
708 self
709 }
710
711 pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
718 self.mount_outputs = Some(outputs);
719 self
720 }
721
722 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
726 let out = self.parse_collecting(input)?;
727 if let Some(err) = out
728 .errors
729 .iter()
730 .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
731 {
732 bail!("{err}");
733 }
734 if !out.errors.is_empty() {
735 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
736 }
737 Ok(out)
738 }
739
740 pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
751 self.parse_collecting(input)
752 }
753
754 pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
769 let mut trace = Trace::new(input);
770 match parse_partial_traced(
771 self.spec,
772 input,
773 self.env.as_ref(),
774 self.mount_outputs.as_ref(),
775 MountTiming::WhenAWordIsUnknown,
776 true,
777 &mut trace,
778 ) {
779 Ok((out, _)) => Ok(out),
782 Err(_) => Err(trace.tokens),
783 }
784 }
785
786 fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
787 let custom_env = self.env.as_ref();
788 let (mut out, overridden_flags) = parse_partial_with_env(
789 self.spec,
790 input,
791 custom_env,
792 self.mount_outputs.as_ref(),
793 MountTiming::WhenAWordIsUnknown,
794 false,
795 )?;
796 restore_current_clause(&mut out);
797 trace!("{out:?}");
798
799 while try_bind_default_missing(
805 &mut out.flags,
806 &mut out.flag_awaiting_value,
807 custom_env,
808 &mut out.flag_origins,
809 )? {}
810 if let Some(flag) = out.flag_awaiting_value.first() {
811 let token = flag
812 .long
813 .first()
814 .map(|l| format!("--{l}"))
815 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
816 .unwrap_or_else(|| flag.name.clone());
817 let rendered = input.join(" ");
818 let span = rendered
819 .rfind(&token)
820 .map(|at| (at, token.len()))
821 .unwrap_or((0, 0));
822 return Err(UsageErr::InvalidFlag {
823 token,
824 reason: "requires an argument".to_string(),
825 span: span.into(),
826 input: rendered,
827 }
828 .into());
829 }
830
831 collect_deprecations(&mut out);
835
836 let get_env = |key: &str| -> Option<String> {
837 if let Some(env_map) = custom_env {
838 env_map.get(key).cloned()
839 } else {
840 std::env::var(key).ok()
841 }
842 };
843
844 for arg in active_args(&out.cmd) {
849 if out.cmd.clause.is_some() {
852 break;
853 }
854 if out.args.contains_key(arg) {
855 continue;
856 }
857 if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
858 if arg_env_is_deprecated(arg, env_name) {
859 out.warnings
860 .push(Warning::env(env_name, arg_current_env(arg)));
861 }
862 let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
863 validate_choice_values(
864 ChoiceTarget::arg(arg),
865 &values,
866 arg.choices.as_ref(),
867 custom_env,
868 )?;
869 let parsed = if arg.var {
870 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
871 ParseValue::MultiString(values)
872 } else {
873 ParseValue::String(values.into_iter().next().unwrap_or_default())
874 };
875 out.args.insert(Arc::new(arg.clone()), parsed);
876 out.arg_origins
877 .entry(Arc::new(arg.clone()))
878 .or_default()
879 .push(ValueOrigin::Env(env_name.to_string()));
880 continue;
881 }
882 if !arg.default.is_empty() {
883 if arg.var {
885 let values = split_fallback_values(&arg.default, arg.delimiter);
886 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
887 validate_choice_values(
888 ChoiceTarget::arg(arg),
889 &values,
890 arg.choices.as_ref(),
891 custom_env,
892 )?;
893 out.args
895 .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
896 out.arg_origins
897 .entry(Arc::new(arg.clone()))
898 .or_default()
899 .push(ValueOrigin::Default);
900 } else {
901 validate_choice_value(
902 ChoiceTarget::arg(arg),
903 &arg.default[0],
904 arg.choices.as_ref(),
905 custom_env,
906 )?;
907 out.args.insert(
909 Arc::new(arg.clone()),
910 ParseValue::String(arg.default[0].clone()),
911 );
912 out.arg_origins
913 .entry(Arc::new(arg.clone()))
914 .or_default()
915 .push(ValueOrigin::Default);
916 }
917 }
918 }
919
920 let flags: Vec<Arc<SpecFlag>> = out
925 .available_flags
926 .values()
927 .filter(|flag| !is_clause_scoped_flag(&out, flag))
928 .cloned()
929 .collect();
930 for flag in &flags {
931 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
932 continue;
933 }
934 if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
935 if let Some(warning) = flag_deprecation(flag) {
939 out.warnings.push(warning);
940 }
941 if flag_env_is_deprecated(flag, env_name) {
942 out.warnings
943 .push(Warning::env(env_name, flag_current_env(flag)));
944 }
945 if let Some(arg) = flag.arg.as_ref() {
946 let values =
947 split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
948 validate_choice_values(
949 ChoiceTarget::option(flag),
950 &values,
951 arg.choices.as_ref(),
952 custom_env,
953 )?;
954 let parsed = if flag.var || arg.var {
955 if flag.var {
956 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
957 }
958 if arg.var {
959 validate_flag_arg_fallback_count(
960 flag,
961 arg,
962 values.len(),
963 &mut out.errors,
964 );
965 }
966 ParseValue::MultiString(values)
967 } else {
968 ParseValue::String(values.into_iter().next().unwrap_or_default())
969 };
970 out.flags.insert(Arc::clone(flag), parsed);
971 } else {
972 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
973 out.flags
974 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
975 }
976 out.flag_origins
977 .entry(Arc::clone(flag))
978 .or_default()
979 .push(ValueOrigin::Env(env_name.to_string()));
980 }
981 }
982 let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
988 for flag in &flags {
989 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
990 continue;
991 }
992 if let Some(condition) = flag.default_if.iter().find(|condition| {
993 default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
994 }) {
995 from_default_if.push((Arc::clone(flag), condition.clone()));
996 }
997 }
998 for (flag, condition) in &from_default_if {
999 bind_flag_fallback(
1003 flag,
1004 std::slice::from_ref(&condition.value),
1005 &mut out,
1006 custom_env,
1007 ValueOrigin::DefaultIf {
1008 selector: condition.selector.clone(),
1009 when: condition.when.clone(),
1010 },
1011 )?;
1012 }
1013 for flag in &flags {
1014 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1015 continue;
1016 }
1017 if !flag.default.is_empty() {
1018 bind_flag_fallback(
1019 flag,
1020 &flag.default,
1021 &mut out,
1022 custom_env,
1023 ValueOrigin::Default,
1024 )?;
1025 continue;
1026 }
1027 if let Some(arg) = flag.arg.as_ref() {
1028 if !arg.default.is_empty() {
1029 bind_flag_fallback(
1030 flag,
1031 &arg.default,
1032 &mut out,
1033 custom_env,
1034 ValueOrigin::Default,
1035 )?;
1036 }
1037 }
1038 }
1039 finalize_current_clause(&mut out);
1044 let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1045 validate_clause_relationships(
1046 &mut out,
1047 &overridden_flags,
1048 custom_env,
1049 clause_explicit.as_deref(),
1050 );
1051 for (arg, parsed) in &out.args {
1056 validate_expression(
1057 &arg.name,
1058 arg.validate.as_deref(),
1059 arg.validate_error.as_deref(),
1060 parsed,
1061 &mut out.errors,
1062 );
1063 }
1064 if let Some(clause) = &out.cmd.clause {
1065 let mut clause_errors = Vec::new();
1066 for (index, instance) in out
1067 .clauses
1068 .get(&clause.name)
1069 .into_iter()
1070 .flatten()
1071 .enumerate()
1072 {
1073 for arg in &clause.args {
1074 let Some(value) = instance.get(arg) else {
1075 if arg.required {
1076 clause_errors.push(UsageErr::MissingClauseArg {
1077 clause: clause.name.clone(),
1078 instance: index + 1,
1079 arg: arg.name.clone(),
1080 });
1081 }
1082 continue;
1083 };
1084 if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1085 if let Some(min) = arg.var_min {
1086 if values.len() < min {
1087 clause_errors.push(UsageErr::VarArgTooFew {
1088 name: format!(
1089 "{} instance {}: {}",
1090 clause.name,
1091 index + 1,
1092 arg.name
1093 ),
1094 min,
1095 got: values.len(),
1096 });
1097 }
1098 }
1099 if let Some(max) = arg.var_max {
1100 if values.len() > max {
1101 clause_errors.push(UsageErr::VarArgTooMany {
1102 name: format!(
1103 "{} instance {}: {}",
1104 clause.name,
1105 index + 1,
1106 arg.name
1107 ),
1108 max,
1109 got: values.len(),
1110 });
1111 }
1112 }
1113 }
1114 }
1115 }
1116 out.errors.extend(clause_errors);
1117 }
1118 let clause_flag_names = out
1119 .cmd
1120 .clause
1121 .iter()
1122 .flat_map(|clause| &clause.flags)
1123 .map(|flag| flag.name.as_str())
1124 .collect::<HashSet<_>>();
1125 for (flag, parsed) in out
1126 .flags
1127 .iter()
1128 .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1129 {
1130 if let Some(arg) = &flag.arg {
1131 validate_expression(
1132 &flag.name,
1133 arg.validate.as_deref(),
1134 arg.validate_error.as_deref(),
1135 parsed,
1136 &mut out.errors,
1137 );
1138 }
1139 }
1140 crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1143 Ok(out)
1144 }
1145}
1146
1147#[must_use = "parsing result should be used"]
1154pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1155 Parser::new(spec).parse(input)
1156}
1157
1158#[must_use = "parsing result should be used"]
1162pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1163 parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1164}
1165
1166pub fn multicall_basename(argv0: &str) -> &str {
1169 let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1170 match name.get(name.len().saturating_sub(4)..) {
1171 Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1172 _ => name,
1173 }
1174}
1175
1176pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1181 let base = multicall_basename(argv0);
1182 if !name.is_empty() && base == multicall_basename(name) {
1183 return None;
1184 }
1185 if let Some(bin) = bin {
1186 if !bin.is_empty() && base == multicall_basename(bin) {
1187 return None;
1188 }
1189 }
1190 Some(base)
1191}
1192
1193#[derive(Clone, Copy, PartialEq, Eq)]
1202enum MountTiming {
1203 Eager,
1204 WhenAWordIsUnknown,
1205}
1206
1207struct Token {
1217 word: String,
1218 argv: usize,
1224 binding: Option<(Arc<SpecFlag>, usize)>,
1236}
1237
1238impl Token {
1239 fn new(word: String, argv: usize) -> Self {
1240 Self {
1241 word,
1242 argv,
1243 binding: None,
1244 }
1245 }
1246}
1247
1248struct Trace {
1254 tokens: Vec<TokenBinding>,
1255}
1256
1257impl Trace {
1258 fn new(input: &[String]) -> Self {
1259 Self {
1260 tokens: input
1261 .iter()
1262 .enumerate()
1263 .map(|(index, word)| TokenBinding {
1264 index,
1265 word: word.clone(),
1266 synthesized: false,
1267 roles: vec![],
1268 })
1269 .collect(),
1270 }
1271 }
1272
1273 fn record(&mut self, argv: usize, role: TokenRole) {
1274 if let Some(token) = self.tokens.get_mut(argv) {
1275 token.roles.push(role);
1276 }
1277 }
1278
1279 fn note_synthesized(&mut self, argv: usize) {
1281 if let Some(token) = self.tokens.get_mut(argv) {
1282 token.synthesized = true;
1283 }
1284 }
1285
1286 fn close(&mut self, unread: &VecDeque<Token>) {
1288 for token in unread {
1289 self.record(token.argv, TokenRole::Unread);
1290 }
1291 }
1292}
1293
1294fn parse_partial_with_env(
1295 spec: &Spec,
1296 input: &[String],
1297 custom_env: Option<&HashMap<String, String>>,
1298 mount_outputs: Option<&HashMap<String, String>>,
1299 mount_timing: MountTiming,
1300 validate_clauses: bool,
1301) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1302 let mut trace = Trace::new(input);
1303 parse_partial_traced(
1304 spec,
1305 input,
1306 custom_env,
1307 mount_outputs,
1308 mount_timing,
1309 validate_clauses,
1310 &mut trace,
1311 )
1312}
1313
1314fn parse_partial_traced(
1321 spec: &Spec,
1322 input: &[String],
1323 custom_env: Option<&HashMap<String, String>>,
1324 mount_outputs: Option<&HashMap<String, String>>,
1325 mount_timing: MountTiming,
1326 validate_clauses: bool,
1327 trace: &mut Trace,
1328) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1329 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1330 let viewed = spec.for_view(view)?;
1331 return parse_partial_traced(
1332 &viewed,
1333 input,
1334 custom_env,
1335 mount_outputs,
1336 mount_timing,
1337 validate_clauses,
1338 trace,
1339 );
1340 }
1341 trace!("parse_partial: {input:?}");
1342 let mut input = input
1343 .iter()
1344 .enumerate()
1345 .map(|(argv, word)| Token::new(word.clone(), argv))
1346 .collect::<VecDeque<_>>();
1347 let argv0 = input.pop_front();
1348 if let Some(argv0) = argv0.as_ref() {
1349 trace.record(argv0.argv, TokenRole::Program);
1350 }
1351 if spec.multicall {
1352 if let Some(raw) = argv0 {
1353 if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1354 trace.note_synthesized(raw.argv);
1358 input.push_front(Token::new(applet.to_string(), raw.argv));
1359 }
1360 }
1361 }
1362 let mut command_has_argv = !input.is_empty();
1366
1367 let mut out = ParseOutput {
1368 cmd: spec.cmd.clone(),
1369 cmds: vec![spec.cmd.clone()],
1370 args: IndexMap::new(),
1371 clauses: IndexMap::new(),
1372 clause_flags: IndexMap::new(),
1373 flags: IndexMap::new(),
1374 tokens: vec![],
1375 flag_origins: IndexMap::new(),
1376 arg_origins: IndexMap::new(),
1377 overridden_flags: BTreeMap::new(),
1378 available_flags: gather_flags(&spec.cmd),
1379 flag_awaiting_value: vec![],
1380 errors: vec![],
1381 warnings: vec![],
1382 next_arg: None,
1383 double_dash_seen: false,
1384 external: None,
1385 };
1386 let mut overridden_flags = HashSet::new();
1389 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1394
1395 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1408 let mut command_arg_found = false;
1411 let mut variadic_flag_active = false;
1412 let mut idx = 0;
1413 let mut used_default_subcommand = false;
1416 let mut mounts_resolved = false;
1420 let default_outranks_mounts =
1430 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1431 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1432 {
1433 mounts_resolved = true;
1434 let mut mounted = out.cmd.clone();
1435 mounted.mount(&[], mount_outputs)?;
1436 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1437 if let Some(last) = out.cmds.last_mut() {
1438 *last = mounted.clone();
1439 }
1440 out.cmd = mounted;
1441 }
1442
1443 while idx < input.len() {
1444 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1454 default_accepts_word(&out.cmd, name, &input[idx].word)
1455 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1456 });
1457 if !mounts_resolved
1458 && !out.cmd.mounts.is_empty()
1459 && !default_catches_it
1460 && is_command_word(&input[idx].word)
1461 && !is_negative_number(&input[idx].word)
1462 && out.cmd.find_subcommand(&input[idx].word).is_none()
1463 {
1464 mounts_resolved = true;
1465 let mut mounted = out.cmd.clone();
1466 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1467 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1468 if let Some(last) = out.cmds.last_mut() {
1469 *last = mounted.clone();
1470 }
1471 out.cmd = mounted;
1472 }
1473 if variadic_flag_active
1474 && out.cmd.find_subcommand(&input[idx].word).is_some()
1475 && !out.cmd.subcommand_precedence_over_arg
1476 {
1477 break;
1478 }
1479 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1480 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1481 bail!(
1482 "subcommand '{}' cannot be used with arguments on its parent command",
1483 input[idx].word
1484 );
1485 }
1486 let mut subcommand = subcommand.clone();
1487 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1489 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1492 merge_subcommand_flags(
1493 &mut out.available_flags,
1494 gather_flags(&subcommand),
1495 crossing_mount,
1496 );
1497 let selected = input.remove(idx);
1499 if let Some(selected) = selected {
1500 trace.record(
1501 selected.argv,
1502 TokenRole::Command {
1503 name: subcommand.name.clone(),
1504 },
1505 );
1506 }
1507 command_has_argv = idx < input.len();
1508 out.cmds.push(subcommand.clone());
1509 out.cmd = subcommand.clone();
1510 mounts_resolved = true;
1512 prefix_flags.clear();
1513 command_arg_found = false;
1514 variadic_flag_active = false;
1515 } else if !is_command_word(&input[idx].word)
1518 || declared_numeric_short(&out.available_flags, &input[idx].word)
1519 {
1520 let word = input[idx].word.clone();
1522 let flag_key = get_flag_key(&word);
1523
1524 let is_bundle = word.starts_with("--")
1530 || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1531 if let Some(f) = out
1532 .available_flags
1533 .get(flag_key)
1534 .cloned()
1535 .filter(|_| is_bundle)
1536 {
1537 command_arg_found = true;
1538 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1539 input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1547 let mut forwarded = f.global.then(|| vec![word.clone()]);
1548 idx += 1;
1549
1550 if f.arg.is_some()
1553 && !word.contains('=')
1554 && idx < input.len()
1555 && accepts_detached_flag_value(&f, &input[idx].word)
1556 {
1557 if let Some(words) = forwarded.as_mut() {
1558 words.push(input[idx].word.clone());
1559 }
1560 idx += 1;
1561 }
1562 if let Some(words) = forwarded {
1563 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1564 prefix_flags.push((f, words));
1565 }
1566 } else {
1567 break;
1570 }
1571 } else {
1572 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1573 idx += 1;
1574 continue;
1575 }
1576 if !used_default_subcommand && out.cmds.len() == 1 {
1585 if let Some(default_name) = &spec.default_subcommand {
1586 if let Some(subcommand) = out
1587 .cmd
1588 .find_subcommand(default_name)
1589 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1590 {
1591 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1592 bail!(
1593 "subcommand '{}' cannot be used with arguments on its parent command",
1594 subcommand.name
1595 );
1596 }
1597 let mut subcommand = subcommand.clone();
1598 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1600 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1601 merge_subcommand_flags(
1602 &mut out.available_flags,
1603 gather_flags(&subcommand),
1604 crossing_mount,
1605 );
1606 out.cmds.push(subcommand.clone());
1607 out.cmd = subcommand.clone();
1608 command_has_argv = true;
1609 prefix_flags.clear();
1610 command_arg_found = false;
1611 variadic_flag_active = false;
1612 mounts_resolved = true;
1615 used_default_subcommand = true;
1616 continue;
1621 }
1622 }
1623 }
1624 if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1629 idx += 1;
1630 continue;
1631 }
1632 if out.cmd.external_subcommand {
1637 let rest: Vec<Token> = input.drain(idx..).collect();
1638 for token in &rest {
1639 trace.record(token.argv, TokenRole::External);
1640 }
1641 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1642 break;
1643 }
1644 break;
1646 }
1647 }
1648
1649 let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1659 let mut enable_flags = true;
1660 let mut grouped_flag = false;
1661 let mut seen_double_dash = false;
1665 let mut restart_seen = false;
1668 let mut double_dash_violations: HashSet<String> = HashSet::new();
1671 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1676
1677 while !input.is_empty() {
1678 let token = input.pop_front().unwrap();
1679 let binding = token.binding;
1681 let argv = token.argv;
1682 let mut w = token.word;
1683 let attached_continuation = grouped_flag;
1688
1689 if !seen_double_dash {
1692 if let Some(clause) = out.cmd.clause.as_ref() {
1693 if clause.separator.as_deref() == Some(w.as_str()) {
1694 while try_bind_default_missing(
1695 &mut out.flags,
1696 &mut out.flag_awaiting_value,
1697 custom_env,
1698 &mut out.flag_origins,
1699 )? {}
1700 if let Some(flag) = out.flag_awaiting_value.first() {
1701 let spelling = flag
1702 .long
1703 .first()
1704 .map(|long| format!("--{long}"))
1705 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1706 .unwrap_or_else(|| flag.name.clone());
1707 return Err(UsageErr::InvalidFlag {
1708 token: spelling.clone(),
1709 reason: "requires an argument".to_string(),
1710 span: (0, spelling.len()).into(),
1711 input: spelling,
1712 }
1713 .into());
1714 }
1715 let name = clause.name.clone();
1716 finalize_current_clause(&mut out);
1717 out.arg_origins.clear();
1718 trace.record(argv, TokenRole::ClauseSeparator { name });
1719 next_arg_idx = 0;
1720 out.flag_awaiting_value.clear();
1721 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1722 enable_flags = true;
1723 seen_double_dash = false;
1724 continue;
1725 }
1726 }
1727 }
1728
1729 if let Some(ref restart_token) = out.cmd.restart_token {
1732 if w == *restart_token {
1733 out.args.clear();
1738 out.arg_origins.clear();
1743 trace.record(argv, TokenRole::Restart);
1744 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1745 restart_seen = true;
1746 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
1750 }
1751 }
1752
1753 if enable_flags
1762 && !attached_continuation
1763 && w.starts_with('-')
1764 && out
1765 .flag_awaiting_value
1766 .last()
1767 .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1768 {
1769 let should_return = bind_pending_flag_value(
1772 spec,
1773 &out.cmd,
1774 &mut out.errors,
1775 &mut out.flags,
1776 &mut out.flag_awaiting_value,
1777 &mut w,
1778 &mut input,
1779 custom_env,
1780 trace,
1781 argv,
1782 false,
1784 )?;
1785 if should_return {
1786 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1787 return Ok((out, overridden_flags));
1788 }
1789 continue;
1790 }
1791
1792 if enable_flags
1798 && !attached_continuation
1799 && !out.flag_awaiting_value.is_empty()
1800 && out.flag_awaiting_value.last().is_some_and(|flag| {
1801 (flag.default_missing.is_some() || flag.value_optional)
1802 && !accepts_detached_flag_value(flag, &w)
1803 })
1804 {
1805 try_bind_default_missing(
1806 &mut out.flags,
1807 &mut out.flag_awaiting_value,
1808 custom_env,
1809 &mut out.flag_origins,
1810 )?;
1811 }
1812
1813 if w == "--" && !seen_double_dash {
1819 enable_flags = false;
1820
1821 let should_preserve = active_args(&out.cmd)
1824 .get(next_arg_idx)
1825 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1826 .unwrap_or(false);
1827
1828 if should_preserve {
1829 } else {
1832 seen_double_dash = true;
1833 trace.record(argv, TokenRole::Separator);
1834
1835 let target = active_args(&out.cmd).iter().position(|arg| {
1841 arg.double_dash == SpecDoubleDashChoices::Required
1842 && !out.args.contains_key(arg)
1843 });
1844 if let Some(target) = target {
1845 if target > next_arg_idx {
1852 next_arg_idx = target;
1853 }
1854 }
1855 continue;
1856 }
1857 }
1858
1859 if enable_flags && w.starts_with("--") {
1861 grouped_flag = false;
1862 let split = w.split_once('=');
1866 let word = split.map(|(word, _)| word).unwrap_or(&w);
1867 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1868 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1869 let command_level = binding
1870 .as_ref()
1871 .map(|(_, level)| *level)
1872 .unwrap_or(out.cmds.len() - 1);
1873 parsed_flag_spellings
1874 .entry(Arc::as_ptr(f) as usize)
1875 .or_default()
1876 .insert(word.to_string());
1877 trace.record(
1880 argv,
1881 TokenRole::Flag {
1882 flag: Arc::clone(f),
1883 spelling: word.to_string(),
1884 negated: f.negate.as_deref() == Some(word),
1885 },
1886 );
1887 if f.action != crate::SpecFlagAction::Set {
1888 out.errors.push(render_action_err(spec, &out.cmd, f, word));
1889 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1890 return Ok((out, overridden_flags));
1891 }
1892 apply_flag_overrides(
1893 f,
1894 &out.available_flags,
1895 &mut out.flags,
1896 &mut out.flag_awaiting_value,
1897 &mut overridden_flags,
1898 &mut out.overridden_flags,
1899 );
1900 if let Some(pending) = out.flag_awaiting_value.first() {
1901 out.errors.push(render_missing_flag_value(pending, &w));
1902 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1903 return Ok((out, overridden_flags));
1904 }
1905 if f.arg.is_some() {
1911 record_scalar_flag_occurrence(
1912 &out.cmds,
1913 f,
1914 command_level,
1915 None,
1916 &mut scalar_occurrences,
1917 &mut out.errors,
1918 );
1919 let f = Arc::clone(f);
1920 out.flag_awaiting_value.push(Arc::clone(&f));
1921 if let Some((_, val)) = split {
1926 let mut val = val.to_string();
1930 let should_return = bind_pending_flag_value(
1931 spec,
1932 &out.cmd,
1933 &mut out.errors,
1934 &mut out.flags,
1935 &mut out.flag_awaiting_value,
1936 &mut val,
1937 &mut input,
1938 custom_env,
1939 trace,
1940 argv,
1941 true,
1944 )?;
1945 if should_return {
1946 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1947 return Ok((out, overridden_flags));
1948 }
1949 }
1950 } else if f.count {
1951 let arr = out
1952 .flags
1953 .entry(Arc::clone(f))
1954 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1955 .try_as_multi_bool_mut()
1956 .unwrap();
1957 arr.push(true);
1958 } else {
1959 let negate = f.negate.clone().unwrap_or_default();
1960 let negated_form = word == negate;
1961 let value = if f.bool_value {
1962 match split.map(|(_, value)| value) {
1963 Some("true") => !negated_form,
1964 Some("false") => negated_form,
1965 Some(value) => {
1966 out.errors.push(UsageErr::InvalidValue {
1967 name: f.name.clone(),
1968 value: value.to_string(),
1969 reason: "expected `true` or `false`".to_string(),
1970 });
1971 continue;
1972 }
1973 None => !negated_form,
1974 }
1975 } else {
1976 !negated_form
1977 };
1978 record_scalar_flag_occurrence(
1983 &out.cmds,
1984 f,
1985 command_level,
1986 Some(!negated_form),
1987 &mut scalar_occurrences,
1988 &mut out.errors,
1989 );
1990 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1991 }
1992 continue;
1993 }
1994 if is_help_arg(spec, &out.cmd, &w) {
1995 out.errors
1996 .push(render_help_err(spec, &out.cmd, w.len() > 2));
1997 trace.record(
1998 argv,
1999 TokenRole::Builtin {
2000 spelling: w.clone(),
2001 },
2002 );
2003 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2004 return Ok((out, overridden_flags));
2005 }
2006 if is_version_arg(spec, &out.cmds, &w) {
2007 out.errors.push(render_version_err(spec, w.len() > 2));
2008 trace.record(
2009 argv,
2010 TokenRole::Builtin {
2011 spelling: w.clone(),
2012 },
2013 );
2014 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2015 return Ok((out, overridden_flags));
2016 }
2017 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2018 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2019 trace.close(&input);
2020 return Err(refused.into());
2021 }
2022 }
2023
2024 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2031 let positional_negative_number = !declared_numeric_short
2032 && is_negative_number(&w)
2033 && active_args(&out.cmd)
2034 .get(next_arg_idx)
2035 .is_some_and(|arg| arg.allow_negative_numbers);
2036 if enable_flags
2037 && !grouped_flag
2038 && binding.is_none()
2041 && w.starts_with('-')
2042 && w.len() > 1
2043 && is_flag_like(&w)
2044 && !positional_negative_number
2045 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2046 {
2047 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2050 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2051 trace.close(&input);
2052 return Err(refused.into());
2053 }
2054 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2055 let short = w.chars().nth(1).unwrap();
2056 if let Some(f) = binding
2057 .as_ref()
2058 .map(|(flag, _)| flag)
2059 .or_else(|| out.available_flags.get(&format!("-{short}")))
2060 {
2061 let command_level = binding
2062 .as_ref()
2063 .map(|(_, level)| *level)
2064 .unwrap_or(out.cmds.len() - 1);
2065 if f.action != crate::SpecFlagAction::Set {
2066 out.errors
2067 .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2068 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2069 return Ok((out, overridden_flags));
2070 }
2071 parsed_flag_spellings
2072 .entry(Arc::as_ptr(f) as usize)
2073 .or_default()
2074 .insert(format!("-{short}"));
2075 trace.record(
2076 argv,
2077 TokenRole::Flag {
2078 flag: Arc::clone(f),
2079 spelling: format!("-{short}"),
2080 negated: false,
2082 },
2083 );
2084 apply_flag_overrides(
2085 f,
2086 &out.available_flags,
2087 &mut out.flags,
2088 &mut out.flag_awaiting_value,
2089 &mut overridden_flags,
2090 &mut out.overridden_flags,
2091 );
2092 if !attached_continuation {
2093 if let Some(pending) = out.flag_awaiting_value.first() {
2094 out.errors.push(render_missing_flag_value(pending, &w));
2095 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2096 return Ok((out, overridden_flags));
2097 }
2098 }
2099 let rest = &w[1 + short.len_utf8()..];
2100 if !rest.is_empty() {
2101 input.push_front(Token::new(format!("-{rest}"), argv));
2104 }
2105 grouped_flag = !rest.is_empty();
2109 if f.arg.is_some() {
2110 record_scalar_flag_occurrence(
2111 &out.cmds,
2112 f,
2113 command_level,
2114 None,
2115 &mut scalar_occurrences,
2116 &mut out.errors,
2117 );
2118 out.flag_awaiting_value.push(Arc::clone(f));
2119 } else if f.count {
2120 let arr = out
2121 .flags
2122 .entry(Arc::clone(f))
2123 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2124 .try_as_multi_bool_mut()
2125 .unwrap();
2126 arr.push(true);
2127 } else {
2128 let negate = f.negate.clone().unwrap_or_default();
2129 let value = w != negate;
2130 record_scalar_flag_occurrence(
2131 &out.cmds,
2132 f,
2133 command_level,
2134 Some(value),
2135 &mut scalar_occurrences,
2136 &mut out.errors,
2137 );
2138 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2139 }
2140 continue;
2141 }
2142 if let Some(err) = supplied_short(spec, &out.cmds, short) {
2146 out.errors.push(err);
2147 trace.record(
2148 argv,
2149 TokenRole::Builtin {
2150 spelling: format!("-{short}"),
2151 },
2152 );
2153 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2154 return Ok((out, overridden_flags));
2155 }
2156 if is_help_arg(spec, &out.cmd, &w) {
2157 out.errors
2158 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2159 trace.record(
2160 argv,
2161 TokenRole::Builtin {
2162 spelling: w.clone(),
2163 },
2164 );
2165 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2166 return Ok((out, overridden_flags));
2167 }
2168 if is_version_arg(spec, &out.cmds, &w) {
2169 out.errors.push(render_version_err(spec, w.len() > 2));
2170 trace.record(
2171 argv,
2172 TokenRole::Builtin {
2173 spelling: w.clone(),
2174 },
2175 );
2176 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2177 return Ok((out, overridden_flags));
2178 }
2179 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2180 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2181 trace.close(&input);
2182 return Err(refused.into());
2183 }
2184 if grouped_flag {
2185 grouped_flag = false;
2186 w.remove(0);
2187 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2191 w.remove(0);
2192 }
2193 }
2194 }
2195
2196 if enable_flags
2207 && !attached_continuation
2208 && !out.flag_awaiting_value.is_empty()
2209 && out
2210 .flag_awaiting_value
2211 .last()
2212 .is_some_and(|flag| flag.require_equals)
2213 {
2214 let flag = out.flag_awaiting_value.last().unwrap();
2215 let token = flag
2216 .long
2217 .first()
2218 .map(|l| format!("--{l}"))
2219 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2220 .unwrap_or_else(|| flag.name.clone());
2221 out.errors.push(UsageErr::InvalidFlag {
2222 token: token.clone(),
2223 reason: "requires an argument".to_string(),
2224 span: (0, 0).into(),
2225 input: format!("{token} {w}"),
2226 });
2227 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2228 return Ok((out, overridden_flags));
2229 }
2230 if enable_flags
2231 && !out.flag_awaiting_value.is_empty()
2232 && (attached_continuation
2233 || out
2234 .flag_awaiting_value
2235 .last()
2236 .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2237 {
2238 let should_return = bind_pending_flag_value(
2241 spec,
2242 &out.cmd,
2243 &mut out.errors,
2244 &mut out.flags,
2245 &mut out.flag_awaiting_value,
2246 &mut w,
2247 &mut input,
2248 custom_env,
2249 trace,
2250 argv,
2251 attached_continuation,
2252 )?;
2253 if should_return {
2254 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2255 return Ok((out, overridden_flags));
2256 }
2257 continue;
2258 }
2259
2260 if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2261 .then(|| match_sigil_arg_chain(&out.cmds, &w))
2262 .flatten()
2263 {
2264 if value.is_empty() {
2265 out.errors.push(UsageErr::InvalidValue {
2266 name: arg.name.clone(),
2267 value: w.clone(),
2268 reason: format!("expected a value after sigil {sigil:?}"),
2269 });
2270 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2271 return Ok((out, overridden_flags));
2272 }
2273 let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2274 let suppress_trailing_delimiter =
2275 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2276 let delimiter = if suppress_trailing_delimiter && trailing_value {
2277 None
2278 } else {
2279 arg.delimiter
2280 };
2281 let parts = match delimiter {
2282 Some(delimiter) => value
2283 .split(delimiter)
2284 .map(str::to_string)
2285 .collect::<Vec<_>>(),
2286 None => vec![value.to_string()],
2287 };
2288 let mut refused = false;
2289 for part in &parts {
2290 if validate_choices(
2291 spec,
2292 &out.cmd,
2293 &mut out.errors,
2294 ChoiceTarget::arg(arg),
2295 part,
2296 arg.choices.as_ref(),
2297 custom_env,
2298 )? {
2299 refused = true;
2300 break;
2301 }
2302 }
2303 if refused {
2304 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2305 return Ok((out, overridden_flags));
2306 }
2307 trace.record(
2308 argv,
2309 TokenRole::Sigil {
2310 arg: Arc::new(arg.clone()),
2311 sigil: sigil.to_string(),
2312 values: parts.clone(),
2313 },
2314 );
2315 let key = Arc::new(arg.clone());
2316 if arg.var {
2317 let arr = out
2318 .args
2319 .entry(key)
2320 .or_insert_with(|| ParseValue::MultiString(vec![]))
2321 .try_as_multi_string_mut()
2322 .unwrap();
2323 arr.extend(parts);
2324 } else {
2325 out.args.insert(key, ParseValue::String(value.to_string()));
2326 }
2327 continue;
2328 }
2329
2330 if out.cmd.allow_missing_positional {
2331 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2332 while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2333 if current.required || out.args.contains_key(current) {
2334 break;
2335 }
2336 let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2337 .iter()
2338 .filter(|arg| arg.required && arg.sigil.is_none())
2339 .count();
2340 if required_after == 0 {
2341 break;
2342 }
2343 let remaining_values = 1 + input
2344 .iter()
2345 .filter(|token| {
2346 (!enable_flags || !is_flag_like(&token.word))
2347 && (!enable_flags
2348 || restart_seen
2349 || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2350 })
2351 .count();
2352 if remaining_values > required_after {
2353 break;
2354 }
2355 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2356 }
2357 }
2358
2359 if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2360 if arg.var
2361 && out.args.contains_key(arg)
2362 && arg.value_terminator.as_deref() == Some(w.as_str())
2363 {
2364 trace.record(
2365 argv,
2366 TokenRole::ValueTerminator {
2367 ends: arg.name.clone(),
2368 },
2369 );
2370 next_arg_idx += 1;
2371 continue;
2372 }
2373 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2377 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2378 trace.record(
2379 argv,
2380 TokenRole::Refused {
2381 reason: format!("{} only accepts words after `--`", arg.name),
2382 },
2383 );
2384 continue;
2388 }
2389 let trailing_value =
2394 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2395 let suppress_trailing_delimiter =
2396 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2397 let delimiter = if suppress_trailing_delimiter && trailing_value {
2398 None
2399 } else {
2400 arg.delimiter
2401 };
2402 let parts: Vec<String> = match delimiter {
2403 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2404 None => vec![w.clone()],
2405 };
2406 let mut refused = false;
2407 for part in &parts {
2408 if validate_choices(
2409 spec,
2410 &out.cmd,
2411 &mut out.errors,
2412 ChoiceTarget::arg(arg),
2413 part,
2414 arg.choices.as_ref(),
2415 custom_env,
2416 )? {
2417 refused = true;
2418 break;
2419 }
2420 }
2421 if refused {
2422 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2423 return Ok((out, overridden_flags));
2424 }
2425 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2430 enable_flags = false;
2431 }
2432 let unknown_flag = enable_flags
2437 && !positional_negative_number
2438 && is_flag_like(&w)
2439 && binding.is_none();
2440 trace.record(
2441 argv,
2442 if unknown_flag {
2443 TokenRole::UnknownFlag {
2444 bound_as: Some(Arc::new(arg.clone())),
2445 }
2446 } else {
2447 TokenRole::Arg {
2448 arg: Arc::new(arg.clone()),
2449 values: parts.clone(),
2450 }
2451 },
2452 );
2453 if arg.var {
2454 let arr = out
2455 .args
2456 .entry(Arc::new(arg.clone()))
2457 .or_insert_with(|| ParseValue::MultiString(vec![]))
2458 .try_as_multi_string_mut()
2459 .unwrap();
2460 arr.extend(parts.iter().cloned());
2464 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2465 next_arg_idx += 1;
2466 }
2467 } else {
2468 out.args
2469 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2470 next_arg_idx += 1;
2471 }
2472 if out
2473 .cmd
2474 .clause
2475 .as_ref()
2476 .is_some_and(|clause| clause.separator.is_none())
2477 && next_arg_idx >= active_args(&out.cmd).len()
2478 {
2479 while try_bind_default_missing(
2480 &mut out.flags,
2481 &mut out.flag_awaiting_value,
2482 custom_env,
2483 &mut out.flag_origins,
2484 )? {}
2485 if let Some(flag) = out.flag_awaiting_value.first() {
2486 let spelling = flag
2487 .long
2488 .first()
2489 .map(|long| format!("--{long}"))
2490 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2491 .unwrap_or_else(|| flag.name.clone());
2492 return Err(UsageErr::InvalidFlag {
2493 token: spelling.clone(),
2494 reason: "requires an argument".to_string(),
2495 span: (0, spelling.len()).into(),
2496 input: spelling,
2497 }
2498 .into());
2499 }
2500 let name = out.cmd.clause.as_ref().unwrap().name.clone();
2501 finalize_current_clause(&mut out);
2502 out.arg_origins.clear();
2503 trace.record(argv, TokenRole::ClauseSeparator { name });
2504 next_arg_idx = 0;
2505 out.flag_awaiting_value.clear();
2506 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2507 enable_flags = true;
2508 seen_double_dash = false;
2509 }
2510 continue;
2511 }
2512 if is_help_arg(spec, &out.cmd, &w) {
2513 out.errors
2514 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2515 trace.record(
2516 argv,
2517 TokenRole::Builtin {
2518 spelling: w.clone(),
2519 },
2520 );
2521 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2522 return Ok((out, overridden_flags));
2523 }
2524 if is_version_arg(spec, &out.cmds, &w) {
2525 out.errors.push(render_version_err(spec, w.len() > 2));
2526 trace.record(
2527 argv,
2528 TokenRole::Builtin {
2529 spelling: w.clone(),
2530 },
2531 );
2532 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2533 return Ok((out, overridden_flags));
2534 }
2535 trace.record(
2536 argv,
2537 TokenRole::Refused {
2538 reason: "no declaration takes this word".to_string(),
2539 },
2540 );
2541 trace.close(&input);
2542 bail!("unexpected word: {w}");
2543 }
2544
2545 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2546 if validate_clauses {
2547 validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2548 }
2549 let clause_flag_names = out
2550 .cmd
2551 .clause
2552 .iter()
2553 .flat_map(|clause| &clause.flags)
2554 .map(|flag| flag.name.clone())
2555 .collect::<HashSet<_>>();
2556
2557 let flag_was_parsed =
2562 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2563
2564 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2572 let declared: HashSet<String> = out
2573 .cmd
2574 .flags
2575 .iter()
2576 .filter(|declared| declared.name == flag.name)
2577 .flat_map(flag_keys)
2578 .collect();
2579 let speaks_for_this_flag = declared.iter().any(|spelling| {
2589 out.available_flags
2590 .get(spelling)
2591 .is_some_and(|available| Arc::ptr_eq(available, flag))
2592 });
2593 if speaks_for_this_flag {
2594 declared
2595 } else {
2596 HashSet::new()
2597 }
2598 };
2599
2600 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2608 let child = child_spellings(flag);
2609 let child_exclusive = !child.is_empty()
2610 && out
2611 .cmd
2612 .flags
2613 .iter()
2614 .any(|declared| declared.name == flag.name && declared.exclusive);
2615 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2616 Some(spellings) => (
2617 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2618 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2619 ),
2620 None => (child_exclusive, flag.exclusive && child.is_empty()),
2624 }
2625 };
2626
2627 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2628 let (child, ancestor) = exclusivity_in_play(flag);
2629 child || ancestor
2630 };
2631
2632 let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2637 .filter(|flag| !clause_flag_names.contains(&flag.name))
2638 .any(|flag| {
2639 exclusive_occurrence(flag)
2640 && !overridden_flags.contains(&flag.name)
2641 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2642 });
2643 let requirements_apply = |command_index: usize| {
2644 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2645 };
2646
2647 if out.cmd.arg_required_else_help && !command_has_argv {
2648 out.errors.push(render_help_err(spec, &out.cmd, false));
2649 }
2650
2651 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2657 let mut names: Vec<&str> = out
2658 .cmd
2659 .subcommands
2660 .iter()
2661 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2664 .map(|(name, _)| name.as_str())
2665 .collect();
2666 names.sort_unstable();
2667 out.errors.push(UsageErr::MissingSubcommand(
2668 out.cmd.name.clone(),
2669 names.join(", "),
2670 ));
2671 }
2672
2673 if !exclusive_present {
2676 for arg in out
2677 .cmds
2678 .iter()
2679 .enumerate()
2680 .filter(|(index, _)| requirements_apply(*index))
2681 .flat_map(|(_, cmd)| &cmd.args)
2682 {
2683 if out.args.contains_key(arg) {
2684 continue;
2685 }
2686 if double_dash_violations.contains(&arg.name) {
2688 continue;
2689 }
2690 let required_if = arg.required_if.iter().any(|selector| {
2691 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2692 });
2693 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2694 selector_explicit_has_value(
2695 &condition.selector,
2696 &condition.value,
2697 &out,
2698 &overridden_flags,
2699 custom_env,
2700 )
2701 });
2702 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2703 && arg.required_if_eq_all.iter().all(|condition| {
2704 selector_explicit_has_value(
2705 &condition.selector,
2706 &condition.value,
2707 &out,
2708 &overridden_flags,
2709 custom_env,
2710 )
2711 });
2712 let unless_any = arg.required_unless.iter().any(|selector| {
2713 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2714 });
2715 let unless_all = !arg.required_unless_all.is_empty()
2716 && arg.required_unless_all.iter().all(|selector| {
2717 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2718 });
2719 let required_unless = !(unless_any
2720 || unless_all
2721 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2722 if (arg.required
2723 || required_if
2724 || required_if_eq
2725 || required_if_eq_all
2726 || required_unless)
2727 && arg.default.is_empty()
2728 {
2729 let has_env = arg
2731 .env
2732 .as_ref()
2733 .is_some_and(|env_var| env_contains(custom_env, env_var));
2734 if !has_env {
2735 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2736 }
2737 }
2738 }
2739 }
2740
2741 for flag in unique_flags(out.available_flags.values())
2753 .filter(|flag| !clause_flag_names.contains(&flag.name))
2754 {
2755 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2756 if !given || overridden_flags.contains(&flag.name) {
2757 continue;
2758 }
2759 for other in &flag.conflicts {
2760 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2761 out.errors.push(UsageErr::InvalidFlag {
2762 token: format!("--{}", flag.name),
2763 reason: format!("conflicts with {other}"),
2764 span: (0, 0).into(),
2765 input: format!("--{} {other}", flag.name),
2766 });
2767 }
2768 }
2769 let owner = out
2780 .cmds
2781 .iter()
2782 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2783 .unwrap_or(out.cmds.len() - 1);
2784 if !exclusive_present && requirements_apply(owner) {
2785 for other in &flag.requires {
2786 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2787 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2788 if other.starts_with('-') {
2789 out.errors.push(UsageErr::MissingFlag(name));
2790 } else {
2791 out.errors.push(UsageErr::MissingArg(name));
2792 }
2793 }
2794 }
2795 for condition in &flag.requires_if {
2796 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2797 && !selector_is_satisfied(
2798 &condition.requires,
2799 &out,
2800 &overridden_flags,
2801 custom_env,
2802 )
2803 {
2804 let name = selector_flag_name(&condition.requires, &out)
2805 .unwrap_or_else(|| condition.requires.clone());
2806 out.errors.push(UsageErr::MissingFlag(name));
2807 }
2808 }
2809 }
2810 }
2811
2812 for (command_index, arg) in out
2815 .cmds
2816 .iter()
2817 .enumerate()
2818 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2819 {
2820 let given = arg_is_explicit(arg, &out, custom_env);
2821 if !given {
2822 continue;
2823 }
2824 for other in &arg.conflicts {
2825 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2826 out.errors.push(UsageErr::InvalidFlag {
2827 token: arg.name.clone(),
2828 reason: format!("conflicts with {other}"),
2829 span: (0, 0).into(),
2830 input: format!("{} {other}", arg.name),
2831 });
2832 }
2833 }
2834 if !exclusive_present && requirements_apply(command_index) {
2835 for other in &arg.requires {
2836 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2837 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2838 if other.starts_with('-') {
2839 out.errors.push(UsageErr::MissingFlag(name));
2840 } else {
2841 out.errors.push(UsageErr::MissingArg(name));
2842 }
2843 }
2844 }
2845 }
2846 }
2847
2848 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
2858 .filter(|flag| !clause_flag_names.contains(&flag.name))
2859 {
2860 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2865 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2866 continue;
2867 }
2868 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2869 .filter(|other| !clause_flag_names.contains(&other.name))
2870 .find(|other| {
2871 !Arc::ptr_eq(other, flag)
2872 && !overridden_flags.contains(&other.name)
2873 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2874 })
2875 .map(|other| format!("--{}", other.name));
2876 let other_arg = active_args(&out.cmd).iter().find(|arg| {
2877 out.args.keys().any(|given| given.name == arg.name)
2878 || out
2879 .clauses
2880 .values()
2881 .flatten()
2882 .any(|instance| instance.keys().any(|given| given.name == arg.name))
2883 || arg
2884 .env
2885 .as_ref()
2886 .is_some_and(|env| env_contains(custom_env, env))
2887 });
2888 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2894 let selected_subcommand =
2895 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2896 let other = other_flag
2897 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2898 .or(selected_subcommand);
2899 if let Some(other) = other {
2900 out.errors.push(UsageErr::InvalidFlag {
2901 token: format!("--{}", flag.name),
2902 reason: format!("must be given on its own, and {other} was given too"),
2903 span: (0, 0).into(),
2904 input: format!("--{} {other}", flag.name),
2905 });
2906 }
2907 }
2908
2909 let mut group_errors: Vec<UsageErr> = Vec::new();
2918 for (command_index, group) in out
2919 .cmds
2920 .iter()
2921 .enumerate()
2922 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2923 {
2924 let mut given: Vec<&str> = Vec::new();
2930 let mut seen: Vec<String> = Vec::new();
2931 for selector in &group.members {
2932 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2933 continue;
2934 }
2935 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2936 if seen.contains(&name) {
2937 continue;
2938 }
2939 seen.push(name);
2940 given.push(selector.as_str());
2941 }
2942 if !group.multiple && given.len() > 1 {
2943 group_errors.push(UsageErr::InvalidFlag {
2944 token: given[1].to_string(),
2945 reason: format!("cannot be used with {} in group {}", given[0], group.name),
2946 span: (0, 0).into(),
2947 input: format!("{} {}", given[0], given[1]),
2948 });
2949 }
2950 let satisfied = group
2955 .members
2956 .iter()
2957 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2958 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2959 group_errors.push(UsageErr::MissingGroup {
2963 group: group.name.clone(),
2964 members: group.members.join(", "),
2965 });
2966 }
2967 }
2968 out.errors.extend(group_errors);
2969
2970 if !exclusive_present {
2971 for flag in unique_flags(out.available_flags.values())
2972 .filter(|flag| !clause_flag_names.contains(&flag.name))
2973 {
2974 let owner = out
2975 .cmds
2976 .iter()
2977 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2978 .unwrap_or(out.cmds.len() - 1);
2979 if !requirements_apply(owner) {
2980 continue;
2981 }
2982 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2983 continue;
2984 }
2985 let has_default =
2986 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2987 let has_env = flag_has_env(flag, custom_env);
2988 let required_if = flag.required_if.iter().any(|selector| {
2989 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2990 });
2991 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2992 selector_explicit_has_value(
2993 &condition.selector,
2994 &condition.value,
2995 &out,
2996 &overridden_flags,
2997 custom_env,
2998 )
2999 });
3000 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3001 && flag.required_if_eq_all.iter().all(|condition| {
3002 selector_explicit_has_value(
3003 &condition.selector,
3004 &condition.value,
3005 &out,
3006 &overridden_flags,
3007 custom_env,
3008 )
3009 });
3010 let unless_any = flag.required_unless.iter().any(|selector| {
3011 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3012 });
3013 let unless_all = !flag.required_unless_all.is_empty()
3014 && flag.required_unless_all.iter().all(|selector| {
3015 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3016 });
3017 let required_unless = !(unless_any
3018 || unless_all
3019 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3020 if (flag.required
3021 || required_if
3022 || required_if_eq
3023 || required_if_eq_all
3024 || required_unless)
3025 && !has_default
3026 && !has_env
3027 {
3028 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3029 }
3030 }
3031 }
3032
3033 for (arg, value) in &out.args {
3035 if arg.var {
3036 if let ParseValue::MultiString(values) = value {
3037 if let Some(min) = arg.var_min {
3038 if values.len() < min {
3039 out.errors.push(UsageErr::VarArgTooFew {
3040 name: arg.name.clone(),
3041 min,
3042 got: values.len(),
3043 });
3044 }
3045 }
3046 if let Some(max) = arg.var_max {
3047 if values.len() > max {
3048 out.errors.push(UsageErr::VarArgTooMany {
3049 name: arg.name.clone(),
3050 max,
3051 got: values.len(),
3052 });
3053 }
3054 }
3055 }
3056 }
3057 }
3058
3059 for flag in unique_flags(out.available_flags.values())
3063 .filter(|flag| !clause_flag_names.contains(&flag.name))
3064 {
3065 if flag.var {
3066 let bound = match out.flags.get(flag) {
3067 Some(ParseValue::MultiString(values)) => values.len(),
3068 Some(ParseValue::MultiBool(values)) => values.len(),
3069 Some(_) => 1,
3070 None => 0,
3071 };
3072 let pending = out
3076 .flag_awaiting_value
3077 .iter()
3078 .filter(|pending| {
3079 Arc::ptr_eq(pending, flag)
3080 && (pending.value_optional || pending.default_missing.is_some())
3081 })
3082 .count();
3083 let count = bound + pending;
3084 if count == 0 {
3085 continue;
3086 }
3087 if let Some(min) = flag.var_min {
3088 if count < min {
3089 out.errors.push(UsageErr::VarFlagTooFew {
3090 name: flag.name.clone(),
3091 min,
3092 got: count,
3093 });
3094 }
3095 }
3096 if let Some(max) = flag.var_max {
3097 if count > max {
3098 out.errors.push(UsageErr::VarFlagTooMany {
3099 name: flag.name.clone(),
3100 max,
3101 got: count,
3102 });
3103 }
3104 }
3105 }
3106 }
3107
3108 Ok((out, overridden_flags))
3109}
3110
3111fn validate_expression(
3112 name: &str,
3113 expression: Option<&str>,
3114 message: Option<&str>,
3115 parsed: &ParseValue,
3116 errors: &mut Vec<UsageErr>,
3117) {
3118 let Some(expression) = expression else {
3119 return;
3120 };
3121 #[cfg(not(feature = "validation"))]
3122 let _ = expression;
3123 let values: &[String] = match parsed {
3124 ParseValue::String(value) => std::slice::from_ref(value),
3125 ParseValue::MultiString(values) => values,
3126 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3127 };
3128 #[cfg(feature = "validation")]
3129 for value in values {
3130 let reason = match usage_validation::validate(expression, value) {
3131 Ok(true) => continue,
3132 Ok(false) => message
3133 .unwrap_or("does not satisfy the validation expression")
3134 .to_string(),
3135 Err(error) => format!("validation expression failed: {error}"),
3136 };
3137 errors.push(UsageErr::InvalidValue {
3138 name: name.to_string(),
3139 value: value.clone(),
3140 reason,
3141 });
3142 break;
3143 }
3144 #[cfg(not(feature = "validation"))]
3145 if let Some(value) = values.first() {
3146 let _ = message;
3147 errors.push(UsageErr::InvalidValue {
3148 name: name.to_string(),
3149 value: value.clone(),
3150 reason: "expression validation requires the `validation` feature".to_string(),
3151 });
3152 }
3153}
3154
3155#[cfg(all(test, not(feature = "validation")))]
3156mod optional_validation_tests {
3157 use crate::{parse, Spec};
3158
3159 #[test]
3160 fn validation_declarations_require_the_opt_in_runtime_feature() {
3161 let spec: Spec = r#"
3162name "ex"
3163bin "ex"
3164arg "<port>" validate="int(value) > 0"
3165 "#
3166 .parse()
3167 .unwrap();
3168 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3169 assert!(
3170 error
3171 .to_string()
3172 .contains("requires the `validation` feature"),
3173 "{error:?}"
3174 );
3175 }
3176}
3177
3178fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3179 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3180}
3181
3182fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3183 overrider
3184 .overrides
3185 .iter()
3186 .any(|selector| flag_matches_selector(overridden, selector))
3187}
3188
3189fn apply_prefix_flag_overrides(
3190 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3191 flag: Arc<SpecFlag>,
3192) {
3193 prefix_flags
3194 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3195}
3196
3197fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3198 prefix_flags
3199 .iter()
3200 .flat_map(|(_, words)| words.iter().cloned())
3201 .collect()
3202}
3203
3204fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3205 match custom_env {
3206 Some(env) => env.contains_key(env_var),
3207 None => std::env::var(env_var).is_ok(),
3208 }
3209}
3210
3211fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3212 flag.env_names()
3213 .any(|env_var| env_contains(custom_env, env_var))
3214}
3215
3216fn fallback_is_true(value: &str) -> bool {
3217 matches!(value, "1" | "true" | "True" | "TRUE")
3218}
3219
3220fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3221 match delimiter {
3222 Some(delimiter) => values
3223 .iter()
3224 .flat_map(|value| value.split(delimiter).map(str::to_string))
3225 .collect(),
3226 None => values.to_vec(),
3227 }
3228}
3229
3230fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3231 if let Some(min) = arg.var_min {
3232 if count < min {
3233 errors.push(UsageErr::VarArgTooFew {
3234 name: arg.name.clone(),
3235 min,
3236 got: count,
3237 });
3238 }
3239 }
3240 if let Some(max) = arg.var_max {
3241 if count > max {
3242 errors.push(UsageErr::VarArgTooMany {
3243 name: arg.name.clone(),
3244 max,
3245 got: count,
3246 });
3247 }
3248 }
3249}
3250
3251fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3252 if let Some(min) = flag.var_min {
3253 if count < min {
3254 errors.push(UsageErr::VarFlagTooFew {
3255 name: flag.name.clone(),
3256 min,
3257 got: count,
3258 });
3259 }
3260 }
3261 if let Some(max) = flag.var_max {
3262 if count > max {
3263 errors.push(UsageErr::VarFlagTooMany {
3264 name: flag.name.clone(),
3265 max,
3266 got: count,
3267 });
3268 }
3269 }
3270}
3271
3272fn validate_flag_arg_fallback_count(
3273 flag: &SpecFlag,
3274 arg: &SpecArg,
3275 count: usize,
3276 errors: &mut Vec<UsageErr>,
3277) {
3278 if let Some(min) = arg.var_min {
3279 if count < min {
3280 errors.push(UsageErr::VarFlagTooFew {
3281 name: flag.name.clone(),
3282 min,
3283 got: count,
3284 });
3285 }
3286 }
3287 if let Some(max) = arg.var_max {
3288 if count > max {
3289 errors.push(UsageErr::VarFlagTooMany {
3290 name: flag.name.clone(),
3291 max,
3292 got: count,
3293 });
3294 }
3295 }
3296}
3297
3298fn bind_flag_fallback(
3301 flag: &Arc<SpecFlag>,
3302 values: &[String],
3303 out: &mut ParseOutput,
3304 custom_env: Option<&HashMap<String, String>>,
3305 origin: ValueOrigin,
3306) -> Result<(), miette::Error> {
3307 let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3308 return Ok(());
3309 };
3310 out.flags.insert(Arc::clone(flag), value);
3311 out.flag_origins
3312 .entry(Arc::clone(flag))
3313 .or_default()
3314 .push(origin);
3315 Ok(())
3316}
3317
3318fn flag_fallback_value(
3319 flag: &SpecFlag,
3320 values: &[String],
3321 errors: &mut Vec<UsageErr>,
3322 custom_env: Option<&HashMap<String, String>>,
3323) -> Result<Option<ParseValue>, miette::Error> {
3324 if values.is_empty() {
3325 return Ok(None);
3326 }
3327 if let Some(arg) = flag.arg.as_ref() {
3328 let values = split_fallback_values(values, arg.delimiter);
3329 if flag.var || arg.var {
3330 if flag.var {
3331 validate_flag_fallback_count(flag, values.len(), errors);
3332 }
3333 if arg.var {
3334 validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3335 }
3336 validate_choice_values(
3337 ChoiceTarget::option(flag),
3338 &values,
3339 arg.choices.as_ref(),
3340 custom_env,
3341 )?;
3342 Ok(Some(ParseValue::MultiString(values)))
3343 } else {
3344 let value = values.into_iter().next().unwrap_or_default();
3345 validate_choice_value(
3346 ChoiceTarget::option(flag),
3347 &value,
3348 arg.choices.as_ref(),
3349 custom_env,
3350 )?;
3351 Ok(Some(ParseValue::String(value)))
3352 }
3353 } else if flag.var {
3354 validate_flag_fallback_count(flag, values.len(), errors);
3355 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3356 Ok(Some(ParseValue::MultiBool(bools)))
3357 } else {
3358 Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3359 }
3360}
3361
3362fn apply_clause_flag_fallbacks(
3368 out: &mut ParseOutput,
3369 overridden_flags: &HashSet<String>,
3370 custom_env: Option<&HashMap<String, String>>,
3371) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3372 let Some(clause) = out.cmd.clause.clone() else {
3373 return Ok(None);
3374 };
3375 let Some(positional_instances) = out.clauses.get(&clause.name) else {
3376 return Ok(None);
3377 };
3378 let positional_instances = positional_instances.clone();
3379 let mut flag_instances = out
3380 .clause_flags
3381 .shift_remove(&clause.name)
3382 .unwrap_or_default();
3383 flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3384 let get_env = |key: &str| -> Option<String> {
3385 custom_env
3386 .and_then(|values| values.get(key).cloned())
3387 .or_else(|| {
3388 custom_env
3389 .is_none()
3390 .then(|| std::env::var(key).ok())
3391 .flatten()
3392 })
3393 };
3394 let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3395
3396 for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3397 let positional = &positional_instances[instance_index];
3398 let mut explicit = scoped
3399 .keys()
3400 .map(|flag| flag.name.clone())
3401 .collect::<HashSet<_>>();
3402
3403 for declared in &clause.flags {
3406 let flag = Arc::new(declared.clone());
3407 if scoped.contains_key(&flag) {
3408 continue;
3409 }
3410 let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3411 continue;
3412 };
3413 if let Some(warning) = flag_deprecation(declared) {
3414 out.warnings.push(warning);
3415 }
3416 if flag_env_is_deprecated(declared, env_name) {
3417 out.warnings
3418 .push(Warning::env(env_name, flag_current_env(declared)));
3419 }
3420 if let Some(value) = flag_fallback_value(
3421 declared,
3422 std::slice::from_ref(&env_value),
3423 &mut out.errors,
3424 custom_env,
3425 )? {
3426 scoped.insert(flag, value);
3427 explicit.insert(declared.name.clone());
3428 }
3429 }
3430
3431 let condition_matches =
3432 |condition: &crate::SpecDefaultIf| {
3433 if let Some(flag) = clause
3434 .flags
3435 .iter()
3436 .find(|flag| flag_matches_selector(flag, &condition.selector))
3437 {
3438 if !explicit.contains(&flag.name) {
3439 return false;
3440 }
3441 return condition.when.as_ref().is_none_or(|expected| {
3442 scoped
3443 .iter()
3444 .find(|(present, _)| present.name == flag.name)
3445 .is_some_and(|(_, value)| parse_value_has(value, expected))
3446 });
3447 }
3448 if let Some(arg) = clause.args.iter().find(|arg| {
3449 !condition.selector.starts_with('-') && arg.name == condition.selector
3450 }) {
3451 return positional.get(arg).is_some_and(|value| {
3452 condition
3453 .when
3454 .as_ref()
3455 .is_none_or(|expected| parse_value_has(value, expected))
3456 });
3457 }
3458 let command_flag = out
3459 .available_flags
3460 .values()
3461 .chain(out.flags.keys())
3462 .filter(|flag| !is_clause_scoped_flag(out, flag))
3463 .find(|flag| flag_matches_selector(flag, &condition.selector));
3464 command_flag.is_some_and(|flag| {
3465 !overridden_flags.contains(&flag.name)
3466 && command_flag_has_explicit_source(flag, out)
3467 && condition.when.as_ref().is_none_or(|expected| {
3468 out.flags
3469 .get(flag)
3470 .is_some_and(|value| parse_value_has(value, expected))
3471 })
3472 })
3473 };
3474
3475 let conditional = clause
3476 .flags
3477 .iter()
3478 .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3479 .filter_map(|flag| {
3480 flag.default_if
3481 .iter()
3482 .find(|condition| condition_matches(condition))
3483 .map(|condition| (flag.clone(), condition.value.clone()))
3484 })
3485 .collect::<Vec<_>>();
3486 for (declared, value) in conditional {
3487 if let Some(value) = flag_fallback_value(
3488 &declared,
3489 std::slice::from_ref(&value),
3490 &mut out.errors,
3491 custom_env,
3492 )? {
3493 scoped.insert(Arc::new(declared), value);
3494 }
3495 }
3496
3497 for declared in &clause.flags {
3498 let flag = Arc::new(declared.clone());
3499 if scoped.contains_key(&flag) {
3500 continue;
3501 }
3502 let values = if !declared.default.is_empty() {
3503 &declared.default
3504 } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3505 &arg.default
3506 } else {
3507 continue;
3508 };
3509 if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3510 {
3511 scoped.insert(flag, value);
3512 }
3513 }
3514 explicit_instances.push(explicit);
3515 }
3516
3517 out.clause_flags.insert(clause.name, flag_instances);
3518 Ok(Some(explicit_instances))
3519}
3520
3521fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3522 out.flags.contains_key(flag)
3523 && out.flag_origins.get(flag).is_none_or(|origins| {
3524 origins
3525 .iter()
3526 .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3527 })
3528}
3529
3530fn default_if_condition_matches(
3531 condition: &crate::SpecDefaultIf,
3532 out: &ParseOutput,
3533 overridden_flags: &HashSet<String>,
3534 custom_env: Option<&HashMap<String, String>>,
3535) -> bool {
3536 match &condition.when {
3537 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3538 Some(when) => {
3539 let Some(flag) = out
3540 .available_flags
3541 .values()
3542 .chain(out.flags.keys())
3543 .find(|flag| flag_matches_selector(flag, &condition.selector))
3544 else {
3545 return false;
3546 };
3547 if overridden_flags.contains(&flag.name) {
3548 return false;
3549 }
3550 explicit_flag_has_value(flag, when, out, custom_env)
3551 }
3552 }
3553}
3554
3555fn explicit_flag_has_value(
3561 flag: &SpecFlag,
3562 expected: &str,
3563 out: &ParseOutput,
3564 custom_env: Option<&HashMap<String, String>>,
3565) -> bool {
3566 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3567 ParseValue::Bool(value) => value.to_string() == expected,
3568 ParseValue::String(value) => value == expected,
3569 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3570 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3571 });
3572 if out.flags.contains_key(flag) {
3573 return parsed_matches;
3574 }
3575
3576 let value = flag.env_names().find_map(|env| match custom_env {
3577 Some(values) => values.get(env).cloned(),
3578 None => std::env::var(env).ok(),
3579 });
3580 value.is_some_and(
3581 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3582 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3583 None if flag.arg.is_none() => {
3584 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3585 }
3586 None => value == expected,
3587 },
3588 )
3589}
3590
3591fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3592 match value {
3593 ParseValue::Bool(value) => value.to_string() == expected,
3594 ParseValue::String(value) => value == expected,
3595 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3596 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3597 }
3598}
3599
3600fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3603 out.cmd
3604 .clause
3605 .as_ref()
3606 .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3607}
3608
3609fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3610 out.cmd
3611 .clause
3612 .as_ref()?
3613 .flags
3614 .iter()
3615 .find(|flag| flag_matches_selector(flag, selector))
3616}
3617
3618fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3619 out.clause_flags
3620 .values()
3621 .flatten()
3622 .any(|instance| instance.keys().any(|present| present.name == flag.name))
3623 || out.flags.keys().any(|present| present.name == flag.name)
3624}
3625
3626fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3627 out.clause_flags.values().flatten().any(|instance| {
3628 instance
3629 .iter()
3630 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3631 }) || out
3632 .flags
3633 .iter()
3634 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3635}
3636
3637fn validate_clause_relationships(
3638 out: &mut ParseOutput,
3639 overridden_flags: &HashSet<String>,
3640 custom_env: Option<&HashMap<String, String>>,
3641 explicit_instances: Option<&[HashSet<String>]>,
3642) {
3643 let Some(clause) = out.cmd.clause.as_ref() else {
3644 return;
3645 };
3646 let Some(instances) = out.clauses.get(&clause.name) else {
3647 return;
3648 };
3649 let command_flag_is_explicit = |selector: &str| {
3650 out.available_flags
3651 .values()
3652 .chain(out.flags.keys())
3653 .filter(|flag| !is_clause_scoped_flag(out, flag))
3654 .any(|flag| {
3655 flag_matches_selector(flag, selector)
3656 && !overridden_flags.contains(&flag.name)
3657 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3658 })
3659 };
3660 let command_flag_matches_value = |selector: &str, expected: &str| {
3661 out.available_flags
3662 .values()
3663 .chain(out.flags.keys())
3664 .filter(|flag| !is_clause_scoped_flag(out, flag))
3665 .find(|flag| flag_matches_selector(flag, selector))
3666 .is_some_and(|flag| {
3667 !overridden_flags.contains(&flag.name)
3668 && explicit_flag_has_value(flag, expected, out, custom_env)
3669 })
3670 };
3671 let command_flag_is_satisfied = |selector: &str| {
3672 out.available_flags
3673 .values()
3674 .chain(out.flags.keys())
3675 .filter(|flag| !is_clause_scoped_flag(out, flag))
3676 .any(|flag| flag_matches_selector(flag, selector))
3677 && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3678 };
3679 let mut errors = Vec::new();
3680 for (instance_index, instance) in instances.iter().enumerate() {
3681 let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
3682 let scoped = out
3683 .clause_flags
3684 .get(&clause.name)
3685 .and_then(|instances| instances.get(instance_index));
3686 let scoped_flag = |selector: &str| {
3687 clause
3688 .flags
3689 .iter()
3690 .find(|flag| flag_matches_selector(flag, selector))
3691 };
3692 let scoped_value = |flag: &SpecFlag| {
3693 scoped.and_then(|values| {
3694 values
3695 .iter()
3696 .find(|(present, _)| present.name == flag.name)
3697 .map(|(_, value)| value)
3698 })
3699 };
3700 let arg_is_explicit = |selector: &str| {
3701 instance
3702 .keys()
3703 .any(|arg| !selector.starts_with('-') && arg.name == selector)
3704 };
3705 let selector_is_explicit = |selector: &str| {
3706 scoped_flag(selector).is_some_and(|flag| {
3707 explicit
3708 .map(|given| given.contains(&flag.name))
3709 .unwrap_or_else(|| scoped_value(flag).is_some())
3710 }) || command_flag_is_explicit(selector)
3711 || arg_is_explicit(selector)
3712 };
3713 let selector_has_value = |selector: &str, expected: &str| {
3714 scoped_flag(selector)
3715 .filter(|flag| {
3716 explicit
3717 .map(|given| given.contains(&flag.name))
3718 .unwrap_or_else(|| scoped_value(flag).is_some())
3719 })
3720 .and_then(scoped_value)
3721 .is_some_and(|value| parse_value_has(value, expected))
3722 || command_flag_matches_value(selector, expected)
3723 || instance.iter().any(|(arg, value)| {
3724 !selector.starts_with('-')
3725 && arg.name == selector
3726 && parse_value_has(value, expected)
3727 })
3728 };
3729 let selector_is_satisfied = |selector: &str| {
3730 scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
3731 || command_flag_is_satisfied(selector)
3732 || arg_is_explicit(selector)
3733 };
3734 let selector_name = |selector: &str| {
3735 scoped_flag(selector)
3736 .map(|flag| flag.name.clone())
3737 .or_else(|| selector_flag_name(selector, out))
3738 .unwrap_or_else(|| selector.to_string())
3739 };
3740
3741 for flag in &clause.flags {
3742 let value = scoped_value(flag);
3743 if value.is_none() {
3744 let required_if = flag
3745 .required_if
3746 .iter()
3747 .any(|selector| selector_is_explicit(selector));
3748 let required_if_eq = flag
3749 .required_if_eq
3750 .iter()
3751 .any(|condition| selector_has_value(&condition.selector, &condition.value));
3752 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3753 && flag
3754 .required_if_eq_all
3755 .iter()
3756 .all(|condition| selector_has_value(&condition.selector, &condition.value));
3757 let unless_any = flag
3758 .required_unless
3759 .iter()
3760 .any(|selector| selector_is_explicit(selector));
3761 let unless_all = !flag.required_unless_all.is_empty()
3762 && flag
3763 .required_unless_all
3764 .iter()
3765 .all(|selector| selector_is_explicit(selector));
3766 let required_unless = !(unless_any
3767 || unless_all
3768 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3769 if flag.required
3770 || required_if
3771 || required_if_eq
3772 || required_if_eq_all
3773 || required_unless
3774 {
3775 errors.push(UsageErr::MissingFlag(flag.name.clone()));
3776 }
3777 continue;
3778 }
3779
3780 let explicitly_provided = explicit
3781 .map(|given| given.contains(&flag.name))
3782 .unwrap_or(true);
3783
3784 if explicitly_provided {
3785 for other in &flag.conflicts {
3786 if selector_is_explicit(other) {
3787 errors.push(UsageErr::InvalidFlag {
3788 token: format!("--{}", flag.name),
3789 reason: format!("conflicts with {other}"),
3790 span: (0, 0).into(),
3791 input: format!("--{} {other}", flag.name),
3792 });
3793 }
3794 }
3795 for other in &flag.requires {
3796 if !selector_is_satisfied(other) {
3797 errors.push(UsageErr::MissingFlag(selector_name(other)));
3798 }
3799 }
3800 for condition in &flag.requires_if {
3801 if value.is_some_and(|value| parse_value_has(value, &condition.value))
3802 && !selector_is_satisfied(&condition.requires)
3803 {
3804 errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
3805 }
3806 }
3807 }
3808 if let (true, Some(value)) = (flag.var, value) {
3809 let count = match value {
3810 ParseValue::MultiString(values) => values.len(),
3811 ParseValue::MultiBool(values) => values.len(),
3812 _ => 1,
3813 };
3814 if let Some(min) = flag.var_min.filter(|min| count < *min) {
3815 errors.push(UsageErr::VarFlagTooFew {
3816 name: flag.name.clone(),
3817 min,
3818 got: count,
3819 });
3820 }
3821 if let Some(max) = flag.var_max.filter(|max| count > *max) {
3822 errors.push(UsageErr::VarFlagTooMany {
3823 name: flag.name.clone(),
3824 max,
3825 got: count,
3826 });
3827 }
3828 }
3829 if let (Some(arg), Some(value)) = (&flag.arg, value) {
3830 validate_expression(
3831 &flag.name,
3832 arg.validate.as_deref(),
3833 arg.validate_error.as_deref(),
3834 value,
3835 &mut errors,
3836 );
3837 }
3838 }
3839 for arg in &clause.args {
3840 let given = instance.keys().any(|present| present.name == arg.name);
3841 if !given {
3842 let required_if = arg
3843 .required_if
3844 .iter()
3845 .any(|selector| selector_is_explicit(selector));
3846 let required_if_eq = arg
3847 .required_if_eq
3848 .iter()
3849 .any(|condition| selector_has_value(&condition.selector, &condition.value));
3850 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3851 && arg
3852 .required_if_eq_all
3853 .iter()
3854 .all(|condition| selector_has_value(&condition.selector, &condition.value));
3855 let unless_any = arg
3856 .required_unless
3857 .iter()
3858 .any(|selector| selector_is_explicit(selector));
3859 let unless_all = !arg.required_unless_all.is_empty()
3860 && arg
3861 .required_unless_all
3862 .iter()
3863 .all(|selector| selector_is_explicit(selector));
3864 let required_unless = !(unless_any
3865 || unless_all
3866 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3867 if required_if || required_if_eq || required_if_eq_all || required_unless {
3868 errors.push(UsageErr::MissingClauseArg {
3869 clause: clause.name.clone(),
3870 instance: instance_index + 1,
3871 arg: arg.name.clone(),
3872 });
3873 }
3874 continue;
3875 }
3876 for other in &arg.conflicts {
3877 if selector_is_explicit(other) {
3878 errors.push(UsageErr::InvalidFlag {
3879 token: arg.name.clone(),
3880 reason: format!("conflicts with {other}"),
3881 span: (0, 0).into(),
3882 input: format!("{} {other}", arg.name),
3883 });
3884 }
3885 }
3886 for other in &arg.requires {
3887 if selector_is_satisfied(other) {
3888 continue;
3889 }
3890 if other.starts_with('-') {
3891 errors.push(UsageErr::MissingFlag(selector_name(other)));
3892 } else {
3893 errors.push(UsageErr::MissingClauseArg {
3894 clause: clause.name.clone(),
3895 instance: instance_index + 1,
3896 arg: other.clone(),
3897 });
3898 }
3899 }
3900 }
3901 }
3902 out.errors.extend(errors);
3903}
3904
3905fn selector_explicit_has_value(
3906 selector: &str,
3907 expected: &str,
3908 out: &ParseOutput,
3909 overridden_flags: &HashSet<String>,
3910 custom_env: Option<&HashMap<String, String>>,
3911) -> bool {
3912 if let Some(flag) = selected_clause_flag(out, selector) {
3913 return clause_flag_has_value(out, flag, expected);
3914 }
3915 if let Some(flag) = out
3916 .available_flags
3917 .values()
3918 .chain(out.flags.keys())
3919 .filter(|flag| !is_clause_scoped_flag(out, flag))
3920 .find(|flag| flag_matches_selector(flag, selector))
3921 {
3922 return !overridden_flags.contains(&flag.name)
3923 && explicit_flag_has_value(flag, expected, out, custom_env);
3924 }
3925 let Some(arg) = selector_arg(selector, out) else {
3926 return false;
3927 };
3928 let parsed = out
3929 .args
3930 .iter()
3931 .find(|(given, _)| given.name == arg.name)
3932 .map(|(_, value)| value)
3933 .or_else(|| {
3934 out.clauses.values().flatten().find_map(|instance| {
3935 instance
3936 .iter()
3937 .find(|(given, _)| given.name == arg.name)
3938 .map(|(_, value)| value)
3939 })
3940 });
3941 if let Some(value) = parsed {
3942 return match value {
3943 ParseValue::String(value) => value == expected,
3944 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3945 ParseValue::Bool(value) => value.to_string() == expected,
3946 ParseValue::MultiBool(values) => {
3947 values.iter().any(|value| value.to_string() == expected)
3948 }
3949 };
3950 }
3951 let value = arg.env_names().find_map(|env| match custom_env {
3952 Some(values) => values.get(env).cloned(),
3953 None => std::env::var(env).ok(),
3954 });
3955 value.is_some_and(|value| match arg.delimiter {
3956 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3957 None => value == expected,
3958 })
3959}
3960
3961fn selector_is_explicit(
3962 selector: &str,
3963 out: &ParseOutput,
3964 overridden_flags: &HashSet<String>,
3965 custom_env: Option<&HashMap<String, String>>,
3966) -> bool {
3967 let scoped_flag_is_explicit =
3968 selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
3969 let flag_is_explicit = out
3970 .available_flags
3971 .values()
3972 .chain(out.flags.keys())
3973 .filter(|flag| !is_clause_scoped_flag(out, flag))
3974 .any(|flag| {
3975 flag_matches_selector(flag, selector)
3976 && !overridden_flags.contains(&flag.name)
3977 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3978 });
3979 scoped_flag_is_explicit
3980 || flag_is_explicit
3981 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3982}
3983
3984fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3990 if let Some(flag) = selected_clause_flag(out, selector) {
3991 return Some(flag.name.clone());
3992 }
3993 out.available_flags
3994 .values()
3995 .chain(out.flags.keys())
3996 .filter(|flag| !is_clause_scoped_flag(out, flag))
3997 .find(|flag| flag_matches_selector(flag, selector))
3998 .map(|flag| flag.name.clone())
3999 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4000}
4001
4002fn selector_is_satisfied(
4012 selector: &str,
4013 out: &ParseOutput,
4014 overridden_flags: &HashSet<String>,
4015 custom_env: Option<&HashMap<String, String>>,
4016) -> bool {
4017 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4018 return true;
4019 }
4020 let flag_is_satisfied = out
4021 .available_flags
4022 .values()
4023 .chain(out.flags.keys())
4024 .filter(|flag| !is_clause_scoped_flag(out, flag))
4025 .filter(|flag| flag_matches_selector(flag, selector))
4026 .any(|flag| {
4027 !overridden_flags.contains(&flag.name)
4028 && (!flag.default.is_empty()
4029 || flag.arg.iter().any(|a| !a.default.is_empty())
4030 || flag.default_if.iter().any(|condition| {
4031 default_if_condition_matches(condition, out, overridden_flags, custom_env)
4032 }))
4033 });
4034 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4035}
4036
4037fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4038 if selector.starts_with('-') {
4041 return None;
4042 }
4043 out.cmds
4044 .iter()
4045 .flat_map(active_args)
4046 .find(|arg| arg.name == selector)
4047}
4048
4049fn arg_is_explicit(
4050 arg: &SpecArg,
4051 out: &ParseOutput,
4052 custom_env: Option<&HashMap<String, String>>,
4053) -> bool {
4054 out.args.keys().any(|given| given.name == arg.name)
4055 || out
4056 .clauses
4057 .values()
4058 .flatten()
4059 .any(|instance| instance.keys().any(|given| given.name == arg.name))
4060 || arg
4061 .env
4062 .as_ref()
4063 .is_some_and(|env| env_contains(custom_env, env))
4064}
4065
4066fn apply_flag_overrides(
4067 flag: &Arc<SpecFlag>,
4068 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4069 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4070 pending_flags: &mut Vec<Arc<SpecFlag>>,
4071 overridden_flags: &mut HashSet<String>,
4072 attributed: &mut BTreeMap<String, String>,
4076) {
4077 let overridden_names: HashSet<String> = available_flags
4078 .values()
4079 .chain(parsed_flags.keys())
4080 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4081 .map(|other| other.name.clone())
4082 .collect();
4083
4084 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4085 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4086 for name in &overridden_names {
4087 attributed.insert(name.clone(), flag.name.clone());
4088 }
4089 overridden_flags.extend(overridden_names);
4090 overridden_flags.remove(&flag.name);
4092 attributed.remove(&flag.name);
4093}
4094
4095#[cfg(feature = "cli-help")]
4096fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
4097 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
4098}
4099
4100#[cfg(feature = "cli-help")]
4101fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
4102 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
4103 if !out.is_empty() {
4104 out.push('\n');
4105 }
4106 out.push_str(&docs::cli::render_help(spec, cmd, true));
4107 let mut children: Vec<_> = cmd
4108 .subcommands
4109 .values()
4110 .filter(|child| !child.hide)
4111 .collect();
4112 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4113 for child in children {
4114 append(out, spec, child);
4115 }
4116 }
4117
4118 let mut out = String::new();
4119 append(&mut out, spec, cmd);
4120 UsageErr::Help(out)
4121}
4122
4123#[cfg(not(feature = "cli-help"))]
4124fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
4125 UsageErr::Help("help".to_string())
4126}
4127
4128#[cfg(not(feature = "cli-help"))]
4129fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
4130 UsageErr::Help("help".to_string())
4131}
4132
4133fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4136 let value = if long {
4137 spec.long_version.as_ref().or(spec.version.as_ref())
4138 } else {
4139 spec.version.as_ref().or(spec.long_version.as_ref())
4140 };
4141 UsageErr::Version(value.cloned().unwrap_or_default())
4142}
4143
4144fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
4145 use crate::SpecFlagAction;
4146 match flag.action {
4147 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
4148 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
4149 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
4150 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
4151 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4152 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4153 }
4154}
4155
4156fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4158 let token = flag
4159 .long
4160 .first()
4161 .map(|long| format!("--{long}"))
4162 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4163 .unwrap_or_else(|| flag.name.clone());
4164 UsageErr::InvalidFlag {
4165 token: token.clone(),
4166 reason: "requires an argument".to_string(),
4167 span: (0, 0).into(),
4168 input: format!("{token} {following}"),
4169 }
4170}
4171
4172#[derive(Copy, Clone)]
4173struct ChoiceTarget<'a> {
4174 kind: &'a str,
4175 name: &'a str,
4176}
4177
4178impl<'a> ChoiceTarget<'a> {
4179 fn arg(arg: &'a SpecArg) -> Self {
4180 Self {
4181 kind: "arg",
4182 name: &arg.name,
4183 }
4184 }
4185
4186 fn option(flag: &'a SpecFlag) -> Self {
4187 Self {
4188 kind: "option",
4189 name: &flag.name,
4190 }
4191 }
4192}
4193
4194fn short_bundle_is_known(
4199 spec: &Spec,
4200 cmds: &[SpecCommand],
4201 available: &BTreeMap<String, Arc<SpecFlag>>,
4202 token: &str,
4203) -> bool {
4204 for c in token.chars().skip(1) {
4205 match available.get(&format!("-{c}")) {
4206 None if supplied_short(spec, cmds, c).is_some() => {}
4211 None => return false,
4212 Some(f) if f.arg.is_some() => return true,
4213 Some(_) => {}
4214 }
4215 }
4216 true
4217}
4218
4219fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
4229 let cmd = cmds.last()?;
4230 match letter {
4231 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
4232 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4233 _ => None,
4234 }
4235}
4236
4237fn reject_unknown_flag_if_asked(
4243 spec: &Spec,
4244 path: &[SpecCommand],
4245 token: &str,
4246) -> Result<(), UsageErr> {
4247 if !is_flag_like(token) {
4250 return Ok(());
4251 }
4252 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4253 return Ok(());
4254 }
4255 Err(UsageErr::InvalidFlag {
4256 token: token.to_string(),
4257 reason: "no such flag".to_string(),
4258 span: (0, 0).into(),
4259 input: token.to_string(),
4260 })
4261}
4262
4263fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4269 path.iter()
4270 .rev()
4271 .find_map(|cmd| cmd.unknown_flags)
4272 .or(spec.unknown_flags)
4273 .unwrap_or_default()
4274}
4275
4276fn is_flag_like(token: &str) -> bool {
4282 match token.strip_prefix('-') {
4283 None | Some("") => false,
4284 Some(_) => true,
4285 }
4286}
4287
4288fn is_negative_number(token: &str) -> bool {
4289 token.strip_prefix('-').is_some_and(is_number)
4290}
4291
4292fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4298 !flag.require_equals
4299 && (!is_flag_like(token)
4300 || flag.allow_hyphen_values()
4301 || (is_negative_number(token)
4302 && (flag
4303 .arg
4304 .as_ref()
4305 .is_some_and(|arg| arg.allow_negative_numbers)
4306 || (flag.default_missing.is_none() && !flag.value_optional))))
4307}
4308
4309fn record_scalar_flag_occurrence(
4310 cmds: &[SpecCommand],
4311 flag: &Arc<SpecFlag>,
4312 command_level: usize,
4313 bool_value: Option<bool>,
4314 occurrences: &mut HashMap<(usize, usize), u8>,
4315 errors: &mut Vec<UsageErr>,
4316) {
4317 let strict = cmds.get(command_level).is_some_and(|cmd| {
4318 !cmd.args_override_self
4319 || cmd
4320 .clause
4321 .as_ref()
4322 .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4323 });
4324 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4325 if !strict || flag.count || collects_values {
4326 return;
4327 }
4328
4329 let bit = match bool_value {
4330 Some(false) if flag.negate.is_some() => 0b10,
4331 _ => 0b01,
4332 };
4333 let key = (Arc::as_ptr(flag) as usize, command_level);
4334 let seen = occurrences.entry(key).or_default();
4335 if *seen & bit != 0 {
4336 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4337 }
4338 *seen |= bit;
4339}
4340
4341fn is_command_word(token: &str) -> bool {
4349 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4350}
4351
4352fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4358 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4359}
4360
4361fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4367 !is_negative_number(token)
4368 || cmd
4369 .find_subcommand(default_name)
4370 .and_then(|default| default.args.first())
4371 .is_some_and(|arg| arg.allow_negative_numbers)
4372}
4373
4374fn is_number(rest: &str) -> bool {
4381 let (mantissa, exponent) = match rest.find(['e', 'E']) {
4382 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4383 None => (rest, None),
4384 };
4385
4386 let mut seen_digit = false;
4387 let mut seen_dot = false;
4388 for c in mantissa.chars() {
4389 match c {
4390 '0'..='9' => seen_digit = true,
4391 '.' if !seen_dot => seen_dot = true,
4392 _ => return false,
4393 }
4394 }
4395 if !seen_digit {
4396 return false;
4397 }
4398
4399 match exponent {
4400 None => true,
4401 Some(exp) => {
4402 let digits = exp
4403 .strip_prefix('+')
4404 .or_else(|| exp.strip_prefix('-'))
4405 .unwrap_or(exp);
4406 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4407 }
4408 }
4409}
4410
4411#[allow(clippy::too_many_arguments)]
4418fn bind_pending_flag_value(
4419 spec: &Spec,
4420 cmd: &SpecCommand,
4421 errors: &mut Vec<UsageErr>,
4422 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4423 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4424 word: &mut String,
4425 input: &mut VecDeque<Token>,
4426 custom_env: Option<&HashMap<String, String>>,
4427 trace: &mut Trace,
4428 argv: usize,
4432 attached: bool,
4433) -> miette::Result<bool> {
4434 let collecting = flag_awaiting_value
4438 .last()
4439 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4440 .cloned()
4441 .map(|flag| {
4442 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4443 (flag, carried)
4444 });
4445 let mut bound = vec![];
4446 let refused = drain_pending_flag_values(
4447 spec,
4448 cmd,
4449 errors,
4450 flags,
4451 flag_awaiting_value,
4452 word,
4453 custom_env,
4454 &mut bound,
4455 )?;
4456 for (flag, values) in bound {
4457 trace.record(
4458 argv,
4459 TokenRole::Value {
4460 flag,
4461 values,
4462 attached,
4463 },
4464 );
4465 }
4466 if refused {
4467 return Ok(true);
4468 }
4469 let Some((flag, carried)) = collecting else {
4470 return Ok(false);
4471 };
4472 collect_variadic_flag_values(
4473 spec,
4474 cmd,
4475 errors,
4476 flags,
4477 flag_awaiting_value,
4478 &flag,
4479 carried,
4480 input,
4481 custom_env,
4482 trace,
4483 )
4484}
4485
4486#[allow(clippy::too_many_arguments)]
4499fn collect_variadic_flag_values(
4500 spec: &Spec,
4501 cmd: &SpecCommand,
4502 errors: &mut Vec<UsageErr>,
4503 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4504 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4505 flag: &Arc<SpecFlag>,
4506 carried: usize,
4507 input: &mut VecDeque<Token>,
4508 custom_env: Option<&HashMap<String, String>>,
4509 trace: &mut Trace,
4510) -> miette::Result<bool> {
4511 let max = flag
4512 .arg
4513 .as_ref()
4514 .and_then(|arg| arg.var_max)
4515 .unwrap_or(usize::MAX);
4516 while flags
4517 .get(flag)
4518 .map(value_count)
4519 .unwrap_or(0)
4520 .saturating_sub(carried)
4521 < max
4522 {
4523 let Some(next) = input.front().map(|token| token.word.as_str()) else {
4524 break;
4525 };
4526 if flag
4527 .arg
4528 .as_ref()
4529 .and_then(|arg| arg.value_terminator.as_deref())
4530 == Some(next)
4531 {
4532 let terminator = input.pop_front().unwrap();
4533 trace.record(
4534 terminator.argv,
4535 TokenRole::ValueTerminator {
4536 ends: flag.name.clone(),
4537 },
4538 );
4539 break;
4540 }
4541 if next == "--"
4544 || (is_flag_like(next)
4545 && !(flag
4546 .arg
4547 .as_ref()
4548 .is_some_and(|arg| arg.allow_negative_numbers)
4549 && is_negative_number(next)))
4550 {
4551 break;
4552 }
4553 let taken = input.pop_front().unwrap();
4554 let argv = taken.argv;
4555 let mut word = taken.word;
4556 flag_awaiting_value.push(Arc::clone(flag));
4557 let mut bound = vec![];
4558 let refused = drain_pending_flag_values(
4559 spec,
4560 cmd,
4561 errors,
4562 flags,
4563 flag_awaiting_value,
4564 &mut word,
4565 custom_env,
4566 &mut bound,
4567 )?;
4568 for (flag, values) in bound {
4569 trace.record(
4572 argv,
4573 TokenRole::Value {
4574 flag,
4575 values,
4576 attached: false,
4577 },
4578 );
4579 }
4580 if refused {
4581 return Ok(true);
4582 }
4583 }
4584 let taken = flags
4593 .get(flag)
4594 .map(value_count)
4595 .unwrap_or(0)
4596 .saturating_sub(carried);
4597 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4598 if taken < min {
4599 errors.push(UsageErr::VarFlagTooFew {
4600 name: flag.name.clone(),
4601 min,
4602 got: taken,
4603 });
4604 }
4605 }
4606 if taken > max {
4607 errors.push(UsageErr::VarFlagTooMany {
4608 name: flag.name.clone(),
4609 max,
4610 got: taken,
4611 });
4612 }
4613 Ok(false)
4614}
4615
4616fn value_count(value: &ParseValue) -> usize {
4618 match value {
4619 ParseValue::MultiString(values) => values.len(),
4620 ParseValue::MultiBool(values) => values.len(),
4621 _ => 1,
4622 }
4623}
4624
4625fn try_bind_default_missing(
4636 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4637 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4638 custom_env: Option<&HashMap<String, String>>,
4639 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4640) -> miette::Result<bool> {
4641 let Some(flag) = flag_awaiting_value.last() else {
4642 return Ok(false);
4643 };
4644 let value = match flag.default_missing.clone() {
4645 Some(value) => value,
4646 None if flag.value_optional => {
4647 let flag = flag_awaiting_value.pop().unwrap();
4648 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4652 origins
4653 .entry(Arc::clone(&flag))
4654 .or_default()
4655 .push(ValueOrigin::DefaultMissing);
4656 if flag.var {
4657 flags
4662 .entry(flag)
4663 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4664 .try_as_multi_string_mut()
4665 .unwrap()
4666 .push(String::new());
4667 } else if variadic_value {
4668 flags
4671 .entry(flag)
4672 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4673 } else {
4674 flags.insert(flag, ParseValue::MultiString(Vec::new()));
4678 }
4679 return Ok(true);
4680 }
4681 None => return Ok(false),
4682 };
4683 if let Some(arg) = flag.arg.as_ref() {
4684 validate_choice_value(
4685 ChoiceTarget::option(flag),
4686 &value,
4687 arg.choices.as_ref(),
4688 custom_env,
4689 )?;
4690 }
4691 let flag = flag_awaiting_value.pop().unwrap();
4692 origins
4693 .entry(Arc::clone(&flag))
4694 .or_default()
4695 .push(ValueOrigin::DefaultMissing);
4696 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4697 if collecting {
4698 let arr = flags
4699 .entry(flag)
4700 .or_insert_with(|| ParseValue::MultiString(vec![]))
4701 .try_as_multi_string_mut()
4702 .unwrap();
4703 arr.push(value);
4704 } else {
4705 flags.insert(flag, ParseValue::String(value));
4706 }
4707 Ok(true)
4708}
4709
4710#[allow(clippy::too_many_arguments)]
4715fn drain_pending_flag_values(
4716 spec: &Spec,
4717 cmd: &SpecCommand,
4718 errors: &mut Vec<UsageErr>,
4719 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4720 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4721 word: &mut String,
4722 custom_env: Option<&HashMap<String, String>>,
4723 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
4724) -> miette::Result<bool> {
4725 while let Some(flag) = flag_awaiting_value.pop() {
4726 let arg = flag.arg.as_ref().unwrap();
4727 let parts: Vec<String> = match arg.delimiter {
4731 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
4732 None => vec![std::mem::take(word)],
4733 };
4734 for part in &parts {
4735 if validate_choices(
4736 spec,
4737 cmd,
4738 errors,
4739 ChoiceTarget::option(&flag),
4740 part,
4741 arg.choices.as_ref(),
4742 custom_env,
4743 )? {
4744 return Ok(true);
4745 }
4746 }
4747 word.clear();
4748 bound.push((Arc::clone(&flag), parts.clone()));
4749 if flag.var || arg.var {
4752 let arr = flags
4753 .entry(flag)
4754 .or_insert_with(|| ParseValue::MultiString(vec![]))
4755 .try_as_multi_string_mut()
4756 .unwrap();
4757 arr.extend(parts);
4758 } else {
4759 flags.insert(
4762 flag,
4763 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
4764 );
4765 }
4766 }
4767 Ok(false)
4768}
4769
4770fn choice_error(
4771 target: ChoiceTarget<'_>,
4772 value: &str,
4773 choices: Option<&SpecChoices>,
4774 custom_env: Option<&HashMap<String, String>>,
4775) -> Option<String> {
4776 let choices = choices?;
4777 if !choices.strict {
4778 return None;
4779 }
4780 let values = choices.values_with_env(custom_env);
4781 if choices.matches_with_env(value, custom_env) {
4782 return None;
4783 }
4784 if let Some(env) = choices.env() {
4785 if values.is_empty() {
4786 return Some(format!(
4787 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
4788 target.kind, target.name,
4789 ));
4790 }
4791 }
4792 Some(format!(
4793 "Invalid choice for {} {}: {value}, expected one of {}",
4794 target.kind,
4795 target.name,
4796 values.join(", ")
4797 ))
4798}
4799
4800fn validate_choices(
4801 spec: &Spec,
4802 cmd: &SpecCommand,
4803 errors: &mut Vec<UsageErr>,
4804 target: ChoiceTarget<'_>,
4805 value: &str,
4806 choices: Option<&SpecChoices>,
4807 custom_env: Option<&HashMap<String, String>>,
4808) -> miette::Result<bool> {
4809 if is_help_arg(spec, cmd, value)
4810 && choices
4811 .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
4812 {
4813 errors.push(render_help_err(spec, cmd, value.len() > 2));
4814 return Ok(true);
4815 }
4816
4817 if let Some(err) = choice_error(target, value, choices, custom_env) {
4818 bail!("{err}");
4819 }
4820 Ok(false)
4821}
4822
4823fn validate_choice_value(
4824 target: ChoiceTarget<'_>,
4825 value: &str,
4826 choices: Option<&SpecChoices>,
4827 custom_env: Option<&HashMap<String, String>>,
4828) -> miette::Result<()> {
4829 if let Some(err) = choice_error(target, value, choices, custom_env) {
4830 bail!("{err}");
4831 }
4832 Ok(())
4833}
4834
4835fn validate_choice_values(
4836 target: ChoiceTarget<'_>,
4837 values: &[String],
4838 choices: Option<&SpecChoices>,
4839 custom_env: Option<&HashMap<String, String>>,
4840) -> miette::Result<()> {
4841 for value in values {
4842 validate_choice_value(target, value, choices, custom_env)?;
4843 }
4844 Ok(())
4845}
4846
4847fn record_stop(
4854 out: &mut ParseOutput,
4855 next_arg_idx: usize,
4856 seen_double_dash: bool,
4857 trace: &mut Trace,
4858 unread: &VecDeque<Token>,
4859) {
4860 out.next_arg = out
4861 .cmd
4862 .args
4863 .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
4864 .cloned()
4865 .map(Arc::new);
4866 out.double_dash_seen = seen_double_dash;
4867 finalize_current_clause(out);
4868 trace.close(unread);
4869 out.tokens = std::mem::take(&mut trace.tokens);
4870}
4871
4872fn finalize_current_clause(out: &mut ParseOutput) {
4873 let Some(clause) = out.cmd.clause.as_ref() else {
4874 return;
4875 };
4876 let has_scoped_flags = clause
4877 .flags
4878 .iter()
4879 .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
4880 if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
4881 return;
4882 }
4883 out.clauses
4884 .entry(clause.name.clone())
4885 .or_default()
4886 .push(std::mem::take(&mut out.args));
4887 let mut flags = IndexMap::new();
4888 for clause_flag in &clause.flags {
4889 let key = Arc::new(clause_flag.clone());
4890 if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
4891 flags.insert(flag, value);
4892 }
4893 }
4894 out.clause_flags
4895 .entry(clause.name.clone())
4896 .or_default()
4897 .push(flags);
4898}
4899
4900fn restore_current_clause(out: &mut ParseOutput) {
4901 let Some(clause) = out.cmd.clause.as_ref() else {
4902 return;
4903 };
4904 if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
4905 out.args = current;
4906 }
4907 if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
4908 out.flags.extend(current);
4909 }
4910}
4911
4912fn reset_clause_scalar_occurrences(
4913 out: &ParseOutput,
4914 occurrences: &mut HashMap<(usize, usize), u8>,
4915) {
4916 let Some(clause) = out.cmd.clause.as_ref() else {
4917 return;
4918 };
4919 let names = clause
4920 .flags
4921 .iter()
4922 .map(|flag| flag.name.as_str())
4923 .collect::<HashSet<_>>();
4924 let pointers = unique_flags(out.available_flags.values())
4925 .filter(|flag| names.contains(flag.name.as_str()))
4926 .map(|flag| Arc::as_ptr(flag) as usize)
4927 .collect::<HashSet<_>>();
4928 occurrences.retain(|(flag, _), _| !pointers.contains(flag));
4929}
4930
4931fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
4932 while active_args(cmd)
4933 .get(idx)
4934 .is_some_and(|arg| arg.sigil.is_some())
4935 {
4936 idx += 1;
4937 }
4938 idx
4939}
4940
4941fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
4942 cmd.clause
4943 .as_ref()
4944 .map(|clause| clause.args.as_slice())
4945 .unwrap_or(cmd.args.as_slice())
4946}
4947
4948fn match_sigil_arg<'a>(
4949 cmd: &'a SpecCommand,
4950 word: &'a str,
4951) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4952 cmd.args
4953 .iter()
4954 .filter_map(|arg| {
4955 let sigil = arg.sigil.as_deref()?;
4956 let value = word.strip_prefix(sigil)?;
4957 Some((arg, sigil, value))
4958 })
4959 .max_by_key(|(_, sigil, _)| sigil.len())
4960}
4961
4962fn match_sigil_arg_chain<'a>(
4963 cmds: &'a [SpecCommand],
4964 word: &'a str,
4965) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4966 cmds.iter()
4967 .filter_map(|cmd| match_sigil_arg(cmd, word))
4968 .max_by_key(|(_, sigil, _)| sigil.len())
4969}
4970
4971fn report_double_dash_violation(
4978 arg: &SpecArg,
4979 errors: &mut Vec<UsageErr>,
4980 violations: &mut HashSet<String>,
4981) {
4982 if violations.insert(arg.name.clone()) {
4983 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4984 }
4985}
4986
4987fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5002 (spec.version.is_some() || spec.long_version.is_some())
5003 && cmds.len() == 1
5004 && !spec.cmd.disable_version_flag
5005 && (w == "--version" || w == "-V")
5006}
5007
5008fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5009 spec.disable_help != Some(true)
5010 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5011 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5012}
5013
5014impl ParseOutput {
5015 pub fn as_env(&self) -> BTreeMap<String, String> {
5016 let mut env = BTreeMap::new();
5017 for (flag, val) in &self.flags {
5018 let key = format!("usage_{}", crate::case::snake(&flag.name));
5019 let val = match val {
5020 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5021 ParseValue::String(s) => s.clone(),
5022 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5023 ParseValue::MultiString(s) => crate::shell_words::join(s),
5024 };
5025 env.insert(key, val);
5026 }
5027 for (arg, val) in &self.args {
5028 let key = format!("usage_{}", crate::case::snake(&arg.name));
5029 env.insert(key, val.to_string());
5030 }
5031 env
5032 }
5033}
5034
5035impl Display for ParseValue {
5036 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5037 match self {
5038 ParseValue::Bool(b) => write!(f, "{b}"),
5039 ParseValue::String(s) => write!(f, "{s}"),
5040 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5041 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5042 }
5043 }
5044}
5045
5046fn render_token(token: &TokenBinding) -> String {
5048 let roles = token.roles.iter().map(render_role).join(", ");
5049 let synthesized = if token.synthesized { " (read as)" } else { "" };
5050 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5051}
5052
5053fn render_role(role: &TokenRole) -> String {
5054 match role {
5055 TokenRole::Program => "program".to_string(),
5056 TokenRole::Command { name } => format!("subcommand {name}"),
5057 TokenRole::Flag {
5058 flag,
5059 spelling,
5060 negated,
5061 } => {
5062 let negated = if *negated { ", negated" } else { "" };
5063 format!("flag {} as {spelling}{negated}", flag.name)
5064 }
5065 TokenRole::Value {
5066 flag,
5067 values,
5068 attached,
5069 } => {
5070 let attached = if *attached { ", attached" } else { "" };
5071 format!("value of {} = {values:?}{attached}", flag.name)
5072 }
5073 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5074 TokenRole::Sigil { arg, sigil, values } => {
5075 format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5076 }
5077 TokenRole::Separator => "separator".to_string(),
5078 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5079 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5080 TokenRole::Restart => "restart".to_string(),
5081 TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5082 TokenRole::UnknownFlag { bound_as } => match bound_as {
5083 Some(arg) => format!("unknown flag, bound as {}", arg.name),
5084 None => "unknown flag".to_string(),
5085 },
5086 TokenRole::Refused { reason } => format!("refused: {reason}"),
5087 TokenRole::External => "external".to_string(),
5088 TokenRole::Unread => "unread".to_string(),
5089 }
5090}
5091
5092impl Debug for ParseOutput {
5093 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5094 f.debug_struct("ParseOutput")
5095 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5096 .field(
5097 "args",
5098 &self
5099 .args
5100 .iter()
5101 .map(|(a, w)| format!("{}: {w}", a.name))
5102 .collect_vec(),
5103 )
5104 .field("clauses", &self.clauses)
5105 .field(
5106 "available_flags",
5107 &self
5108 .available_flags
5109 .iter()
5110 .map(|(f, w)| format!("{f}: {w}"))
5111 .collect_vec(),
5112 )
5113 .field(
5114 "flags",
5115 &self
5116 .flags
5117 .iter()
5118 .map(|(f, w)| format!("{}: {w}", f.name))
5119 .collect_vec(),
5120 )
5121 .field("flag_awaiting_value", &self.flag_awaiting_value)
5122 .field("errors", &self.errors)
5123 .field("external", &self.external)
5124 .field(
5128 "tokens",
5129 &self.tokens.iter().map(render_token).collect_vec(),
5130 )
5131 .field(
5132 "origins",
5133 &self
5134 .flag_origins
5135 .iter()
5136 .map(|(f, o)| format!("{}: {o:?}", f.name))
5137 .chain(
5138 self.arg_origins
5139 .iter()
5140 .map(|(a, o)| format!("{}: {o:?}", a.name)),
5141 )
5142 .collect_vec(),
5143 )
5144 .field("overridden_flags", &self.overridden_flags)
5145 .finish()
5146 }
5147}
5148
5149#[cfg(test)]
5150mod tests {
5151 use super::*;
5152 use crate::SpecFlagAction;
5153
5154 fn input(words: &[&str]) -> Vec<String> {
5155 words.iter().map(|word| (*word).to_string()).collect()
5156 }
5157
5158 #[test]
5159 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5160 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5165 .parse()
5166 .unwrap();
5167
5168 for spelling in ["--version", "-V"] {
5169 let err = parse(&spec, &input(&["ex", spelling]))
5170 .expect_err("answering with a version ends the parse");
5171 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5172 }
5173 }
5174
5175 #[test]
5176 fn the_supplied_version_flag_is_the_roots_alone() {
5177 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5180 .parse()
5181 .unwrap();
5182
5183 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5184 assert_eq!(err.to_string(), "unexpected word: --version");
5185 }
5186
5187 #[test]
5188 fn no_declared_version_supplies_nothing() {
5189 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5192
5193 for spelling in ["--version", "-V"] {
5194 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5195 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5196 }
5197 }
5198
5199 #[test]
5200 fn disable_version_flag_removes_the_supplied_spellings() {
5201 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5202 .parse()
5203 .unwrap();
5204
5205 for spelling in ["--version", "-V"] {
5206 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5207 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5208 }
5209 }
5210
5211 #[test]
5212 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5213 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5216 .parse()
5217 .unwrap();
5218
5219 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5220 assert_eq!(out.flags.len(), 1);
5221
5222 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5223 assert_eq!(err.to_string(), "1.2.3");
5224 }
5225
5226 #[test]
5227 fn the_supplied_spellings_split_the_two_version_texts() {
5228 let spec: Spec =
5231 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5232 .parse()
5233 .unwrap();
5234
5235 assert_eq!(
5236 parse(&spec, &input(&["ex", "--version"]))
5237 .unwrap_err()
5238 .to_string(),
5239 "1.2.3 (abcdef)"
5240 );
5241 assert_eq!(
5242 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5243 "1.2.3"
5244 );
5245 }
5246
5247 #[test]
5248 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5249 let spec: Spec =
5254 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5255 .parse()
5256 .unwrap();
5257
5258 for token in ["-vh", "-hv"] {
5259 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5260 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5261 }
5262 for token in ["-vV", "-Vv"] {
5263 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5264 assert_eq!(err.to_string(), "1.2.3", "{token}");
5265 }
5266 }
5267
5268 #[test]
5269 fn a_bundled_help_letter_asks_for_the_short_page() {
5270 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"
5273 .parse()
5274 .unwrap();
5275
5276 let short = parse(&spec, &input(&["ex", "-vh"]))
5277 .unwrap_err()
5278 .to_string();
5279 let long = parse(&spec, &input(&["ex", "--help"]))
5280 .unwrap_err()
5281 .to_string();
5282 assert!(short.contains("Be loud"), "{short}");
5283 assert!(!short.contains("at length"), "{short}");
5284 assert!(long.contains("at length"), "{long}");
5285 }
5286
5287 #[test]
5288 fn the_bundled_version_letter_is_the_roots_alone() {
5289 let spec: Spec =
5291 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5292 .parse()
5293 .unwrap();
5294
5295 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5296 assert_eq!(err.to_string(), "unexpected word: -vV");
5297 }
5298
5299 #[test]
5300 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5301 let spec: Spec =
5304 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5305 .parse()
5306 .unwrap();
5307
5308 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5309 assert_eq!(out.flags.len(), 2);
5310 assert!(out
5311 .flags
5312 .iter()
5313 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
5314 }
5315
5316 #[test]
5317 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5318 let spec: Spec =
5319 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5320 .parse()
5321 .unwrap();
5322
5323 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5324 assert_eq!(err.to_string(), "unexpected word: -vh");
5325 }
5326
5327 #[test]
5328 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5329 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5332 .parse()
5333 .unwrap();
5334
5335 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5336 assert!(out.flags.is_empty(), "{:?}", out.flags);
5337 assert_eq!(out.args.len(), 1);
5338 }
5339
5340 fn spec_with_arg(arg: SpecArg) -> Spec {
5341 let cmd = SpecCommand::builder().name("test").arg(arg).build();
5342 Spec {
5343 name: "test".to_string(),
5344 bin: "test".to_string(),
5345 cmd,
5346 ..Default::default()
5347 }
5348 }
5349
5350 fn spec_with_flag(flag: SpecFlag) -> Spec {
5351 let cmd = SpecCommand::builder().name("test").flag(flag).build();
5352 Spec {
5353 name: "test".to_string(),
5354 bin: "test".to_string(),
5355 cmd,
5356 ..Default::default()
5357 }
5358 }
5359
5360 fn parse_with_env(
5361 spec: &Spec,
5362 words: &[&str],
5363 env: &[(&str, &str)],
5364 ) -> Result<ParseOutput, miette::Error> {
5365 let env = env
5366 .iter()
5367 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5368 .collect();
5369 Parser::new(spec).with_env(env).parse(&input(words))
5370 }
5371
5372 fn first_string_value(parsed: &ParseOutput) -> &str {
5373 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5374 return value;
5375 }
5376 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5377 return value;
5378 }
5379 panic!("expected first parsed value to be ParseValue::String");
5380 }
5381
5382 #[test]
5383 fn custom_environment_parser_dispatches_executable_views() {
5384 let spec: Spec = r#"
5385bin "ex"
5386view "runner" root="run"
5387cmd "run" {
5388 flag "--token <token>" env="TOKEN"
5389}
5390 "#
5391 .parse()
5392 .unwrap();
5393 let parsed = Parser::new(&spec)
5394 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5395 .parse(&input(&["runner"]))
5396 .unwrap();
5397
5398 assert_eq!(parsed.cmd.name, "runner");
5399 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5400 && matches!(value, ParseValue::String(value) if value == "secret")));
5401 }
5402
5403 #[test]
5404 fn an_executable_view_keeps_the_hosts_version_action() {
5405 let spec: Spec = r#"
5406bin "ex"
5407version "1.2.3"
5408flag "-V --version" action="version"
5409flag "--verbose" global=#true
5410view "runner" root="run" globals=#true
5411cmd "run"
5412 "#
5413 .parse()
5414 .unwrap();
5415
5416 let error = Parser::new(&spec)
5417 .parse(&input(&["runner", "--version"]))
5418 .expect_err("the host version action should answer before view projection");
5419 assert_eq!(error.to_string(), "1.2.3");
5420
5421 let error = Parser::new(&spec)
5422 .parse(&input(&["runner", "--verbose", "--version"]))
5423 .expect_err("the host version action should remain after a carried global");
5424 assert_eq!(error.to_string(), "1.2.3");
5425 }
5426
5427 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5428 let flag = parsed
5429 .flags
5430 .keys()
5431 .find(|flag| flag.name == name)
5432 .unwrap_or_else(|| panic!("expected flag {name}"));
5433 let value = parsed
5434 .flags
5435 .get(flag)
5436 .unwrap_or_else(|| panic!("expected value for flag {name}"));
5437 match value {
5438 ParseValue::String(value) => value,
5439 _ => panic!("expected flag {name} to be ParseValue::String"),
5440 }
5441 }
5442
5443 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5444 let err = result.expect_err("expected parser error");
5445 assert_eq!(format!("{err}"), expected);
5446 }
5447
5448 #[test]
5449 fn a_short_version_action_falls_back_to_the_long_version() {
5450 let flag = SpecFlag::builder()
5451 .short('R')
5452 .action(SpecFlagAction::Version)
5453 .build();
5454 let spec = Spec {
5455 name: "test".to_string(),
5456 bin: "test".to_string(),
5457 long_version: Some("1.2.3\ncommit abc123".to_string()),
5458 ..Default::default()
5459 };
5460 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
5461 panic!("expected version action")
5462 };
5463 assert_eq!(version, "1.2.3\ncommit abc123");
5464 }
5465
5466 #[cfg(feature = "unstable_choices_env")]
5467 fn spec_arg_choices_env(key: &str) -> Spec {
5468 spec_with_arg(
5469 SpecArg::builder()
5470 .name("env")
5471 .choices_env(key)
5472 .required(false)
5473 .build(),
5474 )
5475 }
5476
5477 #[cfg(feature = "unstable_choices_env")]
5478 fn spec_flag_choices_env(key: &str) -> Spec {
5479 spec_with_flag(
5480 SpecFlag::builder()
5481 .long("env")
5482 .arg(SpecArg::builder().name("env").choices_env(key).build())
5483 .build(),
5484 )
5485 }
5486
5487 #[test]
5488 fn test_parse() {
5489 let cmd = SpecCommand::builder()
5490 .name("test")
5491 .arg(SpecArg::builder().name("arg").build())
5492 .flag(SpecFlag::builder().long("flag").build())
5493 .build();
5494 let spec = Spec {
5495 name: "test".to_string(),
5496 bin: "test".to_string(),
5497 cmd,
5498 ..Default::default()
5499 };
5500 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5501 let parsed = parse(&spec, &input).unwrap();
5502 assert_eq!(parsed.cmds.len(), 1);
5503 assert_eq!(parsed.cmds[0].name, "test");
5504 assert_eq!(parsed.args.len(), 1);
5505 assert_eq!(parsed.flags.len(), 1);
5506 assert_eq!(parsed.available_flags.len(), 1);
5507 }
5508
5509 #[test]
5510 fn test_flag_overrides_last_occurrence_wins() {
5511 let spec: Spec = r#"
5512flag "--stdin" default=#true
5513flag "--file <file>" overrides="--stdin"
5514 "#
5515 .parse()
5516 .unwrap();
5517
5518 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5519 assert_eq!(file_wins.flags.len(), 1);
5520 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5521 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5522
5523 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5524 assert_eq!(stdin_wins.flags.len(), 1);
5525 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5526 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5527 }
5528
5529 #[test]
5530 fn test_flag_override_clears_pending_value() {
5531 let spec: Spec = r#"
5532flag "--file <file>" overrides="--stdin"
5533flag "--stdin"
5534arg "[input]"
5535 "#
5536 .parse()
5537 .unwrap();
5538
5539 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5540 assert_eq!(parsed.flags.len(), 1);
5541 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5542 assert_eq!(first_string_value(&parsed), "input.txt");
5543 }
5544
5545 #[cfg(unix)]
5546 #[test]
5547 fn a_mount_on_the_root_discovers_subcommands() {
5548 let spec: Spec = r#"
5552name "ex"
5553bin "ex"
5554cmd "declared"
5555mount run="echo 'cmd \"discovered\"'"
5556"#
5557 .parse()
5558 .unwrap();
5559
5560 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5561 assert_eq!(out.cmd.name, "discovered");
5562 }
5563
5564 #[test]
5565 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5566 let spec: Spec = r#"
5567name "ex"
5568bin "ex"
5569mount run="this command must never run"
5570cmd "declared"
5571"#
5572 .parse()
5573 .unwrap();
5574
5575 Parser::new(&spec)
5576 .with_mount_outputs(HashMap::new())
5577 .parse(&input(&["ex", "declared"]))
5578 .expect("a declared command does not resolve the mount");
5579
5580 let error = Parser::new(&spec)
5581 .with_mount_outputs(HashMap::new())
5582 .parse(&input(&["ex", "discovered"]))
5583 .unwrap_err();
5584 assert!(
5585 error
5586 .to_string()
5587 .contains("No injected output was provided for mount command"),
5588 "{error}"
5589 );
5590 }
5591
5592 #[cfg(unix)]
5593 #[test]
5594 fn completion_sees_root_mounted_commands_with_nothing_typed() {
5595 let spec: Spec = r#"
5599name "ex"
5600bin "ex"
5601cmd "declared"
5602mount run="echo 'cmd \"discovered\"'"
5603"#
5604 .parse()
5605 .unwrap();
5606
5607 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5608 assert!(
5609 out.cmd.subcommands.contains_key("discovered"),
5610 "a completion should see mounted commands; got {:?}",
5611 out.cmd.subcommands.keys().collect::<Vec<_>>()
5612 );
5613 }
5614
5615 #[cfg(unix)]
5616 #[test]
5617 fn completion_and_execution_agree_about_discovery() {
5618 let spec: Spec = r#"
5622name "ex"
5623bin "ex"
5624default_subcommand "run"
5625cmd "run" {
5626 arg "<task>"
5627}
5628mount run="exit 1"
5629"#
5630 .parse()
5631 .unwrap();
5632
5633 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5634 assert!(
5635 !out.cmd.subcommands.contains_key("discovered"),
5636 "a completion must not offer what execution will not route"
5637 );
5638
5639 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5640 assert_eq!(out.cmd.name, "run");
5641 }
5642
5643 #[cfg(unix)]
5644 #[test]
5645 fn a_default_subcommand_outranks_discovery() {
5646 let spec: Spec = r#"
5649name "ex"
5650bin "ex"
5651default_subcommand "run"
5652cmd "run" {
5653 arg "<task>"
5654}
5655mount run="exit 1"
5656"#
5657 .parse()
5658 .unwrap();
5659
5660 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5661 assert_eq!(out.cmd.name, "run");
5662 }
5663
5664 #[cfg(unix)]
5665 #[test]
5666 fn a_mount_may_ask_to_outrank_the_default() {
5667 let spec: Spec = r#"
5670name "ex"
5671bin "ex"
5672default_subcommand "run"
5673cmd "run" {
5674 arg "<task>"
5675}
5676mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5677"#
5678 .parse()
5679 .unwrap();
5680
5681 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5682 assert_eq!(out.cmd.name, "discovered");
5683
5684 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5686 assert_eq!(out.cmd.name, "run");
5687 }
5688
5689 #[cfg(unix)]
5690 #[test]
5691 fn a_flag_does_not_run_the_mount() {
5692 let spec: Spec = r#"
5696name "ex"
5697bin "ex"
5698flag "--verbose"
5699cmd "declared"
5700mount run="exit 1"
5701"#
5702 .parse()
5703 .unwrap();
5704
5705 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
5706 assert_eq!(out.cmd.name, "ex");
5707 }
5708
5709 #[cfg(unix)]
5710 #[test]
5711 fn a_declared_subcommand_does_not_run_the_mount() {
5712 let spec: Spec = r#"
5716name "ex"
5717bin "ex"
5718cmd "declared"
5719mount run="exit 1"
5720"#
5721 .parse()
5722 .unwrap();
5723
5724 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
5725 assert_eq!(out.cmd.name, "declared");
5726 }
5727
5728 #[test]
5729 fn a_root_mount_survives_being_written_out() {
5730 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
5731 .parse()
5732 .unwrap();
5733 assert_eq!(spec.cmd.mounts.len(), 1);
5734
5735 let reparsed: Spec = spec.to_string().parse().unwrap();
5736 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
5737 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
5738 }
5739
5740 #[test]
5741 fn test_mount_prefix_applies_flag_overrides() {
5742 let stdin = Arc::new(
5743 SpecFlag::builder()
5744 .name("stdin")
5745 .long("stdin")
5746 .global(true)
5747 .build(),
5748 );
5749 let file = Arc::new(
5750 SpecFlag::builder()
5751 .name("file")
5752 .long("file")
5753 .arg(SpecArg::builder().name("file").build())
5754 .global(true)
5755 .overrides_with(vec!["--stdin".to_string()])
5756 .build(),
5757 );
5758 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
5759
5760 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
5761 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
5762
5763 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
5764 }
5765
5766 #[test]
5767 fn test_flag_override_suppresses_env_value() {
5768 let spec: Spec = r#"
5769flag "--stdin" env="USE_STDIN"
5770flag "--file <file>" overrides="--stdin"
5771 "#
5772 .parse()
5773 .unwrap();
5774
5775 let parsed = parse_with_env(
5776 &spec,
5777 &["test", "--file", "input.txt"],
5778 &[("USE_STDIN", "true")],
5779 )
5780 .unwrap();
5781 assert_eq!(parsed.flags.len(), 1);
5782 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5783 }
5784
5785 #[test]
5786 fn test_flag_override_suppresses_required_check() {
5787 let spec: Spec = r#"
5788flag "--stdin" required=#true
5789flag "--file <file>" overrides="--stdin"
5790 "#
5791 .parse()
5792 .unwrap();
5793
5794 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5795 assert_eq!(parsed.flags.len(), 1);
5796 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5797 }
5798
5799 #[test]
5800 fn test_flag_required_if() {
5801 let spec: Spec = r#"
5802flag "--dir <dir>"
5803flag "--file <file>" required_if="--dir"
5804 "#
5805 .parse()
5806 .unwrap();
5807
5808 parse(&spec, &input(&["test"])).unwrap();
5809 assert_parse_err(
5810 parse(&spec, &input(&["test", "--dir", "src"])),
5811 "Missing required flag: --file <file>",
5812 );
5813 parse(
5814 &spec,
5815 &input(&["test", "--dir", "src", "--file", "input.txt"]),
5816 )
5817 .unwrap();
5818 }
5819
5820 #[test]
5821 fn test_flag_required_unless() {
5822 let spec: Spec = r#"
5823flag "--stdin"
5824flag "--file <file>" required_unless="--stdin"
5825 "#
5826 .parse()
5827 .unwrap();
5828
5829 assert_parse_err(
5830 parse(&spec, &input(&["test"])),
5831 "Missing required flag: --file <file>",
5832 );
5833 parse(&spec, &input(&["test", "--stdin"])).unwrap();
5834 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5835 }
5836
5837 #[test]
5838 fn complete_required_relationship_truth_tables() {
5839 let spec: Spec = r#"
5840name "ex"
5841bin "ex"
5842flag "--mode <mode>"
5843flag "--scope <scope>"
5844flag "--token <token>" {
5845 required_if_eq "--mode" "remote"
5846}
5847flag "--approval <approval>" {
5848 required_if_eq_all "--mode" "remote" "--scope" "global"
5849}
5850flag "--input <input>" {
5851 required_unless "--stdin" "--file"
5852}
5853flag "--checksum <checksum>" {
5854 required_unless_all "--stdin" "--file"
5855}
5856flag "--stdin"
5857flag "--file <file>"
5858arg "[request]" {
5859 requires "--mode" "--scope"
5860}
5861"#
5862 .parse()
5863 .unwrap();
5864 let parse_args = |args: &[&str]| {
5865 parse(
5866 &spec,
5867 &args
5868 .iter()
5869 .map(|arg| (*arg).to_string())
5870 .collect::<Vec<_>>(),
5871 )
5872 };
5873
5874 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
5875 assert!(parse_args(&[
5876 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
5877 ])
5878 .is_err());
5879 parse_args(&[
5880 "ex",
5881 "--mode",
5882 "remote",
5883 "--token",
5884 "secret",
5885 "--scope",
5886 "global",
5887 "--approval",
5888 "yes",
5889 "--stdin",
5890 "--file",
5891 "in",
5892 ])
5893 .unwrap();
5894 parse_args(&[
5895 "ex",
5896 "--mode",
5897 "local",
5898 "--scope",
5899 "project",
5900 "--stdin",
5901 "--checksum",
5902 "sum",
5903 "request.json",
5904 ])
5905 .unwrap();
5906
5907 let reparsed: Spec = spec.to_string().parse().unwrap();
5908 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
5909 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
5910 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
5911 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
5912 }
5913
5914 #[test]
5915 fn test_conditional_requirements_treat_env_as_explicit() {
5916 let spec: Spec = r#"
5917flag "--dir <dir>" env="INPUT_DIR"
5918flag "--stdin" env="USE_STDIN"
5919flag "--file <file>" required_if="--dir" required_unless="--stdin"
5920 "#
5921 .parse()
5922 .unwrap();
5923
5924 assert_parse_err(
5925 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5926 "Missing required flag: --file <file>",
5927 );
5928 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5929 }
5930
5931 #[test]
5932 fn test_custom_env_does_not_fall_back_to_process_env() {
5933 assert!(std::env::var("PATH").is_ok());
5934 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5935
5936 assert_parse_err(
5937 parse_with_env(&spec, &["test"], &[]),
5938 "Missing required flag: --file <file>",
5939 );
5940 }
5941
5942 #[test]
5943 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5944 let spec: Spec = r#"
5945flag "--dir <dir>" default="src"
5946flag "--file <file>" required_if="--dir"
5947 "#
5948 .parse()
5949 .unwrap();
5950
5951 parse(&spec, &input(&["test"])).unwrap();
5952 }
5953
5954 #[test]
5955 fn test_conditional_requirements_see_overridden_flags_as_absent() {
5956 let spec: Spec = r#"
5957flag "--stdin"
5958flag "--dir <dir>" overrides="--stdin"
5959flag "--file <file>" required_unless="--stdin"
5960 "#
5961 .parse()
5962 .unwrap();
5963
5964 assert_parse_err(
5965 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5966 "Missing required flag: --file <file>",
5967 );
5968 }
5969
5970 #[test]
5971 fn short_flag_is_one_character_not_one_byte() {
5972 let spec = spec_with_flag(
5976 SpecFlag::builder()
5977 .short('磨')
5978 .long("polish")
5979 .arg(SpecArg::builder().name("opt").build())
5980 .build(),
5981 );
5982 let attached = Parser::new(&spec)
5983 .parse(&input(&["test", "-磨VALUE"]))
5984 .unwrap();
5985 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5986 let detached = Parser::new(&spec)
5987 .parse(&input(&["test", "-磨", "V"]))
5988 .unwrap();
5989 assert_eq!(flag_string_value(&detached, "polish"), "V");
5990 }
5991
5992 #[test]
5993 fn test_as_env() {
5994 let cmd = SpecCommand::builder()
5995 .name("test")
5996 .arg(SpecArg::builder().name("arg").build())
5997 .flag(SpecFlag::builder().long("flag").build())
5998 .flag(
5999 SpecFlag::builder()
6000 .long("force")
6001 .negate("--no-force")
6002 .build(),
6003 )
6004 .build();
6005 let spec = Spec {
6006 name: "test".to_string(),
6007 bin: "test".to_string(),
6008 cmd,
6009 ..Default::default()
6010 };
6011 let input = vec![
6012 "test".to_string(),
6013 "--flag".to_string(),
6014 "--no-force".to_string(),
6015 ];
6016 let parsed = parse(&spec, &input).unwrap();
6017 let env = parsed.as_env();
6018 assert_eq!(env.len(), 2);
6019 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6020 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6021 }
6022
6023 #[test]
6024 fn test_arg_env_var() {
6025 let cmd = SpecCommand::builder()
6026 .name("test")
6027 .arg(
6028 SpecArg::builder()
6029 .name("input")
6030 .env("TEST_ARG_INPUT")
6031 .required(true)
6032 .build(),
6033 )
6034 .build();
6035 let spec = Spec {
6036 name: "test".to_string(),
6037 bin: "test".to_string(),
6038 cmd,
6039 ..Default::default()
6040 };
6041
6042 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
6044
6045 let input = vec!["test".to_string()];
6046 let parsed = parse(&spec, &input).unwrap();
6047
6048 assert_eq!(parsed.args.len(), 1);
6049 let arg = parsed.args.keys().next().unwrap();
6050 assert_eq!(arg.name, "input");
6051 let value = parsed.args.values().next().unwrap();
6052 assert_eq!(value.to_string(), "test_file.txt");
6053
6054 std::env::remove_var("TEST_ARG_INPUT");
6056 }
6057
6058 #[test]
6059 fn test_flag_env_var_with_arg() {
6060 let cmd = SpecCommand::builder()
6061 .name("test")
6062 .flag(
6063 SpecFlag::builder()
6064 .long("output")
6065 .env("TEST_FLAG_OUTPUT")
6066 .arg(SpecArg::builder().name("file").build())
6067 .build(),
6068 )
6069 .build();
6070 let spec = Spec {
6071 name: "test".to_string(),
6072 bin: "test".to_string(),
6073 cmd,
6074 ..Default::default()
6075 };
6076
6077 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
6079
6080 let input = vec!["test".to_string()];
6081 let parsed = parse(&spec, &input).unwrap();
6082
6083 assert_eq!(parsed.flags.len(), 1);
6084 let flag = parsed.flags.keys().next().unwrap();
6085 assert_eq!(flag.name, "output");
6086 let value = parsed.flags.values().next().unwrap();
6087 assert_eq!(value.to_string(), "output.txt");
6088
6089 std::env::remove_var("TEST_FLAG_OUTPUT");
6091 }
6092
6093 #[test]
6094 fn test_flag_env_var_boolean() {
6095 let cmd = SpecCommand::builder()
6096 .name("test")
6097 .flag(
6098 SpecFlag::builder()
6099 .long("verbose")
6100 .env("TEST_FLAG_VERBOSE")
6101 .build(),
6102 )
6103 .build();
6104 let spec = Spec {
6105 name: "test".to_string(),
6106 bin: "test".to_string(),
6107 cmd,
6108 ..Default::default()
6109 };
6110
6111 std::env::set_var("TEST_FLAG_VERBOSE", "true");
6113
6114 let input = vec!["test".to_string()];
6115 let parsed = parse(&spec, &input).unwrap();
6116
6117 assert_eq!(parsed.flags.len(), 1);
6118 let flag = parsed.flags.keys().next().unwrap();
6119 assert_eq!(flag.name, "verbose");
6120 let value = parsed.flags.values().next().unwrap();
6121 assert_eq!(value.to_string(), "true");
6122
6123 std::env::remove_var("TEST_FLAG_VERBOSE");
6125 }
6126
6127 #[test]
6128 fn test_env_var_precedence() {
6129 let cmd = SpecCommand::builder()
6131 .name("test")
6132 .arg(
6133 SpecArg::builder()
6134 .name("input")
6135 .env("TEST_PRECEDENCE_INPUT")
6136 .required(true)
6137 .build(),
6138 )
6139 .build();
6140 let spec = Spec {
6141 name: "test".to_string(),
6142 bin: "test".to_string(),
6143 cmd,
6144 ..Default::default()
6145 };
6146
6147 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
6149
6150 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6151 let parsed = parse(&spec, &input).unwrap();
6152
6153 assert_eq!(parsed.args.len(), 1);
6154 let value = parsed.args.values().next().unwrap();
6155 assert_eq!(value.to_string(), "cli_file.txt");
6157
6158 std::env::remove_var("TEST_PRECEDENCE_INPUT");
6160 }
6161
6162 #[test]
6163 fn test_flag_var_true_with_single_default() {
6164 let cmd = SpecCommand::builder()
6166 .name("test")
6167 .flag(
6168 SpecFlag::builder()
6169 .long("foo")
6170 .var(true)
6171 .arg(SpecArg::builder().name("foo").build())
6172 .default_value("bar")
6173 .build(),
6174 )
6175 .build();
6176 let spec = Spec {
6177 name: "test".to_string(),
6178 bin: "test".to_string(),
6179 cmd,
6180 ..Default::default()
6181 };
6182
6183 let input = vec!["test".to_string()];
6185 let parsed = parse(&spec, &input).unwrap();
6186
6187 assert_eq!(parsed.flags.len(), 1);
6188 let flag = parsed.flags.keys().next().unwrap();
6189 assert_eq!(flag.name, "foo");
6190 let value = parsed.flags.values().next().unwrap();
6191 match value {
6193 ParseValue::MultiString(v) => {
6194 assert_eq!(v.len(), 1);
6195 assert_eq!(v[0], "bar");
6196 }
6197 _ => panic!("Expected MultiString, got {:?}", value),
6198 }
6199 }
6200
6201 #[test]
6202 fn test_flag_var_true_with_multiple_defaults() {
6203 let cmd = SpecCommand::builder()
6205 .name("test")
6206 .flag(
6207 SpecFlag::builder()
6208 .long("foo")
6209 .var(true)
6210 .arg(SpecArg::builder().name("foo").build())
6211 .default_values(["xyz", "bar"])
6212 .build(),
6213 )
6214 .build();
6215 let spec = Spec {
6216 name: "test".to_string(),
6217 bin: "test".to_string(),
6218 cmd,
6219 ..Default::default()
6220 };
6221
6222 let input = vec!["test".to_string()];
6224 let parsed = parse(&spec, &input).unwrap();
6225
6226 assert_eq!(parsed.flags.len(), 1);
6227 let value = parsed.flags.values().next().unwrap();
6228 match value {
6230 ParseValue::MultiString(v) => {
6231 assert_eq!(v.len(), 2);
6232 assert_eq!(v[0], "xyz");
6233 assert_eq!(v[1], "bar");
6234 }
6235 _ => panic!("Expected MultiString, got {:?}", value),
6236 }
6237 }
6238
6239 #[test]
6240 fn test_flag_var_false_with_default_remains_string() {
6241 let cmd = SpecCommand::builder()
6243 .name("test")
6244 .flag(
6245 SpecFlag::builder()
6246 .long("foo")
6247 .var(false) .arg(SpecArg::builder().name("foo").build())
6249 .default_value("bar")
6250 .build(),
6251 )
6252 .build();
6253 let spec = Spec {
6254 name: "test".to_string(),
6255 bin: "test".to_string(),
6256 cmd,
6257 ..Default::default()
6258 };
6259
6260 let input = vec!["test".to_string()];
6262 let parsed = parse(&spec, &input).unwrap();
6263
6264 assert_eq!(parsed.flags.len(), 1);
6265 let value = parsed.flags.values().next().unwrap();
6266 match value {
6268 ParseValue::String(s) => {
6269 assert_eq!(s, "bar");
6270 }
6271 _ => panic!("Expected String, got {:?}", value),
6272 }
6273 }
6274
6275 #[test]
6276 fn test_arg_var_true_with_single_default() {
6277 let cmd = SpecCommand::builder()
6279 .name("test")
6280 .arg(
6281 SpecArg::builder()
6282 .name("files")
6283 .var(true)
6284 .default_value("default.txt")
6285 .required(false)
6286 .build(),
6287 )
6288 .build();
6289 let spec = Spec {
6290 name: "test".to_string(),
6291 bin: "test".to_string(),
6292 cmd,
6293 ..Default::default()
6294 };
6295
6296 let input = vec!["test".to_string()];
6298 let parsed = parse(&spec, &input).unwrap();
6299
6300 assert_eq!(parsed.args.len(), 1);
6301 let value = parsed.args.values().next().unwrap();
6302 match value {
6304 ParseValue::MultiString(v) => {
6305 assert_eq!(v.len(), 1);
6306 assert_eq!(v[0], "default.txt");
6307 }
6308 _ => panic!("Expected MultiString, got {:?}", value),
6309 }
6310 }
6311
6312 #[test]
6313 fn test_arg_var_true_with_multiple_defaults() {
6314 let cmd = SpecCommand::builder()
6316 .name("test")
6317 .arg(
6318 SpecArg::builder()
6319 .name("files")
6320 .var(true)
6321 .default_values(["file1.txt", "file2.txt"])
6322 .required(false)
6323 .build(),
6324 )
6325 .build();
6326 let spec = Spec {
6327 name: "test".to_string(),
6328 bin: "test".to_string(),
6329 cmd,
6330 ..Default::default()
6331 };
6332
6333 let input = vec!["test".to_string()];
6335 let parsed = parse(&spec, &input).unwrap();
6336
6337 assert_eq!(parsed.args.len(), 1);
6338 let value = parsed.args.values().next().unwrap();
6339 match value {
6341 ParseValue::MultiString(v) => {
6342 assert_eq!(v.len(), 2);
6343 assert_eq!(v[0], "file1.txt");
6344 assert_eq!(v[1], "file2.txt");
6345 }
6346 _ => panic!("Expected MultiString, got {:?}", value),
6347 }
6348 }
6349
6350 #[test]
6351 fn test_arg_var_false_with_default_remains_string() {
6352 let cmd = SpecCommand::builder()
6354 .name("test")
6355 .arg(
6356 SpecArg::builder()
6357 .name("file")
6358 .var(false)
6359 .default_value("default.txt")
6360 .required(false)
6361 .build(),
6362 )
6363 .build();
6364 let spec = Spec {
6365 name: "test".to_string(),
6366 bin: "test".to_string(),
6367 cmd,
6368 ..Default::default()
6369 };
6370
6371 let input = vec!["test".to_string()];
6373 let parsed = parse(&spec, &input).unwrap();
6374
6375 assert_eq!(parsed.args.len(), 1);
6376 let value = parsed.args.values().next().unwrap();
6377 match value {
6379 ParseValue::String(s) => {
6380 assert_eq!(s, "default.txt");
6381 }
6382 _ => panic!("Expected String, got {:?}", value),
6383 }
6384 }
6385
6386 #[test]
6387 fn test_scalar_defaults_validate_only_first_default_choice() {
6388 let specs = [
6389 spec_with_arg(
6390 SpecArg::builder()
6391 .name("env")
6392 .var(false)
6393 .default_values(["dev", "prod"])
6394 .choices(["dev"])
6395 .required(false)
6396 .build(),
6397 ),
6398 spec_with_flag(
6399 SpecFlag::builder()
6400 .long("env")
6401 .arg(
6402 SpecArg::builder()
6403 .name("env")
6404 .default_values(["dev", "prod"])
6405 .choices(["dev"])
6406 .build(),
6407 )
6408 .build(),
6409 ),
6410 ];
6411
6412 for spec in specs {
6413 let parsed = parse(&spec, &input(&["test"])).unwrap();
6414 assert_eq!(first_string_value(&parsed), "dev");
6415 }
6416 }
6417
6418 #[test]
6419 fn a_delimiter_turns_one_word_into_several_values() {
6420 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6421 .parse()
6422 .unwrap();
6423
6424 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6425 let multi = |value: &ParseValue| match value {
6426 ParseValue::MultiString(values) => values.clone(),
6427 other => panic!("expected several values, got {other:?}"),
6428 };
6429 let tags = parsed
6430 .flags
6431 .iter()
6432 .find(|(f, _)| f.name == "tags")
6433 .map(|(_, v)| v)
6434 .unwrap();
6435 assert_eq!(multi(tags), vec!["a", "b", "c"]);
6436 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6437 }
6438
6439 #[test]
6440 fn a_positional_splits_before_its_choices_are_asked() {
6441 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
6445 .parse()
6446 .unwrap();
6447
6448 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6449
6450 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6451 let message = err.to_string();
6452 assert!(message.contains("nowhere"), "{message}");
6453 assert!(
6454 !message.contains("src:nowhere"),
6455 "the bad half should be named, not the whole word: {message}"
6456 );
6457 }
6458
6459 #[test]
6460 fn a_split_value_is_counted_and_judged_as_values() {
6461 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
6465 .parse()
6466 .unwrap();
6467
6468 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6469 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6470 assert!(err.to_string().contains("staging"), "{err}");
6471 assert!(
6472 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6473 "three values should breach var_max=2"
6474 );
6475 }
6476
6477 #[test]
6478 fn a_split_bound_counts_one_occurrence_at_a_time() {
6479 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
6484 .parse()
6485 .unwrap();
6486
6487 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
6488 assert!(
6489 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
6490 "three values out of one word is still three values"
6491 );
6492 parse(
6495 &spec,
6496 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
6497 )
6498 .expect("two per occurrence, twice, is within the bound");
6499 }
6500
6501 #[test]
6502 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
6503 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
6504 .parse()
6505 .unwrap();
6506
6507 parse(
6508 &spec,
6509 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
6510 )
6511 .expect("each occurrence satisfies the bound independently");
6512
6513 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
6514 assert!(
6515 error
6516 .to_string()
6517 .contains("requires at least 2 value(s), got 1"),
6518 "{error:?}"
6519 );
6520 }
6521
6522 #[test]
6523 fn an_exclusive_flag_has_to_be_alone() {
6524 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6525 .parse()
6526 .unwrap();
6527
6528 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6529
6530 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6532 assert!(err.to_string().contains("on its own"), "{err}");
6533
6534 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6537 assert!(err.to_string().contains("on its own"), "{err}");
6538
6539 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6541 }
6542
6543 #[test]
6544 fn an_exclusive_flag_conflicts_with_clause_arguments() {
6545 let spec: Spec = r#"name "ex"
6546bin "ex"
6547flag "--dump" exclusive=#true
6548clause "tasks" separator=":::" {
6549 arg "<task>"
6550}
6551"#
6552 .parse()
6553 .unwrap();
6554
6555 let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6556 assert!(err.to_string().contains("on its own"), "{err}");
6557 }
6558
6559 #[test]
6560 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6561 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6564 .parse()
6565 .unwrap();
6566
6567 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6568 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6569 }
6570
6571 #[test]
6572 fn an_exclusive_flag_bypasses_required_siblings() {
6573 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6574 .parse()
6575 .unwrap();
6576
6577 parse(&spec, &input(&["ex", "--dump"]))
6578 .expect("exclusive is the command's requiredness escape");
6579 assert!(parse(
6580 &spec,
6581 &input(&["ex", "--dump", "--out", "somewhere", "target"])
6582 )
6583 .is_err());
6584 }
6585
6586 #[test]
6587 fn an_environment_value_counts_for_an_exclusive_flag() {
6588 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6589 .parse()
6590 .unwrap();
6591
6592 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6593 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6594 }
6595
6596 #[test]
6597 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6598 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6599 .parse()
6600 .unwrap();
6601
6602 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6603 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6604 }
6605
6606 #[test]
6607 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6608 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6609 .parse()
6610 .unwrap();
6611
6612 parse(&spec, &input(&["ex", "run", "--clean"]))
6613 .expect("the child flag is alone within the child command");
6614 assert!(
6615 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6616 "the parent flag still conflicts with selecting the child"
6617 );
6618 }
6619
6620 #[test]
6621 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6622 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6623 .parse()
6624 .unwrap();
6625
6626 parse(&spec, &input(&["ex", "run", "--clean"]))
6627 .expect("the child-local exclusive flag is alone inside the child command");
6628 assert!(
6629 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6630 "the ancestor spelling still conflicts with selecting the child"
6631 );
6632 }
6633
6634 #[test]
6635 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6636 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
6637 .parse()
6638 .unwrap();
6639
6640 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6641 assert!(
6642 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6643 "the distinct parent declaration is still company despite sharing a name"
6644 );
6645 }
6646
6647 #[test]
6648 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6649 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6650 .parse()
6651 .unwrap();
6652
6653 parse(&spec, &input(&["ex", "run", "--clean"]))
6654 .expect("the child exclusive flag is valid alone");
6655 assert!(
6656 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6657 "merging with the inherited global must not discard child exclusivity"
6658 );
6659 }
6660
6661 #[test]
6662 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6663 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6664 .parse()
6665 .unwrap();
6666
6667 parse(&spec, &input(&["ex", "run", "--clean"]))
6668 .expect("the typed long form belongs to the child declaration");
6669 assert!(
6670 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6671 "the inherited short form still belongs to the ancestor"
6672 );
6673 assert!(
6674 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6675 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6676 );
6677 }
6678
6679 #[test]
6680 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
6681 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
6682 .parse()
6683 .unwrap();
6684
6685 parse(&spec, &input(&["ex", "run", "--clean"]))
6686 .expect("the child's spelling does not activate the orphan ancestor alias");
6687 assert!(
6688 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6689 "the inherited short alias still belongs to the ancestor exclusive flag"
6690 );
6691 }
6692
6693 #[test]
6694 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
6695 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"
6696 .parse()
6697 .unwrap();
6698
6699 assert!(
6700 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
6701 "the inherited negated alias still belongs to the ancestor exclusive flag"
6702 );
6703 }
6704
6705 #[test]
6706 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
6707 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"
6712 .parse()
6713 .unwrap();
6714
6715 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
6716 assert!(
6717 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6718 "one unrelated alias collision cannot disown the child from its own flag"
6719 );
6720 }
6721
6722 #[test]
6723 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
6724 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6730 .parse()
6731 .unwrap();
6732 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
6733 .expect("the ancestor owns this occurrence, and it is not exclusive");
6734 assert!(
6735 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6736 "after the subcommand word the child's declaration is in scope, and it is exclusive"
6737 );
6738
6739 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6740 .parse()
6741 .unwrap();
6742 assert!(
6743 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
6744 "the same rule, with an exclusive ancestor: selecting the child is company for it"
6745 );
6746 }
6747
6748 #[test]
6749 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
6750 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
6754 .parse()
6755 .unwrap();
6756
6757 assert!(
6758 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6759 "the orphan ancestor alias is still the ancestor's exclusive flag"
6760 );
6761 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
6762 .expect("the child's own spelling drops the exclusivity the child did not restate");
6763 }
6764
6765 #[test]
6766 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
6767 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6771 .parse()
6772 .unwrap();
6773
6774 assert!(
6775 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
6776 "the child spelling is exclusive whatever it was typed beside"
6777 );
6778 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
6779 .expect("the ancestor's own spelling was never exclusive");
6780 }
6781
6782 #[test]
6783 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
6784 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"
6788 .parse()
6789 .unwrap();
6790
6791 assert!(
6792 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
6793 "the child added exclusivity the environment value has to honor"
6794 );
6795
6796 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"
6797 .parse()
6798 .unwrap();
6799
6800 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
6801 .expect("the child dropped the exclusivity, and the environment value follows it");
6802 }
6803
6804 #[test]
6805 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
6806 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"
6809 .parse()
6810 .unwrap();
6811
6812 parse(&spec, &input(&["ex", "run", "--clean"]))
6813 .expect("a merged child exclusive flag is still the command's requiredness escape");
6814 }
6815
6816 #[test]
6817 fn a_group_allows_one_member_and_refuses_two() {
6818 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
6819 .parse()
6820 .unwrap();
6821
6822 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
6824 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
6825
6826 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
6827 assert!(err.to_string().contains("group input"), "{err}");
6828 }
6829
6830 #[test]
6831 fn positional_selectors_work_in_conflicts_and_groups() {
6832 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
6833 .parse()
6834 .unwrap();
6835 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
6836 .expect("the flag alone is valid");
6837 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
6838 assert!(parse(
6839 &conflicts,
6840 &input(&["ex", "--from-file", "vars.env", "literal"])
6841 )
6842 .is_err());
6843
6844 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
6845 .parse()
6846 .unwrap();
6847 assert!(parse(
6848 &positional_source,
6849 &input(&["ex", "--from-file", "vars.env", "literal"])
6850 )
6851 .is_err());
6852
6853 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
6854 .parse()
6855 .unwrap();
6856 assert!(parse(&group, &input(&["ex"])).is_err());
6857 parse(&group, &input(&["ex", "target-name"]))
6858 .expect("a positional satisfies a required group");
6859 assert!(parse(
6860 &group,
6861 &input(&["ex", "--file", "input.txt", "target-name"])
6862 )
6863 .is_err());
6864 }
6865
6866 #[test]
6867 fn a_required_group_needs_one_of_its_members() {
6868 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6869 .parse()
6870 .unwrap();
6871
6872 let err = parse(&spec, &input(&["ex"])).unwrap_err();
6873 assert!(err.to_string().contains("--file, --url"), "{err}");
6876 assert!(err.to_string().contains("input"), "{err}");
6877
6878 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
6879 }
6880
6881 #[test]
6882 fn a_multiple_group_only_polices_requiredness() {
6883 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
6886 .parse()
6887 .unwrap();
6888
6889 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
6890 assert!(parse(&spec, &input(&["ex"])).is_err());
6891 }
6892
6893 #[test]
6894 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
6895 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6901 .parse()
6902 .unwrap();
6903
6904 parse(&spec, &input(&["ex"])).expect("the default fills the group");
6905 parse(&spec, &input(&["ex", "--url", "u"]))
6906 .expect("the default must not conflict with the flag the user typed");
6907 }
6908
6909 #[test]
6910 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
6911 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
6914 .parse()
6915 .unwrap();
6916
6917 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
6918 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
6919
6920 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
6922 assert!(err.to_string().contains("group input"), "{err}");
6923 }
6924
6925 #[test]
6926 fn a_group_reads_the_environment_as_given() {
6927 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6930 .parse()
6931 .unwrap();
6932
6933 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
6934 }
6935
6936 #[test]
6937 fn a_requirement_names_the_flag_that_is_missing() {
6938 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6941 .parse()
6942 .unwrap();
6943
6944 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6945 assert!(
6946 err.to_string().contains("format"),
6947 "the missing flag should be named: {err}"
6948 );
6949
6950 for words in [
6952 &["ex", "--out", "a.txt", "--format", "json"][..],
6953 &["ex", "--format", "json", "--out", "a.txt"][..],
6954 ] {
6955 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6956 }
6957
6958 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6961 }
6962
6963 #[test]
6964 fn a_value_activates_only_its_conditional_requirement() {
6965 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"
6966 .parse()
6967 .unwrap();
6968
6969 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6970 .expect("an unrelated value requires nothing");
6971
6972 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6973 assert!(key.to_string().contains("key"), "{key}");
6974 parse(
6975 &spec,
6976 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6977 )
6978 .expect("the matching requirement is satisfied");
6979
6980 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6981 assert!(token.to_string().contains("token"), "{token}");
6982 }
6983
6984 #[test]
6985 fn conditional_requirements_read_explicit_env_but_not_defaults() {
6986 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"
6987 .parse()
6988 .unwrap();
6989 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6990 assert!(err.to_string().contains("key"), "{err}");
6991
6992 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"
6993 .parse()
6994 .unwrap();
6995 parse(&from_default, &input(&["ex"]))
6996 .expect("a default is not an explicit conditional value");
6997 }
6998
6999 #[test]
7000 fn command_line_values_override_env_for_conditional_requirements() {
7001 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7002 .parse()
7003 .unwrap();
7004
7005 parse_with_env(
7006 &spec,
7007 &["ex", "--config", "ordinary.toml"],
7008 &[("EX_CONFIG", "special.toml")],
7009 )
7010 .expect("the command-line value takes precedence over the environment");
7011 }
7012
7013 #[test]
7014 fn conditional_requirements_normalize_boolean_env_values() {
7015 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7016 .parse()
7017 .unwrap();
7018
7019 for value in ["1", "true", "True", "TRUE"] {
7020 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7021 assert!(err.to_string().contains("key"), "{value}: {err}");
7022 }
7023 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7024 .expect("a false environment value does not activate a true condition");
7025 }
7026
7027 #[test]
7028 fn a_default_satisfies_a_requirement() {
7029 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7033 .parse()
7034 .unwrap();
7035
7036 parse(&spec, &input(&["ex", "--out", "a.txt"]))
7037 .expect("a defaulted flag is not a missing one");
7038 }
7039
7040 #[test]
7041 fn a_present_flag_binds_a_conditional_default() {
7042 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7043 .parse()
7044 .unwrap();
7045
7046 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7047 assert_eq!(
7048 with.as_env().get("usage_bin_names").map(String::as_str),
7049 Some("true")
7050 );
7051
7052 let without = parse(&spec, &input(&["ex"])).unwrap();
7053 assert!(
7054 !without.as_env().contains_key("usage_bin_names"),
7055 "IsPresent does nothing when the selector is absent"
7056 );
7057 }
7058
7059 #[test]
7060 fn an_equals_condition_binds_a_conditional_default() {
7061 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7062 .parse()
7063 .unwrap();
7064
7065 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7066 assert_eq!(
7067 json.as_env().get("usage_style").map(String::as_str),
7068 Some("pretty")
7069 );
7070 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7071 assert!(!yaml.as_env().contains_key("usage_style"));
7072 }
7073
7074 #[test]
7075 fn an_equals_condition_reads_a_negated_flag() {
7076 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7077 .parse()
7078 .unwrap();
7079
7080 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7081 assert_eq!(
7082 off.as_env().get("usage_pretty").map(String::as_str),
7083 Some("true")
7084 );
7085 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7086 assert!(
7087 !on.as_env().contains_key("usage_pretty"),
7088 "--json is true, so when=false should miss"
7089 );
7090 }
7091
7092 #[test]
7093 fn the_first_matching_conditional_default_wins() {
7094 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"
7095 .parse()
7096 .unwrap();
7097
7098 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7099 assert_eq!(
7100 out.as_env().get("usage_style").map(String::as_str),
7101 Some("compact")
7102 );
7103 }
7104
7105 #[test]
7106 fn argv_and_env_suppress_a_conditional_default() {
7107 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7108 .parse()
7109 .unwrap();
7110
7111 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7112 assert_eq!(
7113 from_env.as_env().get("usage_bin_names").map(String::as_str),
7114 Some("false"),
7115 "the target's environment wins over default_if"
7116 );
7117 }
7118
7119 #[test]
7120 fn a_sibling_env_activates_a_conditional_default() {
7121 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7122 .parse()
7123 .unwrap();
7124
7125 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7126 assert_eq!(
7127 out.as_env().get("usage_bin_names").map(String::as_str),
7128 Some("true")
7129 );
7130 }
7131
7132 #[test]
7133 fn a_default_does_not_activate_a_conditional_default() {
7134 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7138 .parse()
7139 .unwrap();
7140
7141 let out = parse(&spec, &input(&["ex"])).unwrap();
7142 assert_eq!(
7143 out.as_env().get("usage_json").map(String::as_str),
7144 Some("true")
7145 );
7146 assert!(
7147 !out.as_env().contains_key("usage_pretty"),
7148 "a default is not an explicit value for default_if"
7149 );
7150 }
7151
7152 #[test]
7153 fn a_conditional_default_does_not_activate_requires_if() {
7154 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"
7155 .parse()
7156 .unwrap();
7157
7158 parse(&spec, &input(&["ex", "--json"]))
7159 .expect("a default_if value is not explicit for requires_if");
7160 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7161 }
7162
7163 #[test]
7164 fn a_conditional_default_satisfies_a_requirement() {
7165 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7166 .parse()
7167 .unwrap();
7168
7169 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7170 .expect("default_if fills the required flag");
7171 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7172 }
7173
7174 #[test]
7175 fn an_environment_value_satisfies_a_requirement() {
7176 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7177 .parse()
7178 .unwrap();
7179
7180 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7181 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7182 .expect("the environment supplies it");
7183 }
7184
7185 #[test]
7186 fn a_requirement_is_satisfied_by_a_short_form() {
7187 let spec: Spec =
7191 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7192 .parse()
7193 .unwrap();
7194
7195 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7196
7197 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7198 assert!(err.to_string().contains("key"), "{err}");
7199 }
7200
7201 #[test]
7202 fn conflicting_flags_are_rejected_in_either_order() {
7203 let spec: Spec =
7207 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7208 .parse()
7209 .unwrap();
7210
7211 for words in [
7212 &["ex", "--file", "a.txt", "--stdin"][..],
7213 &["ex", "--stdin", "--file", "a.txt"][..],
7214 ] {
7215 let err = parse(&spec, &input(words)).unwrap_err();
7216 assert!(
7217 err.to_string().contains("conflicts with --stdin"),
7218 "{words:?} should be refused: {err}"
7219 );
7220 }
7221
7222 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7224 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7225 }
7226
7227 #[test]
7228 fn unknown_flags_are_values_by_default() {
7229 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7232 .parse()
7233 .unwrap();
7234 let out = parse(
7235 &spec,
7236 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7237 )
7238 .unwrap();
7239 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7240 assert_eq!(out.args[rest].to_string(), "--wat x");
7241 }
7242
7243 #[test]
7244 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7245 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7246 .parse()
7247 .unwrap();
7248 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7249 .expect("a repeat is a correction by default");
7250 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7251 assert_eq!(out.flags[jobs].to_string(), "2");
7252 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7253 .expect("switches use the same default");
7254
7255 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7256 .parse()
7257 .unwrap();
7258 for words in [
7259 &["ex", "--jobs", "1", "--jobs", "2"][..],
7260 &["ex", "--verbose", "--verbose"][..],
7261 ] {
7262 let err = parse(&strict, &input(words)).unwrap_err();
7263 assert!(
7264 err.to_string().contains("cannot be used multiple times"),
7265 "{err}"
7266 );
7267 }
7268
7269 let reparsed: Spec = strict.to_string().parse().unwrap();
7270 assert!(!reparsed.cmd.args_override_self);
7271 }
7272
7273 #[test]
7274 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7275 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7276 .parse()
7277 .unwrap();
7278
7279 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7280 .expect("opposite forms override each other");
7281 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7282 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7283
7284 for words in [
7285 &["ex", "--color", "--color"][..],
7286 &["ex", "--no-color", "--no-color"][..],
7287 ] {
7288 let err = parse(&spec, &input(words)).unwrap_err();
7289 assert!(err.to_string().contains("cannot be used multiple times"));
7290 }
7291 }
7292
7293 #[test]
7294 fn strict_global_flags_may_repeat_across_command_levels() {
7295 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"
7296 .parse()
7297 .unwrap();
7298
7299 let out = parse(
7300 &spec,
7301 &input(&[
7302 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7303 ]),
7304 )
7305 .expect("an inherited global is allowed once at each command level");
7306 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7307 assert_eq!(out.flags[jobs].to_string(), "2");
7308
7309 for words in [
7310 &["ex", "--color", "--color", "run", "--no-color"][..],
7311 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7312 ] {
7313 let err = parse(&spec, &input(words)).unwrap_err();
7314 assert!(err.to_string().contains("cannot be used multiple times"));
7315 }
7316 }
7317
7318 #[test]
7319 fn a_subcommand_can_negate_only_its_parents_requirements() {
7320 let base = r#"name "ex"
7321bin "ex"
7322flag "--config" required=#true
7323flag "--mode" requires="--config"
7324flag "--other"
7325arg "<input>"
7326group "source" "--config" "--other" required=#true
7327cmd "run" { flag "--child" required=#true }
7328"#;
7329 let strict: Spec = base.parse().unwrap();
7330 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7331 let message = err.to_string();
7332 assert!(
7333 message.contains("input") || message.contains("config"),
7334 "{message}"
7335 );
7336
7337 let negated: Spec = base
7338 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7339 .parse()
7340 .unwrap();
7341 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7342 assert!(
7343 err.to_string().contains("child"),
7344 "the selected command keeps its own requirements: {err}"
7345 );
7346
7347 let mut child_optional = negated.clone();
7348 child_optional.cmd.subcommands["run"].flags[0].required = false;
7349 parse(&child_optional, &input(&["ex", "run"]))
7350 .expect("the child selection satisfies all parent requirements");
7351 parse(&child_optional, &input(&["ex", "--mode", "run"]))
7352 .expect("parent requires relationships are negated too");
7353 }
7354
7355 #[test]
7356 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7357 let spec: Spec = r#"name "ex"
7358bin "ex"
7359args_conflicts_with_subcommands #true
7360flag "--verbose"
7361cmd "run"
7362"#
7363 .parse()
7364 .unwrap();
7365
7366 parse(&spec, &input(&["ex", "run"]))
7367 .expect("the subcommand is valid without a parent argument");
7368 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7369 assert!(
7370 err.to_string().contains("cannot be used with arguments"),
7371 "{err}"
7372 );
7373 }
7374
7375 #[test]
7376 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7377 let base = r#"name "ex"
7378bin "ex"
7379flag "--values <value>..."
7380cmd "run"
7381"#;
7382 let plain: Spec = base.parse().unwrap();
7383 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7384 assert_eq!(out.cmd.name, "ex");
7385
7386 let precedence: Spec = base
7387 .replacen(
7388 "bin \"ex\"",
7389 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7390 1,
7391 )
7392 .parse()
7393 .unwrap();
7394 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7395 assert_eq!(out.cmd.name, "run");
7396 }
7397
7398 #[test]
7399 fn a_required_positional_can_follow_an_unfilled_optional_one() {
7400 let base = r#"name "ex"
7401bin "ex"
7402arg "[optional]"
7403arg "<required>"
7404"#;
7405 let plain: Spec = base.parse().unwrap();
7406 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7407 assert!(err.to_string().contains("required"), "{err}");
7408
7409 let enabled: Spec = base
7410 .replacen(
7411 "bin \"ex\"",
7412 "bin \"ex\"\nallow_missing_positional #true",
7413 1,
7414 )
7415 .parse()
7416 .unwrap();
7417 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7418 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7419 let value = &out
7420 .args
7421 .iter()
7422 .find(|(arg, _)| arg.name == "required")
7423 .unwrap()
7424 .1;
7425 assert!(matches!(value, ParseValue::String(value) if value == "value"));
7426 }
7427
7428 #[test]
7429 fn sigil_args_do_not_block_optional_positional_skipping() {
7430 let spec: Spec = r#"name "ex"
7431bin "ex"
7432allow_missing_positional #true
7433arg "[optional]"
7434arg "[tool]" sigil="@"
7435arg "<required>"
7436"#
7437 .parse()
7438 .unwrap();
7439
7440 let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7441 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7442 let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7443 let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7444 assert_eq!(out.args[tool].to_string(), "node");
7445 assert_eq!(out.args[required].to_string(), "value");
7446 }
7447
7448 #[test]
7449 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7450 let spec: Spec =
7451 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7452 .parse()
7453 .unwrap();
7454 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7455 assert!(
7456 err.to_string().contains("--wat"),
7457 "the message should name the token: {err}"
7458 );
7459
7460 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
7463 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7464 assert_eq!(out.args[rest].to_string(), "-1");
7465
7466 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
7467 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7468 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7469 }
7470
7471 #[test]
7472 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
7473 let spec: Spec = r#"
7474name "ex"
7475bin "ex"
7476unknown_flags "error"
7477flag "-0 --print0" global=#true
7478cmd "run" {
7479 flag "--force"
7480}
7481"#
7482 .parse()
7483 .unwrap();
7484 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
7485 assert_eq!(out.cmd.name, "run");
7486 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7487 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
7488 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7489 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
7490 }
7491
7492 #[test]
7493 fn a_command_may_override_the_cli_wide_setting() {
7494 let spec: Spec = r#"
7496name "ex"
7497bin "ex"
7498unknown_flags "error"
7499cmd "exec" unknown_flags="value" {
7500 arg "[rest]..."
7501}
7502cmd "build" {
7503 arg "[rest]..."
7504}
7505"#
7506 .parse()
7507 .unwrap();
7508
7509 let out = parse(
7510 &spec,
7511 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
7512 )
7513 .unwrap();
7514 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7515 assert_eq!(out.args[rest].to_string(), "--wat");
7516
7517 assert!(
7518 parse(
7519 &spec,
7520 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
7521 )
7522 .is_err(),
7523 "a command that says nothing inherits the CLI's choice"
7524 );
7525 }
7526
7527 #[test]
7528 fn the_setting_survives_a_round_trip() {
7529 let spec: Spec =
7530 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7531 .parse()
7532 .unwrap();
7533 let reparsed: Spec = spec.to_string().parse().unwrap();
7534 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7535 assert_eq!(
7536 reparsed.cmd.subcommands["x"].unknown_flags,
7537 Some(UnknownFlags::Value)
7538 );
7539 }
7540
7541 #[test]
7542 fn test_default_subcommand() {
7543 let run_cmd = SpecCommand::builder()
7545 .name("run")
7546 .arg(SpecArg::builder().name("task").build())
7547 .build();
7548 let mut cmd = SpecCommand::builder().name("test").build();
7549 cmd.subcommands.insert("run".to_string(), run_cmd);
7550
7551 let spec = Spec {
7552 name: "test".to_string(),
7553 bin: "test".to_string(),
7554 cmd,
7555 default_subcommand: Some("run".to_string()),
7556 ..Default::default()
7557 };
7558
7559 let input = vec!["test".to_string(), "mytask".to_string()];
7561 let parsed = parse(&spec, &input).unwrap();
7562
7563 assert_eq!(parsed.cmds.len(), 2);
7565 assert_eq!(parsed.cmds[1].name, "run");
7566
7567 assert_eq!(parsed.args.len(), 1);
7569 let arg = parsed.args.keys().next().unwrap();
7570 assert_eq!(arg.name, "task");
7571 let value = parsed.args.values().next().unwrap();
7572 assert_eq!(value.to_string(), "mytask");
7573 }
7574
7575 #[test]
7576 fn default_subcommand_outranks_root_sigil_arg() {
7577 let spec: Spec = r#"
7578name "test"
7579bin "test"
7580default_subcommand "run"
7581arg "[tools]..." sigil="+"
7582cmd "run" { arg "<task>" }
7583"#
7584 .parse()
7585 .unwrap();
7586
7587 let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7588 assert_eq!(parsed.cmd.name, "run");
7589 let value = |name| {
7590 parsed
7591 .args
7592 .iter()
7593 .find(|(arg, _)| arg.name == name)
7594 .map(|(_, value)| value.to_string())
7595 .unwrap()
7596 };
7597 assert_eq!(value("tools"), "node");
7598 assert_eq!(value("task"), "node");
7599 }
7600
7601 #[test]
7602 fn test_default_subcommand_explicit_still_works() {
7603 let run_cmd = SpecCommand::builder()
7605 .name("run")
7606 .arg(SpecArg::builder().name("task").build())
7607 .build();
7608 let other_cmd = SpecCommand::builder()
7609 .name("other")
7610 .arg(SpecArg::builder().name("other_arg").build())
7611 .build();
7612 let mut cmd = SpecCommand::builder().name("test").build();
7613 cmd.subcommands.insert("run".to_string(), run_cmd);
7614 cmd.subcommands.insert("other".to_string(), other_cmd);
7615
7616 let spec = Spec {
7617 name: "test".to_string(),
7618 bin: "test".to_string(),
7619 cmd,
7620 default_subcommand: Some("run".to_string()),
7621 ..Default::default()
7622 };
7623
7624 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7626 let parsed = parse(&spec, &input).unwrap();
7627
7628 assert_eq!(parsed.cmds.len(), 2);
7630 assert_eq!(parsed.cmds[1].name, "other");
7631 }
7632
7633 #[test]
7634 fn test_default_subcommand_applies_only_at_the_root() {
7635 let mut config_ls = SpecCommand::builder().name("ls").build();
7641 config_ls.args.push(SpecArg::builder().name("what").build());
7642 let mut config_cmd = SpecCommand::builder().name("config").build();
7643 config_cmd.subcommands.insert("ls".to_string(), config_ls);
7644
7645 let mut root_ls = SpecCommand::builder().name("ls").build();
7648 root_ls.args.push(SpecArg::builder().name("what").build());
7649 let mut cmd = SpecCommand::builder().name("ex").build();
7650 cmd.subcommands.insert("ls".to_string(), root_ls);
7651 cmd.subcommands.insert("config".to_string(), config_cmd);
7652
7653 let spec = Spec {
7654 name: "ex".to_string(),
7655 bin: "ex".to_string(),
7656 cmd,
7657 default_subcommand: Some("ls".to_string()),
7658 ..Default::default()
7659 };
7660
7661 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7665 assert!(
7666 parse(&spec, &input).is_err(),
7667 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7668 );
7669
7670 let input = vec!["ex".to_string(), "zzz".to_string()];
7672 let parsed = parse(&spec, &input).expect("the root's default applies");
7673 assert_eq!(
7674 parsed
7675 .cmds
7676 .iter()
7677 .map(|c| c.name.as_str())
7678 .collect::<Vec<_>>(),
7679 ["ex", "ls"]
7680 );
7681 assert_eq!(
7682 parsed.args.values().next().map(|v| v.to_string()),
7683 Some("zzz".to_string()),
7684 "and the word binds inside the command it reached"
7685 );
7686 }
7687
7688 #[test]
7689 fn test_default_subcommand_with_nested_subcommands() {
7690 let say_cmd = SpecCommand::builder()
7694 .name("say")
7695 .arg(SpecArg::builder().name("name").build())
7696 .build();
7697 let mut run_cmd = SpecCommand::builder().name("run").build();
7698 run_cmd.subcommands.insert("say".to_string(), say_cmd);
7699
7700 let mut cmd = SpecCommand::builder().name("test").build();
7701 cmd.subcommands.insert("run".to_string(), run_cmd);
7702
7703 let spec = Spec {
7704 name: "test".to_string(),
7705 bin: "test".to_string(),
7706 cmd,
7707 default_subcommand: Some("run".to_string()),
7708 ..Default::default()
7709 };
7710
7711 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
7713 let parsed = parse(&spec, &input).unwrap();
7714
7715 assert_eq!(parsed.cmds.len(), 3);
7717 assert_eq!(parsed.cmds[0].name, "test");
7718 assert_eq!(parsed.cmds[1].name, "run");
7719 assert_eq!(parsed.cmds[2].name, "say");
7720
7721 assert_eq!(parsed.args.len(), 1);
7723 let arg = parsed.args.keys().next().unwrap();
7724 assert_eq!(arg.name, "name");
7725 let value = parsed.args.values().next().unwrap();
7726 assert_eq!(value.to_string(), "hello");
7727 }
7728
7729 fn mounted_global_flag_spec() -> Spec {
7737 let task_cmd = SpecCommand::builder()
7738 .name("sample:run")
7739 .arg(
7740 SpecArg::builder()
7741 .name("profile")
7742 .choices(["alpha", "beta", "gamma"])
7743 .build(),
7744 )
7745 .build();
7746 let mut run_cmd = SpecCommand::builder()
7748 .name("run")
7749 .flag(
7750 SpecFlag::builder()
7751 .name("cd")
7752 .short('C')
7753 .long("cd")
7754 .arg(SpecArg::builder().name("dir").build())
7755 .global(false)
7756 .build(),
7757 )
7758 .build();
7759 run_cmd
7760 .subcommands
7761 .insert("sample:run".to_string(), task_cmd);
7762
7763 let mut cmd = SpecCommand::builder()
7764 .name("test")
7765 .flag(
7766 SpecFlag::builder()
7767 .name("cd")
7768 .short('C')
7769 .long("cd")
7770 .arg(SpecArg::builder().name("dir").build())
7771 .global(true)
7772 .build(),
7773 )
7774 .build();
7775 cmd.subcommands.insert("run".to_string(), run_cmd);
7776
7777 Spec {
7778 name: "test".to_string(),
7779 bin: "test".to_string(),
7780 cmd,
7781 ..Default::default()
7782 }
7783 }
7784
7785 #[test]
7786 fn test_prefix_global_flag_does_not_pollute_choices() {
7787 let spec = mounted_global_flag_spec();
7794
7795 for words in [
7798 &["test", "-C", "/tmp", "run", "sample:run"][..],
7799 &["test", "--cd=/tmp", "run", "sample:run"][..],
7801 ] {
7802 let parsed = parse_partial(&spec, &input(words)).unwrap();
7803 assert_eq!(
7804 parsed
7805 .cmds
7806 .iter()
7807 .map(|c| c.name.as_str())
7808 .collect::<Vec<_>>(),
7809 vec!["test", "run", "sample:run"],
7810 );
7811 assert!(
7813 parsed.args.is_empty(),
7814 "args should be empty, got {:?}",
7815 parsed.args
7816 );
7817
7818 let cd = parsed
7821 .available_flags
7822 .get("--cd")
7823 .expect("--cd should remain available after descending into the subcommand");
7824 assert!(cd.global, "--cd must stay global after descent");
7825 assert!(
7826 parsed.available_flags.get("-C").is_some_and(|f| f.global),
7827 "-C must stay global after descent",
7828 );
7829
7830 assert_eq!(
7834 parsed.as_env().get("usage_cd").map(String::as_str),
7835 Some("/tmp"),
7836 "global flag value must survive in as_env(), got {:?}",
7837 parsed.as_env(),
7838 );
7839 }
7840
7841 let parsed = parse_partial(
7843 &spec,
7844 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
7845 )
7846 .unwrap();
7847 assert_eq!(parsed.args.len(), 1);
7848 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7849
7850 assert_parse_err(
7852 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
7853 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7854 );
7855 }
7856
7857 fn mounted_orphan_short_spec() -> Spec {
7862 let task_cmd = SpecCommand::builder()
7863 .name("sample:run")
7864 .arg(
7865 SpecArg::builder()
7866 .name("profile")
7867 .choices(["alpha", "beta", "gamma"])
7868 .build(),
7869 )
7870 .build();
7871 let mut run_cmd = SpecCommand::builder()
7874 .name("run")
7875 .flag(
7876 SpecFlag::builder()
7877 .name("raw")
7878 .short('r')
7879 .long("raw")
7880 .global(false)
7881 .build(),
7882 )
7883 .flag(
7884 SpecFlag::builder()
7885 .name("force")
7886 .short('f')
7887 .long("force")
7888 .global(false)
7889 .build(),
7890 )
7891 .build();
7892 run_cmd
7893 .subcommands
7894 .insert("sample:run".to_string(), task_cmd);
7895
7896 let mut cmd = SpecCommand::builder()
7898 .name("test")
7899 .flag(
7900 SpecFlag::builder()
7901 .name("raw")
7902 .long("raw")
7903 .global(true)
7904 .build(),
7905 )
7906 .build();
7907 cmd.subcommands.insert("run".to_string(), run_cmd);
7908
7909 Spec {
7910 name: "test".to_string(),
7911 bin: "test".to_string(),
7912 cmd,
7913 ..Default::default()
7914 }
7915 }
7916
7917 #[test]
7918 fn test_orphan_short_alias_survives_merge() {
7919 let spec = mounted_orphan_short_spec();
7925
7926 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
7927 assert_eq!(
7928 parsed
7929 .cmds
7930 .iter()
7931 .map(|c| c.name.as_str())
7932 .collect::<Vec<_>>(),
7933 vec!["test", "run", "sample:run"],
7934 );
7935
7936 assert!(
7939 parsed.available_flags.get("-r").is_some_and(|f| f.global),
7940 "-r must be merged onto the inherited global flag and stay global after descent",
7941 );
7942 assert!(
7943 parsed
7944 .available_flags
7945 .get("--raw")
7946 .is_some_and(|f| f.global),
7947 "--raw must stay global after descent",
7948 );
7949
7950 assert!(
7952 parsed.args.is_empty(),
7953 "args should be empty, got {:?}",
7954 parsed.args
7955 );
7956
7957 assert_eq!(
7959 parsed.as_env().get("usage_raw").map(String::as_str),
7960 Some("true"),
7961 "merged short's value must survive in as_env(), got {:?}",
7962 parsed.as_env(),
7963 );
7964
7965 assert!(
7968 !parsed.available_flags.contains_key("-f"),
7969 "purely-local -f must not be promoted onto a global",
7970 );
7971 assert!(
7972 !parsed.available_flags.contains_key("--force"),
7973 "purely-local --force must not be promoted onto a global",
7974 );
7975
7976 let parsed =
7978 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
7979 assert_eq!(parsed.args.len(), 1);
7980 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7981
7982 assert_parse_err(
7984 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
7985 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7986 );
7987 }
7988
7989 #[test]
7990 fn test_orphan_short_does_not_clobber_unrelated_global() {
7991 let run_cmd = SpecCommand::builder()
7996 .name("run")
7997 .flag(
7998 SpecFlag::builder()
7999 .name("raw")
8000 .short('r')
8001 .long("raw")
8002 .global(false)
8003 .build(),
8004 )
8005 .build();
8006 let mut cmd = SpecCommand::builder()
8007 .name("test")
8008 .flag(
8009 SpecFlag::builder()
8010 .name("raw")
8011 .long("raw")
8012 .global(true)
8013 .build(),
8014 )
8015 .flag(
8016 SpecFlag::builder()
8017 .name("restrict")
8018 .short('r')
8019 .long("restrict")
8020 .global(true)
8021 .build(),
8022 )
8023 .build();
8024 cmd.subcommands.insert("run".to_string(), run_cmd);
8025 let spec = Spec {
8026 name: "test".to_string(),
8027 bin: "test".to_string(),
8028 cmd,
8029 ..Default::default()
8030 };
8031
8032 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8033 assert_eq!(
8035 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8036 Some("restrict"),
8037 "-r must remain owned by the unrelated global it already belonged to",
8038 );
8039 assert!(parsed
8041 .available_flags
8042 .get("--raw")
8043 .is_some_and(|f| f.global));
8044 assert!(parsed
8045 .available_flags
8046 .get("--restrict")
8047 .is_some_and(|f| f.global));
8048 }
8049
8050 #[test]
8051 fn test_redeclared_global_aliases_share_one_flag() {
8052 let spec = r#"
8059flag "-y --yes" global=#true effect="write"
8060cmd "run" {
8061 flag "-y --yes --assume-yes"
8062}
8063"#
8064 .parse::<Spec>()
8065 .unwrap();
8066
8067 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8068
8069 for key in ["-y", "--yes", "--assume-yes"] {
8070 let flag = parsed
8071 .available_flags
8072 .get(key)
8073 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8074 assert!(flag.global, "{key} must stay global after the descent");
8075 assert_eq!(
8076 flag.long,
8077 vec!["yes".to_string(), "assume-yes".to_string()],
8078 "{key} must resolve to the flag carrying every alias",
8079 );
8080 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8081 }
8082
8083 assert_eq!(
8085 unique_flags(parsed.available_flags.values()).count(),
8086 1,
8087 "all aliases must point at one flag object, got {:?}",
8088 parsed.available_flags,
8089 );
8090
8091 assert_eq!(
8093 parsed.available_flags["-y"].effect,
8094 Some(crate::SpecCommandEffect::Write),
8095 );
8096 }
8097
8098 #[test]
8099 fn test_redeclared_global_keeps_hidden_alias_metadata() {
8100 let spec = r#"
8101flag "--yes" global=#true {
8102 alias "-q" "--quietly" hide=#true
8103}
8104cmd "run" {
8105 flag "--yes --assume-yes" {
8106 alias "-s" "--secret" hide=#true
8107 }
8108}
8109"#
8110 .parse::<Spec>()
8111 .unwrap();
8112
8113 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8114 let merged = &parsed.available_flags["--yes"];
8115 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8116 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8117 for key in ["-q", "-s", "--quietly", "--secret"] {
8118 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8119 }
8120 }
8121
8122 #[test]
8123 fn test_redeclared_global_can_promote_hidden_aliases() {
8124 let spec = r#"
8125flag "--yes" global=#true {
8126 alias "-q" "--quietly" hide=#true
8127}
8128cmd "run" {
8129 flag "-q --yes --quietly"
8130}
8131"#
8132 .parse::<Spec>()
8133 .unwrap();
8134
8135 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8136 let merged = &parsed.available_flags["--yes"];
8137 assert!(merged.hidden_short_aliases.is_empty());
8138 assert!(merged.hidden_aliases.is_empty());
8139 for key in ["-q", "--quietly"] {
8140 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8141 }
8142 }
8143
8144 #[test]
8145 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8146 let spec = r#"
8152flag "-y --yes --confirm" global=#true
8153cmd "run" {
8154 flag "--yes --assume-yes"
8155}
8156"#
8157 .parse::<Spec>()
8158 .unwrap();
8159
8160 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8161
8162 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8163 let flag = parsed
8164 .available_flags
8165 .get(key)
8166 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8167 assert!(flag.global, "{key} must stay global after the descent");
8168 assert_eq!(
8169 flag.long,
8170 vec![
8171 "yes".to_string(),
8172 "confirm".to_string(),
8173 "assume-yes".to_string()
8174 ],
8175 "{key} must resolve to the flag carrying every alias",
8176 );
8177 }
8178
8179 assert_eq!(
8180 unique_flags(parsed.available_flags.values()).count(),
8181 1,
8182 "all aliases must point at one flag object, got {:?}",
8183 parsed.available_flags,
8184 );
8185 }
8186
8187 fn mounted_task_flag_spec() -> Spec {
8194 let mut task_cmd = SpecCommand::builder()
8195 .name("mytask")
8196 .flag(
8197 SpecFlag::builder()
8198 .name("env")
8199 .long("env")
8200 .arg(
8201 SpecArg::builder()
8202 .name("name")
8203 .choices(["dev", "stage", "prod"])
8204 .build(),
8205 )
8206 .global(false)
8207 .build(),
8208 )
8209 .flag(
8210 SpecFlag::builder()
8211 .name("bump")
8212 .long("bump")
8213 .arg(
8214 SpecArg::builder()
8215 .name("type")
8216 .choices(["auto", "major"])
8217 .build(),
8218 )
8219 .global(false)
8220 .build(),
8221 )
8222 .build();
8223 task_cmd.mounted = true;
8224
8225 let mut run_cmd = SpecCommand::builder()
8226 .name("run")
8227 .flag(
8228 SpecFlag::builder()
8229 .name("force")
8230 .short('f')
8231 .long("force")
8232 .global(false)
8233 .build(),
8234 )
8235 .build();
8236 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8237
8238 let mut cmd = SpecCommand::builder()
8239 .name("test")
8240 .flag(
8241 SpecFlag::builder()
8242 .name("env")
8243 .short('E')
8244 .long("env")
8245 .arg(SpecArg::builder().name("ENV").build())
8246 .global(true)
8247 .build(),
8248 )
8249 .flag(
8250 SpecFlag::builder()
8251 .name("silent")
8252 .long("silent")
8253 .global(true)
8254 .build(),
8255 )
8256 .build();
8257 cmd.subcommands.insert("run".to_string(), run_cmd);
8258
8259 Spec {
8260 name: "test".to_string(),
8261 bin: "test".to_string(),
8262 cmd,
8263 ..Default::default()
8264 }
8265 }
8266
8267 #[test]
8268 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8269 let deep = SpecCommand::builder().name("deep").build();
8275 let mut sub = SpecCommand::builder()
8276 .name("sub")
8277 .flag(
8279 SpecFlag::builder()
8280 .name("cd")
8281 .short('C')
8282 .long("cd")
8283 .arg(SpecArg::builder().name("dir").build())
8284 .global(false)
8285 .build(),
8286 )
8287 .build();
8288 sub.subcommands.insert("deep".to_string(), deep);
8289 let mut task = SpecCommand::builder()
8290 .name("task")
8291 .flag(
8292 SpecFlag::builder()
8293 .name("cd")
8294 .short('C')
8295 .long("cd")
8296 .arg(SpecArg::builder().name("dir").build())
8297 .global(true)
8298 .build(),
8299 )
8300 .build();
8301 task.subcommands.insert("sub".to_string(), sub);
8302 task.mark_mounted();
8303
8304 let mut run_cmd = SpecCommand::builder().name("run").build();
8305 run_cmd.subcommands.insert("task".to_string(), task);
8306 let mut cmd = SpecCommand::builder().name("test").build();
8307 cmd.subcommands.insert("run".to_string(), run_cmd);
8308 let spec = Spec {
8309 name: "test".to_string(),
8310 bin: "test".to_string(),
8311 cmd,
8312 ..Default::default()
8313 };
8314
8315 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8316 assert!(
8317 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8318 "the mounted program's own global must survive descents inside the mounted tree",
8319 );
8320 assert!(
8321 parsed.completion_flags().contains_key("--cd"),
8322 "and must still be offered there: it belongs to the mounted program",
8323 );
8324 assert!(
8325 parsed.completion_flags().contains_key("-C"),
8326 "including the short the nested command re-declared",
8327 );
8328 }
8329
8330 #[test]
8331 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8332 let mut task = SpecCommand::builder()
8337 .name("task")
8338 .flag(
8339 SpecFlag::builder()
8340 .name("bump")
8341 .long("bump")
8342 .global(false)
8343 .build(),
8344 )
8345 .build();
8346 task.mark_mounted();
8347
8348 let mut run_cmd = SpecCommand::builder().name("run").build();
8349 run_cmd.subcommands.insert("task".to_string(), task);
8350 run_cmd.flags = vec![
8352 SpecFlag::builder()
8353 .name("tglobal")
8354 .long("tglobal")
8355 .global(true)
8356 .build(),
8357 SpecFlag::builder()
8358 .name("tlocal")
8359 .long("tlocal")
8360 .global(false)
8361 .build(),
8362 ];
8363 run_cmd.flags_from_mount = true;
8364
8365 let mut cmd = SpecCommand::builder()
8366 .name("test")
8367 .flag(
8368 SpecFlag::builder()
8369 .name("silent")
8370 .long("silent")
8371 .global(true)
8372 .build(),
8373 )
8374 .build();
8375 cmd.subcommands.insert("run".to_string(), run_cmd);
8376 let spec = Spec {
8377 name: "test".to_string(),
8378 bin: "test".to_string(),
8379 cmd,
8380 ..Default::default()
8381 };
8382
8383 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8384 assert_eq!(
8385 parsed.completion_flags().keys().collect::<Vec<_>>(),
8386 vec!["--bump", "--tglobal"],
8387 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8388 `--silent` does not, and the mount's non-global root flag is not inherited",
8389 );
8390 }
8391
8392 #[test]
8393 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8394 let spec = mounted_task_flag_spec();
8399 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8400
8401 assert!(parsed.available_flags.contains_key("--silent"));
8403 assert!(parsed.available_flags.contains_key("-E"));
8404 assert_eq!(
8406 parsed.completion_flags().keys().collect::<Vec<_>>(),
8407 vec!["--bump", "--env"],
8408 "only the mounted command's own flags may be offered",
8409 );
8410
8411 assert!(!parsed.available_flags.contains_key("--force"));
8413 }
8414
8415 #[test]
8416 fn test_mounted_cmd_flag_wins_over_inherited_global() {
8417 let spec = mounted_task_flag_spec();
8420 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8421
8422 let awaiting = parsed
8423 .flag_awaiting_value
8424 .first()
8425 .expect("--env should await a value");
8426 assert_eq!(
8427 awaiting
8428 .arg
8429 .as_ref()
8430 .and_then(|a| a.choices.as_ref())
8431 .map(|c| c.choices.clone()),
8432 Some(vec![
8433 "dev".to_string(),
8434 "stage".to_string(),
8435 "prod".to_string()
8436 ]),
8437 "the mounted command's own --env must win over the inherited global",
8438 );
8439
8440 let parsed =
8443 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8444 assert!(
8445 parsed.args.is_empty(),
8446 "prefix global tokens must not be consumed as positionals, got {:?}",
8447 parsed.args
8448 );
8449 assert_eq!(
8450 parsed.as_env().get("usage_env").map(String::as_str),
8451 Some("anything"),
8452 );
8453 }
8454
8455 #[test]
8456 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
8457 let spec = mounted_task_flag_spec();
8462 let parsed = parse_partial(
8463 &spec,
8464 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
8465 )
8466 .unwrap();
8467 assert!(
8468 parsed.errors.is_empty(),
8469 "prefix global value must not be validated against the mounted flag: {:?}",
8470 parsed
8471 .errors
8472 .iter()
8473 .map(|e| e.to_string())
8474 .collect::<Vec<_>>(),
8475 );
8476 assert_eq!(
8477 parsed.as_env().get("usage_env").map(String::as_str),
8478 Some("not-a-task-choice"),
8479 );
8480
8481 let parsed = parse_partial(
8483 &spec,
8484 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
8485 )
8486 .unwrap();
8487 assert!(parsed.errors.is_empty());
8488 assert_eq!(
8489 parsed.as_env().get("usage_env").map(String::as_str),
8490 Some("not-a-task-choice"),
8491 );
8492
8493 let parsed = parse_partial(
8496 &spec,
8497 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
8498 )
8499 .unwrap();
8500 let awaiting = parsed
8501 .flag_awaiting_value
8502 .first()
8503 .expect("--env should await a value");
8504 assert_eq!(
8505 awaiting
8506 .arg
8507 .as_ref()
8508 .and_then(|a| a.choices.as_ref())
8509 .map(|c| c.choices.clone()),
8510 Some(vec![
8511 "dev".to_string(),
8512 "stage".to_string(),
8513 "prod".to_string()
8514 ]),
8515 "the mounted command's --env must own the name after the mounted command",
8516 );
8517 }
8518
8519 #[test]
8520 fn test_non_global_flag_does_not_hide_subcommand() {
8521 let spec = mounted_task_flag_spec();
8525
8526 for words in [
8527 &["test", "run", "--force", "mytask"][..],
8529 &["test", "run", "-f", "mytask"][..],
8530 &["test", "-E", "prod", "run", "--force", "mytask"][..],
8532 ] {
8533 let parsed = parse_partial(&spec, &input(words)).unwrap();
8534 assert_eq!(
8535 parsed
8536 .cmds
8537 .iter()
8538 .map(|c| c.name.as_str())
8539 .collect::<Vec<_>>(),
8540 vec!["test", "run", "mytask"],
8541 "{words:?} should descend into the mounted command",
8542 );
8543 assert!(
8544 parsed.args.is_empty(),
8545 "{words:?} should not consume a positional, got {:?}",
8546 parsed.args,
8547 );
8548 assert_eq!(
8549 parsed.as_env().get("usage_force").map(String::as_str),
8550 Some("true"),
8551 "the non-global flag must still be recorded for {words:?}",
8552 );
8553 }
8554
8555 let mut run_cmd = SpecCommand::builder()
8558 .name("run")
8559 .flag(
8560 SpecFlag::builder()
8561 .name("output")
8562 .short('o')
8563 .long("output")
8564 .arg(SpecArg::builder().name("mode").build())
8565 .global(false)
8566 .build(),
8567 )
8568 .build();
8569 run_cmd.subcommands.insert(
8570 "task".to_string(),
8571 SpecCommand::builder().name("task").build(),
8572 );
8573 let mut cmd = SpecCommand::builder().name("test").build();
8574 cmd.subcommands.insert("run".to_string(), run_cmd);
8575 let spec = Spec {
8576 name: "test".to_string(),
8577 bin: "test".to_string(),
8578 cmd,
8579 ..Default::default()
8580 };
8581
8582 let parsed =
8583 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8584 assert_eq!(
8585 parsed
8586 .cmds
8587 .iter()
8588 .map(|c| c.name.as_str())
8589 .collect::<Vec<_>>(),
8590 vec!["test", "run", "task"],
8591 );
8592 assert_eq!(
8593 parsed.as_env().get("usage_output").map(String::as_str),
8594 Some("quiet"),
8595 );
8596
8597 assert_parse_err(
8600 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8601 "unexpected word: --nope",
8602 );
8603 }
8604
8605 #[test]
8606 fn test_non_mounted_subcommand_offers_inherited_globals() {
8607 let mut run_cmd = SpecCommand::builder().name("run").build();
8610 run_cmd.subcommands.insert(
8611 "nested".to_string(),
8612 SpecCommand::builder().name("nested").build(),
8613 );
8614 let mut cmd = SpecCommand::builder()
8615 .name("test")
8616 .flag(
8617 SpecFlag::builder()
8618 .name("silent")
8619 .long("silent")
8620 .global(true)
8621 .build(),
8622 )
8623 .build();
8624 cmd.subcommands.insert("run".to_string(), run_cmd);
8625 let spec = Spec {
8626 name: "test".to_string(),
8627 bin: "test".to_string(),
8628 cmd,
8629 ..Default::default()
8630 };
8631
8632 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8633 assert_eq!(
8634 parsed.completion_flags().keys().collect::<Vec<_>>(),
8635 parsed.available_flags.keys().collect::<Vec<_>>(),
8636 );
8637 assert!(parsed.completion_flags().contains_key("--silent"));
8638 }
8639
8640 #[test]
8641 fn test_subcommand_alias_collision_keeps_last_owner() {
8642 let run_cmd = SpecCommand::builder()
8646 .name("run")
8647 .flag(
8648 SpecFlag::builder()
8649 .name("alpha")
8650 .short('x')
8651 .long("alpha")
8652 .global(false)
8653 .build(),
8654 )
8655 .flag(
8656 SpecFlag::builder()
8657 .name("beta")
8658 .short('x')
8659 .long("beta")
8660 .global(false)
8661 .build(),
8662 )
8663 .build();
8664 let mut cmd = SpecCommand::builder().name("test").build();
8665 cmd.subcommands.insert("run".to_string(), run_cmd);
8666 let spec = Spec {
8667 name: "test".to_string(),
8668 bin: "test".to_string(),
8669 cmd,
8670 ..Default::default()
8671 };
8672
8673 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8674 assert_eq!(
8676 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8677 Some("beta"),
8678 "the last-declared flag must keep a shared short alias",
8679 );
8680 assert_eq!(
8682 parsed
8683 .available_flags
8684 .get("--alpha")
8685 .map(|f| f.name.as_str()),
8686 Some("alpha"),
8687 );
8688 assert_eq!(
8689 parsed
8690 .available_flags
8691 .get("--beta")
8692 .map(|f| f.name.as_str()),
8693 Some("beta"),
8694 );
8695 }
8696
8697 #[test]
8698 fn test_default_subcommand_same_name_child() {
8699 let run_task = SpecCommand::builder()
8703 .name("run")
8704 .arg(SpecArg::builder().name("args").build())
8705 .build();
8706 let mut run_cmd = SpecCommand::builder().name("run").build();
8707 run_cmd.subcommands.insert("run".to_string(), run_task);
8708
8709 let mut cmd = SpecCommand::builder().name("test").build();
8710 cmd.subcommands.insert("run".to_string(), run_cmd);
8711
8712 let spec = Spec {
8713 name: "test".to_string(),
8714 bin: "test".to_string(),
8715 cmd,
8716 default_subcommand: Some("run".to_string()),
8717 ..Default::default()
8718 };
8719
8720 let input = vec!["test".to_string(), "run".to_string()];
8722 let parsed = parse(&spec, &input).unwrap();
8723
8724 assert_eq!(parsed.cmds.len(), 2);
8726 assert_eq!(parsed.cmds[0].name, "test");
8727 assert_eq!(parsed.cmds[1].name, "run");
8728
8729 let input = vec![
8731 "test".to_string(),
8732 "run".to_string(),
8733 "run".to_string(),
8734 "hello".to_string(),
8735 ];
8736 let parsed = parse(&spec, &input).unwrap();
8737
8738 assert_eq!(parsed.cmds.len(), 3);
8739 assert_eq!(parsed.cmds[0].name, "test");
8740 assert_eq!(parsed.cmds[1].name, "run");
8741 assert_eq!(parsed.cmds[2].name, "run");
8742 assert_eq!(parsed.args.len(), 1);
8743 let value = parsed.args.values().next().unwrap();
8744 assert_eq!(value.to_string(), "hello");
8745
8746 let mut run_cmd = SpecCommand::builder()
8750 .name("run")
8751 .arg(SpecArg::builder().name("task").build())
8752 .build();
8753 let run_task = SpecCommand::builder().name("run").build();
8754 run_cmd.subcommands.insert("run".to_string(), run_task);
8755
8756 let mut cmd = SpecCommand::builder().name("test").build();
8757 cmd.subcommands.insert("run".to_string(), run_cmd);
8758
8759 let spec = Spec {
8760 name: "test".to_string(),
8761 bin: "test".to_string(),
8762 cmd,
8763 default_subcommand: Some("run".to_string()),
8764 ..Default::default()
8765 };
8766
8767 let input = vec!["test".to_string(), "other".to_string()];
8768 let parsed = parse(&spec, &input).unwrap();
8769
8770 assert_eq!(parsed.cmds.len(), 2);
8773 assert_eq!(parsed.cmds[0].name, "test");
8774 assert_eq!(parsed.cmds[1].name, "run");
8775
8776 assert_eq!(parsed.args.len(), 1);
8778 let value = parsed.args.values().next().unwrap();
8779 assert_eq!(value.to_string(), "other");
8780 }
8781
8782 #[test]
8783 fn test_restart_token() {
8784 let run_cmd = SpecCommand::builder()
8786 .name("run")
8787 .arg(SpecArg::builder().name("task").build())
8788 .restart_token(":::".to_string())
8789 .build();
8790 let mut cmd = SpecCommand::builder().name("test").build();
8791 cmd.subcommands.insert("run".to_string(), run_cmd);
8792
8793 let spec = Spec {
8794 name: "test".to_string(),
8795 bin: "test".to_string(),
8796 cmd,
8797 ..Default::default()
8798 };
8799
8800 let input = vec![
8802 "test".to_string(),
8803 "run".to_string(),
8804 "task1".to_string(),
8805 ":::".to_string(),
8806 "task2".to_string(),
8807 ];
8808 let parsed = parse(&spec, &input).unwrap();
8809
8810 assert_eq!(parsed.args.len(), 1);
8812 let value = parsed.args.values().next().unwrap();
8813 assert_eq!(value.to_string(), "task2");
8814 }
8815
8816 #[test]
8817 fn test_restart_token_multiple() {
8818 let run_cmd = SpecCommand::builder()
8820 .name("run")
8821 .arg(SpecArg::builder().name("task").build())
8822 .restart_token(":::".to_string())
8823 .build();
8824 let mut cmd = SpecCommand::builder().name("test").build();
8825 cmd.subcommands.insert("run".to_string(), run_cmd);
8826
8827 let spec = Spec {
8828 name: "test".to_string(),
8829 bin: "test".to_string(),
8830 cmd,
8831 ..Default::default()
8832 };
8833
8834 let input = vec![
8836 "test".to_string(),
8837 "run".to_string(),
8838 "task1".to_string(),
8839 ":::".to_string(),
8840 "task2".to_string(),
8841 ":::".to_string(),
8842 "task3".to_string(),
8843 ];
8844 let parsed = parse(&spec, &input).unwrap();
8845
8846 assert_eq!(parsed.args.len(), 1);
8848 let value = parsed.args.values().next().unwrap();
8849 assert_eq!(value.to_string(), "task3");
8850 }
8851
8852 #[test]
8853 fn test_restart_token_clears_flag_awaiting_value() {
8854 let run_cmd = SpecCommand::builder()
8856 .name("run")
8857 .arg(SpecArg::builder().name("task").build())
8858 .flag(
8859 SpecFlag::builder()
8860 .name("jobs")
8861 .long("jobs")
8862 .arg(SpecArg::builder().name("count").build())
8863 .build(),
8864 )
8865 .restart_token(":::".to_string())
8866 .build();
8867 let mut cmd = SpecCommand::builder().name("test").build();
8868 cmd.subcommands.insert("run".to_string(), run_cmd);
8869
8870 let spec = Spec {
8871 name: "test".to_string(),
8872 bin: "test".to_string(),
8873 cmd,
8874 ..Default::default()
8875 };
8876
8877 let input = vec![
8879 "test".to_string(),
8880 "run".to_string(),
8881 "task1".to_string(),
8882 "--jobs".to_string(),
8883 ":::".to_string(),
8884 "task2".to_string(),
8885 ];
8886 let parsed = parse(&spec, &input).unwrap();
8887
8888 assert_eq!(parsed.args.len(), 1);
8890 let value = parsed.args.values().next().unwrap();
8891 assert_eq!(value.to_string(), "task2");
8892 assert!(parsed.flag_awaiting_value.is_empty());
8894 }
8895
8896 #[test]
8897 fn test_restart_token_resets_double_dash() {
8898 let run_cmd = SpecCommand::builder()
8900 .name("run")
8901 .arg(SpecArg::builder().name("task").build())
8902 .arg(SpecArg::builder().name("extra_args").var(true).build())
8903 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
8904 .restart_token(":::".to_string())
8905 .build();
8906 let mut cmd = SpecCommand::builder().name("test").build();
8907 cmd.subcommands.insert("run".to_string(), run_cmd);
8908
8909 let spec = Spec {
8910 name: "test".to_string(),
8911 bin: "test".to_string(),
8912 cmd,
8913 ..Default::default()
8914 };
8915
8916 let input = vec![
8918 "test".to_string(),
8919 "run".to_string(),
8920 "task1".to_string(),
8921 "--".to_string(),
8922 "extra".to_string(),
8923 ":::".to_string(),
8924 "--verbose".to_string(),
8925 "task2".to_string(),
8926 ];
8927 let parsed = parse(&spec, &input).unwrap();
8928
8929 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
8931 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8933 let value = parsed.args.get(task_arg).unwrap();
8934 assert_eq!(value.to_string(), "task2");
8935 }
8936
8937 #[test]
8938 fn test_double_dashes_without_preserve() {
8939 let run_cmd = SpecCommand::builder()
8944 .name("run")
8945 .arg(SpecArg::builder().name("args").var(true).build())
8946 .build();
8947 let mut cmd = SpecCommand::builder().name("test").build();
8948 cmd.subcommands.insert("run".to_string(), run_cmd);
8949
8950 let spec = Spec {
8951 name: "test".to_string(),
8952 bin: "test".to_string(),
8953 cmd,
8954 ..Default::default()
8955 };
8956
8957 let input = vec![
8959 "test".to_string(),
8960 "run".to_string(),
8961 "arg1".to_string(),
8962 "--".to_string(),
8963 "arg2".to_string(),
8964 "--".to_string(),
8965 "arg3".to_string(),
8966 ];
8967 let parsed = parse(&spec, &input).unwrap();
8968
8969 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8970 let value = parsed.args.get(args_arg).unwrap();
8971 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
8972 }
8973
8974 #[test]
8975 fn test_double_dashes_with_preserve() {
8976 let run_cmd = SpecCommand::builder()
8978 .name("run")
8979 .arg(
8980 SpecArg::builder()
8981 .name("args")
8982 .var(true)
8983 .double_dash(SpecDoubleDashChoices::Preserve)
8984 .build(),
8985 )
8986 .build();
8987 let mut cmd = SpecCommand::builder().name("test").build();
8988 cmd.subcommands.insert("run".to_string(), run_cmd);
8989
8990 let spec = Spec {
8991 name: "test".to_string(),
8992 bin: "test".to_string(),
8993 cmd,
8994 ..Default::default()
8995 };
8996
8997 let input = vec![
8999 "test".to_string(),
9000 "run".to_string(),
9001 "arg1".to_string(),
9002 "--".to_string(),
9003 "arg2".to_string(),
9004 "--".to_string(),
9005 "arg3".to_string(),
9006 ];
9007 let parsed = parse(&spec, &input).unwrap();
9008
9009 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9010 let value = parsed.args.get(args_arg).unwrap();
9011 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9012 }
9013
9014 #[test]
9015 fn test_double_dashes_with_preserve_only_dashes() {
9016 let run_cmd = SpecCommand::builder()
9019 .name("run")
9020 .arg(
9021 SpecArg::builder()
9022 .name("args")
9023 .var(true)
9024 .double_dash(SpecDoubleDashChoices::Preserve)
9025 .build(),
9026 )
9027 .build();
9028 let mut cmd = SpecCommand::builder().name("test").build();
9029 cmd.subcommands.insert("run".to_string(), run_cmd);
9030
9031 let spec = Spec {
9032 name: "test".to_string(),
9033 bin: "test".to_string(),
9034 cmd,
9035 ..Default::default()
9036 };
9037
9038 let input = vec![
9040 "test".to_string(),
9041 "run".to_string(),
9042 "--".to_string(),
9043 "--".to_string(),
9044 ];
9045 let parsed = parse(&spec, &input).unwrap();
9046
9047 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9048 let value = parsed.args.get(args_arg).unwrap();
9049 assert_eq!(value.to_string(), "-- --");
9050 }
9051
9052 #[test]
9053 fn test_double_dashes_with_preserve_multiple_args() {
9054 let run_cmd = SpecCommand::builder()
9056 .name("run")
9057 .arg(SpecArg::builder().name("task").build())
9058 .arg(
9059 SpecArg::builder()
9060 .name("extra_args")
9061 .var(true)
9062 .double_dash(SpecDoubleDashChoices::Preserve)
9063 .build(),
9064 )
9065 .build();
9066 let mut cmd = SpecCommand::builder().name("test").build();
9067 cmd.subcommands.insert("run".to_string(), run_cmd);
9068
9069 let spec = Spec {
9070 name: "test".to_string(),
9071 bin: "test".to_string(),
9072 cmd,
9073 ..Default::default()
9074 };
9075
9076 let input = vec![
9079 "test".to_string(),
9080 "run".to_string(),
9081 "task1".to_string(),
9082 "--".to_string(),
9083 "arg1".to_string(),
9084 "--".to_string(),
9085 "--foo".to_string(),
9086 ];
9087 let parsed = parse(&spec, &input).unwrap();
9088
9089 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9090 let task_value = parsed.args.get(task_arg).unwrap();
9091 assert_eq!(task_value.to_string(), "task1");
9092
9093 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9094 let extra_value = parsed.args.get(extra_arg).unwrap();
9095 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9096 }
9097
9098 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9099 let cmd = SpecCommand::builder().name("test").args(args).build();
9100 Spec {
9101 name: "test".to_string(),
9102 bin: "test".to_string(),
9103 cmd,
9104 ..Default::default()
9105 }
9106 }
9107
9108 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9109 let arg = parsed
9110 .args
9111 .keys()
9112 .find(|a| a.name == name)
9113 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9114 parsed.args.get(arg).unwrap().to_string()
9115 }
9116
9117 fn required_arg(name: &str) -> SpecArg {
9118 SpecArg::builder()
9119 .name(name)
9120 .var(true)
9121 .required(false)
9122 .double_dash(SpecDoubleDashChoices::Required)
9123 .build()
9124 }
9125
9126 #[test]
9127 fn test_double_dash_required_reports_error_once_for_variadic() {
9128 let spec = spec_with_args([required_arg("files")]);
9130
9131 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9132
9133 assert!(parsed.args.is_empty());
9134 assert_eq!(parsed.errors.len(), 1);
9135 assert!(
9136 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9137 );
9138 }
9139
9140 #[test]
9141 fn test_double_dash_required_suppresses_missing_arg() {
9142 let spec = spec_with_args([SpecArg::builder()
9144 .name("file")
9145 .required(true)
9146 .double_dash(SpecDoubleDashChoices::Required)
9147 .build()]);
9148
9149 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9150
9151 assert_eq!(parsed.errors.len(), 1);
9152 assert!(matches!(
9153 &parsed.errors[0],
9154 UsageErr::ArgRequiresDoubleDash(_)
9155 ));
9156 assert_eq!(
9158 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9159 Some("file")
9160 );
9161 assert!(!parsed.double_dash_seen);
9162 }
9163
9164 #[test]
9165 fn test_double_dash_routes_to_required_arg() {
9166 let spec = spec_with_args([
9169 SpecArg::builder()
9170 .name("tool")
9171 .var(true)
9172 .required(false)
9173 .build(),
9174 required_arg("command"),
9175 ]);
9176
9177 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9178
9179 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9180 assert_eq!(arg_value(&parsed, "command"), "node app.js");
9181 assert!(parsed.double_dash_seen);
9182 }
9183
9184 #[test]
9185 fn test_double_dash_routes_with_gap_reports_missing_arg() {
9186 let spec = spec_with_args([
9189 SpecArg::builder()
9190 .name("tool")
9191 .var(true)
9192 .required(true)
9193 .build(),
9194 required_arg("command"),
9195 ]);
9196
9197 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9198
9199 assert_eq!(arg_value(&parsed, "command"), "ls");
9200 assert!(parsed.args.keys().all(|a| a.name != "tool"));
9201 assert!(parsed
9202 .errors
9203 .iter()
9204 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
9205 }
9206
9207 #[test]
9208 fn test_double_dash_gap_applies_defaults() {
9209 let spec = spec_with_args([
9211 SpecArg::builder()
9212 .name("tool")
9213 .var(true)
9214 .required(false)
9215 .default_value("node@20")
9216 .build(),
9217 required_arg("command"),
9218 ]);
9219
9220 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9221
9222 assert_eq!(arg_value(&parsed, "command"), "ls");
9223 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9224 }
9225
9226 fn spec_with_restart_token_and_required_arg() -> Spec {
9227 let run_cmd = SpecCommand::builder()
9228 .name("run")
9229 .arg(SpecArg::builder().name("task").build())
9230 .arg(required_arg("run_args"))
9231 .restart_token(":::".to_string())
9232 .build();
9233 let mut cmd = SpecCommand::builder().name("test").build();
9234 cmd.subcommands.insert("run".to_string(), run_cmd);
9235 Spec {
9236 name: "test".to_string(),
9237 bin: "test".to_string(),
9238 cmd,
9239 ..Default::default()
9240 }
9241 }
9242
9243 #[test]
9244 fn test_double_dash_required_restart_token_resets_separator() {
9245 let spec = spec_with_restart_token_and_required_arg();
9247
9248 let parsed = parse_partial(
9249 &spec,
9250 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9251 )
9252 .unwrap();
9253
9254 assert_eq!(arg_value(&parsed, "task"), "task2");
9255 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9256 assert_eq!(
9258 parsed
9259 .errors
9260 .iter()
9261 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9262 .count(),
9263 1
9264 );
9265 }
9266
9267 #[test]
9268 fn test_double_dash_required_restart_token_accepts_new_separator() {
9269 let spec = spec_with_restart_token_and_required_arg();
9270
9271 let parsed = parse(
9272 &spec,
9273 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9274 )
9275 .unwrap();
9276
9277 assert_eq!(arg_value(&parsed, "task"), "task2");
9278 assert_eq!(arg_value(&parsed, "run_args"), "c");
9279 }
9280
9281 #[test]
9282 fn test_double_dash_preserve_is_not_a_separator() {
9283 let spec = spec_with_args([
9286 SpecArg::builder()
9287 .name("kept")
9288 .var(true)
9289 .var_max(1)
9290 .required(false)
9291 .double_dash(SpecDoubleDashChoices::Preserve)
9292 .build(),
9293 required_arg("rest"),
9294 ]);
9295
9296 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9297
9298 assert_eq!(arg_value(&parsed, "kept"), "--");
9299 assert!(parsed.args.keys().all(|a| a.name != "rest"));
9300 assert!(!parsed.double_dash_seen);
9301 assert_eq!(parsed.errors.len(), 1);
9302 }
9303
9304 #[test]
9305 fn test_double_dash_required_does_not_bail_in_parse_partial() {
9306 let spec = spec_with_args([required_arg("file")]);
9308
9309 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9310 assert!(parse(&spec, &input(&["test", "x"])).is_err());
9311 }
9312
9313 #[test]
9314 fn test_double_dash_without_required_arg_does_not_move_cursor() {
9315 let spec = spec_with_args([
9317 SpecArg::builder().name("first").required(false).build(),
9318 SpecArg::builder().name("second").required(false).build(),
9319 ]);
9320
9321 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9322
9323 assert_eq!(arg_value(&parsed, "first"), "a");
9324 assert_eq!(arg_value(&parsed, "second"), "b");
9325 assert!(parsed.next_arg.is_none());
9326 }
9327
9328 #[test]
9329 fn test_parser_with_custom_env_for_required_arg() {
9330 let spec = spec_with_arg(
9331 SpecArg::builder()
9332 .name("name")
9333 .env("NAME")
9334 .required(true)
9335 .build(),
9336 );
9337 std::env::remove_var("NAME");
9338
9339 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9340 .expect("parse should succeed with custom env");
9341 assert_eq!(parsed.args.len(), 1);
9342 assert_eq!(first_string_value(&parsed), "john");
9343 }
9344
9345 #[test]
9346 fn test_parser_with_custom_env_for_required_flag() {
9347 let spec = spec_with_flag(
9348 SpecFlag::builder()
9349 .long("name")
9350 .env("NAME")
9351 .required(true)
9352 .arg(SpecArg::builder().name("name").build())
9353 .build(),
9354 );
9355 std::env::remove_var("NAME");
9356
9357 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9358 .expect("parse should succeed with custom env");
9359 assert_eq!(parsed.flags.len(), 1);
9360 assert_eq!(first_string_value(&parsed), "jane");
9361 }
9362
9363 #[test]
9364 fn test_flag_environment_fallbacks_preserve_declaration_order() {
9365 let spec = spec_with_flag(
9366 SpecFlag::builder()
9367 .long("name")
9368 .env("NAME")
9369 .env_fallback("OLD_NAME")
9370 .env_fallback("OLDER_NAME")
9371 .deprecated_env("DEPRECATED_NAME")
9372 .arg(SpecArg::builder().name("name").build())
9373 .build(),
9374 );
9375
9376 let parsed = parse_with_env(
9377 &spec,
9378 &["test"],
9379 &[
9380 ("NAME", "canonical"),
9381 ("OLD_NAME", "fallback"),
9382 ("DEPRECATED_NAME", "deprecated"),
9383 ],
9384 )
9385 .unwrap();
9386 assert_eq!(first_string_value(&parsed), "canonical");
9387
9388 let parsed = parse_with_env(
9389 &spec,
9390 &["test"],
9391 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9392 )
9393 .unwrap();
9394 assert_eq!(first_string_value(&parsed), "old");
9395
9396 let parsed =
9397 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9398 assert_eq!(first_string_value(&parsed), "deprecated");
9399 }
9400
9401 #[test]
9402 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9403 let spec = spec_with_flag(
9404 SpecFlag::builder()
9405 .long("name")
9406 .env("NAME")
9407 .deprecated_env("DEPRECATED_NAME")
9408 .arg(SpecArg::builder().name("name").build())
9409 .build(),
9410 );
9411
9412 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9414 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9415
9416 let parsed =
9417 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9418 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9419 assert_eq!(
9420 parsed.warnings[0].kind,
9421 crate::warn::WarningKind::DeprecatedEnv
9422 );
9423 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9424 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9425 assert_eq!(first_string_value(&parsed), "deprecated");
9427 }
9428
9429 #[test]
9430 fn a_deprecated_flag_reports_only_when_it_was_used() {
9431 let spec = spec_with_flag(
9432 SpecFlag::builder()
9433 .long("output")
9434 .deprecated("use --out")
9435 .deprecated_remove_at("3.0.0")
9436 .arg(SpecArg::builder().name("output").build())
9437 .build(),
9438 );
9439
9440 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9441 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9442
9443 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9444 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9445 assert_eq!(
9446 parsed.warnings[0].kind,
9447 crate::warn::WarningKind::DeprecatedFlag
9448 );
9449 assert_eq!(parsed.warnings[0].name, "--output");
9451 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
9452 assert_eq!(
9453 parsed.warnings[0].render(),
9454 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
9455 );
9456 }
9457
9458 #[test]
9459 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
9460 let flag = SpecFlag::builder()
9461 .long("output")
9462 .deprecated("use --out")
9463 .deprecated_warn_at("9.0.0")
9464 .arg(SpecArg::builder().name("output").build())
9465 .build();
9466 let mut spec = spec_with_flag(flag);
9467 spec.version = Some("2.0.0".to_string());
9468
9469 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9470 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9471
9472 spec.version = Some("9.0.0".to_string());
9474 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9475 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9476 }
9477
9478 #[test]
9479 fn test_parser_with_custom_env_still_fails_when_missing() {
9480 let spec = spec_with_arg(
9481 SpecArg::builder()
9482 .name("name")
9483 .env("NAME")
9484 .required(true)
9485 .build(),
9486 );
9487 std::env::remove_var("NAME");
9488 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
9489 }
9490
9491 #[test]
9492 fn test_parser_does_not_treat_env_choice_value_as_help() {
9493 let spec = spec_with_arg(
9494 SpecArg::builder()
9495 .name("env")
9496 .env("CURRENT_ENV")
9497 .choices(["dev", "staging"])
9498 .required(false)
9499 .build(),
9500 );
9501
9502 assert_parse_err(
9503 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
9504 "Invalid choice for arg env: --help, expected one of dev, staging",
9505 );
9506 }
9507
9508 #[test]
9509 fn test_parser_does_not_treat_default_choice_value_as_help() {
9510 let spec = spec_with_flag(
9511 SpecFlag::builder()
9512 .long("env")
9513 .arg(
9514 SpecArg::builder()
9515 .name("env")
9516 .choices(["dev", "staging"])
9517 .build(),
9518 )
9519 .default_value("--help")
9520 .build(),
9521 );
9522
9523 assert_parse_err(
9524 parse_with_env(&spec, &["test"], &[]),
9525 "Invalid choice for option env: --help, expected one of dev, staging",
9526 );
9527 }
9528
9529 fn words(of: &[&str]) -> Vec<String> {
9531 of.iter().map(|s| s.to_string()).collect()
9532 }
9533
9534 #[test]
9535 fn a_command_that_needs_a_subcommand_says_so() {
9536 let spec: Spec = r#"
9540name "ex"
9541bin "ex"
9542cmd "gen" subcommand_required=#true {
9543 cmd "two" {}
9544 cmd "one" {}
9545 cmd "secret" hide=#true {}
9546 alias "g"
9547}
9548cmd "open" {
9549 cmd "sub" {}
9550}
9551"#
9552 .parse()
9553 .unwrap();
9554
9555 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9556 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9560
9561 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9563 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9564
9565 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9567
9568 parse(&spec, &words(&["ex", "open"])).unwrap();
9571 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9572 }
9573
9574 #[test]
9575 fn arg_required_else_help_observes_the_selected_commands_argv() {
9576 let spec: Spec = r#"
9577name "ex"
9578bin "ex"
9579flag "--verbose" global=#true
9580cmd "run" arg_required_else_help=#true {
9581 flag "--all"
9582}
9583"#
9584 .parse()
9585 .unwrap();
9586 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9587
9588 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9589 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9590
9591 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9594 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9595
9596 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9597 }
9598
9599 #[test]
9600 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9601 let spec: Spec = r#"
9602name "ex"
9603bin "ex"
9604unknown_flags "error"
9605external_subcommand #true
9606cmd "install"
9607flag "-v --verbose" global=#true
9608"#
9609 .parse()
9610 .unwrap();
9611
9612 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9613 assert_eq!(
9614 parsed.external,
9615 Some(vec!["foo".into(), "--help".into(), "bar".into()])
9616 );
9617 assert!(parsed.flags.is_empty());
9618
9619 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9621 assert_eq!(parsed.cmd.name, "install");
9622 assert!(parsed.external.is_none());
9623
9624 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9626 assert_eq!(
9627 parsed.external,
9628 Some(vec!["foo".into(), "--verbose".into()])
9629 );
9630 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9631
9632 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9634
9635 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9639 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9640 }
9641
9642 #[test]
9643 fn an_external_subcommand_satisfies_subcommand_required() {
9644 let mut spec: Spec = r#"
9645name "ex"
9646bin "ex"
9647external_subcommand #true
9648cmd "install"
9649"#
9650 .parse()
9651 .unwrap();
9652 spec.cmd.subcommand_required = true;
9653
9654 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9655 assert!(parse(&spec, &input(&["ex"])).is_err());
9656 }
9657
9658 #[test]
9659 fn a_default_subcommand_outranks_an_external_one() {
9660 let spec: Spec = r#"
9661name "ex"
9662bin "ex"
9663default_subcommand "run"
9664external_subcommand #true
9665cmd "run" {
9666 arg "[task]"
9667}
9668"#
9669 .parse()
9670 .unwrap();
9671
9672 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
9673 assert_eq!(parsed.cmd.name, "run");
9674 assert!(parsed.external.is_none());
9675 assert_eq!(first_string_value(&parsed), "build");
9676 }
9677
9678 #[test]
9679 fn multicall_basename_strips_a_path_and_exe() {
9680 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
9681 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
9682 assert_eq!(multicall_basename("LS.EXE"), "LS");
9683 assert_eq!(multicall_basename("busybox"), "busybox");
9684 }
9685
9686 #[test]
9687 fn a_multicall_applet_is_the_first_word() {
9688 let spec: Spec = r#"
9689name "busybox"
9690bin "busybox"
9691multicall #true
9692cmd "ls" {
9693 arg "[ARGS]" var=#true
9694}
9695cmd "cat"
9696"#
9697 .parse()
9698 .unwrap();
9699
9700 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
9702 assert_eq!(parsed.cmd.name, "ls");
9703 match parsed.args.values().next() {
9704 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
9705 other => panic!("expected ARGS to collect -l, got {other:?}"),
9706 }
9707
9708 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
9710 assert_eq!(parsed.cmd.name, "ls");
9711
9712 let mut configured = spec.clone();
9714 configured.name = "BusyBox".to_string();
9715 configured.bin = "/opt/bin/busybox.exe".to_string();
9716 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
9717 assert_eq!(parsed.cmd.name, "ls");
9718
9719 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
9721 assert_eq!(parsed.cmd.name, "ls");
9722
9723 let mut plain = spec.clone();
9725 plain.multicall = false;
9726 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
9727 assert_eq!(parsed.cmd.name, "ls");
9728 }
9729
9730 #[test]
9731 fn a_multicall_unknown_applet_can_be_external() {
9732 let spec: Spec = r#"
9733name "busybox"
9734bin "busybox"
9735multicall #true
9736unknown_flags "error"
9737external_subcommand #true
9738cmd "ls"
9739"#
9740 .parse()
9741 .unwrap();
9742
9743 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
9744 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
9745
9746 let mut closed = spec.clone();
9747 closed.cmd.external_subcommand = false;
9748 assert!(parse(&closed, &input(&["wat"])).is_err());
9749 }
9750
9751 #[cfg(feature = "unstable_choices_env")]
9752 #[test]
9753 fn test_parser_arg_choices_from_custom_env() {
9754 let spec = spec_arg_choices_env("DEPLOY_ENVS");
9755
9756 let parsed =
9757 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
9758 assert_eq!(first_string_value(&parsed), "bar");
9759
9760 assert_parse_err(
9761 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
9762 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
9763 );
9764 assert_parse_err(
9765 parse_with_env(&spec, &["test", "prod"], &[]),
9766 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
9767 );
9768 }
9769
9770 #[cfg(feature = "unstable_choices_env")]
9771 #[test]
9772 fn test_parser_validates_flag_choices_from_custom_env() {
9773 let spec = spec_flag_choices_env("DEPLOY_ENVS");
9774 let parsed = parse_with_env(
9775 &spec,
9776 &["test", "--env", "baz"],
9777 &[("DEPLOY_ENVS", "foo,bar baz")],
9778 )
9779 .unwrap();
9780 assert_eq!(first_string_value(&parsed), "baz");
9781 }
9782
9783 #[cfg(feature = "unstable_choices_env")]
9784 #[test]
9785 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
9786 let arg_env_spec = spec_with_arg(
9787 SpecArg::builder()
9788 .name("env")
9789 .env("CURRENT_ENV")
9790 .choices_env("DEPLOY_ENVS")
9791 .build(),
9792 );
9793 assert_parse_err(
9794 parse_with_env(
9795 &arg_env_spec,
9796 &["test"],
9797 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
9798 ),
9799 "Invalid choice for arg env: prod, expected one of dev, staging",
9800 );
9801
9802 let flag_default_spec = spec_with_flag(
9803 SpecFlag::builder()
9804 .long("env")
9805 .arg(
9806 SpecArg::builder()
9807 .name("env")
9808 .choices_env("DEPLOY_ENVS")
9809 .build(),
9810 )
9811 .default_value("prod")
9812 .build(),
9813 );
9814 assert_parse_err(
9815 parse_with_env(
9816 &flag_default_spec,
9817 &["test"],
9818 &[("DEPLOY_ENVS", "dev,staging")],
9819 ),
9820 "Invalid choice for option env: prod, expected one of dev, staging",
9821 );
9822 }
9823
9824 #[test]
9825 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
9826 let spec: Spec = r#"
9827 flag "-v --verbose" var=#true
9828 arg "[database]" default="myapp_dev"
9829 arg "[args...]"
9830 "#
9831 .parse()
9832 .unwrap();
9833 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9834 .into_iter()
9835 .map(String::from)
9836 .collect();
9837 let parsed = parse(&spec, &input).unwrap();
9838 let env = parsed.as_env();
9839 assert_eq!(env.get("usage_database").unwrap(), "mydb");
9840 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
9841 }
9842
9843 #[test]
9844 fn test_variadic_arg_captures_unknown_flags() {
9845 let cmd = SpecCommand::builder()
9846 .name("test")
9847 .flag(SpecFlag::builder().short('v').long("verbose").build())
9848 .arg(SpecArg::builder().name("database").required(false).build())
9849 .arg(
9850 SpecArg::builder()
9851 .name("args")
9852 .required(false)
9853 .var(true)
9854 .build(),
9855 )
9856 .build();
9857 let spec = Spec {
9858 name: "test".to_string(),
9859 bin: "test".to_string(),
9860 cmd,
9861 ..Default::default()
9862 };
9863
9864 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9866 .into_iter()
9867 .map(String::from)
9868 .collect();
9869 let parsed = parse(&spec, &input).unwrap();
9870 assert_eq!(parsed.args.len(), 2);
9871 let args_val = parsed
9872 .args
9873 .iter()
9874 .find(|(a, _)| a.name == "args")
9875 .unwrap()
9876 .1;
9877 match args_val {
9878 ParseValue::MultiString(v) => {
9879 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9880 }
9881 _ => panic!("Expected MultiString, got {:?}", args_val),
9882 }
9883 }
9884
9885 #[test]
9886 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
9887 let cmd = SpecCommand::builder()
9888 .name("test")
9889 .flag(SpecFlag::builder().short('v').long("verbose").build())
9890 .arg(SpecArg::builder().name("database").required(false).build())
9891 .arg(
9892 SpecArg::builder()
9893 .name("args")
9894 .required(false)
9895 .var(true)
9896 .build(),
9897 )
9898 .build();
9899 let spec = Spec {
9900 name: "test".to_string(),
9901 bin: "test".to_string(),
9902 cmd,
9903 ..Default::default()
9904 };
9905
9906 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
9908 .into_iter()
9909 .map(String::from)
9910 .collect();
9911 let parsed = parse(&spec, &input).unwrap();
9912 assert_eq!(parsed.args.len(), 2);
9913 let args_val = parsed
9914 .args
9915 .iter()
9916 .find(|(a, _)| a.name == "args")
9917 .unwrap()
9918 .1;
9919 match args_val {
9920 ParseValue::MultiString(v) => {
9921 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9922 }
9923 _ => panic!("Expected MultiString, got {:?}", args_val),
9924 }
9925 }
9926
9927 #[test]
9928 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
9929 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
9932
9933 let input: Vec<String> = vec!["test", "--option=3"]
9936 .into_iter()
9937 .map(String::from)
9938 .collect();
9939 let parsed = parse(&spec, &input).unwrap();
9940 let env = parsed.as_env();
9941 assert_eq!(
9942 env.get("usage_other_args").map(String::as_str),
9943 Some("'--option=3'"),
9944 "expected a single --option=3 token, got {:?}",
9945 env.get("usage_other_args"),
9946 );
9947
9948 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
9950 .into_iter()
9951 .map(String::from)
9952 .collect();
9953 let parsed2 = parse(&spec, &input2).unwrap();
9954 let env2 = parsed2.as_env();
9955 assert_eq!(
9956 env2.get("usage_other_args").map(String::as_str),
9957 Some("'--foo=bar' '--baz=qux'"),
9958 "expected two intact tokens, got {:?}",
9959 env2.get("usage_other_args"),
9960 );
9961
9962 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
9964 .into_iter()
9965 .map(String::from)
9966 .collect();
9967 let parsed3 = parse(&spec, &input3).unwrap();
9968 let env3 = parsed3.as_env();
9969 assert_eq!(
9970 env3.get("usage_other_args").map(String::as_str),
9971 Some("positional1 '--option=3' positional2"),
9972 "expected positional args and intact flag token, got {:?}",
9973 env3.get("usage_other_args"),
9974 );
9975 }
9976
9977 #[test]
9978 fn test_allow_hyphen_values_consumes_short_flag_collision() {
9979 let spec = r#"
9980flag "-d --working-dir <DIR>"
9981flag "-a --args <ARGS>" allow_hyphen_values=#true
9982"#
9983 .parse::<Spec>()
9984 .unwrap();
9985
9986 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9987
9988 assert_eq!(parsed.flags.len(), 1);
9989 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9990 }
9991
9992 #[test]
9993 fn test_allow_hyphen_values_consumes_embedded_long_value() {
9994 let spec = r#"
9995flag "-d --working-dir <DIR>"
9996flag "-a --args <ARGS>" allow_hyphen_values=#true
9997"#
9998 .parse::<Spec>()
9999 .unwrap();
10000
10001 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10002
10003 assert_eq!(parsed.flags.len(), 1);
10004 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10005 }
10006
10007 #[test]
10008 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10009 let spec = r#"
10014flag "-a --args <ARGS>" allow_hyphen_values=#true
10015arg "[rest]..."
10016"#
10017 .parse::<Spec>()
10018 .unwrap();
10019
10020 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10021
10022 assert_eq!(flag_string_value(&parsed, "args"), "--");
10023 let rest = parsed
10024 .args
10025 .values()
10026 .next()
10027 .expect("expected the word after the separator to reach the argument");
10028 assert_eq!(rest.to_string(), "-x");
10029 }
10030
10031 #[test]
10032 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10033 let spec = r#"
10038flag "-a --args <ARGS>..." allow_hyphen_values=#true
10039"#
10040 .parse::<Spec>()
10041 .unwrap();
10042
10043 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10044
10045 let flag = parsed
10046 .flags
10047 .keys()
10048 .find(|flag| flag.name == "args")
10049 .expect("expected args flag");
10050 match parsed.flags.get(flag).expect("expected args value") {
10051 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10052 other => panic!("expected a list of values, got {other:?}"),
10053 }
10054 }
10055
10056 #[test]
10057 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10058 let spec = r#"
10059flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10060"#
10061 .parse::<Spec>()
10062 .unwrap();
10063
10064 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10065
10066 let flag = parsed
10067 .flags
10068 .keys()
10069 .find(|flag| flag.name == "args")
10070 .expect("expected args flag");
10071 let value = parsed.flags.get(flag).expect("expected args value");
10072 match value {
10073 ParseValue::MultiString(values) => {
10074 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10075 }
10076 _ => panic!("expected MultiString, got {value:?}"),
10077 }
10078 }
10079
10080 #[test]
10081 fn test_require_equals_accepts_attached_and_refuses_detached() {
10082 let spec = r#"
10083flag "--inspect <PORT>" require_equals=#true
10084"#
10085 .parse::<Spec>()
10086 .unwrap();
10087
10088 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10089 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10090
10091 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10092 let msg = format!("{err}");
10093 assert!(
10094 msg.contains("requires an argument") || msg.contains("inspect"),
10095 "detached value must be refused: {msg}"
10096 );
10097 }
10098
10099 #[test]
10100 fn boolean_flags_can_accept_attached_values_when_enabled() {
10101 let spec: Spec = r#"
10102name "ex"
10103bin "ex"
10104flag "--color" negate="--no-color" bool_value=#true
10105arg "[rest]"
10106"#
10107 .parse()
10108 .unwrap();
10109
10110 for (token, expected) in [
10111 ("--color", true),
10112 ("--color=true", true),
10113 ("--color=false", false),
10114 ("--no-color", false),
10115 ("--no-color=false", true),
10116 ] {
10117 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10118 assert!(
10119 matches!(
10120 parsed.flags.get(&spec.cmd.flags[0]),
10121 Some(ParseValue::Bool(value)) if *value == expected
10122 ),
10123 "{token}"
10124 );
10125 }
10126
10127 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10128 assert!(matches!(
10129 parsed.args.get(&spec.cmd.args[0]),
10130 Some(ParseValue::String(value)) if value == "word"
10131 ));
10132 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10133 assert!(err.to_string().contains("expected `true` or `false`"));
10134
10135 let strict: Spec = r#"
10136name "ex"
10137bin "ex"
10138args_override_self #false
10139flag "--color" negate="--no-color" bool_value=#true
10140"#
10141 .parse()
10142 .unwrap();
10143 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10144 let parsed = parse(
10145 &strict,
10146 &input(&["ex", "--color=false", "--no-color=false"]),
10147 )
10148 .unwrap();
10149 assert!(matches!(
10150 parsed.flags.get(&strict.cmd.flags[0]),
10151 Some(ParseValue::Bool(true))
10152 ));
10153 }
10154
10155 #[test]
10156 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10157 let spec = r#"
10158flag "-a --all"
10159flag "-i --inspect <PORT>" require_equals=#true
10160"#
10161 .parse::<Spec>()
10162 .unwrap();
10163
10164 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10165 let msg = format!("{err}");
10166 assert!(
10167 msg.contains("requires an argument") || msg.contains("inspect"),
10168 "bundled short must refuse the following word: {msg}"
10169 );
10170 }
10171
10172 #[test]
10173 fn test_default_missing_binds_when_the_value_is_left_off() {
10174 let spec = r#"
10175flag "-c --color <WHEN>" default_missing="always"
10176flag "-v --verbose"
10177"#
10178 .parse::<Spec>()
10179 .unwrap();
10180
10181 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10182 assert_eq!(flag_string_value(&parsed, "color"), "always");
10183
10184 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10185 assert_eq!(flag_string_value(&parsed, "color"), "never");
10186
10187 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10188 assert_eq!(flag_string_value(&parsed, "color"), "never");
10189
10190 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10191 assert_eq!(flag_string_value(&parsed, "color"), "always");
10192 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10193
10194 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10195 assert_eq!(flag_string_value(&parsed, "color"), "");
10196
10197 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10198 assert_eq!(flag_string_value(&parsed, "color"), "never");
10199
10200 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10201 assert_eq!(flag_string_value(&parsed, "color"), "always");
10202 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10203 }
10204
10205 #[test]
10206 fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10207 let spec = r#"
10208flag "--apps <N>"
10209flag "--jobs <N>" default_missing="default missing"
10210flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10211"#
10212 .parse::<Spec>()
10213 .unwrap();
10214
10215 let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10216 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10217
10218 let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10219 assert!(
10220 err.to_string().contains("unexpected word: -1"),
10221 "default_missing must keep an unopted negative value separate: {err}"
10222 );
10223
10224 let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10225 assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10226
10227 let external_spec = r#"
10228external_subcommand #true
10229flag "--apps <N>"
10230"#
10231 .parse::<Spec>()
10232 .unwrap();
10233 let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10234 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10235 assert!(parsed.external.is_none());
10236
10237 for words in [
10238 &["test", "--apps", "--jobs", "1"][..],
10239 &["test", "--apps", "--kids", "-1"][..],
10240 ] {
10241 let err = parse(&spec, &input(words)).unwrap_err();
10242 let message = err.to_string();
10243 assert!(
10244 message.contains("--apps") && message.contains("requires an argument"),
10245 "the earlier flag must report its missing value for {words:?}: {message}"
10246 );
10247 }
10248 }
10249
10250 #[test]
10251 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10252 let spec = r#"
10253flag "--bump [LEVEL]" value_optional=#true
10254flag "--verbose"
10255arg "[FILE]"
10256"#
10257 .parse::<Spec>()
10258 .unwrap();
10259
10260 let absent = parse(&spec, &input(&["test"])).unwrap();
10261 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10262
10263 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10264 let bump = bare
10265 .flags
10266 .iter()
10267 .find(|(flag, _)| flag.name == "bump")
10268 .map(|(_, value)| value)
10269 .unwrap();
10270 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10271 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10272 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10273
10274 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10275 assert_eq!(flag_string_value(&explicit, "bump"), "");
10276
10277 let corrected = parse(
10278 &spec,
10279 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10280 )
10281 .unwrap();
10282 let bump = corrected
10283 .flags
10284 .iter()
10285 .find(|(flag, _)| flag.name == "bump")
10286 .map(|(_, value)| value)
10287 .unwrap();
10288 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10289
10290 let collecting = r#"
10291flag "--tag [TAG]..." value_optional=#true
10292flag "--verbose"
10293"#
10294 .parse::<Spec>()
10295 .unwrap();
10296 let valued = parse(
10297 &collecting,
10298 &input(&["test", "--tag", "one", "two", "--verbose"]),
10299 )
10300 .unwrap();
10301 let tag = valued
10302 .flags
10303 .iter()
10304 .find(|(flag, _)| flag.name == "tag")
10305 .map(|(_, value)| value)
10306 .unwrap();
10307 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10308 }
10309
10310 #[test]
10311 fn test_repeatable_bare_optional_values_count_each_occurrence() {
10312 let spec = r#"
10313flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10314"#
10315 .parse::<Spec>()
10316 .unwrap();
10317
10318 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10319 let tag = parsed
10320 .flags
10321 .iter()
10322 .find(|(flag, _)| flag.name == "tag")
10323 .map(|(_, value)| value)
10324 .unwrap();
10325 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10326
10327 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10328 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10329 }
10330
10331 #[test]
10332 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10333 let spec = r#"
10334flag "--tag [TAG]..." var=#true value_optional=#true
10335flag "--verbose"
10336"#
10337 .parse::<Spec>()
10338 .unwrap();
10339
10340 for argv in [
10341 &["test", "--tag", "one", "two"][..],
10342 &["test", "--tag", "one", "two", "--verbose"][..],
10343 &["test", "--tag", "one", "--tag", "two"][..],
10344 ] {
10345 let parsed = parse(&spec, &input(argv)).unwrap();
10346 let tag = parsed
10347 .flags
10348 .iter()
10349 .find(|(flag, _)| flag.name == "tag")
10350 .map(|(_, value)| value)
10351 .unwrap();
10352 assert!(
10353 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10354 "argv={argv:?}: {tag:?}"
10355 );
10356 }
10357
10358 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10359 let tag = bare
10360 .flags
10361 .iter()
10362 .find(|(flag, _)| flag.name == "tag")
10363 .map(|(_, value)| value)
10364 .unwrap();
10365 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10366 }
10367
10368 #[test]
10369 fn test_default_missing_with_require_equals_refuses_the_following_word() {
10370 let spec = r#"
10371flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10372arg "[rest]"
10373"#
10374 .parse::<Spec>()
10375 .unwrap();
10376
10377 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10378 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10379
10380 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10381 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10382
10383 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10385 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10386 assert_eq!(
10387 parsed
10388 .args
10389 .values()
10390 .next()
10391 .map(|v| v.to_string())
10392 .as_deref(),
10393 Some("80")
10394 );
10395
10396 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10397 assert_eq!(flag_string_value(&parsed, "inspect"), "");
10398 }
10399
10400 #[test]
10401 fn test_default_missing_must_be_a_choice() {
10402 let spec = r#"
10403flag "--color <WHEN>" default_missing="always" {
10404 choices "auto" "always" "never"
10405}
10406"#
10407 .parse::<Spec>()
10408 .unwrap();
10409
10410 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10411 assert_eq!(flag_string_value(&parsed, "color"), "always");
10412
10413 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10414 assert_eq!(flag_string_value(&parsed, "color"), "never");
10415
10416 let spec = r#"
10417flag "--color <WHEN>" default_missing="wat" {
10418 choices "auto" "always" "never"
10419}
10420"#
10421 .parse::<Spec>()
10422 .unwrap();
10423
10424 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10425 let msg = format!("{err}");
10426 assert!(
10427 msg.contains("Invalid choice for option color: wat"),
10428 "missing default has to pass choices the same way a typed value does: {msg}"
10429 );
10430
10431 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
10432 let msg = format!("{err}");
10433 assert!(
10434 msg.contains("Invalid choice for option color: wat"),
10435 "an attached value that is not a choice is still refused: {msg}"
10436 );
10437
10438 let spec = r#"
10439flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
10440 choices "9229" "80"
10441}
10442arg "[rest]"
10443"#
10444 .parse::<Spec>()
10445 .unwrap();
10446
10447 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
10448 let msg = format!("{err}");
10449 assert!(
10450 msg.contains("Invalid choice for option inspect: wat"),
10451 "require_equals still binds the missing string, so the error is the choice: {msg}"
10452 );
10453 }
10454
10455 #[test]
10456 fn test_hyphen_values_still_start_short_flag_parsing() {
10457 let spec = r#"
10458flag "-d --working-dir <DIR>"
10459flag "-a --args <ARGS>"
10460"#
10461 .parse::<Spec>()
10462 .unwrap();
10463
10464 let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
10465 let message = err.to_string();
10466 assert!(
10467 message.contains("--args") && message.contains("requires an argument"),
10468 "the recognized -d must leave the earlier -a missing: {message}"
10469 );
10470 }
10471
10472 mod available_flags {
10475 use super::*;
10476
10477 fn spec() -> Spec {
10478 r#"
10479bin "test"
10480flag "-v --verbose" global=#true
10481flag "--raw" global=#true effect="write"
10482flag "--local-only"
10483cmd "run" {
10484 flag "-r --raw"
10485 flag "-w --watch"
10486 cmd "once"
10487}
10488"#
10489 .parse::<Spec>()
10490 .unwrap()
10491 }
10492
10493 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
10494 let mut chain = vec![&spec.cmd];
10495 for segment in path {
10496 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
10497 }
10498 chain
10499 }
10500
10501 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
10502 let mut names: Vec<_> = available_flags(&chain(spec, path))
10503 .iter()
10504 .map(|f| f.name.clone())
10505 .collect();
10506 names.sort();
10507 names
10508 }
10509
10510 #[test]
10511 fn an_empty_chain_yields_nothing() {
10512 assert!(available_flags(&[]).is_empty());
10513 }
10514
10515 #[test]
10516 fn the_root_gets_its_own_flags() {
10517 let spec = spec();
10518 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
10519 }
10520
10521 #[test]
10522 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
10523 let spec = spec();
10524 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10525 }
10526
10527 #[test]
10528 fn a_re_declared_global_is_listed_once() {
10529 let spec = r#"
10532bin "test"
10533flag "-y --yes" global=#true effect="write"
10534cmd "rm" {
10535 flag "-y --yes"
10536}
10537"#
10538 .parse::<Spec>()
10539 .unwrap();
10540 let flags = available_flags(&chain(&spec, &["rm"]));
10541 assert_eq!(flags.len(), 1, "{flags:?}");
10542 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10543 }
10544
10545 #[test]
10546 fn a_re_declared_global_keeps_the_globals_declaration() {
10547 let spec = spec();
10551 let flags = available_flags(&chain(&spec, &["run"]));
10552 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10553 assert!(raw.global);
10554 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10555 assert_eq!(raw.short, ['r']);
10556 }
10557
10558 #[test]
10559 fn it_matches_what_a_parse_accepts() {
10560 let spec = spec();
10563 for path in [vec![], vec!["run"], vec!["run", "once"]] {
10564 let argv = std::iter::once("test".to_string())
10565 .chain(path.iter().map(|s| s.to_string()))
10566 .collect::<Vec<_>>();
10567 let parsed = parse_partial(&spec, &argv).unwrap();
10568
10569 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10570 .map(|f| f.name.clone())
10571 .collect();
10572 from_parse.sort();
10573 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10574 }
10575 }
10576 }
10577
10578 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10583 parsed
10584 .tokens
10585 .iter()
10586 .find(|token| token.index == index)
10587 .unwrap_or_else(|| panic!("no token at {index}"))
10588 .roles
10589 .iter()
10590 .map(render_role)
10591 .collect()
10592 }
10593
10594 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10595 parsed
10596 .flag_origins
10597 .iter()
10598 .find(|(f, _)| f.name == flag)
10599 .map(|(_, origins)| origins.clone())
10600 .unwrap_or_default()
10601 }
10602
10603 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10604 let env = env
10605 .iter()
10606 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10607 .collect();
10608 Parser::new(spec)
10609 .with_env(env)
10610 .explain(&input(words))
10611 .unwrap()
10612 }
10613
10614 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10615 explain_with_env(spec, words, &[])
10616 }
10617
10618 #[test]
10619 fn an_attached_long_value_is_recorded_on_the_flag_token() {
10620 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10621 .parse()
10622 .unwrap();
10623
10624 let parsed = explain(&spec, &["ex", "--env=prod"]);
10625
10626 assert_eq!(roles(&parsed, 0), ["program"]);
10627 assert_eq!(
10628 roles(&parsed, 1),
10629 ["flag env as --env", "value of env = [\"prod\"], attached"]
10630 );
10631 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10634 }
10635
10636 #[test]
10637 fn a_detached_long_value_is_recorded_on_its_own_token() {
10638 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10639 .parse()
10640 .unwrap();
10641
10642 let parsed = explain(&spec, &["ex", "--env", "prod"]);
10643
10644 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10645 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10646 }
10647
10648 #[test]
10649 fn a_short_bundle_is_attributed_to_the_bundle_token() {
10650 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10651 .parse()
10652 .unwrap();
10653
10654 let parsed = explain(&spec, &["ex", "-abj8"]);
10655
10656 assert_eq!(
10659 roles(&parsed, 1),
10660 [
10661 "flag a as -a",
10662 "flag b as -b",
10663 "flag j as -j",
10664 "value of j = [\"8\"], attached",
10665 ]
10666 );
10667 assert_eq!(parsed.tokens.len(), 2);
10668 }
10669
10670 #[test]
10671 fn a_delimiter_splits_one_token_into_several_values() {
10672 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
10673 .parse()
10674 .unwrap();
10675
10676 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
10677
10678 assert_eq!(
10679 roles(&parsed, 2),
10680 ["value of tags = [\"a\", \"b\", \"c\"]"],
10681 "the values meant, not the word typed"
10682 );
10683 }
10684
10685 #[test]
10686 fn a_separator_and_the_words_after_it_are_distinguished() {
10687 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
10688 .parse()
10689 .unwrap();
10690
10691 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
10692
10693 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
10694 assert_eq!(roles(&parsed, 2), ["separator"]);
10695 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
10697 }
10698
10699 #[test]
10700 fn a_second_separator_is_data() {
10701 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
10702 .parse()
10703 .unwrap();
10704
10705 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
10706
10707 assert_eq!(roles(&parsed, 1), ["separator"]);
10708 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
10709 }
10710
10711 #[test]
10712 fn an_unknown_flag_says_what_took_it() {
10713 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10714 .parse()
10715 .unwrap();
10716
10717 let parsed = explain(&spec, &["ex", "--wat"]);
10718
10719 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
10722 }
10723
10724 #[test]
10725 fn a_subcommand_word_is_not_a_positional() {
10726 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
10727 .parse()
10728 .unwrap();
10729
10730 let parsed = explain(&spec, &["ex", "build", "a"]);
10731
10732 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
10733 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
10734 }
10735
10736 #[test]
10737 fn a_multicall_applet_is_read_at_argv0() {
10738 let spec: Spec =
10739 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
10740 .parse()
10741 .unwrap();
10742
10743 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
10744
10745 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
10748 assert!(parsed.tokens[0].synthesized);
10749 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
10750 }
10751
10752 #[test]
10753 fn words_the_parse_never_reached_say_so() {
10754 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10755 .parse()
10756 .unwrap();
10757
10758 let parsed = Parser::new(&spec)
10759 .explain(&input(&["ex", "--help", "a"]))
10760 .unwrap();
10761
10762 assert_eq!(roles(&parsed, 2), ["unread"]);
10763 }
10764
10765 #[test]
10766 fn an_env_origin_names_the_variable_that_fired() {
10767 let spec: Spec =
10768 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
10769 .parse()
10770 .unwrap();
10771
10772 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
10773 assert_eq!(
10774 origins(&primary, "token"),
10775 [ValueOrigin::Env("EX_TOKEN".to_string())]
10776 );
10777
10778 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
10781 assert_eq!(
10782 origins(&fallback, "token"),
10783 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
10784 );
10785 }
10786
10787 #[test]
10788 fn a_default_origin_is_recorded_for_flags_and_args() {
10789 let spec: Spec =
10790 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
10791 .parse()
10792 .unwrap();
10793
10794 let parsed = explain(&spec, &["ex"]);
10795
10796 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
10797 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
10798 assert_eq!(arg.name, "src");
10799 assert_eq!(origins, &[ValueOrigin::Default]);
10800 }
10801
10802 #[test]
10803 fn a_default_if_origin_carries_the_condition_that_fired() {
10804 let spec: Spec = r#"
10805name "ex"
10806bin "ex"
10807flag "--profile <p>"
10808flag "--strict" {
10809 default_if "--profile" "prod" "true"
10810}
10811 "#
10812 .parse()
10813 .unwrap();
10814
10815 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
10816
10817 assert_eq!(
10820 origins(&parsed, "strict"),
10821 [ValueOrigin::DefaultIf {
10822 selector: "--profile".to_string(),
10823 when: Some("prod".to_string()),
10824 }]
10825 );
10826 }
10827
10828 #[test]
10829 fn a_bare_optional_value_flag_records_default_missing() {
10830 let spec: Spec =
10831 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
10832 .parse()
10833 .unwrap();
10834
10835 let parsed = explain(&spec, &["ex", "--color", "-v"]);
10836
10837 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
10840 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10841 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
10842 }
10843
10844 #[test]
10845 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
10846 let spec: Spec =
10847 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
10848 .parse()
10849 .unwrap();
10850
10851 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
10852
10853 assert_eq!(
10855 roles(&parsed, 1),
10856 [
10857 "flag color as --color",
10858 "value of color = [\"red\"], attached"
10859 ]
10860 );
10861 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10862 }
10863
10864 #[test]
10865 fn an_override_names_the_flag_that_did_it() {
10866 let spec: Spec =
10867 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
10868 .parse()
10869 .unwrap();
10870
10871 let parsed = explain(&spec, &["ex", "--loud"]);
10872
10873 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
10876 assert!(origins(&parsed, "quiet").is_empty());
10877 }
10878
10879 #[test]
10880 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
10881 let spec: Spec = r#"
10882name "ex"
10883bin "ex"
10884cmd "run" restart_token=":::" {
10885 arg "<task>" default="build"
10886}
10887 "#
10888 .parse()
10889 .unwrap();
10890
10891 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
10892
10893 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
10897 assert_eq!(roles(&parsed, 3), ["restart"]);
10900 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
10901 assert!(parsed.arg_origins.is_empty());
10902 }
10903
10904 #[test]
10905 fn a_value_terminator_says_which_run_it_ended() {
10906 let spec: Spec = r#"
10907name "ex"
10908bin "ex"
10909flag "--exec <cmd>..." value_terminator=";"
10910arg "<src>"
10911 "#
10912 .parse()
10913 .unwrap();
10914
10915 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
10916
10917 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
10918 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
10921 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
10922 }
10923
10924 #[test]
10925 fn an_args_value_terminator_says_which_run_it_ended() {
10926 let spec: Spec = r#"
10927name "ex"
10928bin "ex"
10929arg "<files>..." value_terminator=";"
10930arg "[dest]"
10931 "#
10932 .parse()
10933 .unwrap();
10934
10935 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
10936
10937 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
10938 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
10939 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
10940 }
10941
10942 #[test]
10943 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
10944 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
10945 .parse()
10946 .unwrap();
10947
10948 let parsed = Parser::new(&spec)
10949 .explain(&input(&["ex", "--env=prod"]))
10950 .unwrap();
10951
10952 assert!(Parser::new(&spec)
10955 .parse(&input(&["ex", "--env=prod"]))
10956 .is_err());
10957 assert_eq!(
10958 roles(&parsed, 1),
10959 ["flag env as --env", "value of env = [\"prod\"], attached"]
10960 );
10961 assert!(
10962 parsed.errors.iter().any(|e| e.to_string().contains("src")),
10963 "{:?}",
10964 parsed.errors
10965 );
10966 }
10967
10968 #[test]
10969 fn an_external_subcommand_forwards_whole_tokens() {
10970 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
10971 .parse()
10972 .unwrap();
10973
10974 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
10975
10976 assert_eq!(roles(&parsed, 1), ["external"]);
10977 assert_eq!(roles(&parsed, 2), ["external"]);
10978 }
10979
10980 #[test]
10981 fn a_view_keeps_the_callers_argv_positions() {
10982 let spec: Spec = r#"
10983bin "ex"
10984view "runner" root="run"
10985cmd "run" {
10986 flag "--token <token>"
10987}
10988 "#
10989 .parse()
10990 .unwrap();
10991
10992 let parsed = explain(&spec, &["runner", "--token", "secret"]);
10993
10994 assert_eq!(roles(&parsed, 0), ["program"]);
10997 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
10998 }
10999}