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 run_commands: bool,
691}
692
693impl<'a> Parser<'a> {
694 pub fn new(spec: &'a Spec) -> Self {
696 Self {
697 spec,
698 env: None,
699 mount_outputs: None,
700 run_commands: true,
701 }
702 }
703
704 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
709 self.env = Some(env);
710 self
711 }
712
713 pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
720 self.mount_outputs = Some(outputs);
721 self
722 }
723
724 pub fn without_running_commands(mut self) -> Self {
731 self.run_commands = false;
732 self
733 }
734
735 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
739 let out = self.parse_collecting(input)?;
740 if let Some(err) = out
741 .errors
742 .iter()
743 .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
744 {
745 bail!("{err}");
746 }
747 if !out.errors.is_empty() {
748 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
749 }
750 Ok(out)
751 }
752
753 pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
764 self.parse_collecting(input)
765 }
766
767 pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
782 let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
783 let mut trace = Trace::new(input);
784 match parse_partial_traced(
785 self.spec,
786 input,
787 self.env.as_ref(),
788 self.mount_outputs.as_ref(),
789 MountTiming::WhenAWordIsUnknown,
790 true,
791 &mut trace,
792 ) {
793 Ok((out, _)) => Ok(out),
796 Err(_) => Err(trace.tokens),
797 }
798 }
799
800 fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
801 let _runs = crate::spec::choices::RunScope::enter();
804 let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
805 let custom_env = self.env.as_ref();
806 let (mut out, overridden_flags) = parse_partial_with_env(
807 self.spec,
808 input,
809 custom_env,
810 self.mount_outputs.as_ref(),
811 MountTiming::WhenAWordIsUnknown,
812 false,
813 )?;
814 restore_current_clause(&mut out);
815 trace!("{out:?}");
816
817 while try_bind_default_missing(
823 &mut out.flags,
824 &mut out.flag_awaiting_value,
825 custom_env,
826 &mut out.flag_origins,
827 )? {}
828 if let Some(flag) = out.flag_awaiting_value.first() {
829 let token = flag
830 .long
831 .first()
832 .map(|l| format!("--{l}"))
833 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
834 .unwrap_or_else(|| flag.name.clone());
835 let rendered = input.join(" ");
836 let span = rendered
837 .rfind(&token)
838 .map(|at| (at, token.len()))
839 .unwrap_or((0, 0));
840 return Err(UsageErr::InvalidFlag {
841 token,
842 reason: "requires an argument".to_string(),
843 span: span.into(),
844 input: rendered,
845 }
846 .into());
847 }
848
849 collect_deprecations(&mut out);
853
854 let get_env = |key: &str| -> Option<String> {
855 if let Some(env_map) = custom_env {
856 env_map.get(key).cloned()
857 } else {
858 std::env::var(key).ok()
859 }
860 };
861
862 for arg in active_args(&out.cmd) {
867 if out.cmd.clause.is_some() {
870 break;
871 }
872 if out.args.contains_key(arg) {
873 continue;
874 }
875 if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
876 if arg_env_is_deprecated(arg, env_name) {
877 out.warnings
878 .push(Warning::env(env_name, arg_current_env(arg)));
879 }
880 let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
881 validate_choice_values(
882 ChoiceTarget::arg(arg),
883 &values,
884 arg.choices.as_ref(),
885 custom_env,
886 )?;
887 let parsed = if arg.var {
888 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
889 ParseValue::MultiString(values)
890 } else {
891 ParseValue::String(values.into_iter().next().unwrap_or_default())
892 };
893 out.args.insert(Arc::new(arg.clone()), parsed);
894 out.arg_origins
895 .entry(Arc::new(arg.clone()))
896 .or_default()
897 .push(ValueOrigin::Env(env_name.to_string()));
898 continue;
899 }
900 if !arg.default.is_empty() {
901 if arg.var {
903 let values = split_fallback_values(&arg.default, arg.delimiter);
904 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
905 validate_choice_values(
906 ChoiceTarget::arg(arg),
907 &values,
908 arg.choices.as_ref(),
909 custom_env,
910 )?;
911 out.args
913 .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
914 out.arg_origins
915 .entry(Arc::new(arg.clone()))
916 .or_default()
917 .push(ValueOrigin::Default);
918 } else {
919 validate_choice_value(
920 ChoiceTarget::arg(arg),
921 &arg.default[0],
922 arg.choices.as_ref(),
923 custom_env,
924 )?;
925 out.args.insert(
927 Arc::new(arg.clone()),
928 ParseValue::String(arg.default[0].clone()),
929 );
930 out.arg_origins
931 .entry(Arc::new(arg.clone()))
932 .or_default()
933 .push(ValueOrigin::Default);
934 }
935 }
936 }
937
938 let flags: Vec<Arc<SpecFlag>> = out
943 .available_flags
944 .values()
945 .filter(|flag| !is_clause_scoped_flag(&out, flag))
946 .cloned()
947 .collect();
948 for flag in &flags {
949 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
950 continue;
951 }
952 if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
953 if let Some(warning) = flag_deprecation(flag) {
957 out.warnings.push(warning);
958 }
959 if flag_env_is_deprecated(flag, env_name) {
960 out.warnings
961 .push(Warning::env(env_name, flag_current_env(flag)));
962 }
963 if let Some(arg) = flag.arg.as_ref() {
964 let values =
965 split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
966 validate_choice_values(
967 ChoiceTarget::option(flag),
968 &values,
969 arg.choices.as_ref(),
970 custom_env,
971 )?;
972 let parsed = if flag.var || arg.var {
973 if flag.var {
974 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
975 }
976 if arg.var {
977 validate_flag_arg_fallback_count(
978 flag,
979 arg,
980 values.len(),
981 &mut out.errors,
982 );
983 }
984 ParseValue::MultiString(values)
985 } else {
986 ParseValue::String(values.into_iter().next().unwrap_or_default())
987 };
988 out.flags.insert(Arc::clone(flag), parsed);
989 } else {
990 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
991 out.flags
992 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
993 }
994 out.flag_origins
995 .entry(Arc::clone(flag))
996 .or_default()
997 .push(ValueOrigin::Env(env_name.to_string()));
998 }
999 }
1000 let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
1006 for flag in &flags {
1007 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1008 continue;
1009 }
1010 if let Some(condition) = flag.default_if.iter().find(|condition| {
1011 default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
1012 }) {
1013 from_default_if.push((Arc::clone(flag), condition.clone()));
1014 }
1015 }
1016 for (flag, condition) in &from_default_if {
1017 bind_flag_fallback(
1021 flag,
1022 std::slice::from_ref(&condition.value),
1023 &mut out,
1024 custom_env,
1025 ValueOrigin::DefaultIf {
1026 selector: condition.selector.clone(),
1027 when: condition.when.clone(),
1028 },
1029 )?;
1030 }
1031 for flag in &flags {
1032 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1033 continue;
1034 }
1035 if !flag.default.is_empty() {
1036 bind_flag_fallback(
1037 flag,
1038 &flag.default,
1039 &mut out,
1040 custom_env,
1041 ValueOrigin::Default,
1042 )?;
1043 continue;
1044 }
1045 if let Some(arg) = flag.arg.as_ref()
1046 && !arg.default.is_empty()
1047 {
1048 bind_flag_fallback(
1049 flag,
1050 &arg.default,
1051 &mut out,
1052 custom_env,
1053 ValueOrigin::Default,
1054 )?;
1055 }
1056 }
1057 finalize_current_clause(&mut out);
1062 let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1063 validate_clause_relationships(
1064 &mut out,
1065 &overridden_flags,
1066 custom_env,
1067 clause_explicit.as_deref(),
1068 );
1069 for (arg, parsed) in &out.args {
1074 validate_expression(
1075 &arg.name,
1076 arg.validate.as_deref(),
1077 arg.validate_error.as_deref(),
1078 parsed,
1079 &mut out.errors,
1080 );
1081 }
1082 if let Some(clause) = &out.cmd.clause {
1083 let mut clause_errors = Vec::new();
1084 for (index, instance) in out
1085 .clauses
1086 .get(&clause.name)
1087 .into_iter()
1088 .flatten()
1089 .enumerate()
1090 {
1091 for arg in &clause.args {
1092 let Some(value) = instance.get(arg) else {
1093 if arg.required {
1094 clause_errors.push(UsageErr::MissingClauseArg {
1095 clause: clause.name.clone(),
1096 instance: index + 1,
1097 arg: arg.name.clone(),
1098 });
1099 }
1100 continue;
1101 };
1102 if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1103 if let Some(min) = arg.var_min
1104 && values.len() < min
1105 {
1106 clause_errors.push(UsageErr::VarArgTooFew {
1107 name: format!(
1108 "{} instance {}: {}",
1109 clause.name,
1110 index + 1,
1111 arg.name
1112 ),
1113 min,
1114 got: values.len(),
1115 });
1116 }
1117 if let Some(max) = arg.var_max
1118 && values.len() > max
1119 {
1120 clause_errors.push(UsageErr::VarArgTooMany {
1121 name: format!(
1122 "{} instance {}: {}",
1123 clause.name,
1124 index + 1,
1125 arg.name
1126 ),
1127 max,
1128 got: values.len(),
1129 });
1130 }
1131 }
1132 }
1133 }
1134 out.errors.extend(clause_errors);
1135 }
1136 let clause_flag_names = out
1137 .cmd
1138 .clause
1139 .iter()
1140 .flat_map(|clause| &clause.flags)
1141 .map(|flag| flag.name.as_str())
1142 .collect::<HashSet<_>>();
1143 for (flag, parsed) in out
1144 .flags
1145 .iter()
1146 .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1147 {
1148 if let Some(arg) = &flag.arg {
1149 validate_expression(
1150 &flag.name,
1151 arg.validate.as_deref(),
1152 arg.validate_error.as_deref(),
1153 parsed,
1154 &mut out.errors,
1155 );
1156 }
1157 }
1158 crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1161 Ok(out)
1162 }
1163}
1164
1165#[must_use = "parsing result should be used"]
1172pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1173 Parser::new(spec).parse(input)
1174}
1175
1176#[must_use = "parsing result should be used"]
1180pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1181 parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1182}
1183
1184pub fn multicall_basename(argv0: &str) -> &str {
1187 let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1188 match name.get(name.len().saturating_sub(4)..) {
1189 Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1190 _ => name,
1191 }
1192}
1193
1194pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1199 let base = multicall_basename(argv0);
1200 if !name.is_empty() && base == multicall_basename(name) {
1201 return None;
1202 }
1203 if let Some(bin) = bin
1204 && !bin.is_empty()
1205 && base == multicall_basename(bin)
1206 {
1207 return None;
1208 }
1209 Some(base)
1210}
1211
1212#[derive(Clone, Copy, PartialEq, Eq)]
1221enum MountTiming {
1222 Eager,
1223 WhenAWordIsUnknown,
1224}
1225
1226struct Token {
1236 word: String,
1237 argv: usize,
1243 binding: Option<(Arc<SpecFlag>, usize)>,
1255}
1256
1257struct DefaultBundle {
1259 argv: usize,
1260 flags: BTreeMap<String, Arc<SpecFlag>>,
1261 parent_keys: HashSet<String>,
1262}
1263
1264impl DefaultBundle {
1265 fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1267 self.flags.get(key).map(|flag| {
1268 (
1269 Arc::clone(flag),
1270 usize::from(!self.parent_keys.contains(key)),
1271 )
1272 })
1273 }
1274}
1275
1276impl Token {
1277 fn new(word: String, argv: usize) -> Self {
1278 Self {
1279 word,
1280 argv,
1281 binding: None,
1282 }
1283 }
1284}
1285
1286struct Trace {
1292 tokens: Vec<TokenBinding>,
1293}
1294
1295impl Trace {
1296 fn new(input: &[String]) -> Self {
1297 Self {
1298 tokens: input
1299 .iter()
1300 .enumerate()
1301 .map(|(index, word)| TokenBinding {
1302 index,
1303 word: word.clone(),
1304 synthesized: false,
1305 roles: vec![],
1306 })
1307 .collect(),
1308 }
1309 }
1310
1311 fn record(&mut self, argv: usize, role: TokenRole) {
1312 if let Some(token) = self.tokens.get_mut(argv) {
1313 token.roles.push(role);
1314 }
1315 }
1316
1317 fn note_synthesized(&mut self, argv: usize) {
1319 if let Some(token) = self.tokens.get_mut(argv) {
1320 token.synthesized = true;
1321 }
1322 }
1323
1324 fn close(&mut self, unread: &VecDeque<Token>) {
1326 for token in unread {
1327 self.record(token.argv, TokenRole::Unread);
1328 }
1329 }
1330}
1331
1332fn parse_partial_with_env(
1333 spec: &Spec,
1334 input: &[String],
1335 custom_env: Option<&HashMap<String, String>>,
1336 mount_outputs: Option<&HashMap<String, String>>,
1337 mount_timing: MountTiming,
1338 validate_clauses: bool,
1339) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1340 let mut trace = Trace::new(input);
1341 parse_partial_traced(
1342 spec,
1343 input,
1344 custom_env,
1345 mount_outputs,
1346 mount_timing,
1347 validate_clauses,
1348 &mut trace,
1349 )
1350}
1351
1352fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1355 let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1356 let parent = gather_flags(root);
1357 let child = gather_flags(default);
1358 let path = std::slice::from_ref(root);
1359 let mut at = None;
1360 let mut i = 0;
1361 while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1362 let scope = if at.is_some() { default } else { root };
1363 if token == "--"
1364 || token == "-"
1365 || scope
1366 .clause
1367 .as_ref()
1368 .is_some_and(|c| c.separator.as_deref() == Some(token))
1369 {
1370 return at;
1371 }
1372 if !is_flag_like(token) {
1373 if at.is_none()
1374 && (root.find_subcommand(token).is_some()
1375 || (token == "help"
1376 && spec.disable_help != Some(true)
1377 && !root.disable_help_subcommand))
1378 {
1379 return None;
1380 }
1381 return at;
1382 }
1383 let mut value_flag = None;
1384 let mut attached = None;
1385 if token.starts_with("--") {
1386 let (name, value) = token
1387 .split_once('=')
1388 .map_or((token, None), |(n, v)| (n, Some(v)));
1389 if !parent.contains_key(name)
1390 && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1391 {
1392 i += 1;
1393 continue;
1394 }
1395 let flag = parent.get(name).or_else(|| child.get(name));
1396 let Some(flag) = flag else {
1397 return at;
1398 };
1399 if !parent.contains_key(name) {
1400 at.get_or_insert(i);
1401 }
1402 if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1403 value_flag = Some(flag);
1404 attached = value;
1405 }
1406 } else {
1407 for (offset, letter) in token.char_indices().skip(1) {
1408 let key = format!("-{letter}");
1409 if !parent.contains_key(&key) && supplies_short(spec, path, letter) {
1410 continue;
1411 }
1412 let flag = parent.get(&key).or_else(|| child.get(&key));
1413 let Some(flag) = flag else {
1414 return at;
1415 };
1416 if !parent.contains_key(&key) {
1417 at.get_or_insert(i);
1418 }
1419 if flag.arg.is_some() {
1420 value_flag = Some(flag);
1421 let rest = &token[offset + letter.len_utf8()..];
1422 attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1423 break;
1424 }
1425 }
1426 }
1427 i += 1;
1428 if let Some(flag) = value_flag {
1429 let scope = if at.is_some() { default } else { root };
1430 let is_separator = |next: &str| {
1431 scope
1432 .clause
1433 .as_ref()
1434 .is_some_and(|c| c.separator.as_deref() == Some(next))
1435 };
1436 let first = if attached.is_some() {
1437 attached
1438 } else {
1439 input
1440 .get(i)
1441 .map(|t| t.word.as_str())
1442 .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1443 .inspect(|_| i += 1)
1444 };
1445 if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1446 return at;
1447 }
1448 let arg = flag.arg.as_ref().expect("value flag");
1449 if arg.var {
1450 let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1451 let mut count = first.map_or(0, count_values);
1452 while arg.var_max.is_none_or(|max| count < max) {
1453 let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1454 break;
1455 };
1456 if is_separator(next) {
1457 break;
1458 }
1459 if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1460 if next != "--" {
1461 i += 1;
1462 }
1463 break;
1464 }
1465 if is_flag_like(next)
1466 && !(arg.allow_negative_numbers && is_negative_number(next))
1467 {
1468 break;
1469 }
1470 if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1471 break;
1472 }
1473 count += count_values(next);
1474 i += 1;
1475 }
1476 }
1477 }
1478 }
1479 at
1480}
1481
1482fn parse_partial_traced(
1489 spec: &Spec,
1490 input: &[String],
1491 custom_env: Option<&HashMap<String, String>>,
1492 mount_outputs: Option<&HashMap<String, String>>,
1493 mount_timing: MountTiming,
1494 validate_clauses: bool,
1495 trace: &mut Trace,
1496) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1497 let _runs = crate::spec::choices::RunScope::enter();
1499 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1500 let viewed = spec.for_view(view)?;
1501 return parse_partial_traced(
1502 &viewed,
1503 input,
1504 custom_env,
1505 mount_outputs,
1506 mount_timing,
1507 validate_clauses,
1508 trace,
1509 );
1510 }
1511 trace!("parse_partial: {input:?}");
1512 let mut input = input
1513 .iter()
1514 .enumerate()
1515 .map(|(argv, word)| Token::new(word.clone(), argv))
1516 .collect::<VecDeque<_>>();
1517 let argv0 = input.pop_front();
1518 if let Some(argv0) = argv0.as_ref() {
1519 trace.record(argv0.argv, TokenRole::Program);
1520 }
1521 if spec.multicall
1522 && let Some(raw) = argv0
1523 && let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str()))
1524 {
1525 trace.note_synthesized(raw.argv);
1529 input.push_front(Token::new(applet.to_string(), raw.argv));
1530 }
1531 let mut command_has_argv = !input.is_empty();
1535 let mut root_positional_arg_found = false;
1538
1539 let mut out = ParseOutput {
1540 cmd: spec.cmd.clone(),
1541 cmds: vec![spec.cmd.clone()],
1542 args: IndexMap::new(),
1543 clauses: IndexMap::new(),
1544 clause_flags: IndexMap::new(),
1545 flags: IndexMap::new(),
1546 tokens: vec![],
1547 flag_origins: IndexMap::new(),
1548 arg_origins: IndexMap::new(),
1549 overridden_flags: BTreeMap::new(),
1550 available_flags: gather_flags(&spec.cmd),
1551 flag_awaiting_value: vec![],
1552 errors: vec![],
1553 warnings: vec![],
1554 next_arg: None,
1555 double_dash_seen: false,
1556 external: None,
1557 };
1558 let mut overridden_flags = HashSet::new();
1561 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1566
1567 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1580 let mut command_arg_found = false;
1583 let mut variadic_flag_active = false;
1584 let mut idx = 0;
1585 let mut used_default_subcommand = false;
1588 let mut default_bundle: Option<DefaultBundle> = None;
1589 let mut mounts_resolved = false;
1593 let default_outranks_mounts =
1603 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1604 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1605 {
1606 mounts_resolved = true;
1607 let mut mounted = out.cmd.clone();
1608 mounted.mount(&[], mount_outputs)?;
1609 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1610 if let Some(last) = out.cmds.last_mut() {
1611 *last = mounted.clone();
1612 }
1613 out.cmd = mounted;
1614 }
1615
1616 let default_flag_at = spec
1617 .default_subcommand_flags
1618 .then(|| default_flag_route(spec, &out.cmd, &input))
1619 .flatten();
1620
1621 while idx < input.len() {
1622 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1632 default_accepts_word(&out.cmd, name, &input[idx].word)
1633 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1634 });
1635 if !mounts_resolved
1636 && !out.cmd.mounts.is_empty()
1637 && !default_catches_it
1638 && is_command_word(&input[idx].word)
1639 && !is_negative_number(&input[idx].word)
1640 && out.cmd.find_subcommand(&input[idx].word).is_none()
1641 {
1642 mounts_resolved = true;
1643 let mut mounted = out.cmd.clone();
1644 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1645 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1646 if let Some(last) = out.cmds.last_mut() {
1647 *last = mounted.clone();
1648 }
1649 out.cmd = mounted;
1650 }
1651 if variadic_flag_active
1652 && out.cmd.find_subcommand(&input[idx].word).is_some()
1653 && !out.cmd.subcommand_precedence_over_arg
1654 {
1655 break;
1656 }
1657 let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1658 let selected_command = if implicit_default {
1659 out.cmd
1660 .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1661 } else {
1662 out.cmd.find_subcommand(&input[idx].word)
1663 };
1664 if let Some(subcommand) = selected_command {
1665 let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1666 && implicit_default
1667 && input[idx].word.starts_with('-')
1668 && !input[idx].word.starts_with("--")
1669 && {
1670 let child_flags = gather_flags(subcommand);
1671 let mut found = false;
1672 for short in input[idx].word[1..].chars() {
1673 let key = format!("-{short}");
1674 if out.available_flags.contains_key(&key)
1675 || supplies_short(spec, &out.cmds, short)
1676 {
1677 found = true;
1678 break;
1679 }
1680 match child_flags.get(&key) {
1681 Some(flag) if flag.arg.is_none() => {}
1682 _ => break,
1683 }
1684 }
1685 found
1686 };
1687 if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1688 {
1689 bail!(
1690 "subcommand '{}' cannot be used with arguments on its parent command",
1691 subcommand.name
1692 );
1693 }
1694 let boundary_parent = (implicit_default
1695 && input[idx].word.starts_with('-')
1696 && !input[idx].word.starts_with("--"))
1697 .then(|| out.available_flags.clone());
1698 let mut subcommand = subcommand.clone();
1699 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1701 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1704 merge_subcommand_flags(
1705 &mut out.available_flags,
1706 gather_flags(&subcommand),
1707 crossing_mount,
1708 );
1709 if let Some(parent) = boundary_parent {
1710 let parent_keys = parent.keys().cloned().collect();
1711 let mut flags = out.available_flags.clone();
1712 flags.extend(parent);
1713 default_bundle = Some(DefaultBundle {
1714 argv: input[idx].argv,
1715 flags,
1716 parent_keys,
1717 });
1718 }
1719 let selected = if implicit_default {
1721 used_default_subcommand = true;
1722 None
1723 } else {
1724 input.remove(idx)
1725 };
1726 if let Some(selected) = selected {
1727 trace.record(
1728 selected.argv,
1729 TokenRole::Command {
1730 name: subcommand.name.clone(),
1731 },
1732 );
1733 }
1734 command_has_argv = idx < input.len();
1735 out.cmds.push(subcommand.clone());
1736 out.cmd = subcommand.clone();
1737 mounts_resolved = true;
1739 prefix_flags.clear();
1740 command_arg_found = false;
1741 variadic_flag_active = false;
1742 } else if !is_command_word(&input[idx].word)
1745 || declared_numeric_short(&out.available_flags, &input[idx].word)
1746 {
1747 let word = input[idx].word.clone();
1749 let flag_key = get_flag_key(&word);
1750
1751 let boundary = default_bundle
1757 .as_ref()
1758 .filter(|bundle| bundle.argv == input[idx].argv);
1759 let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1760 let is_bundle =
1761 word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1762 if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1763 command_arg_found = true;
1764 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1765 input[idx].binding = boundary
1773 .and_then(|bundle| bundle.binding(flag_key))
1774 .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1775 let mut forwarded = f.global.then(|| vec![word.clone()]);
1776 idx += 1;
1777
1778 if f.arg.is_some()
1781 && !word.contains('=')
1782 && idx < input.len()
1783 && accepts_detached_flag_value(&f, &input[idx].word)
1784 {
1785 if let Some(words) = forwarded.as_mut() {
1786 words.push(input[idx].word.clone());
1787 }
1788 idx += 1;
1789 }
1790 if let Some(words) = forwarded {
1791 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1792 prefix_flags.push((f, words));
1793 }
1794 } else {
1795 break;
1798 }
1799 } else {
1800 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1801 idx += 1;
1802 continue;
1803 }
1804 if !used_default_subcommand
1813 && out.cmds.len() == 1
1814 && let Some(default_name) = &spec.default_subcommand
1815 && let Some(subcommand) = out
1816 .cmd
1817 .find_subcommand(default_name)
1818 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1819 {
1820 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1821 bail!(
1822 "subcommand '{}' cannot be used with arguments on its parent command",
1823 subcommand.name
1824 );
1825 }
1826 let mut subcommand = subcommand.clone();
1827 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1829 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1830 merge_subcommand_flags(
1831 &mut out.available_flags,
1832 gather_flags(&subcommand),
1833 crossing_mount,
1834 );
1835 out.cmds.push(subcommand.clone());
1836 out.cmd = subcommand.clone();
1837 command_has_argv = true;
1838 prefix_flags.clear();
1839 command_arg_found = false;
1840 variadic_flag_active = false;
1841 mounts_resolved = true;
1844 used_default_subcommand = true;
1845 continue;
1850 }
1851 if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1856 idx += 1;
1857 continue;
1858 }
1859 if out.cmd.external_subcommand {
1864 let rest: Vec<Token> = input.drain(idx..).collect();
1865 for token in &rest {
1866 trace.record(token.argv, TokenRole::External);
1867 }
1868 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1869 break;
1870 }
1871 break;
1873 }
1874 }
1875
1876 let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1887 let mut enable_flags = true;
1888 let mut grouped_flag = false;
1889 let mut seen_double_dash = false;
1893 let mut restart_seen = false;
1896 let mut double_dash_violations: HashSet<String> = HashSet::new();
1899 let mut root_clause_separator_seen = false;
1903 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1908
1909 while !input.is_empty() {
1910 let token = input.pop_front().unwrap();
1911 let binding = token.binding;
1913 let argv = token.argv;
1914 let mut w = token.word;
1915 let attached_continuation = grouped_flag;
1920 if spec.default_subcommand_flags
1924 && attached_continuation
1925 && !out.flag_awaiting_value.is_empty()
1926 {
1927 grouped_flag = false;
1928 w.remove(0);
1929 if w.starts_with('=') {
1930 w.remove(0);
1931 }
1932 if bind_pending_flag_value(
1933 spec,
1934 &out.cmd,
1935 &mut out.errors,
1936 &mut out.flags,
1937 &mut out.flag_awaiting_value,
1938 &mut w,
1939 &mut input,
1940 custom_env,
1941 trace,
1942 argv,
1943 true,
1944 )? {
1945 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1946 return Ok((out, overridden_flags));
1947 }
1948 continue;
1949 }
1950
1951 if !seen_double_dash
1954 && let Some(clause) = out.cmd.clause.as_ref()
1955 && clause.separator.as_deref() == Some(w.as_str())
1956 {
1957 while try_bind_default_missing(
1958 &mut out.flags,
1959 &mut out.flag_awaiting_value,
1960 custom_env,
1961 &mut out.flag_origins,
1962 )? {}
1963 if let Some(flag) = out.flag_awaiting_value.first() {
1964 let spelling = flag
1965 .long
1966 .first()
1967 .map(|long| format!("--{long}"))
1968 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
1969 .unwrap_or_else(|| flag.name.clone());
1970 return Err(UsageErr::InvalidFlag {
1971 token: spelling.clone(),
1972 reason: "requires an argument".to_string(),
1973 span: (0, spelling.len()).into(),
1974 input: spelling,
1975 }
1976 .into());
1977 }
1978 let name = clause.name.clone();
1979 if out.cmds.len() == 1 {
1980 root_clause_separator_seen = true;
1981 }
1982 finalize_current_clause(&mut out);
1983 out.arg_origins.clear();
1984 trace.record(argv, TokenRole::ClauseSeparator { name });
1985 next_arg_idx = 0;
1986 out.flag_awaiting_value.clear();
1987 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
1988 enable_flags = true;
1989 seen_double_dash = false;
1990 continue;
1991 }
1992
1993 if let Some(ref restart_token) = out.cmd.restart_token
1996 && w == *restart_token
1997 {
1998 out.args.clear();
2003 out.arg_origins.clear();
2008 trace.record(argv, TokenRole::Restart);
2009 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2010 restart_seen = true;
2011 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
2015 }
2016
2017 if enable_flags
2026 && !attached_continuation
2027 && w.starts_with('-')
2028 && out
2029 .flag_awaiting_value
2030 .last()
2031 .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2032 {
2033 let should_return = bind_pending_flag_value(
2036 spec,
2037 &out.cmd,
2038 &mut out.errors,
2039 &mut out.flags,
2040 &mut out.flag_awaiting_value,
2041 &mut w,
2042 &mut input,
2043 custom_env,
2044 trace,
2045 argv,
2046 false,
2048 )?;
2049 if should_return {
2050 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2051 return Ok((out, overridden_flags));
2052 }
2053 continue;
2054 }
2055
2056 if enable_flags
2062 && !attached_continuation
2063 && !out.flag_awaiting_value.is_empty()
2064 && out.flag_awaiting_value.last().is_some_and(|flag| {
2065 (flag.default_missing.is_some() || flag.value_optional)
2066 && !accepts_detached_flag_value(flag, &w)
2067 })
2068 {
2069 try_bind_default_missing(
2070 &mut out.flags,
2071 &mut out.flag_awaiting_value,
2072 custom_env,
2073 &mut out.flag_origins,
2074 )?;
2075 }
2076
2077 if w == "--" && !seen_double_dash {
2083 enable_flags = false;
2084
2085 let should_preserve = active_args(&out.cmd)
2088 .get(next_arg_idx)
2089 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2090 .unwrap_or(false);
2091
2092 if should_preserve {
2093 } else {
2096 seen_double_dash = true;
2097 trace.record(argv, TokenRole::Separator);
2098
2099 let target = active_args(&out.cmd).iter().position(|arg| {
2105 arg.double_dash == SpecDoubleDashChoices::Required
2106 && !out.args.contains_key(arg)
2107 });
2108 if let Some(target) = target {
2109 if target > next_arg_idx {
2116 next_arg_idx = target;
2117 }
2118 }
2119 continue;
2120 }
2121 }
2122
2123 if enable_flags && w.starts_with("--") {
2125 grouped_flag = false;
2126 let split = w.split_once('=');
2130 let word = split.map(|(word, _)| word).unwrap_or(&w);
2131 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2132 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2133 let command_level = binding
2134 .as_ref()
2135 .map(|(_, level)| *level)
2136 .unwrap_or(out.cmds.len() - 1);
2137 parsed_flag_spellings
2138 .entry(Arc::as_ptr(f) as usize)
2139 .or_default()
2140 .insert(word.to_string());
2141 trace.record(
2144 argv,
2145 TokenRole::Flag {
2146 flag: Arc::clone(f),
2147 spelling: word.to_string(),
2148 negated: f.negate.as_deref() == Some(word),
2149 },
2150 );
2151 if f.action != crate::SpecFlagAction::Set {
2152 out.errors
2153 .push(render_action_err(spec, &out.cmd, f, word, custom_env));
2154 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2155 return Ok((out, overridden_flags));
2156 }
2157 apply_flag_overrides(
2158 f,
2159 &out.available_flags,
2160 &mut out.flags,
2161 &mut out.flag_awaiting_value,
2162 &mut overridden_flags,
2163 &mut out.overridden_flags,
2164 );
2165 if let Some(pending) = out.flag_awaiting_value.first() {
2166 out.errors.push(render_missing_flag_value(pending, &w));
2167 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2168 return Ok((out, overridden_flags));
2169 }
2170 if f.arg.is_some() {
2176 record_scalar_flag_occurrence(
2177 &out.cmds,
2178 f,
2179 command_level,
2180 None,
2181 &mut scalar_occurrences,
2182 &mut out.errors,
2183 );
2184 let f = Arc::clone(f);
2185 out.flag_awaiting_value.push(Arc::clone(&f));
2186 if let Some((_, val)) = split {
2191 let mut val = val.to_string();
2195 let should_return = bind_pending_flag_value(
2196 spec,
2197 &out.cmd,
2198 &mut out.errors,
2199 &mut out.flags,
2200 &mut out.flag_awaiting_value,
2201 &mut val,
2202 &mut input,
2203 custom_env,
2204 trace,
2205 argv,
2206 true,
2209 )?;
2210 if should_return {
2211 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2212 return Ok((out, overridden_flags));
2213 }
2214 }
2215 } else if f.count {
2216 let arr = out
2217 .flags
2218 .entry(Arc::clone(f))
2219 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2220 .try_as_multi_bool_mut()
2221 .unwrap();
2222 arr.push(true);
2223 } else {
2224 let negate = f.negate.clone().unwrap_or_default();
2225 let negated_form = word == negate;
2226 let value = if f.bool_value {
2227 match split.map(|(_, value)| value) {
2228 Some("true") => !negated_form,
2229 Some("false") => negated_form,
2230 Some(value) => {
2231 out.errors.push(UsageErr::InvalidValue {
2232 name: f.name.clone(),
2233 value: value.to_string(),
2234 reason: "expected `true` or `false`".to_string(),
2235 });
2236 continue;
2237 }
2238 None => !negated_form,
2239 }
2240 } else {
2241 !negated_form
2242 };
2243 record_scalar_flag_occurrence(
2248 &out.cmds,
2249 f,
2250 command_level,
2251 Some(!negated_form),
2252 &mut scalar_occurrences,
2253 &mut out.errors,
2254 );
2255 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2256 }
2257 continue;
2258 }
2259 if is_help_arg(spec, &out.cmd, &w) {
2260 out.errors
2261 .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2262 trace.record(
2263 argv,
2264 TokenRole::Builtin {
2265 spelling: w.clone(),
2266 },
2267 );
2268 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2269 return Ok((out, overridden_flags));
2270 }
2271 if is_version_arg(spec, &out.cmds, &w) {
2272 out.errors.push(render_version_err(spec, w.len() > 2));
2273 trace.record(
2274 argv,
2275 TokenRole::Builtin {
2276 spelling: w.clone(),
2277 },
2278 );
2279 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2280 return Ok((out, overridden_flags));
2281 }
2282 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2283 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2284 trace.close(&input);
2285 return Err(refused.into());
2286 }
2287 }
2288
2289 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2296 let positional_negative_number = !declared_numeric_short
2297 && is_negative_number(&w)
2298 && active_args(&out.cmd)
2299 .get(next_arg_idx)
2300 .is_some_and(|arg| arg.allow_negative_numbers);
2301 if enable_flags
2302 && !grouped_flag
2303 && binding.is_none()
2306 && w.starts_with('-')
2307 && w.len() > 1
2308 && is_flag_like(&w)
2309 && !positional_negative_number
2310 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2311 {
2312 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2315 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2316 trace.close(&input);
2317 return Err(refused.into());
2318 }
2319 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2320 let short = w.chars().nth(1).unwrap();
2321 if let Some(f) = binding
2322 .as_ref()
2323 .map(|(flag, _)| flag)
2324 .or_else(|| out.available_flags.get(&format!("-{short}")))
2325 {
2326 let command_level = binding
2327 .as_ref()
2328 .map(|(_, level)| *level)
2329 .unwrap_or(out.cmds.len() - 1);
2330 if f.action != crate::SpecFlagAction::Set {
2331 out.errors.push(render_action_err(
2332 spec,
2333 &out.cmd,
2334 f,
2335 &format!("-{short}"),
2336 custom_env,
2337 ));
2338 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2339 return Ok((out, overridden_flags));
2340 }
2341 parsed_flag_spellings
2342 .entry(Arc::as_ptr(f) as usize)
2343 .or_default()
2344 .insert(format!("-{short}"));
2345 trace.record(
2346 argv,
2347 TokenRole::Flag {
2348 flag: Arc::clone(f),
2349 spelling: format!("-{short}"),
2350 negated: false,
2352 },
2353 );
2354 apply_flag_overrides(
2355 f,
2356 &out.available_flags,
2357 &mut out.flags,
2358 &mut out.flag_awaiting_value,
2359 &mut overridden_flags,
2360 &mut out.overridden_flags,
2361 );
2362 if !attached_continuation && let Some(pending) = out.flag_awaiting_value.first() {
2363 out.errors.push(render_missing_flag_value(pending, &w));
2364 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2365 return Ok((out, overridden_flags));
2366 }
2367 let rest = &w[1 + short.len_utf8()..];
2368 if !rest.is_empty() {
2369 let mut tail = Token::new(format!("-{rest}"), argv);
2372 if f.arg.is_none() {
2373 tail.binding = default_bundle
2374 .as_ref()
2375 .filter(|bundle| bundle.argv == argv)
2376 .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2377 }
2378 input.push_front(tail);
2379 }
2380 grouped_flag = !rest.is_empty();
2384 if f.arg.is_some() {
2385 record_scalar_flag_occurrence(
2386 &out.cmds,
2387 f,
2388 command_level,
2389 None,
2390 &mut scalar_occurrences,
2391 &mut out.errors,
2392 );
2393 out.flag_awaiting_value.push(Arc::clone(f));
2394 } else if f.count {
2395 let arr = out
2396 .flags
2397 .entry(Arc::clone(f))
2398 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2399 .try_as_multi_bool_mut()
2400 .unwrap();
2401 arr.push(true);
2402 } else {
2403 let negate = f.negate.clone().unwrap_or_default();
2404 let value = w != negate;
2405 record_scalar_flag_occurrence(
2406 &out.cmds,
2407 f,
2408 command_level,
2409 Some(value),
2410 &mut scalar_occurrences,
2411 &mut out.errors,
2412 );
2413 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2414 }
2415 continue;
2416 }
2417 if let Some(err) = supplied_short(spec, &out.cmds, short, custom_env) {
2421 out.errors.push(err);
2422 trace.record(
2423 argv,
2424 TokenRole::Builtin {
2425 spelling: format!("-{short}"),
2426 },
2427 );
2428 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2429 return Ok((out, overridden_flags));
2430 }
2431 if is_help_arg(spec, &out.cmd, &w) {
2432 out.errors
2433 .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2434 trace.record(
2435 argv,
2436 TokenRole::Builtin {
2437 spelling: w.clone(),
2438 },
2439 );
2440 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2441 return Ok((out, overridden_flags));
2442 }
2443 if is_version_arg(spec, &out.cmds, &w) {
2444 out.errors.push(render_version_err(spec, w.len() > 2));
2445 trace.record(
2446 argv,
2447 TokenRole::Builtin {
2448 spelling: w.clone(),
2449 },
2450 );
2451 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2452 return Ok((out, overridden_flags));
2453 }
2454 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2455 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2456 trace.close(&input);
2457 return Err(refused.into());
2458 }
2459 if grouped_flag {
2460 grouped_flag = false;
2461 w.remove(0);
2462 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2466 w.remove(0);
2467 }
2468 }
2469 }
2470
2471 if enable_flags
2482 && !attached_continuation
2483 && !out.flag_awaiting_value.is_empty()
2484 && out
2485 .flag_awaiting_value
2486 .last()
2487 .is_some_and(|flag| flag.require_equals)
2488 {
2489 let flag = out.flag_awaiting_value.last().unwrap();
2490 let token = flag
2491 .long
2492 .first()
2493 .map(|l| format!("--{l}"))
2494 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2495 .unwrap_or_else(|| flag.name.clone());
2496 out.errors.push(UsageErr::InvalidFlag {
2497 token: token.clone(),
2498 reason: "requires an argument".to_string(),
2499 span: (0, 0).into(),
2500 input: format!("{token} {w}"),
2501 });
2502 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2503 return Ok((out, overridden_flags));
2504 }
2505 if enable_flags
2506 && !out.flag_awaiting_value.is_empty()
2507 && (attached_continuation
2508 || out
2509 .flag_awaiting_value
2510 .last()
2511 .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2512 {
2513 let should_return = bind_pending_flag_value(
2516 spec,
2517 &out.cmd,
2518 &mut out.errors,
2519 &mut out.flags,
2520 &mut out.flag_awaiting_value,
2521 &mut w,
2522 &mut input,
2523 custom_env,
2524 trace,
2525 argv,
2526 attached_continuation,
2527 )?;
2528 if should_return {
2529 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2530 return Ok((out, overridden_flags));
2531 }
2532 continue;
2533 }
2534
2535 if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2536 .then(|| match_sigil_arg_chain(&out.cmds, &w))
2537 .flatten()
2538 {
2539 if value.is_empty() {
2540 out.errors.push(UsageErr::InvalidValue {
2541 name: arg.name.clone(),
2542 value: w.clone(),
2543 reason: format!("expected a value after sigil {sigil:?}"),
2544 });
2545 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2546 return Ok((out, overridden_flags));
2547 }
2548 let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2549 let suppress_trailing_delimiter =
2550 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2551 let delimiter = if suppress_trailing_delimiter && trailing_value {
2552 None
2553 } else {
2554 arg.delimiter
2555 };
2556 let parts = match delimiter {
2557 Some(delimiter) => value
2558 .split(delimiter)
2559 .map(str::to_string)
2560 .collect::<Vec<_>>(),
2561 None => vec![value.to_string()],
2562 };
2563 let mut refused = false;
2564 for part in &parts {
2565 if validate_choices(
2566 spec,
2567 &out.cmd,
2568 &mut out.errors,
2569 ChoiceTarget::arg(arg),
2570 part,
2571 arg.choices.as_ref(),
2572 custom_env,
2573 )? {
2574 refused = true;
2575 break;
2576 }
2577 }
2578 if refused {
2579 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2580 return Ok((out, overridden_flags));
2581 }
2582 trace.record(
2583 argv,
2584 TokenRole::Sigil {
2585 arg: Arc::new(arg.clone()),
2586 sigil: sigil.to_string(),
2587 values: parts.clone(),
2588 },
2589 );
2590 let key = Arc::new(arg.clone());
2591 if arg.var {
2592 let arr = out
2593 .args
2594 .entry(key)
2595 .or_insert_with(|| ParseValue::MultiString(vec![]))
2596 .try_as_multi_string_mut()
2597 .unwrap();
2598 arr.extend(parts);
2599 } else {
2600 out.args.insert(key, ParseValue::String(value.to_string()));
2601 }
2602 if out.cmds.len() == 1 {
2603 root_positional_arg_found = true;
2604 }
2605 continue;
2606 }
2607
2608 if out.cmd.allow_missing_positional {
2609 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2610 while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2611 if current.required || out.args.contains_key(current) {
2612 break;
2613 }
2614 let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2615 .iter()
2616 .filter(|arg| arg.required && arg.sigil.is_none())
2617 .count();
2618 if required_after == 0 {
2619 break;
2620 }
2621 let remaining_values = 1 + input
2622 .iter()
2623 .filter(|token| {
2624 (!enable_flags || !is_flag_like(&token.word))
2625 && (!enable_flags
2626 || restart_seen
2627 || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2628 })
2629 .count();
2630 if remaining_values > required_after {
2631 break;
2632 }
2633 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2634 }
2635 }
2636
2637 if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2638 if arg.var
2639 && out.args.contains_key(arg)
2640 && arg.value_terminator.as_deref() == Some(w.as_str())
2641 {
2642 trace.record(
2643 argv,
2644 TokenRole::ValueTerminator {
2645 ends: arg.name.clone(),
2646 },
2647 );
2648 next_arg_idx += 1;
2649 continue;
2650 }
2651 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2655 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2656 trace.record(
2657 argv,
2658 TokenRole::Refused {
2659 reason: format!("{} only accepts words after `--`", arg.name),
2660 },
2661 );
2662 continue;
2666 }
2667 let trailing_value =
2672 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2673 let suppress_trailing_delimiter =
2674 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2675 let delimiter = if suppress_trailing_delimiter && trailing_value {
2676 None
2677 } else {
2678 arg.delimiter
2679 };
2680 let parts: Vec<String> = match delimiter {
2681 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2682 None => vec![w.clone()],
2683 };
2684 let mut refused = false;
2685 for part in &parts {
2686 if validate_choices(
2687 spec,
2688 &out.cmd,
2689 &mut out.errors,
2690 ChoiceTarget::arg(arg),
2691 part,
2692 arg.choices.as_ref(),
2693 custom_env,
2694 )? {
2695 refused = true;
2696 break;
2697 }
2698 }
2699 if refused {
2700 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2701 return Ok((out, overridden_flags));
2702 }
2703 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2708 enable_flags = false;
2709 }
2710 let unknown_flag = enable_flags
2715 && !positional_negative_number
2716 && is_flag_like(&w)
2717 && binding.is_none();
2718 trace.record(
2719 argv,
2720 if unknown_flag {
2721 TokenRole::UnknownFlag {
2722 bound_as: Some(Arc::new(arg.clone())),
2723 }
2724 } else {
2725 TokenRole::Arg {
2726 arg: Arc::new(arg.clone()),
2727 values: parts.clone(),
2728 }
2729 },
2730 );
2731 if out.cmds.len() == 1 {
2732 root_positional_arg_found = true;
2733 }
2734 if arg.var {
2735 let arr = out
2736 .args
2737 .entry(Arc::new(arg.clone()))
2738 .or_insert_with(|| ParseValue::MultiString(vec![]))
2739 .try_as_multi_string_mut()
2740 .unwrap();
2741 arr.extend(parts.iter().cloned());
2745 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2746 next_arg_idx += 1;
2747 }
2748 } else {
2749 out.args
2750 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2751 next_arg_idx += 1;
2752 }
2753 if out
2754 .cmd
2755 .clause
2756 .as_ref()
2757 .is_some_and(|clause| clause.separator.is_none())
2758 && next_arg_idx >= active_args(&out.cmd).len()
2759 {
2760 while try_bind_default_missing(
2761 &mut out.flags,
2762 &mut out.flag_awaiting_value,
2763 custom_env,
2764 &mut out.flag_origins,
2765 )? {}
2766 if let Some(flag) = out.flag_awaiting_value.first() {
2767 let spelling = flag
2768 .long
2769 .first()
2770 .map(|long| format!("--{long}"))
2771 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2772 .unwrap_or_else(|| flag.name.clone());
2773 return Err(UsageErr::InvalidFlag {
2774 token: spelling.clone(),
2775 reason: "requires an argument".to_string(),
2776 span: (0, spelling.len()).into(),
2777 input: spelling,
2778 }
2779 .into());
2780 }
2781 let name = out.cmd.clause.as_ref().unwrap().name.clone();
2782 finalize_current_clause(&mut out);
2783 out.arg_origins.clear();
2784 trace.record(argv, TokenRole::ClauseSeparator { name });
2785 next_arg_idx = 0;
2786 out.flag_awaiting_value.clear();
2787 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2788 enable_flags = true;
2789 seen_double_dash = false;
2790 }
2791 continue;
2792 }
2793 if is_help_arg(spec, &out.cmd, &w) {
2794 out.errors
2795 .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2796 trace.record(
2797 argv,
2798 TokenRole::Builtin {
2799 spelling: w.clone(),
2800 },
2801 );
2802 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2803 return Ok((out, overridden_flags));
2804 }
2805 if is_version_arg(spec, &out.cmds, &w) {
2806 out.errors.push(render_version_err(spec, w.len() > 2));
2807 trace.record(
2808 argv,
2809 TokenRole::Builtin {
2810 spelling: w.clone(),
2811 },
2812 );
2813 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2814 return Ok((out, overridden_flags));
2815 }
2816 trace.record(
2817 argv,
2818 TokenRole::Refused {
2819 reason: "no declaration takes this word".to_string(),
2820 },
2821 );
2822 trace.close(&input);
2823 bail!("unexpected word: {w}");
2824 }
2825
2826 if spec.default_subcommand_on_empty
2830 && spec.default_subcommand.is_some()
2831 && out.cmds.len() == 1
2832 && !root_positional_arg_found
2833 && !seen_double_dash
2834 && !root_clause_separator_seen
2835 && out.errors.is_empty()
2836 {
2837 while try_bind_default_missing(
2838 &mut out.flags,
2839 &mut out.flag_awaiting_value,
2840 custom_env,
2841 &mut out.flag_origins,
2842 )? {}
2843 if !out.flag_awaiting_value.is_empty() {
2844 } else if let Some(subcommand) = out
2847 .cmd
2848 .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2849 {
2850 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2851 bail!(
2852 "subcommand '{}' cannot be used with arguments on its parent command",
2853 subcommand.name
2854 );
2855 }
2856 let mut subcommand = subcommand.clone();
2857 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2858 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2859 merge_subcommand_flags(
2860 &mut out.available_flags,
2861 gather_flags(&subcommand),
2862 crossing_mount,
2863 );
2864 out.cmds.push(subcommand.clone());
2865 out.cmd = subcommand;
2866 command_has_argv = false;
2867 prefix_flags.clear();
2868 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2869 }
2870 }
2871
2872 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2873 if validate_clauses {
2874 validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2875 }
2876 let clause_flag_names = out
2877 .cmd
2878 .clause
2879 .iter()
2880 .flat_map(|clause| &clause.flags)
2881 .map(|flag| flag.name.clone())
2882 .collect::<HashSet<_>>();
2883
2884 let flag_was_parsed =
2889 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2890
2891 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2899 let declared: HashSet<String> = out
2900 .cmd
2901 .flags
2902 .iter()
2903 .filter(|declared| declared.name == flag.name)
2904 .flat_map(flag_keys)
2905 .collect();
2906 let speaks_for_this_flag = declared.iter().any(|spelling| {
2916 out.available_flags
2917 .get(spelling)
2918 .is_some_and(|available| Arc::ptr_eq(available, flag))
2919 });
2920 if speaks_for_this_flag {
2921 declared
2922 } else {
2923 HashSet::new()
2924 }
2925 };
2926
2927 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2935 let child = child_spellings(flag);
2936 let child_exclusive = !child.is_empty()
2937 && out
2938 .cmd
2939 .flags
2940 .iter()
2941 .any(|declared| declared.name == flag.name && declared.exclusive);
2942 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2943 Some(spellings) => (
2944 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2945 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2946 ),
2947 None => (child_exclusive, flag.exclusive && child.is_empty()),
2951 }
2952 };
2953
2954 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2955 let (child, ancestor) = exclusivity_in_play(flag);
2956 child || ancestor
2957 };
2958
2959 let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2964 .filter(|flag| !clause_flag_names.contains(&flag.name))
2965 .any(|flag| {
2966 exclusive_occurrence(flag)
2967 && !overridden_flags.contains(&flag.name)
2968 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2969 });
2970 let requirements_apply = |command_index: usize| {
2971 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2972 };
2973
2974 if out.cmd.arg_required_else_help && !command_has_argv {
2975 out.errors
2976 .push(render_help_err(spec, &out.cmd, false, custom_env));
2977 }
2978
2979 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2985 let mut names: Vec<&str> = out
2986 .cmd
2987 .subcommands
2988 .iter()
2989 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2992 .map(|(name, _)| name.as_str())
2993 .collect();
2994 names.sort_unstable();
2995 out.errors.push(UsageErr::MissingSubcommand(
2996 out.cmd.name.clone(),
2997 names.join(", "),
2998 ));
2999 }
3000
3001 if !exclusive_present {
3004 for arg in out
3005 .cmds
3006 .iter()
3007 .enumerate()
3008 .filter(|(index, _)| requirements_apply(*index))
3009 .flat_map(|(_, cmd)| &cmd.args)
3010 {
3011 if out.args.contains_key(arg) {
3012 continue;
3013 }
3014 if double_dash_violations.contains(&arg.name) {
3016 continue;
3017 }
3018 let required_if = arg.required_if.iter().any(|selector| {
3019 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3020 });
3021 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
3022 selector_explicit_has_value(
3023 &condition.selector,
3024 &condition.value,
3025 &out,
3026 &overridden_flags,
3027 custom_env,
3028 )
3029 });
3030 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3031 && arg.required_if_eq_all.iter().all(|condition| {
3032 selector_explicit_has_value(
3033 &condition.selector,
3034 &condition.value,
3035 &out,
3036 &overridden_flags,
3037 custom_env,
3038 )
3039 });
3040 let unless_any = arg.required_unless.iter().any(|selector| {
3041 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3042 });
3043 let unless_all = !arg.required_unless_all.is_empty()
3044 && arg.required_unless_all.iter().all(|selector| {
3045 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3046 });
3047 let required_unless = !(unless_any
3048 || unless_all
3049 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3050 if (arg.required
3051 || required_if
3052 || required_if_eq
3053 || required_if_eq_all
3054 || required_unless)
3055 && arg.default.is_empty()
3056 {
3057 let has_env = arg
3059 .env
3060 .as_ref()
3061 .is_some_and(|env_var| env_contains(custom_env, env_var));
3062 if !has_env {
3063 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3064 }
3065 }
3066 }
3067 }
3068
3069 for flag in unique_flags(out.available_flags.values())
3081 .filter(|flag| !clause_flag_names.contains(&flag.name))
3082 {
3083 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3084 if !given || overridden_flags.contains(&flag.name) {
3085 continue;
3086 }
3087 for other in &flag.conflicts {
3088 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3089 out.errors.push(UsageErr::InvalidFlag {
3090 token: format!("--{}", flag.name),
3091 reason: format!("conflicts with {other}"),
3092 span: (0, 0).into(),
3093 input: format!("--{} {other}", flag.name),
3094 });
3095 }
3096 }
3097 let owner = out
3108 .cmds
3109 .iter()
3110 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3111 .unwrap_or(out.cmds.len() - 1);
3112 if !exclusive_present && requirements_apply(owner) {
3113 for other in &flag.requires {
3114 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3115 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3116 if other.starts_with('-') {
3117 out.errors.push(UsageErr::MissingFlag(name));
3118 } else {
3119 out.errors.push(UsageErr::MissingArg(name));
3120 }
3121 }
3122 }
3123 for condition in &flag.requires_if {
3124 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3125 && !selector_is_satisfied(
3126 &condition.requires,
3127 &out,
3128 &overridden_flags,
3129 custom_env,
3130 )
3131 {
3132 let name = selector_flag_name(&condition.requires, &out)
3133 .unwrap_or_else(|| condition.requires.clone());
3134 out.errors.push(UsageErr::MissingFlag(name));
3135 }
3136 }
3137 }
3138 }
3139
3140 for (command_index, arg) in out
3143 .cmds
3144 .iter()
3145 .enumerate()
3146 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3147 {
3148 let given = arg_is_explicit(arg, &out, custom_env);
3149 if !given {
3150 continue;
3151 }
3152 for other in &arg.conflicts {
3153 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3154 out.errors.push(UsageErr::InvalidFlag {
3155 token: arg.name.clone(),
3156 reason: format!("conflicts with {other}"),
3157 span: (0, 0).into(),
3158 input: format!("{} {other}", arg.name),
3159 });
3160 }
3161 }
3162 if !exclusive_present && requirements_apply(command_index) {
3163 for other in &arg.requires {
3164 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3165 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3166 if other.starts_with('-') {
3167 out.errors.push(UsageErr::MissingFlag(name));
3168 } else {
3169 out.errors.push(UsageErr::MissingArg(name));
3170 }
3171 }
3172 }
3173 }
3174 }
3175
3176 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3186 .filter(|flag| !clause_flag_names.contains(&flag.name))
3187 {
3188 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3193 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3194 continue;
3195 }
3196 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3197 .filter(|other| !clause_flag_names.contains(&other.name))
3198 .find(|other| {
3199 !Arc::ptr_eq(other, flag)
3200 && !overridden_flags.contains(&other.name)
3201 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3202 })
3203 .map(|other| format!("--{}", other.name));
3204 let other_arg = active_args(&out.cmd).iter().find(|arg| {
3205 out.args.keys().any(|given| given.name == arg.name)
3206 || out
3207 .clauses
3208 .values()
3209 .flatten()
3210 .any(|instance| instance.keys().any(|given| given.name == arg.name))
3211 || arg
3212 .env
3213 .as_ref()
3214 .is_some_and(|env| env_contains(custom_env, env))
3215 });
3216 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3222 let selected_subcommand =
3223 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3224 let other = other_flag
3225 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3226 .or(selected_subcommand);
3227 if let Some(other) = other {
3228 out.errors.push(UsageErr::InvalidFlag {
3229 token: format!("--{}", flag.name),
3230 reason: format!("must be given on its own, and {other} was given too"),
3231 span: (0, 0).into(),
3232 input: format!("--{} {other}", flag.name),
3233 });
3234 }
3235 }
3236
3237 let mut group_errors: Vec<UsageErr> = Vec::new();
3246 for (command_index, group) in out
3247 .cmds
3248 .iter()
3249 .enumerate()
3250 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3251 {
3252 let mut given: Vec<&str> = Vec::new();
3258 let mut seen: Vec<String> = Vec::new();
3259 for selector in &group.members {
3260 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3261 continue;
3262 }
3263 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3264 if seen.contains(&name) {
3265 continue;
3266 }
3267 seen.push(name);
3268 given.push(selector.as_str());
3269 }
3270 if !group.multiple && given.len() > 1 {
3271 group_errors.push(UsageErr::InvalidFlag {
3272 token: given[1].to_string(),
3273 reason: format!("cannot be used with {} in group {}", given[0], group.name),
3274 span: (0, 0).into(),
3275 input: format!("{} {}", given[0], given[1]),
3276 });
3277 }
3278 let satisfied = group
3283 .members
3284 .iter()
3285 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3286 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3287 group_errors.push(UsageErr::MissingGroup {
3291 group: group.name.clone(),
3292 members: group.members.join(", "),
3293 });
3294 }
3295 }
3296 out.errors.extend(group_errors);
3297
3298 if !exclusive_present {
3299 for flag in unique_flags(out.available_flags.values())
3300 .filter(|flag| !clause_flag_names.contains(&flag.name))
3301 {
3302 let owner = out
3303 .cmds
3304 .iter()
3305 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3306 .unwrap_or(out.cmds.len() - 1);
3307 if !requirements_apply(owner) {
3308 continue;
3309 }
3310 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3311 continue;
3312 }
3313 let has_default =
3314 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3315 let has_env = flag_has_env(flag, custom_env);
3316 let required_if = flag.required_if.iter().any(|selector| {
3317 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3318 });
3319 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3320 selector_explicit_has_value(
3321 &condition.selector,
3322 &condition.value,
3323 &out,
3324 &overridden_flags,
3325 custom_env,
3326 )
3327 });
3328 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3329 && flag.required_if_eq_all.iter().all(|condition| {
3330 selector_explicit_has_value(
3331 &condition.selector,
3332 &condition.value,
3333 &out,
3334 &overridden_flags,
3335 custom_env,
3336 )
3337 });
3338 let unless_any = flag.required_unless.iter().any(|selector| {
3339 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3340 });
3341 let unless_all = !flag.required_unless_all.is_empty()
3342 && flag.required_unless_all.iter().all(|selector| {
3343 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3344 });
3345 let required_unless = !(unless_any
3346 || unless_all
3347 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3348 if (flag.required
3349 || required_if
3350 || required_if_eq
3351 || required_if_eq_all
3352 || required_unless)
3353 && !has_default
3354 && !has_env
3355 {
3356 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3357 }
3358 }
3359 }
3360
3361 for (arg, value) in &out.args {
3363 if arg.var
3364 && let ParseValue::MultiString(values) = value
3365 {
3366 if let Some(min) = arg.var_min
3367 && values.len() < min
3368 {
3369 out.errors.push(UsageErr::VarArgTooFew {
3370 name: arg.name.clone(),
3371 min,
3372 got: values.len(),
3373 });
3374 }
3375 if let Some(max) = arg.var_max
3376 && values.len() > max
3377 {
3378 out.errors.push(UsageErr::VarArgTooMany {
3379 name: arg.name.clone(),
3380 max,
3381 got: values.len(),
3382 });
3383 }
3384 }
3385 }
3386
3387 for flag in unique_flags(out.available_flags.values())
3391 .filter(|flag| !clause_flag_names.contains(&flag.name))
3392 {
3393 if flag.var {
3394 let bound = match out.flags.get(flag) {
3395 Some(ParseValue::MultiString(values)) => values.len(),
3396 Some(ParseValue::MultiBool(values)) => values.len(),
3397 Some(_) => 1,
3398 None => 0,
3399 };
3400 let pending = out
3404 .flag_awaiting_value
3405 .iter()
3406 .filter(|pending| {
3407 Arc::ptr_eq(pending, flag)
3408 && (pending.value_optional || pending.default_missing.is_some())
3409 })
3410 .count();
3411 let count = bound + pending;
3412 if count == 0 {
3413 continue;
3414 }
3415 if let Some(min) = flag.var_min
3416 && count < min
3417 {
3418 out.errors.push(UsageErr::VarFlagTooFew {
3419 name: flag.name.clone(),
3420 min,
3421 got: count,
3422 });
3423 }
3424 if let Some(max) = flag.var_max
3425 && count > max
3426 {
3427 out.errors.push(UsageErr::VarFlagTooMany {
3428 name: flag.name.clone(),
3429 max,
3430 got: count,
3431 });
3432 }
3433 }
3434 }
3435
3436 Ok((out, overridden_flags))
3437}
3438
3439fn validate_expression(
3440 name: &str,
3441 expression: Option<&str>,
3442 message: Option<&str>,
3443 parsed: &ParseValue,
3444 errors: &mut Vec<UsageErr>,
3445) {
3446 let Some(expression) = expression else {
3447 return;
3448 };
3449 #[cfg(not(feature = "validation"))]
3450 let _ = expression;
3451 let values: &[String] = match parsed {
3452 ParseValue::String(value) => std::slice::from_ref(value),
3453 ParseValue::MultiString(values) => values,
3454 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3455 };
3456 #[cfg(feature = "validation")]
3457 for value in values {
3458 let reason = match usage_validation::validate(expression, value) {
3459 Ok(true) => continue,
3460 Ok(false) => message
3461 .unwrap_or("does not satisfy the validation expression")
3462 .to_string(),
3463 Err(error) => format!("validation expression failed: {error}"),
3464 };
3465 errors.push(UsageErr::InvalidValue {
3466 name: name.to_string(),
3467 value: value.clone(),
3468 reason,
3469 });
3470 break;
3471 }
3472 #[cfg(not(feature = "validation"))]
3473 if let Some(value) = values.first() {
3474 let _ = message;
3475 errors.push(UsageErr::InvalidValue {
3476 name: name.to_string(),
3477 value: value.clone(),
3478 reason: "expression validation requires the `validation` feature".to_string(),
3479 });
3480 }
3481}
3482
3483#[cfg(all(test, not(feature = "validation")))]
3484mod optional_validation_tests {
3485 use crate::{Spec, parse};
3486
3487 #[test]
3488 fn validation_declarations_require_the_opt_in_runtime_feature() {
3489 let spec: Spec = r#"
3490name "ex"
3491bin "ex"
3492arg "<port>" validate="int(value) > 0"
3493 "#
3494 .parse()
3495 .unwrap();
3496 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3497 assert!(
3498 error
3499 .to_string()
3500 .contains("requires the `validation` feature"),
3501 "{error:?}"
3502 );
3503 }
3504}
3505
3506fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3507 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3508}
3509
3510fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3511 overrider
3512 .overrides
3513 .iter()
3514 .any(|selector| flag_matches_selector(overridden, selector))
3515}
3516
3517fn apply_prefix_flag_overrides(
3518 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3519 flag: Arc<SpecFlag>,
3520) {
3521 prefix_flags
3522 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3523}
3524
3525fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3526 prefix_flags
3527 .iter()
3528 .flat_map(|(_, words)| words.iter().cloned())
3529 .collect()
3530}
3531
3532fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3533 match custom_env {
3534 Some(env) => env.contains_key(env_var),
3535 None => std::env::var(env_var).is_ok(),
3536 }
3537}
3538
3539fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3540 flag.env_names()
3541 .any(|env_var| env_contains(custom_env, env_var))
3542}
3543
3544fn fallback_is_true(value: &str) -> bool {
3545 matches!(value, "1" | "true" | "True" | "TRUE")
3546}
3547
3548fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3549 match delimiter {
3550 Some(delimiter) => values
3551 .iter()
3552 .flat_map(|value| value.split(delimiter).map(str::to_string))
3553 .collect(),
3554 None => values.to_vec(),
3555 }
3556}
3557
3558fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3559 if let Some(min) = arg.var_min
3560 && count < min
3561 {
3562 errors.push(UsageErr::VarArgTooFew {
3563 name: arg.name.clone(),
3564 min,
3565 got: count,
3566 });
3567 }
3568 if let Some(max) = arg.var_max
3569 && count > max
3570 {
3571 errors.push(UsageErr::VarArgTooMany {
3572 name: arg.name.clone(),
3573 max,
3574 got: count,
3575 });
3576 }
3577}
3578
3579fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3580 if let Some(min) = flag.var_min
3581 && count < min
3582 {
3583 errors.push(UsageErr::VarFlagTooFew {
3584 name: flag.name.clone(),
3585 min,
3586 got: count,
3587 });
3588 }
3589 if let Some(max) = flag.var_max
3590 && count > max
3591 {
3592 errors.push(UsageErr::VarFlagTooMany {
3593 name: flag.name.clone(),
3594 max,
3595 got: count,
3596 });
3597 }
3598}
3599
3600fn validate_flag_arg_fallback_count(
3601 flag: &SpecFlag,
3602 arg: &SpecArg,
3603 count: usize,
3604 errors: &mut Vec<UsageErr>,
3605) {
3606 if let Some(min) = arg.var_min
3607 && count < min
3608 {
3609 errors.push(UsageErr::VarFlagTooFew {
3610 name: flag.name.clone(),
3611 min,
3612 got: count,
3613 });
3614 }
3615 if let Some(max) = arg.var_max
3616 && count > max
3617 {
3618 errors.push(UsageErr::VarFlagTooMany {
3619 name: flag.name.clone(),
3620 max,
3621 got: count,
3622 });
3623 }
3624}
3625
3626fn bind_flag_fallback(
3629 flag: &Arc<SpecFlag>,
3630 values: &[String],
3631 out: &mut ParseOutput,
3632 custom_env: Option<&HashMap<String, String>>,
3633 origin: ValueOrigin,
3634) -> Result<(), miette::Error> {
3635 let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3636 return Ok(());
3637 };
3638 out.flags.insert(Arc::clone(flag), value);
3639 out.flag_origins
3640 .entry(Arc::clone(flag))
3641 .or_default()
3642 .push(origin);
3643 Ok(())
3644}
3645
3646fn flag_fallback_value(
3647 flag: &SpecFlag,
3648 values: &[String],
3649 errors: &mut Vec<UsageErr>,
3650 custom_env: Option<&HashMap<String, String>>,
3651) -> Result<Option<ParseValue>, miette::Error> {
3652 if values.is_empty() {
3653 return Ok(None);
3654 }
3655 if let Some(arg) = flag.arg.as_ref() {
3656 let values = split_fallback_values(values, arg.delimiter);
3657 if flag.var || arg.var {
3658 if flag.var {
3659 validate_flag_fallback_count(flag, values.len(), errors);
3660 }
3661 if arg.var {
3662 validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3663 }
3664 validate_choice_values(
3665 ChoiceTarget::option(flag),
3666 &values,
3667 arg.choices.as_ref(),
3668 custom_env,
3669 )?;
3670 Ok(Some(ParseValue::MultiString(values)))
3671 } else {
3672 let value = values.into_iter().next().unwrap_or_default();
3673 validate_choice_value(
3674 ChoiceTarget::option(flag),
3675 &value,
3676 arg.choices.as_ref(),
3677 custom_env,
3678 )?;
3679 Ok(Some(ParseValue::String(value)))
3680 }
3681 } else if flag.var {
3682 validate_flag_fallback_count(flag, values.len(), errors);
3683 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3684 Ok(Some(ParseValue::MultiBool(bools)))
3685 } else {
3686 Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3687 }
3688}
3689
3690fn apply_clause_flag_fallbacks(
3696 out: &mut ParseOutput,
3697 overridden_flags: &HashSet<String>,
3698 custom_env: Option<&HashMap<String, String>>,
3699) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3700 let Some(clause) = out.cmd.clause.clone() else {
3701 return Ok(None);
3702 };
3703 let Some(positional_instances) = out.clauses.get(&clause.name) else {
3704 return Ok(None);
3705 };
3706 let positional_instances = positional_instances.clone();
3707 let mut flag_instances = out
3708 .clause_flags
3709 .shift_remove(&clause.name)
3710 .unwrap_or_default();
3711 flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3712 let get_env = |key: &str| -> Option<String> {
3713 custom_env
3714 .and_then(|values| values.get(key).cloned())
3715 .or_else(|| {
3716 custom_env
3717 .is_none()
3718 .then(|| std::env::var(key).ok())
3719 .flatten()
3720 })
3721 };
3722 let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3723
3724 for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3725 let positional = &positional_instances[instance_index];
3726 let mut explicit = scoped
3727 .keys()
3728 .map(|flag| flag.name.clone())
3729 .collect::<HashSet<_>>();
3730
3731 for declared in &clause.flags {
3734 let flag = Arc::new(declared.clone());
3735 if scoped.contains_key(&flag) {
3736 continue;
3737 }
3738 let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3739 continue;
3740 };
3741 if let Some(warning) = flag_deprecation(declared) {
3742 out.warnings.push(warning);
3743 }
3744 if flag_env_is_deprecated(declared, env_name) {
3745 out.warnings
3746 .push(Warning::env(env_name, flag_current_env(declared)));
3747 }
3748 if let Some(value) = flag_fallback_value(
3749 declared,
3750 std::slice::from_ref(&env_value),
3751 &mut out.errors,
3752 custom_env,
3753 )? {
3754 scoped.insert(flag, value);
3755 explicit.insert(declared.name.clone());
3756 }
3757 }
3758
3759 let condition_matches =
3760 |condition: &crate::SpecDefaultIf| {
3761 if let Some(flag) = clause
3762 .flags
3763 .iter()
3764 .find(|flag| flag_matches_selector(flag, &condition.selector))
3765 {
3766 if !explicit.contains(&flag.name) {
3767 return false;
3768 }
3769 return condition.when.as_ref().is_none_or(|expected| {
3770 scoped
3771 .iter()
3772 .find(|(present, _)| present.name == flag.name)
3773 .is_some_and(|(_, value)| parse_value_has(value, expected))
3774 });
3775 }
3776 if let Some(arg) = clause.args.iter().find(|arg| {
3777 !condition.selector.starts_with('-') && arg.name == condition.selector
3778 }) {
3779 return positional.get(arg).is_some_and(|value| {
3780 condition
3781 .when
3782 .as_ref()
3783 .is_none_or(|expected| parse_value_has(value, expected))
3784 });
3785 }
3786 let command_flag = out
3787 .available_flags
3788 .values()
3789 .chain(out.flags.keys())
3790 .filter(|flag| !is_clause_scoped_flag(out, flag))
3791 .find(|flag| flag_matches_selector(flag, &condition.selector));
3792 command_flag.is_some_and(|flag| {
3793 !overridden_flags.contains(&flag.name)
3794 && command_flag_has_explicit_source(flag, out)
3795 && condition.when.as_ref().is_none_or(|expected| {
3796 out.flags
3797 .get(flag)
3798 .is_some_and(|value| parse_value_has(value, expected))
3799 })
3800 })
3801 };
3802
3803 let conditional = clause
3804 .flags
3805 .iter()
3806 .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3807 .filter_map(|flag| {
3808 flag.default_if
3809 .iter()
3810 .find(|condition| condition_matches(condition))
3811 .map(|condition| (flag.clone(), condition.value.clone()))
3812 })
3813 .collect::<Vec<_>>();
3814 for (declared, value) in conditional {
3815 if let Some(value) = flag_fallback_value(
3816 &declared,
3817 std::slice::from_ref(&value),
3818 &mut out.errors,
3819 custom_env,
3820 )? {
3821 scoped.insert(Arc::new(declared), value);
3822 }
3823 }
3824
3825 for declared in &clause.flags {
3826 let flag = Arc::new(declared.clone());
3827 if scoped.contains_key(&flag) {
3828 continue;
3829 }
3830 let values = if !declared.default.is_empty() {
3831 &declared.default
3832 } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3833 &arg.default
3834 } else {
3835 continue;
3836 };
3837 if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3838 {
3839 scoped.insert(flag, value);
3840 }
3841 }
3842 explicit_instances.push(explicit);
3843 }
3844
3845 out.clause_flags.insert(clause.name, flag_instances);
3846 Ok(Some(explicit_instances))
3847}
3848
3849fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3850 out.flags.contains_key(flag)
3851 && out.flag_origins.get(flag).is_none_or(|origins| {
3852 origins
3853 .iter()
3854 .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3855 })
3856}
3857
3858fn default_if_condition_matches(
3859 condition: &crate::SpecDefaultIf,
3860 out: &ParseOutput,
3861 overridden_flags: &HashSet<String>,
3862 custom_env: Option<&HashMap<String, String>>,
3863) -> bool {
3864 match &condition.when {
3865 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3866 Some(when) => {
3867 let Some(flag) = out
3868 .available_flags
3869 .values()
3870 .chain(out.flags.keys())
3871 .find(|flag| flag_matches_selector(flag, &condition.selector))
3872 else {
3873 return false;
3874 };
3875 if overridden_flags.contains(&flag.name) {
3876 return false;
3877 }
3878 explicit_flag_has_value(flag, when, out, custom_env)
3879 }
3880 }
3881}
3882
3883fn explicit_flag_has_value(
3889 flag: &SpecFlag,
3890 expected: &str,
3891 out: &ParseOutput,
3892 custom_env: Option<&HashMap<String, String>>,
3893) -> bool {
3894 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3895 ParseValue::Bool(value) => value.to_string() == expected,
3896 ParseValue::String(value) => value == expected,
3897 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3898 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3899 });
3900 if out.flags.contains_key(flag) {
3901 return parsed_matches;
3902 }
3903
3904 let value = flag.env_names().find_map(|env| match custom_env {
3905 Some(values) => values.get(env).cloned(),
3906 None => std::env::var(env).ok(),
3907 });
3908 value.is_some_and(
3909 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3910 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3911 None if flag.arg.is_none() => {
3912 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3913 }
3914 None => value == expected,
3915 },
3916 )
3917}
3918
3919fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3920 match value {
3921 ParseValue::Bool(value) => value.to_string() == expected,
3922 ParseValue::String(value) => value == expected,
3923 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3924 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3925 }
3926}
3927
3928fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
3931 out.cmd
3932 .clause
3933 .as_ref()
3934 .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
3935}
3936
3937fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
3938 out.cmd
3939 .clause
3940 .as_ref()?
3941 .flags
3942 .iter()
3943 .find(|flag| flag_matches_selector(flag, selector))
3944}
3945
3946fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
3947 out.clause_flags
3948 .values()
3949 .flatten()
3950 .any(|instance| instance.keys().any(|present| present.name == flag.name))
3951 || out.flags.keys().any(|present| present.name == flag.name)
3952}
3953
3954fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
3955 out.clause_flags.values().flatten().any(|instance| {
3956 instance
3957 .iter()
3958 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3959 }) || out
3960 .flags
3961 .iter()
3962 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
3963}
3964
3965fn validate_clause_relationships(
3966 out: &mut ParseOutput,
3967 overridden_flags: &HashSet<String>,
3968 custom_env: Option<&HashMap<String, String>>,
3969 explicit_instances: Option<&[HashSet<String>]>,
3970) {
3971 let Some(clause) = out.cmd.clause.as_ref() else {
3972 return;
3973 };
3974 let Some(instances) = out.clauses.get(&clause.name) else {
3975 return;
3976 };
3977 let command_flag_is_explicit = |selector: &str| {
3978 out.available_flags
3979 .values()
3980 .chain(out.flags.keys())
3981 .filter(|flag| !is_clause_scoped_flag(out, flag))
3982 .any(|flag| {
3983 flag_matches_selector(flag, selector)
3984 && !overridden_flags.contains(&flag.name)
3985 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3986 })
3987 };
3988 let command_flag_matches_value = |selector: &str, expected: &str| {
3989 out.available_flags
3990 .values()
3991 .chain(out.flags.keys())
3992 .filter(|flag| !is_clause_scoped_flag(out, flag))
3993 .find(|flag| flag_matches_selector(flag, selector))
3994 .is_some_and(|flag| {
3995 !overridden_flags.contains(&flag.name)
3996 && explicit_flag_has_value(flag, expected, out, custom_env)
3997 })
3998 };
3999 let command_flag_is_satisfied = |selector: &str| {
4000 out.available_flags
4001 .values()
4002 .chain(out.flags.keys())
4003 .filter(|flag| !is_clause_scoped_flag(out, flag))
4004 .any(|flag| flag_matches_selector(flag, selector))
4005 && selector_is_satisfied(selector, out, overridden_flags, custom_env)
4006 };
4007 let mut errors = Vec::new();
4008 for (instance_index, instance) in instances.iter().enumerate() {
4009 let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
4010 let scoped = out
4011 .clause_flags
4012 .get(&clause.name)
4013 .and_then(|instances| instances.get(instance_index));
4014 let scoped_flag = |selector: &str| {
4015 clause
4016 .flags
4017 .iter()
4018 .find(|flag| flag_matches_selector(flag, selector))
4019 };
4020 let scoped_value = |flag: &SpecFlag| {
4021 scoped.and_then(|values| {
4022 values
4023 .iter()
4024 .find(|(present, _)| present.name == flag.name)
4025 .map(|(_, value)| value)
4026 })
4027 };
4028 let arg_is_explicit = |selector: &str| {
4029 instance
4030 .keys()
4031 .any(|arg| !selector.starts_with('-') && arg.name == selector)
4032 };
4033 let selector_is_explicit = |selector: &str| {
4034 scoped_flag(selector).is_some_and(|flag| {
4035 explicit
4036 .map(|given| given.contains(&flag.name))
4037 .unwrap_or_else(|| scoped_value(flag).is_some())
4038 }) || command_flag_is_explicit(selector)
4039 || arg_is_explicit(selector)
4040 };
4041 let selector_has_value = |selector: &str, expected: &str| {
4042 scoped_flag(selector)
4043 .filter(|flag| {
4044 explicit
4045 .map(|given| given.contains(&flag.name))
4046 .unwrap_or_else(|| scoped_value(flag).is_some())
4047 })
4048 .and_then(scoped_value)
4049 .is_some_and(|value| parse_value_has(value, expected))
4050 || command_flag_matches_value(selector, expected)
4051 || instance.iter().any(|(arg, value)| {
4052 !selector.starts_with('-')
4053 && arg.name == selector
4054 && parse_value_has(value, expected)
4055 })
4056 };
4057 let selector_is_satisfied = |selector: &str| {
4058 scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4059 || command_flag_is_satisfied(selector)
4060 || arg_is_explicit(selector)
4061 };
4062 let selector_name = |selector: &str| {
4063 scoped_flag(selector)
4064 .map(|flag| flag.name.clone())
4065 .or_else(|| selector_flag_name(selector, out))
4066 .unwrap_or_else(|| selector.to_string())
4067 };
4068
4069 for flag in &clause.flags {
4070 let value = scoped_value(flag);
4071 if value.is_none() {
4072 let required_if = flag
4073 .required_if
4074 .iter()
4075 .any(|selector| selector_is_explicit(selector));
4076 let required_if_eq = flag
4077 .required_if_eq
4078 .iter()
4079 .any(|condition| selector_has_value(&condition.selector, &condition.value));
4080 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4081 && flag
4082 .required_if_eq_all
4083 .iter()
4084 .all(|condition| selector_has_value(&condition.selector, &condition.value));
4085 let unless_any = flag
4086 .required_unless
4087 .iter()
4088 .any(|selector| selector_is_explicit(selector));
4089 let unless_all = !flag.required_unless_all.is_empty()
4090 && flag
4091 .required_unless_all
4092 .iter()
4093 .all(|selector| selector_is_explicit(selector));
4094 let required_unless = !(unless_any
4095 || unless_all
4096 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4097 if flag.required
4098 || required_if
4099 || required_if_eq
4100 || required_if_eq_all
4101 || required_unless
4102 {
4103 errors.push(UsageErr::MissingFlag(flag.name.clone()));
4104 }
4105 continue;
4106 }
4107
4108 let explicitly_provided = explicit
4109 .map(|given| given.contains(&flag.name))
4110 .unwrap_or(true);
4111
4112 if explicitly_provided {
4113 for other in &flag.conflicts {
4114 if selector_is_explicit(other) {
4115 errors.push(UsageErr::InvalidFlag {
4116 token: format!("--{}", flag.name),
4117 reason: format!("conflicts with {other}"),
4118 span: (0, 0).into(),
4119 input: format!("--{} {other}", flag.name),
4120 });
4121 }
4122 }
4123 for other in &flag.requires {
4124 if !selector_is_satisfied(other) {
4125 errors.push(UsageErr::MissingFlag(selector_name(other)));
4126 }
4127 }
4128 for condition in &flag.requires_if {
4129 if value.is_some_and(|value| parse_value_has(value, &condition.value))
4130 && !selector_is_satisfied(&condition.requires)
4131 {
4132 errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4133 }
4134 }
4135 }
4136 if let (true, Some(value)) = (flag.var, value) {
4137 let count = match value {
4138 ParseValue::MultiString(values) => values.len(),
4139 ParseValue::MultiBool(values) => values.len(),
4140 _ => 1,
4141 };
4142 if let Some(min) = flag.var_min.filter(|min| count < *min) {
4143 errors.push(UsageErr::VarFlagTooFew {
4144 name: flag.name.clone(),
4145 min,
4146 got: count,
4147 });
4148 }
4149 if let Some(max) = flag.var_max.filter(|max| count > *max) {
4150 errors.push(UsageErr::VarFlagTooMany {
4151 name: flag.name.clone(),
4152 max,
4153 got: count,
4154 });
4155 }
4156 }
4157 if let (Some(arg), Some(value)) = (&flag.arg, value) {
4158 validate_expression(
4159 &flag.name,
4160 arg.validate.as_deref(),
4161 arg.validate_error.as_deref(),
4162 value,
4163 &mut errors,
4164 );
4165 }
4166 }
4167 for arg in &clause.args {
4168 let given = instance.keys().any(|present| present.name == arg.name);
4169 if !given {
4170 let required_if = arg
4171 .required_if
4172 .iter()
4173 .any(|selector| selector_is_explicit(selector));
4174 let required_if_eq = arg
4175 .required_if_eq
4176 .iter()
4177 .any(|condition| selector_has_value(&condition.selector, &condition.value));
4178 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4179 && arg
4180 .required_if_eq_all
4181 .iter()
4182 .all(|condition| selector_has_value(&condition.selector, &condition.value));
4183 let unless_any = arg
4184 .required_unless
4185 .iter()
4186 .any(|selector| selector_is_explicit(selector));
4187 let unless_all = !arg.required_unless_all.is_empty()
4188 && arg
4189 .required_unless_all
4190 .iter()
4191 .all(|selector| selector_is_explicit(selector));
4192 let required_unless = !(unless_any
4193 || unless_all
4194 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4195 if required_if || required_if_eq || required_if_eq_all || required_unless {
4196 errors.push(UsageErr::MissingClauseArg {
4197 clause: clause.name.clone(),
4198 instance: instance_index + 1,
4199 arg: arg.name.clone(),
4200 });
4201 }
4202 continue;
4203 }
4204 for other in &arg.conflicts {
4205 if selector_is_explicit(other) {
4206 errors.push(UsageErr::InvalidFlag {
4207 token: arg.name.clone(),
4208 reason: format!("conflicts with {other}"),
4209 span: (0, 0).into(),
4210 input: format!("{} {other}", arg.name),
4211 });
4212 }
4213 }
4214 for other in &arg.requires {
4215 if selector_is_satisfied(other) {
4216 continue;
4217 }
4218 if other.starts_with('-') {
4219 errors.push(UsageErr::MissingFlag(selector_name(other)));
4220 } else {
4221 errors.push(UsageErr::MissingClauseArg {
4222 clause: clause.name.clone(),
4223 instance: instance_index + 1,
4224 arg: other.clone(),
4225 });
4226 }
4227 }
4228 }
4229 }
4230 out.errors.extend(errors);
4231}
4232
4233fn selector_explicit_has_value(
4234 selector: &str,
4235 expected: &str,
4236 out: &ParseOutput,
4237 overridden_flags: &HashSet<String>,
4238 custom_env: Option<&HashMap<String, String>>,
4239) -> bool {
4240 if let Some(flag) = selected_clause_flag(out, selector) {
4241 return clause_flag_has_value(out, flag, expected);
4242 }
4243 if let Some(flag) = out
4244 .available_flags
4245 .values()
4246 .chain(out.flags.keys())
4247 .filter(|flag| !is_clause_scoped_flag(out, flag))
4248 .find(|flag| flag_matches_selector(flag, selector))
4249 {
4250 return !overridden_flags.contains(&flag.name)
4251 && explicit_flag_has_value(flag, expected, out, custom_env);
4252 }
4253 let Some(arg) = selector_arg(selector, out) else {
4254 return false;
4255 };
4256 let parsed = out
4257 .args
4258 .iter()
4259 .find(|(given, _)| given.name == arg.name)
4260 .map(|(_, value)| value)
4261 .or_else(|| {
4262 out.clauses.values().flatten().find_map(|instance| {
4263 instance
4264 .iter()
4265 .find(|(given, _)| given.name == arg.name)
4266 .map(|(_, value)| value)
4267 })
4268 });
4269 if let Some(value) = parsed {
4270 return match value {
4271 ParseValue::String(value) => value == expected,
4272 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4273 ParseValue::Bool(value) => value.to_string() == expected,
4274 ParseValue::MultiBool(values) => {
4275 values.iter().any(|value| value.to_string() == expected)
4276 }
4277 };
4278 }
4279 let value = arg.env_names().find_map(|env| match custom_env {
4280 Some(values) => values.get(env).cloned(),
4281 None => std::env::var(env).ok(),
4282 });
4283 value.is_some_and(|value| match arg.delimiter {
4284 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4285 None => value == expected,
4286 })
4287}
4288
4289fn selector_is_explicit(
4290 selector: &str,
4291 out: &ParseOutput,
4292 overridden_flags: &HashSet<String>,
4293 custom_env: Option<&HashMap<String, String>>,
4294) -> bool {
4295 let scoped_flag_is_explicit =
4296 selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4297 let flag_is_explicit = out
4298 .available_flags
4299 .values()
4300 .chain(out.flags.keys())
4301 .filter(|flag| !is_clause_scoped_flag(out, flag))
4302 .any(|flag| {
4303 flag_matches_selector(flag, selector)
4304 && !overridden_flags.contains(&flag.name)
4305 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4306 });
4307 scoped_flag_is_explicit
4308 || flag_is_explicit
4309 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4310}
4311
4312fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4318 if let Some(flag) = selected_clause_flag(out, selector) {
4319 return Some(flag.name.clone());
4320 }
4321 out.available_flags
4322 .values()
4323 .chain(out.flags.keys())
4324 .filter(|flag| !is_clause_scoped_flag(out, flag))
4325 .find(|flag| flag_matches_selector(flag, selector))
4326 .map(|flag| flag.name.clone())
4327 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4328}
4329
4330fn selector_is_satisfied(
4340 selector: &str,
4341 out: &ParseOutput,
4342 overridden_flags: &HashSet<String>,
4343 custom_env: Option<&HashMap<String, String>>,
4344) -> bool {
4345 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4346 return true;
4347 }
4348 let flag_is_satisfied = out
4349 .available_flags
4350 .values()
4351 .chain(out.flags.keys())
4352 .filter(|flag| !is_clause_scoped_flag(out, flag))
4353 .filter(|flag| flag_matches_selector(flag, selector))
4354 .any(|flag| {
4355 !overridden_flags.contains(&flag.name)
4356 && (!flag.default.is_empty()
4357 || flag.arg.iter().any(|a| !a.default.is_empty())
4358 || flag.default_if.iter().any(|condition| {
4359 default_if_condition_matches(condition, out, overridden_flags, custom_env)
4360 }))
4361 });
4362 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4363}
4364
4365fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4366 if selector.starts_with('-') {
4369 return None;
4370 }
4371 out.cmds
4372 .iter()
4373 .flat_map(active_args)
4374 .find(|arg| arg.name == selector)
4375}
4376
4377fn arg_is_explicit(
4378 arg: &SpecArg,
4379 out: &ParseOutput,
4380 custom_env: Option<&HashMap<String, String>>,
4381) -> bool {
4382 out.args.keys().any(|given| given.name == arg.name)
4383 || out
4384 .clauses
4385 .values()
4386 .flatten()
4387 .any(|instance| instance.keys().any(|given| given.name == arg.name))
4388 || arg
4389 .env
4390 .as_ref()
4391 .is_some_and(|env| env_contains(custom_env, env))
4392}
4393
4394fn apply_flag_overrides(
4395 flag: &Arc<SpecFlag>,
4396 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4397 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4398 pending_flags: &mut Vec<Arc<SpecFlag>>,
4399 overridden_flags: &mut HashSet<String>,
4400 attributed: &mut BTreeMap<String, String>,
4404) {
4405 let overridden_names: HashSet<String> = available_flags
4406 .values()
4407 .chain(parsed_flags.keys())
4408 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4409 .map(|other| other.name.clone())
4410 .collect();
4411
4412 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4413 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4414 for name in &overridden_names {
4415 attributed.insert(name.clone(), flag.name.clone());
4416 }
4417 overridden_flags.extend(overridden_names);
4418 overridden_flags.remove(&flag.name);
4420 attributed.remove(&flag.name);
4421}
4422
4423#[cfg(feature = "cli-help")]
4426fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool, env: Env<'_>) -> UsageErr {
4427 UsageErr::Help(docs::cli::render_runtime_help(
4428 spec,
4429 cmd,
4430 long,
4431 docs::cli::Style::PLAIN,
4432 env,
4433 ))
4434}
4435
4436#[cfg(feature = "cli-help")]
4437fn render_help_all_err(spec: &Spec, cmd: &SpecCommand, env: Env<'_>) -> UsageErr {
4438 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand, env: Env<'_>) {
4439 if !out.is_empty() {
4440 out.push('\n');
4441 }
4442 out.push_str(&docs::cli::render_runtime_help(
4443 spec,
4444 cmd,
4445 true,
4446 docs::cli::Style::PLAIN,
4447 env,
4448 ));
4449 let mut children: Vec<_> = cmd
4450 .subcommands
4451 .values()
4452 .filter(|child| !child.hide)
4453 .collect();
4454 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4455 for child in children {
4456 append(out, spec, child, env);
4457 }
4458 }
4459
4460 let mut out = String::new();
4461 append(&mut out, spec, cmd, env);
4462 UsageErr::Help(out)
4463}
4464
4465#[cfg(not(feature = "cli-help"))]
4466fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool, _env: Env<'_>) -> UsageErr {
4467 UsageErr::Help("help".to_string())
4468}
4469
4470#[cfg(not(feature = "cli-help"))]
4471fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand, _env: Env<'_>) -> UsageErr {
4472 UsageErr::Help("help".to_string())
4473}
4474
4475fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4478 let value = if long {
4479 spec.long_version.as_ref().or(spec.version.as_ref())
4480 } else {
4481 spec.version.as_ref().or(spec.long_version.as_ref())
4482 };
4483 UsageErr::Version(value.cloned().unwrap_or_default())
4484}
4485
4486type Env<'a> = Option<&'a HashMap<String, String>>;
4488
4489fn render_action_err(
4490 spec: &Spec,
4491 cmd: &SpecCommand,
4492 flag: &SpecFlag,
4493 spelling: &str,
4494 env: Env<'_>,
4495) -> UsageErr {
4496 use crate::SpecFlagAction;
4497 match flag.action {
4498 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--"), env),
4499 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false, env),
4500 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true, env),
4501 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd, env),
4502 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4503 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4504 }
4505}
4506
4507fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4509 let token = flag
4510 .long
4511 .first()
4512 .map(|long| format!("--{long}"))
4513 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4514 .unwrap_or_else(|| flag.name.clone());
4515 UsageErr::InvalidFlag {
4516 token: token.clone(),
4517 reason: "requires an argument".to_string(),
4518 span: (0, 0).into(),
4519 input: format!("{token} {following}"),
4520 }
4521}
4522
4523#[derive(Copy, Clone)]
4524struct ChoiceTarget<'a> {
4525 kind: &'a str,
4526 name: &'a str,
4527}
4528
4529impl<'a> ChoiceTarget<'a> {
4530 fn arg(arg: &'a SpecArg) -> Self {
4531 Self {
4532 kind: "arg",
4533 name: &arg.name,
4534 }
4535 }
4536
4537 fn option(flag: &'a SpecFlag) -> Self {
4538 Self {
4539 kind: "option",
4540 name: &flag.name,
4541 }
4542 }
4543}
4544
4545fn short_bundle_is_known(
4550 spec: &Spec,
4551 cmds: &[SpecCommand],
4552 available: &BTreeMap<String, Arc<SpecFlag>>,
4553 token: &str,
4554) -> bool {
4555 for c in token.chars().skip(1) {
4556 match available.get(&format!("-{c}")) {
4557 None if supplies_short(spec, cmds, c) => {}
4562 None => return false,
4563 Some(f) if f.arg.is_some() => return true,
4564 Some(_) => {}
4565 }
4566 }
4567 true
4568}
4569
4570fn supplied_short(
4580 spec: &Spec,
4581 cmds: &[SpecCommand],
4582 letter: char,
4583 env: Env<'_>,
4584) -> Option<UsageErr> {
4585 let cmd = cmds.last()?;
4586 match letter {
4587 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false, env)),
4588 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4589 _ => None,
4590 }
4591}
4592
4593fn supplies_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> bool {
4596 cmds.last().is_some_and(|cmd| match letter {
4597 'h' => is_help_arg(spec, cmd, "-h"),
4598 'V' => is_version_arg(spec, cmds, "-V"),
4599 _ => false,
4600 })
4601}
4602
4603fn reject_unknown_flag_if_asked(
4609 spec: &Spec,
4610 path: &[SpecCommand],
4611 token: &str,
4612) -> Result<(), UsageErr> {
4613 if !is_flag_like(token) {
4616 return Ok(());
4617 }
4618 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4619 return Ok(());
4620 }
4621 Err(UsageErr::InvalidFlag {
4622 token: token.to_string(),
4623 reason: "no such flag".to_string(),
4624 span: (0, 0).into(),
4625 input: token.to_string(),
4626 })
4627}
4628
4629fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4635 path.iter()
4636 .rev()
4637 .find_map(|cmd| cmd.unknown_flags)
4638 .or(spec.unknown_flags)
4639 .unwrap_or_default()
4640}
4641
4642fn is_flag_like(token: &str) -> bool {
4648 match token.strip_prefix('-') {
4649 None | Some("") => false,
4650 Some(_) => true,
4651 }
4652}
4653
4654fn is_negative_number(token: &str) -> bool {
4655 token.strip_prefix('-').is_some_and(is_number)
4656}
4657
4658fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4664 !flag.require_equals
4665 && (!is_flag_like(token)
4666 || flag.allow_hyphen_values()
4667 || (is_negative_number(token)
4668 && (flag
4669 .arg
4670 .as_ref()
4671 .is_some_and(|arg| arg.allow_negative_numbers)
4672 || (flag.default_missing.is_none() && !flag.value_optional))))
4673}
4674
4675fn record_scalar_flag_occurrence(
4676 cmds: &[SpecCommand],
4677 flag: &Arc<SpecFlag>,
4678 command_level: usize,
4679 bool_value: Option<bool>,
4680 occurrences: &mut HashMap<(usize, usize), u8>,
4681 errors: &mut Vec<UsageErr>,
4682) {
4683 let strict = cmds.get(command_level).is_some_and(|cmd| {
4684 !cmd.args_override_self
4685 || cmd
4686 .clause
4687 .as_ref()
4688 .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4689 });
4690 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4691 if !strict || flag.count || collects_values {
4692 return;
4693 }
4694
4695 let bit = match bool_value {
4696 Some(false) if flag.negate.is_some() => 0b10,
4697 _ => 0b01,
4698 };
4699 let key = (Arc::as_ptr(flag) as usize, command_level);
4700 let seen = occurrences.entry(key).or_default();
4701 if *seen & bit != 0 {
4702 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4703 }
4704 *seen |= bit;
4705}
4706
4707fn is_command_word(token: &str) -> bool {
4715 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4716}
4717
4718fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4724 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4725}
4726
4727fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4733 !is_negative_number(token)
4734 || cmd
4735 .find_subcommand(default_name)
4736 .and_then(|default| default.args.first())
4737 .is_some_and(|arg| arg.allow_negative_numbers)
4738}
4739
4740fn is_number(rest: &str) -> bool {
4747 let (mantissa, exponent) = match rest.find(['e', 'E']) {
4748 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4749 None => (rest, None),
4750 };
4751
4752 let mut seen_digit = false;
4753 let mut seen_dot = false;
4754 for c in mantissa.chars() {
4755 match c {
4756 '0'..='9' => seen_digit = true,
4757 '.' if !seen_dot => seen_dot = true,
4758 _ => return false,
4759 }
4760 }
4761 if !seen_digit {
4762 return false;
4763 }
4764
4765 match exponent {
4766 None => true,
4767 Some(exp) => {
4768 let digits = exp
4769 .strip_prefix('+')
4770 .or_else(|| exp.strip_prefix('-'))
4771 .unwrap_or(exp);
4772 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4773 }
4774 }
4775}
4776
4777#[allow(clippy::too_many_arguments)]
4784fn bind_pending_flag_value(
4785 spec: &Spec,
4786 cmd: &SpecCommand,
4787 errors: &mut Vec<UsageErr>,
4788 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4789 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4790 word: &mut String,
4791 input: &mut VecDeque<Token>,
4792 custom_env: Option<&HashMap<String, String>>,
4793 trace: &mut Trace,
4794 argv: usize,
4798 attached: bool,
4799) -> miette::Result<bool> {
4800 let collecting = flag_awaiting_value
4804 .last()
4805 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4806 .cloned()
4807 .map(|flag| {
4808 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4809 (flag, carried)
4810 });
4811 let mut bound = vec![];
4812 let refused = drain_pending_flag_values(
4813 spec,
4814 cmd,
4815 errors,
4816 flags,
4817 flag_awaiting_value,
4818 word,
4819 custom_env,
4820 &mut bound,
4821 )?;
4822 for (flag, values) in bound {
4823 trace.record(
4824 argv,
4825 TokenRole::Value {
4826 flag,
4827 values,
4828 attached,
4829 },
4830 );
4831 }
4832 if refused {
4833 return Ok(true);
4834 }
4835 let Some((flag, carried)) = collecting else {
4836 return Ok(false);
4837 };
4838 collect_variadic_flag_values(
4839 spec,
4840 cmd,
4841 errors,
4842 flags,
4843 flag_awaiting_value,
4844 &flag,
4845 carried,
4846 input,
4847 custom_env,
4848 trace,
4849 )
4850}
4851
4852#[allow(clippy::too_many_arguments)]
4865fn collect_variadic_flag_values(
4866 spec: &Spec,
4867 cmd: &SpecCommand,
4868 errors: &mut Vec<UsageErr>,
4869 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4870 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4871 flag: &Arc<SpecFlag>,
4872 carried: usize,
4873 input: &mut VecDeque<Token>,
4874 custom_env: Option<&HashMap<String, String>>,
4875 trace: &mut Trace,
4876) -> miette::Result<bool> {
4877 let max = flag
4878 .arg
4879 .as_ref()
4880 .and_then(|arg| arg.var_max)
4881 .unwrap_or(usize::MAX);
4882 while flags
4883 .get(flag)
4884 .map(value_count)
4885 .unwrap_or(0)
4886 .saturating_sub(carried)
4887 < max
4888 {
4889 let Some(next) = input.front().map(|token| token.word.as_str()) else {
4890 break;
4891 };
4892 if flag
4893 .arg
4894 .as_ref()
4895 .and_then(|arg| arg.value_terminator.as_deref())
4896 == Some(next)
4897 {
4898 let terminator = input.pop_front().unwrap();
4899 trace.record(
4900 terminator.argv,
4901 TokenRole::ValueTerminator {
4902 ends: flag.name.clone(),
4903 },
4904 );
4905 break;
4906 }
4907 if next == "--"
4910 || (is_flag_like(next)
4911 && !(flag
4912 .arg
4913 .as_ref()
4914 .is_some_and(|arg| arg.allow_negative_numbers)
4915 && is_negative_number(next)))
4916 {
4917 break;
4918 }
4919 let taken = input.pop_front().unwrap();
4920 let argv = taken.argv;
4921 let mut word = taken.word;
4922 flag_awaiting_value.push(Arc::clone(flag));
4923 let mut bound = vec![];
4924 let refused = drain_pending_flag_values(
4925 spec,
4926 cmd,
4927 errors,
4928 flags,
4929 flag_awaiting_value,
4930 &mut word,
4931 custom_env,
4932 &mut bound,
4933 )?;
4934 for (flag, values) in bound {
4935 trace.record(
4938 argv,
4939 TokenRole::Value {
4940 flag,
4941 values,
4942 attached: false,
4943 },
4944 );
4945 }
4946 if refused {
4947 return Ok(true);
4948 }
4949 }
4950 let taken = flags
4959 .get(flag)
4960 .map(value_count)
4961 .unwrap_or(0)
4962 .saturating_sub(carried);
4963 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min)
4964 && taken < min
4965 {
4966 errors.push(UsageErr::VarFlagTooFew {
4967 name: flag.name.clone(),
4968 min,
4969 got: taken,
4970 });
4971 }
4972 if taken > max {
4973 errors.push(UsageErr::VarFlagTooMany {
4974 name: flag.name.clone(),
4975 max,
4976 got: taken,
4977 });
4978 }
4979 Ok(false)
4980}
4981
4982fn value_count(value: &ParseValue) -> usize {
4984 match value {
4985 ParseValue::MultiString(values) => values.len(),
4986 ParseValue::MultiBool(values) => values.len(),
4987 _ => 1,
4988 }
4989}
4990
4991fn try_bind_default_missing(
5002 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5003 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5004 custom_env: Option<&HashMap<String, String>>,
5005 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
5006) -> miette::Result<bool> {
5007 let Some(flag) = flag_awaiting_value.last() else {
5008 return Ok(false);
5009 };
5010 let value = match flag.default_missing.clone() {
5011 Some(value) => value,
5012 None if flag.value_optional => {
5013 let flag = flag_awaiting_value.pop().unwrap();
5014 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
5018 origins
5019 .entry(Arc::clone(&flag))
5020 .or_default()
5021 .push(ValueOrigin::DefaultMissing);
5022 if flag.var {
5023 flags
5028 .entry(flag)
5029 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
5030 .try_as_multi_string_mut()
5031 .unwrap()
5032 .push(String::new());
5033 } else if variadic_value {
5034 flags
5037 .entry(flag)
5038 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
5039 } else {
5040 flags.insert(flag, ParseValue::MultiString(Vec::new()));
5044 }
5045 return Ok(true);
5046 }
5047 None => return Ok(false),
5048 };
5049 if let Some(arg) = flag.arg.as_ref() {
5050 validate_choice_value(
5051 ChoiceTarget::option(flag),
5052 &value,
5053 arg.choices.as_ref(),
5054 custom_env,
5055 )?;
5056 }
5057 let flag = flag_awaiting_value.pop().unwrap();
5058 origins
5059 .entry(Arc::clone(&flag))
5060 .or_default()
5061 .push(ValueOrigin::DefaultMissing);
5062 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5063 if collecting {
5064 let arr = flags
5065 .entry(flag)
5066 .or_insert_with(|| ParseValue::MultiString(vec![]))
5067 .try_as_multi_string_mut()
5068 .unwrap();
5069 arr.push(value);
5070 } else {
5071 flags.insert(flag, ParseValue::String(value));
5072 }
5073 Ok(true)
5074}
5075
5076#[allow(clippy::too_many_arguments)]
5081fn drain_pending_flag_values(
5082 spec: &Spec,
5083 cmd: &SpecCommand,
5084 errors: &mut Vec<UsageErr>,
5085 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5086 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5087 word: &mut String,
5088 custom_env: Option<&HashMap<String, String>>,
5089 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5090) -> miette::Result<bool> {
5091 while let Some(flag) = flag_awaiting_value.pop() {
5092 let arg = flag.arg.as_ref().unwrap();
5093 let parts: Vec<String> = match arg.delimiter {
5097 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5098 None => vec![std::mem::take(word)],
5099 };
5100 for part in &parts {
5101 if validate_choices(
5102 spec,
5103 cmd,
5104 errors,
5105 ChoiceTarget::option(&flag),
5106 part,
5107 arg.choices.as_ref(),
5108 custom_env,
5109 )? {
5110 return Ok(true);
5111 }
5112 }
5113 word.clear();
5114 bound.push((Arc::clone(&flag), parts.clone()));
5115 if flag.var || arg.var {
5118 let arr = flags
5119 .entry(flag)
5120 .or_insert_with(|| ParseValue::MultiString(vec![]))
5121 .try_as_multi_string_mut()
5122 .unwrap();
5123 arr.extend(parts);
5124 } else {
5125 flags.insert(
5128 flag,
5129 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5130 );
5131 }
5132 }
5133 Ok(false)
5134}
5135
5136fn choice_error(
5137 target: ChoiceTarget<'_>,
5138 value: &str,
5139 choices: Option<&SpecChoices>,
5140 custom_env: Option<&HashMap<String, String>>,
5141) -> Option<String> {
5142 let choices = choices?;
5143 if !choices.strict {
5144 return None;
5145 }
5146 match choices.matches_resolved(value, custom_env) {
5148 Ok(true) => return None,
5149 Ok(false) => {}
5150 Err(_) if choices.run().is_some() && crate::spec::choices::runs_disabled() => return None,
5153 Err(err) => {
5154 return Some(format!(
5155 "Could not check {} {}: {value} against its choices: {err}",
5156 target.kind, target.name,
5157 ));
5158 }
5159 }
5160 let values = choices
5163 .resolved_values(custom_env)
5164 .unwrap_or_else(|_| choices.values_with_env(custom_env));
5165 if let Some(run) = choices.run()
5166 && values.is_empty()
5167 {
5168 return Some(format!(
5169 "Invalid choice for {} {}: {value}, `{run}` printed no choices",
5170 target.kind, target.name,
5171 ));
5172 }
5173 if let Some(env) = choices.env()
5174 && values.is_empty()
5175 {
5176 return Some(format!(
5177 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5178 target.kind, target.name,
5179 ));
5180 }
5181 Some(format!(
5182 "Invalid choice for {} {}: {value}, expected one of {}",
5183 target.kind,
5184 target.name,
5185 values.join(", ")
5186 ))
5187}
5188
5189fn validate_choices(
5190 spec: &Spec,
5191 cmd: &SpecCommand,
5192 errors: &mut Vec<UsageErr>,
5193 target: ChoiceTarget<'_>,
5194 value: &str,
5195 choices: Option<&SpecChoices>,
5196 custom_env: Option<&HashMap<String, String>>,
5197) -> miette::Result<bool> {
5198 if is_help_arg(spec, cmd, value)
5202 && choices.is_some_and(|choices| {
5203 choices.strict && !choices.matches_resolved(value, custom_env).unwrap_or(false)
5204 })
5205 {
5206 errors.push(render_help_err(spec, cmd, value.len() > 2, custom_env));
5207 return Ok(true);
5208 }
5209
5210 if let Some(err) = choice_error(target, value, choices, custom_env) {
5211 bail!("{err}");
5212 }
5213 Ok(false)
5214}
5215
5216fn validate_choice_value(
5217 target: ChoiceTarget<'_>,
5218 value: &str,
5219 choices: Option<&SpecChoices>,
5220 custom_env: Option<&HashMap<String, String>>,
5221) -> miette::Result<()> {
5222 if let Some(err) = choice_error(target, value, choices, custom_env) {
5223 bail!("{err}");
5224 }
5225 Ok(())
5226}
5227
5228fn validate_choice_values(
5229 target: ChoiceTarget<'_>,
5230 values: &[String],
5231 choices: Option<&SpecChoices>,
5232 custom_env: Option<&HashMap<String, String>>,
5233) -> miette::Result<()> {
5234 for value in values {
5235 validate_choice_value(target, value, choices, custom_env)?;
5236 }
5237 Ok(())
5238}
5239
5240fn record_stop(
5247 out: &mut ParseOutput,
5248 next_arg_idx: usize,
5249 seen_double_dash: bool,
5250 trace: &mut Trace,
5251 unread: &VecDeque<Token>,
5252) {
5253 out.next_arg = active_args(&out.cmd)
5254 .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5255 .cloned()
5256 .map(Arc::new);
5257 out.double_dash_seen = seen_double_dash;
5258 finalize_current_clause(out);
5259 trace.close(unread);
5260 out.tokens = std::mem::take(&mut trace.tokens);
5261}
5262
5263fn finalize_current_clause(out: &mut ParseOutput) {
5264 let Some(clause) = out.cmd.clause.as_ref() else {
5265 return;
5266 };
5267 let has_scoped_flags = clause
5268 .flags
5269 .iter()
5270 .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5271 if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5272 return;
5273 }
5274 out.clauses
5275 .entry(clause.name.clone())
5276 .or_default()
5277 .push(std::mem::take(&mut out.args));
5278 let mut flags = IndexMap::new();
5279 for clause_flag in &clause.flags {
5280 let key = Arc::new(clause_flag.clone());
5281 if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5282 flags.insert(flag, value);
5283 }
5284 }
5285 out.clause_flags
5286 .entry(clause.name.clone())
5287 .or_default()
5288 .push(flags);
5289}
5290
5291fn restore_current_clause(out: &mut ParseOutput) {
5292 let Some(clause) = out.cmd.clause.as_ref() else {
5293 return;
5294 };
5295 if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5296 out.args = current;
5297 }
5298 if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5299 out.flags.extend(current);
5300 }
5301}
5302
5303fn reset_clause_scalar_occurrences(
5304 out: &ParseOutput,
5305 occurrences: &mut HashMap<(usize, usize), u8>,
5306) {
5307 let Some(clause) = out.cmd.clause.as_ref() else {
5308 return;
5309 };
5310 let names = clause
5311 .flags
5312 .iter()
5313 .map(|flag| flag.name.as_str())
5314 .collect::<HashSet<_>>();
5315 let pointers = unique_flags(out.available_flags.values())
5316 .filter(|flag| names.contains(flag.name.as_str()))
5317 .map(|flag| Arc::as_ptr(flag) as usize)
5318 .collect::<HashSet<_>>();
5319 occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5320}
5321
5322fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5323 while active_args(cmd)
5324 .get(idx)
5325 .is_some_and(|arg| arg.sigil.is_some())
5326 {
5327 idx += 1;
5328 }
5329 idx
5330}
5331
5332fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5333 cmd.clause
5334 .as_ref()
5335 .map(|clause| clause.args.as_slice())
5336 .unwrap_or(cmd.args.as_slice())
5337}
5338
5339fn match_sigil_arg<'a>(
5340 cmd: &'a SpecCommand,
5341 word: &'a str,
5342) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5343 cmd.args
5344 .iter()
5345 .filter_map(|arg| {
5346 let sigil = arg.sigil.as_deref()?;
5347 let value = word.strip_prefix(sigil)?;
5348 Some((arg, sigil, value))
5349 })
5350 .max_by_key(|(_, sigil, _)| sigil.len())
5351}
5352
5353fn match_sigil_arg_chain<'a>(
5354 cmds: &'a [SpecCommand],
5355 word: &'a str,
5356) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5357 cmds.iter()
5358 .filter_map(|cmd| match_sigil_arg(cmd, word))
5359 .max_by_key(|(_, sigil, _)| sigil.len())
5360}
5361
5362fn report_double_dash_violation(
5369 arg: &SpecArg,
5370 errors: &mut Vec<UsageErr>,
5371 violations: &mut HashSet<String>,
5372) {
5373 if violations.insert(arg.name.clone()) {
5374 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5375 }
5376}
5377
5378fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5393 (spec.version.is_some() || spec.long_version.is_some())
5394 && cmds.len() == 1
5395 && !spec.cmd.disable_version_flag
5396 && (w == "--version" || w == "-V")
5397}
5398
5399fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5400 spec.disable_help != Some(true)
5401 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5402 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5403}
5404
5405impl ParseOutput {
5406 pub fn as_env(&self) -> BTreeMap<String, String> {
5407 let mut env = BTreeMap::new();
5408 for (flag, val) in &self.flags {
5409 let key = format!("usage_{}", crate::case::snake(&flag.name));
5410 let val = match val {
5411 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5412 ParseValue::String(s) => s.clone(),
5413 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5414 ParseValue::MultiString(s) => crate::shell_words::join(s),
5415 };
5416 env.insert(key, val);
5417 }
5418 for (arg, val) in &self.args {
5419 let key = format!("usage_{}", crate::case::snake(&arg.name));
5420 env.insert(key, val.to_string());
5421 }
5422 let path = self.cmds.iter().skip(1).map(|c| c.name.as_str()).join(" ");
5429 let declared = |c: &SpecCommand| {
5430 let clause = c.clause.as_ref();
5431 c.args
5432 .iter()
5433 .chain(clause.into_iter().flat_map(|cl| &cl.args))
5434 .any(|a| crate::case::snake(&a.name) == "cmd")
5435 || c.flags
5436 .iter()
5437 .chain(clause.into_iter().flat_map(|cl| &cl.flags))
5438 .any(|f| crate::case::snake(&f.name) == "cmd")
5439 };
5440 if !path.is_empty() && !self.cmds.iter().any(declared) {
5441 env.entry("usage_cmd".to_string()).or_insert(path);
5442 }
5443 env
5444 }
5445}
5446
5447impl Display for ParseValue {
5448 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5449 match self {
5450 ParseValue::Bool(b) => write!(f, "{b}"),
5451 ParseValue::String(s) => write!(f, "{s}"),
5452 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5453 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5454 }
5455 }
5456}
5457
5458fn render_token(token: &TokenBinding) -> String {
5460 let roles = token.roles.iter().map(render_role).join(", ");
5461 let synthesized = if token.synthesized { " (read as)" } else { "" };
5462 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5463}
5464
5465fn render_role(role: &TokenRole) -> String {
5466 match role {
5467 TokenRole::Program => "program".to_string(),
5468 TokenRole::Command { name } => format!("subcommand {name}"),
5469 TokenRole::Flag {
5470 flag,
5471 spelling,
5472 negated,
5473 } => {
5474 let negated = if *negated { ", negated" } else { "" };
5475 format!("flag {} as {spelling}{negated}", flag.name)
5476 }
5477 TokenRole::Value {
5478 flag,
5479 values,
5480 attached,
5481 } => {
5482 let attached = if *attached { ", attached" } else { "" };
5483 format!("value of {} = {values:?}{attached}", flag.name)
5484 }
5485 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5486 TokenRole::Sigil { arg, sigil, values } => {
5487 format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5488 }
5489 TokenRole::Separator => "separator".to_string(),
5490 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5491 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5492 TokenRole::Restart => "restart".to_string(),
5493 TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5494 TokenRole::UnknownFlag { bound_as } => match bound_as {
5495 Some(arg) => format!("unknown flag, bound as {}", arg.name),
5496 None => "unknown flag".to_string(),
5497 },
5498 TokenRole::Refused { reason } => format!("refused: {reason}"),
5499 TokenRole::External => "external".to_string(),
5500 TokenRole::Unread => "unread".to_string(),
5501 }
5502}
5503
5504impl Debug for ParseOutput {
5505 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5506 f.debug_struct("ParseOutput")
5507 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5508 .field(
5509 "args",
5510 &self
5511 .args
5512 .iter()
5513 .map(|(a, w)| format!("{}: {w}", a.name))
5514 .collect_vec(),
5515 )
5516 .field("clauses", &self.clauses)
5517 .field(
5518 "available_flags",
5519 &self
5520 .available_flags
5521 .iter()
5522 .map(|(f, w)| format!("{f}: {w}"))
5523 .collect_vec(),
5524 )
5525 .field(
5526 "flags",
5527 &self
5528 .flags
5529 .iter()
5530 .map(|(f, w)| format!("{}: {w}", f.name))
5531 .collect_vec(),
5532 )
5533 .field("flag_awaiting_value", &self.flag_awaiting_value)
5534 .field("errors", &self.errors)
5535 .field("external", &self.external)
5536 .field(
5540 "tokens",
5541 &self.tokens.iter().map(render_token).collect_vec(),
5542 )
5543 .field(
5544 "origins",
5545 &self
5546 .flag_origins
5547 .iter()
5548 .map(|(f, o)| format!("{}: {o:?}", f.name))
5549 .chain(
5550 self.arg_origins
5551 .iter()
5552 .map(|(a, o)| format!("{}: {o:?}", a.name)),
5553 )
5554 .collect_vec(),
5555 )
5556 .field("overridden_flags", &self.overridden_flags)
5557 .finish()
5558 }
5559}
5560
5561#[cfg(test)]
5562mod tests {
5563 use super::*;
5564 use crate::SpecFlagAction;
5565
5566 fn input(words: &[&str]) -> Vec<String> {
5567 words.iter().map(|word| (*word).to_string()).collect()
5568 }
5569
5570 #[test]
5571 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5572 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5577 .parse()
5578 .unwrap();
5579
5580 for spelling in ["--version", "-V"] {
5581 let err = parse(&spec, &input(&["ex", spelling]))
5582 .expect_err("answering with a version ends the parse");
5583 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5584 }
5585 }
5586
5587 #[test]
5588 fn the_supplied_version_flag_is_the_roots_alone() {
5589 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5592 .parse()
5593 .unwrap();
5594
5595 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5596 assert_eq!(err.to_string(), "unexpected word: --version");
5597 }
5598
5599 #[test]
5600 fn no_declared_version_supplies_nothing() {
5601 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5604
5605 for spelling in ["--version", "-V"] {
5606 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5607 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5608 }
5609 }
5610
5611 #[test]
5612 fn disable_version_flag_removes_the_supplied_spellings() {
5613 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5614 .parse()
5615 .unwrap();
5616
5617 for spelling in ["--version", "-V"] {
5618 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5619 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5620 }
5621 }
5622
5623 #[test]
5624 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5625 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5628 .parse()
5629 .unwrap();
5630
5631 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5632 assert_eq!(out.flags.len(), 1);
5633
5634 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5635 assert_eq!(err.to_string(), "1.2.3");
5636 }
5637
5638 #[test]
5639 fn the_supplied_spellings_split_the_two_version_texts() {
5640 let spec: Spec =
5643 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5644 .parse()
5645 .unwrap();
5646
5647 assert_eq!(
5648 parse(&spec, &input(&["ex", "--version"]))
5649 .unwrap_err()
5650 .to_string(),
5651 "1.2.3 (abcdef)"
5652 );
5653 assert_eq!(
5654 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5655 "1.2.3"
5656 );
5657 }
5658
5659 #[test]
5660 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5661 let spec: Spec =
5666 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5667 .parse()
5668 .unwrap();
5669
5670 for token in ["-vh", "-hv"] {
5671 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5672 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5673 }
5674 for token in ["-vV", "-Vv"] {
5675 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5676 assert_eq!(err.to_string(), "1.2.3", "{token}");
5677 }
5678 }
5679
5680 #[test]
5681 fn a_bundled_help_letter_asks_for_the_short_page() {
5682 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"
5685 .parse()
5686 .unwrap();
5687
5688 let short = parse(&spec, &input(&["ex", "-vh"]))
5689 .unwrap_err()
5690 .to_string();
5691 let long = parse(&spec, &input(&["ex", "--help"]))
5692 .unwrap_err()
5693 .to_string();
5694 assert!(short.contains("Be loud"), "{short}");
5695 assert!(!short.contains("at length"), "{short}");
5696 assert!(long.contains("at length"), "{long}");
5697 }
5698
5699 #[test]
5700 fn the_bundled_version_letter_is_the_roots_alone() {
5701 let spec: Spec =
5703 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5704 .parse()
5705 .unwrap();
5706
5707 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5708 assert_eq!(err.to_string(), "unexpected word: -vV");
5709 }
5710
5711 #[test]
5712 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5713 let spec: Spec =
5716 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5717 .parse()
5718 .unwrap();
5719
5720 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5721 assert_eq!(out.flags.len(), 2);
5722 assert!(
5723 out.flags
5724 .iter()
5725 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local")
5726 );
5727 }
5728
5729 #[test]
5730 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5731 let spec: Spec =
5732 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5733 .parse()
5734 .unwrap();
5735
5736 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5737 assert_eq!(err.to_string(), "unexpected word: -vh");
5738 }
5739
5740 #[test]
5741 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5742 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5745 .parse()
5746 .unwrap();
5747
5748 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5749 assert!(out.flags.is_empty(), "{:?}", out.flags);
5750 assert_eq!(out.args.len(), 1);
5751 }
5752
5753 fn spec_with_arg(arg: SpecArg) -> Spec {
5754 let cmd = SpecCommand::builder().name("test").arg(arg).build();
5755 Spec {
5756 name: "test".to_string(),
5757 bin: "test".to_string(),
5758 cmd,
5759 ..Default::default()
5760 }
5761 }
5762
5763 fn spec_with_flag(flag: SpecFlag) -> Spec {
5764 let cmd = SpecCommand::builder().name("test").flag(flag).build();
5765 Spec {
5766 name: "test".to_string(),
5767 bin: "test".to_string(),
5768 cmd,
5769 ..Default::default()
5770 }
5771 }
5772
5773 fn parse_with_env(
5774 spec: &Spec,
5775 words: &[&str],
5776 env: &[(&str, &str)],
5777 ) -> Result<ParseOutput, miette::Error> {
5778 let env = env
5779 .iter()
5780 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5781 .collect();
5782 Parser::new(spec).with_env(env).parse(&input(words))
5783 }
5784
5785 fn first_string_value(parsed: &ParseOutput) -> &str {
5786 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5787 return value;
5788 }
5789 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5790 return value;
5791 }
5792 panic!("expected first parsed value to be ParseValue::String");
5793 }
5794
5795 #[test]
5796 fn custom_environment_parser_dispatches_executable_views() {
5797 let spec: Spec = r#"
5798bin "ex"
5799view "runner" root="run"
5800cmd "run" {
5801 flag "--token <token>" env="TOKEN"
5802}
5803 "#
5804 .parse()
5805 .unwrap();
5806 let parsed = Parser::new(&spec)
5807 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5808 .parse(&input(&["runner"]))
5809 .unwrap();
5810
5811 assert_eq!(parsed.cmd.name, "runner");
5812 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5813 && matches!(value, ParseValue::String(value) if value == "secret")));
5814 }
5815
5816 #[test]
5817 fn an_executable_view_keeps_the_hosts_version_action() {
5818 let spec: Spec = r#"
5819bin "ex"
5820version "1.2.3"
5821flag "-V --version" action="version"
5822flag "--verbose" global=#true
5823view "runner" root="run" globals=#true
5824cmd "run"
5825 "#
5826 .parse()
5827 .unwrap();
5828
5829 let error = Parser::new(&spec)
5830 .parse(&input(&["runner", "--version"]))
5831 .expect_err("the host version action should answer before view projection");
5832 assert_eq!(error.to_string(), "1.2.3");
5833
5834 let error = Parser::new(&spec)
5835 .parse(&input(&["runner", "--verbose", "--version"]))
5836 .expect_err("the host version action should remain after a carried global");
5837 assert_eq!(error.to_string(), "1.2.3");
5838 }
5839
5840 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5841 let flag = parsed
5842 .flags
5843 .keys()
5844 .find(|flag| flag.name == name)
5845 .unwrap_or_else(|| panic!("expected flag {name}"));
5846 let value = parsed
5847 .flags
5848 .get(flag)
5849 .unwrap_or_else(|| panic!("expected value for flag {name}"));
5850 match value {
5851 ParseValue::String(value) => value,
5852 _ => panic!("expected flag {name} to be ParseValue::String"),
5853 }
5854 }
5855
5856 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5857 let err = result.expect_err("expected parser error");
5858 assert_eq!(format!("{err}"), expected);
5859 }
5860
5861 #[test]
5862 fn a_short_version_action_falls_back_to_the_long_version() {
5863 let flag = SpecFlag::builder()
5864 .short('R')
5865 .action(SpecFlagAction::Version)
5866 .build();
5867 let spec = Spec {
5868 name: "test".to_string(),
5869 bin: "test".to_string(),
5870 long_version: Some("1.2.3\ncommit abc123".to_string()),
5871 ..Default::default()
5872 };
5873 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R", None)
5874 else {
5875 panic!("expected version action")
5876 };
5877 assert_eq!(version, "1.2.3\ncommit abc123");
5878 }
5879
5880 #[cfg(feature = "unstable_choices_env")]
5881 fn spec_arg_choices_env(key: &str) -> Spec {
5882 spec_with_arg(
5883 SpecArg::builder()
5884 .name("env")
5885 .choices_env(key)
5886 .required(false)
5887 .build(),
5888 )
5889 }
5890
5891 #[cfg(feature = "unstable_choices_env")]
5892 fn spec_flag_choices_env(key: &str) -> Spec {
5893 spec_with_flag(
5894 SpecFlag::builder()
5895 .long("env")
5896 .arg(SpecArg::builder().name("env").choices_env(key).build())
5897 .build(),
5898 )
5899 }
5900
5901 #[test]
5902 fn test_parse() {
5903 let cmd = SpecCommand::builder()
5904 .name("test")
5905 .arg(SpecArg::builder().name("arg").build())
5906 .flag(SpecFlag::builder().long("flag").build())
5907 .build();
5908 let spec = Spec {
5909 name: "test".to_string(),
5910 bin: "test".to_string(),
5911 cmd,
5912 ..Default::default()
5913 };
5914 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5915 let parsed = parse(&spec, &input).unwrap();
5916 assert_eq!(parsed.cmds.len(), 1);
5917 assert_eq!(parsed.cmds[0].name, "test");
5918 assert_eq!(parsed.args.len(), 1);
5919 assert_eq!(parsed.flags.len(), 1);
5920 assert_eq!(parsed.available_flags.len(), 1);
5921 }
5922
5923 #[test]
5924 fn test_flag_overrides_last_occurrence_wins() {
5925 let spec: Spec = r#"
5926flag "--stdin" default=#true
5927flag "--file <file>" overrides="--stdin"
5928 "#
5929 .parse()
5930 .unwrap();
5931
5932 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
5933 assert_eq!(file_wins.flags.len(), 1);
5934 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
5935 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
5936
5937 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5938 assert_eq!(stdin_wins.flags.len(), 1);
5939 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5940 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5941 }
5942
5943 #[test]
5944 fn test_flag_override_clears_pending_value() {
5945 let spec: Spec = r#"
5946flag "--file <file>" overrides="--stdin"
5947flag "--stdin"
5948arg "[input]"
5949 "#
5950 .parse()
5951 .unwrap();
5952
5953 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5954 assert_eq!(parsed.flags.len(), 1);
5955 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5956 assert_eq!(first_string_value(&parsed), "input.txt");
5957 }
5958
5959 #[cfg(unix)]
5960 #[test]
5961 fn a_mount_on_the_root_discovers_subcommands() {
5962 let spec: Spec = r#"
5966name "ex"
5967bin "ex"
5968cmd "declared"
5969mount run="echo 'cmd \"discovered\"'"
5970"#
5971 .parse()
5972 .unwrap();
5973
5974 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5975 assert_eq!(out.cmd.name, "discovered");
5976 }
5977
5978 #[test]
5979 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5980 let spec: Spec = r#"
5981name "ex"
5982bin "ex"
5983mount run="this command must never run"
5984cmd "declared"
5985"#
5986 .parse()
5987 .unwrap();
5988
5989 Parser::new(&spec)
5990 .with_mount_outputs(HashMap::new())
5991 .parse(&input(&["ex", "declared"]))
5992 .expect("a declared command does not resolve the mount");
5993
5994 let error = Parser::new(&spec)
5995 .with_mount_outputs(HashMap::new())
5996 .parse(&input(&["ex", "discovered"]))
5997 .unwrap_err();
5998 assert!(
5999 error
6000 .to_string()
6001 .contains("No injected output was provided for mount command"),
6002 "{error}"
6003 );
6004 }
6005
6006 #[cfg(unix)]
6007 #[test]
6008 fn completion_sees_root_mounted_commands_with_nothing_typed() {
6009 let spec: Spec = r#"
6013name "ex"
6014bin "ex"
6015cmd "declared"
6016mount run="echo 'cmd \"discovered\"'"
6017"#
6018 .parse()
6019 .unwrap();
6020
6021 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6022 assert!(
6023 out.cmd.subcommands.contains_key("discovered"),
6024 "a completion should see mounted commands; got {:?}",
6025 out.cmd.subcommands.keys().collect::<Vec<_>>()
6026 );
6027 }
6028
6029 #[cfg(unix)]
6030 #[test]
6031 fn completion_and_execution_agree_about_discovery() {
6032 let spec: Spec = r#"
6036name "ex"
6037bin "ex"
6038default_subcommand "run"
6039cmd "run" {
6040 arg "<task>"
6041}
6042mount run="exit 1"
6043"#
6044 .parse()
6045 .unwrap();
6046
6047 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6048 assert!(
6049 !out.cmd.subcommands.contains_key("discovered"),
6050 "a completion must not offer what execution will not route"
6051 );
6052
6053 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6054 assert_eq!(out.cmd.name, "run");
6055 }
6056
6057 #[cfg(unix)]
6058 #[test]
6059 fn a_default_subcommand_outranks_discovery() {
6060 let spec: Spec = r#"
6063name "ex"
6064bin "ex"
6065default_subcommand "run"
6066cmd "run" {
6067 arg "<task>"
6068}
6069mount run="exit 1"
6070"#
6071 .parse()
6072 .unwrap();
6073
6074 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6075 assert_eq!(out.cmd.name, "run");
6076 }
6077
6078 #[cfg(unix)]
6079 #[test]
6080 fn a_mount_may_ask_to_outrank_the_default() {
6081 let spec: Spec = r#"
6084name "ex"
6085bin "ex"
6086default_subcommand "run"
6087cmd "run" {
6088 arg "<task>"
6089}
6090mount run="echo 'cmd \"discovered\"'" overrides_default=#true
6091"#
6092 .parse()
6093 .unwrap();
6094
6095 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6096 assert_eq!(out.cmd.name, "discovered");
6097
6098 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6100 assert_eq!(out.cmd.name, "run");
6101 }
6102
6103 #[cfg(unix)]
6104 #[test]
6105 fn a_flag_does_not_run_the_mount() {
6106 let spec: Spec = r#"
6110name "ex"
6111bin "ex"
6112flag "--verbose"
6113cmd "declared"
6114mount run="exit 1"
6115"#
6116 .parse()
6117 .unwrap();
6118
6119 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6120 assert_eq!(out.cmd.name, "ex");
6121 }
6122
6123 #[cfg(unix)]
6124 #[test]
6125 fn a_declared_subcommand_does_not_run_the_mount() {
6126 let spec: Spec = r#"
6130name "ex"
6131bin "ex"
6132cmd "declared"
6133mount run="exit 1"
6134"#
6135 .parse()
6136 .unwrap();
6137
6138 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6139 assert_eq!(out.cmd.name, "declared");
6140 }
6141
6142 #[test]
6143 fn a_root_mount_survives_being_written_out() {
6144 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6145 .parse()
6146 .unwrap();
6147 assert_eq!(spec.cmd.mounts.len(), 1);
6148
6149 let reparsed: Spec = spec.to_string().parse().unwrap();
6150 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6151 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6152 }
6153
6154 #[test]
6155 fn test_mount_prefix_applies_flag_overrides() {
6156 let stdin = Arc::new(
6157 SpecFlag::builder()
6158 .name("stdin")
6159 .long("stdin")
6160 .global(true)
6161 .build(),
6162 );
6163 let file = Arc::new(
6164 SpecFlag::builder()
6165 .name("file")
6166 .long("file")
6167 .arg(SpecArg::builder().name("file").build())
6168 .global(true)
6169 .overrides_with(vec!["--stdin".to_string()])
6170 .build(),
6171 );
6172 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6173
6174 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6175 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6176
6177 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6178 }
6179
6180 #[test]
6181 fn test_flag_override_suppresses_env_value() {
6182 let spec: Spec = r#"
6183flag "--stdin" env="USE_STDIN"
6184flag "--file <file>" overrides="--stdin"
6185 "#
6186 .parse()
6187 .unwrap();
6188
6189 let parsed = parse_with_env(
6190 &spec,
6191 &["test", "--file", "input.txt"],
6192 &[("USE_STDIN", "true")],
6193 )
6194 .unwrap();
6195 assert_eq!(parsed.flags.len(), 1);
6196 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6197 }
6198
6199 #[test]
6200 fn test_flag_override_suppresses_required_check() {
6201 let spec: Spec = r#"
6202flag "--stdin" required=#true
6203flag "--file <file>" overrides="--stdin"
6204 "#
6205 .parse()
6206 .unwrap();
6207
6208 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6209 assert_eq!(parsed.flags.len(), 1);
6210 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6211 }
6212
6213 #[test]
6214 fn test_flag_required_if() {
6215 let spec: Spec = r#"
6216flag "--dir <dir>"
6217flag "--file <file>" required_if="--dir"
6218 "#
6219 .parse()
6220 .unwrap();
6221
6222 parse(&spec, &input(&["test"])).unwrap();
6223 assert_parse_err(
6224 parse(&spec, &input(&["test", "--dir", "src"])),
6225 "Missing required flag: --file <file>",
6226 );
6227 parse(
6228 &spec,
6229 &input(&["test", "--dir", "src", "--file", "input.txt"]),
6230 )
6231 .unwrap();
6232 }
6233
6234 #[test]
6235 fn test_flag_required_unless() {
6236 let spec: Spec = r#"
6237flag "--stdin"
6238flag "--file <file>" required_unless="--stdin"
6239 "#
6240 .parse()
6241 .unwrap();
6242
6243 assert_parse_err(
6244 parse(&spec, &input(&["test"])),
6245 "Missing required flag: --file <file>",
6246 );
6247 parse(&spec, &input(&["test", "--stdin"])).unwrap();
6248 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6249 }
6250
6251 #[test]
6252 fn complete_required_relationship_truth_tables() {
6253 let spec: Spec = r#"
6254name "ex"
6255bin "ex"
6256flag "--mode <mode>"
6257flag "--scope <scope>"
6258flag "--token <token>" {
6259 required_if_eq "--mode" "remote"
6260}
6261flag "--approval <approval>" {
6262 required_if_eq_all "--mode" "remote" "--scope" "global"
6263}
6264flag "--input <input>" {
6265 required_unless "--stdin" "--file"
6266}
6267flag "--checksum <checksum>" {
6268 required_unless_all "--stdin" "--file"
6269}
6270flag "--stdin"
6271flag "--file <file>"
6272arg "[request]" {
6273 requires "--mode" "--scope"
6274}
6275"#
6276 .parse()
6277 .unwrap();
6278 let parse_args = |args: &[&str]| {
6279 parse(
6280 &spec,
6281 &args
6282 .iter()
6283 .map(|arg| (*arg).to_string())
6284 .collect::<Vec<_>>(),
6285 )
6286 };
6287
6288 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6289 assert!(
6290 parse_args(&[
6291 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6292 ])
6293 .is_err()
6294 );
6295 parse_args(&[
6296 "ex",
6297 "--mode",
6298 "remote",
6299 "--token",
6300 "secret",
6301 "--scope",
6302 "global",
6303 "--approval",
6304 "yes",
6305 "--stdin",
6306 "--file",
6307 "in",
6308 ])
6309 .unwrap();
6310 parse_args(&[
6311 "ex",
6312 "--mode",
6313 "local",
6314 "--scope",
6315 "project",
6316 "--stdin",
6317 "--checksum",
6318 "sum",
6319 "request.json",
6320 ])
6321 .unwrap();
6322
6323 let reparsed: Spec = spec.to_string().parse().unwrap();
6324 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6325 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6326 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6327 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6328 }
6329
6330 #[test]
6331 fn test_conditional_requirements_treat_env_as_explicit() {
6332 let spec: Spec = r#"
6333flag "--dir <dir>" env="INPUT_DIR"
6334flag "--stdin" env="USE_STDIN"
6335flag "--file <file>" required_if="--dir" required_unless="--stdin"
6336 "#
6337 .parse()
6338 .unwrap();
6339
6340 assert_parse_err(
6341 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6342 "Missing required flag: --file <file>",
6343 );
6344 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6345 }
6346
6347 #[test]
6348 fn test_custom_env_does_not_fall_back_to_process_env() {
6349 assert!(std::env::var("PATH").is_ok());
6350 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6351
6352 assert_parse_err(
6353 parse_with_env(&spec, &["test"], &[]),
6354 "Missing required flag: --file <file>",
6355 );
6356 }
6357
6358 #[test]
6359 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6360 let spec: Spec = r#"
6361flag "--dir <dir>" default="src"
6362flag "--file <file>" required_if="--dir"
6363 "#
6364 .parse()
6365 .unwrap();
6366
6367 parse(&spec, &input(&["test"])).unwrap();
6368 }
6369
6370 #[test]
6371 fn test_conditional_requirements_see_overridden_flags_as_absent() {
6372 let spec: Spec = r#"
6373flag "--stdin"
6374flag "--dir <dir>" overrides="--stdin"
6375flag "--file <file>" required_unless="--stdin"
6376 "#
6377 .parse()
6378 .unwrap();
6379
6380 assert_parse_err(
6381 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6382 "Missing required flag: --file <file>",
6383 );
6384 }
6385
6386 #[test]
6387 fn short_flag_is_one_character_not_one_byte() {
6388 let spec = spec_with_flag(
6392 SpecFlag::builder()
6393 .short('磨')
6394 .long("polish")
6395 .arg(SpecArg::builder().name("opt").build())
6396 .build(),
6397 );
6398 let attached = Parser::new(&spec)
6399 .parse(&input(&["test", "-磨VALUE"]))
6400 .unwrap();
6401 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6402 let detached = Parser::new(&spec)
6403 .parse(&input(&["test", "-磨", "V"]))
6404 .unwrap();
6405 assert_eq!(flag_string_value(&detached, "polish"), "V");
6406 }
6407
6408 #[test]
6409 fn test_as_env() {
6410 let cmd = SpecCommand::builder()
6411 .name("test")
6412 .arg(SpecArg::builder().name("arg").build())
6413 .flag(SpecFlag::builder().long("flag").build())
6414 .flag(
6415 SpecFlag::builder()
6416 .long("force")
6417 .negate("--no-force")
6418 .build(),
6419 )
6420 .build();
6421 let spec = Spec {
6422 name: "test".to_string(),
6423 bin: "test".to_string(),
6424 cmd,
6425 ..Default::default()
6426 };
6427 let input = vec![
6428 "test".to_string(),
6429 "--flag".to_string(),
6430 "--no-force".to_string(),
6431 ];
6432 let parsed = parse(&spec, &input).unwrap();
6433 let env = parsed.as_env();
6434 assert_eq!(env.len(), 2);
6435 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6436 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6437 }
6438
6439 #[test]
6440 fn as_env_names_the_subcommand_path() {
6441 let spec: Spec = r#"
6442 flag "-v --verbose"
6443 cmd "db" {
6444 cmd "migrate" {
6445 alias "m"
6446 arg "[target]"
6447 }
6448 }
6449 "#
6450 .parse()
6451 .unwrap();
6452 let env = |args: &[&str]| {
6453 let input: Vec<String> = std::iter::once("test")
6454 .chain(args.iter().copied())
6455 .map(String::from)
6456 .collect();
6457 parse(&spec, &input).unwrap().as_env()
6458 };
6459
6460 let nested = env(&["db", "m", "v2"]);
6462 assert_eq!(
6463 nested.get("usage_cmd").map(String::as_str),
6464 Some("db migrate")
6465 );
6466 assert_eq!(nested.get("usage_target").map(String::as_str), Some("v2"));
6467 assert_eq!(
6468 env(&["db"]).get("usage_cmd").map(String::as_str),
6469 Some("db")
6470 );
6471 assert!(!env(&["-v"]).contains_key("usage_cmd"));
6472 }
6473
6474 #[test]
6475 fn as_env_leaves_usage_cmd_to_a_declared_cmd() {
6476 let spec: Spec = r#"
6478 cmd "run" {
6479 arg "[cmd]"
6480 }
6481 "#
6482 .parse()
6483 .unwrap();
6484 let input = ["test", "run", "build"].map(String::from);
6485 let env = parse(&spec, &input).unwrap().as_env();
6486 assert_eq!(env.get("usage_cmd").map(String::as_str), Some("build"));
6487 let input = ["test", "run"].map(String::from);
6489 assert!(
6490 !parse(&spec, &input)
6491 .unwrap()
6492 .as_env()
6493 .contains_key("usage_cmd")
6494 );
6495
6496 let spec: Spec = r#"
6498 flag "--cmd <cmd>" global=#true
6499 cmd "run"
6500 "#
6501 .parse()
6502 .unwrap();
6503 let input = ["test", "run"].map(String::from);
6504 assert!(
6505 !parse(&spec, &input)
6506 .unwrap()
6507 .as_env()
6508 .contains_key("usage_cmd")
6509 );
6510
6511 let spec: Spec = r#"
6513 cmd "run" {
6514 clause "steps" separator=":::" {
6515 arg "[cmd]"
6516 }
6517 }
6518 "#
6519 .parse()
6520 .unwrap();
6521 let input = ["test", "run"].map(String::from);
6522 assert!(
6523 !parse(&spec, &input)
6524 .unwrap()
6525 .as_env()
6526 .contains_key("usage_cmd")
6527 );
6528 }
6529
6530 #[test]
6531 fn test_arg_env_var() {
6532 let cmd = SpecCommand::builder()
6533 .name("test")
6534 .arg(
6535 SpecArg::builder()
6536 .name("input")
6537 .env("TEST_ARG_INPUT")
6538 .required(true)
6539 .build(),
6540 )
6541 .build();
6542 let spec = Spec {
6543 name: "test".to_string(),
6544 bin: "test".to_string(),
6545 cmd,
6546 ..Default::default()
6547 };
6548
6549 unsafe { std::env::set_var("TEST_ARG_INPUT", "test_file.txt") };
6552
6553 let input = vec!["test".to_string()];
6554 let parsed = parse(&spec, &input).unwrap();
6555
6556 assert_eq!(parsed.args.len(), 1);
6557 let arg = parsed.args.keys().next().unwrap();
6558 assert_eq!(arg.name, "input");
6559 let value = parsed.args.values().next().unwrap();
6560 assert_eq!(value.to_string(), "test_file.txt");
6561
6562 unsafe { std::env::remove_var("TEST_ARG_INPUT") };
6565 }
6566
6567 #[test]
6568 fn test_flag_env_var_with_arg() {
6569 let cmd = SpecCommand::builder()
6570 .name("test")
6571 .flag(
6572 SpecFlag::builder()
6573 .long("output")
6574 .env("TEST_FLAG_OUTPUT")
6575 .arg(SpecArg::builder().name("file").build())
6576 .build(),
6577 )
6578 .build();
6579 let spec = Spec {
6580 name: "test".to_string(),
6581 bin: "test".to_string(),
6582 cmd,
6583 ..Default::default()
6584 };
6585
6586 unsafe { std::env::set_var("TEST_FLAG_OUTPUT", "output.txt") };
6589
6590 let input = vec!["test".to_string()];
6591 let parsed = parse(&spec, &input).unwrap();
6592
6593 assert_eq!(parsed.flags.len(), 1);
6594 let flag = parsed.flags.keys().next().unwrap();
6595 assert_eq!(flag.name, "output");
6596 let value = parsed.flags.values().next().unwrap();
6597 assert_eq!(value.to_string(), "output.txt");
6598
6599 unsafe { std::env::remove_var("TEST_FLAG_OUTPUT") };
6602 }
6603
6604 #[test]
6605 fn test_flag_env_var_boolean() {
6606 let cmd = SpecCommand::builder()
6607 .name("test")
6608 .flag(
6609 SpecFlag::builder()
6610 .long("verbose")
6611 .env("TEST_FLAG_VERBOSE")
6612 .build(),
6613 )
6614 .build();
6615 let spec = Spec {
6616 name: "test".to_string(),
6617 bin: "test".to_string(),
6618 cmd,
6619 ..Default::default()
6620 };
6621
6622 unsafe { std::env::set_var("TEST_FLAG_VERBOSE", "true") };
6625
6626 let input = vec!["test".to_string()];
6627 let parsed = parse(&spec, &input).unwrap();
6628
6629 assert_eq!(parsed.flags.len(), 1);
6630 let flag = parsed.flags.keys().next().unwrap();
6631 assert_eq!(flag.name, "verbose");
6632 let value = parsed.flags.values().next().unwrap();
6633 assert_eq!(value.to_string(), "true");
6634
6635 unsafe { std::env::remove_var("TEST_FLAG_VERBOSE") };
6638 }
6639
6640 #[test]
6641 fn test_env_var_precedence() {
6642 let cmd = SpecCommand::builder()
6644 .name("test")
6645 .arg(
6646 SpecArg::builder()
6647 .name("input")
6648 .env("TEST_PRECEDENCE_INPUT")
6649 .required(true)
6650 .build(),
6651 )
6652 .build();
6653 let spec = Spec {
6654 name: "test".to_string(),
6655 bin: "test".to_string(),
6656 cmd,
6657 ..Default::default()
6658 };
6659
6660 unsafe { std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt") };
6663
6664 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6665 let parsed = parse(&spec, &input).unwrap();
6666
6667 assert_eq!(parsed.args.len(), 1);
6668 let value = parsed.args.values().next().unwrap();
6669 assert_eq!(value.to_string(), "cli_file.txt");
6671
6672 unsafe { std::env::remove_var("TEST_PRECEDENCE_INPUT") };
6675 }
6676
6677 #[test]
6678 fn test_flag_var_true_with_single_default() {
6679 let cmd = SpecCommand::builder()
6681 .name("test")
6682 .flag(
6683 SpecFlag::builder()
6684 .long("foo")
6685 .var(true)
6686 .arg(SpecArg::builder().name("foo").build())
6687 .default_value("bar")
6688 .build(),
6689 )
6690 .build();
6691 let spec = Spec {
6692 name: "test".to_string(),
6693 bin: "test".to_string(),
6694 cmd,
6695 ..Default::default()
6696 };
6697
6698 let input = vec!["test".to_string()];
6700 let parsed = parse(&spec, &input).unwrap();
6701
6702 assert_eq!(parsed.flags.len(), 1);
6703 let flag = parsed.flags.keys().next().unwrap();
6704 assert_eq!(flag.name, "foo");
6705 let value = parsed.flags.values().next().unwrap();
6706 match value {
6708 ParseValue::MultiString(v) => {
6709 assert_eq!(v.len(), 1);
6710 assert_eq!(v[0], "bar");
6711 }
6712 _ => panic!("Expected MultiString, got {:?}", value),
6713 }
6714 }
6715
6716 #[test]
6717 fn test_flag_var_true_with_multiple_defaults() {
6718 let cmd = SpecCommand::builder()
6720 .name("test")
6721 .flag(
6722 SpecFlag::builder()
6723 .long("foo")
6724 .var(true)
6725 .arg(SpecArg::builder().name("foo").build())
6726 .default_values(["xyz", "bar"])
6727 .build(),
6728 )
6729 .build();
6730 let spec = Spec {
6731 name: "test".to_string(),
6732 bin: "test".to_string(),
6733 cmd,
6734 ..Default::default()
6735 };
6736
6737 let input = vec!["test".to_string()];
6739 let parsed = parse(&spec, &input).unwrap();
6740
6741 assert_eq!(parsed.flags.len(), 1);
6742 let value = parsed.flags.values().next().unwrap();
6743 match value {
6745 ParseValue::MultiString(v) => {
6746 assert_eq!(v.len(), 2);
6747 assert_eq!(v[0], "xyz");
6748 assert_eq!(v[1], "bar");
6749 }
6750 _ => panic!("Expected MultiString, got {:?}", value),
6751 }
6752 }
6753
6754 #[test]
6755 fn test_flag_var_false_with_default_remains_string() {
6756 let cmd = SpecCommand::builder()
6758 .name("test")
6759 .flag(
6760 SpecFlag::builder()
6761 .long("foo")
6762 .var(false) .arg(SpecArg::builder().name("foo").build())
6764 .default_value("bar")
6765 .build(),
6766 )
6767 .build();
6768 let spec = Spec {
6769 name: "test".to_string(),
6770 bin: "test".to_string(),
6771 cmd,
6772 ..Default::default()
6773 };
6774
6775 let input = vec!["test".to_string()];
6777 let parsed = parse(&spec, &input).unwrap();
6778
6779 assert_eq!(parsed.flags.len(), 1);
6780 let value = parsed.flags.values().next().unwrap();
6781 match value {
6783 ParseValue::String(s) => {
6784 assert_eq!(s, "bar");
6785 }
6786 _ => panic!("Expected String, got {:?}", value),
6787 }
6788 }
6789
6790 #[test]
6791 fn test_arg_var_true_with_single_default() {
6792 let cmd = SpecCommand::builder()
6794 .name("test")
6795 .arg(
6796 SpecArg::builder()
6797 .name("files")
6798 .var(true)
6799 .default_value("default.txt")
6800 .required(false)
6801 .build(),
6802 )
6803 .build();
6804 let spec = Spec {
6805 name: "test".to_string(),
6806 bin: "test".to_string(),
6807 cmd,
6808 ..Default::default()
6809 };
6810
6811 let input = vec!["test".to_string()];
6813 let parsed = parse(&spec, &input).unwrap();
6814
6815 assert_eq!(parsed.args.len(), 1);
6816 let value = parsed.args.values().next().unwrap();
6817 match value {
6819 ParseValue::MultiString(v) => {
6820 assert_eq!(v.len(), 1);
6821 assert_eq!(v[0], "default.txt");
6822 }
6823 _ => panic!("Expected MultiString, got {:?}", value),
6824 }
6825 }
6826
6827 #[test]
6828 fn test_arg_var_true_with_multiple_defaults() {
6829 let cmd = SpecCommand::builder()
6831 .name("test")
6832 .arg(
6833 SpecArg::builder()
6834 .name("files")
6835 .var(true)
6836 .default_values(["file1.txt", "file2.txt"])
6837 .required(false)
6838 .build(),
6839 )
6840 .build();
6841 let spec = Spec {
6842 name: "test".to_string(),
6843 bin: "test".to_string(),
6844 cmd,
6845 ..Default::default()
6846 };
6847
6848 let input = vec!["test".to_string()];
6850 let parsed = parse(&spec, &input).unwrap();
6851
6852 assert_eq!(parsed.args.len(), 1);
6853 let value = parsed.args.values().next().unwrap();
6854 match value {
6856 ParseValue::MultiString(v) => {
6857 assert_eq!(v.len(), 2);
6858 assert_eq!(v[0], "file1.txt");
6859 assert_eq!(v[1], "file2.txt");
6860 }
6861 _ => panic!("Expected MultiString, got {:?}", value),
6862 }
6863 }
6864
6865 #[test]
6866 fn test_arg_var_false_with_default_remains_string() {
6867 let cmd = SpecCommand::builder()
6869 .name("test")
6870 .arg(
6871 SpecArg::builder()
6872 .name("file")
6873 .var(false)
6874 .default_value("default.txt")
6875 .required(false)
6876 .build(),
6877 )
6878 .build();
6879 let spec = Spec {
6880 name: "test".to_string(),
6881 bin: "test".to_string(),
6882 cmd,
6883 ..Default::default()
6884 };
6885
6886 let input = vec!["test".to_string()];
6888 let parsed = parse(&spec, &input).unwrap();
6889
6890 assert_eq!(parsed.args.len(), 1);
6891 let value = parsed.args.values().next().unwrap();
6892 match value {
6894 ParseValue::String(s) => {
6895 assert_eq!(s, "default.txt");
6896 }
6897 _ => panic!("Expected String, got {:?}", value),
6898 }
6899 }
6900
6901 #[test]
6902 fn test_scalar_defaults_validate_only_first_default_choice() {
6903 let specs = [
6904 spec_with_arg(
6905 SpecArg::builder()
6906 .name("env")
6907 .var(false)
6908 .default_values(["dev", "prod"])
6909 .choices(["dev"])
6910 .required(false)
6911 .build(),
6912 ),
6913 spec_with_flag(
6914 SpecFlag::builder()
6915 .long("env")
6916 .arg(
6917 SpecArg::builder()
6918 .name("env")
6919 .default_values(["dev", "prod"])
6920 .choices(["dev"])
6921 .build(),
6922 )
6923 .build(),
6924 ),
6925 ];
6926
6927 for spec in specs {
6928 let parsed = parse(&spec, &input(&["test"])).unwrap();
6929 assert_eq!(first_string_value(&parsed), "dev");
6930 }
6931 }
6932
6933 #[test]
6934 fn a_delimiter_turns_one_word_into_several_values() {
6935 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
6936 .parse()
6937 .unwrap();
6938
6939 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
6940 let multi = |value: &ParseValue| match value {
6941 ParseValue::MultiString(values) => values.clone(),
6942 other => panic!("expected several values, got {other:?}"),
6943 };
6944 let tags = parsed
6945 .flags
6946 .iter()
6947 .find(|(f, _)| f.name == "tags")
6948 .map(|(_, v)| v)
6949 .unwrap();
6950 assert_eq!(multi(tags), vec!["a", "b", "c"]);
6951 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
6952 }
6953
6954 #[test]
6955 fn a_positional_splits_before_its_choices_are_asked() {
6956 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
6960 .parse()
6961 .unwrap();
6962
6963 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
6964
6965 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
6966 let message = err.to_string();
6967 assert!(message.contains("nowhere"), "{message}");
6968 assert!(
6969 !message.contains("src:nowhere"),
6970 "the bad half should be named, not the whole word: {message}"
6971 );
6972 }
6973
6974 #[test]
6975 fn a_split_value_is_counted_and_judged_as_values() {
6976 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
6980 .parse()
6981 .unwrap();
6982
6983 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
6984 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
6985 assert!(err.to_string().contains("staging"), "{err}");
6986 assert!(
6987 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
6988 "three values should breach var_max=2"
6989 );
6990 }
6991
6992 #[test]
6993 fn a_split_bound_counts_one_occurrence_at_a_time() {
6994 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
6999 .parse()
7000 .unwrap();
7001
7002 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
7003 assert!(
7004 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
7005 "three values out of one word is still three values"
7006 );
7007 parse(
7010 &spec,
7011 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
7012 )
7013 .expect("two per occurrence, twice, is within the bound");
7014 }
7015
7016 #[test]
7017 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
7018 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
7019 .parse()
7020 .unwrap();
7021
7022 parse(
7023 &spec,
7024 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
7025 )
7026 .expect("each occurrence satisfies the bound independently");
7027
7028 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
7029 assert!(
7030 error
7031 .to_string()
7032 .contains("requires at least 2 value(s), got 1"),
7033 "{error:?}"
7034 );
7035 }
7036
7037 #[test]
7038 fn an_exclusive_flag_has_to_be_alone() {
7039 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
7040 .parse()
7041 .unwrap();
7042
7043 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
7044
7045 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
7047 assert!(err.to_string().contains("on its own"), "{err}");
7048
7049 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
7052 assert!(err.to_string().contains("on its own"), "{err}");
7053
7054 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
7056 }
7057
7058 #[test]
7059 fn an_exclusive_flag_conflicts_with_clause_arguments() {
7060 let spec: Spec = r#"name "ex"
7061bin "ex"
7062flag "--dump" exclusive=#true
7063clause "tasks" separator=":::" {
7064 arg "<task>"
7065}
7066"#
7067 .parse()
7068 .unwrap();
7069
7070 let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
7071 assert!(err.to_string().contains("on its own"), "{err}");
7072 }
7073
7074 #[test]
7075 fn a_partial_parse_reports_the_next_implicit_clause_argument() {
7076 let spec: Spec = r#"name "ex"
7077bin "ex"
7078clause "tools" {
7079 arg "<tool@version>"
7080}
7081"#
7082 .parse()
7083 .unwrap();
7084
7085 let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
7086
7087 assert_eq!(
7088 parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
7089 Some("tool@version")
7090 );
7091 }
7092
7093 #[test]
7094 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
7095 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
7098 .parse()
7099 .unwrap();
7100
7101 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
7102 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
7103 }
7104
7105 #[test]
7106 fn an_exclusive_flag_bypasses_required_siblings() {
7107 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
7108 .parse()
7109 .unwrap();
7110
7111 parse(&spec, &input(&["ex", "--dump"]))
7112 .expect("exclusive is the command's requiredness escape");
7113 assert!(
7114 parse(
7115 &spec,
7116 &input(&["ex", "--dump", "--out", "somewhere", "target"])
7117 )
7118 .is_err()
7119 );
7120 }
7121
7122 #[test]
7123 fn an_environment_value_counts_for_an_exclusive_flag() {
7124 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
7125 .parse()
7126 .unwrap();
7127
7128 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
7129 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
7130 }
7131
7132 #[test]
7133 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
7134 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
7135 .parse()
7136 .unwrap();
7137
7138 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
7139 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
7140 }
7141
7142 #[test]
7143 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
7144 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7145 .parse()
7146 .unwrap();
7147
7148 parse(&spec, &input(&["ex", "run", "--clean"]))
7149 .expect("the child flag is alone within the child command");
7150 assert!(
7151 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7152 "the parent flag still conflicts with selecting the child"
7153 );
7154 }
7155
7156 #[test]
7157 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
7158 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7159 .parse()
7160 .unwrap();
7161
7162 parse(&spec, &input(&["ex", "run", "--clean"]))
7163 .expect("the child-local exclusive flag is alone inside the child command");
7164 assert!(
7165 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7166 "the ancestor spelling still conflicts with selecting the child"
7167 );
7168 }
7169
7170 #[test]
7171 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
7172 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
7173 .parse()
7174 .unwrap();
7175
7176 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
7177 assert!(
7178 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
7179 "the distinct parent declaration is still company despite sharing a name"
7180 );
7181 }
7182
7183 #[test]
7184 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
7185 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7186 .parse()
7187 .unwrap();
7188
7189 parse(&spec, &input(&["ex", "run", "--clean"]))
7190 .expect("the child exclusive flag is valid alone");
7191 assert!(
7192 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7193 "merging with the inherited global must not discard child exclusivity"
7194 );
7195 }
7196
7197 #[test]
7198 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
7199 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7200 .parse()
7201 .unwrap();
7202
7203 parse(&spec, &input(&["ex", "run", "--clean"]))
7204 .expect("the typed long form belongs to the child declaration");
7205 assert!(
7206 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7207 "the inherited short form still belongs to the ancestor"
7208 );
7209 assert!(
7210 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
7211 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
7212 );
7213 }
7214
7215 #[test]
7216 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7217 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
7218 .parse()
7219 .unwrap();
7220
7221 parse(&spec, &input(&["ex", "run", "--clean"]))
7222 .expect("the child's spelling does not activate the orphan ancestor alias");
7223 assert!(
7224 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7225 "the inherited short alias still belongs to the ancestor exclusive flag"
7226 );
7227 }
7228
7229 #[test]
7230 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7231 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"
7232 .parse()
7233 .unwrap();
7234
7235 assert!(
7236 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7237 "the inherited negated alias still belongs to the ancestor exclusive flag"
7238 );
7239 }
7240
7241 #[test]
7242 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7243 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"
7248 .parse()
7249 .unwrap();
7250
7251 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7252 assert!(
7253 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7254 "one unrelated alias collision cannot disown the child from its own flag"
7255 );
7256 }
7257
7258 #[test]
7259 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7260 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7266 .parse()
7267 .unwrap();
7268 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7269 .expect("the ancestor owns this occurrence, and it is not exclusive");
7270 assert!(
7271 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7272 "after the subcommand word the child's declaration is in scope, and it is exclusive"
7273 );
7274
7275 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7276 .parse()
7277 .unwrap();
7278 assert!(
7279 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7280 "the same rule, with an exclusive ancestor: selecting the child is company for it"
7281 );
7282 }
7283
7284 #[test]
7285 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7286 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
7290 .parse()
7291 .unwrap();
7292
7293 assert!(
7294 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7295 "the orphan ancestor alias is still the ancestor's exclusive flag"
7296 );
7297 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7298 .expect("the child's own spelling drops the exclusivity the child did not restate");
7299 }
7300
7301 #[test]
7302 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7303 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7307 .parse()
7308 .unwrap();
7309
7310 assert!(
7311 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7312 "the child spelling is exclusive whatever it was typed beside"
7313 );
7314 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7315 .expect("the ancestor's own spelling was never exclusive");
7316 }
7317
7318 #[test]
7319 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7320 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"
7324 .parse()
7325 .unwrap();
7326
7327 assert!(
7328 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7329 "the child added exclusivity the environment value has to honor"
7330 );
7331
7332 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"
7333 .parse()
7334 .unwrap();
7335
7336 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7337 .expect("the child dropped the exclusivity, and the environment value follows it");
7338 }
7339
7340 #[test]
7341 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7342 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"
7345 .parse()
7346 .unwrap();
7347
7348 parse(&spec, &input(&["ex", "run", "--clean"]))
7349 .expect("a merged child exclusive flag is still the command's requiredness escape");
7350 }
7351
7352 #[test]
7353 fn a_group_allows_one_member_and_refuses_two() {
7354 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7355 .parse()
7356 .unwrap();
7357
7358 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7360 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7361
7362 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7363 assert!(err.to_string().contains("group input"), "{err}");
7364 }
7365
7366 #[test]
7367 fn positional_selectors_work_in_conflicts_and_groups() {
7368 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7369 .parse()
7370 .unwrap();
7371 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7372 .expect("the flag alone is valid");
7373 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7374 assert!(
7375 parse(
7376 &conflicts,
7377 &input(&["ex", "--from-file", "vars.env", "literal"])
7378 )
7379 .is_err()
7380 );
7381
7382 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7383 .parse()
7384 .unwrap();
7385 assert!(
7386 parse(
7387 &positional_source,
7388 &input(&["ex", "--from-file", "vars.env", "literal"])
7389 )
7390 .is_err()
7391 );
7392
7393 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7394 .parse()
7395 .unwrap();
7396 assert!(parse(&group, &input(&["ex"])).is_err());
7397 parse(&group, &input(&["ex", "target-name"]))
7398 .expect("a positional satisfies a required group");
7399 assert!(
7400 parse(
7401 &group,
7402 &input(&["ex", "--file", "input.txt", "target-name"])
7403 )
7404 .is_err()
7405 );
7406 }
7407
7408 #[test]
7409 fn a_required_group_needs_one_of_its_members() {
7410 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7411 .parse()
7412 .unwrap();
7413
7414 let err = parse(&spec, &input(&["ex"])).unwrap_err();
7415 assert!(err.to_string().contains("--file, --url"), "{err}");
7418 assert!(err.to_string().contains("input"), "{err}");
7419
7420 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7421 }
7422
7423 #[test]
7424 fn a_multiple_group_only_polices_requiredness() {
7425 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7428 .parse()
7429 .unwrap();
7430
7431 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7432 assert!(parse(&spec, &input(&["ex"])).is_err());
7433 }
7434
7435 #[test]
7436 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7437 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7443 .parse()
7444 .unwrap();
7445
7446 parse(&spec, &input(&["ex"])).expect("the default fills the group");
7447 parse(&spec, &input(&["ex", "--url", "u"]))
7448 .expect("the default must not conflict with the flag the user typed");
7449 }
7450
7451 #[test]
7452 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7453 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7456 .parse()
7457 .unwrap();
7458
7459 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7460 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7461
7462 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7464 assert!(err.to_string().contains("group input"), "{err}");
7465 }
7466
7467 #[test]
7468 fn a_group_reads_the_environment_as_given() {
7469 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7472 .parse()
7473 .unwrap();
7474
7475 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7476 }
7477
7478 #[test]
7479 fn a_requirement_names_the_flag_that_is_missing() {
7480 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7483 .parse()
7484 .unwrap();
7485
7486 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7487 assert!(
7488 err.to_string().contains("format"),
7489 "the missing flag should be named: {err}"
7490 );
7491
7492 for words in [
7494 &["ex", "--out", "a.txt", "--format", "json"][..],
7495 &["ex", "--format", "json", "--out", "a.txt"][..],
7496 ] {
7497 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7498 }
7499
7500 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7503 }
7504
7505 #[test]
7506 fn a_value_activates_only_its_conditional_requirement() {
7507 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"
7508 .parse()
7509 .unwrap();
7510
7511 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7512 .expect("an unrelated value requires nothing");
7513
7514 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7515 assert!(key.to_string().contains("key"), "{key}");
7516 parse(
7517 &spec,
7518 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7519 )
7520 .expect("the matching requirement is satisfied");
7521
7522 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7523 assert!(token.to_string().contains("token"), "{token}");
7524 }
7525
7526 #[test]
7527 fn conditional_requirements_read_explicit_env_but_not_defaults() {
7528 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"
7529 .parse()
7530 .unwrap();
7531 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7532 assert!(err.to_string().contains("key"), "{err}");
7533
7534 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"
7535 .parse()
7536 .unwrap();
7537 parse(&from_default, &input(&["ex"]))
7538 .expect("a default is not an explicit conditional value");
7539 }
7540
7541 #[test]
7542 fn command_line_values_override_env_for_conditional_requirements() {
7543 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7544 .parse()
7545 .unwrap();
7546
7547 parse_with_env(
7548 &spec,
7549 &["ex", "--config", "ordinary.toml"],
7550 &[("EX_CONFIG", "special.toml")],
7551 )
7552 .expect("the command-line value takes precedence over the environment");
7553 }
7554
7555 #[test]
7556 fn conditional_requirements_normalize_boolean_env_values() {
7557 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7558 .parse()
7559 .unwrap();
7560
7561 for value in ["1", "true", "True", "TRUE"] {
7562 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7563 assert!(err.to_string().contains("key"), "{value}: {err}");
7564 }
7565 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7566 .expect("a false environment value does not activate a true condition");
7567 }
7568
7569 #[test]
7570 fn a_default_satisfies_a_requirement() {
7571 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7575 .parse()
7576 .unwrap();
7577
7578 parse(&spec, &input(&["ex", "--out", "a.txt"]))
7579 .expect("a defaulted flag is not a missing one");
7580 }
7581
7582 #[test]
7583 fn a_present_flag_binds_a_conditional_default() {
7584 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7585 .parse()
7586 .unwrap();
7587
7588 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7589 assert_eq!(
7590 with.as_env().get("usage_bin_names").map(String::as_str),
7591 Some("true")
7592 );
7593
7594 let without = parse(&spec, &input(&["ex"])).unwrap();
7595 assert!(
7596 !without.as_env().contains_key("usage_bin_names"),
7597 "IsPresent does nothing when the selector is absent"
7598 );
7599 }
7600
7601 #[test]
7602 fn an_equals_condition_binds_a_conditional_default() {
7603 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7604 .parse()
7605 .unwrap();
7606
7607 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7608 assert_eq!(
7609 json.as_env().get("usage_style").map(String::as_str),
7610 Some("pretty")
7611 );
7612 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7613 assert!(!yaml.as_env().contains_key("usage_style"));
7614 }
7615
7616 #[test]
7617 fn an_equals_condition_reads_a_negated_flag() {
7618 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7619 .parse()
7620 .unwrap();
7621
7622 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7623 assert_eq!(
7624 off.as_env().get("usage_pretty").map(String::as_str),
7625 Some("true")
7626 );
7627 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7628 assert!(
7629 !on.as_env().contains_key("usage_pretty"),
7630 "--json is true, so when=false should miss"
7631 );
7632 }
7633
7634 #[test]
7635 fn the_first_matching_conditional_default_wins() {
7636 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"
7637 .parse()
7638 .unwrap();
7639
7640 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7641 assert_eq!(
7642 out.as_env().get("usage_style").map(String::as_str),
7643 Some("compact")
7644 );
7645 }
7646
7647 #[test]
7648 fn argv_and_env_suppress_a_conditional_default() {
7649 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7650 .parse()
7651 .unwrap();
7652
7653 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7654 assert_eq!(
7655 from_env.as_env().get("usage_bin_names").map(String::as_str),
7656 Some("false"),
7657 "the target's environment wins over default_if"
7658 );
7659 }
7660
7661 #[test]
7662 fn a_sibling_env_activates_a_conditional_default() {
7663 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7664 .parse()
7665 .unwrap();
7666
7667 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7668 assert_eq!(
7669 out.as_env().get("usage_bin_names").map(String::as_str),
7670 Some("true")
7671 );
7672 }
7673
7674 #[test]
7675 fn a_default_does_not_activate_a_conditional_default() {
7676 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7680 .parse()
7681 .unwrap();
7682
7683 let out = parse(&spec, &input(&["ex"])).unwrap();
7684 assert_eq!(
7685 out.as_env().get("usage_json").map(String::as_str),
7686 Some("true")
7687 );
7688 assert!(
7689 !out.as_env().contains_key("usage_pretty"),
7690 "a default is not an explicit value for default_if"
7691 );
7692 }
7693
7694 #[test]
7695 fn a_conditional_default_does_not_activate_requires_if() {
7696 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"
7697 .parse()
7698 .unwrap();
7699
7700 parse(&spec, &input(&["ex", "--json"]))
7701 .expect("a default_if value is not explicit for requires_if");
7702 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7703 }
7704
7705 #[test]
7706 fn a_conditional_default_satisfies_a_requirement() {
7707 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7708 .parse()
7709 .unwrap();
7710
7711 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7712 .expect("default_if fills the required flag");
7713 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7714 }
7715
7716 #[test]
7717 fn an_environment_value_satisfies_a_requirement() {
7718 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7719 .parse()
7720 .unwrap();
7721
7722 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7723 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7724 .expect("the environment supplies it");
7725 }
7726
7727 #[test]
7728 fn a_requirement_is_satisfied_by_a_short_form() {
7729 let spec: Spec =
7733 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7734 .parse()
7735 .unwrap();
7736
7737 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7738
7739 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7740 assert!(err.to_string().contains("key"), "{err}");
7741 }
7742
7743 #[test]
7744 fn conflicting_flags_are_rejected_in_either_order() {
7745 let spec: Spec =
7749 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7750 .parse()
7751 .unwrap();
7752
7753 for words in [
7754 &["ex", "--file", "a.txt", "--stdin"][..],
7755 &["ex", "--stdin", "--file", "a.txt"][..],
7756 ] {
7757 let err = parse(&spec, &input(words)).unwrap_err();
7758 assert!(
7759 err.to_string().contains("conflicts with --stdin"),
7760 "{words:?} should be refused: {err}"
7761 );
7762 }
7763
7764 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7766 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7767 }
7768
7769 #[test]
7770 fn unknown_flags_are_values_by_default() {
7771 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7774 .parse()
7775 .unwrap();
7776 let out = parse(
7777 &spec,
7778 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7779 )
7780 .unwrap();
7781 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7782 assert_eq!(out.args[rest].to_string(), "--wat x");
7783 }
7784
7785 #[test]
7786 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7787 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7788 .parse()
7789 .unwrap();
7790 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7791 .expect("a repeat is a correction by default");
7792 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7793 assert_eq!(out.flags[jobs].to_string(), "2");
7794 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7795 .expect("switches use the same default");
7796
7797 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7798 .parse()
7799 .unwrap();
7800 for words in [
7801 &["ex", "--jobs", "1", "--jobs", "2"][..],
7802 &["ex", "--verbose", "--verbose"][..],
7803 ] {
7804 let err = parse(&strict, &input(words)).unwrap_err();
7805 assert!(
7806 err.to_string().contains("cannot be used multiple times"),
7807 "{err}"
7808 );
7809 }
7810
7811 let reparsed: Spec = strict.to_string().parse().unwrap();
7812 assert!(!reparsed.cmd.args_override_self);
7813 }
7814
7815 #[test]
7816 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7817 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7818 .parse()
7819 .unwrap();
7820
7821 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7822 .expect("opposite forms override each other");
7823 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7824 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7825
7826 for words in [
7827 &["ex", "--color", "--color"][..],
7828 &["ex", "--no-color", "--no-color"][..],
7829 ] {
7830 let err = parse(&spec, &input(words)).unwrap_err();
7831 assert!(err.to_string().contains("cannot be used multiple times"));
7832 }
7833 }
7834
7835 #[test]
7836 fn strict_global_flags_may_repeat_across_command_levels() {
7837 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"
7838 .parse()
7839 .unwrap();
7840
7841 let out = parse(
7842 &spec,
7843 &input(&[
7844 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7845 ]),
7846 )
7847 .expect("an inherited global is allowed once at each command level");
7848 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7849 assert_eq!(out.flags[jobs].to_string(), "2");
7850
7851 for words in [
7852 &["ex", "--color", "--color", "run", "--no-color"][..],
7853 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7854 ] {
7855 let err = parse(&spec, &input(words)).unwrap_err();
7856 assert!(err.to_string().contains("cannot be used multiple times"));
7857 }
7858 }
7859
7860 #[test]
7861 fn a_subcommand_can_negate_only_its_parents_requirements() {
7862 let base = r#"name "ex"
7863bin "ex"
7864flag "--config" required=#true
7865flag "--mode" requires="--config"
7866flag "--other"
7867arg "<input>"
7868group "source" "--config" "--other" required=#true
7869cmd "run" { flag "--child" required=#true }
7870"#;
7871 let strict: Spec = base.parse().unwrap();
7872 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7873 let message = err.to_string();
7874 assert!(
7875 message.contains("input") || message.contains("config"),
7876 "{message}"
7877 );
7878
7879 let negated: Spec = base
7880 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7881 .parse()
7882 .unwrap();
7883 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7884 assert!(
7885 err.to_string().contains("child"),
7886 "the selected command keeps its own requirements: {err}"
7887 );
7888
7889 let mut child_optional = negated.clone();
7890 child_optional.cmd.subcommands["run"].flags[0].required = false;
7891 parse(&child_optional, &input(&["ex", "run"]))
7892 .expect("the child selection satisfies all parent requirements");
7893 parse(&child_optional, &input(&["ex", "--mode", "run"]))
7894 .expect("parent requires relationships are negated too");
7895 }
7896
7897 #[test]
7898 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
7899 let spec: Spec = r#"name "ex"
7900bin "ex"
7901args_conflicts_with_subcommands #true
7902flag "--verbose"
7903cmd "run"
7904"#
7905 .parse()
7906 .unwrap();
7907
7908 parse(&spec, &input(&["ex", "run"]))
7909 .expect("the subcommand is valid without a parent argument");
7910 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
7911 assert!(
7912 err.to_string().contains("cannot be used with arguments"),
7913 "{err}"
7914 );
7915 }
7916
7917 #[test]
7918 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
7919 let base = r#"name "ex"
7920bin "ex"
7921flag "--values <value>..."
7922cmd "run"
7923"#;
7924 let plain: Spec = base.parse().unwrap();
7925 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
7926 assert_eq!(out.cmd.name, "ex");
7927
7928 let precedence: Spec = base
7929 .replacen(
7930 "bin \"ex\"",
7931 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
7932 1,
7933 )
7934 .parse()
7935 .unwrap();
7936 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
7937 assert_eq!(out.cmd.name, "run");
7938 }
7939
7940 #[test]
7941 fn a_required_positional_can_follow_an_unfilled_optional_one() {
7942 let base = r#"name "ex"
7943bin "ex"
7944arg "[optional]"
7945arg "<required>"
7946"#;
7947 let plain: Spec = base.parse().unwrap();
7948 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
7949 assert!(err.to_string().contains("required"), "{err}");
7950
7951 let enabled: Spec = base
7952 .replacen(
7953 "bin \"ex\"",
7954 "bin \"ex\"\nallow_missing_positional #true",
7955 1,
7956 )
7957 .parse()
7958 .unwrap();
7959 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
7960 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7961 let value = &out
7962 .args
7963 .iter()
7964 .find(|(arg, _)| arg.name == "required")
7965 .unwrap()
7966 .1;
7967 assert!(matches!(value, ParseValue::String(value) if value == "value"));
7968 }
7969
7970 #[test]
7971 fn sigil_args_do_not_block_optional_positional_skipping() {
7972 let spec: Spec = r#"name "ex"
7973bin "ex"
7974allow_missing_positional #true
7975arg "[optional]"
7976arg "[tool]" sigil="@"
7977arg "<required>"
7978"#
7979 .parse()
7980 .unwrap();
7981
7982 let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
7983 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
7984 let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
7985 let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
7986 assert_eq!(out.args[tool].to_string(), "node");
7987 assert_eq!(out.args[required].to_string(), "value");
7988 }
7989
7990 #[test]
7991 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
7992 let spec: Spec =
7993 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
7994 .parse()
7995 .unwrap();
7996 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
7997 assert!(
7998 err.to_string().contains("--wat"),
7999 "the message should name the token: {err}"
8000 );
8001
8002 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
8005 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8006 assert_eq!(out.args[rest].to_string(), "-1");
8007
8008 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
8009 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8010 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8011 }
8012
8013 #[test]
8014 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
8015 let spec: Spec = r#"
8016name "ex"
8017bin "ex"
8018unknown_flags "error"
8019flag "-0 --print0" global=#true
8020cmd "run" {
8021 flag "--force"
8022}
8023"#
8024 .parse()
8025 .unwrap();
8026 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
8027 assert_eq!(out.cmd.name, "run");
8028 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8029 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
8030 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8031 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
8032 }
8033
8034 #[test]
8035 fn a_command_may_override_the_cli_wide_setting() {
8036 let spec: Spec = r#"
8038name "ex"
8039bin "ex"
8040unknown_flags "error"
8041cmd "exec" unknown_flags="value" {
8042 arg "[rest]..."
8043}
8044cmd "build" {
8045 arg "[rest]..."
8046}
8047"#
8048 .parse()
8049 .unwrap();
8050
8051 let out = parse(
8052 &spec,
8053 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
8054 )
8055 .unwrap();
8056 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8057 assert_eq!(out.args[rest].to_string(), "--wat");
8058
8059 assert!(
8060 parse(
8061 &spec,
8062 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
8063 )
8064 .is_err(),
8065 "a command that says nothing inherits the CLI's choice"
8066 );
8067 }
8068
8069 #[test]
8070 fn the_setting_survives_a_round_trip() {
8071 let spec: Spec =
8072 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
8073 .parse()
8074 .unwrap();
8075 let reparsed: Spec = spec.to_string().parse().unwrap();
8076 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
8077 assert_eq!(
8078 reparsed.cmd.subcommands["x"].unknown_flags,
8079 Some(UnknownFlags::Value)
8080 );
8081 }
8082
8083 #[test]
8084 fn test_default_subcommand() {
8085 let run_cmd = SpecCommand::builder()
8087 .name("run")
8088 .arg(SpecArg::builder().name("task").build())
8089 .build();
8090 let mut cmd = SpecCommand::builder().name("test").build();
8091 cmd.subcommands.insert("run".to_string(), run_cmd);
8092
8093 let spec = Spec {
8094 name: "test".to_string(),
8095 bin: "test".to_string(),
8096 cmd,
8097 default_subcommand: Some("run".to_string()),
8098 ..Default::default()
8099 };
8100
8101 let input = vec!["test".to_string(), "mytask".to_string()];
8103 let parsed = parse(&spec, &input).unwrap();
8104
8105 assert_eq!(parsed.cmds.len(), 2);
8107 assert_eq!(parsed.cmds[1].name, "run");
8108
8109 assert_eq!(parsed.args.len(), 1);
8111 let arg = parsed.args.keys().next().unwrap();
8112 assert_eq!(arg.name, "task");
8113 let value = parsed.args.values().next().unwrap();
8114 assert_eq!(value.to_string(), "mytask");
8115 }
8116
8117 #[test]
8118 fn default_subcommand_outranks_root_sigil_arg() {
8119 let spec: Spec = r#"
8120name "test"
8121bin "test"
8122default_subcommand "run"
8123arg "[tools]..." sigil="+"
8124cmd "run" { arg "<task>" }
8125"#
8126 .parse()
8127 .unwrap();
8128
8129 let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
8130 assert_eq!(parsed.cmd.name, "run");
8131 let value = |name| {
8132 parsed
8133 .args
8134 .iter()
8135 .find(|(arg, _)| arg.name == name)
8136 .map(|(_, value)| value.to_string())
8137 .unwrap()
8138 };
8139 assert_eq!(value("tools"), "node");
8140 assert_eq!(value("task"), "node");
8141 }
8142
8143 #[test]
8144 fn test_default_subcommand_explicit_still_works() {
8145 let run_cmd = SpecCommand::builder()
8147 .name("run")
8148 .arg(SpecArg::builder().name("task").build())
8149 .build();
8150 let other_cmd = SpecCommand::builder()
8151 .name("other")
8152 .arg(SpecArg::builder().name("other_arg").build())
8153 .build();
8154 let mut cmd = SpecCommand::builder().name("test").build();
8155 cmd.subcommands.insert("run".to_string(), run_cmd);
8156 cmd.subcommands.insert("other".to_string(), other_cmd);
8157
8158 let spec = Spec {
8159 name: "test".to_string(),
8160 bin: "test".to_string(),
8161 cmd,
8162 default_subcommand: Some("run".to_string()),
8163 ..Default::default()
8164 };
8165
8166 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
8168 let parsed = parse(&spec, &input).unwrap();
8169
8170 assert_eq!(parsed.cmds.len(), 2);
8172 assert_eq!(parsed.cmds[1].name, "other");
8173 }
8174
8175 #[test]
8176 fn test_default_subcommand_applies_only_at_the_root() {
8177 let mut config_ls = SpecCommand::builder().name("ls").build();
8183 config_ls.args.push(SpecArg::builder().name("what").build());
8184 let mut config_cmd = SpecCommand::builder().name("config").build();
8185 config_cmd.subcommands.insert("ls".to_string(), config_ls);
8186
8187 let mut root_ls = SpecCommand::builder().name("ls").build();
8190 root_ls.args.push(SpecArg::builder().name("what").build());
8191 let mut cmd = SpecCommand::builder().name("ex").build();
8192 cmd.subcommands.insert("ls".to_string(), root_ls);
8193 cmd.subcommands.insert("config".to_string(), config_cmd);
8194
8195 let spec = Spec {
8196 name: "ex".to_string(),
8197 bin: "ex".to_string(),
8198 cmd,
8199 default_subcommand: Some("ls".to_string()),
8200 ..Default::default()
8201 };
8202
8203 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
8207 assert!(
8208 parse(&spec, &input).is_err(),
8209 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
8210 );
8211
8212 let input = vec!["ex".to_string(), "zzz".to_string()];
8214 let parsed = parse(&spec, &input).expect("the root's default applies");
8215 assert_eq!(
8216 parsed
8217 .cmds
8218 .iter()
8219 .map(|c| c.name.as_str())
8220 .collect::<Vec<_>>(),
8221 ["ex", "ls"]
8222 );
8223 assert_eq!(
8224 parsed.args.values().next().map(|v| v.to_string()),
8225 Some("zzz".to_string()),
8226 "and the word binds inside the command it reached"
8227 );
8228 }
8229
8230 #[test]
8231 fn test_default_subcommand_with_nested_subcommands() {
8232 let say_cmd = SpecCommand::builder()
8236 .name("say")
8237 .arg(SpecArg::builder().name("name").build())
8238 .build();
8239 let mut run_cmd = SpecCommand::builder().name("run").build();
8240 run_cmd.subcommands.insert("say".to_string(), say_cmd);
8241
8242 let mut cmd = SpecCommand::builder().name("test").build();
8243 cmd.subcommands.insert("run".to_string(), run_cmd);
8244
8245 let spec = Spec {
8246 name: "test".to_string(),
8247 bin: "test".to_string(),
8248 cmd,
8249 default_subcommand: Some("run".to_string()),
8250 ..Default::default()
8251 };
8252
8253 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8255 let parsed = parse(&spec, &input).unwrap();
8256
8257 assert_eq!(parsed.cmds.len(), 3);
8259 assert_eq!(parsed.cmds[0].name, "test");
8260 assert_eq!(parsed.cmds[1].name, "run");
8261 assert_eq!(parsed.cmds[2].name, "say");
8262
8263 assert_eq!(parsed.args.len(), 1);
8265 let arg = parsed.args.keys().next().unwrap();
8266 assert_eq!(arg.name, "name");
8267 let value = parsed.args.values().next().unwrap();
8268 assert_eq!(value.to_string(), "hello");
8269 }
8270
8271 fn mounted_global_flag_spec() -> Spec {
8279 let task_cmd = SpecCommand::builder()
8280 .name("sample:run")
8281 .arg(
8282 SpecArg::builder()
8283 .name("profile")
8284 .choices(["alpha", "beta", "gamma"])
8285 .build(),
8286 )
8287 .build();
8288 let mut run_cmd = SpecCommand::builder()
8290 .name("run")
8291 .flag(
8292 SpecFlag::builder()
8293 .name("cd")
8294 .short('C')
8295 .long("cd")
8296 .arg(SpecArg::builder().name("dir").build())
8297 .global(false)
8298 .build(),
8299 )
8300 .build();
8301 run_cmd
8302 .subcommands
8303 .insert("sample:run".to_string(), task_cmd);
8304
8305 let mut cmd = SpecCommand::builder()
8306 .name("test")
8307 .flag(
8308 SpecFlag::builder()
8309 .name("cd")
8310 .short('C')
8311 .long("cd")
8312 .arg(SpecArg::builder().name("dir").build())
8313 .global(true)
8314 .build(),
8315 )
8316 .build();
8317 cmd.subcommands.insert("run".to_string(), run_cmd);
8318
8319 Spec {
8320 name: "test".to_string(),
8321 bin: "test".to_string(),
8322 cmd,
8323 ..Default::default()
8324 }
8325 }
8326
8327 #[test]
8328 fn test_prefix_global_flag_does_not_pollute_choices() {
8329 let spec = mounted_global_flag_spec();
8336
8337 for words in [
8340 &["test", "-C", "/tmp", "run", "sample:run"][..],
8341 &["test", "--cd=/tmp", "run", "sample:run"][..],
8343 ] {
8344 let parsed = parse_partial(&spec, &input(words)).unwrap();
8345 assert_eq!(
8346 parsed
8347 .cmds
8348 .iter()
8349 .map(|c| c.name.as_str())
8350 .collect::<Vec<_>>(),
8351 vec!["test", "run", "sample:run"],
8352 );
8353 assert!(
8355 parsed.args.is_empty(),
8356 "args should be empty, got {:?}",
8357 parsed.args
8358 );
8359
8360 let cd = parsed
8363 .available_flags
8364 .get("--cd")
8365 .expect("--cd should remain available after descending into the subcommand");
8366 assert!(cd.global, "--cd must stay global after descent");
8367 assert!(
8368 parsed.available_flags.get("-C").is_some_and(|f| f.global),
8369 "-C must stay global after descent",
8370 );
8371
8372 assert_eq!(
8376 parsed.as_env().get("usage_cd").map(String::as_str),
8377 Some("/tmp"),
8378 "global flag value must survive in as_env(), got {:?}",
8379 parsed.as_env(),
8380 );
8381 }
8382
8383 let parsed = parse_partial(
8385 &spec,
8386 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8387 )
8388 .unwrap();
8389 assert_eq!(parsed.args.len(), 1);
8390 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8391
8392 assert_parse_err(
8394 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8395 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8396 );
8397 }
8398
8399 fn mounted_orphan_short_spec() -> Spec {
8404 let task_cmd = SpecCommand::builder()
8405 .name("sample:run")
8406 .arg(
8407 SpecArg::builder()
8408 .name("profile")
8409 .choices(["alpha", "beta", "gamma"])
8410 .build(),
8411 )
8412 .build();
8413 let mut run_cmd = SpecCommand::builder()
8416 .name("run")
8417 .flag(
8418 SpecFlag::builder()
8419 .name("raw")
8420 .short('r')
8421 .long("raw")
8422 .global(false)
8423 .build(),
8424 )
8425 .flag(
8426 SpecFlag::builder()
8427 .name("force")
8428 .short('f')
8429 .long("force")
8430 .global(false)
8431 .build(),
8432 )
8433 .build();
8434 run_cmd
8435 .subcommands
8436 .insert("sample:run".to_string(), task_cmd);
8437
8438 let mut cmd = SpecCommand::builder()
8440 .name("test")
8441 .flag(
8442 SpecFlag::builder()
8443 .name("raw")
8444 .long("raw")
8445 .global(true)
8446 .build(),
8447 )
8448 .build();
8449 cmd.subcommands.insert("run".to_string(), run_cmd);
8450
8451 Spec {
8452 name: "test".to_string(),
8453 bin: "test".to_string(),
8454 cmd,
8455 ..Default::default()
8456 }
8457 }
8458
8459 #[test]
8460 fn test_orphan_short_alias_survives_merge() {
8461 let spec = mounted_orphan_short_spec();
8467
8468 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
8469 assert_eq!(
8470 parsed
8471 .cmds
8472 .iter()
8473 .map(|c| c.name.as_str())
8474 .collect::<Vec<_>>(),
8475 vec!["test", "run", "sample:run"],
8476 );
8477
8478 assert!(
8481 parsed.available_flags.get("-r").is_some_and(|f| f.global),
8482 "-r must be merged onto the inherited global flag and stay global after descent",
8483 );
8484 assert!(
8485 parsed
8486 .available_flags
8487 .get("--raw")
8488 .is_some_and(|f| f.global),
8489 "--raw must stay global after descent",
8490 );
8491
8492 assert!(
8494 parsed.args.is_empty(),
8495 "args should be empty, got {:?}",
8496 parsed.args
8497 );
8498
8499 assert_eq!(
8501 parsed.as_env().get("usage_raw").map(String::as_str),
8502 Some("true"),
8503 "merged short's value must survive in as_env(), got {:?}",
8504 parsed.as_env(),
8505 );
8506
8507 assert!(
8510 !parsed.available_flags.contains_key("-f"),
8511 "purely-local -f must not be promoted onto a global",
8512 );
8513 assert!(
8514 !parsed.available_flags.contains_key("--force"),
8515 "purely-local --force must not be promoted onto a global",
8516 );
8517
8518 let parsed =
8520 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8521 assert_eq!(parsed.args.len(), 1);
8522 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8523
8524 assert_parse_err(
8526 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8527 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8528 );
8529 }
8530
8531 #[test]
8532 fn test_orphan_short_does_not_clobber_unrelated_global() {
8533 let run_cmd = SpecCommand::builder()
8538 .name("run")
8539 .flag(
8540 SpecFlag::builder()
8541 .name("raw")
8542 .short('r')
8543 .long("raw")
8544 .global(false)
8545 .build(),
8546 )
8547 .build();
8548 let mut cmd = SpecCommand::builder()
8549 .name("test")
8550 .flag(
8551 SpecFlag::builder()
8552 .name("raw")
8553 .long("raw")
8554 .global(true)
8555 .build(),
8556 )
8557 .flag(
8558 SpecFlag::builder()
8559 .name("restrict")
8560 .short('r')
8561 .long("restrict")
8562 .global(true)
8563 .build(),
8564 )
8565 .build();
8566 cmd.subcommands.insert("run".to_string(), run_cmd);
8567 let spec = Spec {
8568 name: "test".to_string(),
8569 bin: "test".to_string(),
8570 cmd,
8571 ..Default::default()
8572 };
8573
8574 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8575 assert_eq!(
8577 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8578 Some("restrict"),
8579 "-r must remain owned by the unrelated global it already belonged to",
8580 );
8581 assert!(
8583 parsed
8584 .available_flags
8585 .get("--raw")
8586 .is_some_and(|f| f.global)
8587 );
8588 assert!(
8589 parsed
8590 .available_flags
8591 .get("--restrict")
8592 .is_some_and(|f| f.global)
8593 );
8594 }
8595
8596 #[test]
8597 fn test_redeclared_global_aliases_share_one_flag() {
8598 let spec = r#"
8605flag "-y --yes" global=#true effect="write"
8606cmd "run" {
8607 flag "-y --yes --assume-yes"
8608}
8609"#
8610 .parse::<Spec>()
8611 .unwrap();
8612
8613 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8614
8615 for key in ["-y", "--yes", "--assume-yes"] {
8616 let flag = parsed
8617 .available_flags
8618 .get(key)
8619 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8620 assert!(flag.global, "{key} must stay global after the descent");
8621 assert_eq!(
8622 flag.long,
8623 vec!["yes".to_string(), "assume-yes".to_string()],
8624 "{key} must resolve to the flag carrying every alias",
8625 );
8626 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8627 }
8628
8629 assert_eq!(
8631 unique_flags(parsed.available_flags.values()).count(),
8632 1,
8633 "all aliases must point at one flag object, got {:?}",
8634 parsed.available_flags,
8635 );
8636
8637 assert_eq!(
8639 parsed.available_flags["-y"].effect,
8640 Some(crate::SpecCommandEffect::Write),
8641 );
8642 }
8643
8644 #[test]
8645 fn test_redeclared_global_keeps_hidden_alias_metadata() {
8646 let spec = r#"
8647flag "--yes" global=#true {
8648 alias "-q" "--quietly" hide=#true
8649}
8650cmd "run" {
8651 flag "--yes --assume-yes" {
8652 alias "-s" "--secret" hide=#true
8653 }
8654}
8655"#
8656 .parse::<Spec>()
8657 .unwrap();
8658
8659 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8660 let merged = &parsed.available_flags["--yes"];
8661 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8662 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8663 for key in ["-q", "-s", "--quietly", "--secret"] {
8664 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8665 }
8666 }
8667
8668 #[test]
8669 fn test_redeclared_global_can_promote_hidden_aliases() {
8670 let spec = r#"
8671flag "--yes" global=#true {
8672 alias "-q" "--quietly" hide=#true
8673}
8674cmd "run" {
8675 flag "-q --yes --quietly"
8676}
8677"#
8678 .parse::<Spec>()
8679 .unwrap();
8680
8681 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8682 let merged = &parsed.available_flags["--yes"];
8683 assert!(merged.hidden_short_aliases.is_empty());
8684 assert!(merged.hidden_aliases.is_empty());
8685 for key in ["-q", "--quietly"] {
8686 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8687 }
8688 }
8689
8690 #[test]
8691 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8692 let spec = r#"
8698flag "-y --yes --confirm" global=#true
8699cmd "run" {
8700 flag "--yes --assume-yes"
8701}
8702"#
8703 .parse::<Spec>()
8704 .unwrap();
8705
8706 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8707
8708 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8709 let flag = parsed
8710 .available_flags
8711 .get(key)
8712 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8713 assert!(flag.global, "{key} must stay global after the descent");
8714 assert_eq!(
8715 flag.long,
8716 vec![
8717 "yes".to_string(),
8718 "confirm".to_string(),
8719 "assume-yes".to_string()
8720 ],
8721 "{key} must resolve to the flag carrying every alias",
8722 );
8723 }
8724
8725 assert_eq!(
8726 unique_flags(parsed.available_flags.values()).count(),
8727 1,
8728 "all aliases must point at one flag object, got {:?}",
8729 parsed.available_flags,
8730 );
8731 }
8732
8733 fn mounted_task_flag_spec() -> Spec {
8740 let mut task_cmd = SpecCommand::builder()
8741 .name("mytask")
8742 .flag(
8743 SpecFlag::builder()
8744 .name("env")
8745 .long("env")
8746 .arg(
8747 SpecArg::builder()
8748 .name("name")
8749 .choices(["dev", "stage", "prod"])
8750 .build(),
8751 )
8752 .global(false)
8753 .build(),
8754 )
8755 .flag(
8756 SpecFlag::builder()
8757 .name("bump")
8758 .long("bump")
8759 .arg(
8760 SpecArg::builder()
8761 .name("type")
8762 .choices(["auto", "major"])
8763 .build(),
8764 )
8765 .global(false)
8766 .build(),
8767 )
8768 .build();
8769 task_cmd.mounted = true;
8770
8771 let mut run_cmd = SpecCommand::builder()
8772 .name("run")
8773 .flag(
8774 SpecFlag::builder()
8775 .name("force")
8776 .short('f')
8777 .long("force")
8778 .global(false)
8779 .build(),
8780 )
8781 .build();
8782 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8783
8784 let mut cmd = SpecCommand::builder()
8785 .name("test")
8786 .flag(
8787 SpecFlag::builder()
8788 .name("env")
8789 .short('E')
8790 .long("env")
8791 .arg(SpecArg::builder().name("ENV").build())
8792 .global(true)
8793 .build(),
8794 )
8795 .flag(
8796 SpecFlag::builder()
8797 .name("silent")
8798 .long("silent")
8799 .global(true)
8800 .build(),
8801 )
8802 .build();
8803 cmd.subcommands.insert("run".to_string(), run_cmd);
8804
8805 Spec {
8806 name: "test".to_string(),
8807 bin: "test".to_string(),
8808 cmd,
8809 ..Default::default()
8810 }
8811 }
8812
8813 #[test]
8814 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8815 let deep = SpecCommand::builder().name("deep").build();
8821 let mut sub = SpecCommand::builder()
8822 .name("sub")
8823 .flag(
8825 SpecFlag::builder()
8826 .name("cd")
8827 .short('C')
8828 .long("cd")
8829 .arg(SpecArg::builder().name("dir").build())
8830 .global(false)
8831 .build(),
8832 )
8833 .build();
8834 sub.subcommands.insert("deep".to_string(), deep);
8835 let mut task = SpecCommand::builder()
8836 .name("task")
8837 .flag(
8838 SpecFlag::builder()
8839 .name("cd")
8840 .short('C')
8841 .long("cd")
8842 .arg(SpecArg::builder().name("dir").build())
8843 .global(true)
8844 .build(),
8845 )
8846 .build();
8847 task.subcommands.insert("sub".to_string(), sub);
8848 task.mark_mounted();
8849
8850 let mut run_cmd = SpecCommand::builder().name("run").build();
8851 run_cmd.subcommands.insert("task".to_string(), task);
8852 let mut cmd = SpecCommand::builder().name("test").build();
8853 cmd.subcommands.insert("run".to_string(), run_cmd);
8854 let spec = Spec {
8855 name: "test".to_string(),
8856 bin: "test".to_string(),
8857 cmd,
8858 ..Default::default()
8859 };
8860
8861 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
8862 assert!(
8863 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
8864 "the mounted program's own global must survive descents inside the mounted tree",
8865 );
8866 assert!(
8867 parsed.completion_flags().contains_key("--cd"),
8868 "and must still be offered there: it belongs to the mounted program",
8869 );
8870 assert!(
8871 parsed.completion_flags().contains_key("-C"),
8872 "including the short the nested command re-declared",
8873 );
8874 }
8875
8876 #[test]
8877 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
8878 let mut task = SpecCommand::builder()
8883 .name("task")
8884 .flag(
8885 SpecFlag::builder()
8886 .name("bump")
8887 .long("bump")
8888 .global(false)
8889 .build(),
8890 )
8891 .build();
8892 task.mark_mounted();
8893
8894 let mut run_cmd = SpecCommand::builder().name("run").build();
8895 run_cmd.subcommands.insert("task".to_string(), task);
8896 run_cmd.flags = vec![
8898 SpecFlag::builder()
8899 .name("tglobal")
8900 .long("tglobal")
8901 .global(true)
8902 .build(),
8903 SpecFlag::builder()
8904 .name("tlocal")
8905 .long("tlocal")
8906 .global(false)
8907 .build(),
8908 ];
8909 run_cmd.flags_from_mount = true;
8910
8911 let mut cmd = SpecCommand::builder()
8912 .name("test")
8913 .flag(
8914 SpecFlag::builder()
8915 .name("silent")
8916 .long("silent")
8917 .global(true)
8918 .build(),
8919 )
8920 .build();
8921 cmd.subcommands.insert("run".to_string(), run_cmd);
8922 let spec = Spec {
8923 name: "test".to_string(),
8924 bin: "test".to_string(),
8925 cmd,
8926 ..Default::default()
8927 };
8928
8929 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
8930 assert_eq!(
8931 parsed.completion_flags().keys().collect::<Vec<_>>(),
8932 vec!["--bump", "--tglobal"],
8933 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
8934 `--silent` does not, and the mount's non-global root flag is not inherited",
8935 );
8936 }
8937
8938 #[test]
8939 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
8940 let spec = mounted_task_flag_spec();
8945 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
8946
8947 assert!(parsed.available_flags.contains_key("--silent"));
8949 assert!(parsed.available_flags.contains_key("-E"));
8950 assert_eq!(
8952 parsed.completion_flags().keys().collect::<Vec<_>>(),
8953 vec!["--bump", "--env"],
8954 "only the mounted command's own flags may be offered",
8955 );
8956
8957 assert!(!parsed.available_flags.contains_key("--force"));
8959 }
8960
8961 #[test]
8962 fn test_mounted_cmd_flag_wins_over_inherited_global() {
8963 let spec = mounted_task_flag_spec();
8966 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
8967
8968 let awaiting = parsed
8969 .flag_awaiting_value
8970 .first()
8971 .expect("--env should await a value");
8972 assert_eq!(
8973 awaiting
8974 .arg
8975 .as_ref()
8976 .and_then(|a| a.choices.as_ref())
8977 .map(|c| c.choices.clone()),
8978 Some(vec![
8979 "dev".to_string(),
8980 "stage".to_string(),
8981 "prod".to_string()
8982 ]),
8983 "the mounted command's own --env must win over the inherited global",
8984 );
8985
8986 let parsed =
8989 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
8990 assert!(
8991 parsed.args.is_empty(),
8992 "prefix global tokens must not be consumed as positionals, got {:?}",
8993 parsed.args
8994 );
8995 assert_eq!(
8996 parsed.as_env().get("usage_env").map(String::as_str),
8997 Some("anything"),
8998 );
8999 }
9000
9001 #[test]
9002 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
9003 let spec = mounted_task_flag_spec();
9008 let parsed = parse_partial(
9009 &spec,
9010 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
9011 )
9012 .unwrap();
9013 assert!(
9014 parsed.errors.is_empty(),
9015 "prefix global value must not be validated against the mounted flag: {:?}",
9016 parsed
9017 .errors
9018 .iter()
9019 .map(|e| e.to_string())
9020 .collect::<Vec<_>>(),
9021 );
9022 assert_eq!(
9023 parsed.as_env().get("usage_env").map(String::as_str),
9024 Some("not-a-task-choice"),
9025 );
9026
9027 let parsed = parse_partial(
9029 &spec,
9030 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
9031 )
9032 .unwrap();
9033 assert!(parsed.errors.is_empty());
9034 assert_eq!(
9035 parsed.as_env().get("usage_env").map(String::as_str),
9036 Some("not-a-task-choice"),
9037 );
9038
9039 let parsed = parse_partial(
9042 &spec,
9043 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
9044 )
9045 .unwrap();
9046 let awaiting = parsed
9047 .flag_awaiting_value
9048 .first()
9049 .expect("--env should await a value");
9050 assert_eq!(
9051 awaiting
9052 .arg
9053 .as_ref()
9054 .and_then(|a| a.choices.as_ref())
9055 .map(|c| c.choices.clone()),
9056 Some(vec![
9057 "dev".to_string(),
9058 "stage".to_string(),
9059 "prod".to_string()
9060 ]),
9061 "the mounted command's --env must own the name after the mounted command",
9062 );
9063 }
9064
9065 #[test]
9066 fn test_non_global_flag_does_not_hide_subcommand() {
9067 let spec = mounted_task_flag_spec();
9071
9072 for words in [
9073 &["test", "run", "--force", "mytask"][..],
9075 &["test", "run", "-f", "mytask"][..],
9076 &["test", "-E", "prod", "run", "--force", "mytask"][..],
9078 ] {
9079 let parsed = parse_partial(&spec, &input(words)).unwrap();
9080 assert_eq!(
9081 parsed
9082 .cmds
9083 .iter()
9084 .map(|c| c.name.as_str())
9085 .collect::<Vec<_>>(),
9086 vec!["test", "run", "mytask"],
9087 "{words:?} should descend into the mounted command",
9088 );
9089 assert!(
9090 parsed.args.is_empty(),
9091 "{words:?} should not consume a positional, got {:?}",
9092 parsed.args,
9093 );
9094 assert_eq!(
9095 parsed.as_env().get("usage_force").map(String::as_str),
9096 Some("true"),
9097 "the non-global flag must still be recorded for {words:?}",
9098 );
9099 }
9100
9101 let mut run_cmd = SpecCommand::builder()
9104 .name("run")
9105 .flag(
9106 SpecFlag::builder()
9107 .name("output")
9108 .short('o')
9109 .long("output")
9110 .arg(SpecArg::builder().name("mode").build())
9111 .global(false)
9112 .build(),
9113 )
9114 .build();
9115 run_cmd.subcommands.insert(
9116 "task".to_string(),
9117 SpecCommand::builder().name("task").build(),
9118 );
9119 let mut cmd = SpecCommand::builder().name("test").build();
9120 cmd.subcommands.insert("run".to_string(), run_cmd);
9121 let spec = Spec {
9122 name: "test".to_string(),
9123 bin: "test".to_string(),
9124 cmd,
9125 ..Default::default()
9126 };
9127
9128 let parsed =
9129 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
9130 assert_eq!(
9131 parsed
9132 .cmds
9133 .iter()
9134 .map(|c| c.name.as_str())
9135 .collect::<Vec<_>>(),
9136 vec!["test", "run", "task"],
9137 );
9138 assert_eq!(
9139 parsed.as_env().get("usage_output").map(String::as_str),
9140 Some("quiet"),
9141 );
9142
9143 assert_parse_err(
9146 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
9147 "unexpected word: --nope",
9148 );
9149 }
9150
9151 #[test]
9152 fn test_non_mounted_subcommand_offers_inherited_globals() {
9153 let mut run_cmd = SpecCommand::builder().name("run").build();
9156 run_cmd.subcommands.insert(
9157 "nested".to_string(),
9158 SpecCommand::builder().name("nested").build(),
9159 );
9160 let mut cmd = SpecCommand::builder()
9161 .name("test")
9162 .flag(
9163 SpecFlag::builder()
9164 .name("silent")
9165 .long("silent")
9166 .global(true)
9167 .build(),
9168 )
9169 .build();
9170 cmd.subcommands.insert("run".to_string(), run_cmd);
9171 let spec = Spec {
9172 name: "test".to_string(),
9173 bin: "test".to_string(),
9174 cmd,
9175 ..Default::default()
9176 };
9177
9178 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
9179 assert_eq!(
9180 parsed.completion_flags().keys().collect::<Vec<_>>(),
9181 parsed.available_flags.keys().collect::<Vec<_>>(),
9182 );
9183 assert!(parsed.completion_flags().contains_key("--silent"));
9184 }
9185
9186 #[test]
9187 fn test_subcommand_alias_collision_keeps_last_owner() {
9188 let run_cmd = SpecCommand::builder()
9192 .name("run")
9193 .flag(
9194 SpecFlag::builder()
9195 .name("alpha")
9196 .short('x')
9197 .long("alpha")
9198 .global(false)
9199 .build(),
9200 )
9201 .flag(
9202 SpecFlag::builder()
9203 .name("beta")
9204 .short('x')
9205 .long("beta")
9206 .global(false)
9207 .build(),
9208 )
9209 .build();
9210 let mut cmd = SpecCommand::builder().name("test").build();
9211 cmd.subcommands.insert("run".to_string(), run_cmd);
9212 let spec = Spec {
9213 name: "test".to_string(),
9214 bin: "test".to_string(),
9215 cmd,
9216 ..Default::default()
9217 };
9218
9219 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
9220 assert_eq!(
9222 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
9223 Some("beta"),
9224 "the last-declared flag must keep a shared short alias",
9225 );
9226 assert_eq!(
9228 parsed
9229 .available_flags
9230 .get("--alpha")
9231 .map(|f| f.name.as_str()),
9232 Some("alpha"),
9233 );
9234 assert_eq!(
9235 parsed
9236 .available_flags
9237 .get("--beta")
9238 .map(|f| f.name.as_str()),
9239 Some("beta"),
9240 );
9241 }
9242
9243 #[test]
9244 fn test_default_subcommand_same_name_child() {
9245 let run_task = SpecCommand::builder()
9249 .name("run")
9250 .arg(SpecArg::builder().name("args").build())
9251 .build();
9252 let mut run_cmd = SpecCommand::builder().name("run").build();
9253 run_cmd.subcommands.insert("run".to_string(), run_task);
9254
9255 let mut cmd = SpecCommand::builder().name("test").build();
9256 cmd.subcommands.insert("run".to_string(), run_cmd);
9257
9258 let spec = Spec {
9259 name: "test".to_string(),
9260 bin: "test".to_string(),
9261 cmd,
9262 default_subcommand: Some("run".to_string()),
9263 ..Default::default()
9264 };
9265
9266 let input = vec!["test".to_string(), "run".to_string()];
9268 let parsed = parse(&spec, &input).unwrap();
9269
9270 assert_eq!(parsed.cmds.len(), 2);
9272 assert_eq!(parsed.cmds[0].name, "test");
9273 assert_eq!(parsed.cmds[1].name, "run");
9274
9275 let input = vec![
9277 "test".to_string(),
9278 "run".to_string(),
9279 "run".to_string(),
9280 "hello".to_string(),
9281 ];
9282 let parsed = parse(&spec, &input).unwrap();
9283
9284 assert_eq!(parsed.cmds.len(), 3);
9285 assert_eq!(parsed.cmds[0].name, "test");
9286 assert_eq!(parsed.cmds[1].name, "run");
9287 assert_eq!(parsed.cmds[2].name, "run");
9288 assert_eq!(parsed.args.len(), 1);
9289 let value = parsed.args.values().next().unwrap();
9290 assert_eq!(value.to_string(), "hello");
9291
9292 let mut run_cmd = SpecCommand::builder()
9296 .name("run")
9297 .arg(SpecArg::builder().name("task").build())
9298 .build();
9299 let run_task = SpecCommand::builder().name("run").build();
9300 run_cmd.subcommands.insert("run".to_string(), run_task);
9301
9302 let mut cmd = SpecCommand::builder().name("test").build();
9303 cmd.subcommands.insert("run".to_string(), run_cmd);
9304
9305 let spec = Spec {
9306 name: "test".to_string(),
9307 bin: "test".to_string(),
9308 cmd,
9309 default_subcommand: Some("run".to_string()),
9310 ..Default::default()
9311 };
9312
9313 let input = vec!["test".to_string(), "other".to_string()];
9314 let parsed = parse(&spec, &input).unwrap();
9315
9316 assert_eq!(parsed.cmds.len(), 2);
9319 assert_eq!(parsed.cmds[0].name, "test");
9320 assert_eq!(parsed.cmds[1].name, "run");
9321
9322 assert_eq!(parsed.args.len(), 1);
9324 let value = parsed.args.values().next().unwrap();
9325 assert_eq!(value.to_string(), "other");
9326 }
9327
9328 #[test]
9329 fn test_restart_token() {
9330 let run_cmd = SpecCommand::builder()
9332 .name("run")
9333 .arg(SpecArg::builder().name("task").build())
9334 .restart_token(":::".to_string())
9335 .build();
9336 let mut cmd = SpecCommand::builder().name("test").build();
9337 cmd.subcommands.insert("run".to_string(), run_cmd);
9338
9339 let spec = Spec {
9340 name: "test".to_string(),
9341 bin: "test".to_string(),
9342 cmd,
9343 ..Default::default()
9344 };
9345
9346 let input = vec![
9348 "test".to_string(),
9349 "run".to_string(),
9350 "task1".to_string(),
9351 ":::".to_string(),
9352 "task2".to_string(),
9353 ];
9354 let parsed = parse(&spec, &input).unwrap();
9355
9356 assert_eq!(parsed.args.len(), 1);
9358 let value = parsed.args.values().next().unwrap();
9359 assert_eq!(value.to_string(), "task2");
9360 }
9361
9362 #[test]
9363 fn test_restart_token_multiple() {
9364 let run_cmd = SpecCommand::builder()
9366 .name("run")
9367 .arg(SpecArg::builder().name("task").build())
9368 .restart_token(":::".to_string())
9369 .build();
9370 let mut cmd = SpecCommand::builder().name("test").build();
9371 cmd.subcommands.insert("run".to_string(), run_cmd);
9372
9373 let spec = Spec {
9374 name: "test".to_string(),
9375 bin: "test".to_string(),
9376 cmd,
9377 ..Default::default()
9378 };
9379
9380 let input = vec![
9382 "test".to_string(),
9383 "run".to_string(),
9384 "task1".to_string(),
9385 ":::".to_string(),
9386 "task2".to_string(),
9387 ":::".to_string(),
9388 "task3".to_string(),
9389 ];
9390 let parsed = parse(&spec, &input).unwrap();
9391
9392 assert_eq!(parsed.args.len(), 1);
9394 let value = parsed.args.values().next().unwrap();
9395 assert_eq!(value.to_string(), "task3");
9396 }
9397
9398 #[test]
9399 fn test_restart_token_clears_flag_awaiting_value() {
9400 let run_cmd = SpecCommand::builder()
9402 .name("run")
9403 .arg(SpecArg::builder().name("task").build())
9404 .flag(
9405 SpecFlag::builder()
9406 .name("jobs")
9407 .long("jobs")
9408 .arg(SpecArg::builder().name("count").build())
9409 .build(),
9410 )
9411 .restart_token(":::".to_string())
9412 .build();
9413 let mut cmd = SpecCommand::builder().name("test").build();
9414 cmd.subcommands.insert("run".to_string(), run_cmd);
9415
9416 let spec = Spec {
9417 name: "test".to_string(),
9418 bin: "test".to_string(),
9419 cmd,
9420 ..Default::default()
9421 };
9422
9423 let input = vec![
9425 "test".to_string(),
9426 "run".to_string(),
9427 "task1".to_string(),
9428 "--jobs".to_string(),
9429 ":::".to_string(),
9430 "task2".to_string(),
9431 ];
9432 let parsed = parse(&spec, &input).unwrap();
9433
9434 assert_eq!(parsed.args.len(), 1);
9436 let value = parsed.args.values().next().unwrap();
9437 assert_eq!(value.to_string(), "task2");
9438 assert!(parsed.flag_awaiting_value.is_empty());
9440 }
9441
9442 #[test]
9443 fn test_restart_token_resets_double_dash() {
9444 let run_cmd = SpecCommand::builder()
9446 .name("run")
9447 .arg(SpecArg::builder().name("task").build())
9448 .arg(SpecArg::builder().name("extra_args").var(true).build())
9449 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9450 .restart_token(":::".to_string())
9451 .build();
9452 let mut cmd = SpecCommand::builder().name("test").build();
9453 cmd.subcommands.insert("run".to_string(), run_cmd);
9454
9455 let spec = Spec {
9456 name: "test".to_string(),
9457 bin: "test".to_string(),
9458 cmd,
9459 ..Default::default()
9460 };
9461
9462 let input = vec![
9464 "test".to_string(),
9465 "run".to_string(),
9466 "task1".to_string(),
9467 "--".to_string(),
9468 "extra".to_string(),
9469 ":::".to_string(),
9470 "--verbose".to_string(),
9471 "task2".to_string(),
9472 ];
9473 let parsed = parse(&spec, &input).unwrap();
9474
9475 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9477 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9479 let value = parsed.args.get(task_arg).unwrap();
9480 assert_eq!(value.to_string(), "task2");
9481 }
9482
9483 #[test]
9484 fn test_double_dashes_without_preserve() {
9485 let run_cmd = SpecCommand::builder()
9490 .name("run")
9491 .arg(SpecArg::builder().name("args").var(true).build())
9492 .build();
9493 let mut cmd = SpecCommand::builder().name("test").build();
9494 cmd.subcommands.insert("run".to_string(), run_cmd);
9495
9496 let spec = Spec {
9497 name: "test".to_string(),
9498 bin: "test".to_string(),
9499 cmd,
9500 ..Default::default()
9501 };
9502
9503 let input = vec![
9505 "test".to_string(),
9506 "run".to_string(),
9507 "arg1".to_string(),
9508 "--".to_string(),
9509 "arg2".to_string(),
9510 "--".to_string(),
9511 "arg3".to_string(),
9512 ];
9513 let parsed = parse(&spec, &input).unwrap();
9514
9515 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9516 let value = parsed.args.get(args_arg).unwrap();
9517 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9518 }
9519
9520 #[test]
9521 fn test_double_dashes_with_preserve() {
9522 let run_cmd = SpecCommand::builder()
9524 .name("run")
9525 .arg(
9526 SpecArg::builder()
9527 .name("args")
9528 .var(true)
9529 .double_dash(SpecDoubleDashChoices::Preserve)
9530 .build(),
9531 )
9532 .build();
9533 let mut cmd = SpecCommand::builder().name("test").build();
9534 cmd.subcommands.insert("run".to_string(), run_cmd);
9535
9536 let spec = Spec {
9537 name: "test".to_string(),
9538 bin: "test".to_string(),
9539 cmd,
9540 ..Default::default()
9541 };
9542
9543 let input = vec![
9545 "test".to_string(),
9546 "run".to_string(),
9547 "arg1".to_string(),
9548 "--".to_string(),
9549 "arg2".to_string(),
9550 "--".to_string(),
9551 "arg3".to_string(),
9552 ];
9553 let parsed = parse(&spec, &input).unwrap();
9554
9555 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9556 let value = parsed.args.get(args_arg).unwrap();
9557 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9558 }
9559
9560 #[test]
9561 fn test_double_dashes_with_preserve_only_dashes() {
9562 let run_cmd = SpecCommand::builder()
9565 .name("run")
9566 .arg(
9567 SpecArg::builder()
9568 .name("args")
9569 .var(true)
9570 .double_dash(SpecDoubleDashChoices::Preserve)
9571 .build(),
9572 )
9573 .build();
9574 let mut cmd = SpecCommand::builder().name("test").build();
9575 cmd.subcommands.insert("run".to_string(), run_cmd);
9576
9577 let spec = Spec {
9578 name: "test".to_string(),
9579 bin: "test".to_string(),
9580 cmd,
9581 ..Default::default()
9582 };
9583
9584 let input = vec![
9586 "test".to_string(),
9587 "run".to_string(),
9588 "--".to_string(),
9589 "--".to_string(),
9590 ];
9591 let parsed = parse(&spec, &input).unwrap();
9592
9593 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9594 let value = parsed.args.get(args_arg).unwrap();
9595 assert_eq!(value.to_string(), "-- --");
9596 }
9597
9598 #[test]
9599 fn test_double_dashes_with_preserve_multiple_args() {
9600 let run_cmd = SpecCommand::builder()
9602 .name("run")
9603 .arg(SpecArg::builder().name("task").build())
9604 .arg(
9605 SpecArg::builder()
9606 .name("extra_args")
9607 .var(true)
9608 .double_dash(SpecDoubleDashChoices::Preserve)
9609 .build(),
9610 )
9611 .build();
9612 let mut cmd = SpecCommand::builder().name("test").build();
9613 cmd.subcommands.insert("run".to_string(), run_cmd);
9614
9615 let spec = Spec {
9616 name: "test".to_string(),
9617 bin: "test".to_string(),
9618 cmd,
9619 ..Default::default()
9620 };
9621
9622 let input = vec![
9625 "test".to_string(),
9626 "run".to_string(),
9627 "task1".to_string(),
9628 "--".to_string(),
9629 "arg1".to_string(),
9630 "--".to_string(),
9631 "--foo".to_string(),
9632 ];
9633 let parsed = parse(&spec, &input).unwrap();
9634
9635 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9636 let task_value = parsed.args.get(task_arg).unwrap();
9637 assert_eq!(task_value.to_string(), "task1");
9638
9639 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9640 let extra_value = parsed.args.get(extra_arg).unwrap();
9641 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9642 }
9643
9644 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9645 let cmd = SpecCommand::builder().name("test").args(args).build();
9646 Spec {
9647 name: "test".to_string(),
9648 bin: "test".to_string(),
9649 cmd,
9650 ..Default::default()
9651 }
9652 }
9653
9654 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9655 let arg = parsed
9656 .args
9657 .keys()
9658 .find(|a| a.name == name)
9659 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9660 parsed.args.get(arg).unwrap().to_string()
9661 }
9662
9663 fn required_arg(name: &str) -> SpecArg {
9664 SpecArg::builder()
9665 .name(name)
9666 .var(true)
9667 .required(false)
9668 .double_dash(SpecDoubleDashChoices::Required)
9669 .build()
9670 }
9671
9672 #[test]
9673 fn test_double_dash_required_reports_error_once_for_variadic() {
9674 let spec = spec_with_args([required_arg("files")]);
9676
9677 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9678
9679 assert!(parsed.args.is_empty());
9680 assert_eq!(parsed.errors.len(), 1);
9681 assert!(
9682 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9683 );
9684 }
9685
9686 #[test]
9687 fn test_double_dash_required_suppresses_missing_arg() {
9688 let spec = spec_with_args([SpecArg::builder()
9690 .name("file")
9691 .required(true)
9692 .double_dash(SpecDoubleDashChoices::Required)
9693 .build()]);
9694
9695 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9696
9697 assert_eq!(parsed.errors.len(), 1);
9698 assert!(matches!(
9699 &parsed.errors[0],
9700 UsageErr::ArgRequiresDoubleDash(_)
9701 ));
9702 assert_eq!(
9704 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9705 Some("file")
9706 );
9707 assert!(!parsed.double_dash_seen);
9708 }
9709
9710 #[test]
9711 fn test_double_dash_routes_to_required_arg() {
9712 let spec = spec_with_args([
9715 SpecArg::builder()
9716 .name("tool")
9717 .var(true)
9718 .required(false)
9719 .build(),
9720 required_arg("command"),
9721 ]);
9722
9723 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9724
9725 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9726 assert_eq!(arg_value(&parsed, "command"), "node app.js");
9727 assert!(parsed.double_dash_seen);
9728 }
9729
9730 #[test]
9731 fn test_double_dash_routes_with_gap_reports_missing_arg() {
9732 let spec = spec_with_args([
9735 SpecArg::builder()
9736 .name("tool")
9737 .var(true)
9738 .required(true)
9739 .build(),
9740 required_arg("command"),
9741 ]);
9742
9743 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9744
9745 assert_eq!(arg_value(&parsed, "command"), "ls");
9746 assert!(parsed.args.keys().all(|a| a.name != "tool"));
9747 assert!(
9748 parsed
9749 .errors
9750 .iter()
9751 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool"))
9752 );
9753 }
9754
9755 #[test]
9756 fn test_double_dash_gap_applies_defaults() {
9757 let spec = spec_with_args([
9759 SpecArg::builder()
9760 .name("tool")
9761 .var(true)
9762 .required(false)
9763 .default_value("node@20")
9764 .build(),
9765 required_arg("command"),
9766 ]);
9767
9768 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9769
9770 assert_eq!(arg_value(&parsed, "command"), "ls");
9771 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9772 }
9773
9774 fn spec_with_restart_token_and_required_arg() -> Spec {
9775 let run_cmd = SpecCommand::builder()
9776 .name("run")
9777 .arg(SpecArg::builder().name("task").build())
9778 .arg(required_arg("run_args"))
9779 .restart_token(":::".to_string())
9780 .build();
9781 let mut cmd = SpecCommand::builder().name("test").build();
9782 cmd.subcommands.insert("run".to_string(), run_cmd);
9783 Spec {
9784 name: "test".to_string(),
9785 bin: "test".to_string(),
9786 cmd,
9787 ..Default::default()
9788 }
9789 }
9790
9791 #[test]
9792 fn test_double_dash_required_restart_token_resets_separator() {
9793 let spec = spec_with_restart_token_and_required_arg();
9795
9796 let parsed = parse_partial(
9797 &spec,
9798 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9799 )
9800 .unwrap();
9801
9802 assert_eq!(arg_value(&parsed, "task"), "task2");
9803 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9804 assert_eq!(
9806 parsed
9807 .errors
9808 .iter()
9809 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9810 .count(),
9811 1
9812 );
9813 }
9814
9815 #[test]
9816 fn test_double_dash_required_restart_token_accepts_new_separator() {
9817 let spec = spec_with_restart_token_and_required_arg();
9818
9819 let parsed = parse(
9820 &spec,
9821 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9822 )
9823 .unwrap();
9824
9825 assert_eq!(arg_value(&parsed, "task"), "task2");
9826 assert_eq!(arg_value(&parsed, "run_args"), "c");
9827 }
9828
9829 #[test]
9830 fn test_double_dash_preserve_is_not_a_separator() {
9831 let spec = spec_with_args([
9834 SpecArg::builder()
9835 .name("kept")
9836 .var(true)
9837 .var_max(1)
9838 .required(false)
9839 .double_dash(SpecDoubleDashChoices::Preserve)
9840 .build(),
9841 required_arg("rest"),
9842 ]);
9843
9844 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
9845
9846 assert_eq!(arg_value(&parsed, "kept"), "--");
9847 assert!(parsed.args.keys().all(|a| a.name != "rest"));
9848 assert!(!parsed.double_dash_seen);
9849 assert_eq!(parsed.errors.len(), 1);
9850 }
9851
9852 #[test]
9853 fn test_double_dash_required_does_not_bail_in_parse_partial() {
9854 let spec = spec_with_args([required_arg("file")]);
9856
9857 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
9858 assert!(parse(&spec, &input(&["test", "x"])).is_err());
9859 }
9860
9861 #[test]
9862 fn test_double_dash_without_required_arg_does_not_move_cursor() {
9863 let spec = spec_with_args([
9865 SpecArg::builder().name("first").required(false).build(),
9866 SpecArg::builder().name("second").required(false).build(),
9867 ]);
9868
9869 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
9870
9871 assert_eq!(arg_value(&parsed, "first"), "a");
9872 assert_eq!(arg_value(&parsed, "second"), "b");
9873 assert!(parsed.next_arg.is_none());
9874 }
9875
9876 #[test]
9877 fn test_parser_with_custom_env_for_required_arg() {
9878 let spec = spec_with_arg(
9879 SpecArg::builder()
9880 .name("name")
9881 .env("NAME")
9882 .required(true)
9883 .build(),
9884 );
9885 unsafe { std::env::remove_var("NAME") };
9887
9888 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
9889 .expect("parse should succeed with custom env");
9890 assert_eq!(parsed.args.len(), 1);
9891 assert_eq!(first_string_value(&parsed), "john");
9892 }
9893
9894 #[test]
9895 fn test_parser_with_custom_env_for_required_flag() {
9896 let spec = spec_with_flag(
9897 SpecFlag::builder()
9898 .long("name")
9899 .env("NAME")
9900 .required(true)
9901 .arg(SpecArg::builder().name("name").build())
9902 .build(),
9903 );
9904 unsafe { std::env::remove_var("NAME") };
9906
9907 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
9908 .expect("parse should succeed with custom env");
9909 assert_eq!(parsed.flags.len(), 1);
9910 assert_eq!(first_string_value(&parsed), "jane");
9911 }
9912
9913 #[test]
9914 fn test_flag_environment_fallbacks_preserve_declaration_order() {
9915 let spec = spec_with_flag(
9916 SpecFlag::builder()
9917 .long("name")
9918 .env("NAME")
9919 .env_fallback("OLD_NAME")
9920 .env_fallback("OLDER_NAME")
9921 .deprecated_env("DEPRECATED_NAME")
9922 .arg(SpecArg::builder().name("name").build())
9923 .build(),
9924 );
9925
9926 let parsed = parse_with_env(
9927 &spec,
9928 &["test"],
9929 &[
9930 ("NAME", "canonical"),
9931 ("OLD_NAME", "fallback"),
9932 ("DEPRECATED_NAME", "deprecated"),
9933 ],
9934 )
9935 .unwrap();
9936 assert_eq!(first_string_value(&parsed), "canonical");
9937
9938 let parsed = parse_with_env(
9939 &spec,
9940 &["test"],
9941 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
9942 )
9943 .unwrap();
9944 assert_eq!(first_string_value(&parsed), "old");
9945
9946 let parsed =
9947 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9948 assert_eq!(first_string_value(&parsed), "deprecated");
9949 }
9950
9951 #[test]
9952 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
9953 let spec = spec_with_flag(
9954 SpecFlag::builder()
9955 .long("name")
9956 .env("NAME")
9957 .deprecated_env("DEPRECATED_NAME")
9958 .arg(SpecArg::builder().name("name").build())
9959 .build(),
9960 );
9961
9962 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
9964 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9965
9966 let parsed =
9967 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
9968 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9969 assert_eq!(
9970 parsed.warnings[0].kind,
9971 crate::warn::WarningKind::DeprecatedEnv
9972 );
9973 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
9974 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
9975 assert_eq!(first_string_value(&parsed), "deprecated");
9977 }
9978
9979 #[test]
9980 fn a_deprecated_flag_reports_only_when_it_was_used() {
9981 let spec = spec_with_flag(
9982 SpecFlag::builder()
9983 .long("output")
9984 .deprecated("use --out")
9985 .deprecated_remove_at("3.0.0")
9986 .arg(SpecArg::builder().name("output").build())
9987 .build(),
9988 );
9989
9990 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
9991 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
9992
9993 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
9994 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
9995 assert_eq!(
9996 parsed.warnings[0].kind,
9997 crate::warn::WarningKind::DeprecatedFlag
9998 );
9999 assert_eq!(parsed.warnings[0].name, "--output");
10001 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
10002 assert_eq!(
10003 parsed.warnings[0].render(),
10004 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
10005 );
10006 }
10007
10008 #[test]
10009 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
10010 let flag = SpecFlag::builder()
10011 .long("output")
10012 .deprecated("use --out")
10013 .deprecated_warn_at("9.0.0")
10014 .arg(SpecArg::builder().name("output").build())
10015 .build();
10016 let mut spec = spec_with_flag(flag);
10017 spec.version = Some("2.0.0".to_string());
10018
10019 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10020 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10021
10022 spec.version = Some("9.0.0".to_string());
10024 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10025 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10026 }
10027
10028 #[test]
10029 fn test_parser_with_custom_env_still_fails_when_missing() {
10030 let spec = spec_with_arg(
10031 SpecArg::builder()
10032 .name("name")
10033 .env("NAME")
10034 .required(true)
10035 .build(),
10036 );
10037 unsafe { std::env::remove_var("NAME") };
10039 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
10040 }
10041
10042 #[test]
10043 fn test_parser_does_not_treat_env_choice_value_as_help() {
10044 let spec = spec_with_arg(
10045 SpecArg::builder()
10046 .name("env")
10047 .env("CURRENT_ENV")
10048 .choices(["dev", "staging"])
10049 .required(false)
10050 .build(),
10051 );
10052
10053 assert_parse_err(
10054 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
10055 "Invalid choice for arg env: --help, expected one of dev, staging",
10056 );
10057 }
10058
10059 #[test]
10060 fn test_parser_does_not_treat_default_choice_value_as_help() {
10061 let spec = spec_with_flag(
10062 SpecFlag::builder()
10063 .long("env")
10064 .arg(
10065 SpecArg::builder()
10066 .name("env")
10067 .choices(["dev", "staging"])
10068 .build(),
10069 )
10070 .default_value("--help")
10071 .build(),
10072 );
10073
10074 assert_parse_err(
10075 parse_with_env(&spec, &["test"], &[]),
10076 "Invalid choice for option env: --help, expected one of dev, staging",
10077 );
10078 }
10079
10080 fn words(of: &[&str]) -> Vec<String> {
10082 of.iter().map(|s| s.to_string()).collect()
10083 }
10084
10085 #[test]
10086 fn a_command_that_needs_a_subcommand_says_so() {
10087 let spec: Spec = r#"
10091name "ex"
10092bin "ex"
10093cmd "gen" subcommand_required=#true {
10094 cmd "two" {}
10095 cmd "one" {}
10096 cmd "secret" hide=#true {}
10097 alias "g"
10098}
10099cmd "open" {
10100 cmd "sub" {}
10101}
10102"#
10103 .parse()
10104 .unwrap();
10105
10106 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
10107 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
10111
10112 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
10114 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
10115
10116 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
10118
10119 parse(&spec, &words(&["ex", "open"])).unwrap();
10122 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
10123 }
10124
10125 #[test]
10126 fn arg_required_else_help_observes_the_selected_commands_argv() {
10127 let spec: Spec = r#"
10128name "ex"
10129bin "ex"
10130flag "--verbose" global=#true
10131cmd "run" arg_required_else_help=#true {
10132 flag "--all"
10133}
10134"#
10135 .parse()
10136 .unwrap();
10137 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10138
10139 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
10140 assert!(err.to_string().contains("Usage: ex run"), "{err}");
10141
10142 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
10145 assert!(err.to_string().contains("Usage: ex run"), "{err}");
10146
10147 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
10148 }
10149
10150 #[test]
10151 fn empty_default_with_parent_flags_observes_child_argv() {
10152 let spec: Spec = r#"
10153name "ex"
10154bin "ex"
10155default_subcommand "run"
10156default_subcommand_on_empty #true
10157flag "--verbose" global=#true
10158cmd "run" arg_required_else_help=#true {}
10159"#
10160 .parse()
10161 .unwrap();
10162 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10163 let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
10164 assert!(err.to_string().contains("Usage: ex run"), "{err}");
10165 }
10166
10167 #[test]
10168 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
10169 let spec: Spec = r#"
10170name "ex"
10171bin "ex"
10172unknown_flags "error"
10173external_subcommand #true
10174cmd "install"
10175flag "-v --verbose" global=#true
10176"#
10177 .parse()
10178 .unwrap();
10179
10180 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
10181 assert_eq!(
10182 parsed.external,
10183 Some(vec!["foo".into(), "--help".into(), "bar".into()])
10184 );
10185 assert!(parsed.flags.is_empty());
10186
10187 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
10189 assert_eq!(parsed.cmd.name, "install");
10190 assert!(parsed.external.is_none());
10191
10192 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
10194 assert_eq!(
10195 parsed.external,
10196 Some(vec!["foo".into(), "--verbose".into()])
10197 );
10198 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
10199
10200 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
10202
10203 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
10207 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
10208 }
10209
10210 #[test]
10211 fn an_external_subcommand_satisfies_subcommand_required() {
10212 let mut spec: Spec = r#"
10213name "ex"
10214bin "ex"
10215external_subcommand #true
10216cmd "install"
10217"#
10218 .parse()
10219 .unwrap();
10220 spec.cmd.subcommand_required = true;
10221
10222 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
10223 assert!(parse(&spec, &input(&["ex"])).is_err());
10224 }
10225
10226 #[test]
10227 fn a_default_subcommand_outranks_an_external_one() {
10228 let spec: Spec = r#"
10229name "ex"
10230bin "ex"
10231default_subcommand "run"
10232external_subcommand #true
10233cmd "run" {
10234 arg "[task]"
10235}
10236"#
10237 .parse()
10238 .unwrap();
10239
10240 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10241 assert_eq!(parsed.cmd.name, "run");
10242 assert!(parsed.external.is_none());
10243 assert_eq!(first_string_value(&parsed), "build");
10244 }
10245
10246 #[test]
10247 fn multicall_basename_strips_a_path_and_exe() {
10248 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10249 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10250 assert_eq!(multicall_basename("LS.EXE"), "LS");
10251 assert_eq!(multicall_basename("busybox"), "busybox");
10252 }
10253
10254 #[test]
10255 fn a_multicall_applet_is_the_first_word() {
10256 let spec: Spec = r#"
10257name "busybox"
10258bin "busybox"
10259multicall #true
10260cmd "ls" {
10261 arg "[ARGS]" var=#true
10262}
10263cmd "cat"
10264"#
10265 .parse()
10266 .unwrap();
10267
10268 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10270 assert_eq!(parsed.cmd.name, "ls");
10271 match parsed.args.values().next() {
10272 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10273 other => panic!("expected ARGS to collect -l, got {other:?}"),
10274 }
10275
10276 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10278 assert_eq!(parsed.cmd.name, "ls");
10279
10280 let mut configured = spec.clone();
10282 configured.name = "BusyBox".to_string();
10283 configured.bin = "/opt/bin/busybox.exe".to_string();
10284 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10285 assert_eq!(parsed.cmd.name, "ls");
10286
10287 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10289 assert_eq!(parsed.cmd.name, "ls");
10290
10291 let mut plain = spec.clone();
10293 plain.multicall = false;
10294 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10295 assert_eq!(parsed.cmd.name, "ls");
10296 }
10297
10298 #[test]
10299 fn a_multicall_unknown_applet_can_be_external() {
10300 let spec: Spec = r#"
10301name "busybox"
10302bin "busybox"
10303multicall #true
10304unknown_flags "error"
10305external_subcommand #true
10306cmd "ls"
10307"#
10308 .parse()
10309 .unwrap();
10310
10311 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10312 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10313
10314 let mut closed = spec.clone();
10315 closed.cmd.external_subcommand = false;
10316 assert!(parse(&closed, &input(&["wat"])).is_err());
10317 }
10318
10319 #[cfg(feature = "unstable_choices_env")]
10320 #[test]
10321 fn test_parser_arg_choices_from_custom_env() {
10322 let spec = spec_arg_choices_env("DEPLOY_ENVS");
10323
10324 let parsed =
10325 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10326 assert_eq!(first_string_value(&parsed), "bar");
10327
10328 assert_parse_err(
10329 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10330 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10331 );
10332 assert_parse_err(
10333 parse_with_env(&spec, &["test", "prod"], &[]),
10334 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10335 );
10336 }
10337
10338 #[cfg(feature = "unstable_choices_env")]
10339 #[test]
10340 fn test_parser_validates_flag_choices_from_custom_env() {
10341 let spec = spec_flag_choices_env("DEPLOY_ENVS");
10342 let parsed = parse_with_env(
10343 &spec,
10344 &["test", "--env", "baz"],
10345 &[("DEPLOY_ENVS", "foo,bar baz")],
10346 )
10347 .unwrap();
10348 assert_eq!(first_string_value(&parsed), "baz");
10349 }
10350
10351 #[cfg(feature = "unstable_choices_env")]
10352 #[test]
10353 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10354 let arg_env_spec = spec_with_arg(
10355 SpecArg::builder()
10356 .name("env")
10357 .env("CURRENT_ENV")
10358 .choices_env("DEPLOY_ENVS")
10359 .build(),
10360 );
10361 assert_parse_err(
10362 parse_with_env(
10363 &arg_env_spec,
10364 &["test"],
10365 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10366 ),
10367 "Invalid choice for arg env: prod, expected one of dev, staging",
10368 );
10369
10370 let flag_default_spec = spec_with_flag(
10371 SpecFlag::builder()
10372 .long("env")
10373 .arg(
10374 SpecArg::builder()
10375 .name("env")
10376 .choices_env("DEPLOY_ENVS")
10377 .build(),
10378 )
10379 .default_value("prod")
10380 .build(),
10381 );
10382 assert_parse_err(
10383 parse_with_env(
10384 &flag_default_spec,
10385 &["test"],
10386 &[("DEPLOY_ENVS", "dev,staging")],
10387 ),
10388 "Invalid choice for option env: prod, expected one of dev, staging",
10389 );
10390 }
10391
10392 #[test]
10393 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10394 let spec: Spec = r#"
10395 flag "-v --verbose" var=#true
10396 arg "[database]" default="myapp_dev"
10397 arg "[args...]"
10398 "#
10399 .parse()
10400 .unwrap();
10401 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10402 .into_iter()
10403 .map(String::from)
10404 .collect();
10405 let parsed = parse(&spec, &input).unwrap();
10406 let env = parsed.as_env();
10407 assert_eq!(env.get("usage_database").unwrap(), "mydb");
10408 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10409 }
10410
10411 #[test]
10412 fn test_variadic_arg_captures_unknown_flags() {
10413 let cmd = SpecCommand::builder()
10414 .name("test")
10415 .flag(SpecFlag::builder().short('v').long("verbose").build())
10416 .arg(SpecArg::builder().name("database").required(false).build())
10417 .arg(
10418 SpecArg::builder()
10419 .name("args")
10420 .required(false)
10421 .var(true)
10422 .build(),
10423 )
10424 .build();
10425 let spec = Spec {
10426 name: "test".to_string(),
10427 bin: "test".to_string(),
10428 cmd,
10429 ..Default::default()
10430 };
10431
10432 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10434 .into_iter()
10435 .map(String::from)
10436 .collect();
10437 let parsed = parse(&spec, &input).unwrap();
10438 assert_eq!(parsed.args.len(), 2);
10439 let args_val = parsed
10440 .args
10441 .iter()
10442 .find(|(a, _)| a.name == "args")
10443 .unwrap()
10444 .1;
10445 match args_val {
10446 ParseValue::MultiString(v) => {
10447 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10448 }
10449 _ => panic!("Expected MultiString, got {:?}", args_val),
10450 }
10451 }
10452
10453 #[test]
10454 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10455 let cmd = SpecCommand::builder()
10456 .name("test")
10457 .flag(SpecFlag::builder().short('v').long("verbose").build())
10458 .arg(SpecArg::builder().name("database").required(false).build())
10459 .arg(
10460 SpecArg::builder()
10461 .name("args")
10462 .required(false)
10463 .var(true)
10464 .build(),
10465 )
10466 .build();
10467 let spec = Spec {
10468 name: "test".to_string(),
10469 bin: "test".to_string(),
10470 cmd,
10471 ..Default::default()
10472 };
10473
10474 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10476 .into_iter()
10477 .map(String::from)
10478 .collect();
10479 let parsed = parse(&spec, &input).unwrap();
10480 assert_eq!(parsed.args.len(), 2);
10481 let args_val = parsed
10482 .args
10483 .iter()
10484 .find(|(a, _)| a.name == "args")
10485 .unwrap()
10486 .1;
10487 match args_val {
10488 ParseValue::MultiString(v) => {
10489 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10490 }
10491 _ => panic!("Expected MultiString, got {:?}", args_val),
10492 }
10493 }
10494
10495 #[test]
10496 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10497 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10500
10501 let input: Vec<String> = vec!["test", "--option=3"]
10504 .into_iter()
10505 .map(String::from)
10506 .collect();
10507 let parsed = parse(&spec, &input).unwrap();
10508 let env = parsed.as_env();
10509 assert_eq!(
10510 env.get("usage_other_args").map(String::as_str),
10511 Some("'--option=3'"),
10512 "expected a single --option=3 token, got {:?}",
10513 env.get("usage_other_args"),
10514 );
10515
10516 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10518 .into_iter()
10519 .map(String::from)
10520 .collect();
10521 let parsed2 = parse(&spec, &input2).unwrap();
10522 let env2 = parsed2.as_env();
10523 assert_eq!(
10524 env2.get("usage_other_args").map(String::as_str),
10525 Some("'--foo=bar' '--baz=qux'"),
10526 "expected two intact tokens, got {:?}",
10527 env2.get("usage_other_args"),
10528 );
10529
10530 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10532 .into_iter()
10533 .map(String::from)
10534 .collect();
10535 let parsed3 = parse(&spec, &input3).unwrap();
10536 let env3 = parsed3.as_env();
10537 assert_eq!(
10538 env3.get("usage_other_args").map(String::as_str),
10539 Some("positional1 '--option=3' positional2"),
10540 "expected positional args and intact flag token, got {:?}",
10541 env3.get("usage_other_args"),
10542 );
10543 }
10544
10545 #[test]
10546 fn test_allow_hyphen_values_consumes_short_flag_collision() {
10547 let spec = r#"
10548flag "-d --working-dir <DIR>"
10549flag "-a --args <ARGS>" allow_hyphen_values=#true
10550"#
10551 .parse::<Spec>()
10552 .unwrap();
10553
10554 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10555
10556 assert_eq!(parsed.flags.len(), 1);
10557 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10558 }
10559
10560 #[test]
10561 fn test_allow_hyphen_values_consumes_embedded_long_value() {
10562 let spec = r#"
10563flag "-d --working-dir <DIR>"
10564flag "-a --args <ARGS>" allow_hyphen_values=#true
10565"#
10566 .parse::<Spec>()
10567 .unwrap();
10568
10569 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10570
10571 assert_eq!(parsed.flags.len(), 1);
10572 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10573 }
10574
10575 #[test]
10576 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10577 let spec = r#"
10582flag "-a --args <ARGS>" allow_hyphen_values=#true
10583arg "[rest]..."
10584"#
10585 .parse::<Spec>()
10586 .unwrap();
10587
10588 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10589
10590 assert_eq!(flag_string_value(&parsed, "args"), "--");
10591 let rest = parsed
10592 .args
10593 .values()
10594 .next()
10595 .expect("expected the word after the separator to reach the argument");
10596 assert_eq!(rest.to_string(), "-x");
10597 }
10598
10599 #[test]
10600 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10601 let spec = r#"
10606flag "-a --args <ARGS>..." allow_hyphen_values=#true
10607"#
10608 .parse::<Spec>()
10609 .unwrap();
10610
10611 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10612
10613 let flag = parsed
10614 .flags
10615 .keys()
10616 .find(|flag| flag.name == "args")
10617 .expect("expected args flag");
10618 match parsed.flags.get(flag).expect("expected args value") {
10619 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10620 other => panic!("expected a list of values, got {other:?}"),
10621 }
10622 }
10623
10624 #[test]
10625 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10626 let spec = r#"
10627flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10628"#
10629 .parse::<Spec>()
10630 .unwrap();
10631
10632 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10633
10634 let flag = parsed
10635 .flags
10636 .keys()
10637 .find(|flag| flag.name == "args")
10638 .expect("expected args flag");
10639 let value = parsed.flags.get(flag).expect("expected args value");
10640 match value {
10641 ParseValue::MultiString(values) => {
10642 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10643 }
10644 _ => panic!("expected MultiString, got {value:?}"),
10645 }
10646 }
10647
10648 #[test]
10649 fn test_require_equals_accepts_attached_and_refuses_detached() {
10650 let spec = r#"
10651flag "--inspect <PORT>" require_equals=#true
10652"#
10653 .parse::<Spec>()
10654 .unwrap();
10655
10656 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10657 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10658
10659 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10660 let msg = format!("{err}");
10661 assert!(
10662 msg.contains("requires an argument") || msg.contains("inspect"),
10663 "detached value must be refused: {msg}"
10664 );
10665 }
10666
10667 #[test]
10668 fn boolean_flags_can_accept_attached_values_when_enabled() {
10669 let spec: Spec = r#"
10670name "ex"
10671bin "ex"
10672flag "--color" negate="--no-color" bool_value=#true
10673arg "[rest]"
10674"#
10675 .parse()
10676 .unwrap();
10677
10678 for (token, expected) in [
10679 ("--color", true),
10680 ("--color=true", true),
10681 ("--color=false", false),
10682 ("--no-color", false),
10683 ("--no-color=false", true),
10684 ] {
10685 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10686 assert!(
10687 matches!(
10688 parsed.flags.get(&spec.cmd.flags[0]),
10689 Some(ParseValue::Bool(value)) if *value == expected
10690 ),
10691 "{token}"
10692 );
10693 }
10694
10695 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10696 assert!(matches!(
10697 parsed.args.get(&spec.cmd.args[0]),
10698 Some(ParseValue::String(value)) if value == "word"
10699 ));
10700 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10701 assert!(err.to_string().contains("expected `true` or `false`"));
10702
10703 let strict: Spec = r#"
10704name "ex"
10705bin "ex"
10706args_override_self #false
10707flag "--color" negate="--no-color" bool_value=#true
10708"#
10709 .parse()
10710 .unwrap();
10711 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10712 let parsed = parse(
10713 &strict,
10714 &input(&["ex", "--color=false", "--no-color=false"]),
10715 )
10716 .unwrap();
10717 assert!(matches!(
10718 parsed.flags.get(&strict.cmd.flags[0]),
10719 Some(ParseValue::Bool(true))
10720 ));
10721 }
10722
10723 #[test]
10724 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10725 let spec = r#"
10726flag "-a --all"
10727flag "-i --inspect <PORT>" require_equals=#true
10728"#
10729 .parse::<Spec>()
10730 .unwrap();
10731
10732 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10733 let msg = format!("{err}");
10734 assert!(
10735 msg.contains("requires an argument") || msg.contains("inspect"),
10736 "bundled short must refuse the following word: {msg}"
10737 );
10738 }
10739
10740 #[test]
10741 fn test_default_missing_binds_when_the_value_is_left_off() {
10742 let spec = r#"
10743flag "-c --color <WHEN>" default_missing="always"
10744flag "-v --verbose"
10745"#
10746 .parse::<Spec>()
10747 .unwrap();
10748
10749 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10750 assert_eq!(flag_string_value(&parsed, "color"), "always");
10751
10752 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10753 assert_eq!(flag_string_value(&parsed, "color"), "never");
10754
10755 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10756 assert_eq!(flag_string_value(&parsed, "color"), "never");
10757
10758 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10759 assert_eq!(flag_string_value(&parsed, "color"), "always");
10760 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10761
10762 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10763 assert_eq!(flag_string_value(&parsed, "color"), "");
10764
10765 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10766 assert_eq!(flag_string_value(&parsed, "color"), "never");
10767
10768 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10769 assert_eq!(flag_string_value(&parsed, "color"), "always");
10770 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10771 }
10772
10773 #[test]
10774 fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10775 let spec = r#"
10776flag "--apps <N>"
10777flag "--jobs <N>" default_missing="default missing"
10778flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10779"#
10780 .parse::<Spec>()
10781 .unwrap();
10782
10783 let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10784 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10785
10786 let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10787 assert!(
10788 err.to_string().contains("unexpected word: -1"),
10789 "default_missing must keep an unopted negative value separate: {err}"
10790 );
10791
10792 let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10793 assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10794
10795 let external_spec = r#"
10796external_subcommand #true
10797flag "--apps <N>"
10798"#
10799 .parse::<Spec>()
10800 .unwrap();
10801 let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10802 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10803 assert!(parsed.external.is_none());
10804
10805 for words in [
10806 &["test", "--apps", "--jobs", "1"][..],
10807 &["test", "--apps", "--kids", "-1"][..],
10808 ] {
10809 let err = parse(&spec, &input(words)).unwrap_err();
10810 let message = err.to_string();
10811 assert!(
10812 message.contains("--apps") && message.contains("requires an argument"),
10813 "the earlier flag must report its missing value for {words:?}: {message}"
10814 );
10815 }
10816 }
10817
10818 #[test]
10819 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10820 let spec = r#"
10821flag "--bump [LEVEL]" value_optional=#true
10822flag "--verbose"
10823arg "[FILE]"
10824"#
10825 .parse::<Spec>()
10826 .unwrap();
10827
10828 let absent = parse(&spec, &input(&["test"])).unwrap();
10829 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10830
10831 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10832 let bump = bare
10833 .flags
10834 .iter()
10835 .find(|(flag, _)| flag.name == "bump")
10836 .map(|(_, value)| value)
10837 .unwrap();
10838 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10839 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
10840 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
10841
10842 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
10843 assert_eq!(flag_string_value(&explicit, "bump"), "");
10844
10845 let corrected = parse(
10846 &spec,
10847 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
10848 )
10849 .unwrap();
10850 let bump = corrected
10851 .flags
10852 .iter()
10853 .find(|(flag, _)| flag.name == "bump")
10854 .map(|(_, value)| value)
10855 .unwrap();
10856 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
10857
10858 let collecting = r#"
10859flag "--tag [TAG]..." value_optional=#true
10860flag "--verbose"
10861"#
10862 .parse::<Spec>()
10863 .unwrap();
10864 let valued = parse(
10865 &collecting,
10866 &input(&["test", "--tag", "one", "two", "--verbose"]),
10867 )
10868 .unwrap();
10869 let tag = valued
10870 .flags
10871 .iter()
10872 .find(|(flag, _)| flag.name == "tag")
10873 .map(|(_, value)| value)
10874 .unwrap();
10875 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
10876 }
10877
10878 #[test]
10879 fn test_repeatable_bare_optional_values_count_each_occurrence() {
10880 let spec = r#"
10881flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
10882"#
10883 .parse::<Spec>()
10884 .unwrap();
10885
10886 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
10887 let tag = parsed
10888 .flags
10889 .iter()
10890 .find(|(flag, _)| flag.name == "tag")
10891 .map(|(_, value)| value)
10892 .unwrap();
10893 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
10894
10895 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
10896 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
10897 }
10898
10899 #[test]
10900 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
10901 let spec = r#"
10902flag "--tag [TAG]..." var=#true value_optional=#true
10903flag "--verbose"
10904"#
10905 .parse::<Spec>()
10906 .unwrap();
10907
10908 for argv in [
10909 &["test", "--tag", "one", "two"][..],
10910 &["test", "--tag", "one", "two", "--verbose"][..],
10911 &["test", "--tag", "one", "--tag", "two"][..],
10912 ] {
10913 let parsed = parse(&spec, &input(argv)).unwrap();
10914 let tag = parsed
10915 .flags
10916 .iter()
10917 .find(|(flag, _)| flag.name == "tag")
10918 .map(|(_, value)| value)
10919 .unwrap();
10920 assert!(
10921 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
10922 "argv={argv:?}: {tag:?}"
10923 );
10924 }
10925
10926 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
10927 let tag = bare
10928 .flags
10929 .iter()
10930 .find(|(flag, _)| flag.name == "tag")
10931 .map(|(_, value)| value)
10932 .unwrap();
10933 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
10934 }
10935
10936 #[test]
10937 fn test_default_missing_with_require_equals_refuses_the_following_word() {
10938 let spec = r#"
10939flag "--inspect <PORT>" require_equals=#true default_missing="9229"
10940arg "[rest]"
10941"#
10942 .parse::<Spec>()
10943 .unwrap();
10944
10945 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
10946 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10947
10948 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
10949 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
10950
10951 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
10953 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10954 assert_eq!(
10955 parsed
10956 .args
10957 .values()
10958 .next()
10959 .map(|v| v.to_string())
10960 .as_deref(),
10961 Some("80")
10962 );
10963
10964 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
10965 assert_eq!(flag_string_value(&parsed, "inspect"), "");
10966 }
10967
10968 #[test]
10969 fn test_default_missing_must_be_a_choice() {
10970 let spec = r#"
10971flag "--color <WHEN>" default_missing="always" {
10972 choices "auto" "always" "never"
10973}
10974"#
10975 .parse::<Spec>()
10976 .unwrap();
10977
10978 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10979 assert_eq!(flag_string_value(&parsed, "color"), "always");
10980
10981 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10982 assert_eq!(flag_string_value(&parsed, "color"), "never");
10983
10984 let spec = r#"
10985flag "--color <WHEN>" default_missing="wat" {
10986 choices "auto" "always" "never"
10987}
10988"#
10989 .parse::<Spec>()
10990 .unwrap();
10991
10992 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
10993 let msg = format!("{err}");
10994 assert!(
10995 msg.contains("Invalid choice for option color: wat"),
10996 "missing default has to pass choices the same way a typed value does: {msg}"
10997 );
10998
10999 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
11000 let msg = format!("{err}");
11001 assert!(
11002 msg.contains("Invalid choice for option color: wat"),
11003 "an attached value that is not a choice is still refused: {msg}"
11004 );
11005
11006 let spec = r#"
11007flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
11008 choices "9229" "80"
11009}
11010arg "[rest]"
11011"#
11012 .parse::<Spec>()
11013 .unwrap();
11014
11015 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
11016 let msg = format!("{err}");
11017 assert!(
11018 msg.contains("Invalid choice for option inspect: wat"),
11019 "require_equals still binds the missing string, so the error is the choice: {msg}"
11020 );
11021 }
11022
11023 #[test]
11024 fn test_hyphen_values_still_start_short_flag_parsing() {
11025 let spec = r#"
11026flag "-d --working-dir <DIR>"
11027flag "-a --args <ARGS>"
11028"#
11029 .parse::<Spec>()
11030 .unwrap();
11031
11032 let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
11033 let message = err.to_string();
11034 assert!(
11035 message.contains("--args") && message.contains("requires an argument"),
11036 "the recognized -d must leave the earlier -a missing: {message}"
11037 );
11038 }
11039
11040 mod available_flags {
11043 use super::*;
11044
11045 fn spec() -> Spec {
11046 r#"
11047bin "test"
11048flag "-v --verbose" global=#true
11049flag "--raw" global=#true effect="write"
11050flag "--local-only"
11051cmd "run" {
11052 flag "-r --raw"
11053 flag "-w --watch"
11054 cmd "once"
11055}
11056"#
11057 .parse::<Spec>()
11058 .unwrap()
11059 }
11060
11061 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
11062 let mut chain = vec![&spec.cmd];
11063 for segment in path {
11064 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
11065 }
11066 chain
11067 }
11068
11069 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
11070 let mut names: Vec<_> = available_flags(&chain(spec, path))
11071 .iter()
11072 .map(|f| f.name.clone())
11073 .collect();
11074 names.sort();
11075 names
11076 }
11077
11078 #[test]
11079 fn an_empty_chain_yields_nothing() {
11080 assert!(available_flags(&[]).is_empty());
11081 }
11082
11083 #[test]
11084 fn the_root_gets_its_own_flags() {
11085 let spec = spec();
11086 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
11087 }
11088
11089 #[test]
11090 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
11091 let spec = spec();
11092 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
11093 }
11094
11095 #[test]
11096 fn a_re_declared_global_is_listed_once() {
11097 let spec = r#"
11100bin "test"
11101flag "-y --yes" global=#true effect="write"
11102cmd "rm" {
11103 flag "-y --yes"
11104}
11105"#
11106 .parse::<Spec>()
11107 .unwrap();
11108 let flags = available_flags(&chain(&spec, &["rm"]));
11109 assert_eq!(flags.len(), 1, "{flags:?}");
11110 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
11111 }
11112
11113 #[test]
11114 fn a_re_declared_global_keeps_the_globals_declaration() {
11115 let spec = spec();
11119 let flags = available_flags(&chain(&spec, &["run"]));
11120 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
11121 assert!(raw.global);
11122 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
11123 assert_eq!(raw.short, ['r']);
11124 }
11125
11126 #[test]
11127 fn it_matches_what_a_parse_accepts() {
11128 let spec = spec();
11131 for path in [vec![], vec!["run"], vec!["run", "once"]] {
11132 let argv = std::iter::once("test".to_string())
11133 .chain(path.iter().map(|s| s.to_string()))
11134 .collect::<Vec<_>>();
11135 let parsed = parse_partial(&spec, &argv).unwrap();
11136
11137 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
11138 .map(|f| f.name.clone())
11139 .collect();
11140 from_parse.sort();
11141 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
11142 }
11143 }
11144 }
11145
11146 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
11151 parsed
11152 .tokens
11153 .iter()
11154 .find(|token| token.index == index)
11155 .unwrap_or_else(|| panic!("no token at {index}"))
11156 .roles
11157 .iter()
11158 .map(render_role)
11159 .collect()
11160 }
11161
11162 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
11163 parsed
11164 .flag_origins
11165 .iter()
11166 .find(|(f, _)| f.name == flag)
11167 .map(|(_, origins)| origins.clone())
11168 .unwrap_or_default()
11169 }
11170
11171 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
11172 let env = env
11173 .iter()
11174 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
11175 .collect();
11176 Parser::new(spec)
11177 .with_env(env)
11178 .explain(&input(words))
11179 .unwrap()
11180 }
11181
11182 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
11183 explain_with_env(spec, words, &[])
11184 }
11185
11186 #[test]
11187 fn an_attached_long_value_is_recorded_on_the_flag_token() {
11188 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11189 .parse()
11190 .unwrap();
11191
11192 let parsed = explain(&spec, &["ex", "--env=prod"]);
11193
11194 assert_eq!(roles(&parsed, 0), ["program"]);
11195 assert_eq!(
11196 roles(&parsed, 1),
11197 ["flag env as --env", "value of env = [\"prod\"], attached"]
11198 );
11199 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
11202 }
11203
11204 #[test]
11205 fn a_detached_long_value_is_recorded_on_its_own_token() {
11206 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11207 .parse()
11208 .unwrap();
11209
11210 let parsed = explain(&spec, &["ex", "--env", "prod"]);
11211
11212 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
11213 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
11214 }
11215
11216 #[test]
11217 fn a_short_bundle_is_attributed_to_the_bundle_token() {
11218 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
11219 .parse()
11220 .unwrap();
11221
11222 let parsed = explain(&spec, &["ex", "-abj8"]);
11223
11224 assert_eq!(
11227 roles(&parsed, 1),
11228 [
11229 "flag a as -a",
11230 "flag b as -b",
11231 "flag j as -j",
11232 "value of j = [\"8\"], attached",
11233 ]
11234 );
11235 assert_eq!(parsed.tokens.len(), 2);
11236 }
11237
11238 #[test]
11239 fn a_delimiter_splits_one_token_into_several_values() {
11240 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11241 .parse()
11242 .unwrap();
11243
11244 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11245
11246 assert_eq!(
11247 roles(&parsed, 2),
11248 ["value of tags = [\"a\", \"b\", \"c\"]"],
11249 "the values meant, not the word typed"
11250 );
11251 }
11252
11253 #[test]
11254 fn a_separator_and_the_words_after_it_are_distinguished() {
11255 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11256 .parse()
11257 .unwrap();
11258
11259 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11260
11261 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11262 assert_eq!(roles(&parsed, 2), ["separator"]);
11263 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11265 }
11266
11267 #[test]
11268 fn a_second_separator_is_data() {
11269 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11270 .parse()
11271 .unwrap();
11272
11273 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11274
11275 assert_eq!(roles(&parsed, 1), ["separator"]);
11276 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11277 }
11278
11279 #[test]
11280 fn an_unknown_flag_says_what_took_it() {
11281 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11282 .parse()
11283 .unwrap();
11284
11285 let parsed = explain(&spec, &["ex", "--wat"]);
11286
11287 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11290 }
11291
11292 #[test]
11293 fn a_subcommand_word_is_not_a_positional() {
11294 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
11295 .parse()
11296 .unwrap();
11297
11298 let parsed = explain(&spec, &["ex", "build", "a"]);
11299
11300 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11301 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11302 }
11303
11304 #[test]
11305 fn a_multicall_applet_is_read_at_argv0() {
11306 let spec: Spec =
11307 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
11308 .parse()
11309 .unwrap();
11310
11311 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11312
11313 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11316 assert!(parsed.tokens[0].synthesized);
11317 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11318 }
11319
11320 #[test]
11321 fn words_the_parse_never_reached_say_so() {
11322 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11323 .parse()
11324 .unwrap();
11325
11326 let parsed = Parser::new(&spec)
11327 .explain(&input(&["ex", "--help", "a"]))
11328 .unwrap();
11329
11330 assert_eq!(roles(&parsed, 2), ["unread"]);
11331 }
11332
11333 #[test]
11334 fn an_env_origin_names_the_variable_that_fired() {
11335 let spec: Spec =
11336 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11337 .parse()
11338 .unwrap();
11339
11340 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11341 assert_eq!(
11342 origins(&primary, "token"),
11343 [ValueOrigin::Env("EX_TOKEN".to_string())]
11344 );
11345
11346 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11349 assert_eq!(
11350 origins(&fallback, "token"),
11351 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11352 );
11353 }
11354
11355 #[test]
11356 fn a_default_origin_is_recorded_for_flags_and_args() {
11357 let spec: Spec =
11358 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11359 .parse()
11360 .unwrap();
11361
11362 let parsed = explain(&spec, &["ex"]);
11363
11364 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11365 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11366 assert_eq!(arg.name, "src");
11367 assert_eq!(origins, &[ValueOrigin::Default]);
11368 }
11369
11370 #[test]
11371 fn a_default_if_origin_carries_the_condition_that_fired() {
11372 let spec: Spec = r#"
11373name "ex"
11374bin "ex"
11375flag "--profile <p>"
11376flag "--strict" {
11377 default_if "--profile" "prod" "true"
11378}
11379 "#
11380 .parse()
11381 .unwrap();
11382
11383 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11384
11385 assert_eq!(
11388 origins(&parsed, "strict"),
11389 [ValueOrigin::DefaultIf {
11390 selector: "--profile".to_string(),
11391 when: Some("prod".to_string()),
11392 }]
11393 );
11394 }
11395
11396 #[test]
11397 fn a_bare_optional_value_flag_records_default_missing() {
11398 let spec: Spec =
11399 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11400 .parse()
11401 .unwrap();
11402
11403 let parsed = explain(&spec, &["ex", "--color", "-v"]);
11404
11405 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11408 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11409 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11410 }
11411
11412 #[test]
11413 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11414 let spec: Spec =
11415 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11416 .parse()
11417 .unwrap();
11418
11419 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11420
11421 assert_eq!(
11423 roles(&parsed, 1),
11424 [
11425 "flag color as --color",
11426 "value of color = [\"red\"], attached"
11427 ]
11428 );
11429 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11430 }
11431
11432 #[test]
11433 fn an_override_names_the_flag_that_did_it() {
11434 let spec: Spec =
11435 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11436 .parse()
11437 .unwrap();
11438
11439 let parsed = explain(&spec, &["ex", "--loud"]);
11440
11441 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11444 assert!(origins(&parsed, "quiet").is_empty());
11445 }
11446
11447 #[test]
11448 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11449 let spec: Spec = r#"
11450name "ex"
11451bin "ex"
11452cmd "run" restart_token=":::" {
11453 arg "<task>" default="build"
11454}
11455 "#
11456 .parse()
11457 .unwrap();
11458
11459 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11460
11461 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11465 assert_eq!(roles(&parsed, 3), ["restart"]);
11468 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11469 assert!(parsed.arg_origins.is_empty());
11470 }
11471
11472 #[test]
11473 fn a_value_terminator_says_which_run_it_ended() {
11474 let spec: Spec = r#"
11475name "ex"
11476bin "ex"
11477flag "--exec <cmd>..." value_terminator=";"
11478arg "<src>"
11479 "#
11480 .parse()
11481 .unwrap();
11482
11483 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11484
11485 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11486 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11489 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11490 }
11491
11492 #[test]
11493 fn an_args_value_terminator_says_which_run_it_ended() {
11494 let spec: Spec = r#"
11495name "ex"
11496bin "ex"
11497arg "<files>..." value_terminator=";"
11498arg "[dest]"
11499 "#
11500 .parse()
11501 .unwrap();
11502
11503 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11504
11505 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11506 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11507 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11508 }
11509
11510 #[test]
11511 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11512 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11513 .parse()
11514 .unwrap();
11515
11516 let parsed = Parser::new(&spec)
11517 .explain(&input(&["ex", "--env=prod"]))
11518 .unwrap();
11519
11520 assert!(
11523 Parser::new(&spec)
11524 .parse(&input(&["ex", "--env=prod"]))
11525 .is_err()
11526 );
11527 assert_eq!(
11528 roles(&parsed, 1),
11529 ["flag env as --env", "value of env = [\"prod\"], attached"]
11530 );
11531 assert!(
11532 parsed.errors.iter().any(|e| e.to_string().contains("src")),
11533 "{:?}",
11534 parsed.errors
11535 );
11536 }
11537
11538 #[test]
11539 fn an_external_subcommand_forwards_whole_tokens() {
11540 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11541 .parse()
11542 .unwrap();
11543
11544 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11545
11546 assert_eq!(roles(&parsed, 1), ["external"]);
11547 assert_eq!(roles(&parsed, 2), ["external"]);
11548 }
11549
11550 #[test]
11551 fn a_view_keeps_the_callers_argv_positions() {
11552 let spec: Spec = r#"
11553bin "ex"
11554view "runner" root="run"
11555cmd "run" {
11556 flag "--token <token>"
11557}
11558 "#
11559 .parse()
11560 .unwrap();
11561
11562 let parsed = explain(&spec, &["runner", "--token", "secret"]);
11563
11564 assert_eq!(roles(&parsed, 0), ["program"]);
11567 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11568 }
11569
11570 #[test]
11571 fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11572 let spec: Spec = r#"
11573name "ex"
11574bin "ex"
11575default_subcommand "run"
11576default_subcommand_on_empty #true
11577flag "--value <value>"
11578cmd "run" arg_required_else_help=#true
11579"#
11580 .parse()
11581 .unwrap();
11582 let err = Parser::new(&spec)
11583 .parse(&input(&["ex", "--value"]))
11584 .unwrap_err();
11585 assert!(err.to_string().contains("requires an argument"), "{err}");
11586 assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11587 }
11588
11589 #[test]
11590 fn empty_default_honors_parent_args_conflict_policy() {
11591 let spec: Spec = r#"
11592name "ex"
11593bin "ex"
11594default_subcommand "run"
11595default_subcommand_on_empty #true
11596args_conflicts_with_subcommands #true
11597flag "--verbose"
11598cmd "run"
11599"#
11600 .parse()
11601 .unwrap();
11602 let err = Parser::new(&spec)
11603 .parse(&input(&["ex", "--verbose"]))
11604 .unwrap_err();
11605 assert!(
11606 err.to_string().contains("cannot be used with arguments"),
11607 "{err}"
11608 );
11609 }
11610
11611 #[test]
11612 fn empty_default_mount_matches_explicit_selection() {
11613 let spec: Spec = r#"
11614name "ex"
11615bin "ex"
11616default_subcommand "run"
11617default_subcommand_on_empty #true
11618cmd "run" { mount run="discover" }
11619"#
11620 .parse()
11621 .unwrap();
11622 let mut mounts = HashMap::new();
11623 mounts.insert(
11624 "discover".to_string(),
11625 "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11626 );
11627 for argv in [
11628 vec!["ex".to_string()],
11629 vec!["ex".to_string(), "run".to_string()],
11630 ] {
11631 let out = Parser::new(&spec)
11632 .with_mount_outputs(mounts.clone())
11633 .parse(&argv)
11634 .unwrap();
11635 assert_eq!(out.cmds.last().unwrap().name, "run");
11636 assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11637 }
11638 }
11639
11640 #[test]
11641 fn pending_parent_value_stays_pending_in_partial_parse() {
11642 let spec: Spec = r#"
11643name "ex"
11644bin "ex"
11645default_subcommand "run"
11646default_subcommand_on_empty #true
11647flag "--value <value>"
11648cmd "run" {}
11649"#
11650 .parse()
11651 .unwrap();
11652 let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11653 assert_eq!(out.cmd.name, "ex");
11654 assert_eq!(out.flag_awaiting_value[0].name, "value");
11655 }
11656
11657 #[test]
11658 fn optional_default_missing_still_selects_empty_child() {
11659 let spec: Spec = r#"
11660name "ex"
11661bin "ex"
11662default_subcommand "run"
11663default_subcommand_on_empty #true
11664flag "--value <value>" default_missing="fallback"
11665cmd "run" {}
11666"#
11667 .parse()
11668 .unwrap();
11669 let out = Parser::new(&spec)
11670 .parse(&input(&["ex", "--value"]))
11671 .unwrap();
11672 assert_eq!(out.cmd.name, "run");
11673 assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11674 }
11675
11676 #[test]
11677 fn empty_default_resets_cursor_after_root_sigil() {
11678 let spec: Spec = r#"
11679name "ex"
11680bin "ex"
11681default_subcommand "run"
11682default_subcommand_on_empty #true
11683arg "[tool]..." sigil="@"
11684cmd "run" { arg "[path]" }
11685"#
11686 .parse()
11687 .unwrap();
11688 let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11689 assert_eq!(out.cmd.name, "run");
11690 assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11691 }
11692
11693 #[test]
11694 fn root_clause_positional_prevents_empty_default() {
11695 let spec: Spec = r#"
11696name "ex"
11697bin "ex"
11698default_subcommand "run"
11699default_subcommand_on_empty #true
11700clause "tasks" {
11701 arg "<number>" allow_negative_numbers=#true
11702}
11703cmd "run" { arg "[task]" }
11704"#
11705 .parse()
11706 .unwrap();
11707 let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11708 assert!(
11709 out.cmds.iter().all(|cmd| cmd.name != "run"),
11710 "cmds={:?} args={:?}",
11711 out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11712 out.args
11713 );
11714 let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11715 assert_eq!(
11716 out.clauses["tasks"][0].get(number).unwrap().to_string(),
11717 "-7"
11718 );
11719 }
11720}