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
1238struct DefaultBundle {
1240 argv: usize,
1241 flags: BTreeMap<String, Arc<SpecFlag>>,
1242 parent_keys: HashSet<String>,
1243}
1244
1245impl DefaultBundle {
1246 fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1248 self.flags.get(key).map(|flag| {
1249 (
1250 Arc::clone(flag),
1251 usize::from(!self.parent_keys.contains(key)),
1252 )
1253 })
1254 }
1255}
1256
1257impl Token {
1258 fn new(word: String, argv: usize) -> Self {
1259 Self {
1260 word,
1261 argv,
1262 binding: None,
1263 }
1264 }
1265}
1266
1267struct Trace {
1273 tokens: Vec<TokenBinding>,
1274}
1275
1276impl Trace {
1277 fn new(input: &[String]) -> Self {
1278 Self {
1279 tokens: input
1280 .iter()
1281 .enumerate()
1282 .map(|(index, word)| TokenBinding {
1283 index,
1284 word: word.clone(),
1285 synthesized: false,
1286 roles: vec![],
1287 })
1288 .collect(),
1289 }
1290 }
1291
1292 fn record(&mut self, argv: usize, role: TokenRole) {
1293 if let Some(token) = self.tokens.get_mut(argv) {
1294 token.roles.push(role);
1295 }
1296 }
1297
1298 fn note_synthesized(&mut self, argv: usize) {
1300 if let Some(token) = self.tokens.get_mut(argv) {
1301 token.synthesized = true;
1302 }
1303 }
1304
1305 fn close(&mut self, unread: &VecDeque<Token>) {
1307 for token in unread {
1308 self.record(token.argv, TokenRole::Unread);
1309 }
1310 }
1311}
1312
1313fn parse_partial_with_env(
1314 spec: &Spec,
1315 input: &[String],
1316 custom_env: Option<&HashMap<String, String>>,
1317 mount_outputs: Option<&HashMap<String, String>>,
1318 mount_timing: MountTiming,
1319 validate_clauses: bool,
1320) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1321 let mut trace = Trace::new(input);
1322 parse_partial_traced(
1323 spec,
1324 input,
1325 custom_env,
1326 mount_outputs,
1327 mount_timing,
1328 validate_clauses,
1329 &mut trace,
1330 )
1331}
1332
1333fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1336 let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1337 let parent = gather_flags(root);
1338 let child = gather_flags(default);
1339 let path = std::slice::from_ref(root);
1340 let mut at = None;
1341 let mut i = 0;
1342 while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1343 let scope = if at.is_some() { default } else { root };
1344 if token == "--"
1345 || token == "-"
1346 || scope
1347 .clause
1348 .as_ref()
1349 .is_some_and(|c| c.separator.as_deref() == Some(token))
1350 {
1351 return at;
1352 }
1353 if !is_flag_like(token) {
1354 if at.is_none()
1355 && (root.find_subcommand(token).is_some()
1356 || (token == "help"
1357 && spec.disable_help != Some(true)
1358 && !root.disable_help_subcommand))
1359 {
1360 return None;
1361 }
1362 return at;
1363 }
1364 let mut value_flag = None;
1365 let mut attached = None;
1366 if token.starts_with("--") {
1367 let (name, value) = token
1368 .split_once('=')
1369 .map_or((token, None), |(n, v)| (n, Some(v)));
1370 if !parent.contains_key(name)
1371 && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1372 {
1373 i += 1;
1374 continue;
1375 }
1376 let flag = parent.get(name).or_else(|| child.get(name));
1377 let Some(flag) = flag else {
1378 return at;
1379 };
1380 if !parent.contains_key(name) {
1381 at.get_or_insert(i);
1382 }
1383 if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1384 value_flag = Some(flag);
1385 attached = value;
1386 }
1387 } else {
1388 for (offset, letter) in token.char_indices().skip(1) {
1389 let key = format!("-{letter}");
1390 if !parent.contains_key(&key) && supplied_short(spec, path, letter).is_some() {
1391 continue;
1392 }
1393 let flag = parent.get(&key).or_else(|| child.get(&key));
1394 let Some(flag) = flag else {
1395 return at;
1396 };
1397 if !parent.contains_key(&key) {
1398 at.get_or_insert(i);
1399 }
1400 if flag.arg.is_some() {
1401 value_flag = Some(flag);
1402 let rest = &token[offset + letter.len_utf8()..];
1403 attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1404 break;
1405 }
1406 }
1407 }
1408 i += 1;
1409 if let Some(flag) = value_flag {
1410 let scope = if at.is_some() { default } else { root };
1411 let is_separator = |next: &str| {
1412 scope
1413 .clause
1414 .as_ref()
1415 .is_some_and(|c| c.separator.as_deref() == Some(next))
1416 };
1417 let first = if attached.is_some() {
1418 attached
1419 } else {
1420 input
1421 .get(i)
1422 .map(|t| t.word.as_str())
1423 .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1424 .inspect(|_| i += 1)
1425 };
1426 if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1427 return at;
1428 }
1429 let arg = flag.arg.as_ref().expect("value flag");
1430 if arg.var {
1431 let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1432 let mut count = first.map_or(0, count_values);
1433 while arg.var_max.is_none_or(|max| count < max) {
1434 let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1435 break;
1436 };
1437 if is_separator(next) {
1438 break;
1439 }
1440 if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1441 if next != "--" {
1442 i += 1;
1443 }
1444 break;
1445 }
1446 if is_flag_like(next)
1447 && !(arg.allow_negative_numbers && is_negative_number(next))
1448 {
1449 break;
1450 }
1451 if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1452 break;
1453 }
1454 count += count_values(next);
1455 i += 1;
1456 }
1457 }
1458 }
1459 }
1460 at
1461}
1462
1463fn parse_partial_traced(
1470 spec: &Spec,
1471 input: &[String],
1472 custom_env: Option<&HashMap<String, String>>,
1473 mount_outputs: Option<&HashMap<String, String>>,
1474 mount_timing: MountTiming,
1475 validate_clauses: bool,
1476 trace: &mut Trace,
1477) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1478 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1479 let viewed = spec.for_view(view)?;
1480 return parse_partial_traced(
1481 &viewed,
1482 input,
1483 custom_env,
1484 mount_outputs,
1485 mount_timing,
1486 validate_clauses,
1487 trace,
1488 );
1489 }
1490 trace!("parse_partial: {input:?}");
1491 let mut input = input
1492 .iter()
1493 .enumerate()
1494 .map(|(argv, word)| Token::new(word.clone(), argv))
1495 .collect::<VecDeque<_>>();
1496 let argv0 = input.pop_front();
1497 if let Some(argv0) = argv0.as_ref() {
1498 trace.record(argv0.argv, TokenRole::Program);
1499 }
1500 if spec.multicall {
1501 if let Some(raw) = argv0 {
1502 if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1503 trace.note_synthesized(raw.argv);
1507 input.push_front(Token::new(applet.to_string(), raw.argv));
1508 }
1509 }
1510 }
1511 let mut command_has_argv = !input.is_empty();
1515 let mut root_positional_arg_found = false;
1518
1519 let mut out = ParseOutput {
1520 cmd: spec.cmd.clone(),
1521 cmds: vec![spec.cmd.clone()],
1522 args: IndexMap::new(),
1523 clauses: IndexMap::new(),
1524 clause_flags: IndexMap::new(),
1525 flags: IndexMap::new(),
1526 tokens: vec![],
1527 flag_origins: IndexMap::new(),
1528 arg_origins: IndexMap::new(),
1529 overridden_flags: BTreeMap::new(),
1530 available_flags: gather_flags(&spec.cmd),
1531 flag_awaiting_value: vec![],
1532 errors: vec![],
1533 warnings: vec![],
1534 next_arg: None,
1535 double_dash_seen: false,
1536 external: None,
1537 };
1538 let mut overridden_flags = HashSet::new();
1541 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1546
1547 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1560 let mut command_arg_found = false;
1563 let mut variadic_flag_active = false;
1564 let mut idx = 0;
1565 let mut used_default_subcommand = false;
1568 let mut default_bundle: Option<DefaultBundle> = None;
1569 let mut mounts_resolved = false;
1573 let default_outranks_mounts =
1583 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1584 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1585 {
1586 mounts_resolved = true;
1587 let mut mounted = out.cmd.clone();
1588 mounted.mount(&[], mount_outputs)?;
1589 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1590 if let Some(last) = out.cmds.last_mut() {
1591 *last = mounted.clone();
1592 }
1593 out.cmd = mounted;
1594 }
1595
1596 let default_flag_at = spec
1597 .default_subcommand_flags
1598 .then(|| default_flag_route(spec, &out.cmd, &input))
1599 .flatten();
1600
1601 while idx < input.len() {
1602 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1612 default_accepts_word(&out.cmd, name, &input[idx].word)
1613 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1614 });
1615 if !mounts_resolved
1616 && !out.cmd.mounts.is_empty()
1617 && !default_catches_it
1618 && is_command_word(&input[idx].word)
1619 && !is_negative_number(&input[idx].word)
1620 && out.cmd.find_subcommand(&input[idx].word).is_none()
1621 {
1622 mounts_resolved = true;
1623 let mut mounted = out.cmd.clone();
1624 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1625 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1626 if let Some(last) = out.cmds.last_mut() {
1627 *last = mounted.clone();
1628 }
1629 out.cmd = mounted;
1630 }
1631 if variadic_flag_active
1632 && out.cmd.find_subcommand(&input[idx].word).is_some()
1633 && !out.cmd.subcommand_precedence_over_arg
1634 {
1635 break;
1636 }
1637 let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1638 let selected_command = if implicit_default {
1639 out.cmd
1640 .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1641 } else {
1642 out.cmd.find_subcommand(&input[idx].word)
1643 };
1644 if let Some(subcommand) = selected_command {
1645 let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1646 && implicit_default
1647 && input[idx].word.starts_with('-')
1648 && !input[idx].word.starts_with("--")
1649 && {
1650 let child_flags = gather_flags(subcommand);
1651 let mut found = false;
1652 for short in input[idx].word[1..].chars() {
1653 let key = format!("-{short}");
1654 if out.available_flags.contains_key(&key)
1655 || supplied_short(spec, &out.cmds, short).is_some()
1656 {
1657 found = true;
1658 break;
1659 }
1660 match child_flags.get(&key) {
1661 Some(flag) if flag.arg.is_none() => {}
1662 _ => break,
1663 }
1664 }
1665 found
1666 };
1667 if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1668 {
1669 bail!(
1670 "subcommand '{}' cannot be used with arguments on its parent command",
1671 subcommand.name
1672 );
1673 }
1674 let boundary_parent = (implicit_default
1675 && input[idx].word.starts_with('-')
1676 && !input[idx].word.starts_with("--"))
1677 .then(|| out.available_flags.clone());
1678 let mut subcommand = subcommand.clone();
1679 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1681 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1684 merge_subcommand_flags(
1685 &mut out.available_flags,
1686 gather_flags(&subcommand),
1687 crossing_mount,
1688 );
1689 if let Some(parent) = boundary_parent {
1690 let parent_keys = parent.keys().cloned().collect();
1691 let mut flags = out.available_flags.clone();
1692 flags.extend(parent);
1693 default_bundle = Some(DefaultBundle {
1694 argv: input[idx].argv,
1695 flags,
1696 parent_keys,
1697 });
1698 }
1699 let selected = if implicit_default {
1701 used_default_subcommand = true;
1702 None
1703 } else {
1704 input.remove(idx)
1705 };
1706 if let Some(selected) = selected {
1707 trace.record(
1708 selected.argv,
1709 TokenRole::Command {
1710 name: subcommand.name.clone(),
1711 },
1712 );
1713 }
1714 command_has_argv = idx < input.len();
1715 out.cmds.push(subcommand.clone());
1716 out.cmd = subcommand.clone();
1717 mounts_resolved = true;
1719 prefix_flags.clear();
1720 command_arg_found = false;
1721 variadic_flag_active = false;
1722 } else if !is_command_word(&input[idx].word)
1725 || declared_numeric_short(&out.available_flags, &input[idx].word)
1726 {
1727 let word = input[idx].word.clone();
1729 let flag_key = get_flag_key(&word);
1730
1731 let boundary = default_bundle
1737 .as_ref()
1738 .filter(|bundle| bundle.argv == input[idx].argv);
1739 let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1740 let is_bundle =
1741 word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1742 if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1743 command_arg_found = true;
1744 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1745 input[idx].binding = boundary
1753 .and_then(|bundle| bundle.binding(flag_key))
1754 .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1755 let mut forwarded = f.global.then(|| vec![word.clone()]);
1756 idx += 1;
1757
1758 if f.arg.is_some()
1761 && !word.contains('=')
1762 && idx < input.len()
1763 && accepts_detached_flag_value(&f, &input[idx].word)
1764 {
1765 if let Some(words) = forwarded.as_mut() {
1766 words.push(input[idx].word.clone());
1767 }
1768 idx += 1;
1769 }
1770 if let Some(words) = forwarded {
1771 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1772 prefix_flags.push((f, words));
1773 }
1774 } else {
1775 break;
1778 }
1779 } else {
1780 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1781 idx += 1;
1782 continue;
1783 }
1784 if !used_default_subcommand && out.cmds.len() == 1 {
1793 if let Some(default_name) = &spec.default_subcommand {
1794 if let Some(subcommand) = out
1795 .cmd
1796 .find_subcommand(default_name)
1797 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1798 {
1799 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1800 bail!(
1801 "subcommand '{}' cannot be used with arguments on its parent command",
1802 subcommand.name
1803 );
1804 }
1805 let mut subcommand = subcommand.clone();
1806 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1808 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1809 merge_subcommand_flags(
1810 &mut out.available_flags,
1811 gather_flags(&subcommand),
1812 crossing_mount,
1813 );
1814 out.cmds.push(subcommand.clone());
1815 out.cmd = subcommand.clone();
1816 command_has_argv = true;
1817 prefix_flags.clear();
1818 command_arg_found = false;
1819 variadic_flag_active = false;
1820 mounts_resolved = true;
1823 used_default_subcommand = true;
1824 continue;
1829 }
1830 }
1831 }
1832 if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1837 idx += 1;
1838 continue;
1839 }
1840 if out.cmd.external_subcommand {
1845 let rest: Vec<Token> = input.drain(idx..).collect();
1846 for token in &rest {
1847 trace.record(token.argv, TokenRole::External);
1848 }
1849 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1850 break;
1851 }
1852 break;
1854 }
1855 }
1856
1857 let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1868 let mut enable_flags = true;
1869 let mut grouped_flag = false;
1870 let mut seen_double_dash = false;
1874 let mut restart_seen = false;
1877 let mut double_dash_violations: HashSet<String> = HashSet::new();
1880 let mut root_clause_separator_seen = false;
1884 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1889
1890 while !input.is_empty() {
1891 let token = input.pop_front().unwrap();
1892 let binding = token.binding;
1894 let argv = token.argv;
1895 let mut w = token.word;
1896 let attached_continuation = grouped_flag;
1901 if spec.default_subcommand_flags
1905 && attached_continuation
1906 && !out.flag_awaiting_value.is_empty()
1907 {
1908 grouped_flag = false;
1909 w.remove(0);
1910 if w.starts_with('=') {
1911 w.remove(0);
1912 }
1913 if bind_pending_flag_value(
1914 spec,
1915 &out.cmd,
1916 &mut out.errors,
1917 &mut out.flags,
1918 &mut out.flag_awaiting_value,
1919 &mut w,
1920 &mut input,
1921 custom_env,
1922 trace,
1923 argv,
1924 true,
1925 )? {
1926 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1927 return Ok((out, overridden_flags));
1928 }
1929 continue;
1930 }
1931
1932 if !seen_double_dash {
1935 if let Some(clause) = out.cmd.clause.as_ref() {
1936 if clause.separator.as_deref() == Some(w.as_str()) {
1937 while try_bind_default_missing(
1938 &mut out.flags,
1939 &mut out.flag_awaiting_value,
1940 custom_env,
1941 &mut out.flag_origins,
1942 )? {}
1943 if let Some(flag) = out.flag_awaiting_value.first() {
1944 let spelling = flag
1945 .long
1946 .first()
1947 .map(|long| format!("--{long}"))
1948 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1949 .unwrap_or_else(|| flag.name.clone());
1950 return Err(UsageErr::InvalidFlag {
1951 token: spelling.clone(),
1952 reason: "requires an argument".to_string(),
1953 span: (0, spelling.len()).into(),
1954 input: spelling,
1955 }
1956 .into());
1957 }
1958 let name = clause.name.clone();
1959 if out.cmds.len() == 1 {
1960 root_clause_separator_seen = true;
1961 }
1962 finalize_current_clause(&mut out);
1963 out.arg_origins.clear();
1964 trace.record(argv, TokenRole::ClauseSeparator { name });
1965 next_arg_idx = 0;
1966 out.flag_awaiting_value.clear();
1967 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1968 enable_flags = true;
1969 seen_double_dash = false;
1970 continue;
1971 }
1972 }
1973 }
1974
1975 if let Some(ref restart_token) = out.cmd.restart_token {
1978 if w == *restart_token {
1979 out.args.clear();
1984 out.arg_origins.clear();
1989 trace.record(argv, TokenRole::Restart);
1990 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1991 restart_seen = true;
1992 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
1996 }
1997 }
1998
1999 if enable_flags
2008 && !attached_continuation
2009 && w.starts_with('-')
2010 && out
2011 .flag_awaiting_value
2012 .last()
2013 .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2014 {
2015 let should_return = bind_pending_flag_value(
2018 spec,
2019 &out.cmd,
2020 &mut out.errors,
2021 &mut out.flags,
2022 &mut out.flag_awaiting_value,
2023 &mut w,
2024 &mut input,
2025 custom_env,
2026 trace,
2027 argv,
2028 false,
2030 )?;
2031 if should_return {
2032 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2033 return Ok((out, overridden_flags));
2034 }
2035 continue;
2036 }
2037
2038 if enable_flags
2044 && !attached_continuation
2045 && !out.flag_awaiting_value.is_empty()
2046 && out.flag_awaiting_value.last().is_some_and(|flag| {
2047 (flag.default_missing.is_some() || flag.value_optional)
2048 && !accepts_detached_flag_value(flag, &w)
2049 })
2050 {
2051 try_bind_default_missing(
2052 &mut out.flags,
2053 &mut out.flag_awaiting_value,
2054 custom_env,
2055 &mut out.flag_origins,
2056 )?;
2057 }
2058
2059 if w == "--" && !seen_double_dash {
2065 enable_flags = false;
2066
2067 let should_preserve = active_args(&out.cmd)
2070 .get(next_arg_idx)
2071 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2072 .unwrap_or(false);
2073
2074 if should_preserve {
2075 } else {
2078 seen_double_dash = true;
2079 trace.record(argv, TokenRole::Separator);
2080
2081 let target = active_args(&out.cmd).iter().position(|arg| {
2087 arg.double_dash == SpecDoubleDashChoices::Required
2088 && !out.args.contains_key(arg)
2089 });
2090 if let Some(target) = target {
2091 if target > next_arg_idx {
2098 next_arg_idx = target;
2099 }
2100 }
2101 continue;
2102 }
2103 }
2104
2105 if enable_flags && w.starts_with("--") {
2107 grouped_flag = false;
2108 let split = w.split_once('=');
2112 let word = split.map(|(word, _)| word).unwrap_or(&w);
2113 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2114 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2115 let command_level = binding
2116 .as_ref()
2117 .map(|(_, level)| *level)
2118 .unwrap_or(out.cmds.len() - 1);
2119 parsed_flag_spellings
2120 .entry(Arc::as_ptr(f) as usize)
2121 .or_default()
2122 .insert(word.to_string());
2123 trace.record(
2126 argv,
2127 TokenRole::Flag {
2128 flag: Arc::clone(f),
2129 spelling: word.to_string(),
2130 negated: f.negate.as_deref() == Some(word),
2131 },
2132 );
2133 if f.action != crate::SpecFlagAction::Set {
2134 out.errors.push(render_action_err(spec, &out.cmd, f, word));
2135 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2136 return Ok((out, overridden_flags));
2137 }
2138 apply_flag_overrides(
2139 f,
2140 &out.available_flags,
2141 &mut out.flags,
2142 &mut out.flag_awaiting_value,
2143 &mut overridden_flags,
2144 &mut out.overridden_flags,
2145 );
2146 if let Some(pending) = out.flag_awaiting_value.first() {
2147 out.errors.push(render_missing_flag_value(pending, &w));
2148 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2149 return Ok((out, overridden_flags));
2150 }
2151 if f.arg.is_some() {
2157 record_scalar_flag_occurrence(
2158 &out.cmds,
2159 f,
2160 command_level,
2161 None,
2162 &mut scalar_occurrences,
2163 &mut out.errors,
2164 );
2165 let f = Arc::clone(f);
2166 out.flag_awaiting_value.push(Arc::clone(&f));
2167 if let Some((_, val)) = split {
2172 let mut val = val.to_string();
2176 let should_return = bind_pending_flag_value(
2177 spec,
2178 &out.cmd,
2179 &mut out.errors,
2180 &mut out.flags,
2181 &mut out.flag_awaiting_value,
2182 &mut val,
2183 &mut input,
2184 custom_env,
2185 trace,
2186 argv,
2187 true,
2190 )?;
2191 if should_return {
2192 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2193 return Ok((out, overridden_flags));
2194 }
2195 }
2196 } else if f.count {
2197 let arr = out
2198 .flags
2199 .entry(Arc::clone(f))
2200 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2201 .try_as_multi_bool_mut()
2202 .unwrap();
2203 arr.push(true);
2204 } else {
2205 let negate = f.negate.clone().unwrap_or_default();
2206 let negated_form = word == negate;
2207 let value = if f.bool_value {
2208 match split.map(|(_, value)| value) {
2209 Some("true") => !negated_form,
2210 Some("false") => negated_form,
2211 Some(value) => {
2212 out.errors.push(UsageErr::InvalidValue {
2213 name: f.name.clone(),
2214 value: value.to_string(),
2215 reason: "expected `true` or `false`".to_string(),
2216 });
2217 continue;
2218 }
2219 None => !negated_form,
2220 }
2221 } else {
2222 !negated_form
2223 };
2224 record_scalar_flag_occurrence(
2229 &out.cmds,
2230 f,
2231 command_level,
2232 Some(!negated_form),
2233 &mut scalar_occurrences,
2234 &mut out.errors,
2235 );
2236 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2237 }
2238 continue;
2239 }
2240 if is_help_arg(spec, &out.cmd, &w) {
2241 out.errors
2242 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2243 trace.record(
2244 argv,
2245 TokenRole::Builtin {
2246 spelling: w.clone(),
2247 },
2248 );
2249 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2250 return Ok((out, overridden_flags));
2251 }
2252 if is_version_arg(spec, &out.cmds, &w) {
2253 out.errors.push(render_version_err(spec, w.len() > 2));
2254 trace.record(
2255 argv,
2256 TokenRole::Builtin {
2257 spelling: w.clone(),
2258 },
2259 );
2260 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2261 return Ok((out, overridden_flags));
2262 }
2263 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2264 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2265 trace.close(&input);
2266 return Err(refused.into());
2267 }
2268 }
2269
2270 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2277 let positional_negative_number = !declared_numeric_short
2278 && is_negative_number(&w)
2279 && active_args(&out.cmd)
2280 .get(next_arg_idx)
2281 .is_some_and(|arg| arg.allow_negative_numbers);
2282 if enable_flags
2283 && !grouped_flag
2284 && binding.is_none()
2287 && w.starts_with('-')
2288 && w.len() > 1
2289 && is_flag_like(&w)
2290 && !positional_negative_number
2291 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2292 {
2293 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2296 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2297 trace.close(&input);
2298 return Err(refused.into());
2299 }
2300 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2301 let short = w.chars().nth(1).unwrap();
2302 if let Some(f) = binding
2303 .as_ref()
2304 .map(|(flag, _)| flag)
2305 .or_else(|| out.available_flags.get(&format!("-{short}")))
2306 {
2307 let command_level = binding
2308 .as_ref()
2309 .map(|(_, level)| *level)
2310 .unwrap_or(out.cmds.len() - 1);
2311 if f.action != crate::SpecFlagAction::Set {
2312 out.errors
2313 .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2314 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2315 return Ok((out, overridden_flags));
2316 }
2317 parsed_flag_spellings
2318 .entry(Arc::as_ptr(f) as usize)
2319 .or_default()
2320 .insert(format!("-{short}"));
2321 trace.record(
2322 argv,
2323 TokenRole::Flag {
2324 flag: Arc::clone(f),
2325 spelling: format!("-{short}"),
2326 negated: false,
2328 },
2329 );
2330 apply_flag_overrides(
2331 f,
2332 &out.available_flags,
2333 &mut out.flags,
2334 &mut out.flag_awaiting_value,
2335 &mut overridden_flags,
2336 &mut out.overridden_flags,
2337 );
2338 if !attached_continuation {
2339 if let Some(pending) = out.flag_awaiting_value.first() {
2340 out.errors.push(render_missing_flag_value(pending, &w));
2341 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2342 return Ok((out, overridden_flags));
2343 }
2344 }
2345 let rest = &w[1 + short.len_utf8()..];
2346 if !rest.is_empty() {
2347 let mut tail = Token::new(format!("-{rest}"), argv);
2350 if f.arg.is_none() {
2351 tail.binding = default_bundle
2352 .as_ref()
2353 .filter(|bundle| bundle.argv == argv)
2354 .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2355 }
2356 input.push_front(tail);
2357 }
2358 grouped_flag = !rest.is_empty();
2362 if f.arg.is_some() {
2363 record_scalar_flag_occurrence(
2364 &out.cmds,
2365 f,
2366 command_level,
2367 None,
2368 &mut scalar_occurrences,
2369 &mut out.errors,
2370 );
2371 out.flag_awaiting_value.push(Arc::clone(f));
2372 } else if f.count {
2373 let arr = out
2374 .flags
2375 .entry(Arc::clone(f))
2376 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2377 .try_as_multi_bool_mut()
2378 .unwrap();
2379 arr.push(true);
2380 } else {
2381 let negate = f.negate.clone().unwrap_or_default();
2382 let value = w != negate;
2383 record_scalar_flag_occurrence(
2384 &out.cmds,
2385 f,
2386 command_level,
2387 Some(value),
2388 &mut scalar_occurrences,
2389 &mut out.errors,
2390 );
2391 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2392 }
2393 continue;
2394 }
2395 if let Some(err) = supplied_short(spec, &out.cmds, short) {
2399 out.errors.push(err);
2400 trace.record(
2401 argv,
2402 TokenRole::Builtin {
2403 spelling: format!("-{short}"),
2404 },
2405 );
2406 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2407 return Ok((out, overridden_flags));
2408 }
2409 if is_help_arg(spec, &out.cmd, &w) {
2410 out.errors
2411 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2412 trace.record(
2413 argv,
2414 TokenRole::Builtin {
2415 spelling: w.clone(),
2416 },
2417 );
2418 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2419 return Ok((out, overridden_flags));
2420 }
2421 if is_version_arg(spec, &out.cmds, &w) {
2422 out.errors.push(render_version_err(spec, w.len() > 2));
2423 trace.record(
2424 argv,
2425 TokenRole::Builtin {
2426 spelling: w.clone(),
2427 },
2428 );
2429 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2430 return Ok((out, overridden_flags));
2431 }
2432 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2433 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2434 trace.close(&input);
2435 return Err(refused.into());
2436 }
2437 if grouped_flag {
2438 grouped_flag = false;
2439 w.remove(0);
2440 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2444 w.remove(0);
2445 }
2446 }
2447 }
2448
2449 if enable_flags
2460 && !attached_continuation
2461 && !out.flag_awaiting_value.is_empty()
2462 && out
2463 .flag_awaiting_value
2464 .last()
2465 .is_some_and(|flag| flag.require_equals)
2466 {
2467 let flag = out.flag_awaiting_value.last().unwrap();
2468 let token = flag
2469 .long
2470 .first()
2471 .map(|l| format!("--{l}"))
2472 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2473 .unwrap_or_else(|| flag.name.clone());
2474 out.errors.push(UsageErr::InvalidFlag {
2475 token: token.clone(),
2476 reason: "requires an argument".to_string(),
2477 span: (0, 0).into(),
2478 input: format!("{token} {w}"),
2479 });
2480 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2481 return Ok((out, overridden_flags));
2482 }
2483 if enable_flags
2484 && !out.flag_awaiting_value.is_empty()
2485 && (attached_continuation
2486 || out
2487 .flag_awaiting_value
2488 .last()
2489 .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2490 {
2491 let should_return = bind_pending_flag_value(
2494 spec,
2495 &out.cmd,
2496 &mut out.errors,
2497 &mut out.flags,
2498 &mut out.flag_awaiting_value,
2499 &mut w,
2500 &mut input,
2501 custom_env,
2502 trace,
2503 argv,
2504 attached_continuation,
2505 )?;
2506 if should_return {
2507 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2508 return Ok((out, overridden_flags));
2509 }
2510 continue;
2511 }
2512
2513 if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2514 .then(|| match_sigil_arg_chain(&out.cmds, &w))
2515 .flatten()
2516 {
2517 if value.is_empty() {
2518 out.errors.push(UsageErr::InvalidValue {
2519 name: arg.name.clone(),
2520 value: w.clone(),
2521 reason: format!("expected a value after sigil {sigil:?}"),
2522 });
2523 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2524 return Ok((out, overridden_flags));
2525 }
2526 let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2527 let suppress_trailing_delimiter =
2528 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2529 let delimiter = if suppress_trailing_delimiter && trailing_value {
2530 None
2531 } else {
2532 arg.delimiter
2533 };
2534 let parts = match delimiter {
2535 Some(delimiter) => value
2536 .split(delimiter)
2537 .map(str::to_string)
2538 .collect::<Vec<_>>(),
2539 None => vec![value.to_string()],
2540 };
2541 let mut refused = false;
2542 for part in &parts {
2543 if validate_choices(
2544 spec,
2545 &out.cmd,
2546 &mut out.errors,
2547 ChoiceTarget::arg(arg),
2548 part,
2549 arg.choices.as_ref(),
2550 custom_env,
2551 )? {
2552 refused = true;
2553 break;
2554 }
2555 }
2556 if refused {
2557 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2558 return Ok((out, overridden_flags));
2559 }
2560 trace.record(
2561 argv,
2562 TokenRole::Sigil {
2563 arg: Arc::new(arg.clone()),
2564 sigil: sigil.to_string(),
2565 values: parts.clone(),
2566 },
2567 );
2568 let key = Arc::new(arg.clone());
2569 if arg.var {
2570 let arr = out
2571 .args
2572 .entry(key)
2573 .or_insert_with(|| ParseValue::MultiString(vec![]))
2574 .try_as_multi_string_mut()
2575 .unwrap();
2576 arr.extend(parts);
2577 } else {
2578 out.args.insert(key, ParseValue::String(value.to_string()));
2579 }
2580 if out.cmds.len() == 1 {
2581 root_positional_arg_found = true;
2582 }
2583 continue;
2584 }
2585
2586 if out.cmd.allow_missing_positional {
2587 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2588 while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2589 if current.required || out.args.contains_key(current) {
2590 break;
2591 }
2592 let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2593 .iter()
2594 .filter(|arg| arg.required && arg.sigil.is_none())
2595 .count();
2596 if required_after == 0 {
2597 break;
2598 }
2599 let remaining_values = 1 + input
2600 .iter()
2601 .filter(|token| {
2602 (!enable_flags || !is_flag_like(&token.word))
2603 && (!enable_flags
2604 || restart_seen
2605 || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2606 })
2607 .count();
2608 if remaining_values > required_after {
2609 break;
2610 }
2611 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2612 }
2613 }
2614
2615 if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2616 if arg.var
2617 && out.args.contains_key(arg)
2618 && arg.value_terminator.as_deref() == Some(w.as_str())
2619 {
2620 trace.record(
2621 argv,
2622 TokenRole::ValueTerminator {
2623 ends: arg.name.clone(),
2624 },
2625 );
2626 next_arg_idx += 1;
2627 continue;
2628 }
2629 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2633 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2634 trace.record(
2635 argv,
2636 TokenRole::Refused {
2637 reason: format!("{} only accepts words after `--`", arg.name),
2638 },
2639 );
2640 continue;
2644 }
2645 let trailing_value =
2650 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2651 let suppress_trailing_delimiter =
2652 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2653 let delimiter = if suppress_trailing_delimiter && trailing_value {
2654 None
2655 } else {
2656 arg.delimiter
2657 };
2658 let parts: Vec<String> = match delimiter {
2659 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2660 None => vec![w.clone()],
2661 };
2662 let mut refused = false;
2663 for part in &parts {
2664 if validate_choices(
2665 spec,
2666 &out.cmd,
2667 &mut out.errors,
2668 ChoiceTarget::arg(arg),
2669 part,
2670 arg.choices.as_ref(),
2671 custom_env,
2672 )? {
2673 refused = true;
2674 break;
2675 }
2676 }
2677 if refused {
2678 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2679 return Ok((out, overridden_flags));
2680 }
2681 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2686 enable_flags = false;
2687 }
2688 let unknown_flag = enable_flags
2693 && !positional_negative_number
2694 && is_flag_like(&w)
2695 && binding.is_none();
2696 trace.record(
2697 argv,
2698 if unknown_flag {
2699 TokenRole::UnknownFlag {
2700 bound_as: Some(Arc::new(arg.clone())),
2701 }
2702 } else {
2703 TokenRole::Arg {
2704 arg: Arc::new(arg.clone()),
2705 values: parts.clone(),
2706 }
2707 },
2708 );
2709 if out.cmds.len() == 1 {
2710 root_positional_arg_found = true;
2711 }
2712 if arg.var {
2713 let arr = out
2714 .args
2715 .entry(Arc::new(arg.clone()))
2716 .or_insert_with(|| ParseValue::MultiString(vec![]))
2717 .try_as_multi_string_mut()
2718 .unwrap();
2719 arr.extend(parts.iter().cloned());
2723 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2724 next_arg_idx += 1;
2725 }
2726 } else {
2727 out.args
2728 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2729 next_arg_idx += 1;
2730 }
2731 if out
2732 .cmd
2733 .clause
2734 .as_ref()
2735 .is_some_and(|clause| clause.separator.is_none())
2736 && next_arg_idx >= active_args(&out.cmd).len()
2737 {
2738 while try_bind_default_missing(
2739 &mut out.flags,
2740 &mut out.flag_awaiting_value,
2741 custom_env,
2742 &mut out.flag_origins,
2743 )? {}
2744 if let Some(flag) = out.flag_awaiting_value.first() {
2745 let spelling = flag
2746 .long
2747 .first()
2748 .map(|long| format!("--{long}"))
2749 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2750 .unwrap_or_else(|| flag.name.clone());
2751 return Err(UsageErr::InvalidFlag {
2752 token: spelling.clone(),
2753 reason: "requires an argument".to_string(),
2754 span: (0, spelling.len()).into(),
2755 input: spelling,
2756 }
2757 .into());
2758 }
2759 let name = out.cmd.clause.as_ref().unwrap().name.clone();
2760 finalize_current_clause(&mut out);
2761 out.arg_origins.clear();
2762 trace.record(argv, TokenRole::ClauseSeparator { name });
2763 next_arg_idx = 0;
2764 out.flag_awaiting_value.clear();
2765 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2766 enable_flags = true;
2767 seen_double_dash = false;
2768 }
2769 continue;
2770 }
2771 if is_help_arg(spec, &out.cmd, &w) {
2772 out.errors
2773 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2774 trace.record(
2775 argv,
2776 TokenRole::Builtin {
2777 spelling: w.clone(),
2778 },
2779 );
2780 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2781 return Ok((out, overridden_flags));
2782 }
2783 if is_version_arg(spec, &out.cmds, &w) {
2784 out.errors.push(render_version_err(spec, w.len() > 2));
2785 trace.record(
2786 argv,
2787 TokenRole::Builtin {
2788 spelling: w.clone(),
2789 },
2790 );
2791 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2792 return Ok((out, overridden_flags));
2793 }
2794 trace.record(
2795 argv,
2796 TokenRole::Refused {
2797 reason: "no declaration takes this word".to_string(),
2798 },
2799 );
2800 trace.close(&input);
2801 bail!("unexpected word: {w}");
2802 }
2803
2804 if spec.default_subcommand_on_empty
2808 && spec.default_subcommand.is_some()
2809 && out.cmds.len() == 1
2810 && !root_positional_arg_found
2811 && !seen_double_dash
2812 && !root_clause_separator_seen
2813 && out.errors.is_empty()
2814 {
2815 while try_bind_default_missing(
2816 &mut out.flags,
2817 &mut out.flag_awaiting_value,
2818 custom_env,
2819 &mut out.flag_origins,
2820 )? {}
2821 if !out.flag_awaiting_value.is_empty() {
2822 } else if let Some(subcommand) = out
2825 .cmd
2826 .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2827 {
2828 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2829 bail!(
2830 "subcommand '{}' cannot be used with arguments on its parent command",
2831 subcommand.name
2832 );
2833 }
2834 let mut subcommand = subcommand.clone();
2835 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2836 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2837 merge_subcommand_flags(
2838 &mut out.available_flags,
2839 gather_flags(&subcommand),
2840 crossing_mount,
2841 );
2842 out.cmds.push(subcommand.clone());
2843 out.cmd = subcommand;
2844 command_has_argv = false;
2845 prefix_flags.clear();
2846 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2847 }
2848 }
2849
2850 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2851 if validate_clauses {
2852 validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2853 }
2854 let clause_flag_names = out
2855 .cmd
2856 .clause
2857 .iter()
2858 .flat_map(|clause| &clause.flags)
2859 .map(|flag| flag.name.clone())
2860 .collect::<HashSet<_>>();
2861
2862 let flag_was_parsed =
2867 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2868
2869 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2877 let declared: HashSet<String> = out
2878 .cmd
2879 .flags
2880 .iter()
2881 .filter(|declared| declared.name == flag.name)
2882 .flat_map(flag_keys)
2883 .collect();
2884 let speaks_for_this_flag = declared.iter().any(|spelling| {
2894 out.available_flags
2895 .get(spelling)
2896 .is_some_and(|available| Arc::ptr_eq(available, flag))
2897 });
2898 if speaks_for_this_flag {
2899 declared
2900 } else {
2901 HashSet::new()
2902 }
2903 };
2904
2905 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2913 let child = child_spellings(flag);
2914 let child_exclusive = !child.is_empty()
2915 && out
2916 .cmd
2917 .flags
2918 .iter()
2919 .any(|declared| declared.name == flag.name && declared.exclusive);
2920 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2921 Some(spellings) => (
2922 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2923 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2924 ),
2925 None => (child_exclusive, flag.exclusive && child.is_empty()),
2929 }
2930 };
2931
2932 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2933 let (child, ancestor) = exclusivity_in_play(flag);
2934 child || ancestor
2935 };
2936
2937 let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2942 .filter(|flag| !clause_flag_names.contains(&flag.name))
2943 .any(|flag| {
2944 exclusive_occurrence(flag)
2945 && !overridden_flags.contains(&flag.name)
2946 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2947 });
2948 let requirements_apply = |command_index: usize| {
2949 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2950 };
2951
2952 if out.cmd.arg_required_else_help && !command_has_argv {
2953 out.errors.push(render_help_err(spec, &out.cmd, false));
2954 }
2955
2956 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2962 let mut names: Vec<&str> = out
2963 .cmd
2964 .subcommands
2965 .iter()
2966 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2969 .map(|(name, _)| name.as_str())
2970 .collect();
2971 names.sort_unstable();
2972 out.errors.push(UsageErr::MissingSubcommand(
2973 out.cmd.name.clone(),
2974 names.join(", "),
2975 ));
2976 }
2977
2978 if !exclusive_present {
2981 for arg in out
2982 .cmds
2983 .iter()
2984 .enumerate()
2985 .filter(|(index, _)| requirements_apply(*index))
2986 .flat_map(|(_, cmd)| &cmd.args)
2987 {
2988 if out.args.contains_key(arg) {
2989 continue;
2990 }
2991 if double_dash_violations.contains(&arg.name) {
2993 continue;
2994 }
2995 let required_if = arg.required_if.iter().any(|selector| {
2996 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2997 });
2998 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2999 selector_explicit_has_value(
3000 &condition.selector,
3001 &condition.value,
3002 &out,
3003 &overridden_flags,
3004 custom_env,
3005 )
3006 });
3007 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3008 && arg.required_if_eq_all.iter().all(|condition| {
3009 selector_explicit_has_value(
3010 &condition.selector,
3011 &condition.value,
3012 &out,
3013 &overridden_flags,
3014 custom_env,
3015 )
3016 });
3017 let unless_any = arg.required_unless.iter().any(|selector| {
3018 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3019 });
3020 let unless_all = !arg.required_unless_all.is_empty()
3021 && arg.required_unless_all.iter().all(|selector| {
3022 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3023 });
3024 let required_unless = !(unless_any
3025 || unless_all
3026 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3027 if (arg.required
3028 || required_if
3029 || required_if_eq
3030 || required_if_eq_all
3031 || required_unless)
3032 && arg.default.is_empty()
3033 {
3034 let has_env = arg
3036 .env
3037 .as_ref()
3038 .is_some_and(|env_var| env_contains(custom_env, env_var));
3039 if !has_env {
3040 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3041 }
3042 }
3043 }
3044 }
3045
3046 for flag in unique_flags(out.available_flags.values())
3058 .filter(|flag| !clause_flag_names.contains(&flag.name))
3059 {
3060 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3061 if !given || overridden_flags.contains(&flag.name) {
3062 continue;
3063 }
3064 for other in &flag.conflicts {
3065 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3066 out.errors.push(UsageErr::InvalidFlag {
3067 token: format!("--{}", flag.name),
3068 reason: format!("conflicts with {other}"),
3069 span: (0, 0).into(),
3070 input: format!("--{} {other}", flag.name),
3071 });
3072 }
3073 }
3074 let owner = out
3085 .cmds
3086 .iter()
3087 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3088 .unwrap_or(out.cmds.len() - 1);
3089 if !exclusive_present && requirements_apply(owner) {
3090 for other in &flag.requires {
3091 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3092 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3093 if other.starts_with('-') {
3094 out.errors.push(UsageErr::MissingFlag(name));
3095 } else {
3096 out.errors.push(UsageErr::MissingArg(name));
3097 }
3098 }
3099 }
3100 for condition in &flag.requires_if {
3101 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3102 && !selector_is_satisfied(
3103 &condition.requires,
3104 &out,
3105 &overridden_flags,
3106 custom_env,
3107 )
3108 {
3109 let name = selector_flag_name(&condition.requires, &out)
3110 .unwrap_or_else(|| condition.requires.clone());
3111 out.errors.push(UsageErr::MissingFlag(name));
3112 }
3113 }
3114 }
3115 }
3116
3117 for (command_index, arg) in out
3120 .cmds
3121 .iter()
3122 .enumerate()
3123 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3124 {
3125 let given = arg_is_explicit(arg, &out, custom_env);
3126 if !given {
3127 continue;
3128 }
3129 for other in &arg.conflicts {
3130 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3131 out.errors.push(UsageErr::InvalidFlag {
3132 token: arg.name.clone(),
3133 reason: format!("conflicts with {other}"),
3134 span: (0, 0).into(),
3135 input: format!("{} {other}", arg.name),
3136 });
3137 }
3138 }
3139 if !exclusive_present && requirements_apply(command_index) {
3140 for other in &arg.requires {
3141 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3142 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3143 if other.starts_with('-') {
3144 out.errors.push(UsageErr::MissingFlag(name));
3145 } else {
3146 out.errors.push(UsageErr::MissingArg(name));
3147 }
3148 }
3149 }
3150 }
3151 }
3152
3153 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3163 .filter(|flag| !clause_flag_names.contains(&flag.name))
3164 {
3165 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3170 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3171 continue;
3172 }
3173 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3174 .filter(|other| !clause_flag_names.contains(&other.name))
3175 .find(|other| {
3176 !Arc::ptr_eq(other, flag)
3177 && !overridden_flags.contains(&other.name)
3178 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3179 })
3180 .map(|other| format!("--{}", other.name));
3181 let other_arg = active_args(&out.cmd).iter().find(|arg| {
3182 out.args.keys().any(|given| given.name == arg.name)
3183 || out
3184 .clauses
3185 .values()
3186 .flatten()
3187 .any(|instance| instance.keys().any(|given| given.name == arg.name))
3188 || arg
3189 .env
3190 .as_ref()
3191 .is_some_and(|env| env_contains(custom_env, env))
3192 });
3193 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3199 let selected_subcommand =
3200 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3201 let other = other_flag
3202 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3203 .or(selected_subcommand);
3204 if let Some(other) = other {
3205 out.errors.push(UsageErr::InvalidFlag {
3206 token: format!("--{}", flag.name),
3207 reason: format!("must be given on its own, and {other} was given too"),
3208 span: (0, 0).into(),
3209 input: format!("--{} {other}", flag.name),
3210 });
3211 }
3212 }
3213
3214 let mut group_errors: Vec<UsageErr> = Vec::new();
3223 for (command_index, group) in out
3224 .cmds
3225 .iter()
3226 .enumerate()
3227 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3228 {
3229 let mut given: Vec<&str> = Vec::new();
3235 let mut seen: Vec<String> = Vec::new();
3236 for selector in &group.members {
3237 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3238 continue;
3239 }
3240 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3241 if seen.contains(&name) {
3242 continue;
3243 }
3244 seen.push(name);
3245 given.push(selector.as_str());
3246 }
3247 if !group.multiple && given.len() > 1 {
3248 group_errors.push(UsageErr::InvalidFlag {
3249 token: given[1].to_string(),
3250 reason: format!("cannot be used with {} in group {}", given[0], group.name),
3251 span: (0, 0).into(),
3252 input: format!("{} {}", given[0], given[1]),
3253 });
3254 }
3255 let satisfied = group
3260 .members
3261 .iter()
3262 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3263 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3264 group_errors.push(UsageErr::MissingGroup {
3268 group: group.name.clone(),
3269 members: group.members.join(", "),
3270 });
3271 }
3272 }
3273 out.errors.extend(group_errors);
3274
3275 if !exclusive_present {
3276 for flag in unique_flags(out.available_flags.values())
3277 .filter(|flag| !clause_flag_names.contains(&flag.name))
3278 {
3279 let owner = out
3280 .cmds
3281 .iter()
3282 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3283 .unwrap_or(out.cmds.len() - 1);
3284 if !requirements_apply(owner) {
3285 continue;
3286 }
3287 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3288 continue;
3289 }
3290 let has_default =
3291 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3292 let has_env = flag_has_env(flag, custom_env);
3293 let required_if = flag.required_if.iter().any(|selector| {
3294 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3295 });
3296 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3297 selector_explicit_has_value(
3298 &condition.selector,
3299 &condition.value,
3300 &out,
3301 &overridden_flags,
3302 custom_env,
3303 )
3304 });
3305 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3306 && flag.required_if_eq_all.iter().all(|condition| {
3307 selector_explicit_has_value(
3308 &condition.selector,
3309 &condition.value,
3310 &out,
3311 &overridden_flags,
3312 custom_env,
3313 )
3314 });
3315 let unless_any = flag.required_unless.iter().any(|selector| {
3316 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3317 });
3318 let unless_all = !flag.required_unless_all.is_empty()
3319 && flag.required_unless_all.iter().all(|selector| {
3320 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3321 });
3322 let required_unless = !(unless_any
3323 || unless_all
3324 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3325 if (flag.required
3326 || required_if
3327 || required_if_eq
3328 || required_if_eq_all
3329 || required_unless)
3330 && !has_default
3331 && !has_env
3332 {
3333 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3334 }
3335 }
3336 }
3337
3338 for (arg, value) in &out.args {
3340 if arg.var {
3341 if let ParseValue::MultiString(values) = value {
3342 if let Some(min) = arg.var_min {
3343 if values.len() < min {
3344 out.errors.push(UsageErr::VarArgTooFew {
3345 name: arg.name.clone(),
3346 min,
3347 got: values.len(),
3348 });
3349 }
3350 }
3351 if let Some(max) = arg.var_max {
3352 if values.len() > max {
3353 out.errors.push(UsageErr::VarArgTooMany {
3354 name: arg.name.clone(),
3355 max,
3356 got: values.len(),
3357 });
3358 }
3359 }
3360 }
3361 }
3362 }
3363
3364 for flag in unique_flags(out.available_flags.values())
3368 .filter(|flag| !clause_flag_names.contains(&flag.name))
3369 {
3370 if flag.var {
3371 let bound = match out.flags.get(flag) {
3372 Some(ParseValue::MultiString(values)) => values.len(),
3373 Some(ParseValue::MultiBool(values)) => values.len(),
3374 Some(_) => 1,
3375 None => 0,
3376 };
3377 let pending = out
3381 .flag_awaiting_value
3382 .iter()
3383 .filter(|pending| {
3384 Arc::ptr_eq(pending, flag)
3385 && (pending.value_optional || pending.default_missing.is_some())
3386 })
3387 .count();
3388 let count = bound + pending;
3389 if count == 0 {
3390 continue;
3391 }
3392 if let Some(min) = flag.var_min {
3393 if count < min {
3394 out.errors.push(UsageErr::VarFlagTooFew {
3395 name: flag.name.clone(),
3396 min,
3397 got: count,
3398 });
3399 }
3400 }
3401 if let Some(max) = flag.var_max {
3402 if count > max {
3403 out.errors.push(UsageErr::VarFlagTooMany {
3404 name: flag.name.clone(),
3405 max,
3406 got: count,
3407 });
3408 }
3409 }
3410 }
3411 }
3412
3413 Ok((out, overridden_flags))
3414}
3415
3416fn validate_expression(
3417 name: &str,
3418 expression: Option<&str>,
3419 message: Option<&str>,
3420 parsed: &ParseValue,
3421 errors: &mut Vec<UsageErr>,
3422) {
3423 let Some(expression) = expression else {
3424 return;
3425 };
3426 #[cfg(not(feature = "validation"))]
3427 let _ = expression;
3428 let values: &[String] = match parsed {
3429 ParseValue::String(value) => std::slice::from_ref(value),
3430 ParseValue::MultiString(values) => values,
3431 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3432 };
3433 #[cfg(feature = "validation")]
3434 for value in values {
3435 let reason = match usage_validation::validate(expression, value) {
3436 Ok(true) => continue,
3437 Ok(false) => message
3438 .unwrap_or("does not satisfy the validation expression")
3439 .to_string(),
3440 Err(error) => format!("validation expression failed: {error}"),
3441 };
3442 errors.push(UsageErr::InvalidValue {
3443 name: name.to_string(),
3444 value: value.clone(),
3445 reason,
3446 });
3447 break;
3448 }
3449 #[cfg(not(feature = "validation"))]
3450 if let Some(value) = values.first() {
3451 let _ = message;
3452 errors.push(UsageErr::InvalidValue {
3453 name: name.to_string(),
3454 value: value.clone(),
3455 reason: "expression validation requires the `validation` feature".to_string(),
3456 });
3457 }
3458}
3459
3460#[cfg(all(test, not(feature = "validation")))]
3461mod optional_validation_tests {
3462 use crate::{parse, Spec};
3463
3464 #[test]
3465 fn validation_declarations_require_the_opt_in_runtime_feature() {
3466 let spec: Spec = r#"
3467name "ex"
3468bin "ex"
3469arg "<port>" validate="int(value) > 0"
3470 "#
3471 .parse()
3472 .unwrap();
3473 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3474 assert!(
3475 error
3476 .to_string()
3477 .contains("requires the `validation` feature"),
3478 "{error:?}"
3479 );
3480 }
3481}
3482
3483fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3484 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3485}
3486
3487fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3488 overrider
3489 .overrides
3490 .iter()
3491 .any(|selector| flag_matches_selector(overridden, selector))
3492}
3493
3494fn apply_prefix_flag_overrides(
3495 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3496 flag: Arc<SpecFlag>,
3497) {
3498 prefix_flags
3499 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3500}
3501
3502fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3503 prefix_flags
3504 .iter()
3505 .flat_map(|(_, words)| words.iter().cloned())
3506 .collect()
3507}
3508
3509fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3510 match custom_env {
3511 Some(env) => env.contains_key(env_var),
3512 None => std::env::var(env_var).is_ok(),
3513 }
3514}
3515
3516fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3517 flag.env_names()
3518 .any(|env_var| env_contains(custom_env, env_var))
3519}
3520
3521fn fallback_is_true(value: &str) -> bool {
3522 matches!(value, "1" | "true" | "True" | "TRUE")
3523}
3524
3525fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3526 match delimiter {
3527 Some(delimiter) => values
3528 .iter()
3529 .flat_map(|value| value.split(delimiter).map(str::to_string))
3530 .collect(),
3531 None => values.to_vec(),
3532 }
3533}
3534
3535fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3536 if let Some(min) = arg.var_min {
3537 if count < min {
3538 errors.push(UsageErr::VarArgTooFew {
3539 name: arg.name.clone(),
3540 min,
3541 got: count,
3542 });
3543 }
3544 }
3545 if let Some(max) = arg.var_max {
3546 if count > max {
3547 errors.push(UsageErr::VarArgTooMany {
3548 name: arg.name.clone(),
3549 max,
3550 got: count,
3551 });
3552 }
3553 }
3554}
3555
3556fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3557 if let Some(min) = flag.var_min {
3558 if count < min {
3559 errors.push(UsageErr::VarFlagTooFew {
3560 name: flag.name.clone(),
3561 min,
3562 got: count,
3563 });
3564 }
3565 }
3566 if let Some(max) = flag.var_max {
3567 if count > max {
3568 errors.push(UsageErr::VarFlagTooMany {
3569 name: flag.name.clone(),
3570 max,
3571 got: count,
3572 });
3573 }
3574 }
3575}
3576
3577fn validate_flag_arg_fallback_count(
3578 flag: &SpecFlag,
3579 arg: &SpecArg,
3580 count: usize,
3581 errors: &mut Vec<UsageErr>,
3582) {
3583 if let Some(min) = arg.var_min {
3584 if count < min {
3585 errors.push(UsageErr::VarFlagTooFew {
3586 name: flag.name.clone(),
3587 min,
3588 got: count,
3589 });
3590 }
3591 }
3592 if let Some(max) = arg.var_max {
3593 if count > max {
3594 errors.push(UsageErr::VarFlagTooMany {
3595 name: flag.name.clone(),
3596 max,
3597 got: count,
3598 });
3599 }
3600 }
3601}
3602
3603fn bind_flag_fallback(
3606 flag: &Arc<SpecFlag>,
3607 values: &[String],
3608 out: &mut ParseOutput,
3609 custom_env: Option<&HashMap<String, String>>,
3610 origin: ValueOrigin,
3611) -> Result<(), miette::Error> {
3612 let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3613 return Ok(());
3614 };
3615 out.flags.insert(Arc::clone(flag), value);
3616 out.flag_origins
3617 .entry(Arc::clone(flag))
3618 .or_default()
3619 .push(origin);
3620 Ok(())
3621}
3622
3623fn flag_fallback_value(
3624 flag: &SpecFlag,
3625 values: &[String],
3626 errors: &mut Vec<UsageErr>,
3627 custom_env: Option<&HashMap<String, String>>,
3628) -> Result<Option<ParseValue>, miette::Error> {
3629 if values.is_empty() {
3630 return Ok(None);
3631 }
3632 if let Some(arg) = flag.arg.as_ref() {
3633 let values = split_fallback_values(values, arg.delimiter);
3634 if flag.var || arg.var {
3635 if flag.var {
3636 validate_flag_fallback_count(flag, values.len(), errors);
3637 }
3638 if arg.var {
3639 validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3640 }
3641 validate_choice_values(
3642 ChoiceTarget::option(flag),
3643 &values,
3644 arg.choices.as_ref(),
3645 custom_env,
3646 )?;
3647 Ok(Some(ParseValue::MultiString(values)))
3648 } else {
3649 let value = values.into_iter().next().unwrap_or_default();
3650 validate_choice_value(
3651 ChoiceTarget::option(flag),
3652 &value,
3653 arg.choices.as_ref(),
3654 custom_env,
3655 )?;
3656 Ok(Some(ParseValue::String(value)))
3657 }
3658 } else if flag.var {
3659 validate_flag_fallback_count(flag, values.len(), errors);
3660 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3661 Ok(Some(ParseValue::MultiBool(bools)))
3662 } else {
3663 Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3664 }
3665}
3666
3667fn apply_clause_flag_fallbacks(
3673 out: &mut ParseOutput,
3674 overridden_flags: &HashSet<String>,
3675 custom_env: Option<&HashMap<String, String>>,
3676) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3677 let Some(clause) = out.cmd.clause.clone() else {
3678 return Ok(None);
3679 };
3680 let Some(positional_instances) = out.clauses.get(&clause.name) else {
3681 return Ok(None);
3682 };
3683 let positional_instances = positional_instances.clone();
3684 let mut flag_instances = out
3685 .clause_flags
3686 .shift_remove(&clause.name)
3687 .unwrap_or_default();
3688 flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3689 let get_env = |key: &str| -> Option<String> {
3690 custom_env
3691 .and_then(|values| values.get(key).cloned())
3692 .or_else(|| {
3693 custom_env
3694 .is_none()
3695 .then(|| std::env::var(key).ok())
3696 .flatten()
3697 })
3698 };
3699 let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3700
3701 for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3702 let positional = &positional_instances[instance_index];
3703 let mut explicit = scoped
3704 .keys()
3705 .map(|flag| flag.name.clone())
3706 .collect::<HashSet<_>>();
3707
3708 for declared in &clause.flags {
3711 let flag = Arc::new(declared.clone());
3712 if scoped.contains_key(&flag) {
3713 continue;
3714 }
3715 let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3716 continue;
3717 };
3718 if let Some(warning) = flag_deprecation(declared) {
3719 out.warnings.push(warning);
3720 }
3721 if flag_env_is_deprecated(declared, env_name) {
3722 out.warnings
3723 .push(Warning::env(env_name, flag_current_env(declared)));
3724 }
3725 if let Some(value) = flag_fallback_value(
3726 declared,
3727 std::slice::from_ref(&env_value),
3728 &mut out.errors,
3729 custom_env,
3730 )? {
3731 scoped.insert(flag, value);
3732 explicit.insert(declared.name.clone());
3733 }
3734 }
3735
3736 let condition_matches =
3737 |condition: &crate::SpecDefaultIf| {
3738 if let Some(flag) = clause
3739 .flags
3740 .iter()
3741 .find(|flag| flag_matches_selector(flag, &condition.selector))
3742 {
3743 if !explicit.contains(&flag.name) {
3744 return false;
3745 }
3746 return condition.when.as_ref().is_none_or(|expected| {
3747 scoped
3748 .iter()
3749 .find(|(present, _)| present.name == flag.name)
3750 .is_some_and(|(_, value)| parse_value_has(value, expected))
3751 });
3752 }
3753 if let Some(arg) = clause.args.iter().find(|arg| {
3754 !condition.selector.starts_with('-') && arg.name == condition.selector
3755 }) {
3756 return positional.get(arg).is_some_and(|value| {
3757 condition
3758 .when
3759 .as_ref()
3760 .is_none_or(|expected| parse_value_has(value, expected))
3761 });
3762 }
3763 let command_flag = out
3764 .available_flags
3765 .values()
3766 .chain(out.flags.keys())
3767 .filter(|flag| !is_clause_scoped_flag(out, flag))
3768 .find(|flag| flag_matches_selector(flag, &condition.selector));
3769 command_flag.is_some_and(|flag| {
3770 !overridden_flags.contains(&flag.name)
3771 && command_flag_has_explicit_source(flag, out)
3772 && condition.when.as_ref().is_none_or(|expected| {
3773 out.flags
3774 .get(flag)
3775 .is_some_and(|value| parse_value_has(value, expected))
3776 })
3777 })
3778 };
3779
3780 let conditional = clause
3781 .flags
3782 .iter()
3783 .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3784 .filter_map(|flag| {
3785 flag.default_if
3786 .iter()
3787 .find(|condition| condition_matches(condition))
3788 .map(|condition| (flag.clone(), condition.value.clone()))
3789 })
3790 .collect::<Vec<_>>();
3791 for (declared, value) in conditional {
3792 if let Some(value) = flag_fallback_value(
3793 &declared,
3794 std::slice::from_ref(&value),
3795 &mut out.errors,
3796 custom_env,
3797 )? {
3798 scoped.insert(Arc::new(declared), value);
3799 }
3800 }
3801
3802 for declared in &clause.flags {
3803 let flag = Arc::new(declared.clone());
3804 if scoped.contains_key(&flag) {
3805 continue;
3806 }
3807 let values = if !declared.default.is_empty() {
3808 &declared.default
3809 } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3810 &arg.default
3811 } else {
3812 continue;
3813 };
3814 if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3815 {
3816 scoped.insert(flag, value);
3817 }
3818 }
3819 explicit_instances.push(explicit);
3820 }
3821
3822 out.clause_flags.insert(clause.name, flag_instances);
3823 Ok(Some(explicit_instances))
3824}
3825
3826fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3827 out.flags.contains_key(flag)
3828 && out.flag_origins.get(flag).is_none_or(|origins| {
3829 origins
3830 .iter()
3831 .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3832 })
3833}
3834
3835fn default_if_condition_matches(
3836 condition: &crate::SpecDefaultIf,
3837 out: &ParseOutput,
3838 overridden_flags: &HashSet<String>,
3839 custom_env: Option<&HashMap<String, String>>,
3840) -> bool {
3841 match &condition.when {
3842 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3843 Some(when) => {
3844 let Some(flag) = out
3845 .available_flags
3846 .values()
3847 .chain(out.flags.keys())
3848 .find(|flag| flag_matches_selector(flag, &condition.selector))
3849 else {
3850 return false;
3851 };
3852 if overridden_flags.contains(&flag.name) {
3853 return false;
3854 }
3855 explicit_flag_has_value(flag, when, out, custom_env)
3856 }
3857 }
3858}
3859
3860fn explicit_flag_has_value(
3866 flag: &SpecFlag,
3867 expected: &str,
3868 out: &ParseOutput,
3869 custom_env: Option<&HashMap<String, String>>,
3870) -> bool {
3871 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3872 ParseValue::Bool(value) => value.to_string() == expected,
3873 ParseValue::String(value) => value == expected,
3874 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3875 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3876 });
3877 if out.flags.contains_key(flag) {
3878 return parsed_matches;
3879 }
3880
3881 let value = flag.env_names().find_map(|env| match custom_env {
3882 Some(values) => values.get(env).cloned(),
3883 None => std::env::var(env).ok(),
3884 });
3885 value.is_some_and(
3886 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3887 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3888 None if flag.arg.is_none() => {
3889 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3890 }
3891 None => value == expected,
3892 },
3893 )
3894}
3895
3896fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3897 match value {
3898 ParseValue::Bool(value) => value.to_string() == expected,
3899 ParseValue::String(value) => value == expected,
3900 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3901 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3902 }
3903}
3904
3905fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3908 out.cmd
3909 .clause
3910 .as_ref()
3911 .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3912}
3913
3914fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3915 out.cmd
3916 .clause
3917 .as_ref()?
3918 .flags
3919 .iter()
3920 .find(|flag| flag_matches_selector(flag, selector))
3921}
3922
3923fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3924 out.clause_flags
3925 .values()
3926 .flatten()
3927 .any(|instance| instance.keys().any(|present| present.name == flag.name))
3928 || out.flags.keys().any(|present| present.name == flag.name)
3929}
3930
3931fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3932 out.clause_flags.values().flatten().any(|instance| {
3933 instance
3934 .iter()
3935 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3936 }) || out
3937 .flags
3938 .iter()
3939 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3940}
3941
3942fn validate_clause_relationships(
3943 out: &mut ParseOutput,
3944 overridden_flags: &HashSet<String>,
3945 custom_env: Option<&HashMap<String, String>>,
3946 explicit_instances: Option<&[HashSet<String>]>,
3947) {
3948 let Some(clause) = out.cmd.clause.as_ref() else {
3949 return;
3950 };
3951 let Some(instances) = out.clauses.get(&clause.name) else {
3952 return;
3953 };
3954 let command_flag_is_explicit = |selector: &str| {
3955 out.available_flags
3956 .values()
3957 .chain(out.flags.keys())
3958 .filter(|flag| !is_clause_scoped_flag(out, flag))
3959 .any(|flag| {
3960 flag_matches_selector(flag, selector)
3961 && !overridden_flags.contains(&flag.name)
3962 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3963 })
3964 };
3965 let command_flag_matches_value = |selector: &str, expected: &str| {
3966 out.available_flags
3967 .values()
3968 .chain(out.flags.keys())
3969 .filter(|flag| !is_clause_scoped_flag(out, flag))
3970 .find(|flag| flag_matches_selector(flag, selector))
3971 .is_some_and(|flag| {
3972 !overridden_flags.contains(&flag.name)
3973 && explicit_flag_has_value(flag, expected, out, custom_env)
3974 })
3975 };
3976 let command_flag_is_satisfied = |selector: &str| {
3977 out.available_flags
3978 .values()
3979 .chain(out.flags.keys())
3980 .filter(|flag| !is_clause_scoped_flag(out, flag))
3981 .any(|flag| flag_matches_selector(flag, selector))
3982 && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3983 };
3984 let mut errors = Vec::new();
3985 for (instance_index, instance) in instances.iter().enumerate() {
3986 let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
3987 let scoped = out
3988 .clause_flags
3989 .get(&clause.name)
3990 .and_then(|instances| instances.get(instance_index));
3991 let scoped_flag = |selector: &str| {
3992 clause
3993 .flags
3994 .iter()
3995 .find(|flag| flag_matches_selector(flag, selector))
3996 };
3997 let scoped_value = |flag: &SpecFlag| {
3998 scoped.and_then(|values| {
3999 values
4000 .iter()
4001 .find(|(present, _)| present.name == flag.name)
4002 .map(|(_, value)| value)
4003 })
4004 };
4005 let arg_is_explicit = |selector: &str| {
4006 instance
4007 .keys()
4008 .any(|arg| !selector.starts_with('-') && arg.name == selector)
4009 };
4010 let selector_is_explicit = |selector: &str| {
4011 scoped_flag(selector).is_some_and(|flag| {
4012 explicit
4013 .map(|given| given.contains(&flag.name))
4014 .unwrap_or_else(|| scoped_value(flag).is_some())
4015 }) || command_flag_is_explicit(selector)
4016 || arg_is_explicit(selector)
4017 };
4018 let selector_has_value = |selector: &str, expected: &str| {
4019 scoped_flag(selector)
4020 .filter(|flag| {
4021 explicit
4022 .map(|given| given.contains(&flag.name))
4023 .unwrap_or_else(|| scoped_value(flag).is_some())
4024 })
4025 .and_then(scoped_value)
4026 .is_some_and(|value| parse_value_has(value, expected))
4027 || command_flag_matches_value(selector, expected)
4028 || instance.iter().any(|(arg, value)| {
4029 !selector.starts_with('-')
4030 && arg.name == selector
4031 && parse_value_has(value, expected)
4032 })
4033 };
4034 let selector_is_satisfied = |selector: &str| {
4035 scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4036 || command_flag_is_satisfied(selector)
4037 || arg_is_explicit(selector)
4038 };
4039 let selector_name = |selector: &str| {
4040 scoped_flag(selector)
4041 .map(|flag| flag.name.clone())
4042 .or_else(|| selector_flag_name(selector, out))
4043 .unwrap_or_else(|| selector.to_string())
4044 };
4045
4046 for flag in &clause.flags {
4047 let value = scoped_value(flag);
4048 if value.is_none() {
4049 let required_if = flag
4050 .required_if
4051 .iter()
4052 .any(|selector| selector_is_explicit(selector));
4053 let required_if_eq = flag
4054 .required_if_eq
4055 .iter()
4056 .any(|condition| selector_has_value(&condition.selector, &condition.value));
4057 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4058 && flag
4059 .required_if_eq_all
4060 .iter()
4061 .all(|condition| selector_has_value(&condition.selector, &condition.value));
4062 let unless_any = flag
4063 .required_unless
4064 .iter()
4065 .any(|selector| selector_is_explicit(selector));
4066 let unless_all = !flag.required_unless_all.is_empty()
4067 && flag
4068 .required_unless_all
4069 .iter()
4070 .all(|selector| selector_is_explicit(selector));
4071 let required_unless = !(unless_any
4072 || unless_all
4073 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4074 if flag.required
4075 || required_if
4076 || required_if_eq
4077 || required_if_eq_all
4078 || required_unless
4079 {
4080 errors.push(UsageErr::MissingFlag(flag.name.clone()));
4081 }
4082 continue;
4083 }
4084
4085 let explicitly_provided = explicit
4086 .map(|given| given.contains(&flag.name))
4087 .unwrap_or(true);
4088
4089 if explicitly_provided {
4090 for other in &flag.conflicts {
4091 if selector_is_explicit(other) {
4092 errors.push(UsageErr::InvalidFlag {
4093 token: format!("--{}", flag.name),
4094 reason: format!("conflicts with {other}"),
4095 span: (0, 0).into(),
4096 input: format!("--{} {other}", flag.name),
4097 });
4098 }
4099 }
4100 for other in &flag.requires {
4101 if !selector_is_satisfied(other) {
4102 errors.push(UsageErr::MissingFlag(selector_name(other)));
4103 }
4104 }
4105 for condition in &flag.requires_if {
4106 if value.is_some_and(|value| parse_value_has(value, &condition.value))
4107 && !selector_is_satisfied(&condition.requires)
4108 {
4109 errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4110 }
4111 }
4112 }
4113 if let (true, Some(value)) = (flag.var, value) {
4114 let count = match value {
4115 ParseValue::MultiString(values) => values.len(),
4116 ParseValue::MultiBool(values) => values.len(),
4117 _ => 1,
4118 };
4119 if let Some(min) = flag.var_min.filter(|min| count < *min) {
4120 errors.push(UsageErr::VarFlagTooFew {
4121 name: flag.name.clone(),
4122 min,
4123 got: count,
4124 });
4125 }
4126 if let Some(max) = flag.var_max.filter(|max| count > *max) {
4127 errors.push(UsageErr::VarFlagTooMany {
4128 name: flag.name.clone(),
4129 max,
4130 got: count,
4131 });
4132 }
4133 }
4134 if let (Some(arg), Some(value)) = (&flag.arg, value) {
4135 validate_expression(
4136 &flag.name,
4137 arg.validate.as_deref(),
4138 arg.validate_error.as_deref(),
4139 value,
4140 &mut errors,
4141 );
4142 }
4143 }
4144 for arg in &clause.args {
4145 let given = instance.keys().any(|present| present.name == arg.name);
4146 if !given {
4147 let required_if = arg
4148 .required_if
4149 .iter()
4150 .any(|selector| selector_is_explicit(selector));
4151 let required_if_eq = arg
4152 .required_if_eq
4153 .iter()
4154 .any(|condition| selector_has_value(&condition.selector, &condition.value));
4155 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4156 && arg
4157 .required_if_eq_all
4158 .iter()
4159 .all(|condition| selector_has_value(&condition.selector, &condition.value));
4160 let unless_any = arg
4161 .required_unless
4162 .iter()
4163 .any(|selector| selector_is_explicit(selector));
4164 let unless_all = !arg.required_unless_all.is_empty()
4165 && arg
4166 .required_unless_all
4167 .iter()
4168 .all(|selector| selector_is_explicit(selector));
4169 let required_unless = !(unless_any
4170 || unless_all
4171 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4172 if required_if || required_if_eq || required_if_eq_all || required_unless {
4173 errors.push(UsageErr::MissingClauseArg {
4174 clause: clause.name.clone(),
4175 instance: instance_index + 1,
4176 arg: arg.name.clone(),
4177 });
4178 }
4179 continue;
4180 }
4181 for other in &arg.conflicts {
4182 if selector_is_explicit(other) {
4183 errors.push(UsageErr::InvalidFlag {
4184 token: arg.name.clone(),
4185 reason: format!("conflicts with {other}"),
4186 span: (0, 0).into(),
4187 input: format!("{} {other}", arg.name),
4188 });
4189 }
4190 }
4191 for other in &arg.requires {
4192 if selector_is_satisfied(other) {
4193 continue;
4194 }
4195 if other.starts_with('-') {
4196 errors.push(UsageErr::MissingFlag(selector_name(other)));
4197 } else {
4198 errors.push(UsageErr::MissingClauseArg {
4199 clause: clause.name.clone(),
4200 instance: instance_index + 1,
4201 arg: other.clone(),
4202 });
4203 }
4204 }
4205 }
4206 }
4207 out.errors.extend(errors);
4208}
4209
4210fn selector_explicit_has_value(
4211 selector: &str,
4212 expected: &str,
4213 out: &ParseOutput,
4214 overridden_flags: &HashSet<String>,
4215 custom_env: Option<&HashMap<String, String>>,
4216) -> bool {
4217 if let Some(flag) = selected_clause_flag(out, selector) {
4218 return clause_flag_has_value(out, flag, expected);
4219 }
4220 if let Some(flag) = out
4221 .available_flags
4222 .values()
4223 .chain(out.flags.keys())
4224 .filter(|flag| !is_clause_scoped_flag(out, flag))
4225 .find(|flag| flag_matches_selector(flag, selector))
4226 {
4227 return !overridden_flags.contains(&flag.name)
4228 && explicit_flag_has_value(flag, expected, out, custom_env);
4229 }
4230 let Some(arg) = selector_arg(selector, out) else {
4231 return false;
4232 };
4233 let parsed = out
4234 .args
4235 .iter()
4236 .find(|(given, _)| given.name == arg.name)
4237 .map(|(_, value)| value)
4238 .or_else(|| {
4239 out.clauses.values().flatten().find_map(|instance| {
4240 instance
4241 .iter()
4242 .find(|(given, _)| given.name == arg.name)
4243 .map(|(_, value)| value)
4244 })
4245 });
4246 if let Some(value) = parsed {
4247 return match value {
4248 ParseValue::String(value) => value == expected,
4249 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4250 ParseValue::Bool(value) => value.to_string() == expected,
4251 ParseValue::MultiBool(values) => {
4252 values.iter().any(|value| value.to_string() == expected)
4253 }
4254 };
4255 }
4256 let value = arg.env_names().find_map(|env| match custom_env {
4257 Some(values) => values.get(env).cloned(),
4258 None => std::env::var(env).ok(),
4259 });
4260 value.is_some_and(|value| match arg.delimiter {
4261 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4262 None => value == expected,
4263 })
4264}
4265
4266fn selector_is_explicit(
4267 selector: &str,
4268 out: &ParseOutput,
4269 overridden_flags: &HashSet<String>,
4270 custom_env: Option<&HashMap<String, String>>,
4271) -> bool {
4272 let scoped_flag_is_explicit =
4273 selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4274 let flag_is_explicit = out
4275 .available_flags
4276 .values()
4277 .chain(out.flags.keys())
4278 .filter(|flag| !is_clause_scoped_flag(out, flag))
4279 .any(|flag| {
4280 flag_matches_selector(flag, selector)
4281 && !overridden_flags.contains(&flag.name)
4282 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4283 });
4284 scoped_flag_is_explicit
4285 || flag_is_explicit
4286 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4287}
4288
4289fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4295 if let Some(flag) = selected_clause_flag(out, selector) {
4296 return Some(flag.name.clone());
4297 }
4298 out.available_flags
4299 .values()
4300 .chain(out.flags.keys())
4301 .filter(|flag| !is_clause_scoped_flag(out, flag))
4302 .find(|flag| flag_matches_selector(flag, selector))
4303 .map(|flag| flag.name.clone())
4304 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4305}
4306
4307fn selector_is_satisfied(
4317 selector: &str,
4318 out: &ParseOutput,
4319 overridden_flags: &HashSet<String>,
4320 custom_env: Option<&HashMap<String, String>>,
4321) -> bool {
4322 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4323 return true;
4324 }
4325 let flag_is_satisfied = out
4326 .available_flags
4327 .values()
4328 .chain(out.flags.keys())
4329 .filter(|flag| !is_clause_scoped_flag(out, flag))
4330 .filter(|flag| flag_matches_selector(flag, selector))
4331 .any(|flag| {
4332 !overridden_flags.contains(&flag.name)
4333 && (!flag.default.is_empty()
4334 || flag.arg.iter().any(|a| !a.default.is_empty())
4335 || flag.default_if.iter().any(|condition| {
4336 default_if_condition_matches(condition, out, overridden_flags, custom_env)
4337 }))
4338 });
4339 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4340}
4341
4342fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4343 if selector.starts_with('-') {
4346 return None;
4347 }
4348 out.cmds
4349 .iter()
4350 .flat_map(active_args)
4351 .find(|arg| arg.name == selector)
4352}
4353
4354fn arg_is_explicit(
4355 arg: &SpecArg,
4356 out: &ParseOutput,
4357 custom_env: Option<&HashMap<String, String>>,
4358) -> bool {
4359 out.args.keys().any(|given| given.name == arg.name)
4360 || out
4361 .clauses
4362 .values()
4363 .flatten()
4364 .any(|instance| instance.keys().any(|given| given.name == arg.name))
4365 || arg
4366 .env
4367 .as_ref()
4368 .is_some_and(|env| env_contains(custom_env, env))
4369}
4370
4371fn apply_flag_overrides(
4372 flag: &Arc<SpecFlag>,
4373 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4374 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4375 pending_flags: &mut Vec<Arc<SpecFlag>>,
4376 overridden_flags: &mut HashSet<String>,
4377 attributed: &mut BTreeMap<String, String>,
4381) {
4382 let overridden_names: HashSet<String> = available_flags
4383 .values()
4384 .chain(parsed_flags.keys())
4385 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4386 .map(|other| other.name.clone())
4387 .collect();
4388
4389 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4390 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4391 for name in &overridden_names {
4392 attributed.insert(name.clone(), flag.name.clone());
4393 }
4394 overridden_flags.extend(overridden_names);
4395 overridden_flags.remove(&flag.name);
4397 attributed.remove(&flag.name);
4398}
4399
4400#[cfg(feature = "cli-help")]
4401fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
4402 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
4403}
4404
4405#[cfg(feature = "cli-help")]
4406fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
4407 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
4408 if !out.is_empty() {
4409 out.push('\n');
4410 }
4411 out.push_str(&docs::cli::render_help(spec, cmd, true));
4412 let mut children: Vec<_> = cmd
4413 .subcommands
4414 .values()
4415 .filter(|child| !child.hide)
4416 .collect();
4417 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4418 for child in children {
4419 append(out, spec, child);
4420 }
4421 }
4422
4423 let mut out = String::new();
4424 append(&mut out, spec, cmd);
4425 UsageErr::Help(out)
4426}
4427
4428#[cfg(not(feature = "cli-help"))]
4429fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
4430 UsageErr::Help("help".to_string())
4431}
4432
4433#[cfg(not(feature = "cli-help"))]
4434fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
4435 UsageErr::Help("help".to_string())
4436}
4437
4438fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4441 let value = if long {
4442 spec.long_version.as_ref().or(spec.version.as_ref())
4443 } else {
4444 spec.version.as_ref().or(spec.long_version.as_ref())
4445 };
4446 UsageErr::Version(value.cloned().unwrap_or_default())
4447}
4448
4449fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
4450 use crate::SpecFlagAction;
4451 match flag.action {
4452 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
4453 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
4454 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
4455 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
4456 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4457 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4458 }
4459}
4460
4461fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4463 let token = flag
4464 .long
4465 .first()
4466 .map(|long| format!("--{long}"))
4467 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4468 .unwrap_or_else(|| flag.name.clone());
4469 UsageErr::InvalidFlag {
4470 token: token.clone(),
4471 reason: "requires an argument".to_string(),
4472 span: (0, 0).into(),
4473 input: format!("{token} {following}"),
4474 }
4475}
4476
4477#[derive(Copy, Clone)]
4478struct ChoiceTarget<'a> {
4479 kind: &'a str,
4480 name: &'a str,
4481}
4482
4483impl<'a> ChoiceTarget<'a> {
4484 fn arg(arg: &'a SpecArg) -> Self {
4485 Self {
4486 kind: "arg",
4487 name: &arg.name,
4488 }
4489 }
4490
4491 fn option(flag: &'a SpecFlag) -> Self {
4492 Self {
4493 kind: "option",
4494 name: &flag.name,
4495 }
4496 }
4497}
4498
4499fn short_bundle_is_known(
4504 spec: &Spec,
4505 cmds: &[SpecCommand],
4506 available: &BTreeMap<String, Arc<SpecFlag>>,
4507 token: &str,
4508) -> bool {
4509 for c in token.chars().skip(1) {
4510 match available.get(&format!("-{c}")) {
4511 None if supplied_short(spec, cmds, c).is_some() => {}
4516 None => return false,
4517 Some(f) if f.arg.is_some() => return true,
4518 Some(_) => {}
4519 }
4520 }
4521 true
4522}
4523
4524fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
4534 let cmd = cmds.last()?;
4535 match letter {
4536 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
4537 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4538 _ => None,
4539 }
4540}
4541
4542fn reject_unknown_flag_if_asked(
4548 spec: &Spec,
4549 path: &[SpecCommand],
4550 token: &str,
4551) -> Result<(), UsageErr> {
4552 if !is_flag_like(token) {
4555 return Ok(());
4556 }
4557 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4558 return Ok(());
4559 }
4560 Err(UsageErr::InvalidFlag {
4561 token: token.to_string(),
4562 reason: "no such flag".to_string(),
4563 span: (0, 0).into(),
4564 input: token.to_string(),
4565 })
4566}
4567
4568fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4574 path.iter()
4575 .rev()
4576 .find_map(|cmd| cmd.unknown_flags)
4577 .or(spec.unknown_flags)
4578 .unwrap_or_default()
4579}
4580
4581fn is_flag_like(token: &str) -> bool {
4587 match token.strip_prefix('-') {
4588 None | Some("") => false,
4589 Some(_) => true,
4590 }
4591}
4592
4593fn is_negative_number(token: &str) -> bool {
4594 token.strip_prefix('-').is_some_and(is_number)
4595}
4596
4597fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4603 !flag.require_equals
4604 && (!is_flag_like(token)
4605 || flag.allow_hyphen_values()
4606 || (is_negative_number(token)
4607 && (flag
4608 .arg
4609 .as_ref()
4610 .is_some_and(|arg| arg.allow_negative_numbers)
4611 || (flag.default_missing.is_none() && !flag.value_optional))))
4612}
4613
4614fn record_scalar_flag_occurrence(
4615 cmds: &[SpecCommand],
4616 flag: &Arc<SpecFlag>,
4617 command_level: usize,
4618 bool_value: Option<bool>,
4619 occurrences: &mut HashMap<(usize, usize), u8>,
4620 errors: &mut Vec<UsageErr>,
4621) {
4622 let strict = cmds.get(command_level).is_some_and(|cmd| {
4623 !cmd.args_override_self
4624 || cmd
4625 .clause
4626 .as_ref()
4627 .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4628 });
4629 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4630 if !strict || flag.count || collects_values {
4631 return;
4632 }
4633
4634 let bit = match bool_value {
4635 Some(false) if flag.negate.is_some() => 0b10,
4636 _ => 0b01,
4637 };
4638 let key = (Arc::as_ptr(flag) as usize, command_level);
4639 let seen = occurrences.entry(key).or_default();
4640 if *seen & bit != 0 {
4641 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4642 }
4643 *seen |= bit;
4644}
4645
4646fn is_command_word(token: &str) -> bool {
4654 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4655}
4656
4657fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4663 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4664}
4665
4666fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4672 !is_negative_number(token)
4673 || cmd
4674 .find_subcommand(default_name)
4675 .and_then(|default| default.args.first())
4676 .is_some_and(|arg| arg.allow_negative_numbers)
4677}
4678
4679fn is_number(rest: &str) -> bool {
4686 let (mantissa, exponent) = match rest.find(['e', 'E']) {
4687 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4688 None => (rest, None),
4689 };
4690
4691 let mut seen_digit = false;
4692 let mut seen_dot = false;
4693 for c in mantissa.chars() {
4694 match c {
4695 '0'..='9' => seen_digit = true,
4696 '.' if !seen_dot => seen_dot = true,
4697 _ => return false,
4698 }
4699 }
4700 if !seen_digit {
4701 return false;
4702 }
4703
4704 match exponent {
4705 None => true,
4706 Some(exp) => {
4707 let digits = exp
4708 .strip_prefix('+')
4709 .or_else(|| exp.strip_prefix('-'))
4710 .unwrap_or(exp);
4711 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4712 }
4713 }
4714}
4715
4716#[allow(clippy::too_many_arguments)]
4723fn bind_pending_flag_value(
4724 spec: &Spec,
4725 cmd: &SpecCommand,
4726 errors: &mut Vec<UsageErr>,
4727 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4728 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4729 word: &mut String,
4730 input: &mut VecDeque<Token>,
4731 custom_env: Option<&HashMap<String, String>>,
4732 trace: &mut Trace,
4733 argv: usize,
4737 attached: bool,
4738) -> miette::Result<bool> {
4739 let collecting = flag_awaiting_value
4743 .last()
4744 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4745 .cloned()
4746 .map(|flag| {
4747 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4748 (flag, carried)
4749 });
4750 let mut bound = vec![];
4751 let refused = drain_pending_flag_values(
4752 spec,
4753 cmd,
4754 errors,
4755 flags,
4756 flag_awaiting_value,
4757 word,
4758 custom_env,
4759 &mut bound,
4760 )?;
4761 for (flag, values) in bound {
4762 trace.record(
4763 argv,
4764 TokenRole::Value {
4765 flag,
4766 values,
4767 attached,
4768 },
4769 );
4770 }
4771 if refused {
4772 return Ok(true);
4773 }
4774 let Some((flag, carried)) = collecting else {
4775 return Ok(false);
4776 };
4777 collect_variadic_flag_values(
4778 spec,
4779 cmd,
4780 errors,
4781 flags,
4782 flag_awaiting_value,
4783 &flag,
4784 carried,
4785 input,
4786 custom_env,
4787 trace,
4788 )
4789}
4790
4791#[allow(clippy::too_many_arguments)]
4804fn collect_variadic_flag_values(
4805 spec: &Spec,
4806 cmd: &SpecCommand,
4807 errors: &mut Vec<UsageErr>,
4808 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4809 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4810 flag: &Arc<SpecFlag>,
4811 carried: usize,
4812 input: &mut VecDeque<Token>,
4813 custom_env: Option<&HashMap<String, String>>,
4814 trace: &mut Trace,
4815) -> miette::Result<bool> {
4816 let max = flag
4817 .arg
4818 .as_ref()
4819 .and_then(|arg| arg.var_max)
4820 .unwrap_or(usize::MAX);
4821 while flags
4822 .get(flag)
4823 .map(value_count)
4824 .unwrap_or(0)
4825 .saturating_sub(carried)
4826 < max
4827 {
4828 let Some(next) = input.front().map(|token| token.word.as_str()) else {
4829 break;
4830 };
4831 if flag
4832 .arg
4833 .as_ref()
4834 .and_then(|arg| arg.value_terminator.as_deref())
4835 == Some(next)
4836 {
4837 let terminator = input.pop_front().unwrap();
4838 trace.record(
4839 terminator.argv,
4840 TokenRole::ValueTerminator {
4841 ends: flag.name.clone(),
4842 },
4843 );
4844 break;
4845 }
4846 if next == "--"
4849 || (is_flag_like(next)
4850 && !(flag
4851 .arg
4852 .as_ref()
4853 .is_some_and(|arg| arg.allow_negative_numbers)
4854 && is_negative_number(next)))
4855 {
4856 break;
4857 }
4858 let taken = input.pop_front().unwrap();
4859 let argv = taken.argv;
4860 let mut word = taken.word;
4861 flag_awaiting_value.push(Arc::clone(flag));
4862 let mut bound = vec![];
4863 let refused = drain_pending_flag_values(
4864 spec,
4865 cmd,
4866 errors,
4867 flags,
4868 flag_awaiting_value,
4869 &mut word,
4870 custom_env,
4871 &mut bound,
4872 )?;
4873 for (flag, values) in bound {
4874 trace.record(
4877 argv,
4878 TokenRole::Value {
4879 flag,
4880 values,
4881 attached: false,
4882 },
4883 );
4884 }
4885 if refused {
4886 return Ok(true);
4887 }
4888 }
4889 let taken = flags
4898 .get(flag)
4899 .map(value_count)
4900 .unwrap_or(0)
4901 .saturating_sub(carried);
4902 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4903 if taken < min {
4904 errors.push(UsageErr::VarFlagTooFew {
4905 name: flag.name.clone(),
4906 min,
4907 got: taken,
4908 });
4909 }
4910 }
4911 if taken > max {
4912 errors.push(UsageErr::VarFlagTooMany {
4913 name: flag.name.clone(),
4914 max,
4915 got: taken,
4916 });
4917 }
4918 Ok(false)
4919}
4920
4921fn value_count(value: &ParseValue) -> usize {
4923 match value {
4924 ParseValue::MultiString(values) => values.len(),
4925 ParseValue::MultiBool(values) => values.len(),
4926 _ => 1,
4927 }
4928}
4929
4930fn try_bind_default_missing(
4941 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4942 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4943 custom_env: Option<&HashMap<String, String>>,
4944 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4945) -> miette::Result<bool> {
4946 let Some(flag) = flag_awaiting_value.last() else {
4947 return Ok(false);
4948 };
4949 let value = match flag.default_missing.clone() {
4950 Some(value) => value,
4951 None if flag.value_optional => {
4952 let flag = flag_awaiting_value.pop().unwrap();
4953 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4957 origins
4958 .entry(Arc::clone(&flag))
4959 .or_default()
4960 .push(ValueOrigin::DefaultMissing);
4961 if flag.var {
4962 flags
4967 .entry(flag)
4968 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4969 .try_as_multi_string_mut()
4970 .unwrap()
4971 .push(String::new());
4972 } else if variadic_value {
4973 flags
4976 .entry(flag)
4977 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4978 } else {
4979 flags.insert(flag, ParseValue::MultiString(Vec::new()));
4983 }
4984 return Ok(true);
4985 }
4986 None => return Ok(false),
4987 };
4988 if let Some(arg) = flag.arg.as_ref() {
4989 validate_choice_value(
4990 ChoiceTarget::option(flag),
4991 &value,
4992 arg.choices.as_ref(),
4993 custom_env,
4994 )?;
4995 }
4996 let flag = flag_awaiting_value.pop().unwrap();
4997 origins
4998 .entry(Arc::clone(&flag))
4999 .or_default()
5000 .push(ValueOrigin::DefaultMissing);
5001 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5002 if collecting {
5003 let arr = flags
5004 .entry(flag)
5005 .or_insert_with(|| ParseValue::MultiString(vec![]))
5006 .try_as_multi_string_mut()
5007 .unwrap();
5008 arr.push(value);
5009 } else {
5010 flags.insert(flag, ParseValue::String(value));
5011 }
5012 Ok(true)
5013}
5014
5015#[allow(clippy::too_many_arguments)]
5020fn drain_pending_flag_values(
5021 spec: &Spec,
5022 cmd: &SpecCommand,
5023 errors: &mut Vec<UsageErr>,
5024 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5025 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5026 word: &mut String,
5027 custom_env: Option<&HashMap<String, String>>,
5028 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5029) -> miette::Result<bool> {
5030 while let Some(flag) = flag_awaiting_value.pop() {
5031 let arg = flag.arg.as_ref().unwrap();
5032 let parts: Vec<String> = match arg.delimiter {
5036 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5037 None => vec![std::mem::take(word)],
5038 };
5039 for part in &parts {
5040 if validate_choices(
5041 spec,
5042 cmd,
5043 errors,
5044 ChoiceTarget::option(&flag),
5045 part,
5046 arg.choices.as_ref(),
5047 custom_env,
5048 )? {
5049 return Ok(true);
5050 }
5051 }
5052 word.clear();
5053 bound.push((Arc::clone(&flag), parts.clone()));
5054 if flag.var || arg.var {
5057 let arr = flags
5058 .entry(flag)
5059 .or_insert_with(|| ParseValue::MultiString(vec![]))
5060 .try_as_multi_string_mut()
5061 .unwrap();
5062 arr.extend(parts);
5063 } else {
5064 flags.insert(
5067 flag,
5068 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5069 );
5070 }
5071 }
5072 Ok(false)
5073}
5074
5075fn choice_error(
5076 target: ChoiceTarget<'_>,
5077 value: &str,
5078 choices: Option<&SpecChoices>,
5079 custom_env: Option<&HashMap<String, String>>,
5080) -> Option<String> {
5081 let choices = choices?;
5082 if !choices.strict {
5083 return None;
5084 }
5085 let values = choices.values_with_env(custom_env);
5086 if choices.matches_with_env(value, custom_env) {
5087 return None;
5088 }
5089 if let Some(env) = choices.env() {
5090 if values.is_empty() {
5091 return Some(format!(
5092 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5093 target.kind, target.name,
5094 ));
5095 }
5096 }
5097 Some(format!(
5098 "Invalid choice for {} {}: {value}, expected one of {}",
5099 target.kind,
5100 target.name,
5101 values.join(", ")
5102 ))
5103}
5104
5105fn validate_choices(
5106 spec: &Spec,
5107 cmd: &SpecCommand,
5108 errors: &mut Vec<UsageErr>,
5109 target: ChoiceTarget<'_>,
5110 value: &str,
5111 choices: Option<&SpecChoices>,
5112 custom_env: Option<&HashMap<String, String>>,
5113) -> miette::Result<bool> {
5114 if is_help_arg(spec, cmd, value)
5115 && choices
5116 .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
5117 {
5118 errors.push(render_help_err(spec, cmd, value.len() > 2));
5119 return Ok(true);
5120 }
5121
5122 if let Some(err) = choice_error(target, value, choices, custom_env) {
5123 bail!("{err}");
5124 }
5125 Ok(false)
5126}
5127
5128fn validate_choice_value(
5129 target: ChoiceTarget<'_>,
5130 value: &str,
5131 choices: Option<&SpecChoices>,
5132 custom_env: Option<&HashMap<String, String>>,
5133) -> miette::Result<()> {
5134 if let Some(err) = choice_error(target, value, choices, custom_env) {
5135 bail!("{err}");
5136 }
5137 Ok(())
5138}
5139
5140fn validate_choice_values(
5141 target: ChoiceTarget<'_>,
5142 values: &[String],
5143 choices: Option<&SpecChoices>,
5144 custom_env: Option<&HashMap<String, String>>,
5145) -> miette::Result<()> {
5146 for value in values {
5147 validate_choice_value(target, value, choices, custom_env)?;
5148 }
5149 Ok(())
5150}
5151
5152fn record_stop(
5159 out: &mut ParseOutput,
5160 next_arg_idx: usize,
5161 seen_double_dash: bool,
5162 trace: &mut Trace,
5163 unread: &VecDeque<Token>,
5164) {
5165 out.next_arg = active_args(&out.cmd)
5166 .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5167 .cloned()
5168 .map(Arc::new);
5169 out.double_dash_seen = seen_double_dash;
5170 finalize_current_clause(out);
5171 trace.close(unread);
5172 out.tokens = std::mem::take(&mut trace.tokens);
5173}
5174
5175fn finalize_current_clause(out: &mut ParseOutput) {
5176 let Some(clause) = out.cmd.clause.as_ref() else {
5177 return;
5178 };
5179 let has_scoped_flags = clause
5180 .flags
5181 .iter()
5182 .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5183 if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5184 return;
5185 }
5186 out.clauses
5187 .entry(clause.name.clone())
5188 .or_default()
5189 .push(std::mem::take(&mut out.args));
5190 let mut flags = IndexMap::new();
5191 for clause_flag in &clause.flags {
5192 let key = Arc::new(clause_flag.clone());
5193 if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5194 flags.insert(flag, value);
5195 }
5196 }
5197 out.clause_flags
5198 .entry(clause.name.clone())
5199 .or_default()
5200 .push(flags);
5201}
5202
5203fn restore_current_clause(out: &mut ParseOutput) {
5204 let Some(clause) = out.cmd.clause.as_ref() else {
5205 return;
5206 };
5207 if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5208 out.args = current;
5209 }
5210 if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5211 out.flags.extend(current);
5212 }
5213}
5214
5215fn reset_clause_scalar_occurrences(
5216 out: &ParseOutput,
5217 occurrences: &mut HashMap<(usize, usize), u8>,
5218) {
5219 let Some(clause) = out.cmd.clause.as_ref() else {
5220 return;
5221 };
5222 let names = clause
5223 .flags
5224 .iter()
5225 .map(|flag| flag.name.as_str())
5226 .collect::<HashSet<_>>();
5227 let pointers = unique_flags(out.available_flags.values())
5228 .filter(|flag| names.contains(flag.name.as_str()))
5229 .map(|flag| Arc::as_ptr(flag) as usize)
5230 .collect::<HashSet<_>>();
5231 occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5232}
5233
5234fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5235 while active_args(cmd)
5236 .get(idx)
5237 .is_some_and(|arg| arg.sigil.is_some())
5238 {
5239 idx += 1;
5240 }
5241 idx
5242}
5243
5244fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5245 cmd.clause
5246 .as_ref()
5247 .map(|clause| clause.args.as_slice())
5248 .unwrap_or(cmd.args.as_slice())
5249}
5250
5251fn match_sigil_arg<'a>(
5252 cmd: &'a SpecCommand,
5253 word: &'a str,
5254) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5255 cmd.args
5256 .iter()
5257 .filter_map(|arg| {
5258 let sigil = arg.sigil.as_deref()?;
5259 let value = word.strip_prefix(sigil)?;
5260 Some((arg, sigil, value))
5261 })
5262 .max_by_key(|(_, sigil, _)| sigil.len())
5263}
5264
5265fn match_sigil_arg_chain<'a>(
5266 cmds: &'a [SpecCommand],
5267 word: &'a str,
5268) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5269 cmds.iter()
5270 .filter_map(|cmd| match_sigil_arg(cmd, word))
5271 .max_by_key(|(_, sigil, _)| sigil.len())
5272}
5273
5274fn report_double_dash_violation(
5281 arg: &SpecArg,
5282 errors: &mut Vec<UsageErr>,
5283 violations: &mut HashSet<String>,
5284) {
5285 if violations.insert(arg.name.clone()) {
5286 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5287 }
5288}
5289
5290fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5305 (spec.version.is_some() || spec.long_version.is_some())
5306 && cmds.len() == 1
5307 && !spec.cmd.disable_version_flag
5308 && (w == "--version" || w == "-V")
5309}
5310
5311fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5312 spec.disable_help != Some(true)
5313 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5314 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5315}
5316
5317impl ParseOutput {
5318 pub fn as_env(&self) -> BTreeMap<String, String> {
5319 let mut env = BTreeMap::new();
5320 for (flag, val) in &self.flags {
5321 let key = format!("usage_{}", crate::case::snake(&flag.name));
5322 let val = match val {
5323 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5324 ParseValue::String(s) => s.clone(),
5325 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5326 ParseValue::MultiString(s) => crate::shell_words::join(s),
5327 };
5328 env.insert(key, val);
5329 }
5330 for (arg, val) in &self.args {
5331 let key = format!("usage_{}", crate::case::snake(&arg.name));
5332 env.insert(key, val.to_string());
5333 }
5334 env
5335 }
5336}
5337
5338impl Display for ParseValue {
5339 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5340 match self {
5341 ParseValue::Bool(b) => write!(f, "{b}"),
5342 ParseValue::String(s) => write!(f, "{s}"),
5343 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5344 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5345 }
5346 }
5347}
5348
5349fn render_token(token: &TokenBinding) -> String {
5351 let roles = token.roles.iter().map(render_role).join(", ");
5352 let synthesized = if token.synthesized { " (read as)" } else { "" };
5353 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5354}
5355
5356fn render_role(role: &TokenRole) -> String {
5357 match role {
5358 TokenRole::Program => "program".to_string(),
5359 TokenRole::Command { name } => format!("subcommand {name}"),
5360 TokenRole::Flag {
5361 flag,
5362 spelling,
5363 negated,
5364 } => {
5365 let negated = if *negated { ", negated" } else { "" };
5366 format!("flag {} as {spelling}{negated}", flag.name)
5367 }
5368 TokenRole::Value {
5369 flag,
5370 values,
5371 attached,
5372 } => {
5373 let attached = if *attached { ", attached" } else { "" };
5374 format!("value of {} = {values:?}{attached}", flag.name)
5375 }
5376 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5377 TokenRole::Sigil { arg, sigil, values } => {
5378 format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5379 }
5380 TokenRole::Separator => "separator".to_string(),
5381 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5382 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5383 TokenRole::Restart => "restart".to_string(),
5384 TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5385 TokenRole::UnknownFlag { bound_as } => match bound_as {
5386 Some(arg) => format!("unknown flag, bound as {}", arg.name),
5387 None => "unknown flag".to_string(),
5388 },
5389 TokenRole::Refused { reason } => format!("refused: {reason}"),
5390 TokenRole::External => "external".to_string(),
5391 TokenRole::Unread => "unread".to_string(),
5392 }
5393}
5394
5395impl Debug for ParseOutput {
5396 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5397 f.debug_struct("ParseOutput")
5398 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5399 .field(
5400 "args",
5401 &self
5402 .args
5403 .iter()
5404 .map(|(a, w)| format!("{}: {w}", a.name))
5405 .collect_vec(),
5406 )
5407 .field("clauses", &self.clauses)
5408 .field(
5409 "available_flags",
5410 &self
5411 .available_flags
5412 .iter()
5413 .map(|(f, w)| format!("{f}: {w}"))
5414 .collect_vec(),
5415 )
5416 .field(
5417 "flags",
5418 &self
5419 .flags
5420 .iter()
5421 .map(|(f, w)| format!("{}: {w}", f.name))
5422 .collect_vec(),
5423 )
5424 .field("flag_awaiting_value", &self.flag_awaiting_value)
5425 .field("errors", &self.errors)
5426 .field("external", &self.external)
5427 .field(
5431 "tokens",
5432 &self.tokens.iter().map(render_token).collect_vec(),
5433 )
5434 .field(
5435 "origins",
5436 &self
5437 .flag_origins
5438 .iter()
5439 .map(|(f, o)| format!("{}: {o:?}", f.name))
5440 .chain(
5441 self.arg_origins
5442 .iter()
5443 .map(|(a, o)| format!("{}: {o:?}", a.name)),
5444 )
5445 .collect_vec(),
5446 )
5447 .field("overridden_flags", &self.overridden_flags)
5448 .finish()
5449 }
5450}
5451
5452#[cfg(test)]
5453mod tests {
5454 use super::*;
5455 use crate::SpecFlagAction;
5456
5457 fn input(words: &[&str]) -> Vec<String> {
5458 words.iter().map(|word| (*word).to_string()).collect()
5459 }
5460
5461 #[test]
5462 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5463 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5468 .parse()
5469 .unwrap();
5470
5471 for spelling in ["--version", "-V"] {
5472 let err = parse(&spec, &input(&["ex", spelling]))
5473 .expect_err("answering with a version ends the parse");
5474 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5475 }
5476 }
5477
5478 #[test]
5479 fn the_supplied_version_flag_is_the_roots_alone() {
5480 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5483 .parse()
5484 .unwrap();
5485
5486 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5487 assert_eq!(err.to_string(), "unexpected word: --version");
5488 }
5489
5490 #[test]
5491 fn no_declared_version_supplies_nothing() {
5492 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5495
5496 for spelling in ["--version", "-V"] {
5497 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5498 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5499 }
5500 }
5501
5502 #[test]
5503 fn disable_version_flag_removes_the_supplied_spellings() {
5504 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5505 .parse()
5506 .unwrap();
5507
5508 for spelling in ["--version", "-V"] {
5509 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5510 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5511 }
5512 }
5513
5514 #[test]
5515 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5516 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5519 .parse()
5520 .unwrap();
5521
5522 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5523 assert_eq!(out.flags.len(), 1);
5524
5525 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5526 assert_eq!(err.to_string(), "1.2.3");
5527 }
5528
5529 #[test]
5530 fn the_supplied_spellings_split_the_two_version_texts() {
5531 let spec: Spec =
5534 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5535 .parse()
5536 .unwrap();
5537
5538 assert_eq!(
5539 parse(&spec, &input(&["ex", "--version"]))
5540 .unwrap_err()
5541 .to_string(),
5542 "1.2.3 (abcdef)"
5543 );
5544 assert_eq!(
5545 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5546 "1.2.3"
5547 );
5548 }
5549
5550 #[test]
5551 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5552 let spec: Spec =
5557 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5558 .parse()
5559 .unwrap();
5560
5561 for token in ["-vh", "-hv"] {
5562 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5563 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5564 }
5565 for token in ["-vV", "-Vv"] {
5566 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5567 assert_eq!(err.to_string(), "1.2.3", "{token}");
5568 }
5569 }
5570
5571 #[test]
5572 fn a_bundled_help_letter_asks_for_the_short_page() {
5573 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"
5576 .parse()
5577 .unwrap();
5578
5579 let short = parse(&spec, &input(&["ex", "-vh"]))
5580 .unwrap_err()
5581 .to_string();
5582 let long = parse(&spec, &input(&["ex", "--help"]))
5583 .unwrap_err()
5584 .to_string();
5585 assert!(short.contains("Be loud"), "{short}");
5586 assert!(!short.contains("at length"), "{short}");
5587 assert!(long.contains("at length"), "{long}");
5588 }
5589
5590 #[test]
5591 fn the_bundled_version_letter_is_the_roots_alone() {
5592 let spec: Spec =
5594 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5595 .parse()
5596 .unwrap();
5597
5598 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5599 assert_eq!(err.to_string(), "unexpected word: -vV");
5600 }
5601
5602 #[test]
5603 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5604 let spec: Spec =
5607 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5608 .parse()
5609 .unwrap();
5610
5611 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5612 assert_eq!(out.flags.len(), 2);
5613 assert!(out
5614 .flags
5615 .iter()
5616 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
5617 }
5618
5619 #[test]
5620 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5621 let spec: Spec =
5622 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5623 .parse()
5624 .unwrap();
5625
5626 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5627 assert_eq!(err.to_string(), "unexpected word: -vh");
5628 }
5629
5630 #[test]
5631 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5632 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5635 .parse()
5636 .unwrap();
5637
5638 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5639 assert!(out.flags.is_empty(), "{:?}", out.flags);
5640 assert_eq!(out.args.len(), 1);
5641 }
5642
5643 fn spec_with_arg(arg: SpecArg) -> Spec {
5644 let cmd = SpecCommand::builder().name("test").arg(arg).build();
5645 Spec {
5646 name: "test".to_string(),
5647 bin: "test".to_string(),
5648 cmd,
5649 ..Default::default()
5650 }
5651 }
5652
5653 fn spec_with_flag(flag: SpecFlag) -> Spec {
5654 let cmd = SpecCommand::builder().name("test").flag(flag).build();
5655 Spec {
5656 name: "test".to_string(),
5657 bin: "test".to_string(),
5658 cmd,
5659 ..Default::default()
5660 }
5661 }
5662
5663 fn parse_with_env(
5664 spec: &Spec,
5665 words: &[&str],
5666 env: &[(&str, &str)],
5667 ) -> Result<ParseOutput, miette::Error> {
5668 let env = env
5669 .iter()
5670 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5671 .collect();
5672 Parser::new(spec).with_env(env).parse(&input(words))
5673 }
5674
5675 fn first_string_value(parsed: &ParseOutput) -> &str {
5676 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5677 return value;
5678 }
5679 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5680 return value;
5681 }
5682 panic!("expected first parsed value to be ParseValue::String");
5683 }
5684
5685 #[test]
5686 fn custom_environment_parser_dispatches_executable_views() {
5687 let spec: Spec = r#"
5688bin "ex"
5689view "runner" root="run"
5690cmd "run" {
5691 flag "--token <token>" env="TOKEN"
5692}
5693 "#
5694 .parse()
5695 .unwrap();
5696 let parsed = Parser::new(&spec)
5697 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5698 .parse(&input(&["runner"]))
5699 .unwrap();
5700
5701 assert_eq!(parsed.cmd.name, "runner");
5702 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5703 && matches!(value, ParseValue::String(value) if value == "secret")));
5704 }
5705
5706 #[test]
5707 fn an_executable_view_keeps_the_hosts_version_action() {
5708 let spec: Spec = r#"
5709bin "ex"
5710version "1.2.3"
5711flag "-V --version" action="version"
5712flag "--verbose" global=#true
5713view "runner" root="run" globals=#true
5714cmd "run"
5715 "#
5716 .parse()
5717 .unwrap();
5718
5719 let error = Parser::new(&spec)
5720 .parse(&input(&["runner", "--version"]))
5721 .expect_err("the host version action should answer before view projection");
5722 assert_eq!(error.to_string(), "1.2.3");
5723
5724 let error = Parser::new(&spec)
5725 .parse(&input(&["runner", "--verbose", "--version"]))
5726 .expect_err("the host version action should remain after a carried global");
5727 assert_eq!(error.to_string(), "1.2.3");
5728 }
5729
5730 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5731 let flag = parsed
5732 .flags
5733 .keys()
5734 .find(|flag| flag.name == name)
5735 .unwrap_or_else(|| panic!("expected flag {name}"));
5736 let value = parsed
5737 .flags
5738 .get(flag)
5739 .unwrap_or_else(|| panic!("expected value for flag {name}"));
5740 match value {
5741 ParseValue::String(value) => value,
5742 _ => panic!("expected flag {name} to be ParseValue::String"),
5743 }
5744 }
5745
5746 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5747 let err = result.expect_err("expected parser error");
5748 assert_eq!(format!("{err}"), expected);
5749 }
5750
5751 #[test]
5752 fn a_short_version_action_falls_back_to_the_long_version() {
5753 let flag = SpecFlag::builder()
5754 .short('R')
5755 .action(SpecFlagAction::Version)
5756 .build();
5757 let spec = Spec {
5758 name: "test".to_string(),
5759 bin: "test".to_string(),
5760 long_version: Some("1.2.3\ncommit abc123".to_string()),
5761 ..Default::default()
5762 };
5763 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
5764 panic!("expected version action")
5765 };
5766 assert_eq!(version, "1.2.3\ncommit abc123");
5767 }
5768
5769 #[cfg(feature = "unstable_choices_env")]
5770 fn spec_arg_choices_env(key: &str) -> Spec {
5771 spec_with_arg(
5772 SpecArg::builder()
5773 .name("env")
5774 .choices_env(key)
5775 .required(false)
5776 .build(),
5777 )
5778 }
5779
5780 #[cfg(feature = "unstable_choices_env")]
5781 fn spec_flag_choices_env(key: &str) -> Spec {
5782 spec_with_flag(
5783 SpecFlag::builder()
5784 .long("env")
5785 .arg(SpecArg::builder().name("env").choices_env(key).build())
5786 .build(),
5787 )
5788 }
5789
5790 #[test]
5791 fn test_parse() {
5792 let cmd = SpecCommand::builder()
5793 .name("test")
5794 .arg(SpecArg::builder().name("arg").build())
5795 .flag(SpecFlag::builder().long("flag").build())
5796 .build();
5797 let spec = Spec {
5798 name: "test".to_string(),
5799 bin: "test".to_string(),
5800 cmd,
5801 ..Default::default()
5802 };
5803 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5804 let parsed = parse(&spec, &input).unwrap();
5805 assert_eq!(parsed.cmds.len(), 1);
5806 assert_eq!(parsed.cmds[0].name, "test");
5807 assert_eq!(parsed.args.len(), 1);
5808 assert_eq!(parsed.flags.len(), 1);
5809 assert_eq!(parsed.available_flags.len(), 1);
5810 }
5811
5812 #[test]
5813 fn test_flag_overrides_last_occurrence_wins() {
5814 let spec: Spec = r#"
5815flag "--stdin" default=#true
5816flag "--file <file>" overrides="--stdin"
5817 "#
5818 .parse()
5819 .unwrap();
5820
5821 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5822 assert_eq!(file_wins.flags.len(), 1);
5823 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5824 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5825
5826 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5827 assert_eq!(stdin_wins.flags.len(), 1);
5828 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5829 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5830 }
5831
5832 #[test]
5833 fn test_flag_override_clears_pending_value() {
5834 let spec: Spec = r#"
5835flag "--file <file>" overrides="--stdin"
5836flag "--stdin"
5837arg "[input]"
5838 "#
5839 .parse()
5840 .unwrap();
5841
5842 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5843 assert_eq!(parsed.flags.len(), 1);
5844 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5845 assert_eq!(first_string_value(&parsed), "input.txt");
5846 }
5847
5848 #[cfg(unix)]
5849 #[test]
5850 fn a_mount_on_the_root_discovers_subcommands() {
5851 let spec: Spec = r#"
5855name "ex"
5856bin "ex"
5857cmd "declared"
5858mount run="echo 'cmd \"discovered\"'"
5859"#
5860 .parse()
5861 .unwrap();
5862
5863 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5864 assert_eq!(out.cmd.name, "discovered");
5865 }
5866
5867 #[test]
5868 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5869 let spec: Spec = r#"
5870name "ex"
5871bin "ex"
5872mount run="this command must never run"
5873cmd "declared"
5874"#
5875 .parse()
5876 .unwrap();
5877
5878 Parser::new(&spec)
5879 .with_mount_outputs(HashMap::new())
5880 .parse(&input(&["ex", "declared"]))
5881 .expect("a declared command does not resolve the mount");
5882
5883 let error = Parser::new(&spec)
5884 .with_mount_outputs(HashMap::new())
5885 .parse(&input(&["ex", "discovered"]))
5886 .unwrap_err();
5887 assert!(
5888 error
5889 .to_string()
5890 .contains("No injected output was provided for mount command"),
5891 "{error}"
5892 );
5893 }
5894
5895 #[cfg(unix)]
5896 #[test]
5897 fn completion_sees_root_mounted_commands_with_nothing_typed() {
5898 let spec: Spec = r#"
5902name "ex"
5903bin "ex"
5904cmd "declared"
5905mount run="echo 'cmd \"discovered\"'"
5906"#
5907 .parse()
5908 .unwrap();
5909
5910 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5911 assert!(
5912 out.cmd.subcommands.contains_key("discovered"),
5913 "a completion should see mounted commands; got {:?}",
5914 out.cmd.subcommands.keys().collect::<Vec<_>>()
5915 );
5916 }
5917
5918 #[cfg(unix)]
5919 #[test]
5920 fn completion_and_execution_agree_about_discovery() {
5921 let spec: Spec = r#"
5925name "ex"
5926bin "ex"
5927default_subcommand "run"
5928cmd "run" {
5929 arg "<task>"
5930}
5931mount run="exit 1"
5932"#
5933 .parse()
5934 .unwrap();
5935
5936 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5937 assert!(
5938 !out.cmd.subcommands.contains_key("discovered"),
5939 "a completion must not offer what execution will not route"
5940 );
5941
5942 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5943 assert_eq!(out.cmd.name, "run");
5944 }
5945
5946 #[cfg(unix)]
5947 #[test]
5948 fn a_default_subcommand_outranks_discovery() {
5949 let spec: Spec = r#"
5952name "ex"
5953bin "ex"
5954default_subcommand "run"
5955cmd "run" {
5956 arg "<task>"
5957}
5958mount run="exit 1"
5959"#
5960 .parse()
5961 .unwrap();
5962
5963 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5964 assert_eq!(out.cmd.name, "run");
5965 }
5966
5967 #[cfg(unix)]
5968 #[test]
5969 fn a_mount_may_ask_to_outrank_the_default() {
5970 let spec: Spec = r#"
5973name "ex"
5974bin "ex"
5975default_subcommand "run"
5976cmd "run" {
5977 arg "<task>"
5978}
5979mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5980"#
5981 .parse()
5982 .unwrap();
5983
5984 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5985 assert_eq!(out.cmd.name, "discovered");
5986
5987 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5989 assert_eq!(out.cmd.name, "run");
5990 }
5991
5992 #[cfg(unix)]
5993 #[test]
5994 fn a_flag_does_not_run_the_mount() {
5995 let spec: Spec = r#"
5999name "ex"
6000bin "ex"
6001flag "--verbose"
6002cmd "declared"
6003mount run="exit 1"
6004"#
6005 .parse()
6006 .unwrap();
6007
6008 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6009 assert_eq!(out.cmd.name, "ex");
6010 }
6011
6012 #[cfg(unix)]
6013 #[test]
6014 fn a_declared_subcommand_does_not_run_the_mount() {
6015 let spec: Spec = r#"
6019name "ex"
6020bin "ex"
6021cmd "declared"
6022mount run="exit 1"
6023"#
6024 .parse()
6025 .unwrap();
6026
6027 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6028 assert_eq!(out.cmd.name, "declared");
6029 }
6030
6031 #[test]
6032 fn a_root_mount_survives_being_written_out() {
6033 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6034 .parse()
6035 .unwrap();
6036 assert_eq!(spec.cmd.mounts.len(), 1);
6037
6038 let reparsed: Spec = spec.to_string().parse().unwrap();
6039 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6040 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6041 }
6042
6043 #[test]
6044 fn test_mount_prefix_applies_flag_overrides() {
6045 let stdin = Arc::new(
6046 SpecFlag::builder()
6047 .name("stdin")
6048 .long("stdin")
6049 .global(true)
6050 .build(),
6051 );
6052 let file = Arc::new(
6053 SpecFlag::builder()
6054 .name("file")
6055 .long("file")
6056 .arg(SpecArg::builder().name("file").build())
6057 .global(true)
6058 .overrides_with(vec!["--stdin".to_string()])
6059 .build(),
6060 );
6061 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6062
6063 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6064 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6065
6066 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6067 }
6068
6069 #[test]
6070 fn test_flag_override_suppresses_env_value() {
6071 let spec: Spec = r#"
6072flag "--stdin" env="USE_STDIN"
6073flag "--file <file>" overrides="--stdin"
6074 "#
6075 .parse()
6076 .unwrap();
6077
6078 let parsed = parse_with_env(
6079 &spec,
6080 &["test", "--file", "input.txt"],
6081 &[("USE_STDIN", "true")],
6082 )
6083 .unwrap();
6084 assert_eq!(parsed.flags.len(), 1);
6085 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6086 }
6087
6088 #[test]
6089 fn test_flag_override_suppresses_required_check() {
6090 let spec: Spec = r#"
6091flag "--stdin" required=#true
6092flag "--file <file>" overrides="--stdin"
6093 "#
6094 .parse()
6095 .unwrap();
6096
6097 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6098 assert_eq!(parsed.flags.len(), 1);
6099 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6100 }
6101
6102 #[test]
6103 fn test_flag_required_if() {
6104 let spec: Spec = r#"
6105flag "--dir <dir>"
6106flag "--file <file>" required_if="--dir"
6107 "#
6108 .parse()
6109 .unwrap();
6110
6111 parse(&spec, &input(&["test"])).unwrap();
6112 assert_parse_err(
6113 parse(&spec, &input(&["test", "--dir", "src"])),
6114 "Missing required flag: --file <file>",
6115 );
6116 parse(
6117 &spec,
6118 &input(&["test", "--dir", "src", "--file", "input.txt"]),
6119 )
6120 .unwrap();
6121 }
6122
6123 #[test]
6124 fn test_flag_required_unless() {
6125 let spec: Spec = r#"
6126flag "--stdin"
6127flag "--file <file>" required_unless="--stdin"
6128 "#
6129 .parse()
6130 .unwrap();
6131
6132 assert_parse_err(
6133 parse(&spec, &input(&["test"])),
6134 "Missing required flag: --file <file>",
6135 );
6136 parse(&spec, &input(&["test", "--stdin"])).unwrap();
6137 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6138 }
6139
6140 #[test]
6141 fn complete_required_relationship_truth_tables() {
6142 let spec: Spec = r#"
6143name "ex"
6144bin "ex"
6145flag "--mode <mode>"
6146flag "--scope <scope>"
6147flag "--token <token>" {
6148 required_if_eq "--mode" "remote"
6149}
6150flag "--approval <approval>" {
6151 required_if_eq_all "--mode" "remote" "--scope" "global"
6152}
6153flag "--input <input>" {
6154 required_unless "--stdin" "--file"
6155}
6156flag "--checksum <checksum>" {
6157 required_unless_all "--stdin" "--file"
6158}
6159flag "--stdin"
6160flag "--file <file>"
6161arg "[request]" {
6162 requires "--mode" "--scope"
6163}
6164"#
6165 .parse()
6166 .unwrap();
6167 let parse_args = |args: &[&str]| {
6168 parse(
6169 &spec,
6170 &args
6171 .iter()
6172 .map(|arg| (*arg).to_string())
6173 .collect::<Vec<_>>(),
6174 )
6175 };
6176
6177 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6178 assert!(parse_args(&[
6179 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6180 ])
6181 .is_err());
6182 parse_args(&[
6183 "ex",
6184 "--mode",
6185 "remote",
6186 "--token",
6187 "secret",
6188 "--scope",
6189 "global",
6190 "--approval",
6191 "yes",
6192 "--stdin",
6193 "--file",
6194 "in",
6195 ])
6196 .unwrap();
6197 parse_args(&[
6198 "ex",
6199 "--mode",
6200 "local",
6201 "--scope",
6202 "project",
6203 "--stdin",
6204 "--checksum",
6205 "sum",
6206 "request.json",
6207 ])
6208 .unwrap();
6209
6210 let reparsed: Spec = spec.to_string().parse().unwrap();
6211 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6212 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6213 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6214 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6215 }
6216
6217 #[test]
6218 fn test_conditional_requirements_treat_env_as_explicit() {
6219 let spec: Spec = r#"
6220flag "--dir <dir>" env="INPUT_DIR"
6221flag "--stdin" env="USE_STDIN"
6222flag "--file <file>" required_if="--dir" required_unless="--stdin"
6223 "#
6224 .parse()
6225 .unwrap();
6226
6227 assert_parse_err(
6228 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6229 "Missing required flag: --file <file>",
6230 );
6231 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6232 }
6233
6234 #[test]
6235 fn test_custom_env_does_not_fall_back_to_process_env() {
6236 assert!(std::env::var("PATH").is_ok());
6237 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6238
6239 assert_parse_err(
6240 parse_with_env(&spec, &["test"], &[]),
6241 "Missing required flag: --file <file>",
6242 );
6243 }
6244
6245 #[test]
6246 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6247 let spec: Spec = r#"
6248flag "--dir <dir>" default="src"
6249flag "--file <file>" required_if="--dir"
6250 "#
6251 .parse()
6252 .unwrap();
6253
6254 parse(&spec, &input(&["test"])).unwrap();
6255 }
6256
6257 #[test]
6258 fn test_conditional_requirements_see_overridden_flags_as_absent() {
6259 let spec: Spec = r#"
6260flag "--stdin"
6261flag "--dir <dir>" overrides="--stdin"
6262flag "--file <file>" required_unless="--stdin"
6263 "#
6264 .parse()
6265 .unwrap();
6266
6267 assert_parse_err(
6268 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6269 "Missing required flag: --file <file>",
6270 );
6271 }
6272
6273 #[test]
6274 fn short_flag_is_one_character_not_one_byte() {
6275 let spec = spec_with_flag(
6279 SpecFlag::builder()
6280 .short('磨')
6281 .long("polish")
6282 .arg(SpecArg::builder().name("opt").build())
6283 .build(),
6284 );
6285 let attached = Parser::new(&spec)
6286 .parse(&input(&["test", "-磨VALUE"]))
6287 .unwrap();
6288 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6289 let detached = Parser::new(&spec)
6290 .parse(&input(&["test", "-磨", "V"]))
6291 .unwrap();
6292 assert_eq!(flag_string_value(&detached, "polish"), "V");
6293 }
6294
6295 #[test]
6296 fn test_as_env() {
6297 let cmd = SpecCommand::builder()
6298 .name("test")
6299 .arg(SpecArg::builder().name("arg").build())
6300 .flag(SpecFlag::builder().long("flag").build())
6301 .flag(
6302 SpecFlag::builder()
6303 .long("force")
6304 .negate("--no-force")
6305 .build(),
6306 )
6307 .build();
6308 let spec = Spec {
6309 name: "test".to_string(),
6310 bin: "test".to_string(),
6311 cmd,
6312 ..Default::default()
6313 };
6314 let input = vec![
6315 "test".to_string(),
6316 "--flag".to_string(),
6317 "--no-force".to_string(),
6318 ];
6319 let parsed = parse(&spec, &input).unwrap();
6320 let env = parsed.as_env();
6321 assert_eq!(env.len(), 2);
6322 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6323 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6324 }
6325
6326 #[test]
6327 fn test_arg_env_var() {
6328 let cmd = SpecCommand::builder()
6329 .name("test")
6330 .arg(
6331 SpecArg::builder()
6332 .name("input")
6333 .env("TEST_ARG_INPUT")
6334 .required(true)
6335 .build(),
6336 )
6337 .build();
6338 let spec = Spec {
6339 name: "test".to_string(),
6340 bin: "test".to_string(),
6341 cmd,
6342 ..Default::default()
6343 };
6344
6345 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
6347
6348 let input = vec!["test".to_string()];
6349 let parsed = parse(&spec, &input).unwrap();
6350
6351 assert_eq!(parsed.args.len(), 1);
6352 let arg = parsed.args.keys().next().unwrap();
6353 assert_eq!(arg.name, "input");
6354 let value = parsed.args.values().next().unwrap();
6355 assert_eq!(value.to_string(), "test_file.txt");
6356
6357 std::env::remove_var("TEST_ARG_INPUT");
6359 }
6360
6361 #[test]
6362 fn test_flag_env_var_with_arg() {
6363 let cmd = SpecCommand::builder()
6364 .name("test")
6365 .flag(
6366 SpecFlag::builder()
6367 .long("output")
6368 .env("TEST_FLAG_OUTPUT")
6369 .arg(SpecArg::builder().name("file").build())
6370 .build(),
6371 )
6372 .build();
6373 let spec = Spec {
6374 name: "test".to_string(),
6375 bin: "test".to_string(),
6376 cmd,
6377 ..Default::default()
6378 };
6379
6380 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
6382
6383 let input = vec!["test".to_string()];
6384 let parsed = parse(&spec, &input).unwrap();
6385
6386 assert_eq!(parsed.flags.len(), 1);
6387 let flag = parsed.flags.keys().next().unwrap();
6388 assert_eq!(flag.name, "output");
6389 let value = parsed.flags.values().next().unwrap();
6390 assert_eq!(value.to_string(), "output.txt");
6391
6392 std::env::remove_var("TEST_FLAG_OUTPUT");
6394 }
6395
6396 #[test]
6397 fn test_flag_env_var_boolean() {
6398 let cmd = SpecCommand::builder()
6399 .name("test")
6400 .flag(
6401 SpecFlag::builder()
6402 .long("verbose")
6403 .env("TEST_FLAG_VERBOSE")
6404 .build(),
6405 )
6406 .build();
6407 let spec = Spec {
6408 name: "test".to_string(),
6409 bin: "test".to_string(),
6410 cmd,
6411 ..Default::default()
6412 };
6413
6414 std::env::set_var("TEST_FLAG_VERBOSE", "true");
6416
6417 let input = vec!["test".to_string()];
6418 let parsed = parse(&spec, &input).unwrap();
6419
6420 assert_eq!(parsed.flags.len(), 1);
6421 let flag = parsed.flags.keys().next().unwrap();
6422 assert_eq!(flag.name, "verbose");
6423 let value = parsed.flags.values().next().unwrap();
6424 assert_eq!(value.to_string(), "true");
6425
6426 std::env::remove_var("TEST_FLAG_VERBOSE");
6428 }
6429
6430 #[test]
6431 fn test_env_var_precedence() {
6432 let cmd = SpecCommand::builder()
6434 .name("test")
6435 .arg(
6436 SpecArg::builder()
6437 .name("input")
6438 .env("TEST_PRECEDENCE_INPUT")
6439 .required(true)
6440 .build(),
6441 )
6442 .build();
6443 let spec = Spec {
6444 name: "test".to_string(),
6445 bin: "test".to_string(),
6446 cmd,
6447 ..Default::default()
6448 };
6449
6450 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
6452
6453 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6454 let parsed = parse(&spec, &input).unwrap();
6455
6456 assert_eq!(parsed.args.len(), 1);
6457 let value = parsed.args.values().next().unwrap();
6458 assert_eq!(value.to_string(), "cli_file.txt");
6460
6461 std::env::remove_var("TEST_PRECEDENCE_INPUT");
6463 }
6464
6465 #[test]
6466 fn test_flag_var_true_with_single_default() {
6467 let cmd = SpecCommand::builder()
6469 .name("test")
6470 .flag(
6471 SpecFlag::builder()
6472 .long("foo")
6473 .var(true)
6474 .arg(SpecArg::builder().name("foo").build())
6475 .default_value("bar")
6476 .build(),
6477 )
6478 .build();
6479 let spec = Spec {
6480 name: "test".to_string(),
6481 bin: "test".to_string(),
6482 cmd,
6483 ..Default::default()
6484 };
6485
6486 let input = vec!["test".to_string()];
6488 let parsed = parse(&spec, &input).unwrap();
6489
6490 assert_eq!(parsed.flags.len(), 1);
6491 let flag = parsed.flags.keys().next().unwrap();
6492 assert_eq!(flag.name, "foo");
6493 let value = parsed.flags.values().next().unwrap();
6494 match value {
6496 ParseValue::MultiString(v) => {
6497 assert_eq!(v.len(), 1);
6498 assert_eq!(v[0], "bar");
6499 }
6500 _ => panic!("Expected MultiString, got {:?}", value),
6501 }
6502 }
6503
6504 #[test]
6505 fn test_flag_var_true_with_multiple_defaults() {
6506 let cmd = SpecCommand::builder()
6508 .name("test")
6509 .flag(
6510 SpecFlag::builder()
6511 .long("foo")
6512 .var(true)
6513 .arg(SpecArg::builder().name("foo").build())
6514 .default_values(["xyz", "bar"])
6515 .build(),
6516 )
6517 .build();
6518 let spec = Spec {
6519 name: "test".to_string(),
6520 bin: "test".to_string(),
6521 cmd,
6522 ..Default::default()
6523 };
6524
6525 let input = vec!["test".to_string()];
6527 let parsed = parse(&spec, &input).unwrap();
6528
6529 assert_eq!(parsed.flags.len(), 1);
6530 let value = parsed.flags.values().next().unwrap();
6531 match value {
6533 ParseValue::MultiString(v) => {
6534 assert_eq!(v.len(), 2);
6535 assert_eq!(v[0], "xyz");
6536 assert_eq!(v[1], "bar");
6537 }
6538 _ => panic!("Expected MultiString, got {:?}", value),
6539 }
6540 }
6541
6542 #[test]
6543 fn test_flag_var_false_with_default_remains_string() {
6544 let cmd = SpecCommand::builder()
6546 .name("test")
6547 .flag(
6548 SpecFlag::builder()
6549 .long("foo")
6550 .var(false) .arg(SpecArg::builder().name("foo").build())
6552 .default_value("bar")
6553 .build(),
6554 )
6555 .build();
6556 let spec = Spec {
6557 name: "test".to_string(),
6558 bin: "test".to_string(),
6559 cmd,
6560 ..Default::default()
6561 };
6562
6563 let input = vec!["test".to_string()];
6565 let parsed = parse(&spec, &input).unwrap();
6566
6567 assert_eq!(parsed.flags.len(), 1);
6568 let value = parsed.flags.values().next().unwrap();
6569 match value {
6571 ParseValue::String(s) => {
6572 assert_eq!(s, "bar");
6573 }
6574 _ => panic!("Expected String, got {:?}", value),
6575 }
6576 }
6577
6578 #[test]
6579 fn test_arg_var_true_with_single_default() {
6580 let cmd = SpecCommand::builder()
6582 .name("test")
6583 .arg(
6584 SpecArg::builder()
6585 .name("files")
6586 .var(true)
6587 .default_value("default.txt")
6588 .required(false)
6589 .build(),
6590 )
6591 .build();
6592 let spec = Spec {
6593 name: "test".to_string(),
6594 bin: "test".to_string(),
6595 cmd,
6596 ..Default::default()
6597 };
6598
6599 let input = vec!["test".to_string()];
6601 let parsed = parse(&spec, &input).unwrap();
6602
6603 assert_eq!(parsed.args.len(), 1);
6604 let value = parsed.args.values().next().unwrap();
6605 match value {
6607 ParseValue::MultiString(v) => {
6608 assert_eq!(v.len(), 1);
6609 assert_eq!(v[0], "default.txt");
6610 }
6611 _ => panic!("Expected MultiString, got {:?}", value),
6612 }
6613 }
6614
6615 #[test]
6616 fn test_arg_var_true_with_multiple_defaults() {
6617 let cmd = SpecCommand::builder()
6619 .name("test")
6620 .arg(
6621 SpecArg::builder()
6622 .name("files")
6623 .var(true)
6624 .default_values(["file1.txt", "file2.txt"])
6625 .required(false)
6626 .build(),
6627 )
6628 .build();
6629 let spec = Spec {
6630 name: "test".to_string(),
6631 bin: "test".to_string(),
6632 cmd,
6633 ..Default::default()
6634 };
6635
6636 let input = vec!["test".to_string()];
6638 let parsed = parse(&spec, &input).unwrap();
6639
6640 assert_eq!(parsed.args.len(), 1);
6641 let value = parsed.args.values().next().unwrap();
6642 match value {
6644 ParseValue::MultiString(v) => {
6645 assert_eq!(v.len(), 2);
6646 assert_eq!(v[0], "file1.txt");
6647 assert_eq!(v[1], "file2.txt");
6648 }
6649 _ => panic!("Expected MultiString, got {:?}", value),
6650 }
6651 }
6652
6653 #[test]
6654 fn test_arg_var_false_with_default_remains_string() {
6655 let cmd = SpecCommand::builder()
6657 .name("test")
6658 .arg(
6659 SpecArg::builder()
6660 .name("file")
6661 .var(false)
6662 .default_value("default.txt")
6663 .required(false)
6664 .build(),
6665 )
6666 .build();
6667 let spec = Spec {
6668 name: "test".to_string(),
6669 bin: "test".to_string(),
6670 cmd,
6671 ..Default::default()
6672 };
6673
6674 let input = vec!["test".to_string()];
6676 let parsed = parse(&spec, &input).unwrap();
6677
6678 assert_eq!(parsed.args.len(), 1);
6679 let value = parsed.args.values().next().unwrap();
6680 match value {
6682 ParseValue::String(s) => {
6683 assert_eq!(s, "default.txt");
6684 }
6685 _ => panic!("Expected String, got {:?}", value),
6686 }
6687 }
6688
6689 #[test]
6690 fn test_scalar_defaults_validate_only_first_default_choice() {
6691 let specs = [
6692 spec_with_arg(
6693 SpecArg::builder()
6694 .name("env")
6695 .var(false)
6696 .default_values(["dev", "prod"])
6697 .choices(["dev"])
6698 .required(false)
6699 .build(),
6700 ),
6701 spec_with_flag(
6702 SpecFlag::builder()
6703 .long("env")
6704 .arg(
6705 SpecArg::builder()
6706 .name("env")
6707 .default_values(["dev", "prod"])
6708 .choices(["dev"])
6709 .build(),
6710 )
6711 .build(),
6712 ),
6713 ];
6714
6715 for spec in specs {
6716 let parsed = parse(&spec, &input(&["test"])).unwrap();
6717 assert_eq!(first_string_value(&parsed), "dev");
6718 }
6719 }
6720
6721 #[test]
6722 fn a_delimiter_turns_one_word_into_several_values() {
6723 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6724 .parse()
6725 .unwrap();
6726
6727 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6728 let multi = |value: &ParseValue| match value {
6729 ParseValue::MultiString(values) => values.clone(),
6730 other => panic!("expected several values, got {other:?}"),
6731 };
6732 let tags = parsed
6733 .flags
6734 .iter()
6735 .find(|(f, _)| f.name == "tags")
6736 .map(|(_, v)| v)
6737 .unwrap();
6738 assert_eq!(multi(tags), vec!["a", "b", "c"]);
6739 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6740 }
6741
6742 #[test]
6743 fn a_positional_splits_before_its_choices_are_asked() {
6744 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
6748 .parse()
6749 .unwrap();
6750
6751 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6752
6753 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6754 let message = err.to_string();
6755 assert!(message.contains("nowhere"), "{message}");
6756 assert!(
6757 !message.contains("src:nowhere"),
6758 "the bad half should be named, not the whole word: {message}"
6759 );
6760 }
6761
6762 #[test]
6763 fn a_split_value_is_counted_and_judged_as_values() {
6764 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
6768 .parse()
6769 .unwrap();
6770
6771 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6772 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6773 assert!(err.to_string().contains("staging"), "{err}");
6774 assert!(
6775 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6776 "three values should breach var_max=2"
6777 );
6778 }
6779
6780 #[test]
6781 fn a_split_bound_counts_one_occurrence_at_a_time() {
6782 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
6787 .parse()
6788 .unwrap();
6789
6790 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
6791 assert!(
6792 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
6793 "three values out of one word is still three values"
6794 );
6795 parse(
6798 &spec,
6799 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
6800 )
6801 .expect("two per occurrence, twice, is within the bound");
6802 }
6803
6804 #[test]
6805 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
6806 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
6807 .parse()
6808 .unwrap();
6809
6810 parse(
6811 &spec,
6812 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
6813 )
6814 .expect("each occurrence satisfies the bound independently");
6815
6816 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
6817 assert!(
6818 error
6819 .to_string()
6820 .contains("requires at least 2 value(s), got 1"),
6821 "{error:?}"
6822 );
6823 }
6824
6825 #[test]
6826 fn an_exclusive_flag_has_to_be_alone() {
6827 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6828 .parse()
6829 .unwrap();
6830
6831 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6832
6833 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6835 assert!(err.to_string().contains("on its own"), "{err}");
6836
6837 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6840 assert!(err.to_string().contains("on its own"), "{err}");
6841
6842 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6844 }
6845
6846 #[test]
6847 fn an_exclusive_flag_conflicts_with_clause_arguments() {
6848 let spec: Spec = r#"name "ex"
6849bin "ex"
6850flag "--dump" exclusive=#true
6851clause "tasks" separator=":::" {
6852 arg "<task>"
6853}
6854"#
6855 .parse()
6856 .unwrap();
6857
6858 let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6859 assert!(err.to_string().contains("on its own"), "{err}");
6860 }
6861
6862 #[test]
6863 fn a_partial_parse_reports_the_next_implicit_clause_argument() {
6864 let spec: Spec = r#"name "ex"
6865bin "ex"
6866clause "tools" {
6867 arg "<tool@version>"
6868}
6869"#
6870 .parse()
6871 .unwrap();
6872
6873 let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
6874
6875 assert_eq!(
6876 parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
6877 Some("tool@version")
6878 );
6879 }
6880
6881 #[test]
6882 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6883 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6886 .parse()
6887 .unwrap();
6888
6889 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6890 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6891 }
6892
6893 #[test]
6894 fn an_exclusive_flag_bypasses_required_siblings() {
6895 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6896 .parse()
6897 .unwrap();
6898
6899 parse(&spec, &input(&["ex", "--dump"]))
6900 .expect("exclusive is the command's requiredness escape");
6901 assert!(parse(
6902 &spec,
6903 &input(&["ex", "--dump", "--out", "somewhere", "target"])
6904 )
6905 .is_err());
6906 }
6907
6908 #[test]
6909 fn an_environment_value_counts_for_an_exclusive_flag() {
6910 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6911 .parse()
6912 .unwrap();
6913
6914 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6915 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6916 }
6917
6918 #[test]
6919 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6920 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6921 .parse()
6922 .unwrap();
6923
6924 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6925 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6926 }
6927
6928 #[test]
6929 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6930 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6931 .parse()
6932 .unwrap();
6933
6934 parse(&spec, &input(&["ex", "run", "--clean"]))
6935 .expect("the child flag is alone within the child command");
6936 assert!(
6937 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6938 "the parent flag still conflicts with selecting the child"
6939 );
6940 }
6941
6942 #[test]
6943 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6944 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6945 .parse()
6946 .unwrap();
6947
6948 parse(&spec, &input(&["ex", "run", "--clean"]))
6949 .expect("the child-local exclusive flag is alone inside the child command");
6950 assert!(
6951 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6952 "the ancestor spelling still conflicts with selecting the child"
6953 );
6954 }
6955
6956 #[test]
6957 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6958 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
6959 .parse()
6960 .unwrap();
6961
6962 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6963 assert!(
6964 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6965 "the distinct parent declaration is still company despite sharing a name"
6966 );
6967 }
6968
6969 #[test]
6970 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6971 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6972 .parse()
6973 .unwrap();
6974
6975 parse(&spec, &input(&["ex", "run", "--clean"]))
6976 .expect("the child exclusive flag is valid alone");
6977 assert!(
6978 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6979 "merging with the inherited global must not discard child exclusivity"
6980 );
6981 }
6982
6983 #[test]
6984 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6985 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6986 .parse()
6987 .unwrap();
6988
6989 parse(&spec, &input(&["ex", "run", "--clean"]))
6990 .expect("the typed long form belongs to the child declaration");
6991 assert!(
6992 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6993 "the inherited short form still belongs to the ancestor"
6994 );
6995 assert!(
6996 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6997 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6998 );
6999 }
7000
7001 #[test]
7002 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7003 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
7004 .parse()
7005 .unwrap();
7006
7007 parse(&spec, &input(&["ex", "run", "--clean"]))
7008 .expect("the child's spelling does not activate the orphan ancestor alias");
7009 assert!(
7010 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7011 "the inherited short alias still belongs to the ancestor exclusive flag"
7012 );
7013 }
7014
7015 #[test]
7016 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7017 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"
7018 .parse()
7019 .unwrap();
7020
7021 assert!(
7022 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7023 "the inherited negated alias still belongs to the ancestor exclusive flag"
7024 );
7025 }
7026
7027 #[test]
7028 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7029 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"
7034 .parse()
7035 .unwrap();
7036
7037 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7038 assert!(
7039 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7040 "one unrelated alias collision cannot disown the child from its own flag"
7041 );
7042 }
7043
7044 #[test]
7045 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7046 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7052 .parse()
7053 .unwrap();
7054 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7055 .expect("the ancestor owns this occurrence, and it is not exclusive");
7056 assert!(
7057 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7058 "after the subcommand word the child's declaration is in scope, and it is exclusive"
7059 );
7060
7061 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7062 .parse()
7063 .unwrap();
7064 assert!(
7065 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7066 "the same rule, with an exclusive ancestor: selecting the child is company for it"
7067 );
7068 }
7069
7070 #[test]
7071 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7072 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
7076 .parse()
7077 .unwrap();
7078
7079 assert!(
7080 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7081 "the orphan ancestor alias is still the ancestor's exclusive flag"
7082 );
7083 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7084 .expect("the child's own spelling drops the exclusivity the child did not restate");
7085 }
7086
7087 #[test]
7088 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7089 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7093 .parse()
7094 .unwrap();
7095
7096 assert!(
7097 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7098 "the child spelling is exclusive whatever it was typed beside"
7099 );
7100 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7101 .expect("the ancestor's own spelling was never exclusive");
7102 }
7103
7104 #[test]
7105 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7106 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"
7110 .parse()
7111 .unwrap();
7112
7113 assert!(
7114 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7115 "the child added exclusivity the environment value has to honor"
7116 );
7117
7118 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"
7119 .parse()
7120 .unwrap();
7121
7122 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7123 .expect("the child dropped the exclusivity, and the environment value follows it");
7124 }
7125
7126 #[test]
7127 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7128 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"
7131 .parse()
7132 .unwrap();
7133
7134 parse(&spec, &input(&["ex", "run", "--clean"]))
7135 .expect("a merged child exclusive flag is still the command's requiredness escape");
7136 }
7137
7138 #[test]
7139 fn a_group_allows_one_member_and_refuses_two() {
7140 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7141 .parse()
7142 .unwrap();
7143
7144 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7146 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7147
7148 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7149 assert!(err.to_string().contains("group input"), "{err}");
7150 }
7151
7152 #[test]
7153 fn positional_selectors_work_in_conflicts_and_groups() {
7154 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7155 .parse()
7156 .unwrap();
7157 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7158 .expect("the flag alone is valid");
7159 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7160 assert!(parse(
7161 &conflicts,
7162 &input(&["ex", "--from-file", "vars.env", "literal"])
7163 )
7164 .is_err());
7165
7166 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7167 .parse()
7168 .unwrap();
7169 assert!(parse(
7170 &positional_source,
7171 &input(&["ex", "--from-file", "vars.env", "literal"])
7172 )
7173 .is_err());
7174
7175 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7176 .parse()
7177 .unwrap();
7178 assert!(parse(&group, &input(&["ex"])).is_err());
7179 parse(&group, &input(&["ex", "target-name"]))
7180 .expect("a positional satisfies a required group");
7181 assert!(parse(
7182 &group,
7183 &input(&["ex", "--file", "input.txt", "target-name"])
7184 )
7185 .is_err());
7186 }
7187
7188 #[test]
7189 fn a_required_group_needs_one_of_its_members() {
7190 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7191 .parse()
7192 .unwrap();
7193
7194 let err = parse(&spec, &input(&["ex"])).unwrap_err();
7195 assert!(err.to_string().contains("--file, --url"), "{err}");
7198 assert!(err.to_string().contains("input"), "{err}");
7199
7200 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7201 }
7202
7203 #[test]
7204 fn a_multiple_group_only_polices_requiredness() {
7205 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7208 .parse()
7209 .unwrap();
7210
7211 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7212 assert!(parse(&spec, &input(&["ex"])).is_err());
7213 }
7214
7215 #[test]
7216 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7217 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7223 .parse()
7224 .unwrap();
7225
7226 parse(&spec, &input(&["ex"])).expect("the default fills the group");
7227 parse(&spec, &input(&["ex", "--url", "u"]))
7228 .expect("the default must not conflict with the flag the user typed");
7229 }
7230
7231 #[test]
7232 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7233 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7236 .parse()
7237 .unwrap();
7238
7239 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7240 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7241
7242 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7244 assert!(err.to_string().contains("group input"), "{err}");
7245 }
7246
7247 #[test]
7248 fn a_group_reads_the_environment_as_given() {
7249 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7252 .parse()
7253 .unwrap();
7254
7255 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7256 }
7257
7258 #[test]
7259 fn a_requirement_names_the_flag_that_is_missing() {
7260 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7263 .parse()
7264 .unwrap();
7265
7266 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7267 assert!(
7268 err.to_string().contains("format"),
7269 "the missing flag should be named: {err}"
7270 );
7271
7272 for words in [
7274 &["ex", "--out", "a.txt", "--format", "json"][..],
7275 &["ex", "--format", "json", "--out", "a.txt"][..],
7276 ] {
7277 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7278 }
7279
7280 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7283 }
7284
7285 #[test]
7286 fn a_value_activates_only_its_conditional_requirement() {
7287 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"
7288 .parse()
7289 .unwrap();
7290
7291 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7292 .expect("an unrelated value requires nothing");
7293
7294 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7295 assert!(key.to_string().contains("key"), "{key}");
7296 parse(
7297 &spec,
7298 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7299 )
7300 .expect("the matching requirement is satisfied");
7301
7302 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7303 assert!(token.to_string().contains("token"), "{token}");
7304 }
7305
7306 #[test]
7307 fn conditional_requirements_read_explicit_env_but_not_defaults() {
7308 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"
7309 .parse()
7310 .unwrap();
7311 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7312 assert!(err.to_string().contains("key"), "{err}");
7313
7314 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"
7315 .parse()
7316 .unwrap();
7317 parse(&from_default, &input(&["ex"]))
7318 .expect("a default is not an explicit conditional value");
7319 }
7320
7321 #[test]
7322 fn command_line_values_override_env_for_conditional_requirements() {
7323 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7324 .parse()
7325 .unwrap();
7326
7327 parse_with_env(
7328 &spec,
7329 &["ex", "--config", "ordinary.toml"],
7330 &[("EX_CONFIG", "special.toml")],
7331 )
7332 .expect("the command-line value takes precedence over the environment");
7333 }
7334
7335 #[test]
7336 fn conditional_requirements_normalize_boolean_env_values() {
7337 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7338 .parse()
7339 .unwrap();
7340
7341 for value in ["1", "true", "True", "TRUE"] {
7342 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7343 assert!(err.to_string().contains("key"), "{value}: {err}");
7344 }
7345 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7346 .expect("a false environment value does not activate a true condition");
7347 }
7348
7349 #[test]
7350 fn a_default_satisfies_a_requirement() {
7351 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7355 .parse()
7356 .unwrap();
7357
7358 parse(&spec, &input(&["ex", "--out", "a.txt"]))
7359 .expect("a defaulted flag is not a missing one");
7360 }
7361
7362 #[test]
7363 fn a_present_flag_binds_a_conditional_default() {
7364 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7365 .parse()
7366 .unwrap();
7367
7368 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7369 assert_eq!(
7370 with.as_env().get("usage_bin_names").map(String::as_str),
7371 Some("true")
7372 );
7373
7374 let without = parse(&spec, &input(&["ex"])).unwrap();
7375 assert!(
7376 !without.as_env().contains_key("usage_bin_names"),
7377 "IsPresent does nothing when the selector is absent"
7378 );
7379 }
7380
7381 #[test]
7382 fn an_equals_condition_binds_a_conditional_default() {
7383 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7384 .parse()
7385 .unwrap();
7386
7387 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7388 assert_eq!(
7389 json.as_env().get("usage_style").map(String::as_str),
7390 Some("pretty")
7391 );
7392 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7393 assert!(!yaml.as_env().contains_key("usage_style"));
7394 }
7395
7396 #[test]
7397 fn an_equals_condition_reads_a_negated_flag() {
7398 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7399 .parse()
7400 .unwrap();
7401
7402 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7403 assert_eq!(
7404 off.as_env().get("usage_pretty").map(String::as_str),
7405 Some("true")
7406 );
7407 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7408 assert!(
7409 !on.as_env().contains_key("usage_pretty"),
7410 "--json is true, so when=false should miss"
7411 );
7412 }
7413
7414 #[test]
7415 fn the_first_matching_conditional_default_wins() {
7416 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"
7417 .parse()
7418 .unwrap();
7419
7420 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7421 assert_eq!(
7422 out.as_env().get("usage_style").map(String::as_str),
7423 Some("compact")
7424 );
7425 }
7426
7427 #[test]
7428 fn argv_and_env_suppress_a_conditional_default() {
7429 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7430 .parse()
7431 .unwrap();
7432
7433 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7434 assert_eq!(
7435 from_env.as_env().get("usage_bin_names").map(String::as_str),
7436 Some("false"),
7437 "the target's environment wins over default_if"
7438 );
7439 }
7440
7441 #[test]
7442 fn a_sibling_env_activates_a_conditional_default() {
7443 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7444 .parse()
7445 .unwrap();
7446
7447 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7448 assert_eq!(
7449 out.as_env().get("usage_bin_names").map(String::as_str),
7450 Some("true")
7451 );
7452 }
7453
7454 #[test]
7455 fn a_default_does_not_activate_a_conditional_default() {
7456 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7460 .parse()
7461 .unwrap();
7462
7463 let out = parse(&spec, &input(&["ex"])).unwrap();
7464 assert_eq!(
7465 out.as_env().get("usage_json").map(String::as_str),
7466 Some("true")
7467 );
7468 assert!(
7469 !out.as_env().contains_key("usage_pretty"),
7470 "a default is not an explicit value for default_if"
7471 );
7472 }
7473
7474 #[test]
7475 fn a_conditional_default_does_not_activate_requires_if() {
7476 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"
7477 .parse()
7478 .unwrap();
7479
7480 parse(&spec, &input(&["ex", "--json"]))
7481 .expect("a default_if value is not explicit for requires_if");
7482 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7483 }
7484
7485 #[test]
7486 fn a_conditional_default_satisfies_a_requirement() {
7487 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7488 .parse()
7489 .unwrap();
7490
7491 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7492 .expect("default_if fills the required flag");
7493 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7494 }
7495
7496 #[test]
7497 fn an_environment_value_satisfies_a_requirement() {
7498 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7499 .parse()
7500 .unwrap();
7501
7502 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7503 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7504 .expect("the environment supplies it");
7505 }
7506
7507 #[test]
7508 fn a_requirement_is_satisfied_by_a_short_form() {
7509 let spec: Spec =
7513 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7514 .parse()
7515 .unwrap();
7516
7517 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7518
7519 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7520 assert!(err.to_string().contains("key"), "{err}");
7521 }
7522
7523 #[test]
7524 fn conflicting_flags_are_rejected_in_either_order() {
7525 let spec: Spec =
7529 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7530 .parse()
7531 .unwrap();
7532
7533 for words in [
7534 &["ex", "--file", "a.txt", "--stdin"][..],
7535 &["ex", "--stdin", "--file", "a.txt"][..],
7536 ] {
7537 let err = parse(&spec, &input(words)).unwrap_err();
7538 assert!(
7539 err.to_string().contains("conflicts with --stdin"),
7540 "{words:?} should be refused: {err}"
7541 );
7542 }
7543
7544 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7546 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7547 }
7548
7549 #[test]
7550 fn unknown_flags_are_values_by_default() {
7551 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7554 .parse()
7555 .unwrap();
7556 let out = parse(
7557 &spec,
7558 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7559 )
7560 .unwrap();
7561 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7562 assert_eq!(out.args[rest].to_string(), "--wat x");
7563 }
7564
7565 #[test]
7566 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7567 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7568 .parse()
7569 .unwrap();
7570 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7571 .expect("a repeat is a correction by default");
7572 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7573 assert_eq!(out.flags[jobs].to_string(), "2");
7574 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7575 .expect("switches use the same default");
7576
7577 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7578 .parse()
7579 .unwrap();
7580 for words in [
7581 &["ex", "--jobs", "1", "--jobs", "2"][..],
7582 &["ex", "--verbose", "--verbose"][..],
7583 ] {
7584 let err = parse(&strict, &input(words)).unwrap_err();
7585 assert!(
7586 err.to_string().contains("cannot be used multiple times"),
7587 "{err}"
7588 );
7589 }
7590
7591 let reparsed: Spec = strict.to_string().parse().unwrap();
7592 assert!(!reparsed.cmd.args_override_self);
7593 }
7594
7595 #[test]
7596 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7597 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7598 .parse()
7599 .unwrap();
7600
7601 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7602 .expect("opposite forms override each other");
7603 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7604 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7605
7606 for words in [
7607 &["ex", "--color", "--color"][..],
7608 &["ex", "--no-color", "--no-color"][..],
7609 ] {
7610 let err = parse(&spec, &input(words)).unwrap_err();
7611 assert!(err.to_string().contains("cannot be used multiple times"));
7612 }
7613 }
7614
7615 #[test]
7616 fn strict_global_flags_may_repeat_across_command_levels() {
7617 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"
7618 .parse()
7619 .unwrap();
7620
7621 let out = parse(
7622 &spec,
7623 &input(&[
7624 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7625 ]),
7626 )
7627 .expect("an inherited global is allowed once at each command level");
7628 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7629 assert_eq!(out.flags[jobs].to_string(), "2");
7630
7631 for words in [
7632 &["ex", "--color", "--color", "run", "--no-color"][..],
7633 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7634 ] {
7635 let err = parse(&spec, &input(words)).unwrap_err();
7636 assert!(err.to_string().contains("cannot be used multiple times"));
7637 }
7638 }
7639
7640 #[test]
7641 fn a_subcommand_can_negate_only_its_parents_requirements() {
7642 let base = r#"name "ex"
7643bin "ex"
7644flag "--config" required=#true
7645flag "--mode" requires="--config"
7646flag "--other"
7647arg "<input>"
7648group "source" "--config" "--other" required=#true
7649cmd "run" { flag "--child" required=#true }
7650"#;
7651 let strict: Spec = base.parse().unwrap();
7652 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7653 let message = err.to_string();
7654 assert!(
7655 message.contains("input") || message.contains("config"),
7656 "{message}"
7657 );
7658
7659 let negated: Spec = base
7660 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7661 .parse()
7662 .unwrap();
7663 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7664 assert!(
7665 err.to_string().contains("child"),
7666 "the selected command keeps its own requirements: {err}"
7667 );
7668
7669 let mut child_optional = negated.clone();
7670 child_optional.cmd.subcommands["run"].flags[0].required = false;
7671 parse(&child_optional, &input(&["ex", "run"]))
7672 .expect("the child selection satisfies all parent requirements");
7673 parse(&child_optional, &input(&["ex", "--mode", "run"]))
7674 .expect("parent requires relationships are negated too");
7675 }
7676
7677 #[test]
7678 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7679 let spec: Spec = r#"name "ex"
7680bin "ex"
7681args_conflicts_with_subcommands #true
7682flag "--verbose"
7683cmd "run"
7684"#
7685 .parse()
7686 .unwrap();
7687
7688 parse(&spec, &input(&["ex", "run"]))
7689 .expect("the subcommand is valid without a parent argument");
7690 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7691 assert!(
7692 err.to_string().contains("cannot be used with arguments"),
7693 "{err}"
7694 );
7695 }
7696
7697 #[test]
7698 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7699 let base = r#"name "ex"
7700bin "ex"
7701flag "--values <value>..."
7702cmd "run"
7703"#;
7704 let plain: Spec = base.parse().unwrap();
7705 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7706 assert_eq!(out.cmd.name, "ex");
7707
7708 let precedence: Spec = base
7709 .replacen(
7710 "bin \"ex\"",
7711 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7712 1,
7713 )
7714 .parse()
7715 .unwrap();
7716 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7717 assert_eq!(out.cmd.name, "run");
7718 }
7719
7720 #[test]
7721 fn a_required_positional_can_follow_an_unfilled_optional_one() {
7722 let base = r#"name "ex"
7723bin "ex"
7724arg "[optional]"
7725arg "<required>"
7726"#;
7727 let plain: Spec = base.parse().unwrap();
7728 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7729 assert!(err.to_string().contains("required"), "{err}");
7730
7731 let enabled: Spec = base
7732 .replacen(
7733 "bin \"ex\"",
7734 "bin \"ex\"\nallow_missing_positional #true",
7735 1,
7736 )
7737 .parse()
7738 .unwrap();
7739 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7740 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7741 let value = &out
7742 .args
7743 .iter()
7744 .find(|(arg, _)| arg.name == "required")
7745 .unwrap()
7746 .1;
7747 assert!(matches!(value, ParseValue::String(value) if value == "value"));
7748 }
7749
7750 #[test]
7751 fn sigil_args_do_not_block_optional_positional_skipping() {
7752 let spec: Spec = r#"name "ex"
7753bin "ex"
7754allow_missing_positional #true
7755arg "[optional]"
7756arg "[tool]" sigil="@"
7757arg "<required>"
7758"#
7759 .parse()
7760 .unwrap();
7761
7762 let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7763 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7764 let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7765 let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7766 assert_eq!(out.args[tool].to_string(), "node");
7767 assert_eq!(out.args[required].to_string(), "value");
7768 }
7769
7770 #[test]
7771 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7772 let spec: Spec =
7773 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7774 .parse()
7775 .unwrap();
7776 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7777 assert!(
7778 err.to_string().contains("--wat"),
7779 "the message should name the token: {err}"
7780 );
7781
7782 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
7785 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7786 assert_eq!(out.args[rest].to_string(), "-1");
7787
7788 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
7789 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7790 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7791 }
7792
7793 #[test]
7794 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
7795 let spec: Spec = r#"
7796name "ex"
7797bin "ex"
7798unknown_flags "error"
7799flag "-0 --print0" global=#true
7800cmd "run" {
7801 flag "--force"
7802}
7803"#
7804 .parse()
7805 .unwrap();
7806 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
7807 assert_eq!(out.cmd.name, "run");
7808 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
7809 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
7810 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
7811 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
7812 }
7813
7814 #[test]
7815 fn a_command_may_override_the_cli_wide_setting() {
7816 let spec: Spec = r#"
7818name "ex"
7819bin "ex"
7820unknown_flags "error"
7821cmd "exec" unknown_flags="value" {
7822 arg "[rest]..."
7823}
7824cmd "build" {
7825 arg "[rest]..."
7826}
7827"#
7828 .parse()
7829 .unwrap();
7830
7831 let out = parse(
7832 &spec,
7833 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
7834 )
7835 .unwrap();
7836 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7837 assert_eq!(out.args[rest].to_string(), "--wat");
7838
7839 assert!(
7840 parse(
7841 &spec,
7842 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
7843 )
7844 .is_err(),
7845 "a command that says nothing inherits the CLI's choice"
7846 );
7847 }
7848
7849 #[test]
7850 fn the_setting_survives_a_round_trip() {
7851 let spec: Spec =
7852 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7853 .parse()
7854 .unwrap();
7855 let reparsed: Spec = spec.to_string().parse().unwrap();
7856 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7857 assert_eq!(
7858 reparsed.cmd.subcommands["x"].unknown_flags,
7859 Some(UnknownFlags::Value)
7860 );
7861 }
7862
7863 #[test]
7864 fn test_default_subcommand() {
7865 let run_cmd = SpecCommand::builder()
7867 .name("run")
7868 .arg(SpecArg::builder().name("task").build())
7869 .build();
7870 let mut cmd = SpecCommand::builder().name("test").build();
7871 cmd.subcommands.insert("run".to_string(), run_cmd);
7872
7873 let spec = Spec {
7874 name: "test".to_string(),
7875 bin: "test".to_string(),
7876 cmd,
7877 default_subcommand: Some("run".to_string()),
7878 ..Default::default()
7879 };
7880
7881 let input = vec!["test".to_string(), "mytask".to_string()];
7883 let parsed = parse(&spec, &input).unwrap();
7884
7885 assert_eq!(parsed.cmds.len(), 2);
7887 assert_eq!(parsed.cmds[1].name, "run");
7888
7889 assert_eq!(parsed.args.len(), 1);
7891 let arg = parsed.args.keys().next().unwrap();
7892 assert_eq!(arg.name, "task");
7893 let value = parsed.args.values().next().unwrap();
7894 assert_eq!(value.to_string(), "mytask");
7895 }
7896
7897 #[test]
7898 fn default_subcommand_outranks_root_sigil_arg() {
7899 let spec: Spec = r#"
7900name "test"
7901bin "test"
7902default_subcommand "run"
7903arg "[tools]..." sigil="+"
7904cmd "run" { arg "<task>" }
7905"#
7906 .parse()
7907 .unwrap();
7908
7909 let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7910 assert_eq!(parsed.cmd.name, "run");
7911 let value = |name| {
7912 parsed
7913 .args
7914 .iter()
7915 .find(|(arg, _)| arg.name == name)
7916 .map(|(_, value)| value.to_string())
7917 .unwrap()
7918 };
7919 assert_eq!(value("tools"), "node");
7920 assert_eq!(value("task"), "node");
7921 }
7922
7923 #[test]
7924 fn test_default_subcommand_explicit_still_works() {
7925 let run_cmd = SpecCommand::builder()
7927 .name("run")
7928 .arg(SpecArg::builder().name("task").build())
7929 .build();
7930 let other_cmd = SpecCommand::builder()
7931 .name("other")
7932 .arg(SpecArg::builder().name("other_arg").build())
7933 .build();
7934 let mut cmd = SpecCommand::builder().name("test").build();
7935 cmd.subcommands.insert("run".to_string(), run_cmd);
7936 cmd.subcommands.insert("other".to_string(), other_cmd);
7937
7938 let spec = Spec {
7939 name: "test".to_string(),
7940 bin: "test".to_string(),
7941 cmd,
7942 default_subcommand: Some("run".to_string()),
7943 ..Default::default()
7944 };
7945
7946 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7948 let parsed = parse(&spec, &input).unwrap();
7949
7950 assert_eq!(parsed.cmds.len(), 2);
7952 assert_eq!(parsed.cmds[1].name, "other");
7953 }
7954
7955 #[test]
7956 fn test_default_subcommand_applies_only_at_the_root() {
7957 let mut config_ls = SpecCommand::builder().name("ls").build();
7963 config_ls.args.push(SpecArg::builder().name("what").build());
7964 let mut config_cmd = SpecCommand::builder().name("config").build();
7965 config_cmd.subcommands.insert("ls".to_string(), config_ls);
7966
7967 let mut root_ls = SpecCommand::builder().name("ls").build();
7970 root_ls.args.push(SpecArg::builder().name("what").build());
7971 let mut cmd = SpecCommand::builder().name("ex").build();
7972 cmd.subcommands.insert("ls".to_string(), root_ls);
7973 cmd.subcommands.insert("config".to_string(), config_cmd);
7974
7975 let spec = Spec {
7976 name: "ex".to_string(),
7977 bin: "ex".to_string(),
7978 cmd,
7979 default_subcommand: Some("ls".to_string()),
7980 ..Default::default()
7981 };
7982
7983 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7987 assert!(
7988 parse(&spec, &input).is_err(),
7989 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7990 );
7991
7992 let input = vec!["ex".to_string(), "zzz".to_string()];
7994 let parsed = parse(&spec, &input).expect("the root's default applies");
7995 assert_eq!(
7996 parsed
7997 .cmds
7998 .iter()
7999 .map(|c| c.name.as_str())
8000 .collect::<Vec<_>>(),
8001 ["ex", "ls"]
8002 );
8003 assert_eq!(
8004 parsed.args.values().next().map(|v| v.to_string()),
8005 Some("zzz".to_string()),
8006 "and the word binds inside the command it reached"
8007 );
8008 }
8009
8010 #[test]
8011 fn test_default_subcommand_with_nested_subcommands() {
8012 let say_cmd = SpecCommand::builder()
8016 .name("say")
8017 .arg(SpecArg::builder().name("name").build())
8018 .build();
8019 let mut run_cmd = SpecCommand::builder().name("run").build();
8020 run_cmd.subcommands.insert("say".to_string(), say_cmd);
8021
8022 let mut cmd = SpecCommand::builder().name("test").build();
8023 cmd.subcommands.insert("run".to_string(), run_cmd);
8024
8025 let spec = Spec {
8026 name: "test".to_string(),
8027 bin: "test".to_string(),
8028 cmd,
8029 default_subcommand: Some("run".to_string()),
8030 ..Default::default()
8031 };
8032
8033 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8035 let parsed = parse(&spec, &input).unwrap();
8036
8037 assert_eq!(parsed.cmds.len(), 3);
8039 assert_eq!(parsed.cmds[0].name, "test");
8040 assert_eq!(parsed.cmds[1].name, "run");
8041 assert_eq!(parsed.cmds[2].name, "say");
8042
8043 assert_eq!(parsed.args.len(), 1);
8045 let arg = parsed.args.keys().next().unwrap();
8046 assert_eq!(arg.name, "name");
8047 let value = parsed.args.values().next().unwrap();
8048 assert_eq!(value.to_string(), "hello");
8049 }
8050
8051 fn mounted_global_flag_spec() -> Spec {
8059 let task_cmd = SpecCommand::builder()
8060 .name("sample:run")
8061 .arg(
8062 SpecArg::builder()
8063 .name("profile")
8064 .choices(["alpha", "beta", "gamma"])
8065 .build(),
8066 )
8067 .build();
8068 let mut run_cmd = SpecCommand::builder()
8070 .name("run")
8071 .flag(
8072 SpecFlag::builder()
8073 .name("cd")
8074 .short('C')
8075 .long("cd")
8076 .arg(SpecArg::builder().name("dir").build())
8077 .global(false)
8078 .build(),
8079 )
8080 .build();
8081 run_cmd
8082 .subcommands
8083 .insert("sample:run".to_string(), task_cmd);
8084
8085 let mut cmd = SpecCommand::builder()
8086 .name("test")
8087 .flag(
8088 SpecFlag::builder()
8089 .name("cd")
8090 .short('C')
8091 .long("cd")
8092 .arg(SpecArg::builder().name("dir").build())
8093 .global(true)
8094 .build(),
8095 )
8096 .build();
8097 cmd.subcommands.insert("run".to_string(), run_cmd);
8098
8099 Spec {
8100 name: "test".to_string(),
8101 bin: "test".to_string(),
8102 cmd,
8103 ..Default::default()
8104 }
8105 }
8106
8107 #[test]
8108 fn test_prefix_global_flag_does_not_pollute_choices() {
8109 let spec = mounted_global_flag_spec();
8116
8117 for words in [
8120 &["test", "-C", "/tmp", "run", "sample:run"][..],
8121 &["test", "--cd=/tmp", "run", "sample:run"][..],
8123 ] {
8124 let parsed = parse_partial(&spec, &input(words)).unwrap();
8125 assert_eq!(
8126 parsed
8127 .cmds
8128 .iter()
8129 .map(|c| c.name.as_str())
8130 .collect::<Vec<_>>(),
8131 vec!["test", "run", "sample:run"],
8132 );
8133 assert!(
8135 parsed.args.is_empty(),
8136 "args should be empty, got {:?}",
8137 parsed.args
8138 );
8139
8140 let cd = parsed
8143 .available_flags
8144 .get("--cd")
8145 .expect("--cd should remain available after descending into the subcommand");
8146 assert!(cd.global, "--cd must stay global after descent");
8147 assert!(
8148 parsed.available_flags.get("-C").is_some_and(|f| f.global),
8149 "-C must stay global after descent",
8150 );
8151
8152 assert_eq!(
8156 parsed.as_env().get("usage_cd").map(String::as_str),
8157 Some("/tmp"),
8158 "global flag value must survive in as_env(), got {:?}",
8159 parsed.as_env(),
8160 );
8161 }
8162
8163 let parsed = parse_partial(
8165 &spec,
8166 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8167 )
8168 .unwrap();
8169 assert_eq!(parsed.args.len(), 1);
8170 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8171
8172 assert_parse_err(
8174 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8175 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8176 );
8177 }
8178
8179 fn mounted_orphan_short_spec() -> Spec {
8184 let task_cmd = SpecCommand::builder()
8185 .name("sample:run")
8186 .arg(
8187 SpecArg::builder()
8188 .name("profile")
8189 .choices(["alpha", "beta", "gamma"])
8190 .build(),
8191 )
8192 .build();
8193 let mut run_cmd = SpecCommand::builder()
8196 .name("run")
8197 .flag(
8198 SpecFlag::builder()
8199 .name("raw")
8200 .short('r')
8201 .long("raw")
8202 .global(false)
8203 .build(),
8204 )
8205 .flag(
8206 SpecFlag::builder()
8207 .name("force")
8208 .short('f')
8209 .long("force")
8210 .global(false)
8211 .build(),
8212 )
8213 .build();
8214 run_cmd
8215 .subcommands
8216 .insert("sample:run".to_string(), task_cmd);
8217
8218 let mut cmd = SpecCommand::builder()
8220 .name("test")
8221 .flag(
8222 SpecFlag::builder()
8223 .name("raw")
8224 .long("raw")
8225 .global(true)
8226 .build(),
8227 )
8228 .build();
8229 cmd.subcommands.insert("run".to_string(), run_cmd);
8230
8231 Spec {
8232 name: "test".to_string(),
8233 bin: "test".to_string(),
8234 cmd,
8235 ..Default::default()
8236 }
8237 }
8238
8239 #[test]
8240 fn test_orphan_short_alias_survives_merge() {
8241 let spec = mounted_orphan_short_spec();
8247
8248 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
8249 assert_eq!(
8250 parsed
8251 .cmds
8252 .iter()
8253 .map(|c| c.name.as_str())
8254 .collect::<Vec<_>>(),
8255 vec!["test", "run", "sample:run"],
8256 );
8257
8258 assert!(
8261 parsed.available_flags.get("-r").is_some_and(|f| f.global),
8262 "-r must be merged onto the inherited global flag and stay global after descent",
8263 );
8264 assert!(
8265 parsed
8266 .available_flags
8267 .get("--raw")
8268 .is_some_and(|f| f.global),
8269 "--raw must stay global after descent",
8270 );
8271
8272 assert!(
8274 parsed.args.is_empty(),
8275 "args should be empty, got {:?}",
8276 parsed.args
8277 );
8278
8279 assert_eq!(
8281 parsed.as_env().get("usage_raw").map(String::as_str),
8282 Some("true"),
8283 "merged short's value must survive in as_env(), got {:?}",
8284 parsed.as_env(),
8285 );
8286
8287 assert!(
8290 !parsed.available_flags.contains_key("-f"),
8291 "purely-local -f must not be promoted onto a global",
8292 );
8293 assert!(
8294 !parsed.available_flags.contains_key("--force"),
8295 "purely-local --force must not be promoted onto a global",
8296 );
8297
8298 let parsed =
8300 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8301 assert_eq!(parsed.args.len(), 1);
8302 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8303
8304 assert_parse_err(
8306 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8307 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8308 );
8309 }
8310
8311 #[test]
8312 fn test_orphan_short_does_not_clobber_unrelated_global() {
8313 let run_cmd = SpecCommand::builder()
8318 .name("run")
8319 .flag(
8320 SpecFlag::builder()
8321 .name("raw")
8322 .short('r')
8323 .long("raw")
8324 .global(false)
8325 .build(),
8326 )
8327 .build();
8328 let mut cmd = SpecCommand::builder()
8329 .name("test")
8330 .flag(
8331 SpecFlag::builder()
8332 .name("raw")
8333 .long("raw")
8334 .global(true)
8335 .build(),
8336 )
8337 .flag(
8338 SpecFlag::builder()
8339 .name("restrict")
8340 .short('r')
8341 .long("restrict")
8342 .global(true)
8343 .build(),
8344 )
8345 .build();
8346 cmd.subcommands.insert("run".to_string(), run_cmd);
8347 let spec = Spec {
8348 name: "test".to_string(),
8349 bin: "test".to_string(),
8350 cmd,
8351 ..Default::default()
8352 };
8353
8354 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8355 assert_eq!(
8357 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8358 Some("restrict"),
8359 "-r must remain owned by the unrelated global it already belonged to",
8360 );
8361 assert!(parsed
8363 .available_flags
8364 .get("--raw")
8365 .is_some_and(|f| f.global));
8366 assert!(parsed
8367 .available_flags
8368 .get("--restrict")
8369 .is_some_and(|f| f.global));
8370 }
8371
8372 #[test]
8373 fn test_redeclared_global_aliases_share_one_flag() {
8374 let spec = r#"
8381flag "-y --yes" global=#true effect="write"
8382cmd "run" {
8383 flag "-y --yes --assume-yes"
8384}
8385"#
8386 .parse::<Spec>()
8387 .unwrap();
8388
8389 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8390
8391 for key in ["-y", "--yes", "--assume-yes"] {
8392 let flag = parsed
8393 .available_flags
8394 .get(key)
8395 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8396 assert!(flag.global, "{key} must stay global after the descent");
8397 assert_eq!(
8398 flag.long,
8399 vec!["yes".to_string(), "assume-yes".to_string()],
8400 "{key} must resolve to the flag carrying every alias",
8401 );
8402 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8403 }
8404
8405 assert_eq!(
8407 unique_flags(parsed.available_flags.values()).count(),
8408 1,
8409 "all aliases must point at one flag object, got {:?}",
8410 parsed.available_flags,
8411 );
8412
8413 assert_eq!(
8415 parsed.available_flags["-y"].effect,
8416 Some(crate::SpecCommandEffect::Write),
8417 );
8418 }
8419
8420 #[test]
8421 fn test_redeclared_global_keeps_hidden_alias_metadata() {
8422 let spec = r#"
8423flag "--yes" global=#true {
8424 alias "-q" "--quietly" hide=#true
8425}
8426cmd "run" {
8427 flag "--yes --assume-yes" {
8428 alias "-s" "--secret" hide=#true
8429 }
8430}
8431"#
8432 .parse::<Spec>()
8433 .unwrap();
8434
8435 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8436 let merged = &parsed.available_flags["--yes"];
8437 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8438 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8439 for key in ["-q", "-s", "--quietly", "--secret"] {
8440 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8441 }
8442 }
8443
8444 #[test]
8445 fn test_redeclared_global_can_promote_hidden_aliases() {
8446 let spec = r#"
8447flag "--yes" global=#true {
8448 alias "-q" "--quietly" hide=#true
8449}
8450cmd "run" {
8451 flag "-q --yes --quietly"
8452}
8453"#
8454 .parse::<Spec>()
8455 .unwrap();
8456
8457 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8458 let merged = &parsed.available_flags["--yes"];
8459 assert!(merged.hidden_short_aliases.is_empty());
8460 assert!(merged.hidden_aliases.is_empty());
8461 for key in ["-q", "--quietly"] {
8462 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8463 }
8464 }
8465
8466 #[test]
8467 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8468 let spec = r#"
8474flag "-y --yes --confirm" global=#true
8475cmd "run" {
8476 flag "--yes --assume-yes"
8477}
8478"#
8479 .parse::<Spec>()
8480 .unwrap();
8481
8482 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8483
8484 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8485 let flag = parsed
8486 .available_flags
8487 .get(key)
8488 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8489 assert!(flag.global, "{key} must stay global after the descent");
8490 assert_eq!(
8491 flag.long,
8492 vec![
8493 "yes".to_string(),
8494 "confirm".to_string(),
8495 "assume-yes".to_string()
8496 ],
8497 "{key} must resolve to the flag carrying every alias",
8498 );
8499 }
8500
8501 assert_eq!(
8502 unique_flags(parsed.available_flags.values()).count(),
8503 1,
8504 "all aliases must point at one flag object, got {:?}",
8505 parsed.available_flags,
8506 );
8507 }
8508
8509 fn mounted_task_flag_spec() -> Spec {
8516 let mut task_cmd = SpecCommand::builder()
8517 .name("mytask")
8518 .flag(
8519 SpecFlag::builder()
8520 .name("env")
8521 .long("env")
8522 .arg(
8523 SpecArg::builder()
8524 .name("name")
8525 .choices(["dev", "stage", "prod"])
8526 .build(),
8527 )
8528 .global(false)
8529 .build(),
8530 )
8531 .flag(
8532 SpecFlag::builder()
8533 .name("bump")
8534 .long("bump")
8535 .arg(
8536 SpecArg::builder()
8537 .name("type")
8538 .choices(["auto", "major"])
8539 .build(),
8540 )
8541 .global(false)
8542 .build(),
8543 )
8544 .build();
8545 task_cmd.mounted = true;
8546
8547 let mut run_cmd = SpecCommand::builder()
8548 .name("run")
8549 .flag(
8550 SpecFlag::builder()
8551 .name("force")
8552 .short('f')
8553 .long("force")
8554 .global(false)
8555 .build(),
8556 )
8557 .build();
8558 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8559
8560 let mut cmd = SpecCommand::builder()
8561 .name("test")
8562 .flag(
8563 SpecFlag::builder()
8564 .name("env")
8565 .short('E')
8566 .long("env")
8567 .arg(SpecArg::builder().name("ENV").build())
8568 .global(true)
8569 .build(),
8570 )
8571 .flag(
8572 SpecFlag::builder()
8573 .name("silent")
8574 .long("silent")
8575 .global(true)
8576 .build(),
8577 )
8578 .build();
8579 cmd.subcommands.insert("run".to_string(), run_cmd);
8580
8581 Spec {
8582 name: "test".to_string(),
8583 bin: "test".to_string(),
8584 cmd,
8585 ..Default::default()
8586 }
8587 }
8588
8589 #[test]
8590 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8591 let deep = SpecCommand::builder().name("deep").build();
8597 let mut sub = SpecCommand::builder()
8598 .name("sub")
8599 .flag(
8601 SpecFlag::builder()
8602 .name("cd")
8603 .short('C')
8604 .long("cd")
8605 .arg(SpecArg::builder().name("dir").build())
8606 .global(false)
8607 .build(),
8608 )
8609 .build();
8610 sub.subcommands.insert("deep".to_string(), deep);
8611 let mut task = SpecCommand::builder()
8612 .name("task")
8613 .flag(
8614 SpecFlag::builder()
8615 .name("cd")
8616 .short('C')
8617 .long("cd")
8618 .arg(SpecArg::builder().name("dir").build())
8619 .global(true)
8620 .build(),
8621 )
8622 .build();
8623 task.subcommands.insert("sub".to_string(), sub);
8624 task.mark_mounted();
8625
8626 let mut run_cmd = SpecCommand::builder().name("run").build();
8627 run_cmd.subcommands.insert("task".to_string(), task);
8628 let mut cmd = SpecCommand::builder().name("test").build();
8629 cmd.subcommands.insert("run".to_string(), run_cmd);
8630 let spec = Spec {
8631 name: "test".to_string(),
8632 bin: "test".to_string(),
8633 cmd,
8634 ..Default::default()
8635 };
8636
8637 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8638 assert!(
8639 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8640 "the mounted program's own global must survive descents inside the mounted tree",
8641 );
8642 assert!(
8643 parsed.completion_flags().contains_key("--cd"),
8644 "and must still be offered there: it belongs to the mounted program",
8645 );
8646 assert!(
8647 parsed.completion_flags().contains_key("-C"),
8648 "including the short the nested command re-declared",
8649 );
8650 }
8651
8652 #[test]
8653 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8654 let mut task = SpecCommand::builder()
8659 .name("task")
8660 .flag(
8661 SpecFlag::builder()
8662 .name("bump")
8663 .long("bump")
8664 .global(false)
8665 .build(),
8666 )
8667 .build();
8668 task.mark_mounted();
8669
8670 let mut run_cmd = SpecCommand::builder().name("run").build();
8671 run_cmd.subcommands.insert("task".to_string(), task);
8672 run_cmd.flags = vec![
8674 SpecFlag::builder()
8675 .name("tglobal")
8676 .long("tglobal")
8677 .global(true)
8678 .build(),
8679 SpecFlag::builder()
8680 .name("tlocal")
8681 .long("tlocal")
8682 .global(false)
8683 .build(),
8684 ];
8685 run_cmd.flags_from_mount = true;
8686
8687 let mut cmd = SpecCommand::builder()
8688 .name("test")
8689 .flag(
8690 SpecFlag::builder()
8691 .name("silent")
8692 .long("silent")
8693 .global(true)
8694 .build(),
8695 )
8696 .build();
8697 cmd.subcommands.insert("run".to_string(), run_cmd);
8698 let spec = Spec {
8699 name: "test".to_string(),
8700 bin: "test".to_string(),
8701 cmd,
8702 ..Default::default()
8703 };
8704
8705 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8706 assert_eq!(
8707 parsed.completion_flags().keys().collect::<Vec<_>>(),
8708 vec!["--bump", "--tglobal"],
8709 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8710 `--silent` does not, and the mount's non-global root flag is not inherited",
8711 );
8712 }
8713
8714 #[test]
8715 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8716 let spec = mounted_task_flag_spec();
8721 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8722
8723 assert!(parsed.available_flags.contains_key("--silent"));
8725 assert!(parsed.available_flags.contains_key("-E"));
8726 assert_eq!(
8728 parsed.completion_flags().keys().collect::<Vec<_>>(),
8729 vec!["--bump", "--env"],
8730 "only the mounted command's own flags may be offered",
8731 );
8732
8733 assert!(!parsed.available_flags.contains_key("--force"));
8735 }
8736
8737 #[test]
8738 fn test_mounted_cmd_flag_wins_over_inherited_global() {
8739 let spec = mounted_task_flag_spec();
8742 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8743
8744 let awaiting = parsed
8745 .flag_awaiting_value
8746 .first()
8747 .expect("--env should await a value");
8748 assert_eq!(
8749 awaiting
8750 .arg
8751 .as_ref()
8752 .and_then(|a| a.choices.as_ref())
8753 .map(|c| c.choices.clone()),
8754 Some(vec![
8755 "dev".to_string(),
8756 "stage".to_string(),
8757 "prod".to_string()
8758 ]),
8759 "the mounted command's own --env must win over the inherited global",
8760 );
8761
8762 let parsed =
8765 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8766 assert!(
8767 parsed.args.is_empty(),
8768 "prefix global tokens must not be consumed as positionals, got {:?}",
8769 parsed.args
8770 );
8771 assert_eq!(
8772 parsed.as_env().get("usage_env").map(String::as_str),
8773 Some("anything"),
8774 );
8775 }
8776
8777 #[test]
8778 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
8779 let spec = mounted_task_flag_spec();
8784 let parsed = parse_partial(
8785 &spec,
8786 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
8787 )
8788 .unwrap();
8789 assert!(
8790 parsed.errors.is_empty(),
8791 "prefix global value must not be validated against the mounted flag: {:?}",
8792 parsed
8793 .errors
8794 .iter()
8795 .map(|e| e.to_string())
8796 .collect::<Vec<_>>(),
8797 );
8798 assert_eq!(
8799 parsed.as_env().get("usage_env").map(String::as_str),
8800 Some("not-a-task-choice"),
8801 );
8802
8803 let parsed = parse_partial(
8805 &spec,
8806 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
8807 )
8808 .unwrap();
8809 assert!(parsed.errors.is_empty());
8810 assert_eq!(
8811 parsed.as_env().get("usage_env").map(String::as_str),
8812 Some("not-a-task-choice"),
8813 );
8814
8815 let parsed = parse_partial(
8818 &spec,
8819 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
8820 )
8821 .unwrap();
8822 let awaiting = parsed
8823 .flag_awaiting_value
8824 .first()
8825 .expect("--env should await a value");
8826 assert_eq!(
8827 awaiting
8828 .arg
8829 .as_ref()
8830 .and_then(|a| a.choices.as_ref())
8831 .map(|c| c.choices.clone()),
8832 Some(vec![
8833 "dev".to_string(),
8834 "stage".to_string(),
8835 "prod".to_string()
8836 ]),
8837 "the mounted command's --env must own the name after the mounted command",
8838 );
8839 }
8840
8841 #[test]
8842 fn test_non_global_flag_does_not_hide_subcommand() {
8843 let spec = mounted_task_flag_spec();
8847
8848 for words in [
8849 &["test", "run", "--force", "mytask"][..],
8851 &["test", "run", "-f", "mytask"][..],
8852 &["test", "-E", "prod", "run", "--force", "mytask"][..],
8854 ] {
8855 let parsed = parse_partial(&spec, &input(words)).unwrap();
8856 assert_eq!(
8857 parsed
8858 .cmds
8859 .iter()
8860 .map(|c| c.name.as_str())
8861 .collect::<Vec<_>>(),
8862 vec!["test", "run", "mytask"],
8863 "{words:?} should descend into the mounted command",
8864 );
8865 assert!(
8866 parsed.args.is_empty(),
8867 "{words:?} should not consume a positional, got {:?}",
8868 parsed.args,
8869 );
8870 assert_eq!(
8871 parsed.as_env().get("usage_force").map(String::as_str),
8872 Some("true"),
8873 "the non-global flag must still be recorded for {words:?}",
8874 );
8875 }
8876
8877 let mut run_cmd = SpecCommand::builder()
8880 .name("run")
8881 .flag(
8882 SpecFlag::builder()
8883 .name("output")
8884 .short('o')
8885 .long("output")
8886 .arg(SpecArg::builder().name("mode").build())
8887 .global(false)
8888 .build(),
8889 )
8890 .build();
8891 run_cmd.subcommands.insert(
8892 "task".to_string(),
8893 SpecCommand::builder().name("task").build(),
8894 );
8895 let mut cmd = SpecCommand::builder().name("test").build();
8896 cmd.subcommands.insert("run".to_string(), run_cmd);
8897 let spec = Spec {
8898 name: "test".to_string(),
8899 bin: "test".to_string(),
8900 cmd,
8901 ..Default::default()
8902 };
8903
8904 let parsed =
8905 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8906 assert_eq!(
8907 parsed
8908 .cmds
8909 .iter()
8910 .map(|c| c.name.as_str())
8911 .collect::<Vec<_>>(),
8912 vec!["test", "run", "task"],
8913 );
8914 assert_eq!(
8915 parsed.as_env().get("usage_output").map(String::as_str),
8916 Some("quiet"),
8917 );
8918
8919 assert_parse_err(
8922 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8923 "unexpected word: --nope",
8924 );
8925 }
8926
8927 #[test]
8928 fn test_non_mounted_subcommand_offers_inherited_globals() {
8929 let mut run_cmd = SpecCommand::builder().name("run").build();
8932 run_cmd.subcommands.insert(
8933 "nested".to_string(),
8934 SpecCommand::builder().name("nested").build(),
8935 );
8936 let mut cmd = SpecCommand::builder()
8937 .name("test")
8938 .flag(
8939 SpecFlag::builder()
8940 .name("silent")
8941 .long("silent")
8942 .global(true)
8943 .build(),
8944 )
8945 .build();
8946 cmd.subcommands.insert("run".to_string(), run_cmd);
8947 let spec = Spec {
8948 name: "test".to_string(),
8949 bin: "test".to_string(),
8950 cmd,
8951 ..Default::default()
8952 };
8953
8954 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8955 assert_eq!(
8956 parsed.completion_flags().keys().collect::<Vec<_>>(),
8957 parsed.available_flags.keys().collect::<Vec<_>>(),
8958 );
8959 assert!(parsed.completion_flags().contains_key("--silent"));
8960 }
8961
8962 #[test]
8963 fn test_subcommand_alias_collision_keeps_last_owner() {
8964 let run_cmd = SpecCommand::builder()
8968 .name("run")
8969 .flag(
8970 SpecFlag::builder()
8971 .name("alpha")
8972 .short('x')
8973 .long("alpha")
8974 .global(false)
8975 .build(),
8976 )
8977 .flag(
8978 SpecFlag::builder()
8979 .name("beta")
8980 .short('x')
8981 .long("beta")
8982 .global(false)
8983 .build(),
8984 )
8985 .build();
8986 let mut cmd = SpecCommand::builder().name("test").build();
8987 cmd.subcommands.insert("run".to_string(), run_cmd);
8988 let spec = Spec {
8989 name: "test".to_string(),
8990 bin: "test".to_string(),
8991 cmd,
8992 ..Default::default()
8993 };
8994
8995 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8996 assert_eq!(
8998 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8999 Some("beta"),
9000 "the last-declared flag must keep a shared short alias",
9001 );
9002 assert_eq!(
9004 parsed
9005 .available_flags
9006 .get("--alpha")
9007 .map(|f| f.name.as_str()),
9008 Some("alpha"),
9009 );
9010 assert_eq!(
9011 parsed
9012 .available_flags
9013 .get("--beta")
9014 .map(|f| f.name.as_str()),
9015 Some("beta"),
9016 );
9017 }
9018
9019 #[test]
9020 fn test_default_subcommand_same_name_child() {
9021 let run_task = SpecCommand::builder()
9025 .name("run")
9026 .arg(SpecArg::builder().name("args").build())
9027 .build();
9028 let mut run_cmd = SpecCommand::builder().name("run").build();
9029 run_cmd.subcommands.insert("run".to_string(), run_task);
9030
9031 let mut cmd = SpecCommand::builder().name("test").build();
9032 cmd.subcommands.insert("run".to_string(), run_cmd);
9033
9034 let spec = Spec {
9035 name: "test".to_string(),
9036 bin: "test".to_string(),
9037 cmd,
9038 default_subcommand: Some("run".to_string()),
9039 ..Default::default()
9040 };
9041
9042 let input = vec!["test".to_string(), "run".to_string()];
9044 let parsed = parse(&spec, &input).unwrap();
9045
9046 assert_eq!(parsed.cmds.len(), 2);
9048 assert_eq!(parsed.cmds[0].name, "test");
9049 assert_eq!(parsed.cmds[1].name, "run");
9050
9051 let input = vec![
9053 "test".to_string(),
9054 "run".to_string(),
9055 "run".to_string(),
9056 "hello".to_string(),
9057 ];
9058 let parsed = parse(&spec, &input).unwrap();
9059
9060 assert_eq!(parsed.cmds.len(), 3);
9061 assert_eq!(parsed.cmds[0].name, "test");
9062 assert_eq!(parsed.cmds[1].name, "run");
9063 assert_eq!(parsed.cmds[2].name, "run");
9064 assert_eq!(parsed.args.len(), 1);
9065 let value = parsed.args.values().next().unwrap();
9066 assert_eq!(value.to_string(), "hello");
9067
9068 let mut run_cmd = SpecCommand::builder()
9072 .name("run")
9073 .arg(SpecArg::builder().name("task").build())
9074 .build();
9075 let run_task = SpecCommand::builder().name("run").build();
9076 run_cmd.subcommands.insert("run".to_string(), run_task);
9077
9078 let mut cmd = SpecCommand::builder().name("test").build();
9079 cmd.subcommands.insert("run".to_string(), run_cmd);
9080
9081 let spec = Spec {
9082 name: "test".to_string(),
9083 bin: "test".to_string(),
9084 cmd,
9085 default_subcommand: Some("run".to_string()),
9086 ..Default::default()
9087 };
9088
9089 let input = vec!["test".to_string(), "other".to_string()];
9090 let parsed = parse(&spec, &input).unwrap();
9091
9092 assert_eq!(parsed.cmds.len(), 2);
9095 assert_eq!(parsed.cmds[0].name, "test");
9096 assert_eq!(parsed.cmds[1].name, "run");
9097
9098 assert_eq!(parsed.args.len(), 1);
9100 let value = parsed.args.values().next().unwrap();
9101 assert_eq!(value.to_string(), "other");
9102 }
9103
9104 #[test]
9105 fn test_restart_token() {
9106 let run_cmd = SpecCommand::builder()
9108 .name("run")
9109 .arg(SpecArg::builder().name("task").build())
9110 .restart_token(":::".to_string())
9111 .build();
9112 let mut cmd = SpecCommand::builder().name("test").build();
9113 cmd.subcommands.insert("run".to_string(), run_cmd);
9114
9115 let spec = Spec {
9116 name: "test".to_string(),
9117 bin: "test".to_string(),
9118 cmd,
9119 ..Default::default()
9120 };
9121
9122 let input = vec![
9124 "test".to_string(),
9125 "run".to_string(),
9126 "task1".to_string(),
9127 ":::".to_string(),
9128 "task2".to_string(),
9129 ];
9130 let parsed = parse(&spec, &input).unwrap();
9131
9132 assert_eq!(parsed.args.len(), 1);
9134 let value = parsed.args.values().next().unwrap();
9135 assert_eq!(value.to_string(), "task2");
9136 }
9137
9138 #[test]
9139 fn test_restart_token_multiple() {
9140 let run_cmd = SpecCommand::builder()
9142 .name("run")
9143 .arg(SpecArg::builder().name("task").build())
9144 .restart_token(":::".to_string())
9145 .build();
9146 let mut cmd = SpecCommand::builder().name("test").build();
9147 cmd.subcommands.insert("run".to_string(), run_cmd);
9148
9149 let spec = Spec {
9150 name: "test".to_string(),
9151 bin: "test".to_string(),
9152 cmd,
9153 ..Default::default()
9154 };
9155
9156 let input = vec![
9158 "test".to_string(),
9159 "run".to_string(),
9160 "task1".to_string(),
9161 ":::".to_string(),
9162 "task2".to_string(),
9163 ":::".to_string(),
9164 "task3".to_string(),
9165 ];
9166 let parsed = parse(&spec, &input).unwrap();
9167
9168 assert_eq!(parsed.args.len(), 1);
9170 let value = parsed.args.values().next().unwrap();
9171 assert_eq!(value.to_string(), "task3");
9172 }
9173
9174 #[test]
9175 fn test_restart_token_clears_flag_awaiting_value() {
9176 let run_cmd = SpecCommand::builder()
9178 .name("run")
9179 .arg(SpecArg::builder().name("task").build())
9180 .flag(
9181 SpecFlag::builder()
9182 .name("jobs")
9183 .long("jobs")
9184 .arg(SpecArg::builder().name("count").build())
9185 .build(),
9186 )
9187 .restart_token(":::".to_string())
9188 .build();
9189 let mut cmd = SpecCommand::builder().name("test").build();
9190 cmd.subcommands.insert("run".to_string(), run_cmd);
9191
9192 let spec = Spec {
9193 name: "test".to_string(),
9194 bin: "test".to_string(),
9195 cmd,
9196 ..Default::default()
9197 };
9198
9199 let input = vec![
9201 "test".to_string(),
9202 "run".to_string(),
9203 "task1".to_string(),
9204 "--jobs".to_string(),
9205 ":::".to_string(),
9206 "task2".to_string(),
9207 ];
9208 let parsed = parse(&spec, &input).unwrap();
9209
9210 assert_eq!(parsed.args.len(), 1);
9212 let value = parsed.args.values().next().unwrap();
9213 assert_eq!(value.to_string(), "task2");
9214 assert!(parsed.flag_awaiting_value.is_empty());
9216 }
9217
9218 #[test]
9219 fn test_restart_token_resets_double_dash() {
9220 let run_cmd = SpecCommand::builder()
9222 .name("run")
9223 .arg(SpecArg::builder().name("task").build())
9224 .arg(SpecArg::builder().name("extra_args").var(true).build())
9225 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9226 .restart_token(":::".to_string())
9227 .build();
9228 let mut cmd = SpecCommand::builder().name("test").build();
9229 cmd.subcommands.insert("run".to_string(), run_cmd);
9230
9231 let spec = Spec {
9232 name: "test".to_string(),
9233 bin: "test".to_string(),
9234 cmd,
9235 ..Default::default()
9236 };
9237
9238 let input = vec![
9240 "test".to_string(),
9241 "run".to_string(),
9242 "task1".to_string(),
9243 "--".to_string(),
9244 "extra".to_string(),
9245 ":::".to_string(),
9246 "--verbose".to_string(),
9247 "task2".to_string(),
9248 ];
9249 let parsed = parse(&spec, &input).unwrap();
9250
9251 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9253 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9255 let value = parsed.args.get(task_arg).unwrap();
9256 assert_eq!(value.to_string(), "task2");
9257 }
9258
9259 #[test]
9260 fn test_double_dashes_without_preserve() {
9261 let run_cmd = SpecCommand::builder()
9266 .name("run")
9267 .arg(SpecArg::builder().name("args").var(true).build())
9268 .build();
9269 let mut cmd = SpecCommand::builder().name("test").build();
9270 cmd.subcommands.insert("run".to_string(), run_cmd);
9271
9272 let spec = Spec {
9273 name: "test".to_string(),
9274 bin: "test".to_string(),
9275 cmd,
9276 ..Default::default()
9277 };
9278
9279 let input = vec![
9281 "test".to_string(),
9282 "run".to_string(),
9283 "arg1".to_string(),
9284 "--".to_string(),
9285 "arg2".to_string(),
9286 "--".to_string(),
9287 "arg3".to_string(),
9288 ];
9289 let parsed = parse(&spec, &input).unwrap();
9290
9291 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9292 let value = parsed.args.get(args_arg).unwrap();
9293 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9294 }
9295
9296 #[test]
9297 fn test_double_dashes_with_preserve() {
9298 let run_cmd = SpecCommand::builder()
9300 .name("run")
9301 .arg(
9302 SpecArg::builder()
9303 .name("args")
9304 .var(true)
9305 .double_dash(SpecDoubleDashChoices::Preserve)
9306 .build(),
9307 )
9308 .build();
9309 let mut cmd = SpecCommand::builder().name("test").build();
9310 cmd.subcommands.insert("run".to_string(), run_cmd);
9311
9312 let spec = Spec {
9313 name: "test".to_string(),
9314 bin: "test".to_string(),
9315 cmd,
9316 ..Default::default()
9317 };
9318
9319 let input = vec![
9321 "test".to_string(),
9322 "run".to_string(),
9323 "arg1".to_string(),
9324 "--".to_string(),
9325 "arg2".to_string(),
9326 "--".to_string(),
9327 "arg3".to_string(),
9328 ];
9329 let parsed = parse(&spec, &input).unwrap();
9330
9331 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9332 let value = parsed.args.get(args_arg).unwrap();
9333 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9334 }
9335
9336 #[test]
9337 fn test_double_dashes_with_preserve_only_dashes() {
9338 let run_cmd = SpecCommand::builder()
9341 .name("run")
9342 .arg(
9343 SpecArg::builder()
9344 .name("args")
9345 .var(true)
9346 .double_dash(SpecDoubleDashChoices::Preserve)
9347 .build(),
9348 )
9349 .build();
9350 let mut cmd = SpecCommand::builder().name("test").build();
9351 cmd.subcommands.insert("run".to_string(), run_cmd);
9352
9353 let spec = Spec {
9354 name: "test".to_string(),
9355 bin: "test".to_string(),
9356 cmd,
9357 ..Default::default()
9358 };
9359
9360 let input = vec![
9362 "test".to_string(),
9363 "run".to_string(),
9364 "--".to_string(),
9365 "--".to_string(),
9366 ];
9367 let parsed = parse(&spec, &input).unwrap();
9368
9369 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9370 let value = parsed.args.get(args_arg).unwrap();
9371 assert_eq!(value.to_string(), "-- --");
9372 }
9373
9374 #[test]
9375 fn test_double_dashes_with_preserve_multiple_args() {
9376 let run_cmd = SpecCommand::builder()
9378 .name("run")
9379 .arg(SpecArg::builder().name("task").build())
9380 .arg(
9381 SpecArg::builder()
9382 .name("extra_args")
9383 .var(true)
9384 .double_dash(SpecDoubleDashChoices::Preserve)
9385 .build(),
9386 )
9387 .build();
9388 let mut cmd = SpecCommand::builder().name("test").build();
9389 cmd.subcommands.insert("run".to_string(), run_cmd);
9390
9391 let spec = Spec {
9392 name: "test".to_string(),
9393 bin: "test".to_string(),
9394 cmd,
9395 ..Default::default()
9396 };
9397
9398 let input = vec![
9401 "test".to_string(),
9402 "run".to_string(),
9403 "task1".to_string(),
9404 "--".to_string(),
9405 "arg1".to_string(),
9406 "--".to_string(),
9407 "--foo".to_string(),
9408 ];
9409 let parsed = parse(&spec, &input).unwrap();
9410
9411 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9412 let task_value = parsed.args.get(task_arg).unwrap();
9413 assert_eq!(task_value.to_string(), "task1");
9414
9415 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9416 let extra_value = parsed.args.get(extra_arg).unwrap();
9417 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9418 }
9419
9420 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9421 let cmd = SpecCommand::builder().name("test").args(args).build();
9422 Spec {
9423 name: "test".to_string(),
9424 bin: "test".to_string(),
9425 cmd,
9426 ..Default::default()
9427 }
9428 }
9429
9430 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9431 let arg = parsed
9432 .args
9433 .keys()
9434 .find(|a| a.name == name)
9435 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9436 parsed.args.get(arg).unwrap().to_string()
9437 }
9438
9439 fn required_arg(name: &str) -> SpecArg {
9440 SpecArg::builder()
9441 .name(name)
9442 .var(true)
9443 .required(false)
9444 .double_dash(SpecDoubleDashChoices::Required)
9445 .build()
9446 }
9447
9448 #[test]
9449 fn test_double_dash_required_reports_error_once_for_variadic() {
9450 let spec = spec_with_args([required_arg("files")]);
9452
9453 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9454
9455 assert!(parsed.args.is_empty());
9456 assert_eq!(parsed.errors.len(), 1);
9457 assert!(
9458 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9459 );
9460 }
9461
9462 #[test]
9463 fn test_double_dash_required_suppresses_missing_arg() {
9464 let spec = spec_with_args([SpecArg::builder()
9466 .name("file")
9467 .required(true)
9468 .double_dash(SpecDoubleDashChoices::Required)
9469 .build()]);
9470
9471 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9472
9473 assert_eq!(parsed.errors.len(), 1);
9474 assert!(matches!(
9475 &parsed.errors[0],
9476 UsageErr::ArgRequiresDoubleDash(_)
9477 ));
9478 assert_eq!(
9480 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9481 Some("file")
9482 );
9483 assert!(!parsed.double_dash_seen);
9484 }
9485
9486 #[test]
9487 fn test_double_dash_routes_to_required_arg() {
9488 let spec = spec_with_args([
9491 SpecArg::builder()
9492 .name("tool")
9493 .var(true)
9494 .required(false)
9495 .build(),
9496 required_arg("command"),
9497 ]);
9498
9499 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9500
9501 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9502 assert_eq!(arg_value(&parsed, "command"), "node app.js");
9503 assert!(parsed.double_dash_seen);
9504 }
9505
9506 #[test]
9507 fn test_double_dash_routes_with_gap_reports_missing_arg() {
9508 let spec = spec_with_args([
9511 SpecArg::builder()
9512 .name("tool")
9513 .var(true)
9514 .required(true)
9515 .build(),
9516 required_arg("command"),
9517 ]);
9518
9519 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9520
9521 assert_eq!(arg_value(&parsed, "command"), "ls");
9522 assert!(parsed.args.keys().all(|a| a.name != "tool"));
9523 assert!(parsed
9524 .errors
9525 .iter()
9526 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
9527 }
9528
9529 #[test]
9530 fn test_double_dash_gap_applies_defaults() {
9531 let spec = spec_with_args([
9533 SpecArg::builder()
9534 .name("tool")
9535 .var(true)
9536 .required(false)
9537 .default_value("node@20")
9538 .build(),
9539 required_arg("command"),
9540 ]);
9541
9542 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9543
9544 assert_eq!(arg_value(&parsed, "command"), "ls");
9545 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9546 }
9547
9548 fn spec_with_restart_token_and_required_arg() -> Spec {
9549 let run_cmd = SpecCommand::builder()
9550 .name("run")
9551 .arg(SpecArg::builder().name("task").build())
9552 .arg(required_arg("run_args"))
9553 .restart_token(":::".to_string())
9554 .build();
9555 let mut cmd = SpecCommand::builder().name("test").build();
9556 cmd.subcommands.insert("run".to_string(), run_cmd);
9557 Spec {
9558 name: "test".to_string(),
9559 bin: "test".to_string(),
9560 cmd,
9561 ..Default::default()
9562 }
9563 }
9564
9565 #[test]
9566 fn test_double_dash_required_restart_token_resets_separator() {
9567 let spec = spec_with_restart_token_and_required_arg();
9569
9570 let parsed = parse_partial(
9571 &spec,
9572 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9573 )
9574 .unwrap();
9575
9576 assert_eq!(arg_value(&parsed, "task"), "task2");
9577 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9578 assert_eq!(
9580 parsed
9581 .errors
9582 .iter()
9583 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9584 .count(),
9585 1
9586 );
9587 }
9588
9589 #[test]
9590 fn test_double_dash_required_restart_token_accepts_new_separator() {
9591 let spec = spec_with_restart_token_and_required_arg();
9592
9593 let parsed = parse(
9594 &spec,
9595 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9596 )
9597 .unwrap();
9598
9599 assert_eq!(arg_value(&parsed, "task"), "task2");
9600 assert_eq!(arg_value(&parsed, "run_args"), "c");
9601 }
9602
9603 #[test]
9604 fn test_double_dash_preserve_is_not_a_separator() {
9605 let spec = spec_with_args([
9608 SpecArg::builder()
9609 .name("kept")
9610 .var(true)
9611 .var_max(1)
9612 .required(false)
9613 .double_dash(SpecDoubleDashChoices::Preserve)
9614 .build(),
9615 required_arg("rest"),
9616 ]);
9617
9618 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9619
9620 assert_eq!(arg_value(&parsed, "kept"), "--");
9621 assert!(parsed.args.keys().all(|a| a.name != "rest"));
9622 assert!(!parsed.double_dash_seen);
9623 assert_eq!(parsed.errors.len(), 1);
9624 }
9625
9626 #[test]
9627 fn test_double_dash_required_does_not_bail_in_parse_partial() {
9628 let spec = spec_with_args([required_arg("file")]);
9630
9631 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9632 assert!(parse(&spec, &input(&["test", "x"])).is_err());
9633 }
9634
9635 #[test]
9636 fn test_double_dash_without_required_arg_does_not_move_cursor() {
9637 let spec = spec_with_args([
9639 SpecArg::builder().name("first").required(false).build(),
9640 SpecArg::builder().name("second").required(false).build(),
9641 ]);
9642
9643 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9644
9645 assert_eq!(arg_value(&parsed, "first"), "a");
9646 assert_eq!(arg_value(&parsed, "second"), "b");
9647 assert!(parsed.next_arg.is_none());
9648 }
9649
9650 #[test]
9651 fn test_parser_with_custom_env_for_required_arg() {
9652 let spec = spec_with_arg(
9653 SpecArg::builder()
9654 .name("name")
9655 .env("NAME")
9656 .required(true)
9657 .build(),
9658 );
9659 std::env::remove_var("NAME");
9660
9661 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9662 .expect("parse should succeed with custom env");
9663 assert_eq!(parsed.args.len(), 1);
9664 assert_eq!(first_string_value(&parsed), "john");
9665 }
9666
9667 #[test]
9668 fn test_parser_with_custom_env_for_required_flag() {
9669 let spec = spec_with_flag(
9670 SpecFlag::builder()
9671 .long("name")
9672 .env("NAME")
9673 .required(true)
9674 .arg(SpecArg::builder().name("name").build())
9675 .build(),
9676 );
9677 std::env::remove_var("NAME");
9678
9679 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9680 .expect("parse should succeed with custom env");
9681 assert_eq!(parsed.flags.len(), 1);
9682 assert_eq!(first_string_value(&parsed), "jane");
9683 }
9684
9685 #[test]
9686 fn test_flag_environment_fallbacks_preserve_declaration_order() {
9687 let spec = spec_with_flag(
9688 SpecFlag::builder()
9689 .long("name")
9690 .env("NAME")
9691 .env_fallback("OLD_NAME")
9692 .env_fallback("OLDER_NAME")
9693 .deprecated_env("DEPRECATED_NAME")
9694 .arg(SpecArg::builder().name("name").build())
9695 .build(),
9696 );
9697
9698 let parsed = parse_with_env(
9699 &spec,
9700 &["test"],
9701 &[
9702 ("NAME", "canonical"),
9703 ("OLD_NAME", "fallback"),
9704 ("DEPRECATED_NAME", "deprecated"),
9705 ],
9706 )
9707 .unwrap();
9708 assert_eq!(first_string_value(&parsed), "canonical");
9709
9710 let parsed = parse_with_env(
9711 &spec,
9712 &["test"],
9713 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9714 )
9715 .unwrap();
9716 assert_eq!(first_string_value(&parsed), "old");
9717
9718 let parsed =
9719 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9720 assert_eq!(first_string_value(&parsed), "deprecated");
9721 }
9722
9723 #[test]
9724 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9725 let spec = spec_with_flag(
9726 SpecFlag::builder()
9727 .long("name")
9728 .env("NAME")
9729 .deprecated_env("DEPRECATED_NAME")
9730 .arg(SpecArg::builder().name("name").build())
9731 .build(),
9732 );
9733
9734 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9736 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9737
9738 let parsed =
9739 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9740 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9741 assert_eq!(
9742 parsed.warnings[0].kind,
9743 crate::warn::WarningKind::DeprecatedEnv
9744 );
9745 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9746 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9747 assert_eq!(first_string_value(&parsed), "deprecated");
9749 }
9750
9751 #[test]
9752 fn a_deprecated_flag_reports_only_when_it_was_used() {
9753 let spec = spec_with_flag(
9754 SpecFlag::builder()
9755 .long("output")
9756 .deprecated("use --out")
9757 .deprecated_remove_at("3.0.0")
9758 .arg(SpecArg::builder().name("output").build())
9759 .build(),
9760 );
9761
9762 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9763 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9764
9765 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9766 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9767 assert_eq!(
9768 parsed.warnings[0].kind,
9769 crate::warn::WarningKind::DeprecatedFlag
9770 );
9771 assert_eq!(parsed.warnings[0].name, "--output");
9773 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
9774 assert_eq!(
9775 parsed.warnings[0].render(),
9776 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
9777 );
9778 }
9779
9780 #[test]
9781 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
9782 let flag = SpecFlag::builder()
9783 .long("output")
9784 .deprecated("use --out")
9785 .deprecated_warn_at("9.0.0")
9786 .arg(SpecArg::builder().name("output").build())
9787 .build();
9788 let mut spec = spec_with_flag(flag);
9789 spec.version = Some("2.0.0".to_string());
9790
9791 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9792 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9793
9794 spec.version = Some("9.0.0".to_string());
9796 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9797 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9798 }
9799
9800 #[test]
9801 fn test_parser_with_custom_env_still_fails_when_missing() {
9802 let spec = spec_with_arg(
9803 SpecArg::builder()
9804 .name("name")
9805 .env("NAME")
9806 .required(true)
9807 .build(),
9808 );
9809 std::env::remove_var("NAME");
9810 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
9811 }
9812
9813 #[test]
9814 fn test_parser_does_not_treat_env_choice_value_as_help() {
9815 let spec = spec_with_arg(
9816 SpecArg::builder()
9817 .name("env")
9818 .env("CURRENT_ENV")
9819 .choices(["dev", "staging"])
9820 .required(false)
9821 .build(),
9822 );
9823
9824 assert_parse_err(
9825 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
9826 "Invalid choice for arg env: --help, expected one of dev, staging",
9827 );
9828 }
9829
9830 #[test]
9831 fn test_parser_does_not_treat_default_choice_value_as_help() {
9832 let spec = spec_with_flag(
9833 SpecFlag::builder()
9834 .long("env")
9835 .arg(
9836 SpecArg::builder()
9837 .name("env")
9838 .choices(["dev", "staging"])
9839 .build(),
9840 )
9841 .default_value("--help")
9842 .build(),
9843 );
9844
9845 assert_parse_err(
9846 parse_with_env(&spec, &["test"], &[]),
9847 "Invalid choice for option env: --help, expected one of dev, staging",
9848 );
9849 }
9850
9851 fn words(of: &[&str]) -> Vec<String> {
9853 of.iter().map(|s| s.to_string()).collect()
9854 }
9855
9856 #[test]
9857 fn a_command_that_needs_a_subcommand_says_so() {
9858 let spec: Spec = r#"
9862name "ex"
9863bin "ex"
9864cmd "gen" subcommand_required=#true {
9865 cmd "two" {}
9866 cmd "one" {}
9867 cmd "secret" hide=#true {}
9868 alias "g"
9869}
9870cmd "open" {
9871 cmd "sub" {}
9872}
9873"#
9874 .parse()
9875 .unwrap();
9876
9877 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9878 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9882
9883 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9885 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9886
9887 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9889
9890 parse(&spec, &words(&["ex", "open"])).unwrap();
9893 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9894 }
9895
9896 #[test]
9897 fn arg_required_else_help_observes_the_selected_commands_argv() {
9898 let spec: Spec = r#"
9899name "ex"
9900bin "ex"
9901flag "--verbose" global=#true
9902cmd "run" arg_required_else_help=#true {
9903 flag "--all"
9904}
9905"#
9906 .parse()
9907 .unwrap();
9908 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9909
9910 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9911 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9912
9913 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9916 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9917
9918 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9919 }
9920
9921 #[test]
9922 fn empty_default_with_parent_flags_observes_child_argv() {
9923 let spec: Spec = r#"
9924name "ex"
9925bin "ex"
9926default_subcommand "run"
9927default_subcommand_on_empty #true
9928flag "--verbose" global=#true
9929cmd "run" arg_required_else_help=#true {}
9930"#
9931 .parse()
9932 .unwrap();
9933 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9934 let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
9935 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9936 }
9937
9938 #[test]
9939 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9940 let spec: Spec = r#"
9941name "ex"
9942bin "ex"
9943unknown_flags "error"
9944external_subcommand #true
9945cmd "install"
9946flag "-v --verbose" global=#true
9947"#
9948 .parse()
9949 .unwrap();
9950
9951 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9952 assert_eq!(
9953 parsed.external,
9954 Some(vec!["foo".into(), "--help".into(), "bar".into()])
9955 );
9956 assert!(parsed.flags.is_empty());
9957
9958 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9960 assert_eq!(parsed.cmd.name, "install");
9961 assert!(parsed.external.is_none());
9962
9963 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9965 assert_eq!(
9966 parsed.external,
9967 Some(vec!["foo".into(), "--verbose".into()])
9968 );
9969 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9970
9971 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9973
9974 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9978 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9979 }
9980
9981 #[test]
9982 fn an_external_subcommand_satisfies_subcommand_required() {
9983 let mut spec: Spec = r#"
9984name "ex"
9985bin "ex"
9986external_subcommand #true
9987cmd "install"
9988"#
9989 .parse()
9990 .unwrap();
9991 spec.cmd.subcommand_required = true;
9992
9993 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9994 assert!(parse(&spec, &input(&["ex"])).is_err());
9995 }
9996
9997 #[test]
9998 fn a_default_subcommand_outranks_an_external_one() {
9999 let spec: Spec = r#"
10000name "ex"
10001bin "ex"
10002default_subcommand "run"
10003external_subcommand #true
10004cmd "run" {
10005 arg "[task]"
10006}
10007"#
10008 .parse()
10009 .unwrap();
10010
10011 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10012 assert_eq!(parsed.cmd.name, "run");
10013 assert!(parsed.external.is_none());
10014 assert_eq!(first_string_value(&parsed), "build");
10015 }
10016
10017 #[test]
10018 fn multicall_basename_strips_a_path_and_exe() {
10019 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10020 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10021 assert_eq!(multicall_basename("LS.EXE"), "LS");
10022 assert_eq!(multicall_basename("busybox"), "busybox");
10023 }
10024
10025 #[test]
10026 fn a_multicall_applet_is_the_first_word() {
10027 let spec: Spec = r#"
10028name "busybox"
10029bin "busybox"
10030multicall #true
10031cmd "ls" {
10032 arg "[ARGS]" var=#true
10033}
10034cmd "cat"
10035"#
10036 .parse()
10037 .unwrap();
10038
10039 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10041 assert_eq!(parsed.cmd.name, "ls");
10042 match parsed.args.values().next() {
10043 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10044 other => panic!("expected ARGS to collect -l, got {other:?}"),
10045 }
10046
10047 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10049 assert_eq!(parsed.cmd.name, "ls");
10050
10051 let mut configured = spec.clone();
10053 configured.name = "BusyBox".to_string();
10054 configured.bin = "/opt/bin/busybox.exe".to_string();
10055 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10056 assert_eq!(parsed.cmd.name, "ls");
10057
10058 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10060 assert_eq!(parsed.cmd.name, "ls");
10061
10062 let mut plain = spec.clone();
10064 plain.multicall = false;
10065 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10066 assert_eq!(parsed.cmd.name, "ls");
10067 }
10068
10069 #[test]
10070 fn a_multicall_unknown_applet_can_be_external() {
10071 let spec: Spec = r#"
10072name "busybox"
10073bin "busybox"
10074multicall #true
10075unknown_flags "error"
10076external_subcommand #true
10077cmd "ls"
10078"#
10079 .parse()
10080 .unwrap();
10081
10082 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10083 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10084
10085 let mut closed = spec.clone();
10086 closed.cmd.external_subcommand = false;
10087 assert!(parse(&closed, &input(&["wat"])).is_err());
10088 }
10089
10090 #[cfg(feature = "unstable_choices_env")]
10091 #[test]
10092 fn test_parser_arg_choices_from_custom_env() {
10093 let spec = spec_arg_choices_env("DEPLOY_ENVS");
10094
10095 let parsed =
10096 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10097 assert_eq!(first_string_value(&parsed), "bar");
10098
10099 assert_parse_err(
10100 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10101 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10102 );
10103 assert_parse_err(
10104 parse_with_env(&spec, &["test", "prod"], &[]),
10105 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10106 );
10107 }
10108
10109 #[cfg(feature = "unstable_choices_env")]
10110 #[test]
10111 fn test_parser_validates_flag_choices_from_custom_env() {
10112 let spec = spec_flag_choices_env("DEPLOY_ENVS");
10113 let parsed = parse_with_env(
10114 &spec,
10115 &["test", "--env", "baz"],
10116 &[("DEPLOY_ENVS", "foo,bar baz")],
10117 )
10118 .unwrap();
10119 assert_eq!(first_string_value(&parsed), "baz");
10120 }
10121
10122 #[cfg(feature = "unstable_choices_env")]
10123 #[test]
10124 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10125 let arg_env_spec = spec_with_arg(
10126 SpecArg::builder()
10127 .name("env")
10128 .env("CURRENT_ENV")
10129 .choices_env("DEPLOY_ENVS")
10130 .build(),
10131 );
10132 assert_parse_err(
10133 parse_with_env(
10134 &arg_env_spec,
10135 &["test"],
10136 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10137 ),
10138 "Invalid choice for arg env: prod, expected one of dev, staging",
10139 );
10140
10141 let flag_default_spec = spec_with_flag(
10142 SpecFlag::builder()
10143 .long("env")
10144 .arg(
10145 SpecArg::builder()
10146 .name("env")
10147 .choices_env("DEPLOY_ENVS")
10148 .build(),
10149 )
10150 .default_value("prod")
10151 .build(),
10152 );
10153 assert_parse_err(
10154 parse_with_env(
10155 &flag_default_spec,
10156 &["test"],
10157 &[("DEPLOY_ENVS", "dev,staging")],
10158 ),
10159 "Invalid choice for option env: prod, expected one of dev, staging",
10160 );
10161 }
10162
10163 #[test]
10164 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10165 let spec: Spec = r#"
10166 flag "-v --verbose" var=#true
10167 arg "[database]" default="myapp_dev"
10168 arg "[args...]"
10169 "#
10170 .parse()
10171 .unwrap();
10172 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10173 .into_iter()
10174 .map(String::from)
10175 .collect();
10176 let parsed = parse(&spec, &input).unwrap();
10177 let env = parsed.as_env();
10178 assert_eq!(env.get("usage_database").unwrap(), "mydb");
10179 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10180 }
10181
10182 #[test]
10183 fn test_variadic_arg_captures_unknown_flags() {
10184 let cmd = SpecCommand::builder()
10185 .name("test")
10186 .flag(SpecFlag::builder().short('v').long("verbose").build())
10187 .arg(SpecArg::builder().name("database").required(false).build())
10188 .arg(
10189 SpecArg::builder()
10190 .name("args")
10191 .required(false)
10192 .var(true)
10193 .build(),
10194 )
10195 .build();
10196 let spec = Spec {
10197 name: "test".to_string(),
10198 bin: "test".to_string(),
10199 cmd,
10200 ..Default::default()
10201 };
10202
10203 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10205 .into_iter()
10206 .map(String::from)
10207 .collect();
10208 let parsed = parse(&spec, &input).unwrap();
10209 assert_eq!(parsed.args.len(), 2);
10210 let args_val = parsed
10211 .args
10212 .iter()
10213 .find(|(a, _)| a.name == "args")
10214 .unwrap()
10215 .1;
10216 match args_val {
10217 ParseValue::MultiString(v) => {
10218 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10219 }
10220 _ => panic!("Expected MultiString, got {:?}", args_val),
10221 }
10222 }
10223
10224 #[test]
10225 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10226 let cmd = SpecCommand::builder()
10227 .name("test")
10228 .flag(SpecFlag::builder().short('v').long("verbose").build())
10229 .arg(SpecArg::builder().name("database").required(false).build())
10230 .arg(
10231 SpecArg::builder()
10232 .name("args")
10233 .required(false)
10234 .var(true)
10235 .build(),
10236 )
10237 .build();
10238 let spec = Spec {
10239 name: "test".to_string(),
10240 bin: "test".to_string(),
10241 cmd,
10242 ..Default::default()
10243 };
10244
10245 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10247 .into_iter()
10248 .map(String::from)
10249 .collect();
10250 let parsed = parse(&spec, &input).unwrap();
10251 assert_eq!(parsed.args.len(), 2);
10252 let args_val = parsed
10253 .args
10254 .iter()
10255 .find(|(a, _)| a.name == "args")
10256 .unwrap()
10257 .1;
10258 match args_val {
10259 ParseValue::MultiString(v) => {
10260 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10261 }
10262 _ => panic!("Expected MultiString, got {:?}", args_val),
10263 }
10264 }
10265
10266 #[test]
10267 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10268 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10271
10272 let input: Vec<String> = vec!["test", "--option=3"]
10275 .into_iter()
10276 .map(String::from)
10277 .collect();
10278 let parsed = parse(&spec, &input).unwrap();
10279 let env = parsed.as_env();
10280 assert_eq!(
10281 env.get("usage_other_args").map(String::as_str),
10282 Some("'--option=3'"),
10283 "expected a single --option=3 token, got {:?}",
10284 env.get("usage_other_args"),
10285 );
10286
10287 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10289 .into_iter()
10290 .map(String::from)
10291 .collect();
10292 let parsed2 = parse(&spec, &input2).unwrap();
10293 let env2 = parsed2.as_env();
10294 assert_eq!(
10295 env2.get("usage_other_args").map(String::as_str),
10296 Some("'--foo=bar' '--baz=qux'"),
10297 "expected two intact tokens, got {:?}",
10298 env2.get("usage_other_args"),
10299 );
10300
10301 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10303 .into_iter()
10304 .map(String::from)
10305 .collect();
10306 let parsed3 = parse(&spec, &input3).unwrap();
10307 let env3 = parsed3.as_env();
10308 assert_eq!(
10309 env3.get("usage_other_args").map(String::as_str),
10310 Some("positional1 '--option=3' positional2"),
10311 "expected positional args and intact flag token, got {:?}",
10312 env3.get("usage_other_args"),
10313 );
10314 }
10315
10316 #[test]
10317 fn test_allow_hyphen_values_consumes_short_flag_collision() {
10318 let spec = r#"
10319flag "-d --working-dir <DIR>"
10320flag "-a --args <ARGS>" allow_hyphen_values=#true
10321"#
10322 .parse::<Spec>()
10323 .unwrap();
10324
10325 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10326
10327 assert_eq!(parsed.flags.len(), 1);
10328 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10329 }
10330
10331 #[test]
10332 fn test_allow_hyphen_values_consumes_embedded_long_value() {
10333 let spec = r#"
10334flag "-d --working-dir <DIR>"
10335flag "-a --args <ARGS>" allow_hyphen_values=#true
10336"#
10337 .parse::<Spec>()
10338 .unwrap();
10339
10340 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10341
10342 assert_eq!(parsed.flags.len(), 1);
10343 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10344 }
10345
10346 #[test]
10347 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10348 let spec = r#"
10353flag "-a --args <ARGS>" allow_hyphen_values=#true
10354arg "[rest]..."
10355"#
10356 .parse::<Spec>()
10357 .unwrap();
10358
10359 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10360
10361 assert_eq!(flag_string_value(&parsed, "args"), "--");
10362 let rest = parsed
10363 .args
10364 .values()
10365 .next()
10366 .expect("expected the word after the separator to reach the argument");
10367 assert_eq!(rest.to_string(), "-x");
10368 }
10369
10370 #[test]
10371 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10372 let spec = r#"
10377flag "-a --args <ARGS>..." allow_hyphen_values=#true
10378"#
10379 .parse::<Spec>()
10380 .unwrap();
10381
10382 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10383
10384 let flag = parsed
10385 .flags
10386 .keys()
10387 .find(|flag| flag.name == "args")
10388 .expect("expected args flag");
10389 match parsed.flags.get(flag).expect("expected args value") {
10390 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10391 other => panic!("expected a list of values, got {other:?}"),
10392 }
10393 }
10394
10395 #[test]
10396 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10397 let spec = r#"
10398flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10399"#
10400 .parse::<Spec>()
10401 .unwrap();
10402
10403 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10404
10405 let flag = parsed
10406 .flags
10407 .keys()
10408 .find(|flag| flag.name == "args")
10409 .expect("expected args flag");
10410 let value = parsed.flags.get(flag).expect("expected args value");
10411 match value {
10412 ParseValue::MultiString(values) => {
10413 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10414 }
10415 _ => panic!("expected MultiString, got {value:?}"),
10416 }
10417 }
10418
10419 #[test]
10420 fn test_require_equals_accepts_attached_and_refuses_detached() {
10421 let spec = r#"
10422flag "--inspect <PORT>" require_equals=#true
10423"#
10424 .parse::<Spec>()
10425 .unwrap();
10426
10427 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10428 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10429
10430 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10431 let msg = format!("{err}");
10432 assert!(
10433 msg.contains("requires an argument") || msg.contains("inspect"),
10434 "detached value must be refused: {msg}"
10435 );
10436 }
10437
10438 #[test]
10439 fn boolean_flags_can_accept_attached_values_when_enabled() {
10440 let spec: Spec = r#"
10441name "ex"
10442bin "ex"
10443flag "--color" negate="--no-color" bool_value=#true
10444arg "[rest]"
10445"#
10446 .parse()
10447 .unwrap();
10448
10449 for (token, expected) in [
10450 ("--color", true),
10451 ("--color=true", true),
10452 ("--color=false", false),
10453 ("--no-color", false),
10454 ("--no-color=false", true),
10455 ] {
10456 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10457 assert!(
10458 matches!(
10459 parsed.flags.get(&spec.cmd.flags[0]),
10460 Some(ParseValue::Bool(value)) if *value == expected
10461 ),
10462 "{token}"
10463 );
10464 }
10465
10466 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10467 assert!(matches!(
10468 parsed.args.get(&spec.cmd.args[0]),
10469 Some(ParseValue::String(value)) if value == "word"
10470 ));
10471 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10472 assert!(err.to_string().contains("expected `true` or `false`"));
10473
10474 let strict: Spec = r#"
10475name "ex"
10476bin "ex"
10477args_override_self #false
10478flag "--color" negate="--no-color" bool_value=#true
10479"#
10480 .parse()
10481 .unwrap();
10482 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10483 let parsed = parse(
10484 &strict,
10485 &input(&["ex", "--color=false", "--no-color=false"]),
10486 )
10487 .unwrap();
10488 assert!(matches!(
10489 parsed.flags.get(&strict.cmd.flags[0]),
10490 Some(ParseValue::Bool(true))
10491 ));
10492 }
10493
10494 #[test]
10495 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10496 let spec = r#"
10497flag "-a --all"
10498flag "-i --inspect <PORT>" require_equals=#true
10499"#
10500 .parse::<Spec>()
10501 .unwrap();
10502
10503 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10504 let msg = format!("{err}");
10505 assert!(
10506 msg.contains("requires an argument") || msg.contains("inspect"),
10507 "bundled short must refuse the following word: {msg}"
10508 );
10509 }
10510
10511 #[test]
10512 fn test_default_missing_binds_when_the_value_is_left_off() {
10513 let spec = r#"
10514flag "-c --color <WHEN>" default_missing="always"
10515flag "-v --verbose"
10516"#
10517 .parse::<Spec>()
10518 .unwrap();
10519
10520 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10521 assert_eq!(flag_string_value(&parsed, "color"), "always");
10522
10523 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10524 assert_eq!(flag_string_value(&parsed, "color"), "never");
10525
10526 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10527 assert_eq!(flag_string_value(&parsed, "color"), "never");
10528
10529 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10530 assert_eq!(flag_string_value(&parsed, "color"), "always");
10531 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10532
10533 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10534 assert_eq!(flag_string_value(&parsed, "color"), "");
10535
10536 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10537 assert_eq!(flag_string_value(&parsed, "color"), "never");
10538
10539 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10540 assert_eq!(flag_string_value(&parsed, "color"), "always");
10541 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10542 }
10543
10544 #[test]
10545 fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10546 let spec = r#"
10547flag "--apps <N>"
10548flag "--jobs <N>" default_missing="default missing"
10549flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10550"#
10551 .parse::<Spec>()
10552 .unwrap();
10553
10554 let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10555 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10556
10557 let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10558 assert!(
10559 err.to_string().contains("unexpected word: -1"),
10560 "default_missing must keep an unopted negative value separate: {err}"
10561 );
10562
10563 let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10564 assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10565
10566 let external_spec = r#"
10567external_subcommand #true
10568flag "--apps <N>"
10569"#
10570 .parse::<Spec>()
10571 .unwrap();
10572 let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10573 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10574 assert!(parsed.external.is_none());
10575
10576 for words in [
10577 &["test", "--apps", "--jobs", "1"][..],
10578 &["test", "--apps", "--kids", "-1"][..],
10579 ] {
10580 let err = parse(&spec, &input(words)).unwrap_err();
10581 let message = err.to_string();
10582 assert!(
10583 message.contains("--apps") && message.contains("requires an argument"),
10584 "the earlier flag must report its missing value for {words:?}: {message}"
10585 );
10586 }
10587 }
10588
10589 #[test]
10590 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10591 let spec = r#"
10592flag "--bump [LEVEL]" value_optional=#true
10593flag "--verbose"
10594arg "[FILE]"
10595"#
10596 .parse::<Spec>()
10597 .unwrap();
10598
10599 let absent = parse(&spec, &input(&["test"])).unwrap();
10600 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10601
10602 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10603 let bump = bare
10604 .flags
10605 .iter()
10606 .find(|(flag, _)| flag.name == "bump")
10607 .map(|(_, value)| value)
10608 .unwrap();
10609 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10610 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10611 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10612
10613 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10614 assert_eq!(flag_string_value(&explicit, "bump"), "");
10615
10616 let corrected = parse(
10617 &spec,
10618 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10619 )
10620 .unwrap();
10621 let bump = corrected
10622 .flags
10623 .iter()
10624 .find(|(flag, _)| flag.name == "bump")
10625 .map(|(_, value)| value)
10626 .unwrap();
10627 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10628
10629 let collecting = r#"
10630flag "--tag [TAG]..." value_optional=#true
10631flag "--verbose"
10632"#
10633 .parse::<Spec>()
10634 .unwrap();
10635 let valued = parse(
10636 &collecting,
10637 &input(&["test", "--tag", "one", "two", "--verbose"]),
10638 )
10639 .unwrap();
10640 let tag = valued
10641 .flags
10642 .iter()
10643 .find(|(flag, _)| flag.name == "tag")
10644 .map(|(_, value)| value)
10645 .unwrap();
10646 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10647 }
10648
10649 #[test]
10650 fn test_repeatable_bare_optional_values_count_each_occurrence() {
10651 let spec = r#"
10652flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10653"#
10654 .parse::<Spec>()
10655 .unwrap();
10656
10657 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10658 let tag = parsed
10659 .flags
10660 .iter()
10661 .find(|(flag, _)| flag.name == "tag")
10662 .map(|(_, value)| value)
10663 .unwrap();
10664 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10665
10666 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10667 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10668 }
10669
10670 #[test]
10671 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10672 let spec = r#"
10673flag "--tag [TAG]..." var=#true value_optional=#true
10674flag "--verbose"
10675"#
10676 .parse::<Spec>()
10677 .unwrap();
10678
10679 for argv in [
10680 &["test", "--tag", "one", "two"][..],
10681 &["test", "--tag", "one", "two", "--verbose"][..],
10682 &["test", "--tag", "one", "--tag", "two"][..],
10683 ] {
10684 let parsed = parse(&spec, &input(argv)).unwrap();
10685 let tag = parsed
10686 .flags
10687 .iter()
10688 .find(|(flag, _)| flag.name == "tag")
10689 .map(|(_, value)| value)
10690 .unwrap();
10691 assert!(
10692 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10693 "argv={argv:?}: {tag:?}"
10694 );
10695 }
10696
10697 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10698 let tag = bare
10699 .flags
10700 .iter()
10701 .find(|(flag, _)| flag.name == "tag")
10702 .map(|(_, value)| value)
10703 .unwrap();
10704 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10705 }
10706
10707 #[test]
10708 fn test_default_missing_with_require_equals_refuses_the_following_word() {
10709 let spec = r#"
10710flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10711arg "[rest]"
10712"#
10713 .parse::<Spec>()
10714 .unwrap();
10715
10716 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10717 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10718
10719 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10720 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10721
10722 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10724 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10725 assert_eq!(
10726 parsed
10727 .args
10728 .values()
10729 .next()
10730 .map(|v| v.to_string())
10731 .as_deref(),
10732 Some("80")
10733 );
10734
10735 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10736 assert_eq!(flag_string_value(&parsed, "inspect"), "");
10737 }
10738
10739 #[test]
10740 fn test_default_missing_must_be_a_choice() {
10741 let spec = r#"
10742flag "--color <WHEN>" default_missing="always" {
10743 choices "auto" "always" "never"
10744}
10745"#
10746 .parse::<Spec>()
10747 .unwrap();
10748
10749 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10750 assert_eq!(flag_string_value(&parsed, "color"), "always");
10751
10752 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10753 assert_eq!(flag_string_value(&parsed, "color"), "never");
10754
10755 let spec = r#"
10756flag "--color <WHEN>" default_missing="wat" {
10757 choices "auto" "always" "never"
10758}
10759"#
10760 .parse::<Spec>()
10761 .unwrap();
10762
10763 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10764 let msg = format!("{err}");
10765 assert!(
10766 msg.contains("Invalid choice for option color: wat"),
10767 "missing default has to pass choices the same way a typed value does: {msg}"
10768 );
10769
10770 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
10771 let msg = format!("{err}");
10772 assert!(
10773 msg.contains("Invalid choice for option color: wat"),
10774 "an attached value that is not a choice is still refused: {msg}"
10775 );
10776
10777 let spec = r#"
10778flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
10779 choices "9229" "80"
10780}
10781arg "[rest]"
10782"#
10783 .parse::<Spec>()
10784 .unwrap();
10785
10786 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
10787 let msg = format!("{err}");
10788 assert!(
10789 msg.contains("Invalid choice for option inspect: wat"),
10790 "require_equals still binds the missing string, so the error is the choice: {msg}"
10791 );
10792 }
10793
10794 #[test]
10795 fn test_hyphen_values_still_start_short_flag_parsing() {
10796 let spec = r#"
10797flag "-d --working-dir <DIR>"
10798flag "-a --args <ARGS>"
10799"#
10800 .parse::<Spec>()
10801 .unwrap();
10802
10803 let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
10804 let message = err.to_string();
10805 assert!(
10806 message.contains("--args") && message.contains("requires an argument"),
10807 "the recognized -d must leave the earlier -a missing: {message}"
10808 );
10809 }
10810
10811 mod available_flags {
10814 use super::*;
10815
10816 fn spec() -> Spec {
10817 r#"
10818bin "test"
10819flag "-v --verbose" global=#true
10820flag "--raw" global=#true effect="write"
10821flag "--local-only"
10822cmd "run" {
10823 flag "-r --raw"
10824 flag "-w --watch"
10825 cmd "once"
10826}
10827"#
10828 .parse::<Spec>()
10829 .unwrap()
10830 }
10831
10832 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
10833 let mut chain = vec![&spec.cmd];
10834 for segment in path {
10835 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
10836 }
10837 chain
10838 }
10839
10840 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
10841 let mut names: Vec<_> = available_flags(&chain(spec, path))
10842 .iter()
10843 .map(|f| f.name.clone())
10844 .collect();
10845 names.sort();
10846 names
10847 }
10848
10849 #[test]
10850 fn an_empty_chain_yields_nothing() {
10851 assert!(available_flags(&[]).is_empty());
10852 }
10853
10854 #[test]
10855 fn the_root_gets_its_own_flags() {
10856 let spec = spec();
10857 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
10858 }
10859
10860 #[test]
10861 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
10862 let spec = spec();
10863 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10864 }
10865
10866 #[test]
10867 fn a_re_declared_global_is_listed_once() {
10868 let spec = r#"
10871bin "test"
10872flag "-y --yes" global=#true effect="write"
10873cmd "rm" {
10874 flag "-y --yes"
10875}
10876"#
10877 .parse::<Spec>()
10878 .unwrap();
10879 let flags = available_flags(&chain(&spec, &["rm"]));
10880 assert_eq!(flags.len(), 1, "{flags:?}");
10881 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10882 }
10883
10884 #[test]
10885 fn a_re_declared_global_keeps_the_globals_declaration() {
10886 let spec = spec();
10890 let flags = available_flags(&chain(&spec, &["run"]));
10891 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10892 assert!(raw.global);
10893 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10894 assert_eq!(raw.short, ['r']);
10895 }
10896
10897 #[test]
10898 fn it_matches_what_a_parse_accepts() {
10899 let spec = spec();
10902 for path in [vec![], vec!["run"], vec!["run", "once"]] {
10903 let argv = std::iter::once("test".to_string())
10904 .chain(path.iter().map(|s| s.to_string()))
10905 .collect::<Vec<_>>();
10906 let parsed = parse_partial(&spec, &argv).unwrap();
10907
10908 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10909 .map(|f| f.name.clone())
10910 .collect();
10911 from_parse.sort();
10912 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10913 }
10914 }
10915 }
10916
10917 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10922 parsed
10923 .tokens
10924 .iter()
10925 .find(|token| token.index == index)
10926 .unwrap_or_else(|| panic!("no token at {index}"))
10927 .roles
10928 .iter()
10929 .map(render_role)
10930 .collect()
10931 }
10932
10933 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10934 parsed
10935 .flag_origins
10936 .iter()
10937 .find(|(f, _)| f.name == flag)
10938 .map(|(_, origins)| origins.clone())
10939 .unwrap_or_default()
10940 }
10941
10942 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10943 let env = env
10944 .iter()
10945 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10946 .collect();
10947 Parser::new(spec)
10948 .with_env(env)
10949 .explain(&input(words))
10950 .unwrap()
10951 }
10952
10953 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10954 explain_with_env(spec, words, &[])
10955 }
10956
10957 #[test]
10958 fn an_attached_long_value_is_recorded_on_the_flag_token() {
10959 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10960 .parse()
10961 .unwrap();
10962
10963 let parsed = explain(&spec, &["ex", "--env=prod"]);
10964
10965 assert_eq!(roles(&parsed, 0), ["program"]);
10966 assert_eq!(
10967 roles(&parsed, 1),
10968 ["flag env as --env", "value of env = [\"prod\"], attached"]
10969 );
10970 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10973 }
10974
10975 #[test]
10976 fn a_detached_long_value_is_recorded_on_its_own_token() {
10977 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10978 .parse()
10979 .unwrap();
10980
10981 let parsed = explain(&spec, &["ex", "--env", "prod"]);
10982
10983 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10984 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10985 }
10986
10987 #[test]
10988 fn a_short_bundle_is_attributed_to_the_bundle_token() {
10989 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10990 .parse()
10991 .unwrap();
10992
10993 let parsed = explain(&spec, &["ex", "-abj8"]);
10994
10995 assert_eq!(
10998 roles(&parsed, 1),
10999 [
11000 "flag a as -a",
11001 "flag b as -b",
11002 "flag j as -j",
11003 "value of j = [\"8\"], attached",
11004 ]
11005 );
11006 assert_eq!(parsed.tokens.len(), 2);
11007 }
11008
11009 #[test]
11010 fn a_delimiter_splits_one_token_into_several_values() {
11011 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11012 .parse()
11013 .unwrap();
11014
11015 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11016
11017 assert_eq!(
11018 roles(&parsed, 2),
11019 ["value of tags = [\"a\", \"b\", \"c\"]"],
11020 "the values meant, not the word typed"
11021 );
11022 }
11023
11024 #[test]
11025 fn a_separator_and_the_words_after_it_are_distinguished() {
11026 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11027 .parse()
11028 .unwrap();
11029
11030 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11031
11032 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11033 assert_eq!(roles(&parsed, 2), ["separator"]);
11034 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11036 }
11037
11038 #[test]
11039 fn a_second_separator_is_data() {
11040 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11041 .parse()
11042 .unwrap();
11043
11044 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11045
11046 assert_eq!(roles(&parsed, 1), ["separator"]);
11047 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11048 }
11049
11050 #[test]
11051 fn an_unknown_flag_says_what_took_it() {
11052 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11053 .parse()
11054 .unwrap();
11055
11056 let parsed = explain(&spec, &["ex", "--wat"]);
11057
11058 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11061 }
11062
11063 #[test]
11064 fn a_subcommand_word_is_not_a_positional() {
11065 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
11066 .parse()
11067 .unwrap();
11068
11069 let parsed = explain(&spec, &["ex", "build", "a"]);
11070
11071 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11072 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11073 }
11074
11075 #[test]
11076 fn a_multicall_applet_is_read_at_argv0() {
11077 let spec: Spec =
11078 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
11079 .parse()
11080 .unwrap();
11081
11082 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11083
11084 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11087 assert!(parsed.tokens[0].synthesized);
11088 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11089 }
11090
11091 #[test]
11092 fn words_the_parse_never_reached_say_so() {
11093 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11094 .parse()
11095 .unwrap();
11096
11097 let parsed = Parser::new(&spec)
11098 .explain(&input(&["ex", "--help", "a"]))
11099 .unwrap();
11100
11101 assert_eq!(roles(&parsed, 2), ["unread"]);
11102 }
11103
11104 #[test]
11105 fn an_env_origin_names_the_variable_that_fired() {
11106 let spec: Spec =
11107 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11108 .parse()
11109 .unwrap();
11110
11111 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11112 assert_eq!(
11113 origins(&primary, "token"),
11114 [ValueOrigin::Env("EX_TOKEN".to_string())]
11115 );
11116
11117 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11120 assert_eq!(
11121 origins(&fallback, "token"),
11122 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11123 );
11124 }
11125
11126 #[test]
11127 fn a_default_origin_is_recorded_for_flags_and_args() {
11128 let spec: Spec =
11129 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11130 .parse()
11131 .unwrap();
11132
11133 let parsed = explain(&spec, &["ex"]);
11134
11135 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11136 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11137 assert_eq!(arg.name, "src");
11138 assert_eq!(origins, &[ValueOrigin::Default]);
11139 }
11140
11141 #[test]
11142 fn a_default_if_origin_carries_the_condition_that_fired() {
11143 let spec: Spec = r#"
11144name "ex"
11145bin "ex"
11146flag "--profile <p>"
11147flag "--strict" {
11148 default_if "--profile" "prod" "true"
11149}
11150 "#
11151 .parse()
11152 .unwrap();
11153
11154 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11155
11156 assert_eq!(
11159 origins(&parsed, "strict"),
11160 [ValueOrigin::DefaultIf {
11161 selector: "--profile".to_string(),
11162 when: Some("prod".to_string()),
11163 }]
11164 );
11165 }
11166
11167 #[test]
11168 fn a_bare_optional_value_flag_records_default_missing() {
11169 let spec: Spec =
11170 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11171 .parse()
11172 .unwrap();
11173
11174 let parsed = explain(&spec, &["ex", "--color", "-v"]);
11175
11176 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11179 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11180 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11181 }
11182
11183 #[test]
11184 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11185 let spec: Spec =
11186 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11187 .parse()
11188 .unwrap();
11189
11190 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11191
11192 assert_eq!(
11194 roles(&parsed, 1),
11195 [
11196 "flag color as --color",
11197 "value of color = [\"red\"], attached"
11198 ]
11199 );
11200 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11201 }
11202
11203 #[test]
11204 fn an_override_names_the_flag_that_did_it() {
11205 let spec: Spec =
11206 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11207 .parse()
11208 .unwrap();
11209
11210 let parsed = explain(&spec, &["ex", "--loud"]);
11211
11212 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11215 assert!(origins(&parsed, "quiet").is_empty());
11216 }
11217
11218 #[test]
11219 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11220 let spec: Spec = r#"
11221name "ex"
11222bin "ex"
11223cmd "run" restart_token=":::" {
11224 arg "<task>" default="build"
11225}
11226 "#
11227 .parse()
11228 .unwrap();
11229
11230 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11231
11232 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11236 assert_eq!(roles(&parsed, 3), ["restart"]);
11239 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11240 assert!(parsed.arg_origins.is_empty());
11241 }
11242
11243 #[test]
11244 fn a_value_terminator_says_which_run_it_ended() {
11245 let spec: Spec = r#"
11246name "ex"
11247bin "ex"
11248flag "--exec <cmd>..." value_terminator=";"
11249arg "<src>"
11250 "#
11251 .parse()
11252 .unwrap();
11253
11254 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11255
11256 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11257 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11260 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11261 }
11262
11263 #[test]
11264 fn an_args_value_terminator_says_which_run_it_ended() {
11265 let spec: Spec = r#"
11266name "ex"
11267bin "ex"
11268arg "<files>..." value_terminator=";"
11269arg "[dest]"
11270 "#
11271 .parse()
11272 .unwrap();
11273
11274 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11275
11276 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11277 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11278 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11279 }
11280
11281 #[test]
11282 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11283 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11284 .parse()
11285 .unwrap();
11286
11287 let parsed = Parser::new(&spec)
11288 .explain(&input(&["ex", "--env=prod"]))
11289 .unwrap();
11290
11291 assert!(Parser::new(&spec)
11294 .parse(&input(&["ex", "--env=prod"]))
11295 .is_err());
11296 assert_eq!(
11297 roles(&parsed, 1),
11298 ["flag env as --env", "value of env = [\"prod\"], attached"]
11299 );
11300 assert!(
11301 parsed.errors.iter().any(|e| e.to_string().contains("src")),
11302 "{:?}",
11303 parsed.errors
11304 );
11305 }
11306
11307 #[test]
11308 fn an_external_subcommand_forwards_whole_tokens() {
11309 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11310 .parse()
11311 .unwrap();
11312
11313 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11314
11315 assert_eq!(roles(&parsed, 1), ["external"]);
11316 assert_eq!(roles(&parsed, 2), ["external"]);
11317 }
11318
11319 #[test]
11320 fn a_view_keeps_the_callers_argv_positions() {
11321 let spec: Spec = r#"
11322bin "ex"
11323view "runner" root="run"
11324cmd "run" {
11325 flag "--token <token>"
11326}
11327 "#
11328 .parse()
11329 .unwrap();
11330
11331 let parsed = explain(&spec, &["runner", "--token", "secret"]);
11332
11333 assert_eq!(roles(&parsed, 0), ["program"]);
11336 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11337 }
11338
11339 #[test]
11340 fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11341 let spec: Spec = r#"
11342name "ex"
11343bin "ex"
11344default_subcommand "run"
11345default_subcommand_on_empty #true
11346flag "--value <value>"
11347cmd "run" arg_required_else_help=#true
11348"#
11349 .parse()
11350 .unwrap();
11351 let err = Parser::new(&spec)
11352 .parse(&input(&["ex", "--value"]))
11353 .unwrap_err();
11354 assert!(err.to_string().contains("requires an argument"), "{err}");
11355 assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11356 }
11357
11358 #[test]
11359 fn empty_default_honors_parent_args_conflict_policy() {
11360 let spec: Spec = r#"
11361name "ex"
11362bin "ex"
11363default_subcommand "run"
11364default_subcommand_on_empty #true
11365args_conflicts_with_subcommands #true
11366flag "--verbose"
11367cmd "run"
11368"#
11369 .parse()
11370 .unwrap();
11371 let err = Parser::new(&spec)
11372 .parse(&input(&["ex", "--verbose"]))
11373 .unwrap_err();
11374 assert!(
11375 err.to_string().contains("cannot be used with arguments"),
11376 "{err}"
11377 );
11378 }
11379
11380 #[test]
11381 fn empty_default_mount_matches_explicit_selection() {
11382 let spec: Spec = r#"
11383name "ex"
11384bin "ex"
11385default_subcommand "run"
11386default_subcommand_on_empty #true
11387cmd "run" { mount run="discover" }
11388"#
11389 .parse()
11390 .unwrap();
11391 let mut mounts = HashMap::new();
11392 mounts.insert(
11393 "discover".to_string(),
11394 "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11395 );
11396 for argv in [
11397 vec!["ex".to_string()],
11398 vec!["ex".to_string(), "run".to_string()],
11399 ] {
11400 let out = Parser::new(&spec)
11401 .with_mount_outputs(mounts.clone())
11402 .parse(&argv)
11403 .unwrap();
11404 assert_eq!(out.cmds.last().unwrap().name, "run");
11405 assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11406 }
11407 }
11408
11409 #[test]
11410 fn pending_parent_value_stays_pending_in_partial_parse() {
11411 let spec: Spec = r#"
11412name "ex"
11413bin "ex"
11414default_subcommand "run"
11415default_subcommand_on_empty #true
11416flag "--value <value>"
11417cmd "run" {}
11418"#
11419 .parse()
11420 .unwrap();
11421 let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11422 assert_eq!(out.cmd.name, "ex");
11423 assert_eq!(out.flag_awaiting_value[0].name, "value");
11424 }
11425
11426 #[test]
11427 fn optional_default_missing_still_selects_empty_child() {
11428 let spec: Spec = r#"
11429name "ex"
11430bin "ex"
11431default_subcommand "run"
11432default_subcommand_on_empty #true
11433flag "--value <value>" default_missing="fallback"
11434cmd "run" {}
11435"#
11436 .parse()
11437 .unwrap();
11438 let out = Parser::new(&spec)
11439 .parse(&input(&["ex", "--value"]))
11440 .unwrap();
11441 assert_eq!(out.cmd.name, "run");
11442 assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11443 }
11444
11445 #[test]
11446 fn empty_default_resets_cursor_after_root_sigil() {
11447 let spec: Spec = r#"
11448name "ex"
11449bin "ex"
11450default_subcommand "run"
11451default_subcommand_on_empty #true
11452arg "[tool]..." sigil="@"
11453cmd "run" { arg "[path]" }
11454"#
11455 .parse()
11456 .unwrap();
11457 let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11458 assert_eq!(out.cmd.name, "run");
11459 assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11460 }
11461
11462 #[test]
11463 fn root_clause_positional_prevents_empty_default() {
11464 let spec: Spec = r#"
11465name "ex"
11466bin "ex"
11467default_subcommand "run"
11468default_subcommand_on_empty #true
11469clause "tasks" {
11470 arg "<number>" allow_negative_numbers=#true
11471}
11472cmd "run" { arg "[task]" }
11473"#
11474 .parse()
11475 .unwrap();
11476 let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11477 assert!(
11478 out.cmds.iter().all(|cmd| cmd.name != "run"),
11479 "cmds={:?} args={:?}",
11480 out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11481 out.args
11482 );
11483 let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11484 assert_eq!(
11485 out.clauses["tasks"][0].get(number).unwrap().to_string(),
11486 "-7"
11487 );
11488 }
11489}