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