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