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(
39 available: &mut BTreeMap<String, Arc<SpecFlag>>,
40 new_flags: BTreeMap<String, Arc<SpecFlag>>,
41 crossing_mount: bool,
42) -> Vec<(Arc<SpecFlag>, usize)> {
43 let mut merges = Vec::new();
44 available.retain(|_, f| f.global);
46
47 if crossing_mount {
48 for (key, flag) in new_flags {
53 available.insert(key, flag);
54 }
55 return merges;
56 }
57
58 let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
61 let mut merged_origin: HashMap<usize, usize> = HashMap::new();
65 fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
67 let ptr = Arc::as_ptr(flag) as usize;
68 *merged_origin.get(&ptr).unwrap_or(&ptr)
69 }
70
71 for (key, flag) in new_flags {
76 if flag.global {
77 available.insert(key, flag);
79 continue;
80 }
81
82 let inherited_global = flag.long.iter().find_map(|l| {
91 available
92 .get(&format!("--{l}"))
93 .filter(|f| f.global)
94 .cloned()
95 });
96 if let Some(global_flag) = inherited_global {
97 let global_origin = origin_of(&merged_origin, &global_flag);
107 if available.get(&key).is_some_and(|existing| {
108 existing.global && origin_of(&merged_origin, existing) != global_origin
109 }) {
110 continue;
111 }
112 let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
113 Some(merged) => merged.clone(),
114 None => {
115 let mut merged = (*global_flag).clone();
116 for s in &flag.short {
124 if !merged.short.contains(s) {
125 merged.short.push(*s);
126 }
127 }
128 for l in &flag.long {
129 if !merged.long.contains(l) {
130 merged.long.push(l.clone());
131 }
132 }
133 merged.hidden_short_aliases.retain(|alias| {
137 !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
138 });
139 merged.hidden_aliases.retain(|alias| {
140 !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
141 });
142 for s in &flag.hidden_short_aliases {
143 if !merged.hidden_short_aliases.contains(s) {
144 merged.hidden_short_aliases.push(*s);
145 }
146 }
147 for l in &flag.hidden_aliases {
148 if !merged.hidden_aliases.contains(l) {
149 merged.hidden_aliases.push(l.clone());
150 }
151 }
152 let merged = Arc::new(merged);
153 merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
154 merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
155 merges.push((Arc::clone(&merged), global_origin));
156 for existing in available.values_mut() {
162 if origin_of(&merged_origin, existing) == global_origin {
163 *existing = Arc::clone(&merged);
164 }
165 }
166 merged
167 }
168 };
169 available.insert(key, merged);
170 continue;
171 }
172
173 if available.contains_key(&key) {
177 continue;
178 }
179 available.insert(key, flag);
180 }
181 merges
182}
183
184fn flag_keys(flag: &SpecFlag) -> Vec<String> {
187 let mut keys: Vec<String> = flag
188 .long
189 .iter()
190 .map(|l| format!("--{l}"))
191 .chain(flag.short.iter().map(|s| format!("-{s}")))
192 .collect();
193 if let Some(negate) = &flag.negate {
194 keys.push(negate.clone());
195 }
196 keys
197}
198
199fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
201 cmd.flags
202 .iter()
203 .chain(cmd.clause.iter().flat_map(|clause| &clause.flags))
204 .flat_map(|f| {
205 let f = Arc::new(f.clone()); flag_keys(&f)
207 .into_iter()
208 .map(|key| (key, Arc::clone(&f)))
209 .collect::<Vec<_>>()
210 })
211 .collect()
212}
213
214fn unique_flags<'a>(
215 flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
216) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
217 let mut seen = HashSet::new();
218 flags
219 .into_iter()
220 .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
221}
222
223pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
236 let Some((root, rest)) = chain.split_first() else {
237 return vec![];
238 };
239 let mut available = gather_flags(root);
240 for cmd in rest {
241 merge_subcommand_flags(&mut available, gather_flags(cmd), false);
242 }
243
244 let mut seen_names = HashSet::new();
255 unique_flags(available.values())
256 .filter(|f| seen_names.insert(f.name.clone()))
257 .cloned()
258 .collect()
259}
260
261fn get_flag_key(word: &str) -> &str {
264 if word.starts_with("--") {
265 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
267 } else if let Some((end, _)) = word.char_indices().nth(2) {
268 &word[0..end]
271 } else {
272 word
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
283#[non_exhaustive]
284pub enum ValueOrigin {
285 DefaultMissing,
290 Env(String),
296 Default,
302 DefaultIf {
306 selector: String,
307 when: Option<String>,
308 },
309}
310
311#[derive(Debug, Clone)]
316#[non_exhaustive]
317pub enum TokenRole {
318 Program,
320 Command { name: String },
322 Flag {
324 flag: Arc<SpecFlag>,
325 spelling: String,
326 negated: bool,
327 },
328 Value {
330 flag: Arc<SpecFlag>,
331 values: Vec<String>,
332 attached: bool,
335 },
336 Arg {
338 arg: Arc<SpecArg>,
339 values: Vec<String>,
340 },
341 Separator,
343 Builtin { spelling: String },
348 ValueTerminator { ends: String },
352 Restart,
356 UnknownFlag { bound_as: Option<Arc<SpecArg>> },
359 Refused { reason: String },
362 External,
364 Unread,
366 Sigil {
368 arg: Arc<SpecArg>,
369 sigil: String,
370 values: Vec<String>,
371 },
372 ClauseSeparator { name: String },
374}
375
376#[derive(Debug, Clone)]
378#[non_exhaustive]
379pub struct TokenBinding {
380 pub index: usize,
382 pub word: String,
383 pub synthesized: bool,
387 pub roles: Vec<TokenRole>,
388}
389
390#[non_exhaustive]
391pub struct ParseOutput {
392 pub cmd: SpecCommand,
393 pub cmds: Vec<SpecCommand>,
394 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
395 pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
397 pub clause_flags: IndexMap<String, Vec<IndexMap<Arc<SpecFlag>, ParseValue>>>,
399 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
400 pub tokens: Vec<TokenBinding>,
407 pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
413 pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
416 pub overridden_flags: BTreeMap<String, String>,
422 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
429 mounted_flags: Vec<Arc<SpecFlag>>,
434 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
435 pub errors: Vec<UsageErr>,
436 pub warnings: Vec<Warning>,
440 pub next_arg: Option<Arc<SpecArg>>,
447 pub double_dash_seen: bool,
453 pub external: Option<Vec<String>>,
458}
459
460impl ParseOutput {
461 pub fn is_mounted_flag(&self, flag: &Arc<SpecFlag>) -> bool {
470 self.mounted_flags
471 .iter()
472 .any(|known| Arc::ptr_eq(known, flag))
473 }
474
475 fn merge_command_flags(&mut self, cmd: &SpecCommand, crossing_mount: bool) {
478 let flags = gather_flags(cmd);
479 if cmd.mounted || cmd.flags_from_mount {
480 self.mounted_flags.extend(flags.values().cloned());
481 }
482 for (merged, origin) in
483 merge_subcommand_flags(&mut self.available_flags, flags, crossing_mount)
484 {
485 if self
486 .mounted_flags
487 .iter()
488 .any(|known| Arc::as_ptr(known) as usize == origin)
489 {
490 self.mounted_flags.push(merged);
491 }
492 }
493 }
494
495 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
504 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
505 return self.available_flags.clone();
506 };
507 let start = match boundary.checked_sub(1) {
511 Some(prev) if self.cmds[prev].flags_from_mount => prev,
512 _ => boundary,
513 };
514 let mut offered = gather_flags(&self.cmds[start]);
518 for cmd in &self.cmds[start + 1..] {
519 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
520 }
521 offered
522 }
523}
524
525#[derive(Debug, Clone)]
526pub enum ParseValue {
527 Bool(bool),
528 String(String),
529 MultiBool(Vec<bool>),
530 MultiString(Vec<String>),
531}
532
533impl ParseValue {
534 pub fn try_as_bool(self) -> Option<bool> {
535 match self {
536 Self::Bool(value) => Some(value),
537 _ => None,
538 }
539 }
540
541 pub const fn try_as_bool_ref(&self) -> Option<&bool> {
542 match self {
543 Self::Bool(value) => Some(value),
544 _ => None,
545 }
546 }
547
548 pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
549 match self {
550 Self::Bool(value) => Some(value),
551 _ => None,
552 }
553 }
554
555 pub fn try_as_string(self) -> Option<String> {
556 match self {
557 Self::String(value) => Some(value),
558 _ => None,
559 }
560 }
561
562 pub const fn try_as_string_ref(&self) -> Option<&String> {
563 match self {
564 Self::String(value) => Some(value),
565 _ => None,
566 }
567 }
568
569 pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
570 match self {
571 Self::String(value) => Some(value),
572 _ => None,
573 }
574 }
575
576 pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
577 match self {
578 Self::MultiBool(value) => Some(value),
579 _ => None,
580 }
581 }
582
583 pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
584 match self {
585 Self::MultiBool(value) => Some(value),
586 _ => None,
587 }
588 }
589
590 pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
591 match self {
592 Self::MultiBool(value) => Some(value),
593 _ => None,
594 }
595 }
596
597 pub fn try_as_multi_string(self) -> Option<Vec<String>> {
598 match self {
599 Self::MultiString(value) => Some(value),
600 _ => None,
601 }
602 }
603
604 pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
605 match self {
606 Self::MultiString(value) => Some(value),
607 _ => None,
608 }
609 }
610
611 pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
612 match self {
613 Self::MultiString(value) => Some(value),
614 _ => None,
615 }
616 }
617}
618
619fn collect_deprecations(out: &mut ParseOutput) {
630 for cmd in out.cmds.iter().skip(1) {
631 if cmd.deprecated.is_none()
632 && cmd.deprecated_warn_at.is_none()
633 && cmd.deprecated_remove_at.is_none()
634 {
635 continue;
636 }
637 out.warnings.push(Warning::command(
638 cmd.name.clone(),
639 cmd.deprecated.clone(),
640 cmd.deprecated_warn_at.clone(),
641 cmd.deprecated_remove_at.clone(),
642 ));
643 }
644 for flag in out.flags.keys() {
645 if let Some(warning) = flag_deprecation(flag) {
646 out.warnings.push(warning);
647 }
648 }
649}
650
651fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
653 if flag.deprecated.is_none()
654 && flag.deprecated_warn_at.is_none()
655 && flag.deprecated_remove_at.is_none()
656 {
657 return None;
658 }
659 Some(Warning::flag(
660 flag_spelling(flag),
661 flag.deprecated.clone(),
662 flag.deprecated_warn_at.clone(),
663 flag.deprecated_remove_at.clone(),
664 ))
665}
666
667fn flag_spelling(flag: &SpecFlag) -> String {
670 flag.long
671 .first()
672 .map(|long| format!("--{long}"))
673 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
674 .unwrap_or_else(|| flag.name.clone())
675}
676
677fn flag_current_env(flag: &SpecFlag) -> Option<String> {
679 flag.env
680 .clone()
681 .or_else(|| flag.env_fallback.first().cloned())
682}
683
684fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
685 flag.deprecated_env.iter().any(|declared| declared == name)
686}
687
688fn arg_current_env(arg: &SpecArg) -> Option<String> {
690 arg.env
691 .clone()
692 .or_else(|| arg.env_fallback.first().cloned())
693}
694
695fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
696 arg.deprecated_env.iter().any(|declared| declared == name)
697}
698
699fn first_set_env<'a>(
706 mut names: impl Iterator<Item = &'a str>,
707 get_env: &impl Fn(&str) -> Option<String>,
708) -> Option<(&'a str, String)> {
709 names.find_map(|name| get_env(name).map(|value| (name, value)))
710}
711
712#[non_exhaustive]
732pub struct Parser<'a> {
733 spec: &'a Spec,
734 env: Option<HashMap<String, String>>,
735 mount_outputs: Option<HashMap<String, String>>,
736 run_commands: bool,
737}
738
739impl<'a> Parser<'a> {
740 pub fn new(spec: &'a Spec) -> Self {
742 Self {
743 spec,
744 env: None,
745 mount_outputs: None,
746 run_commands: true,
747 }
748 }
749
750 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
755 self.env = Some(env);
756 self
757 }
758
759 pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
766 self.mount_outputs = Some(outputs);
767 self
768 }
769
770 pub fn without_running_commands(mut self) -> Self {
777 self.run_commands = false;
778 self
779 }
780
781 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
785 let out = self.parse_collecting(input)?;
786 if let Some(err) = out
787 .errors
788 .iter()
789 .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
790 {
791 bail!("{err}");
792 }
793 if !out.errors.is_empty() {
794 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
795 }
796 Ok(out)
797 }
798
799 pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
810 self.parse_collecting(input)
811 }
812
813 pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
828 let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
829 let mut trace = Trace::new(input);
830 match parse_partial_traced(
831 self.spec,
832 input,
833 self.env.as_ref(),
834 self.mount_outputs.as_ref(),
835 MountTiming::WhenAWordIsUnknown,
836 true,
837 &mut trace,
838 ) {
839 Ok((out, _)) => Ok(out),
842 Err(_) => Err(trace.tokens),
843 }
844 }
845
846 fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
847 let _runs = crate::spec::choices::RunScope::enter();
850 let _no_runs = (!self.run_commands).then(crate::spec::choices::NoRunScope::enter);
851 let custom_env = self.env.as_ref();
852 let (mut out, overridden_flags) = parse_partial_with_env(
853 self.spec,
854 input,
855 custom_env,
856 self.mount_outputs.as_ref(),
857 MountTiming::WhenAWordIsUnknown,
858 false,
859 )?;
860 restore_current_clause(&mut out);
861 trace!("{out:?}");
862
863 while try_bind_default_missing(
869 &mut out.flags,
870 &mut out.flag_awaiting_value,
871 custom_env,
872 &mut out.flag_origins,
873 )? {}
874 if let Some(flag) = out.flag_awaiting_value.first() {
875 let token = flag
876 .long
877 .first()
878 .map(|l| format!("--{l}"))
879 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
880 .unwrap_or_else(|| flag.name.clone());
881 let rendered = input.join(" ");
882 let span = rendered
883 .rfind(&token)
884 .map(|at| (at, token.len()))
885 .unwrap_or((0, 0));
886 return Err(UsageErr::InvalidFlag {
887 token,
888 reason: "requires an argument".to_string(),
889 span: span.into(),
890 input: rendered,
891 }
892 .into());
893 }
894
895 collect_deprecations(&mut out);
899
900 let get_env = |key: &str| -> Option<String> {
901 if let Some(env_map) = custom_env {
902 env_map.get(key).cloned()
903 } else {
904 std::env::var(key).ok()
905 }
906 };
907
908 for arg in active_args(&out.cmd) {
913 if out.cmd.clause.is_some() {
916 break;
917 }
918 if out.args.contains_key(arg) {
919 continue;
920 }
921 if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
922 if arg_env_is_deprecated(arg, env_name) {
923 out.warnings
924 .push(Warning::env(env_name, arg_current_env(arg)));
925 }
926 let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
927 validate_choice_values(
928 ChoiceTarget::arg(arg),
929 &values,
930 arg.choices.as_ref(),
931 custom_env,
932 )?;
933 let parsed = if arg.var {
934 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
935 ParseValue::MultiString(values)
936 } else {
937 ParseValue::String(values.into_iter().next().unwrap_or_default())
938 };
939 out.args.insert(Arc::new(arg.clone()), parsed);
940 out.arg_origins
941 .entry(Arc::new(arg.clone()))
942 .or_default()
943 .push(ValueOrigin::Env(env_name.to_string()));
944 continue;
945 }
946 if !arg.default.is_empty() {
947 if arg.var {
949 let values = split_fallback_values(&arg.default, arg.delimiter);
950 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
951 validate_choice_values(
952 ChoiceTarget::arg(arg),
953 &values,
954 arg.choices.as_ref(),
955 custom_env,
956 )?;
957 out.args
959 .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
960 out.arg_origins
961 .entry(Arc::new(arg.clone()))
962 .or_default()
963 .push(ValueOrigin::Default);
964 } else {
965 validate_choice_value(
966 ChoiceTarget::arg(arg),
967 &arg.default[0],
968 arg.choices.as_ref(),
969 custom_env,
970 )?;
971 out.args.insert(
973 Arc::new(arg.clone()),
974 ParseValue::String(arg.default[0].clone()),
975 );
976 out.arg_origins
977 .entry(Arc::new(arg.clone()))
978 .or_default()
979 .push(ValueOrigin::Default);
980 }
981 }
982 }
983
984 let flags: Vec<Arc<SpecFlag>> = out
989 .available_flags
990 .values()
991 .filter(|flag| !is_clause_scoped_flag(&out, flag))
992 .cloned()
993 .collect();
994 for flag in &flags {
995 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
996 continue;
997 }
998 if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
999 if let Some(warning) = flag_deprecation(flag) {
1003 out.warnings.push(warning);
1004 }
1005 if flag_env_is_deprecated(flag, env_name) {
1006 out.warnings
1007 .push(Warning::env(env_name, flag_current_env(flag)));
1008 }
1009 if let Some(arg) = flag.arg.as_ref() {
1010 let values =
1011 split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
1012 validate_choice_values(
1013 ChoiceTarget::option(flag),
1014 &values,
1015 arg.choices.as_ref(),
1016 custom_env,
1017 )?;
1018 let parsed = if flag.var || arg.var {
1019 if flag.var {
1020 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
1021 }
1022 if arg.var {
1023 validate_flag_arg_fallback_count(
1024 flag,
1025 arg,
1026 values.len(),
1027 &mut out.errors,
1028 );
1029 }
1030 ParseValue::MultiString(values)
1031 } else {
1032 ParseValue::String(values.into_iter().next().unwrap_or_default())
1033 };
1034 out.flags.insert(Arc::clone(flag), parsed);
1035 } else {
1036 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
1037 out.flags
1038 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
1039 }
1040 out.flag_origins
1041 .entry(Arc::clone(flag))
1042 .or_default()
1043 .push(ValueOrigin::Env(env_name.to_string()));
1044 }
1045 }
1046 let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
1052 for flag in &flags {
1053 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1054 continue;
1055 }
1056 if let Some(condition) = flag.default_if.iter().find(|condition| {
1057 default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
1058 }) {
1059 from_default_if.push((Arc::clone(flag), condition.clone()));
1060 }
1061 }
1062 for (flag, condition) in &from_default_if {
1063 bind_flag_fallback(
1067 flag,
1068 std::slice::from_ref(&condition.value),
1069 &mut out,
1070 custom_env,
1071 ValueOrigin::DefaultIf {
1072 selector: condition.selector.clone(),
1073 when: condition.when.clone(),
1074 },
1075 )?;
1076 }
1077 for flag in &flags {
1078 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1079 continue;
1080 }
1081 if !flag.default.is_empty() {
1082 bind_flag_fallback(
1083 flag,
1084 &flag.default,
1085 &mut out,
1086 custom_env,
1087 ValueOrigin::Default,
1088 )?;
1089 continue;
1090 }
1091 if let Some(arg) = flag.arg.as_ref()
1092 && !arg.default.is_empty()
1093 {
1094 bind_flag_fallback(
1095 flag,
1096 &arg.default,
1097 &mut out,
1098 custom_env,
1099 ValueOrigin::Default,
1100 )?;
1101 }
1102 }
1103 finalize_current_clause(&mut out);
1108 let clause_explicit = apply_clause_flag_fallbacks(&mut out, &overridden_flags, custom_env)?;
1109 validate_clause_relationships(
1110 &mut out,
1111 &overridden_flags,
1112 custom_env,
1113 clause_explicit.as_deref(),
1114 );
1115 for (arg, parsed) in &out.args {
1120 validate_expression(
1121 &arg.name,
1122 arg.validate.as_deref(),
1123 arg.validate_error.as_deref(),
1124 parsed,
1125 &mut out.errors,
1126 );
1127 }
1128 if let Some(clause) = &out.cmd.clause {
1129 let mut clause_errors = Vec::new();
1130 for (index, instance) in out
1131 .clauses
1132 .get(&clause.name)
1133 .into_iter()
1134 .flatten()
1135 .enumerate()
1136 {
1137 for arg in &clause.args {
1138 let Some(value) = instance.get(arg) else {
1139 if arg.required {
1140 clause_errors.push(UsageErr::MissingClauseArg {
1141 clause: clause.name.clone(),
1142 instance: index + 1,
1143 arg: arg.name.clone(),
1144 });
1145 }
1146 continue;
1147 };
1148 if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1149 if let Some(min) = arg.var_min
1150 && values.len() < min
1151 {
1152 clause_errors.push(UsageErr::VarArgTooFew {
1153 name: format!(
1154 "{} instance {}: {}",
1155 clause.name,
1156 index + 1,
1157 arg.name
1158 ),
1159 min,
1160 got: values.len(),
1161 });
1162 }
1163 if let Some(max) = arg.var_max
1164 && values.len() > max
1165 {
1166 clause_errors.push(UsageErr::VarArgTooMany {
1167 name: format!(
1168 "{} instance {}: {}",
1169 clause.name,
1170 index + 1,
1171 arg.name
1172 ),
1173 max,
1174 got: values.len(),
1175 });
1176 }
1177 }
1178 }
1179 }
1180 out.errors.extend(clause_errors);
1181 }
1182 let clause_flag_names = out
1183 .cmd
1184 .clause
1185 .iter()
1186 .flat_map(|clause| &clause.flags)
1187 .map(|flag| flag.name.as_str())
1188 .collect::<HashSet<_>>();
1189 for (flag, parsed) in out
1190 .flags
1191 .iter()
1192 .filter(|(flag, _)| !clause_flag_names.contains(flag.name.as_str()))
1193 {
1194 if let Some(arg) = &flag.arg {
1195 validate_expression(
1196 &flag.name,
1197 arg.validate.as_deref(),
1198 arg.validate_error.as_deref(),
1199 parsed,
1200 &mut out.errors,
1201 );
1202 }
1203 }
1204 let answering = out
1213 .errors
1214 .iter()
1215 .any(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)));
1216 if let Some(unknown_env) = self.spec.unknown_env.as_ref().filter(|_| !answering) {
1217 let process: Vec<std::ffi::OsString>;
1218 let (names, exact): (Vec<&std::ffi::OsStr>, bool) = match custom_env {
1223 Some(env) => (env.keys().map(std::ffi::OsStr::new).collect(), true),
1224 None => {
1225 process = std::env::vars_os().map(|(name, _)| name).collect();
1226 (process.iter().map(|n| n.as_os_str()).collect(), false)
1227 }
1228 };
1229 let unknown =
1230 unknown_env.unknown_with(self.spec, &out.cmds, names.iter().copied(), exact);
1231 if !unknown.is_empty() {
1232 out.errors.push(UsageErr::UnknownEnv {
1233 prefix: unknown_env.prefix.clone(),
1234 names: unknown,
1235 });
1236 }
1237 }
1238 crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1241 Ok(out)
1242 }
1243}
1244
1245#[must_use = "parsing result should be used"]
1252pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1253 Parser::new(spec).parse(input)
1254}
1255
1256#[must_use = "parsing result should be used"]
1260pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1261 parse_partial_with_env(spec, input, None, None, MountTiming::Eager, true).map(|(out, _)| out)
1262}
1263
1264pub fn multicall_basename(argv0: &str) -> &str {
1267 let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1268 match name.get(name.len().saturating_sub(4)..) {
1269 Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1270 _ => name,
1271 }
1272}
1273
1274pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1279 let base = multicall_basename(argv0);
1280 if !name.is_empty() && base == multicall_basename(name) {
1281 return None;
1282 }
1283 if let Some(bin) = bin
1284 && !bin.is_empty()
1285 && base == multicall_basename(bin)
1286 {
1287 return None;
1288 }
1289 Some(base)
1290}
1291
1292#[derive(Clone, Copy, PartialEq, Eq)]
1301enum MountTiming {
1302 Eager,
1303 WhenAWordIsUnknown,
1304}
1305
1306struct Token {
1316 word: String,
1317 argv: usize,
1323 binding: Option<(Arc<SpecFlag>, usize)>,
1335}
1336
1337struct DefaultBundle {
1339 argv: usize,
1340 flags: BTreeMap<String, Arc<SpecFlag>>,
1341 parent_keys: HashSet<String>,
1342}
1343
1344impl DefaultBundle {
1345 fn binding(&self, key: &str) -> Option<(Arc<SpecFlag>, usize)> {
1347 self.flags.get(key).map(|flag| {
1348 (
1349 Arc::clone(flag),
1350 usize::from(!self.parent_keys.contains(key)),
1351 )
1352 })
1353 }
1354}
1355
1356impl Token {
1357 fn new(word: String, argv: usize) -> Self {
1358 Self {
1359 word,
1360 argv,
1361 binding: None,
1362 }
1363 }
1364}
1365
1366struct Trace {
1372 tokens: Vec<TokenBinding>,
1373}
1374
1375impl Trace {
1376 fn new(input: &[String]) -> Self {
1377 Self {
1378 tokens: input
1379 .iter()
1380 .enumerate()
1381 .map(|(index, word)| TokenBinding {
1382 index,
1383 word: word.clone(),
1384 synthesized: false,
1385 roles: vec![],
1386 })
1387 .collect(),
1388 }
1389 }
1390
1391 fn record(&mut self, argv: usize, role: TokenRole) {
1392 if let Some(token) = self.tokens.get_mut(argv) {
1393 token.roles.push(role);
1394 }
1395 }
1396
1397 fn note_synthesized(&mut self, argv: usize) {
1399 if let Some(token) = self.tokens.get_mut(argv) {
1400 token.synthesized = true;
1401 }
1402 }
1403
1404 fn close(&mut self, unread: &VecDeque<Token>) {
1406 for token in unread {
1407 self.record(token.argv, TokenRole::Unread);
1408 }
1409 }
1410}
1411
1412fn parse_partial_with_env(
1413 spec: &Spec,
1414 input: &[String],
1415 custom_env: Option<&HashMap<String, String>>,
1416 mount_outputs: Option<&HashMap<String, String>>,
1417 mount_timing: MountTiming,
1418 validate_clauses: bool,
1419) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1420 let mut trace = Trace::new(input);
1421 parse_partial_traced(
1422 spec,
1423 input,
1424 custom_env,
1425 mount_outputs,
1426 mount_timing,
1427 validate_clauses,
1428 &mut trace,
1429 )
1430}
1431
1432fn default_flag_route(spec: &Spec, root: &SpecCommand, input: &VecDeque<Token>) -> Option<usize> {
1435 let default = root.find_subcommand(spec.default_subcommand.as_deref()?)?;
1436 let parent = gather_flags(root);
1437 let child = gather_flags(default);
1438 let path = std::slice::from_ref(root);
1439 let mut at = None;
1440 let mut i = 0;
1441 while let Some(token) = input.get(i).map(|t| t.word.as_str()) {
1442 let scope = if at.is_some() { default } else { root };
1443 if token == "--"
1444 || token == "-"
1445 || scope
1446 .clause
1447 .as_ref()
1448 .is_some_and(|c| c.separator.as_deref() == Some(token))
1449 {
1450 return at;
1451 }
1452 if !is_flag_like(token) {
1453 if at.is_none()
1454 && (root.find_subcommand(token).is_some()
1455 || (token == "help"
1456 && spec.disable_help != Some(true)
1457 && !root.disable_help_subcommand))
1458 {
1459 return None;
1460 }
1461 return at;
1462 }
1463 let mut value_flag = None;
1464 let mut attached = None;
1465 if token.starts_with("--") {
1466 let (name, value) = token
1467 .split_once('=')
1468 .map_or((token, None), |(n, v)| (n, Some(v)));
1469 if !parent.contains_key(name)
1470 && (is_help_arg(spec, root, name) || is_version_arg(spec, path, name))
1471 {
1472 i += 1;
1473 continue;
1474 }
1475 let flag = parent.get(name).or_else(|| child.get(name));
1476 let Some(flag) = flag else {
1477 return at;
1478 };
1479 if !parent.contains_key(name) {
1480 at.get_or_insert(i);
1481 }
1482 if flag.arg.is_some() && flag.negate.as_deref() != Some(name) {
1483 value_flag = Some(flag);
1484 attached = value;
1485 }
1486 } else {
1487 for (offset, letter) in token.char_indices().skip(1) {
1488 let key = format!("-{letter}");
1489 if !parent.contains_key(&key) && supplies_short(spec, path, letter) {
1490 continue;
1491 }
1492 let flag = parent.get(&key).or_else(|| child.get(&key));
1493 let Some(flag) = flag else {
1494 return at;
1495 };
1496 if !parent.contains_key(&key) {
1497 at.get_or_insert(i);
1498 }
1499 if flag.arg.is_some() {
1500 value_flag = Some(flag);
1501 let rest = &token[offset + letter.len_utf8()..];
1502 attached = (!rest.is_empty()).then_some(rest.strip_prefix('=').unwrap_or(rest));
1503 break;
1504 }
1505 }
1506 }
1507 i += 1;
1508 if let Some(flag) = value_flag {
1509 let scope = if at.is_some() { default } else { root };
1510 let is_separator = |next: &str| {
1511 scope
1512 .clause
1513 .as_ref()
1514 .is_some_and(|c| c.separator.as_deref() == Some(next))
1515 };
1516 let first = if attached.is_some() {
1517 attached
1518 } else {
1519 input
1520 .get(i)
1521 .map(|t| t.word.as_str())
1522 .filter(|next| !is_separator(next) && accepts_detached_flag_value(flag, next))
1523 .inspect(|_| i += 1)
1524 };
1525 if first.is_none() && !flag.value_optional && flag.default_missing.is_none() {
1526 return at;
1527 }
1528 let arg = flag.arg.as_ref().expect("value flag");
1529 if arg.var {
1530 let count_values = |v: &str| arg.delimiter.map_or(1, |d| v.split(d).count());
1531 let mut count = first.map_or(0, count_values);
1532 while arg.var_max.is_none_or(|max| count < max) {
1533 let Some(next) = input.get(i).map(|t| t.word.as_str()) else {
1534 break;
1535 };
1536 if is_separator(next) {
1537 break;
1538 }
1539 if next == "--" || arg.value_terminator.as_deref() == Some(next) {
1540 if next != "--" {
1541 i += 1;
1542 }
1543 break;
1544 }
1545 if is_flag_like(next)
1546 && !(arg.allow_negative_numbers && is_negative_number(next))
1547 {
1548 break;
1549 }
1550 if root.subcommand_precedence_over_arg && root.find_subcommand(next).is_some() {
1551 break;
1552 }
1553 count += count_values(next);
1554 i += 1;
1555 }
1556 }
1557 }
1558 }
1559 at
1560}
1561
1562fn parse_partial_traced(
1569 spec: &Spec,
1570 input: &[String],
1571 custom_env: Option<&HashMap<String, String>>,
1572 mount_outputs: Option<&HashMap<String, String>>,
1573 mount_timing: MountTiming,
1574 validate_clauses: bool,
1575 trace: &mut Trace,
1576) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1577 let _runs = crate::spec::choices::RunScope::enter();
1579 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1580 let viewed = spec.for_view(view)?;
1581 return parse_partial_traced(
1582 &viewed,
1583 input,
1584 custom_env,
1585 mount_outputs,
1586 mount_timing,
1587 validate_clauses,
1588 trace,
1589 );
1590 }
1591 trace!("parse_partial: {input:?}");
1592 let mut input = input
1593 .iter()
1594 .enumerate()
1595 .map(|(argv, word)| Token::new(word.clone(), argv))
1596 .collect::<VecDeque<_>>();
1597 let argv0 = input.pop_front();
1598 if let Some(argv0) = argv0.as_ref() {
1599 trace.record(argv0.argv, TokenRole::Program);
1600 }
1601 if spec.multicall
1602 && let Some(raw) = argv0
1603 && let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str()))
1604 {
1605 trace.note_synthesized(raw.argv);
1609 input.push_front(Token::new(applet.to_string(), raw.argv));
1610 }
1611 let mut command_has_argv = !input.is_empty();
1615 let mut root_positional_arg_found = false;
1618
1619 let mut out = ParseOutput {
1620 cmd: spec.cmd.clone(),
1621 cmds: vec![spec.cmd.clone()],
1622 args: IndexMap::new(),
1623 clauses: IndexMap::new(),
1624 clause_flags: IndexMap::new(),
1625 flags: IndexMap::new(),
1626 tokens: vec![],
1627 flag_origins: IndexMap::new(),
1628 arg_origins: IndexMap::new(),
1629 overridden_flags: BTreeMap::new(),
1630 available_flags: gather_flags(&spec.cmd),
1631 mounted_flags: vec![],
1632 flag_awaiting_value: vec![],
1633 errors: vec![],
1634 warnings: vec![],
1635 next_arg: None,
1636 double_dash_seen: false,
1637 external: None,
1638 };
1639 if spec.cmd.mounted || spec.cmd.flags_from_mount {
1641 out.mounted_flags = out.available_flags.values().cloned().collect();
1642 }
1643 let mut overridden_flags = HashSet::new();
1646 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1651
1652 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1665 let mut command_arg_found = false;
1668 let mut variadic_flag_active = false;
1669 let mut idx = 0;
1670 let mut used_default_subcommand = false;
1673 let mut default_bundle: Option<DefaultBundle> = None;
1674 let mut mounts_resolved = false;
1678 let default_outranks_mounts =
1688 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1689 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1690 {
1691 mounts_resolved = true;
1692 let mut mounted = out.cmd.clone();
1693 mounted.mount(&[], mount_outputs)?;
1694 out.merge_command_flags(&mounted, false);
1695 if let Some(last) = out.cmds.last_mut() {
1696 *last = mounted.clone();
1697 }
1698 out.cmd = mounted;
1699 }
1700
1701 let default_flag_at = spec
1702 .default_subcommand_flags
1703 .then(|| default_flag_route(spec, &out.cmd, &input))
1704 .flatten();
1705
1706 while idx < input.len() {
1707 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1717 default_accepts_word(&out.cmd, name, &input[idx].word)
1718 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1719 });
1720 if !mounts_resolved
1721 && !out.cmd.mounts.is_empty()
1722 && !default_catches_it
1723 && is_command_word(&input[idx].word)
1724 && !is_negative_number(&input[idx].word)
1725 && out.cmd.find_subcommand(&input[idx].word).is_none()
1726 {
1727 mounts_resolved = true;
1728 let mut mounted = out.cmd.clone();
1729 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1730 out.merge_command_flags(&mounted, false);
1731 if let Some(last) = out.cmds.last_mut() {
1732 *last = mounted.clone();
1733 }
1734 out.cmd = mounted;
1735 }
1736 if variadic_flag_active
1737 && out.cmd.find_subcommand(&input[idx].word).is_some()
1738 && !out.cmd.subcommand_precedence_over_arg
1739 {
1740 break;
1741 }
1742 let implicit_default = !used_default_subcommand && default_flag_at == Some(idx);
1743 let selected_command = if implicit_default {
1744 out.cmd
1745 .find_subcommand(spec.default_subcommand.as_deref().expect("default route"))
1746 } else {
1747 out.cmd.find_subcommand(&input[idx].word)
1748 };
1749 if let Some(subcommand) = selected_command {
1750 let boundary_has_parent = out.cmd.args_conflicts_with_subcommands
1751 && implicit_default
1752 && input[idx].word.starts_with('-')
1753 && !input[idx].word.starts_with("--")
1754 && {
1755 let child_flags = gather_flags(subcommand);
1756 let mut found = false;
1757 for short in input[idx].word[1..].chars() {
1758 let key = format!("-{short}");
1759 if out.available_flags.contains_key(&key)
1760 || supplies_short(spec, &out.cmds, short)
1761 {
1762 found = true;
1763 break;
1764 }
1765 match child_flags.get(&key) {
1766 Some(flag) if flag.arg.is_none() => {}
1767 _ => break,
1768 }
1769 }
1770 found
1771 };
1772 if out.cmd.args_conflicts_with_subcommands && (command_arg_found || boundary_has_parent)
1773 {
1774 bail!(
1775 "subcommand '{}' cannot be used with arguments on its parent command",
1776 subcommand.name
1777 );
1778 }
1779 let boundary_parent = (implicit_default
1780 && input[idx].word.starts_with('-')
1781 && !input[idx].word.starts_with("--"))
1782 .then(|| out.available_flags.clone());
1783 let mut subcommand = subcommand.clone();
1784 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1786 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1789 out.merge_command_flags(&subcommand, crossing_mount);
1790 if let Some(parent) = boundary_parent {
1791 let parent_keys = parent.keys().cloned().collect();
1792 let mut flags = out.available_flags.clone();
1793 flags.extend(parent);
1794 default_bundle = Some(DefaultBundle {
1795 argv: input[idx].argv,
1796 flags,
1797 parent_keys,
1798 });
1799 }
1800 let selected = if implicit_default {
1802 used_default_subcommand = true;
1803 None
1804 } else {
1805 input.remove(idx)
1806 };
1807 if let Some(selected) = selected {
1808 trace.record(
1809 selected.argv,
1810 TokenRole::Command {
1811 name: subcommand.name.clone(),
1812 },
1813 );
1814 }
1815 command_has_argv = idx < input.len();
1816 out.cmds.push(subcommand.clone());
1817 out.cmd = subcommand.clone();
1818 mounts_resolved = true;
1820 prefix_flags.clear();
1821 command_arg_found = false;
1822 variadic_flag_active = false;
1823 } else if !is_command_word(&input[idx].word)
1826 || declared_numeric_short(&out.available_flags, &input[idx].word)
1827 {
1828 let word = input[idx].word.clone();
1830 let flag_key = get_flag_key(&word);
1831
1832 let boundary = default_bundle
1838 .as_ref()
1839 .filter(|bundle| bundle.argv == input[idx].argv);
1840 let flags = boundary.map_or(&out.available_flags, |bundle| &bundle.flags);
1841 let is_bundle =
1842 word.starts_with("--") || short_bundle_is_known(spec, &out.cmds, flags, &word);
1843 if let Some(f) = flags.get(flag_key).cloned().filter(|_| is_bundle) {
1844 command_arg_found = true;
1845 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1846 input[idx].binding = boundary
1854 .and_then(|bundle| bundle.binding(flag_key))
1855 .or_else(|| Some((Arc::clone(&f), out.cmds.len() - 1)));
1856 let mut forwarded = f.global.then(|| vec![word.clone()]);
1857 idx += 1;
1858
1859 if f.arg.is_some()
1862 && !word.contains('=')
1863 && idx < input.len()
1864 && accepts_detached_flag_value(&f, &input[idx].word)
1865 {
1866 if let Some(words) = forwarded.as_mut() {
1867 words.push(input[idx].word.clone());
1868 }
1869 idx += 1;
1870 }
1871 if let Some(words) = forwarded {
1872 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1873 prefix_flags.push((f, words));
1874 }
1875 } else {
1876 break;
1879 }
1880 } else {
1881 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1882 idx += 1;
1883 continue;
1884 }
1885 if !used_default_subcommand
1894 && out.cmds.len() == 1
1895 && let Some(default_name) = &spec.default_subcommand
1896 && let Some(subcommand) = out
1897 .cmd
1898 .find_subcommand(default_name)
1899 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1900 {
1901 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1902 bail!(
1903 "subcommand '{}' cannot be used with arguments on its parent command",
1904 subcommand.name
1905 );
1906 }
1907 let mut subcommand = subcommand.clone();
1908 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1910 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1911 out.merge_command_flags(&subcommand, crossing_mount);
1912 out.cmds.push(subcommand.clone());
1913 out.cmd = subcommand.clone();
1914 command_has_argv = true;
1915 prefix_flags.clear();
1916 command_arg_found = false;
1917 variadic_flag_active = false;
1918 mounts_resolved = true;
1921 used_default_subcommand = true;
1922 continue;
1927 }
1928 if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1933 idx += 1;
1934 continue;
1935 }
1936 if out.cmd.external_subcommand {
1941 let rest: Vec<Token> = input.drain(idx..).collect();
1942 for token in &rest {
1943 trace.record(token.argv, TokenRole::External);
1944 }
1945 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1946 break;
1947 }
1948 break;
1950 }
1951 }
1952
1953 let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1964 let mut enable_flags = true;
1965 let mut grouped_flag = false;
1966 let mut seen_double_dash = false;
1970 let mut restart_seen = false;
1973 let mut double_dash_violations: HashSet<String> = HashSet::new();
1976 let mut root_clause_separator_seen = false;
1980 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1985
1986 while !input.is_empty() {
1987 let token = input.pop_front().unwrap();
1988 let binding = token.binding;
1990 let argv = token.argv;
1991 let mut w = token.word;
1992 let attached_continuation = grouped_flag;
1997 if spec.default_subcommand_flags
2001 && attached_continuation
2002 && !out.flag_awaiting_value.is_empty()
2003 {
2004 grouped_flag = false;
2005 w.remove(0);
2006 if w.starts_with('=') {
2007 w.remove(0);
2008 }
2009 if bind_pending_flag_value(
2010 spec,
2011 &out.cmd,
2012 &mut out.errors,
2013 &mut out.flags,
2014 &mut out.flag_awaiting_value,
2015 &mut w,
2016 &mut input,
2017 custom_env,
2018 trace,
2019 argv,
2020 true,
2021 )? {
2022 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2023 return Ok((out, overridden_flags));
2024 }
2025 continue;
2026 }
2027
2028 if !seen_double_dash
2031 && let Some(clause) = out.cmd.clause.as_ref()
2032 && clause.separator.as_deref() == Some(w.as_str())
2033 {
2034 while try_bind_default_missing(
2035 &mut out.flags,
2036 &mut out.flag_awaiting_value,
2037 custom_env,
2038 &mut out.flag_origins,
2039 )? {}
2040 if let Some(flag) = out.flag_awaiting_value.first() {
2041 let spelling = flag
2042 .long
2043 .first()
2044 .map(|long| format!("--{long}"))
2045 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2046 .unwrap_or_else(|| flag.name.clone());
2047 return Err(UsageErr::InvalidFlag {
2048 token: spelling.clone(),
2049 reason: "requires an argument".to_string(),
2050 span: (0, spelling.len()).into(),
2051 input: spelling,
2052 }
2053 .into());
2054 }
2055 let name = clause.name.clone();
2056 if out.cmds.len() == 1 {
2057 root_clause_separator_seen = true;
2058 }
2059 finalize_current_clause(&mut out);
2060 out.arg_origins.clear();
2061 trace.record(argv, TokenRole::ClauseSeparator { name });
2062 next_arg_idx = 0;
2063 out.flag_awaiting_value.clear();
2064 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2065 enable_flags = true;
2066 seen_double_dash = false;
2067 continue;
2068 }
2069
2070 if let Some(ref restart_token) = out.cmd.restart_token
2073 && w == *restart_token
2074 {
2075 out.args.clear();
2080 out.arg_origins.clear();
2085 trace.record(argv, TokenRole::Restart);
2086 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2087 restart_seen = true;
2088 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
2092 }
2093
2094 if enable_flags
2103 && !attached_continuation
2104 && w.starts_with('-')
2105 && out
2106 .flag_awaiting_value
2107 .last()
2108 .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
2109 {
2110 let should_return = bind_pending_flag_value(
2113 spec,
2114 &out.cmd,
2115 &mut out.errors,
2116 &mut out.flags,
2117 &mut out.flag_awaiting_value,
2118 &mut w,
2119 &mut input,
2120 custom_env,
2121 trace,
2122 argv,
2123 false,
2125 )?;
2126 if should_return {
2127 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2128 return Ok((out, overridden_flags));
2129 }
2130 continue;
2131 }
2132
2133 if enable_flags
2139 && !attached_continuation
2140 && !out.flag_awaiting_value.is_empty()
2141 && out.flag_awaiting_value.last().is_some_and(|flag| {
2142 (flag.default_missing.is_some() || flag.value_optional)
2143 && !accepts_detached_flag_value(flag, &w)
2144 })
2145 {
2146 try_bind_default_missing(
2147 &mut out.flags,
2148 &mut out.flag_awaiting_value,
2149 custom_env,
2150 &mut out.flag_origins,
2151 )?;
2152 }
2153
2154 if w == "--" && !seen_double_dash {
2160 enable_flags = false;
2161
2162 let should_preserve = active_args(&out.cmd)
2165 .get(next_arg_idx)
2166 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
2167 .unwrap_or(false);
2168
2169 if should_preserve {
2170 } else {
2173 seen_double_dash = true;
2174 trace.record(argv, TokenRole::Separator);
2175
2176 let target = active_args(&out.cmd).iter().position(|arg| {
2182 arg.double_dash == SpecDoubleDashChoices::Required
2183 && !out.args.contains_key(arg)
2184 });
2185 if let Some(target) = target {
2186 if target > next_arg_idx {
2193 next_arg_idx = target;
2194 }
2195 }
2196 continue;
2197 }
2198 }
2199
2200 if enable_flags && w.starts_with("--") {
2202 grouped_flag = false;
2203 let split = w.split_once('=');
2207 let word = split.map(|(word, _)| word).unwrap_or(&w);
2208 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
2209 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
2210 let command_level = binding
2211 .as_ref()
2212 .map(|(_, level)| *level)
2213 .unwrap_or(out.cmds.len() - 1);
2214 parsed_flag_spellings
2215 .entry(Arc::as_ptr(f) as usize)
2216 .or_default()
2217 .insert(word.to_string());
2218 trace.record(
2221 argv,
2222 TokenRole::Flag {
2223 flag: Arc::clone(f),
2224 spelling: word.to_string(),
2225 negated: f.negate.as_deref() == Some(word),
2226 },
2227 );
2228 if f.action != crate::SpecFlagAction::Set {
2229 out.errors
2230 .push(render_action_err(spec, &out.cmd, f, word, custom_env));
2231 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2232 return Ok((out, overridden_flags));
2233 }
2234 apply_flag_overrides(
2235 f,
2236 &out.available_flags,
2237 &mut out.flags,
2238 &mut out.flag_awaiting_value,
2239 &mut overridden_flags,
2240 &mut out.overridden_flags,
2241 );
2242 if let Some(pending) = out.flag_awaiting_value.first() {
2243 out.errors.push(render_missing_flag_value(pending, &w));
2244 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2245 return Ok((out, overridden_flags));
2246 }
2247 if f.arg.is_some() {
2253 record_scalar_flag_occurrence(
2254 &out.cmds,
2255 f,
2256 command_level,
2257 None,
2258 &mut scalar_occurrences,
2259 &mut out.errors,
2260 );
2261 let f = Arc::clone(f);
2262 out.flag_awaiting_value.push(Arc::clone(&f));
2263 if let Some((_, val)) = split {
2268 let mut val = val.to_string();
2272 let should_return = bind_pending_flag_value(
2273 spec,
2274 &out.cmd,
2275 &mut out.errors,
2276 &mut out.flags,
2277 &mut out.flag_awaiting_value,
2278 &mut val,
2279 &mut input,
2280 custom_env,
2281 trace,
2282 argv,
2283 true,
2286 )?;
2287 if should_return {
2288 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2289 return Ok((out, overridden_flags));
2290 }
2291 }
2292 } else if f.count {
2293 let arr = out
2294 .flags
2295 .entry(Arc::clone(f))
2296 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2297 .try_as_multi_bool_mut()
2298 .unwrap();
2299 arr.push(true);
2300 } else {
2301 let negate = f.negate.clone().unwrap_or_default();
2302 let negated_form = word == negate;
2303 let value = if f.bool_value {
2304 match split.map(|(_, value)| value) {
2305 Some("true") => !negated_form,
2306 Some("false") => negated_form,
2307 Some(value) => {
2308 out.errors.push(UsageErr::InvalidValue {
2309 name: f.name.clone(),
2310 value: value.to_string(),
2311 reason: "expected `true` or `false`".to_string(),
2312 });
2313 continue;
2314 }
2315 None => !negated_form,
2316 }
2317 } else {
2318 !negated_form
2319 };
2320 record_scalar_flag_occurrence(
2325 &out.cmds,
2326 f,
2327 command_level,
2328 Some(!negated_form),
2329 &mut scalar_occurrences,
2330 &mut out.errors,
2331 );
2332 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2333 }
2334 continue;
2335 }
2336 if is_help_arg(spec, &out.cmd, &w) {
2337 out.errors
2338 .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2339 trace.record(
2340 argv,
2341 TokenRole::Builtin {
2342 spelling: w.clone(),
2343 },
2344 );
2345 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2346 return Ok((out, overridden_flags));
2347 }
2348 if is_version_arg(spec, &out.cmds, &w) {
2349 out.errors.push(render_version_err(spec, w.len() > 2));
2350 trace.record(
2351 argv,
2352 TokenRole::Builtin {
2353 spelling: w.clone(),
2354 },
2355 );
2356 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2357 return Ok((out, overridden_flags));
2358 }
2359 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2360 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2361 trace.close(&input);
2362 return Err(refused.into());
2363 }
2364 }
2365
2366 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
2373 let positional_negative_number = !declared_numeric_short
2374 && is_negative_number(&w)
2375 && active_args(&out.cmd)
2376 .get(next_arg_idx)
2377 .is_some_and(|arg| arg.allow_negative_numbers);
2378 if enable_flags
2379 && !grouped_flag
2380 && binding.is_none()
2383 && w.starts_with('-')
2384 && w.len() > 1
2385 && is_flag_like(&w)
2386 && !positional_negative_number
2387 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
2388 {
2389 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2392 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2393 trace.close(&input);
2394 return Err(refused.into());
2395 }
2396 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2397 let short = w.chars().nth(1).unwrap();
2398 if let Some(f) = binding
2399 .as_ref()
2400 .map(|(flag, _)| flag)
2401 .or_else(|| out.available_flags.get(&format!("-{short}")))
2402 {
2403 let command_level = binding
2404 .as_ref()
2405 .map(|(_, level)| *level)
2406 .unwrap_or(out.cmds.len() - 1);
2407 if f.action != crate::SpecFlagAction::Set {
2408 out.errors.push(render_action_err(
2409 spec,
2410 &out.cmd,
2411 f,
2412 &format!("-{short}"),
2413 custom_env,
2414 ));
2415 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2416 return Ok((out, overridden_flags));
2417 }
2418 parsed_flag_spellings
2419 .entry(Arc::as_ptr(f) as usize)
2420 .or_default()
2421 .insert(format!("-{short}"));
2422 trace.record(
2423 argv,
2424 TokenRole::Flag {
2425 flag: Arc::clone(f),
2426 spelling: format!("-{short}"),
2427 negated: false,
2429 },
2430 );
2431 apply_flag_overrides(
2432 f,
2433 &out.available_flags,
2434 &mut out.flags,
2435 &mut out.flag_awaiting_value,
2436 &mut overridden_flags,
2437 &mut out.overridden_flags,
2438 );
2439 if !attached_continuation && let Some(pending) = out.flag_awaiting_value.first() {
2440 out.errors.push(render_missing_flag_value(pending, &w));
2441 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2442 return Ok((out, overridden_flags));
2443 }
2444 let rest = &w[1 + short.len_utf8()..];
2445 if !rest.is_empty() {
2446 let mut tail = Token::new(format!("-{rest}"), argv);
2449 if f.arg.is_none() {
2450 tail.binding = default_bundle
2451 .as_ref()
2452 .filter(|bundle| bundle.argv == argv)
2453 .and_then(|bundle| bundle.binding(get_flag_key(&tail.word)));
2454 }
2455 input.push_front(tail);
2456 }
2457 grouped_flag = !rest.is_empty();
2461 if f.arg.is_some() {
2462 record_scalar_flag_occurrence(
2463 &out.cmds,
2464 f,
2465 command_level,
2466 None,
2467 &mut scalar_occurrences,
2468 &mut out.errors,
2469 );
2470 out.flag_awaiting_value.push(Arc::clone(f));
2471 } else if f.count {
2472 let arr = out
2473 .flags
2474 .entry(Arc::clone(f))
2475 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2476 .try_as_multi_bool_mut()
2477 .unwrap();
2478 arr.push(true);
2479 } else {
2480 let negate = f.negate.clone().unwrap_or_default();
2481 let value = w != negate;
2482 record_scalar_flag_occurrence(
2483 &out.cmds,
2484 f,
2485 command_level,
2486 Some(value),
2487 &mut scalar_occurrences,
2488 &mut out.errors,
2489 );
2490 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2491 }
2492 continue;
2493 }
2494 if let Some(err) = supplied_short(spec, &out.cmds, short, custom_env) {
2498 out.errors.push(err);
2499 trace.record(
2500 argv,
2501 TokenRole::Builtin {
2502 spelling: format!("-{short}"),
2503 },
2504 );
2505 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2506 return Ok((out, overridden_flags));
2507 }
2508 if is_help_arg(spec, &out.cmd, &w) {
2509 out.errors
2510 .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2511 trace.record(
2512 argv,
2513 TokenRole::Builtin {
2514 spelling: w.clone(),
2515 },
2516 );
2517 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2518 return Ok((out, overridden_flags));
2519 }
2520 if is_version_arg(spec, &out.cmds, &w) {
2521 out.errors.push(render_version_err(spec, w.len() > 2));
2522 trace.record(
2523 argv,
2524 TokenRole::Builtin {
2525 spelling: w.clone(),
2526 },
2527 );
2528 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2529 return Ok((out, overridden_flags));
2530 }
2531 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2532 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2533 trace.close(&input);
2534 return Err(refused.into());
2535 }
2536 if grouped_flag {
2537 grouped_flag = false;
2538 w.remove(0);
2539 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2543 w.remove(0);
2544 }
2545 }
2546 }
2547
2548 if enable_flags
2559 && !attached_continuation
2560 && !out.flag_awaiting_value.is_empty()
2561 && out
2562 .flag_awaiting_value
2563 .last()
2564 .is_some_and(|flag| flag.require_equals)
2565 {
2566 let flag = out.flag_awaiting_value.last().unwrap();
2567 let token = flag
2568 .long
2569 .first()
2570 .map(|l| format!("--{l}"))
2571 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2572 .unwrap_or_else(|| flag.name.clone());
2573 out.errors.push(UsageErr::InvalidFlag {
2574 token: token.clone(),
2575 reason: "requires an argument".to_string(),
2576 span: (0, 0).into(),
2577 input: format!("{token} {w}"),
2578 });
2579 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2580 return Ok((out, overridden_flags));
2581 }
2582 if enable_flags
2583 && !out.flag_awaiting_value.is_empty()
2584 && (attached_continuation
2585 || out
2586 .flag_awaiting_value
2587 .last()
2588 .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2589 {
2590 let should_return = bind_pending_flag_value(
2593 spec,
2594 &out.cmd,
2595 &mut out.errors,
2596 &mut out.flags,
2597 &mut out.flag_awaiting_value,
2598 &mut w,
2599 &mut input,
2600 custom_env,
2601 trace,
2602 argv,
2603 attached_continuation,
2604 )?;
2605 if should_return {
2606 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2607 return Ok((out, overridden_flags));
2608 }
2609 continue;
2610 }
2611
2612 if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2613 .then(|| match_sigil_arg_chain(&out.cmds, &w))
2614 .flatten()
2615 {
2616 if value.is_empty() {
2617 out.errors.push(UsageErr::InvalidValue {
2618 name: arg.name.clone(),
2619 value: w.clone(),
2620 reason: format!("expected a value after sigil {sigil:?}"),
2621 });
2622 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2623 return Ok((out, overridden_flags));
2624 }
2625 let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2626 let suppress_trailing_delimiter =
2627 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2628 let delimiter = if suppress_trailing_delimiter && trailing_value {
2629 None
2630 } else {
2631 arg.delimiter
2632 };
2633 let parts = match delimiter {
2634 Some(delimiter) => value
2635 .split(delimiter)
2636 .map(str::to_string)
2637 .collect::<Vec<_>>(),
2638 None => vec![value.to_string()],
2639 };
2640 let mut refused = false;
2641 for part in &parts {
2642 if validate_choices(
2643 spec,
2644 &out.cmd,
2645 &mut out.errors,
2646 ChoiceTarget::arg(arg),
2647 part,
2648 arg.choices.as_ref(),
2649 custom_env,
2650 )? {
2651 refused = true;
2652 break;
2653 }
2654 }
2655 if refused {
2656 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2657 return Ok((out, overridden_flags));
2658 }
2659 trace.record(
2660 argv,
2661 TokenRole::Sigil {
2662 arg: Arc::new(arg.clone()),
2663 sigil: sigil.to_string(),
2664 values: parts.clone(),
2665 },
2666 );
2667 let key = Arc::new(arg.clone());
2668 if arg.var {
2669 let arr = out
2670 .args
2671 .entry(key)
2672 .or_insert_with(|| ParseValue::MultiString(vec![]))
2673 .try_as_multi_string_mut()
2674 .unwrap();
2675 arr.extend(parts);
2676 } else {
2677 out.args.insert(key, ParseValue::String(value.to_string()));
2678 }
2679 if out.cmds.len() == 1 {
2680 root_positional_arg_found = true;
2681 }
2682 continue;
2683 }
2684
2685 if out.cmd.allow_missing_positional {
2686 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2687 while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2688 if current.required || out.args.contains_key(current) {
2689 break;
2690 }
2691 let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2692 .iter()
2693 .filter(|arg| arg.required && arg.sigil.is_none())
2694 .count();
2695 if required_after == 0 {
2696 break;
2697 }
2698 let remaining_values = 1 + input
2699 .iter()
2700 .filter(|token| {
2701 (!enable_flags || !is_flag_like(&token.word))
2702 && (!enable_flags
2703 || restart_seen
2704 || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2705 })
2706 .count();
2707 if remaining_values > required_after {
2708 break;
2709 }
2710 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2711 }
2712 }
2713
2714 if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2715 if arg.var
2716 && out.args.contains_key(arg)
2717 && arg.value_terminator.as_deref() == Some(w.as_str())
2718 {
2719 trace.record(
2720 argv,
2721 TokenRole::ValueTerminator {
2722 ends: arg.name.clone(),
2723 },
2724 );
2725 next_arg_idx += 1;
2726 continue;
2727 }
2728 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2732 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2733 trace.record(
2734 argv,
2735 TokenRole::Refused {
2736 reason: format!("{} only accepts words after `--`", arg.name),
2737 },
2738 );
2739 continue;
2743 }
2744 let trailing_value =
2749 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2750 let suppress_trailing_delimiter =
2751 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2752 let delimiter = if suppress_trailing_delimiter && trailing_value {
2753 None
2754 } else {
2755 arg.delimiter
2756 };
2757 let parts: Vec<String> = match delimiter {
2758 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2759 None => vec![w.clone()],
2760 };
2761 let mut refused = false;
2762 for part in &parts {
2763 if validate_choices(
2764 spec,
2765 &out.cmd,
2766 &mut out.errors,
2767 ChoiceTarget::arg(arg),
2768 part,
2769 arg.choices.as_ref(),
2770 custom_env,
2771 )? {
2772 refused = true;
2773 break;
2774 }
2775 }
2776 if refused {
2777 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2778 return Ok((out, overridden_flags));
2779 }
2780 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2785 enable_flags = false;
2786 }
2787 let unknown_flag = enable_flags
2792 && !positional_negative_number
2793 && is_flag_like(&w)
2794 && binding.is_none();
2795 trace.record(
2796 argv,
2797 if unknown_flag {
2798 TokenRole::UnknownFlag {
2799 bound_as: Some(Arc::new(arg.clone())),
2800 }
2801 } else {
2802 TokenRole::Arg {
2803 arg: Arc::new(arg.clone()),
2804 values: parts.clone(),
2805 }
2806 },
2807 );
2808 if out.cmds.len() == 1 {
2809 root_positional_arg_found = true;
2810 }
2811 if arg.var {
2812 let arr = out
2813 .args
2814 .entry(Arc::new(arg.clone()))
2815 .or_insert_with(|| ParseValue::MultiString(vec![]))
2816 .try_as_multi_string_mut()
2817 .unwrap();
2818 arr.extend(parts.iter().cloned());
2822 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2823 next_arg_idx += 1;
2824 }
2825 } else {
2826 out.args
2827 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2828 next_arg_idx += 1;
2829 }
2830 if out
2831 .cmd
2832 .clause
2833 .as_ref()
2834 .is_some_and(|clause| clause.separator.is_none())
2835 && next_arg_idx >= active_args(&out.cmd).len()
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 let Some(flag) = out.flag_awaiting_value.first() {
2844 let spelling = flag
2845 .long
2846 .first()
2847 .map(|long| format!("--{long}"))
2848 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
2849 .unwrap_or_else(|| flag.name.clone());
2850 return Err(UsageErr::InvalidFlag {
2851 token: spelling.clone(),
2852 reason: "requires an argument".to_string(),
2853 span: (0, spelling.len()).into(),
2854 input: spelling,
2855 }
2856 .into());
2857 }
2858 let name = out.cmd.clause.as_ref().unwrap().name.clone();
2859 finalize_current_clause(&mut out);
2860 out.arg_origins.clear();
2861 trace.record(argv, TokenRole::ClauseSeparator { name });
2862 next_arg_idx = 0;
2863 out.flag_awaiting_value.clear();
2864 reset_clause_scalar_occurrences(&out, &mut scalar_occurrences);
2865 enable_flags = true;
2866 seen_double_dash = false;
2867 }
2868 continue;
2869 }
2870 if is_help_arg(spec, &out.cmd, &w) {
2871 out.errors
2872 .push(render_help_err(spec, &out.cmd, w.len() > 2, custom_env));
2873 trace.record(
2874 argv,
2875 TokenRole::Builtin {
2876 spelling: w.clone(),
2877 },
2878 );
2879 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2880 return Ok((out, overridden_flags));
2881 }
2882 if is_version_arg(spec, &out.cmds, &w) {
2883 out.errors.push(render_version_err(spec, w.len() > 2));
2884 trace.record(
2885 argv,
2886 TokenRole::Builtin {
2887 spelling: w.clone(),
2888 },
2889 );
2890 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2891 return Ok((out, overridden_flags));
2892 }
2893 trace.record(
2894 argv,
2895 TokenRole::Refused {
2896 reason: "no declaration takes this word".to_string(),
2897 },
2898 );
2899 trace.close(&input);
2900 bail!("unexpected word: {w}");
2901 }
2902
2903 if spec.default_subcommand_on_empty
2907 && spec.default_subcommand.is_some()
2908 && out.cmds.len() == 1
2909 && !root_positional_arg_found
2910 && !seen_double_dash
2911 && !root_clause_separator_seen
2912 && out.errors.is_empty()
2913 {
2914 while try_bind_default_missing(
2915 &mut out.flags,
2916 &mut out.flag_awaiting_value,
2917 custom_env,
2918 &mut out.flag_origins,
2919 )? {}
2920 if !out.flag_awaiting_value.is_empty() {
2921 } else if let Some(subcommand) = out
2924 .cmd
2925 .find_subcommand(spec.default_subcommand.as_deref().unwrap())
2926 {
2927 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
2928 bail!(
2929 "subcommand '{}' cannot be used with arguments on its parent command",
2930 subcommand.name
2931 );
2932 }
2933 let mut subcommand = subcommand.clone();
2934 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
2935 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
2936 out.merge_command_flags(&subcommand, crossing_mount);
2937 out.cmds.push(subcommand.clone());
2938 out.cmd = subcommand;
2939 command_has_argv = false;
2940 prefix_flags.clear();
2941 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
2942 }
2943 }
2944
2945 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2946 if validate_clauses {
2947 validate_clause_relationships(&mut out, &overridden_flags, custom_env, None);
2948 }
2949 let clause_flag_names = out
2950 .cmd
2951 .clause
2952 .iter()
2953 .flat_map(|clause| &clause.flags)
2954 .map(|flag| flag.name.clone())
2955 .collect::<HashSet<_>>();
2956
2957 let flag_was_parsed =
2962 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2963
2964 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2972 let declared: HashSet<String> = out
2973 .cmd
2974 .flags
2975 .iter()
2976 .filter(|declared| declared.name == flag.name)
2977 .flat_map(flag_keys)
2978 .collect();
2979 let speaks_for_this_flag = declared.iter().any(|spelling| {
2989 out.available_flags
2990 .get(spelling)
2991 .is_some_and(|available| Arc::ptr_eq(available, flag))
2992 });
2993 if speaks_for_this_flag {
2994 declared
2995 } else {
2996 HashSet::new()
2997 }
2998 };
2999
3000 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
3008 let child = child_spellings(flag);
3009 let child_exclusive = !child.is_empty()
3010 && out
3011 .cmd
3012 .flags
3013 .iter()
3014 .any(|declared| declared.name == flag.name && declared.exclusive);
3015 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
3016 Some(spellings) => (
3017 child_exclusive && spellings.iter().any(|s| child.contains(s)),
3018 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
3019 ),
3020 None => (child_exclusive, flag.exclusive && child.is_empty()),
3024 }
3025 };
3026
3027 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
3028 let (child, ancestor) = exclusivity_in_play(flag);
3029 child || ancestor
3030 };
3031
3032 let exclusive_present = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3037 .filter(|flag| !clause_flag_names.contains(&flag.name))
3038 .any(|flag| {
3039 exclusive_occurrence(flag)
3040 && !overridden_flags.contains(&flag.name)
3041 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
3042 });
3043 let requirements_apply = |command_index: usize| {
3044 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
3045 };
3046
3047 if out.cmd.arg_required_else_help && !command_has_argv {
3048 out.errors
3049 .push(render_help_err(spec, &out.cmd, false, custom_env));
3050 }
3051
3052 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
3058 let mut names: Vec<&str> = out
3059 .cmd
3060 .subcommands
3061 .iter()
3062 .filter(|(name, sub)| sub.name == **name && !sub.hide)
3065 .map(|(name, _)| name.as_str())
3066 .collect();
3067 names.sort_unstable();
3068 out.errors.push(UsageErr::MissingSubcommand(
3069 out.cmd.name.clone(),
3070 names.join(", "),
3071 ));
3072 }
3073
3074 if !exclusive_present {
3077 for arg in out
3078 .cmds
3079 .iter()
3080 .enumerate()
3081 .filter(|(index, _)| requirements_apply(*index))
3082 .flat_map(|(_, cmd)| &cmd.args)
3083 {
3084 if out.args.contains_key(arg) {
3085 continue;
3086 }
3087 if double_dash_violations.contains(&arg.name) {
3089 continue;
3090 }
3091 let required_if = arg.required_if.iter().any(|selector| {
3092 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3093 });
3094 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
3095 selector_explicit_has_value(
3096 &condition.selector,
3097 &condition.value,
3098 &out,
3099 &overridden_flags,
3100 custom_env,
3101 )
3102 });
3103 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3104 && arg.required_if_eq_all.iter().all(|condition| {
3105 selector_explicit_has_value(
3106 &condition.selector,
3107 &condition.value,
3108 &out,
3109 &overridden_flags,
3110 custom_env,
3111 )
3112 });
3113 let unless_any = arg.required_unless.iter().any(|selector| {
3114 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3115 });
3116 let unless_all = !arg.required_unless_all.is_empty()
3117 && arg.required_unless_all.iter().all(|selector| {
3118 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3119 });
3120 let required_unless = !(unless_any
3121 || unless_all
3122 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3123 if (arg.required
3124 || required_if
3125 || required_if_eq
3126 || required_if_eq_all
3127 || required_unless)
3128 && arg.default.is_empty()
3129 {
3130 let has_env = arg
3132 .env
3133 .as_ref()
3134 .is_some_and(|env_var| env_contains(custom_env, env_var));
3135 if !has_env {
3136 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
3137 }
3138 }
3139 }
3140 }
3141
3142 for flag in unique_flags(out.available_flags.values())
3154 .filter(|flag| !clause_flag_names.contains(&flag.name))
3155 {
3156 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
3157 if !given || overridden_flags.contains(&flag.name) {
3158 continue;
3159 }
3160 for other in &flag.conflicts {
3161 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3162 out.errors.push(UsageErr::InvalidFlag {
3163 token: format!("--{}", flag.name),
3164 reason: format!("conflicts with {other}"),
3165 span: (0, 0).into(),
3166 input: format!("--{} {other}", flag.name),
3167 });
3168 }
3169 }
3170 let owner = out
3181 .cmds
3182 .iter()
3183 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3184 .unwrap_or(out.cmds.len() - 1);
3185 if !exclusive_present && requirements_apply(owner) {
3186 for other in &flag.requires {
3187 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3188 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3189 if other.starts_with('-') {
3190 out.errors.push(UsageErr::MissingFlag(name));
3191 } else {
3192 out.errors.push(UsageErr::MissingArg(name));
3193 }
3194 }
3195 }
3196 for condition in &flag.requires_if {
3197 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
3198 && !selector_is_satisfied(
3199 &condition.requires,
3200 &out,
3201 &overridden_flags,
3202 custom_env,
3203 )
3204 {
3205 let name = selector_flag_name(&condition.requires, &out)
3206 .unwrap_or_else(|| condition.requires.clone());
3207 out.errors.push(UsageErr::MissingFlag(name));
3208 }
3209 }
3210 }
3211 }
3212
3213 for (command_index, arg) in out
3216 .cmds
3217 .iter()
3218 .enumerate()
3219 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
3220 {
3221 let given = arg_is_explicit(arg, &out, custom_env);
3222 if !given {
3223 continue;
3224 }
3225 for other in &arg.conflicts {
3226 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
3227 out.errors.push(UsageErr::InvalidFlag {
3228 token: arg.name.clone(),
3229 reason: format!("conflicts with {other}"),
3230 span: (0, 0).into(),
3231 input: format!("{} {other}", arg.name),
3232 });
3233 }
3234 }
3235 if !exclusive_present && requirements_apply(command_index) {
3236 for other in &arg.requires {
3237 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
3238 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
3239 if other.starts_with('-') {
3240 out.errors.push(UsageErr::MissingFlag(name));
3241 } else {
3242 out.errors.push(UsageErr::MissingArg(name));
3243 }
3244 }
3245 }
3246 }
3247 }
3248
3249 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys()))
3259 .filter(|flag| !clause_flag_names.contains(&flag.name))
3260 {
3261 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
3266 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
3267 continue;
3268 }
3269 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
3270 .filter(|other| !clause_flag_names.contains(&other.name))
3271 .find(|other| {
3272 !Arc::ptr_eq(other, flag)
3273 && !overridden_flags.contains(&other.name)
3274 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
3275 })
3276 .map(|other| format!("--{}", other.name));
3277 let other_arg = active_args(&out.cmd).iter().find(|arg| {
3278 out.args.keys().any(|given| given.name == arg.name)
3279 || out
3280 .clauses
3281 .values()
3282 .flatten()
3283 .any(|instance| instance.keys().any(|given| given.name == arg.name))
3284 || arg
3285 .env
3286 .as_ref()
3287 .is_some_and(|env| env_contains(custom_env, env))
3288 });
3289 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
3295 let selected_subcommand =
3296 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
3297 let other = other_flag
3298 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
3299 .or(selected_subcommand);
3300 if let Some(other) = other {
3301 out.errors.push(UsageErr::InvalidFlag {
3302 token: format!("--{}", flag.name),
3303 reason: format!("must be given on its own, and {other} was given too"),
3304 span: (0, 0).into(),
3305 input: format!("--{} {other}", flag.name),
3306 });
3307 }
3308 }
3309
3310 let mut group_errors: Vec<UsageErr> = Vec::new();
3319 for (command_index, group) in out
3320 .cmds
3321 .iter()
3322 .enumerate()
3323 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
3324 {
3325 let mut given: Vec<&str> = Vec::new();
3331 let mut seen: Vec<String> = Vec::new();
3332 for selector in &group.members {
3333 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
3334 continue;
3335 }
3336 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
3337 if seen.contains(&name) {
3338 continue;
3339 }
3340 seen.push(name);
3341 given.push(selector.as_str());
3342 }
3343 if !group.multiple && given.len() > 1 {
3344 group_errors.push(UsageErr::InvalidFlag {
3345 token: given[1].to_string(),
3346 reason: format!("cannot be used with {} in group {}", given[0], group.name),
3347 span: (0, 0).into(),
3348 input: format!("{} {}", given[0], given[1]),
3349 });
3350 }
3351 let satisfied = group
3356 .members
3357 .iter()
3358 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
3359 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
3360 group_errors.push(UsageErr::MissingGroup {
3364 group: group.name.clone(),
3365 members: group.members.join(", "),
3366 });
3367 }
3368 }
3369 out.errors.extend(group_errors);
3370
3371 if !exclusive_present {
3372 for flag in unique_flags(out.available_flags.values())
3373 .filter(|flag| !clause_flag_names.contains(&flag.name))
3374 {
3375 let owner = out
3376 .cmds
3377 .iter()
3378 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
3379 .unwrap_or(out.cmds.len() - 1);
3380 if !requirements_apply(owner) {
3381 continue;
3382 }
3383 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
3384 continue;
3385 }
3386 let has_default =
3387 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
3388 let has_env = flag_has_env(flag, custom_env);
3389 let required_if = flag.required_if.iter().any(|selector| {
3390 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3391 });
3392 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
3393 selector_explicit_has_value(
3394 &condition.selector,
3395 &condition.value,
3396 &out,
3397 &overridden_flags,
3398 custom_env,
3399 )
3400 });
3401 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
3402 && flag.required_if_eq_all.iter().all(|condition| {
3403 selector_explicit_has_value(
3404 &condition.selector,
3405 &condition.value,
3406 &out,
3407 &overridden_flags,
3408 custom_env,
3409 )
3410 });
3411 let unless_any = flag.required_unless.iter().any(|selector| {
3412 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3413 });
3414 let unless_all = !flag.required_unless_all.is_empty()
3415 && flag.required_unless_all.iter().all(|selector| {
3416 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
3417 });
3418 let required_unless = !(unless_any
3419 || unless_all
3420 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
3421 if (flag.required
3422 || required_if
3423 || required_if_eq
3424 || required_if_eq_all
3425 || required_unless)
3426 && !has_default
3427 && !has_env
3428 {
3429 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
3430 }
3431 }
3432 }
3433
3434 for (arg, value) in &out.args {
3436 if arg.var
3437 && let ParseValue::MultiString(values) = value
3438 {
3439 if let Some(min) = arg.var_min
3440 && values.len() < min
3441 {
3442 out.errors.push(UsageErr::VarArgTooFew {
3443 name: arg.name.clone(),
3444 min,
3445 got: values.len(),
3446 });
3447 }
3448 if let Some(max) = arg.var_max
3449 && values.len() > max
3450 {
3451 out.errors.push(UsageErr::VarArgTooMany {
3452 name: arg.name.clone(),
3453 max,
3454 got: values.len(),
3455 });
3456 }
3457 }
3458 }
3459
3460 for flag in unique_flags(out.available_flags.values())
3464 .filter(|flag| !clause_flag_names.contains(&flag.name))
3465 {
3466 if flag.var {
3467 let bound = match out.flags.get(flag) {
3468 Some(ParseValue::MultiString(values)) => values.len(),
3469 Some(ParseValue::MultiBool(values)) => values.len(),
3470 Some(_) => 1,
3471 None => 0,
3472 };
3473 let pending = out
3477 .flag_awaiting_value
3478 .iter()
3479 .filter(|pending| {
3480 Arc::ptr_eq(pending, flag)
3481 && (pending.value_optional || pending.default_missing.is_some())
3482 })
3483 .count();
3484 let count = bound + pending;
3485 if count == 0 {
3486 continue;
3487 }
3488 if let Some(min) = flag.var_min
3489 && count < min
3490 {
3491 out.errors.push(UsageErr::VarFlagTooFew {
3492 name: flag.name.clone(),
3493 min,
3494 got: count,
3495 });
3496 }
3497 if let Some(max) = flag.var_max
3498 && count > max
3499 {
3500 out.errors.push(UsageErr::VarFlagTooMany {
3501 name: flag.name.clone(),
3502 max,
3503 got: count,
3504 });
3505 }
3506 }
3507 }
3508
3509 Ok((out, overridden_flags))
3510}
3511
3512fn validate_expression(
3513 name: &str,
3514 expression: Option<&str>,
3515 message: Option<&str>,
3516 parsed: &ParseValue,
3517 errors: &mut Vec<UsageErr>,
3518) {
3519 let Some(expression) = expression else {
3520 return;
3521 };
3522 #[cfg(not(feature = "validation"))]
3523 let _ = expression;
3524 let values: &[String] = match parsed {
3525 ParseValue::String(value) => std::slice::from_ref(value),
3526 ParseValue::MultiString(values) => values,
3527 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3528 };
3529 #[cfg(feature = "validation")]
3530 for value in values {
3531 let reason = match usage_validation::validate(expression, value) {
3532 Ok(true) => continue,
3533 Ok(false) => message
3534 .unwrap_or("does not satisfy the validation expression")
3535 .to_string(),
3536 Err(error) => format!("validation expression failed: {error}"),
3537 };
3538 errors.push(UsageErr::InvalidValue {
3539 name: name.to_string(),
3540 value: value.clone(),
3541 reason,
3542 });
3543 break;
3544 }
3545 #[cfg(not(feature = "validation"))]
3546 if let Some(value) = values.first() {
3547 let _ = message;
3548 errors.push(UsageErr::InvalidValue {
3549 name: name.to_string(),
3550 value: value.clone(),
3551 reason: "expression validation requires the `validation` feature".to_string(),
3552 });
3553 }
3554}
3555
3556#[cfg(all(test, not(feature = "validation")))]
3557mod optional_validation_tests {
3558 use crate::{Spec, parse};
3559
3560 #[test]
3561 fn validation_declarations_require_the_opt_in_runtime_feature() {
3562 let spec: Spec = r#"
3563name "ex"
3564bin "ex"
3565arg "<port>" validate="int(value) > 0"
3566 "#
3567 .parse()
3568 .unwrap();
3569 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3570 assert!(
3571 error
3572 .to_string()
3573 .contains("requires the `validation` feature"),
3574 "{error:?}"
3575 );
3576 }
3577}
3578
3579fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3580 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3581}
3582
3583fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3584 overrider
3585 .overrides
3586 .iter()
3587 .any(|selector| flag_matches_selector(overridden, selector))
3588}
3589
3590fn apply_prefix_flag_overrides(
3591 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3592 flag: Arc<SpecFlag>,
3593) {
3594 prefix_flags
3595 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3596}
3597
3598fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3599 prefix_flags
3600 .iter()
3601 .flat_map(|(_, words)| words.iter().cloned())
3602 .collect()
3603}
3604
3605fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3606 match custom_env {
3607 Some(env) => env.contains_key(env_var),
3608 None => std::env::var(env_var).is_ok(),
3609 }
3610}
3611
3612fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3613 flag.env_names()
3614 .any(|env_var| env_contains(custom_env, env_var))
3615}
3616
3617fn fallback_is_true(value: &str) -> bool {
3618 matches!(value, "1" | "true" | "True" | "TRUE")
3619}
3620
3621fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3622 match delimiter {
3623 Some(delimiter) => values
3624 .iter()
3625 .flat_map(|value| value.split(delimiter).map(str::to_string))
3626 .collect(),
3627 None => values.to_vec(),
3628 }
3629}
3630
3631fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3632 if let Some(min) = arg.var_min
3633 && count < min
3634 {
3635 errors.push(UsageErr::VarArgTooFew {
3636 name: arg.name.clone(),
3637 min,
3638 got: count,
3639 });
3640 }
3641 if let Some(max) = arg.var_max
3642 && count > max
3643 {
3644 errors.push(UsageErr::VarArgTooMany {
3645 name: arg.name.clone(),
3646 max,
3647 got: count,
3648 });
3649 }
3650}
3651
3652fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3653 if let Some(min) = flag.var_min
3654 && count < min
3655 {
3656 errors.push(UsageErr::VarFlagTooFew {
3657 name: flag.name.clone(),
3658 min,
3659 got: count,
3660 });
3661 }
3662 if let Some(max) = flag.var_max
3663 && count > max
3664 {
3665 errors.push(UsageErr::VarFlagTooMany {
3666 name: flag.name.clone(),
3667 max,
3668 got: count,
3669 });
3670 }
3671}
3672
3673fn validate_flag_arg_fallback_count(
3674 flag: &SpecFlag,
3675 arg: &SpecArg,
3676 count: usize,
3677 errors: &mut Vec<UsageErr>,
3678) {
3679 if let Some(min) = arg.var_min
3680 && count < min
3681 {
3682 errors.push(UsageErr::VarFlagTooFew {
3683 name: flag.name.clone(),
3684 min,
3685 got: count,
3686 });
3687 }
3688 if let Some(max) = arg.var_max
3689 && count > max
3690 {
3691 errors.push(UsageErr::VarFlagTooMany {
3692 name: flag.name.clone(),
3693 max,
3694 got: count,
3695 });
3696 }
3697}
3698
3699fn bind_flag_fallback(
3702 flag: &Arc<SpecFlag>,
3703 values: &[String],
3704 out: &mut ParseOutput,
3705 custom_env: Option<&HashMap<String, String>>,
3706 origin: ValueOrigin,
3707) -> Result<(), miette::Error> {
3708 let Some(value) = flag_fallback_value(flag, values, &mut out.errors, custom_env)? else {
3709 return Ok(());
3710 };
3711 out.flags.insert(Arc::clone(flag), value);
3712 out.flag_origins
3713 .entry(Arc::clone(flag))
3714 .or_default()
3715 .push(origin);
3716 Ok(())
3717}
3718
3719fn flag_fallback_value(
3720 flag: &SpecFlag,
3721 values: &[String],
3722 errors: &mut Vec<UsageErr>,
3723 custom_env: Option<&HashMap<String, String>>,
3724) -> Result<Option<ParseValue>, miette::Error> {
3725 if values.is_empty() {
3726 return Ok(None);
3727 }
3728 if let Some(arg) = flag.arg.as_ref() {
3729 let values = split_fallback_values(values, arg.delimiter);
3730 if flag.var || arg.var {
3731 if flag.var {
3732 validate_flag_fallback_count(flag, values.len(), errors);
3733 }
3734 if arg.var {
3735 validate_flag_arg_fallback_count(flag, arg, values.len(), errors);
3736 }
3737 validate_choice_values(
3738 ChoiceTarget::option(flag),
3739 &values,
3740 arg.choices.as_ref(),
3741 custom_env,
3742 )?;
3743 Ok(Some(ParseValue::MultiString(values)))
3744 } else {
3745 let value = values.into_iter().next().unwrap_or_default();
3746 validate_choice_value(
3747 ChoiceTarget::option(flag),
3748 &value,
3749 arg.choices.as_ref(),
3750 custom_env,
3751 )?;
3752 Ok(Some(ParseValue::String(value)))
3753 }
3754 } else if flag.var {
3755 validate_flag_fallback_count(flag, values.len(), errors);
3756 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3757 Ok(Some(ParseValue::MultiBool(bools)))
3758 } else {
3759 Ok(Some(ParseValue::Bool(fallback_is_true(&values[0]))))
3760 }
3761}
3762
3763fn apply_clause_flag_fallbacks(
3769 out: &mut ParseOutput,
3770 overridden_flags: &HashSet<String>,
3771 custom_env: Option<&HashMap<String, String>>,
3772) -> Result<Option<Vec<HashSet<String>>>, miette::Error> {
3773 let Some(clause) = out.cmd.clause.clone() else {
3774 return Ok(None);
3775 };
3776 let Some(positional_instances) = out.clauses.get(&clause.name) else {
3777 return Ok(None);
3778 };
3779 let positional_instances = positional_instances.clone();
3780 let mut flag_instances = out
3781 .clause_flags
3782 .shift_remove(&clause.name)
3783 .unwrap_or_default();
3784 flag_instances.resize_with(positional_instances.len(), IndexMap::new);
3785 let get_env = |key: &str| -> Option<String> {
3786 custom_env
3787 .and_then(|values| values.get(key).cloned())
3788 .or_else(|| {
3789 custom_env
3790 .is_none()
3791 .then(|| std::env::var(key).ok())
3792 .flatten()
3793 })
3794 };
3795 let mut explicit_instances = Vec::with_capacity(flag_instances.len());
3796
3797 for (instance_index, scoped) in flag_instances.iter_mut().enumerate() {
3798 let positional = &positional_instances[instance_index];
3799 let mut explicit = scoped
3800 .keys()
3801 .map(|flag| flag.name.clone())
3802 .collect::<HashSet<_>>();
3803
3804 for declared in &clause.flags {
3807 let flag = Arc::new(declared.clone());
3808 if scoped.contains_key(&flag) {
3809 continue;
3810 }
3811 let Some((env_name, env_value)) = first_set_env(declared.env_names(), &get_env) else {
3812 continue;
3813 };
3814 if let Some(warning) = flag_deprecation(declared) {
3815 out.warnings.push(warning);
3816 }
3817 if flag_env_is_deprecated(declared, env_name) {
3818 out.warnings
3819 .push(Warning::env(env_name, flag_current_env(declared)));
3820 }
3821 if let Some(value) = flag_fallback_value(
3822 declared,
3823 std::slice::from_ref(&env_value),
3824 &mut out.errors,
3825 custom_env,
3826 )? {
3827 scoped.insert(flag, value);
3828 explicit.insert(declared.name.clone());
3829 }
3830 }
3831
3832 let condition_matches =
3833 |condition: &crate::SpecDefaultIf| {
3834 if let Some(flag) = clause
3835 .flags
3836 .iter()
3837 .find(|flag| flag_matches_selector(flag, &condition.selector))
3838 {
3839 if !explicit.contains(&flag.name) {
3840 return false;
3841 }
3842 return condition.when.as_ref().is_none_or(|expected| {
3843 scoped
3844 .iter()
3845 .find(|(present, _)| present.name == flag.name)
3846 .is_some_and(|(_, value)| parse_value_has(value, expected))
3847 });
3848 }
3849 if let Some(arg) = clause.args.iter().find(|arg| {
3850 !condition.selector.starts_with('-') && arg.name == condition.selector
3851 }) {
3852 return positional.get(arg).is_some_and(|value| {
3853 condition
3854 .when
3855 .as_ref()
3856 .is_none_or(|expected| parse_value_has(value, expected))
3857 });
3858 }
3859 let command_flag = out
3860 .available_flags
3861 .values()
3862 .chain(out.flags.keys())
3863 .filter(|flag| !is_clause_scoped_flag(out, flag))
3864 .find(|flag| flag_matches_selector(flag, &condition.selector));
3865 command_flag.is_some_and(|flag| {
3866 !overridden_flags.contains(&flag.name)
3867 && command_flag_has_explicit_source(flag, out)
3868 && condition.when.as_ref().is_none_or(|expected| {
3869 out.flags
3870 .get(flag)
3871 .is_some_and(|value| parse_value_has(value, expected))
3872 })
3873 })
3874 };
3875
3876 let conditional = clause
3877 .flags
3878 .iter()
3879 .filter(|flag| !scoped.keys().any(|present| present.name == flag.name))
3880 .filter_map(|flag| {
3881 flag.default_if
3882 .iter()
3883 .find(|condition| condition_matches(condition))
3884 .map(|condition| (flag.clone(), condition.value.clone()))
3885 })
3886 .collect::<Vec<_>>();
3887 for (declared, value) in conditional {
3888 if let Some(value) = flag_fallback_value(
3889 &declared,
3890 std::slice::from_ref(&value),
3891 &mut out.errors,
3892 custom_env,
3893 )? {
3894 scoped.insert(Arc::new(declared), value);
3895 }
3896 }
3897
3898 for declared in &clause.flags {
3899 let flag = Arc::new(declared.clone());
3900 if scoped.contains_key(&flag) {
3901 continue;
3902 }
3903 let values = if !declared.default.is_empty() {
3904 &declared.default
3905 } else if let Some(arg) = declared.arg.as_ref().filter(|arg| !arg.default.is_empty()) {
3906 &arg.default
3907 } else {
3908 continue;
3909 };
3910 if let Some(value) = flag_fallback_value(declared, values, &mut out.errors, custom_env)?
3911 {
3912 scoped.insert(flag, value);
3913 }
3914 }
3915 explicit_instances.push(explicit);
3916 }
3917
3918 out.clause_flags.insert(clause.name, flag_instances);
3919 Ok(Some(explicit_instances))
3920}
3921
3922fn command_flag_has_explicit_source(flag: &SpecFlag, out: &ParseOutput) -> bool {
3923 out.flags.contains_key(flag)
3924 && out.flag_origins.get(flag).is_none_or(|origins| {
3925 origins
3926 .iter()
3927 .any(|origin| matches!(origin, ValueOrigin::DefaultMissing | ValueOrigin::Env(_)))
3928 })
3929}
3930
3931fn default_if_condition_matches(
3932 condition: &crate::SpecDefaultIf,
3933 out: &ParseOutput,
3934 overridden_flags: &HashSet<String>,
3935 custom_env: Option<&HashMap<String, String>>,
3936) -> bool {
3937 match &condition.when {
3938 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3939 Some(when) => {
3940 let Some(flag) = out
3941 .available_flags
3942 .values()
3943 .chain(out.flags.keys())
3944 .find(|flag| flag_matches_selector(flag, &condition.selector))
3945 else {
3946 return false;
3947 };
3948 if overridden_flags.contains(&flag.name) {
3949 return false;
3950 }
3951 explicit_flag_has_value(flag, when, out, custom_env)
3952 }
3953 }
3954}
3955
3956fn explicit_flag_has_value(
3962 flag: &SpecFlag,
3963 expected: &str,
3964 out: &ParseOutput,
3965 custom_env: Option<&HashMap<String, String>>,
3966) -> bool {
3967 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3968 ParseValue::Bool(value) => value.to_string() == expected,
3969 ParseValue::String(value) => value == expected,
3970 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3971 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3972 });
3973 if out.flags.contains_key(flag) {
3974 return parsed_matches;
3975 }
3976
3977 let value = flag.env_names().find_map(|env| match custom_env {
3978 Some(values) => values.get(env).cloned(),
3979 None => std::env::var(env).ok(),
3980 });
3981 value.is_some_and(
3982 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3983 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3984 None if flag.arg.is_none() => {
3985 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3986 }
3987 None => value == expected,
3988 },
3989 )
3990}
3991
3992fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3993 match value {
3994 ParseValue::Bool(value) => value.to_string() == expected,
3995 ParseValue::String(value) => value == expected,
3996 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3997 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3998 }
3999}
4000
4001fn is_clause_scoped_flag(out: &ParseOutput, flag: &SpecFlag) -> bool {
4004 out.cmd
4005 .clause
4006 .as_ref()
4007 .is_some_and(|clause| clause.flags.iter().any(|scoped| scoped.name == flag.name))
4008}
4009
4010fn selected_clause_flag<'a>(out: &'a ParseOutput, selector: &str) -> Option<&'a SpecFlag> {
4011 out.cmd
4012 .clause
4013 .as_ref()?
4014 .flags
4015 .iter()
4016 .find(|flag| flag_matches_selector(flag, selector))
4017}
4018
4019fn clause_flag_is_explicit(out: &ParseOutput, flag: &SpecFlag) -> bool {
4020 out.clause_flags
4021 .values()
4022 .flatten()
4023 .any(|instance| instance.keys().any(|present| present.name == flag.name))
4024 || out.flags.keys().any(|present| present.name == flag.name)
4025}
4026
4027fn clause_flag_has_value(out: &ParseOutput, flag: &SpecFlag, expected: &str) -> bool {
4028 out.clause_flags.values().flatten().any(|instance| {
4029 instance
4030 .iter()
4031 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
4032 }) || out
4033 .flags
4034 .iter()
4035 .any(|(present, value)| present.name == flag.name && parse_value_has(value, expected))
4036}
4037
4038fn validate_clause_relationships(
4039 out: &mut ParseOutput,
4040 overridden_flags: &HashSet<String>,
4041 custom_env: Option<&HashMap<String, String>>,
4042 explicit_instances: Option<&[HashSet<String>]>,
4043) {
4044 let Some(clause) = out.cmd.clause.as_ref() else {
4045 return;
4046 };
4047 let Some(instances) = out.clauses.get(&clause.name) else {
4048 return;
4049 };
4050 let command_flag_is_explicit = |selector: &str| {
4051 out.available_flags
4052 .values()
4053 .chain(out.flags.keys())
4054 .filter(|flag| !is_clause_scoped_flag(out, flag))
4055 .any(|flag| {
4056 flag_matches_selector(flag, selector)
4057 && !overridden_flags.contains(&flag.name)
4058 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4059 })
4060 };
4061 let command_flag_matches_value = |selector: &str, expected: &str| {
4062 out.available_flags
4063 .values()
4064 .chain(out.flags.keys())
4065 .filter(|flag| !is_clause_scoped_flag(out, flag))
4066 .find(|flag| flag_matches_selector(flag, selector))
4067 .is_some_and(|flag| {
4068 !overridden_flags.contains(&flag.name)
4069 && explicit_flag_has_value(flag, expected, out, custom_env)
4070 })
4071 };
4072 let command_flag_is_satisfied = |selector: &str| {
4073 out.available_flags
4074 .values()
4075 .chain(out.flags.keys())
4076 .filter(|flag| !is_clause_scoped_flag(out, flag))
4077 .any(|flag| flag_matches_selector(flag, selector))
4078 && selector_is_satisfied(selector, out, overridden_flags, custom_env)
4079 };
4080 let mut errors = Vec::new();
4081 for (instance_index, instance) in instances.iter().enumerate() {
4082 let explicit = explicit_instances.and_then(|instances| instances.get(instance_index));
4083 let scoped = out
4084 .clause_flags
4085 .get(&clause.name)
4086 .and_then(|instances| instances.get(instance_index));
4087 let scoped_flag = |selector: &str| {
4088 clause
4089 .flags
4090 .iter()
4091 .find(|flag| flag_matches_selector(flag, selector))
4092 };
4093 let scoped_value = |flag: &SpecFlag| {
4094 scoped.and_then(|values| {
4095 values
4096 .iter()
4097 .find(|(present, _)| present.name == flag.name)
4098 .map(|(_, value)| value)
4099 })
4100 };
4101 let arg_is_explicit = |selector: &str| {
4102 instance
4103 .keys()
4104 .any(|arg| !selector.starts_with('-') && arg.name == selector)
4105 };
4106 let selector_is_explicit = |selector: &str| {
4107 scoped_flag(selector).is_some_and(|flag| {
4108 explicit
4109 .map(|given| given.contains(&flag.name))
4110 .unwrap_or_else(|| scoped_value(flag).is_some())
4111 }) || command_flag_is_explicit(selector)
4112 || arg_is_explicit(selector)
4113 };
4114 let selector_has_value = |selector: &str, expected: &str| {
4115 scoped_flag(selector)
4116 .filter(|flag| {
4117 explicit
4118 .map(|given| given.contains(&flag.name))
4119 .unwrap_or_else(|| scoped_value(flag).is_some())
4120 })
4121 .and_then(scoped_value)
4122 .is_some_and(|value| parse_value_has(value, expected))
4123 || command_flag_matches_value(selector, expected)
4124 || instance.iter().any(|(arg, value)| {
4125 !selector.starts_with('-')
4126 && arg.name == selector
4127 && parse_value_has(value, expected)
4128 })
4129 };
4130 let selector_is_satisfied = |selector: &str| {
4131 scoped_flag(selector).is_some_and(|flag| scoped_value(flag).is_some())
4132 || command_flag_is_satisfied(selector)
4133 || arg_is_explicit(selector)
4134 };
4135 let selector_name = |selector: &str| {
4136 scoped_flag(selector)
4137 .map(|flag| flag.name.clone())
4138 .or_else(|| selector_flag_name(selector, out))
4139 .unwrap_or_else(|| selector.to_string())
4140 };
4141
4142 for flag in &clause.flags {
4143 let value = scoped_value(flag);
4144 if value.is_none() {
4145 let required_if = flag
4146 .required_if
4147 .iter()
4148 .any(|selector| selector_is_explicit(selector));
4149 let required_if_eq = flag
4150 .required_if_eq
4151 .iter()
4152 .any(|condition| selector_has_value(&condition.selector, &condition.value));
4153 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
4154 && flag
4155 .required_if_eq_all
4156 .iter()
4157 .all(|condition| selector_has_value(&condition.selector, &condition.value));
4158 let unless_any = flag
4159 .required_unless
4160 .iter()
4161 .any(|selector| selector_is_explicit(selector));
4162 let unless_all = !flag.required_unless_all.is_empty()
4163 && flag
4164 .required_unless_all
4165 .iter()
4166 .all(|selector| selector_is_explicit(selector));
4167 let required_unless = !(unless_any
4168 || unless_all
4169 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
4170 if flag.required
4171 || required_if
4172 || required_if_eq
4173 || required_if_eq_all
4174 || required_unless
4175 {
4176 errors.push(UsageErr::MissingFlag(flag.name.clone()));
4177 }
4178 continue;
4179 }
4180
4181 let explicitly_provided = explicit
4182 .map(|given| given.contains(&flag.name))
4183 .unwrap_or(true);
4184
4185 if explicitly_provided {
4186 for other in &flag.conflicts {
4187 if selector_is_explicit(other) {
4188 errors.push(UsageErr::InvalidFlag {
4189 token: format!("--{}", flag.name),
4190 reason: format!("conflicts with {other}"),
4191 span: (0, 0).into(),
4192 input: format!("--{} {other}", flag.name),
4193 });
4194 }
4195 }
4196 for other in &flag.requires {
4197 if !selector_is_satisfied(other) {
4198 errors.push(UsageErr::MissingFlag(selector_name(other)));
4199 }
4200 }
4201 for condition in &flag.requires_if {
4202 if value.is_some_and(|value| parse_value_has(value, &condition.value))
4203 && !selector_is_satisfied(&condition.requires)
4204 {
4205 errors.push(UsageErr::MissingFlag(selector_name(&condition.requires)));
4206 }
4207 }
4208 }
4209 if let (true, Some(value)) = (flag.var, value) {
4210 let count = match value {
4211 ParseValue::MultiString(values) => values.len(),
4212 ParseValue::MultiBool(values) => values.len(),
4213 _ => 1,
4214 };
4215 if let Some(min) = flag.var_min.filter(|min| count < *min) {
4216 errors.push(UsageErr::VarFlagTooFew {
4217 name: flag.name.clone(),
4218 min,
4219 got: count,
4220 });
4221 }
4222 if let Some(max) = flag.var_max.filter(|max| count > *max) {
4223 errors.push(UsageErr::VarFlagTooMany {
4224 name: flag.name.clone(),
4225 max,
4226 got: count,
4227 });
4228 }
4229 }
4230 if let (Some(arg), Some(value)) = (&flag.arg, value) {
4231 validate_expression(
4232 &flag.name,
4233 arg.validate.as_deref(),
4234 arg.validate_error.as_deref(),
4235 value,
4236 &mut errors,
4237 );
4238 }
4239 }
4240 for arg in &clause.args {
4241 let given = instance.keys().any(|present| present.name == arg.name);
4242 if !given {
4243 let required_if = arg
4244 .required_if
4245 .iter()
4246 .any(|selector| selector_is_explicit(selector));
4247 let required_if_eq = arg
4248 .required_if_eq
4249 .iter()
4250 .any(|condition| selector_has_value(&condition.selector, &condition.value));
4251 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
4252 && arg
4253 .required_if_eq_all
4254 .iter()
4255 .all(|condition| selector_has_value(&condition.selector, &condition.value));
4256 let unless_any = arg
4257 .required_unless
4258 .iter()
4259 .any(|selector| selector_is_explicit(selector));
4260 let unless_all = !arg.required_unless_all.is_empty()
4261 && arg
4262 .required_unless_all
4263 .iter()
4264 .all(|selector| selector_is_explicit(selector));
4265 let required_unless = !(unless_any
4266 || unless_all
4267 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
4268 if required_if || required_if_eq || required_if_eq_all || required_unless {
4269 errors.push(UsageErr::MissingClauseArg {
4270 clause: clause.name.clone(),
4271 instance: instance_index + 1,
4272 arg: arg.name.clone(),
4273 });
4274 }
4275 continue;
4276 }
4277 for other in &arg.conflicts {
4278 if selector_is_explicit(other) {
4279 errors.push(UsageErr::InvalidFlag {
4280 token: arg.name.clone(),
4281 reason: format!("conflicts with {other}"),
4282 span: (0, 0).into(),
4283 input: format!("{} {other}", arg.name),
4284 });
4285 }
4286 }
4287 for other in &arg.requires {
4288 if selector_is_satisfied(other) {
4289 continue;
4290 }
4291 if other.starts_with('-') {
4292 errors.push(UsageErr::MissingFlag(selector_name(other)));
4293 } else {
4294 errors.push(UsageErr::MissingClauseArg {
4295 clause: clause.name.clone(),
4296 instance: instance_index + 1,
4297 arg: other.clone(),
4298 });
4299 }
4300 }
4301 }
4302 }
4303 out.errors.extend(errors);
4304}
4305
4306fn selector_explicit_has_value(
4307 selector: &str,
4308 expected: &str,
4309 out: &ParseOutput,
4310 overridden_flags: &HashSet<String>,
4311 custom_env: Option<&HashMap<String, String>>,
4312) -> bool {
4313 if let Some(flag) = selected_clause_flag(out, selector) {
4314 return clause_flag_has_value(out, flag, expected);
4315 }
4316 if let Some(flag) = out
4317 .available_flags
4318 .values()
4319 .chain(out.flags.keys())
4320 .filter(|flag| !is_clause_scoped_flag(out, flag))
4321 .find(|flag| flag_matches_selector(flag, selector))
4322 {
4323 return !overridden_flags.contains(&flag.name)
4324 && explicit_flag_has_value(flag, expected, out, custom_env);
4325 }
4326 let Some(arg) = selector_arg(selector, out) else {
4327 return false;
4328 };
4329 let parsed = out
4330 .args
4331 .iter()
4332 .find(|(given, _)| given.name == arg.name)
4333 .map(|(_, value)| value)
4334 .or_else(|| {
4335 out.clauses.values().flatten().find_map(|instance| {
4336 instance
4337 .iter()
4338 .find(|(given, _)| given.name == arg.name)
4339 .map(|(_, value)| value)
4340 })
4341 });
4342 if let Some(value) = parsed {
4343 return match value {
4344 ParseValue::String(value) => value == expected,
4345 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
4346 ParseValue::Bool(value) => value.to_string() == expected,
4347 ParseValue::MultiBool(values) => {
4348 values.iter().any(|value| value.to_string() == expected)
4349 }
4350 };
4351 }
4352 let value = arg.env_names().find_map(|env| match custom_env {
4353 Some(values) => values.get(env).cloned(),
4354 None => std::env::var(env).ok(),
4355 });
4356 value.is_some_and(|value| match arg.delimiter {
4357 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
4358 None => value == expected,
4359 })
4360}
4361
4362fn selector_is_explicit(
4363 selector: &str,
4364 out: &ParseOutput,
4365 overridden_flags: &HashSet<String>,
4366 custom_env: Option<&HashMap<String, String>>,
4367) -> bool {
4368 let scoped_flag_is_explicit =
4369 selected_clause_flag(out, selector).is_some_and(|flag| clause_flag_is_explicit(out, flag));
4370 let flag_is_explicit = out
4371 .available_flags
4372 .values()
4373 .chain(out.flags.keys())
4374 .filter(|flag| !is_clause_scoped_flag(out, flag))
4375 .any(|flag| {
4376 flag_matches_selector(flag, selector)
4377 && !overridden_flags.contains(&flag.name)
4378 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
4379 });
4380 scoped_flag_is_explicit
4381 || flag_is_explicit
4382 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
4383}
4384
4385fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
4391 if let Some(flag) = selected_clause_flag(out, selector) {
4392 return Some(flag.name.clone());
4393 }
4394 out.available_flags
4395 .values()
4396 .chain(out.flags.keys())
4397 .filter(|flag| !is_clause_scoped_flag(out, flag))
4398 .find(|flag| flag_matches_selector(flag, selector))
4399 .map(|flag| flag.name.clone())
4400 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
4401}
4402
4403fn selector_is_satisfied(
4413 selector: &str,
4414 out: &ParseOutput,
4415 overridden_flags: &HashSet<String>,
4416 custom_env: Option<&HashMap<String, String>>,
4417) -> bool {
4418 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
4419 return true;
4420 }
4421 let flag_is_satisfied = out
4422 .available_flags
4423 .values()
4424 .chain(out.flags.keys())
4425 .filter(|flag| !is_clause_scoped_flag(out, flag))
4426 .filter(|flag| flag_matches_selector(flag, selector))
4427 .any(|flag| {
4428 !overridden_flags.contains(&flag.name)
4429 && (!flag.default.is_empty()
4430 || flag.arg.iter().any(|a| !a.default.is_empty())
4431 || flag.default_if.iter().any(|condition| {
4432 default_if_condition_matches(condition, out, overridden_flags, custom_env)
4433 }))
4434 });
4435 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
4436}
4437
4438fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
4439 if selector.starts_with('-') {
4442 return None;
4443 }
4444 out.cmds
4445 .iter()
4446 .flat_map(active_args)
4447 .find(|arg| arg.name == selector)
4448}
4449
4450fn arg_is_explicit(
4451 arg: &SpecArg,
4452 out: &ParseOutput,
4453 custom_env: Option<&HashMap<String, String>>,
4454) -> bool {
4455 out.args.keys().any(|given| given.name == arg.name)
4456 || out
4457 .clauses
4458 .values()
4459 .flatten()
4460 .any(|instance| instance.keys().any(|given| given.name == arg.name))
4461 || arg
4462 .env
4463 .as_ref()
4464 .is_some_and(|env| env_contains(custom_env, env))
4465}
4466
4467fn apply_flag_overrides(
4468 flag: &Arc<SpecFlag>,
4469 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
4470 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4471 pending_flags: &mut Vec<Arc<SpecFlag>>,
4472 overridden_flags: &mut HashSet<String>,
4473 attributed: &mut BTreeMap<String, String>,
4477) {
4478 let overridden_names: HashSet<String> = available_flags
4479 .values()
4480 .chain(parsed_flags.keys())
4481 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
4482 .map(|other| other.name.clone())
4483 .collect();
4484
4485 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
4486 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
4487 for name in &overridden_names {
4488 attributed.insert(name.clone(), flag.name.clone());
4489 }
4490 overridden_flags.extend(overridden_names);
4491 overridden_flags.remove(&flag.name);
4493 attributed.remove(&flag.name);
4494}
4495
4496#[cfg(feature = "cli-help")]
4499fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool, env: Env<'_>) -> UsageErr {
4500 UsageErr::Help(docs::cli::render_runtime_help(
4501 spec,
4502 cmd,
4503 long,
4504 docs::cli::Style::PLAIN,
4505 env,
4506 ))
4507}
4508
4509#[cfg(feature = "cli-help")]
4510fn render_help_all_err(spec: &Spec, cmd: &SpecCommand, env: Env<'_>) -> UsageErr {
4511 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand, env: Env<'_>) {
4512 if !out.is_empty() {
4513 out.push('\n');
4514 }
4515 out.push_str(&docs::cli::render_runtime_help(
4516 spec,
4517 cmd,
4518 true,
4519 docs::cli::Style::PLAIN,
4520 env,
4521 ));
4522 let mut children: Vec<_> = cmd
4523 .subcommands
4524 .values()
4525 .filter(|child| !child.hide)
4526 .collect();
4527 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
4528 for child in children {
4529 append(out, spec, child, env);
4530 }
4531 }
4532
4533 let mut out = String::new();
4534 append(&mut out, spec, cmd, env);
4535 UsageErr::Help(out)
4536}
4537
4538#[cfg(not(feature = "cli-help"))]
4539fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool, _env: Env<'_>) -> UsageErr {
4540 UsageErr::Help("help".to_string())
4541}
4542
4543#[cfg(not(feature = "cli-help"))]
4544fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand, _env: Env<'_>) -> UsageErr {
4545 UsageErr::Help("help".to_string())
4546}
4547
4548fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
4551 let value = if long {
4552 spec.long_version.as_ref().or(spec.version.as_ref())
4553 } else {
4554 spec.version.as_ref().or(spec.long_version.as_ref())
4555 };
4556 UsageErr::Version(value.cloned().unwrap_or_default())
4557}
4558
4559type Env<'a> = Option<&'a HashMap<String, String>>;
4561
4562fn render_action_err(
4563 spec: &Spec,
4564 cmd: &SpecCommand,
4565 flag: &SpecFlag,
4566 spelling: &str,
4567 env: Env<'_>,
4568) -> UsageErr {
4569 use crate::SpecFlagAction;
4570 match flag.action {
4571 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--"), env),
4572 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false, env),
4573 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true, env),
4574 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd, env),
4575 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
4576 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
4577 }
4578}
4579
4580fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
4582 let token = flag
4583 .long
4584 .first()
4585 .map(|long| format!("--{long}"))
4586 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
4587 .unwrap_or_else(|| flag.name.clone());
4588 UsageErr::InvalidFlag {
4589 token: token.clone(),
4590 reason: "requires an argument".to_string(),
4591 span: (0, 0).into(),
4592 input: format!("{token} {following}"),
4593 }
4594}
4595
4596#[derive(Copy, Clone)]
4597struct ChoiceTarget<'a> {
4598 kind: &'a str,
4599 name: &'a str,
4600}
4601
4602impl<'a> ChoiceTarget<'a> {
4603 fn arg(arg: &'a SpecArg) -> Self {
4604 Self {
4605 kind: "arg",
4606 name: &arg.name,
4607 }
4608 }
4609
4610 fn option(flag: &'a SpecFlag) -> Self {
4611 Self {
4612 kind: "option",
4613 name: &flag.name,
4614 }
4615 }
4616}
4617
4618fn short_bundle_is_known(
4623 spec: &Spec,
4624 cmds: &[SpecCommand],
4625 available: &BTreeMap<String, Arc<SpecFlag>>,
4626 token: &str,
4627) -> bool {
4628 for c in token.chars().skip(1) {
4629 match available.get(&format!("-{c}")) {
4630 None if supplies_short(spec, cmds, c) => {}
4635 None => return false,
4636 Some(f) if f.arg.is_some() => return true,
4637 Some(_) => {}
4638 }
4639 }
4640 true
4641}
4642
4643fn supplied_short(
4653 spec: &Spec,
4654 cmds: &[SpecCommand],
4655 letter: char,
4656 env: Env<'_>,
4657) -> Option<UsageErr> {
4658 let cmd = cmds.last()?;
4659 match letter {
4660 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false, env)),
4661 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
4662 _ => None,
4663 }
4664}
4665
4666fn supplies_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> bool {
4669 cmds.last().is_some_and(|cmd| match letter {
4670 'h' => is_help_arg(spec, cmd, "-h"),
4671 'V' => is_version_arg(spec, cmds, "-V"),
4672 _ => false,
4673 })
4674}
4675
4676fn reject_unknown_flag_if_asked(
4682 spec: &Spec,
4683 path: &[SpecCommand],
4684 token: &str,
4685) -> Result<(), UsageErr> {
4686 if !is_flag_like(token) {
4689 return Ok(());
4690 }
4691 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
4692 return Ok(());
4693 }
4694 Err(UsageErr::InvalidFlag {
4695 token: token.to_string(),
4696 reason: "no such flag".to_string(),
4697 span: (0, 0).into(),
4698 input: token.to_string(),
4699 })
4700}
4701
4702fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
4708 path.iter()
4709 .rev()
4710 .find_map(|cmd| cmd.unknown_flags)
4711 .or(spec.unknown_flags)
4712 .unwrap_or_default()
4713}
4714
4715fn is_flag_like(token: &str) -> bool {
4721 match token.strip_prefix('-') {
4722 None | Some("") => false,
4723 Some(_) => true,
4724 }
4725}
4726
4727fn is_negative_number(token: &str) -> bool {
4728 token.strip_prefix('-').is_some_and(is_number)
4729}
4730
4731fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
4737 !flag.require_equals
4738 && (!is_flag_like(token)
4739 || flag.allow_hyphen_values()
4740 || (is_negative_number(token)
4741 && (flag
4742 .arg
4743 .as_ref()
4744 .is_some_and(|arg| arg.allow_negative_numbers)
4745 || (flag.default_missing.is_none() && !flag.value_optional))))
4746}
4747
4748fn record_scalar_flag_occurrence(
4749 cmds: &[SpecCommand],
4750 flag: &Arc<SpecFlag>,
4751 command_level: usize,
4752 bool_value: Option<bool>,
4753 occurrences: &mut HashMap<(usize, usize), u8>,
4754 errors: &mut Vec<UsageErr>,
4755) {
4756 let strict = cmds.get(command_level).is_some_and(|cmd| {
4757 !cmd.args_override_self
4758 || cmd
4759 .clause
4760 .as_ref()
4761 .is_some_and(|clause| clause.flags.iter().any(|candidate| candidate == &**flag))
4762 });
4763 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4764 if !strict || flag.count || collects_values {
4765 return;
4766 }
4767
4768 let bit = match bool_value {
4769 Some(false) if flag.negate.is_some() => 0b10,
4770 _ => 0b01,
4771 };
4772 let key = (Arc::as_ptr(flag) as usize, command_level);
4773 let seen = occurrences.entry(key).or_default();
4774 if *seen & bit != 0 {
4775 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
4776 }
4777 *seen |= bit;
4778}
4779
4780fn is_command_word(token: &str) -> bool {
4788 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
4789}
4790
4791fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
4797 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
4798}
4799
4800fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
4806 !is_negative_number(token)
4807 || cmd
4808 .find_subcommand(default_name)
4809 .and_then(|default| default.args.first())
4810 .is_some_and(|arg| arg.allow_negative_numbers)
4811}
4812
4813fn is_number(rest: &str) -> bool {
4820 let (mantissa, exponent) = match rest.find(['e', 'E']) {
4821 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
4822 None => (rest, None),
4823 };
4824
4825 let mut seen_digit = false;
4826 let mut seen_dot = false;
4827 for c in mantissa.chars() {
4828 match c {
4829 '0'..='9' => seen_digit = true,
4830 '.' if !seen_dot => seen_dot = true,
4831 _ => return false,
4832 }
4833 }
4834 if !seen_digit {
4835 return false;
4836 }
4837
4838 match exponent {
4839 None => true,
4840 Some(exp) => {
4841 let digits = exp
4842 .strip_prefix('+')
4843 .or_else(|| exp.strip_prefix('-'))
4844 .unwrap_or(exp);
4845 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
4846 }
4847 }
4848}
4849
4850#[allow(clippy::too_many_arguments)]
4857fn bind_pending_flag_value(
4858 spec: &Spec,
4859 cmd: &SpecCommand,
4860 errors: &mut Vec<UsageErr>,
4861 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4862 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4863 word: &mut String,
4864 input: &mut VecDeque<Token>,
4865 custom_env: Option<&HashMap<String, String>>,
4866 trace: &mut Trace,
4867 argv: usize,
4871 attached: bool,
4872) -> miette::Result<bool> {
4873 let collecting = flag_awaiting_value
4877 .last()
4878 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
4879 .cloned()
4880 .map(|flag| {
4881 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
4882 (flag, carried)
4883 });
4884 let mut bound = vec![];
4885 let refused = drain_pending_flag_values(
4886 spec,
4887 cmd,
4888 errors,
4889 flags,
4890 flag_awaiting_value,
4891 word,
4892 custom_env,
4893 &mut bound,
4894 )?;
4895 for (flag, values) in bound {
4896 trace.record(
4897 argv,
4898 TokenRole::Value {
4899 flag,
4900 values,
4901 attached,
4902 },
4903 );
4904 }
4905 if refused {
4906 return Ok(true);
4907 }
4908 let Some((flag, carried)) = collecting else {
4909 return Ok(false);
4910 };
4911 collect_variadic_flag_values(
4912 spec,
4913 cmd,
4914 errors,
4915 flags,
4916 flag_awaiting_value,
4917 &flag,
4918 carried,
4919 input,
4920 custom_env,
4921 trace,
4922 )
4923}
4924
4925#[allow(clippy::too_many_arguments)]
4938fn collect_variadic_flag_values(
4939 spec: &Spec,
4940 cmd: &SpecCommand,
4941 errors: &mut Vec<UsageErr>,
4942 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4943 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4944 flag: &Arc<SpecFlag>,
4945 carried: usize,
4946 input: &mut VecDeque<Token>,
4947 custom_env: Option<&HashMap<String, String>>,
4948 trace: &mut Trace,
4949) -> miette::Result<bool> {
4950 let max = flag
4951 .arg
4952 .as_ref()
4953 .and_then(|arg| arg.var_max)
4954 .unwrap_or(usize::MAX);
4955 while flags
4956 .get(flag)
4957 .map(value_count)
4958 .unwrap_or(0)
4959 .saturating_sub(carried)
4960 < max
4961 {
4962 let Some(next) = input.front().map(|token| token.word.as_str()) else {
4963 break;
4964 };
4965 if flag
4966 .arg
4967 .as_ref()
4968 .and_then(|arg| arg.value_terminator.as_deref())
4969 == Some(next)
4970 {
4971 let terminator = input.pop_front().unwrap();
4972 trace.record(
4973 terminator.argv,
4974 TokenRole::ValueTerminator {
4975 ends: flag.name.clone(),
4976 },
4977 );
4978 break;
4979 }
4980 if next == "--"
4983 || (is_flag_like(next)
4984 && !(flag
4985 .arg
4986 .as_ref()
4987 .is_some_and(|arg| arg.allow_negative_numbers)
4988 && is_negative_number(next)))
4989 {
4990 break;
4991 }
4992 let taken = input.pop_front().unwrap();
4993 let argv = taken.argv;
4994 let mut word = taken.word;
4995 flag_awaiting_value.push(Arc::clone(flag));
4996 let mut bound = vec![];
4997 let refused = drain_pending_flag_values(
4998 spec,
4999 cmd,
5000 errors,
5001 flags,
5002 flag_awaiting_value,
5003 &mut word,
5004 custom_env,
5005 &mut bound,
5006 )?;
5007 for (flag, values) in bound {
5008 trace.record(
5011 argv,
5012 TokenRole::Value {
5013 flag,
5014 values,
5015 attached: false,
5016 },
5017 );
5018 }
5019 if refused {
5020 return Ok(true);
5021 }
5022 }
5023 let taken = flags
5032 .get(flag)
5033 .map(value_count)
5034 .unwrap_or(0)
5035 .saturating_sub(carried);
5036 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min)
5037 && taken < min
5038 {
5039 errors.push(UsageErr::VarFlagTooFew {
5040 name: flag.name.clone(),
5041 min,
5042 got: taken,
5043 });
5044 }
5045 if taken > max {
5046 errors.push(UsageErr::VarFlagTooMany {
5047 name: flag.name.clone(),
5048 max,
5049 got: taken,
5050 });
5051 }
5052 Ok(false)
5053}
5054
5055fn value_count(value: &ParseValue) -> usize {
5057 match value {
5058 ParseValue::MultiString(values) => values.len(),
5059 ParseValue::MultiBool(values) => values.len(),
5060 _ => 1,
5061 }
5062}
5063
5064fn try_bind_default_missing(
5075 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5076 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5077 custom_env: Option<&HashMap<String, String>>,
5078 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
5079) -> miette::Result<bool> {
5080 let Some(flag) = flag_awaiting_value.last() else {
5081 return Ok(false);
5082 };
5083 let value = match flag.default_missing.clone() {
5084 Some(value) => value,
5085 None if flag.value_optional => {
5086 let flag = flag_awaiting_value.pop().unwrap();
5087 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
5091 origins
5092 .entry(Arc::clone(&flag))
5093 .or_default()
5094 .push(ValueOrigin::DefaultMissing);
5095 if flag.var {
5096 flags
5101 .entry(flag)
5102 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
5103 .try_as_multi_string_mut()
5104 .unwrap()
5105 .push(String::new());
5106 } else if variadic_value {
5107 flags
5110 .entry(flag)
5111 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
5112 } else {
5113 flags.insert(flag, ParseValue::MultiString(Vec::new()));
5117 }
5118 return Ok(true);
5119 }
5120 None => return Ok(false),
5121 };
5122 if let Some(arg) = flag.arg.as_ref() {
5123 validate_choice_value(
5124 ChoiceTarget::option(flag),
5125 &value,
5126 arg.choices.as_ref(),
5127 custom_env,
5128 )?;
5129 }
5130 let flag = flag_awaiting_value.pop().unwrap();
5131 origins
5132 .entry(Arc::clone(&flag))
5133 .or_default()
5134 .push(ValueOrigin::DefaultMissing);
5135 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
5136 if collecting {
5137 let arr = flags
5138 .entry(flag)
5139 .or_insert_with(|| ParseValue::MultiString(vec![]))
5140 .try_as_multi_string_mut()
5141 .unwrap();
5142 arr.push(value);
5143 } else {
5144 flags.insert(flag, ParseValue::String(value));
5145 }
5146 Ok(true)
5147}
5148
5149#[allow(clippy::too_many_arguments)]
5154fn drain_pending_flag_values(
5155 spec: &Spec,
5156 cmd: &SpecCommand,
5157 errors: &mut Vec<UsageErr>,
5158 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
5159 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
5160 word: &mut String,
5161 custom_env: Option<&HashMap<String, String>>,
5162 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
5163) -> miette::Result<bool> {
5164 while let Some(flag) = flag_awaiting_value.pop() {
5165 let arg = flag.arg.as_ref().unwrap();
5166 let parts: Vec<String> = match arg.delimiter {
5170 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
5171 None => vec![std::mem::take(word)],
5172 };
5173 for part in &parts {
5174 if validate_choices(
5175 spec,
5176 cmd,
5177 errors,
5178 ChoiceTarget::option(&flag),
5179 part,
5180 arg.choices.as_ref(),
5181 custom_env,
5182 )? {
5183 return Ok(true);
5184 }
5185 }
5186 word.clear();
5187 bound.push((Arc::clone(&flag), parts.clone()));
5188 if flag.var || arg.var {
5191 let arr = flags
5192 .entry(flag)
5193 .or_insert_with(|| ParseValue::MultiString(vec![]))
5194 .try_as_multi_string_mut()
5195 .unwrap();
5196 arr.extend(parts);
5197 } else {
5198 flags.insert(
5201 flag,
5202 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
5203 );
5204 }
5205 }
5206 Ok(false)
5207}
5208
5209fn choice_error(
5210 target: ChoiceTarget<'_>,
5211 value: &str,
5212 choices: Option<&SpecChoices>,
5213 custom_env: Option<&HashMap<String, String>>,
5214) -> Option<String> {
5215 let choices = choices?;
5216 if !choices.strict {
5217 return None;
5218 }
5219 match choices.matches_resolved(value, custom_env) {
5221 Ok(true) => return None,
5222 Ok(false) => {}
5223 Err(_) if choices.run().is_some() && crate::spec::choices::runs_disabled() => return None,
5226 Err(err) => {
5227 return Some(format!(
5228 "Could not check {} {}: {value} against its choices: {err}",
5229 target.kind, target.name,
5230 ));
5231 }
5232 }
5233 let values = choices
5236 .resolved_values(custom_env)
5237 .unwrap_or_else(|_| choices.values_with_env(custom_env));
5238 if let Some(run) = choices.run()
5239 && values.is_empty()
5240 {
5241 return Some(format!(
5242 "Invalid choice for {} {}: {value}, `{run}` printed no choices",
5243 target.kind, target.name,
5244 ));
5245 }
5246 if let Some(env) = choices.env()
5247 && values.is_empty()
5248 {
5249 return Some(format!(
5250 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
5251 target.kind, target.name,
5252 ));
5253 }
5254 Some(format!(
5255 "Invalid choice for {} {}: {value}, expected one of {}",
5256 target.kind,
5257 target.name,
5258 values.join(", ")
5259 ))
5260}
5261
5262fn validate_choices(
5263 spec: &Spec,
5264 cmd: &SpecCommand,
5265 errors: &mut Vec<UsageErr>,
5266 target: ChoiceTarget<'_>,
5267 value: &str,
5268 choices: Option<&SpecChoices>,
5269 custom_env: Option<&HashMap<String, String>>,
5270) -> miette::Result<bool> {
5271 if is_help_arg(spec, cmd, value)
5275 && choices.is_some_and(|choices| {
5276 choices.strict && !choices.matches_resolved(value, custom_env).unwrap_or(false)
5277 })
5278 {
5279 errors.push(render_help_err(spec, cmd, value.len() > 2, custom_env));
5280 return Ok(true);
5281 }
5282
5283 if let Some(err) = choice_error(target, value, choices, custom_env) {
5284 bail!("{err}");
5285 }
5286 Ok(false)
5287}
5288
5289fn validate_choice_value(
5290 target: ChoiceTarget<'_>,
5291 value: &str,
5292 choices: Option<&SpecChoices>,
5293 custom_env: Option<&HashMap<String, String>>,
5294) -> miette::Result<()> {
5295 if let Some(err) = choice_error(target, value, choices, custom_env) {
5296 bail!("{err}");
5297 }
5298 Ok(())
5299}
5300
5301fn validate_choice_values(
5302 target: ChoiceTarget<'_>,
5303 values: &[String],
5304 choices: Option<&SpecChoices>,
5305 custom_env: Option<&HashMap<String, String>>,
5306) -> miette::Result<()> {
5307 for value in values {
5308 validate_choice_value(target, value, choices, custom_env)?;
5309 }
5310 Ok(())
5311}
5312
5313fn record_stop(
5320 out: &mut ParseOutput,
5321 next_arg_idx: usize,
5322 seen_double_dash: bool,
5323 trace: &mut Trace,
5324 unread: &VecDeque<Token>,
5325) {
5326 out.next_arg = active_args(&out.cmd)
5327 .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
5328 .cloned()
5329 .map(Arc::new);
5330 out.double_dash_seen = seen_double_dash;
5331 finalize_current_clause(out);
5332 trace.close(unread);
5333 out.tokens = std::mem::take(&mut trace.tokens);
5334}
5335
5336fn finalize_current_clause(out: &mut ParseOutput) {
5337 let Some(clause) = out.cmd.clause.as_ref() else {
5338 return;
5339 };
5340 let has_scoped_flags = clause
5341 .flags
5342 .iter()
5343 .any(|flag| out.flags.contains_key(&Arc::new(flag.clone())));
5344 if clause.separator.is_none() && out.args.is_empty() && !has_scoped_flags {
5345 return;
5346 }
5347 out.clauses
5348 .entry(clause.name.clone())
5349 .or_default()
5350 .push(std::mem::take(&mut out.args));
5351 let mut flags = IndexMap::new();
5352 for clause_flag in &clause.flags {
5353 let key = Arc::new(clause_flag.clone());
5354 if let Some((flag, value)) = out.flags.shift_remove_entry(&key) {
5355 flags.insert(flag, value);
5356 }
5357 }
5358 out.clause_flags
5359 .entry(clause.name.clone())
5360 .or_default()
5361 .push(flags);
5362}
5363
5364fn restore_current_clause(out: &mut ParseOutput) {
5365 let Some(clause) = out.cmd.clause.as_ref() else {
5366 return;
5367 };
5368 if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
5369 out.args = current;
5370 }
5371 if let Some(current) = out.clause_flags.get_mut(&clause.name).and_then(Vec::pop) {
5372 out.flags.extend(current);
5373 }
5374}
5375
5376fn reset_clause_scalar_occurrences(
5377 out: &ParseOutput,
5378 occurrences: &mut HashMap<(usize, usize), u8>,
5379) {
5380 let Some(clause) = out.cmd.clause.as_ref() else {
5381 return;
5382 };
5383 let names = clause
5384 .flags
5385 .iter()
5386 .map(|flag| flag.name.as_str())
5387 .collect::<HashSet<_>>();
5388 let pointers = unique_flags(out.available_flags.values())
5389 .filter(|flag| names.contains(flag.name.as_str()))
5390 .map(|flag| Arc::as_ptr(flag) as usize)
5391 .collect::<HashSet<_>>();
5392 occurrences.retain(|(flag, _), _| !pointers.contains(flag));
5393}
5394
5395fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
5396 while active_args(cmd)
5397 .get(idx)
5398 .is_some_and(|arg| arg.sigil.is_some())
5399 {
5400 idx += 1;
5401 }
5402 idx
5403}
5404
5405fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
5406 cmd.clause
5407 .as_ref()
5408 .map(|clause| clause.args.as_slice())
5409 .unwrap_or(cmd.args.as_slice())
5410}
5411
5412fn match_sigil_arg<'a>(
5413 cmd: &'a SpecCommand,
5414 word: &'a str,
5415) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5416 cmd.args
5417 .iter()
5418 .filter_map(|arg| {
5419 let sigil = arg.sigil.as_deref()?;
5420 let value = word.strip_prefix(sigil)?;
5421 Some((arg, sigil, value))
5422 })
5423 .max_by_key(|(_, sigil, _)| sigil.len())
5424}
5425
5426fn match_sigil_arg_chain<'a>(
5427 cmds: &'a [SpecCommand],
5428 word: &'a str,
5429) -> Option<(&'a SpecArg, &'a str, &'a str)> {
5430 cmds.iter()
5431 .filter_map(|cmd| match_sigil_arg(cmd, word))
5432 .max_by_key(|(_, sigil, _)| sigil.len())
5433}
5434
5435fn report_double_dash_violation(
5442 arg: &SpecArg,
5443 errors: &mut Vec<UsageErr>,
5444 violations: &mut HashSet<String>,
5445) {
5446 if violations.insert(arg.name.clone()) {
5447 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
5448 }
5449}
5450
5451fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
5466 (spec.version.is_some() || spec.long_version.is_some())
5467 && cmds.len() == 1
5468 && !spec.cmd.disable_version_flag
5469 && (w == "--version" || w == "-V")
5470}
5471
5472fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
5473 spec.disable_help != Some(true)
5474 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
5475 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
5476}
5477
5478impl ParseOutput {
5479 pub fn as_env(&self) -> BTreeMap<String, String> {
5480 let mut env = BTreeMap::new();
5481 for (flag, val) in &self.flags {
5482 let key = format!("usage_{}", crate::case::snake(&flag.name));
5483 let val = match val {
5484 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
5485 ParseValue::String(s) => s.clone(),
5486 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
5487 ParseValue::MultiString(s) => crate::shell_words::join(s),
5488 };
5489 env.insert(key, val);
5490 }
5491 for (arg, val) in &self.args {
5492 let key = format!("usage_{}", crate::case::snake(&arg.name));
5493 env.insert(key, val.to_string());
5494 }
5495 let path = self.cmds.iter().skip(1).map(|c| c.name.as_str()).join(" ");
5502 let declared = |c: &SpecCommand| {
5503 let clause = c.clause.as_ref();
5504 c.args
5505 .iter()
5506 .chain(clause.into_iter().flat_map(|cl| &cl.args))
5507 .any(|a| crate::case::snake(&a.name) == "cmd")
5508 || c.flags
5509 .iter()
5510 .chain(clause.into_iter().flat_map(|cl| &cl.flags))
5511 .any(|f| crate::case::snake(&f.name) == "cmd")
5512 };
5513 if !path.is_empty() && !self.cmds.iter().any(declared) {
5514 env.entry("usage_cmd".to_string()).or_insert(path);
5515 }
5516 env
5517 }
5518}
5519
5520impl Display for ParseValue {
5521 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5522 match self {
5523 ParseValue::Bool(b) => write!(f, "{b}"),
5524 ParseValue::String(s) => write!(f, "{s}"),
5525 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
5526 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
5527 }
5528 }
5529}
5530
5531fn render_token(token: &TokenBinding) -> String {
5533 let roles = token.roles.iter().map(render_role).join(", ");
5534 let synthesized = if token.synthesized { " (read as)" } else { "" };
5535 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
5536}
5537
5538fn render_role(role: &TokenRole) -> String {
5539 match role {
5540 TokenRole::Program => "program".to_string(),
5541 TokenRole::Command { name } => format!("subcommand {name}"),
5542 TokenRole::Flag {
5543 flag,
5544 spelling,
5545 negated,
5546 } => {
5547 let negated = if *negated { ", negated" } else { "" };
5548 format!("flag {} as {spelling}{negated}", flag.name)
5549 }
5550 TokenRole::Value {
5551 flag,
5552 values,
5553 attached,
5554 } => {
5555 let attached = if *attached { ", attached" } else { "" };
5556 format!("value of {} = {values:?}{attached}", flag.name)
5557 }
5558 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
5559 TokenRole::Sigil { arg, sigil, values } => {
5560 format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
5561 }
5562 TokenRole::Separator => "separator".to_string(),
5563 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
5564 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
5565 TokenRole::Restart => "restart".to_string(),
5566 TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
5567 TokenRole::UnknownFlag { bound_as } => match bound_as {
5568 Some(arg) => format!("unknown flag, bound as {}", arg.name),
5569 None => "unknown flag".to_string(),
5570 },
5571 TokenRole::Refused { reason } => format!("refused: {reason}"),
5572 TokenRole::External => "external".to_string(),
5573 TokenRole::Unread => "unread".to_string(),
5574 }
5575}
5576
5577impl Debug for ParseOutput {
5578 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5579 f.debug_struct("ParseOutput")
5580 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
5581 .field(
5582 "args",
5583 &self
5584 .args
5585 .iter()
5586 .map(|(a, w)| format!("{}: {w}", a.name))
5587 .collect_vec(),
5588 )
5589 .field("clauses", &self.clauses)
5590 .field(
5591 "available_flags",
5592 &self
5593 .available_flags
5594 .iter()
5595 .map(|(f, w)| format!("{f}: {w}"))
5596 .collect_vec(),
5597 )
5598 .field(
5599 "flags",
5600 &self
5601 .flags
5602 .iter()
5603 .map(|(f, w)| format!("{}: {w}", f.name))
5604 .collect_vec(),
5605 )
5606 .field("flag_awaiting_value", &self.flag_awaiting_value)
5607 .field("errors", &self.errors)
5608 .field("external", &self.external)
5609 .field(
5613 "tokens",
5614 &self.tokens.iter().map(render_token).collect_vec(),
5615 )
5616 .field(
5617 "origins",
5618 &self
5619 .flag_origins
5620 .iter()
5621 .map(|(f, o)| format!("{}: {o:?}", f.name))
5622 .chain(
5623 self.arg_origins
5624 .iter()
5625 .map(|(a, o)| format!("{}: {o:?}", a.name)),
5626 )
5627 .collect_vec(),
5628 )
5629 .field("overridden_flags", &self.overridden_flags)
5630 .finish()
5631 }
5632}
5633
5634#[cfg(test)]
5635mod tests {
5636 use super::*;
5637 use crate::SpecFlagAction;
5638
5639 fn input(words: &[&str]) -> Vec<String> {
5640 words.iter().map(|word| (*word).to_string()).collect()
5641 }
5642
5643 #[test]
5644 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
5645 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5650 .parse()
5651 .unwrap();
5652
5653 for spelling in ["--version", "-V"] {
5654 let err = parse(&spec, &input(&["ex", spelling]))
5655 .expect_err("answering with a version ends the parse");
5656 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
5657 }
5658 }
5659
5660 #[test]
5661 fn the_supplied_version_flag_is_the_roots_alone() {
5662 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
5665 .parse()
5666 .unwrap();
5667
5668 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
5669 assert_eq!(err.to_string(), "unexpected word: --version");
5670 }
5671
5672 #[test]
5673 fn no_declared_version_supplies_nothing() {
5674 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
5677
5678 for spelling in ["--version", "-V"] {
5679 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5680 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5681 }
5682 }
5683
5684 #[test]
5685 fn disable_version_flag_removes_the_supplied_spellings() {
5686 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
5687 .parse()
5688 .unwrap();
5689
5690 for spelling in ["--version", "-V"] {
5691 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
5692 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
5693 }
5694 }
5695
5696 #[test]
5697 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
5698 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
5701 .parse()
5702 .unwrap();
5703
5704 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
5705 assert_eq!(out.flags.len(), 1);
5706
5707 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
5708 assert_eq!(err.to_string(), "1.2.3");
5709 }
5710
5711 #[test]
5712 fn the_supplied_spellings_split_the_two_version_texts() {
5713 let spec: Spec =
5716 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
5717 .parse()
5718 .unwrap();
5719
5720 assert_eq!(
5721 parse(&spec, &input(&["ex", "--version"]))
5722 .unwrap_err()
5723 .to_string(),
5724 "1.2.3 (abcdef)"
5725 );
5726 assert_eq!(
5727 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
5728 "1.2.3"
5729 );
5730 }
5731
5732 #[test]
5733 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
5734 let spec: Spec =
5739 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
5740 .parse()
5741 .unwrap();
5742
5743 for token in ["-vh", "-hv"] {
5744 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
5745 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
5746 }
5747 for token in ["-vV", "-Vv"] {
5748 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
5749 assert_eq!(err.to_string(), "1.2.3", "{token}");
5750 }
5751 }
5752
5753 #[test]
5754 fn a_bundled_help_letter_asks_for_the_short_page() {
5755 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"
5758 .parse()
5759 .unwrap();
5760
5761 let short = parse(&spec, &input(&["ex", "-vh"]))
5762 .unwrap_err()
5763 .to_string();
5764 let long = parse(&spec, &input(&["ex", "--help"]))
5765 .unwrap_err()
5766 .to_string();
5767 assert!(short.contains("Be loud"), "{short}");
5768 assert!(!short.contains("at length"), "{short}");
5769 assert!(long.contains("at length"), "{long}");
5770 }
5771
5772 #[test]
5773 fn the_bundled_version_letter_is_the_roots_alone() {
5774 let spec: Spec =
5776 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
5777 .parse()
5778 .unwrap();
5779
5780 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
5781 assert_eq!(err.to_string(), "unexpected word: -vV");
5782 }
5783
5784 #[test]
5785 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
5786 let spec: Spec =
5789 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
5790 .parse()
5791 .unwrap();
5792
5793 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
5794 assert_eq!(out.flags.len(), 2);
5795 assert!(
5796 out.flags
5797 .iter()
5798 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local")
5799 );
5800 }
5801
5802 #[test]
5803 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
5804 let spec: Spec =
5805 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
5806 .parse()
5807 .unwrap();
5808
5809 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
5810 assert_eq!(err.to_string(), "unexpected word: -vh");
5811 }
5812
5813 #[test]
5814 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
5815 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
5818 .parse()
5819 .unwrap();
5820
5821 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
5822 assert!(out.flags.is_empty(), "{:?}", out.flags);
5823 assert_eq!(out.args.len(), 1);
5824 }
5825
5826 fn spec_with_arg(arg: SpecArg) -> Spec {
5827 let cmd = SpecCommand::builder().name("test").arg(arg).build();
5828 Spec {
5829 name: "test".to_string(),
5830 bin: "test".to_string(),
5831 cmd,
5832 ..Default::default()
5833 }
5834 }
5835
5836 fn spec_with_flag(flag: SpecFlag) -> Spec {
5837 let cmd = SpecCommand::builder().name("test").flag(flag).build();
5838 Spec {
5839 name: "test".to_string(),
5840 bin: "test".to_string(),
5841 cmd,
5842 ..Default::default()
5843 }
5844 }
5845
5846 fn parse_with_env(
5847 spec: &Spec,
5848 words: &[&str],
5849 env: &[(&str, &str)],
5850 ) -> Result<ParseOutput, miette::Error> {
5851 let env = env
5852 .iter()
5853 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
5854 .collect();
5855 Parser::new(spec).with_env(env).parse(&input(words))
5856 }
5857
5858 fn first_string_value(parsed: &ParseOutput) -> &str {
5859 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
5860 return value;
5861 }
5862 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
5863 return value;
5864 }
5865 panic!("expected first parsed value to be ParseValue::String");
5866 }
5867
5868 #[test]
5869 fn custom_environment_parser_dispatches_executable_views() {
5870 let spec: Spec = r#"
5871bin "ex"
5872view "runner" root="run"
5873cmd "run" {
5874 flag "--token <token>" env="TOKEN"
5875}
5876 "#
5877 .parse()
5878 .unwrap();
5879 let parsed = Parser::new(&spec)
5880 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
5881 .parse(&input(&["runner"]))
5882 .unwrap();
5883
5884 assert_eq!(parsed.cmd.name, "runner");
5885 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
5886 && matches!(value, ParseValue::String(value) if value == "secret")));
5887 }
5888
5889 #[test]
5890 fn an_executable_view_keeps_the_hosts_version_action() {
5891 let spec: Spec = r#"
5892bin "ex"
5893version "1.2.3"
5894flag "-V --version" action="version"
5895flag "--verbose" global=#true
5896view "runner" root="run" globals=#true
5897cmd "run"
5898 "#
5899 .parse()
5900 .unwrap();
5901
5902 let error = Parser::new(&spec)
5903 .parse(&input(&["runner", "--version"]))
5904 .expect_err("the host version action should answer before view projection");
5905 assert_eq!(error.to_string(), "1.2.3");
5906
5907 let error = Parser::new(&spec)
5908 .parse(&input(&["runner", "--verbose", "--version"]))
5909 .expect_err("the host version action should remain after a carried global");
5910 assert_eq!(error.to_string(), "1.2.3");
5911 }
5912
5913 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
5914 let flag = parsed
5915 .flags
5916 .keys()
5917 .find(|flag| flag.name == name)
5918 .unwrap_or_else(|| panic!("expected flag {name}"));
5919 let value = parsed
5920 .flags
5921 .get(flag)
5922 .unwrap_or_else(|| panic!("expected value for flag {name}"));
5923 match value {
5924 ParseValue::String(value) => value,
5925 _ => panic!("expected flag {name} to be ParseValue::String"),
5926 }
5927 }
5928
5929 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
5930 let err = result.expect_err("expected parser error");
5931 assert_eq!(format!("{err}"), expected);
5932 }
5933
5934 #[test]
5935 fn a_short_version_action_falls_back_to_the_long_version() {
5936 let flag = SpecFlag::builder()
5937 .short('R')
5938 .action(SpecFlagAction::Version)
5939 .build();
5940 let spec = Spec {
5941 name: "test".to_string(),
5942 bin: "test".to_string(),
5943 long_version: Some("1.2.3\ncommit abc123".to_string()),
5944 ..Default::default()
5945 };
5946 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R", None)
5947 else {
5948 panic!("expected version action")
5949 };
5950 assert_eq!(version, "1.2.3\ncommit abc123");
5951 }
5952
5953 #[cfg(feature = "unstable_choices_env")]
5954 fn spec_arg_choices_env(key: &str) -> Spec {
5955 spec_with_arg(
5956 SpecArg::builder()
5957 .name("env")
5958 .choices_env(key)
5959 .required(false)
5960 .build(),
5961 )
5962 }
5963
5964 #[cfg(feature = "unstable_choices_env")]
5965 fn spec_flag_choices_env(key: &str) -> Spec {
5966 spec_with_flag(
5967 SpecFlag::builder()
5968 .long("env")
5969 .arg(SpecArg::builder().name("env").choices_env(key).build())
5970 .build(),
5971 )
5972 }
5973
5974 #[test]
5975 fn test_parse() {
5976 let cmd = SpecCommand::builder()
5977 .name("test")
5978 .arg(SpecArg::builder().name("arg").build())
5979 .flag(SpecFlag::builder().long("flag").build())
5980 .build();
5981 let spec = Spec {
5982 name: "test".to_string(),
5983 bin: "test".to_string(),
5984 cmd,
5985 ..Default::default()
5986 };
5987 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
5988 let parsed = parse(&spec, &input).unwrap();
5989 assert_eq!(parsed.cmds.len(), 1);
5990 assert_eq!(parsed.cmds[0].name, "test");
5991 assert_eq!(parsed.args.len(), 1);
5992 assert_eq!(parsed.flags.len(), 1);
5993 assert_eq!(parsed.available_flags.len(), 1);
5994 }
5995
5996 #[test]
5997 fn test_flag_overrides_last_occurrence_wins() {
5998 let spec: Spec = r#"
5999flag "--stdin" default=#true
6000flag "--file <file>" overrides="--stdin"
6001 "#
6002 .parse()
6003 .unwrap();
6004
6005 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
6006 assert_eq!(file_wins.flags.len(), 1);
6007 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
6008 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
6009
6010 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
6011 assert_eq!(stdin_wins.flags.len(), 1);
6012 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
6013 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
6014 }
6015
6016 #[test]
6017 fn test_flag_override_clears_pending_value() {
6018 let spec: Spec = r#"
6019flag "--file <file>" overrides="--stdin"
6020flag "--stdin"
6021arg "[input]"
6022 "#
6023 .parse()
6024 .unwrap();
6025
6026 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
6027 assert_eq!(parsed.flags.len(), 1);
6028 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
6029 assert_eq!(first_string_value(&parsed), "input.txt");
6030 }
6031
6032 #[cfg(unix)]
6033 #[test]
6034 fn a_mount_on_the_root_discovers_subcommands() {
6035 let spec: Spec = r#"
6039name "ex"
6040bin "ex"
6041cmd "declared"
6042mount run="echo 'cmd \"discovered\"'"
6043"#
6044 .parse()
6045 .unwrap();
6046
6047 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6048 assert_eq!(out.cmd.name, "discovered");
6049 }
6050
6051 #[test]
6052 fn unknown_env_knows_the_variables_a_mounted_command_declares() {
6053 let spec: Spec = r#"
6054name "ex"
6055bin "ex"
6056unknown_env prefix="EX_"
6057mount run="discover"
6058cmd "publish"
6059"#
6060 .parse()
6061 .unwrap();
6062 let mounts: HashMap<String, String> = [(
6063 "discover".to_string(),
6064 "cmd \"serve\" {\n flag \"--port <port>\" env=\"EX_PORT\"\n}\n".to_string(),
6065 )]
6066 .into();
6067 let env = |name: &str| -> HashMap<String, String> { [(name.into(), "1".into())].into() };
6068
6069 let out = Parser::new(&spec)
6070 .with_mount_outputs(mounts.clone())
6071 .with_env(env("EX_PORT"))
6072 .parse(&input(&["ex", "serve"]))
6073 .expect("the mounted command declares EX_PORT");
6074 assert_eq!(out.cmd.name, "serve");
6075
6076 let err = Parser::new(&spec)
6079 .with_mount_outputs(mounts.clone())
6080 .with_env(env("EX_PORT"))
6081 .parse(&input(&["ex", "publish"]))
6082 .unwrap_err();
6083 assert!(err.to_string().contains("EX_PORT"), "{err}");
6084
6085 let err = Parser::new(&spec)
6086 .with_mount_outputs(mounts)
6087 .with_env(env("EX_PROT"))
6088 .parse(&input(&["ex", "serve"]))
6089 .unwrap_err();
6090 assert!(err.to_string().contains("EX_PROT"), "{err}");
6091 }
6092
6093 #[test]
6094 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
6095 let spec: Spec = r#"
6096name "ex"
6097bin "ex"
6098mount run="this command must never run"
6099cmd "declared"
6100"#
6101 .parse()
6102 .unwrap();
6103
6104 Parser::new(&spec)
6105 .with_mount_outputs(HashMap::new())
6106 .parse(&input(&["ex", "declared"]))
6107 .expect("a declared command does not resolve the mount");
6108
6109 let error = Parser::new(&spec)
6110 .with_mount_outputs(HashMap::new())
6111 .parse(&input(&["ex", "discovered"]))
6112 .unwrap_err();
6113 assert!(
6114 error
6115 .to_string()
6116 .contains("No injected output was provided for mount command"),
6117 "{error}"
6118 );
6119 }
6120
6121 #[cfg(unix)]
6122 #[test]
6123 fn completion_sees_root_mounted_commands_with_nothing_typed() {
6124 let spec: Spec = r#"
6128name "ex"
6129bin "ex"
6130cmd "declared"
6131mount run="echo 'cmd \"discovered\"'"
6132"#
6133 .parse()
6134 .unwrap();
6135
6136 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6137 assert!(
6138 out.cmd.subcommands.contains_key("discovered"),
6139 "a completion should see mounted commands; got {:?}",
6140 out.cmd.subcommands.keys().collect::<Vec<_>>()
6141 );
6142 }
6143
6144 #[cfg(unix)]
6145 #[test]
6146 fn completion_and_execution_agree_about_discovery() {
6147 let spec: Spec = r#"
6151name "ex"
6152bin "ex"
6153default_subcommand "run"
6154cmd "run" {
6155 arg "<task>"
6156}
6157mount run="exit 1"
6158"#
6159 .parse()
6160 .unwrap();
6161
6162 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
6163 assert!(
6164 !out.cmd.subcommands.contains_key("discovered"),
6165 "a completion must not offer what execution will not route"
6166 );
6167
6168 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6169 assert_eq!(out.cmd.name, "run");
6170 }
6171
6172 #[cfg(unix)]
6173 #[test]
6174 fn a_default_subcommand_outranks_discovery() {
6175 let spec: Spec = r#"
6178name "ex"
6179bin "ex"
6180default_subcommand "run"
6181cmd "run" {
6182 arg "<task>"
6183}
6184mount run="exit 1"
6185"#
6186 .parse()
6187 .unwrap();
6188
6189 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6190 assert_eq!(out.cmd.name, "run");
6191 }
6192
6193 #[cfg(unix)]
6194 #[test]
6195 fn a_mount_may_ask_to_outrank_the_default() {
6196 let spec: Spec = r#"
6199name "ex"
6200bin "ex"
6201default_subcommand "run"
6202cmd "run" {
6203 arg "<task>"
6204}
6205mount run="echo 'cmd \"discovered\"'" overrides_default=#true
6206"#
6207 .parse()
6208 .unwrap();
6209
6210 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
6211 assert_eq!(out.cmd.name, "discovered");
6212
6213 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
6215 assert_eq!(out.cmd.name, "run");
6216 }
6217
6218 #[cfg(unix)]
6219 #[test]
6220 fn a_flag_does_not_run_the_mount() {
6221 let spec: Spec = r#"
6225name "ex"
6226bin "ex"
6227flag "--verbose"
6228cmd "declared"
6229mount run="exit 1"
6230"#
6231 .parse()
6232 .unwrap();
6233
6234 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
6235 assert_eq!(out.cmd.name, "ex");
6236 }
6237
6238 #[cfg(unix)]
6239 #[test]
6240 fn a_declared_subcommand_does_not_run_the_mount() {
6241 let spec: Spec = r#"
6245name "ex"
6246bin "ex"
6247cmd "declared"
6248mount run="exit 1"
6249"#
6250 .parse()
6251 .unwrap();
6252
6253 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
6254 assert_eq!(out.cmd.name, "declared");
6255 }
6256
6257 #[test]
6258 fn a_root_mount_survives_being_written_out() {
6259 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
6260 .parse()
6261 .unwrap();
6262 assert_eq!(spec.cmd.mounts.len(), 1);
6263
6264 let reparsed: Spec = spec.to_string().parse().unwrap();
6265 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
6266 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
6267 }
6268
6269 #[test]
6270 fn test_mount_prefix_applies_flag_overrides() {
6271 let stdin = Arc::new(
6272 SpecFlag::builder()
6273 .name("stdin")
6274 .long("stdin")
6275 .global(true)
6276 .build(),
6277 );
6278 let file = Arc::new(
6279 SpecFlag::builder()
6280 .name("file")
6281 .long("file")
6282 .arg(SpecArg::builder().name("file").build())
6283 .global(true)
6284 .overrides_with(vec!["--stdin".to_string()])
6285 .build(),
6286 );
6287 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
6288
6289 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
6290 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
6291
6292 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
6293 }
6294
6295 #[test]
6296 fn test_flag_override_suppresses_env_value() {
6297 let spec: Spec = r#"
6298flag "--stdin" env="USE_STDIN"
6299flag "--file <file>" overrides="--stdin"
6300 "#
6301 .parse()
6302 .unwrap();
6303
6304 let parsed = parse_with_env(
6305 &spec,
6306 &["test", "--file", "input.txt"],
6307 &[("USE_STDIN", "true")],
6308 )
6309 .unwrap();
6310 assert_eq!(parsed.flags.len(), 1);
6311 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6312 }
6313
6314 #[test]
6315 fn test_flag_override_suppresses_required_check() {
6316 let spec: Spec = r#"
6317flag "--stdin" required=#true
6318flag "--file <file>" overrides="--stdin"
6319 "#
6320 .parse()
6321 .unwrap();
6322
6323 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6324 assert_eq!(parsed.flags.len(), 1);
6325 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
6326 }
6327
6328 #[test]
6329 fn test_flag_required_if() {
6330 let spec: Spec = r#"
6331flag "--dir <dir>"
6332flag "--file <file>" required_if="--dir"
6333 "#
6334 .parse()
6335 .unwrap();
6336
6337 parse(&spec, &input(&["test"])).unwrap();
6338 assert_parse_err(
6339 parse(&spec, &input(&["test", "--dir", "src"])),
6340 "Missing required flag: --file <file>",
6341 );
6342 parse(
6343 &spec,
6344 &input(&["test", "--dir", "src", "--file", "input.txt"]),
6345 )
6346 .unwrap();
6347 }
6348
6349 #[test]
6350 fn test_flag_required_unless() {
6351 let spec: Spec = r#"
6352flag "--stdin"
6353flag "--file <file>" required_unless="--stdin"
6354 "#
6355 .parse()
6356 .unwrap();
6357
6358 assert_parse_err(
6359 parse(&spec, &input(&["test"])),
6360 "Missing required flag: --file <file>",
6361 );
6362 parse(&spec, &input(&["test", "--stdin"])).unwrap();
6363 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
6364 }
6365
6366 #[test]
6367 fn complete_required_relationship_truth_tables() {
6368 let spec: Spec = r#"
6369name "ex"
6370bin "ex"
6371flag "--mode <mode>"
6372flag "--scope <scope>"
6373flag "--token <token>" {
6374 required_if_eq "--mode" "remote"
6375}
6376flag "--approval <approval>" {
6377 required_if_eq_all "--mode" "remote" "--scope" "global"
6378}
6379flag "--input <input>" {
6380 required_unless "--stdin" "--file"
6381}
6382flag "--checksum <checksum>" {
6383 required_unless_all "--stdin" "--file"
6384}
6385flag "--stdin"
6386flag "--file <file>"
6387arg "[request]" {
6388 requires "--mode" "--scope"
6389}
6390"#
6391 .parse()
6392 .unwrap();
6393 let parse_args = |args: &[&str]| {
6394 parse(
6395 &spec,
6396 &args
6397 .iter()
6398 .map(|arg| (*arg).to_string())
6399 .collect::<Vec<_>>(),
6400 )
6401 };
6402
6403 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
6404 assert!(
6405 parse_args(&[
6406 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
6407 ])
6408 .is_err()
6409 );
6410 parse_args(&[
6411 "ex",
6412 "--mode",
6413 "remote",
6414 "--token",
6415 "secret",
6416 "--scope",
6417 "global",
6418 "--approval",
6419 "yes",
6420 "--stdin",
6421 "--file",
6422 "in",
6423 ])
6424 .unwrap();
6425 parse_args(&[
6426 "ex",
6427 "--mode",
6428 "local",
6429 "--scope",
6430 "project",
6431 "--stdin",
6432 "--checksum",
6433 "sum",
6434 "request.json",
6435 ])
6436 .unwrap();
6437
6438 let reparsed: Spec = spec.to_string().parse().unwrap();
6439 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
6440 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
6441 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
6442 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
6443 }
6444
6445 #[test]
6446 fn test_conditional_requirements_treat_env_as_explicit() {
6447 let spec: Spec = r#"
6448flag "--dir <dir>" env="INPUT_DIR"
6449flag "--stdin" env="USE_STDIN"
6450flag "--file <file>" required_if="--dir" required_unless="--stdin"
6451 "#
6452 .parse()
6453 .unwrap();
6454
6455 assert_parse_err(
6456 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
6457 "Missing required flag: --file <file>",
6458 );
6459 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
6460 }
6461
6462 #[test]
6463 fn test_custom_env_does_not_fall_back_to_process_env() {
6464 assert!(std::env::var("PATH").is_ok());
6465 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
6466
6467 assert_parse_err(
6468 parse_with_env(&spec, &["test"], &[]),
6469 "Missing required flag: --file <file>",
6470 );
6471 }
6472
6473 #[test]
6474 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
6475 let spec: Spec = r#"
6476flag "--dir <dir>" default="src"
6477flag "--file <file>" required_if="--dir"
6478 "#
6479 .parse()
6480 .unwrap();
6481
6482 parse(&spec, &input(&["test"])).unwrap();
6483 }
6484
6485 #[test]
6486 fn test_conditional_requirements_see_overridden_flags_as_absent() {
6487 let spec: Spec = r#"
6488flag "--stdin"
6489flag "--dir <dir>" overrides="--stdin"
6490flag "--file <file>" required_unless="--stdin"
6491 "#
6492 .parse()
6493 .unwrap();
6494
6495 assert_parse_err(
6496 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
6497 "Missing required flag: --file <file>",
6498 );
6499 }
6500
6501 #[test]
6502 fn short_flag_is_one_character_not_one_byte() {
6503 let spec = spec_with_flag(
6507 SpecFlag::builder()
6508 .short('磨')
6509 .long("polish")
6510 .arg(SpecArg::builder().name("opt").build())
6511 .build(),
6512 );
6513 let attached = Parser::new(&spec)
6514 .parse(&input(&["test", "-磨VALUE"]))
6515 .unwrap();
6516 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
6517 let detached = Parser::new(&spec)
6518 .parse(&input(&["test", "-磨", "V"]))
6519 .unwrap();
6520 assert_eq!(flag_string_value(&detached, "polish"), "V");
6521 }
6522
6523 #[test]
6524 fn test_as_env() {
6525 let cmd = SpecCommand::builder()
6526 .name("test")
6527 .arg(SpecArg::builder().name("arg").build())
6528 .flag(SpecFlag::builder().long("flag").build())
6529 .flag(
6530 SpecFlag::builder()
6531 .long("force")
6532 .negate("--no-force")
6533 .build(),
6534 )
6535 .build();
6536 let spec = Spec {
6537 name: "test".to_string(),
6538 bin: "test".to_string(),
6539 cmd,
6540 ..Default::default()
6541 };
6542 let input = vec![
6543 "test".to_string(),
6544 "--flag".to_string(),
6545 "--no-force".to_string(),
6546 ];
6547 let parsed = parse(&spec, &input).unwrap();
6548 let env = parsed.as_env();
6549 assert_eq!(env.len(), 2);
6550 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
6551 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
6552 }
6553
6554 #[test]
6555 fn as_env_names_the_subcommand_path() {
6556 let spec: Spec = r#"
6557 flag "-v --verbose"
6558 cmd "db" {
6559 cmd "migrate" {
6560 alias "m"
6561 arg "[target]"
6562 }
6563 }
6564 "#
6565 .parse()
6566 .unwrap();
6567 let env = |args: &[&str]| {
6568 let input: Vec<String> = std::iter::once("test")
6569 .chain(args.iter().copied())
6570 .map(String::from)
6571 .collect();
6572 parse(&spec, &input).unwrap().as_env()
6573 };
6574
6575 let nested = env(&["db", "m", "v2"]);
6577 assert_eq!(
6578 nested.get("usage_cmd").map(String::as_str),
6579 Some("db migrate")
6580 );
6581 assert_eq!(nested.get("usage_target").map(String::as_str), Some("v2"));
6582 assert_eq!(
6583 env(&["db"]).get("usage_cmd").map(String::as_str),
6584 Some("db")
6585 );
6586 assert!(!env(&["-v"]).contains_key("usage_cmd"));
6587 }
6588
6589 #[test]
6590 fn as_env_leaves_usage_cmd_to_a_declared_cmd() {
6591 let spec: Spec = r#"
6593 cmd "run" {
6594 arg "[cmd]"
6595 }
6596 "#
6597 .parse()
6598 .unwrap();
6599 let input = ["test", "run", "build"].map(String::from);
6600 let env = parse(&spec, &input).unwrap().as_env();
6601 assert_eq!(env.get("usage_cmd").map(String::as_str), Some("build"));
6602 let input = ["test", "run"].map(String::from);
6604 assert!(
6605 !parse(&spec, &input)
6606 .unwrap()
6607 .as_env()
6608 .contains_key("usage_cmd")
6609 );
6610
6611 let spec: Spec = r#"
6613 flag "--cmd <cmd>" global=#true
6614 cmd "run"
6615 "#
6616 .parse()
6617 .unwrap();
6618 let input = ["test", "run"].map(String::from);
6619 assert!(
6620 !parse(&spec, &input)
6621 .unwrap()
6622 .as_env()
6623 .contains_key("usage_cmd")
6624 );
6625
6626 let spec: Spec = r#"
6628 cmd "run" {
6629 clause "steps" separator=":::" {
6630 arg "[cmd]"
6631 }
6632 }
6633 "#
6634 .parse()
6635 .unwrap();
6636 let input = ["test", "run"].map(String::from);
6637 assert!(
6638 !parse(&spec, &input)
6639 .unwrap()
6640 .as_env()
6641 .contains_key("usage_cmd")
6642 );
6643 }
6644
6645 #[test]
6646 fn test_arg_env_var() {
6647 let cmd = SpecCommand::builder()
6648 .name("test")
6649 .arg(
6650 SpecArg::builder()
6651 .name("input")
6652 .env("TEST_ARG_INPUT")
6653 .required(true)
6654 .build(),
6655 )
6656 .build();
6657 let spec = Spec {
6658 name: "test".to_string(),
6659 bin: "test".to_string(),
6660 cmd,
6661 ..Default::default()
6662 };
6663
6664 unsafe { std::env::set_var("TEST_ARG_INPUT", "test_file.txt") };
6667
6668 let input = vec!["test".to_string()];
6669 let parsed = parse(&spec, &input).unwrap();
6670
6671 assert_eq!(parsed.args.len(), 1);
6672 let arg = parsed.args.keys().next().unwrap();
6673 assert_eq!(arg.name, "input");
6674 let value = parsed.args.values().next().unwrap();
6675 assert_eq!(value.to_string(), "test_file.txt");
6676
6677 unsafe { std::env::remove_var("TEST_ARG_INPUT") };
6680 }
6681
6682 #[test]
6683 fn test_flag_env_var_with_arg() {
6684 let cmd = SpecCommand::builder()
6685 .name("test")
6686 .flag(
6687 SpecFlag::builder()
6688 .long("output")
6689 .env("TEST_FLAG_OUTPUT")
6690 .arg(SpecArg::builder().name("file").build())
6691 .build(),
6692 )
6693 .build();
6694 let spec = Spec {
6695 name: "test".to_string(),
6696 bin: "test".to_string(),
6697 cmd,
6698 ..Default::default()
6699 };
6700
6701 unsafe { std::env::set_var("TEST_FLAG_OUTPUT", "output.txt") };
6704
6705 let input = vec!["test".to_string()];
6706 let parsed = parse(&spec, &input).unwrap();
6707
6708 assert_eq!(parsed.flags.len(), 1);
6709 let flag = parsed.flags.keys().next().unwrap();
6710 assert_eq!(flag.name, "output");
6711 let value = parsed.flags.values().next().unwrap();
6712 assert_eq!(value.to_string(), "output.txt");
6713
6714 unsafe { std::env::remove_var("TEST_FLAG_OUTPUT") };
6717 }
6718
6719 #[test]
6720 fn test_flag_env_var_boolean() {
6721 let cmd = SpecCommand::builder()
6722 .name("test")
6723 .flag(
6724 SpecFlag::builder()
6725 .long("verbose")
6726 .env("TEST_FLAG_VERBOSE")
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 unsafe { std::env::set_var("TEST_FLAG_VERBOSE", "true") };
6740
6741 let input = vec!["test".to_string()];
6742 let parsed = parse(&spec, &input).unwrap();
6743
6744 assert_eq!(parsed.flags.len(), 1);
6745 let flag = parsed.flags.keys().next().unwrap();
6746 assert_eq!(flag.name, "verbose");
6747 let value = parsed.flags.values().next().unwrap();
6748 assert_eq!(value.to_string(), "true");
6749
6750 unsafe { std::env::remove_var("TEST_FLAG_VERBOSE") };
6753 }
6754
6755 #[test]
6756 fn test_env_var_precedence() {
6757 let cmd = SpecCommand::builder()
6759 .name("test")
6760 .arg(
6761 SpecArg::builder()
6762 .name("input")
6763 .env("TEST_PRECEDENCE_INPUT")
6764 .required(true)
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 unsafe { std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt") };
6778
6779 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
6780 let parsed = parse(&spec, &input).unwrap();
6781
6782 assert_eq!(parsed.args.len(), 1);
6783 let value = parsed.args.values().next().unwrap();
6784 assert_eq!(value.to_string(), "cli_file.txt");
6786
6787 unsafe { std::env::remove_var("TEST_PRECEDENCE_INPUT") };
6790 }
6791
6792 #[test]
6793 fn test_flag_var_true_with_single_default() {
6794 let cmd = SpecCommand::builder()
6796 .name("test")
6797 .flag(
6798 SpecFlag::builder()
6799 .long("foo")
6800 .var(true)
6801 .arg(SpecArg::builder().name("foo").build())
6802 .default_value("bar")
6803 .build(),
6804 )
6805 .build();
6806 let spec = Spec {
6807 name: "test".to_string(),
6808 bin: "test".to_string(),
6809 cmd,
6810 ..Default::default()
6811 };
6812
6813 let input = vec!["test".to_string()];
6815 let parsed = parse(&spec, &input).unwrap();
6816
6817 assert_eq!(parsed.flags.len(), 1);
6818 let flag = parsed.flags.keys().next().unwrap();
6819 assert_eq!(flag.name, "foo");
6820 let value = parsed.flags.values().next().unwrap();
6821 match value {
6823 ParseValue::MultiString(v) => {
6824 assert_eq!(v.len(), 1);
6825 assert_eq!(v[0], "bar");
6826 }
6827 _ => panic!("Expected MultiString, got {:?}", value),
6828 }
6829 }
6830
6831 #[test]
6832 fn test_flag_var_true_with_multiple_defaults() {
6833 let cmd = SpecCommand::builder()
6835 .name("test")
6836 .flag(
6837 SpecFlag::builder()
6838 .long("foo")
6839 .var(true)
6840 .arg(SpecArg::builder().name("foo").build())
6841 .default_values(["xyz", "bar"])
6842 .build(),
6843 )
6844 .build();
6845 let spec = Spec {
6846 name: "test".to_string(),
6847 bin: "test".to_string(),
6848 cmd,
6849 ..Default::default()
6850 };
6851
6852 let input = vec!["test".to_string()];
6854 let parsed = parse(&spec, &input).unwrap();
6855
6856 assert_eq!(parsed.flags.len(), 1);
6857 let value = parsed.flags.values().next().unwrap();
6858 match value {
6860 ParseValue::MultiString(v) => {
6861 assert_eq!(v.len(), 2);
6862 assert_eq!(v[0], "xyz");
6863 assert_eq!(v[1], "bar");
6864 }
6865 _ => panic!("Expected MultiString, got {:?}", value),
6866 }
6867 }
6868
6869 #[test]
6870 fn test_flag_var_false_with_default_remains_string() {
6871 let cmd = SpecCommand::builder()
6873 .name("test")
6874 .flag(
6875 SpecFlag::builder()
6876 .long("foo")
6877 .var(false) .arg(SpecArg::builder().name("foo").build())
6879 .default_value("bar")
6880 .build(),
6881 )
6882 .build();
6883 let spec = Spec {
6884 name: "test".to_string(),
6885 bin: "test".to_string(),
6886 cmd,
6887 ..Default::default()
6888 };
6889
6890 let input = vec!["test".to_string()];
6892 let parsed = parse(&spec, &input).unwrap();
6893
6894 assert_eq!(parsed.flags.len(), 1);
6895 let value = parsed.flags.values().next().unwrap();
6896 match value {
6898 ParseValue::String(s) => {
6899 assert_eq!(s, "bar");
6900 }
6901 _ => panic!("Expected String, got {:?}", value),
6902 }
6903 }
6904
6905 #[test]
6906 fn test_arg_var_true_with_single_default() {
6907 let cmd = SpecCommand::builder()
6909 .name("test")
6910 .arg(
6911 SpecArg::builder()
6912 .name("files")
6913 .var(true)
6914 .default_value("default.txt")
6915 .required(false)
6916 .build(),
6917 )
6918 .build();
6919 let spec = Spec {
6920 name: "test".to_string(),
6921 bin: "test".to_string(),
6922 cmd,
6923 ..Default::default()
6924 };
6925
6926 let input = vec!["test".to_string()];
6928 let parsed = parse(&spec, &input).unwrap();
6929
6930 assert_eq!(parsed.args.len(), 1);
6931 let value = parsed.args.values().next().unwrap();
6932 match value {
6934 ParseValue::MultiString(v) => {
6935 assert_eq!(v.len(), 1);
6936 assert_eq!(v[0], "default.txt");
6937 }
6938 _ => panic!("Expected MultiString, got {:?}", value),
6939 }
6940 }
6941
6942 #[test]
6943 fn test_arg_var_true_with_multiple_defaults() {
6944 let cmd = SpecCommand::builder()
6946 .name("test")
6947 .arg(
6948 SpecArg::builder()
6949 .name("files")
6950 .var(true)
6951 .default_values(["file1.txt", "file2.txt"])
6952 .required(false)
6953 .build(),
6954 )
6955 .build();
6956 let spec = Spec {
6957 name: "test".to_string(),
6958 bin: "test".to_string(),
6959 cmd,
6960 ..Default::default()
6961 };
6962
6963 let input = vec!["test".to_string()];
6965 let parsed = parse(&spec, &input).unwrap();
6966
6967 assert_eq!(parsed.args.len(), 1);
6968 let value = parsed.args.values().next().unwrap();
6969 match value {
6971 ParseValue::MultiString(v) => {
6972 assert_eq!(v.len(), 2);
6973 assert_eq!(v[0], "file1.txt");
6974 assert_eq!(v[1], "file2.txt");
6975 }
6976 _ => panic!("Expected MultiString, got {:?}", value),
6977 }
6978 }
6979
6980 #[test]
6981 fn test_arg_var_false_with_default_remains_string() {
6982 let cmd = SpecCommand::builder()
6984 .name("test")
6985 .arg(
6986 SpecArg::builder()
6987 .name("file")
6988 .var(false)
6989 .default_value("default.txt")
6990 .required(false)
6991 .build(),
6992 )
6993 .build();
6994 let spec = Spec {
6995 name: "test".to_string(),
6996 bin: "test".to_string(),
6997 cmd,
6998 ..Default::default()
6999 };
7000
7001 let input = vec!["test".to_string()];
7003 let parsed = parse(&spec, &input).unwrap();
7004
7005 assert_eq!(parsed.args.len(), 1);
7006 let value = parsed.args.values().next().unwrap();
7007 match value {
7009 ParseValue::String(s) => {
7010 assert_eq!(s, "default.txt");
7011 }
7012 _ => panic!("Expected String, got {:?}", value),
7013 }
7014 }
7015
7016 #[test]
7017 fn test_scalar_defaults_validate_only_first_default_choice() {
7018 let specs = [
7019 spec_with_arg(
7020 SpecArg::builder()
7021 .name("env")
7022 .var(false)
7023 .default_values(["dev", "prod"])
7024 .choices(["dev"])
7025 .required(false)
7026 .build(),
7027 ),
7028 spec_with_flag(
7029 SpecFlag::builder()
7030 .long("env")
7031 .arg(
7032 SpecArg::builder()
7033 .name("env")
7034 .default_values(["dev", "prod"])
7035 .choices(["dev"])
7036 .build(),
7037 )
7038 .build(),
7039 ),
7040 ];
7041
7042 for spec in specs {
7043 let parsed = parse(&spec, &input(&["test"])).unwrap();
7044 assert_eq!(first_string_value(&parsed), "dev");
7045 }
7046 }
7047
7048 #[test]
7049 fn a_delimiter_turns_one_word_into_several_values() {
7050 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
7051 .parse()
7052 .unwrap();
7053
7054 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
7055 let multi = |value: &ParseValue| match value {
7056 ParseValue::MultiString(values) => values.clone(),
7057 other => panic!("expected several values, got {other:?}"),
7058 };
7059 let tags = parsed
7060 .flags
7061 .iter()
7062 .find(|(f, _)| f.name == "tags")
7063 .map(|(_, v)| v)
7064 .unwrap();
7065 assert_eq!(multi(tags), vec!["a", "b", "c"]);
7066 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
7067 }
7068
7069 #[test]
7070 fn a_positional_splits_before_its_choices_are_asked() {
7071 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
7075 .parse()
7076 .unwrap();
7077
7078 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
7079
7080 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
7081 let message = err.to_string();
7082 assert!(message.contains("nowhere"), "{message}");
7083 assert!(
7084 !message.contains("src:nowhere"),
7085 "the bad half should be named, not the whole word: {message}"
7086 );
7087 }
7088
7089 #[test]
7090 fn a_split_value_is_counted_and_judged_as_values() {
7091 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
7095 .parse()
7096 .unwrap();
7097
7098 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
7099 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
7100 assert!(err.to_string().contains("staging"), "{err}");
7101 assert!(
7102 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
7103 "three values should breach var_max=2"
7104 );
7105 }
7106
7107 #[test]
7108 fn a_split_bound_counts_one_occurrence_at_a_time() {
7109 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
7114 .parse()
7115 .unwrap();
7116
7117 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
7118 assert!(
7119 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
7120 "three values out of one word is still three values"
7121 );
7122 parse(
7125 &spec,
7126 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
7127 )
7128 .expect("two per occurrence, twice, is within the bound");
7129 }
7130
7131 #[test]
7132 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
7133 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
7134 .parse()
7135 .unwrap();
7136
7137 parse(
7138 &spec,
7139 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
7140 )
7141 .expect("each occurrence satisfies the bound independently");
7142
7143 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
7144 assert!(
7145 error
7146 .to_string()
7147 .contains("requires at least 2 value(s), got 1"),
7148 "{error:?}"
7149 );
7150 }
7151
7152 #[test]
7153 fn an_exclusive_flag_has_to_be_alone() {
7154 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
7155 .parse()
7156 .unwrap();
7157
7158 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
7159
7160 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
7162 assert!(err.to_string().contains("on its own"), "{err}");
7163
7164 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
7167 assert!(err.to_string().contains("on its own"), "{err}");
7168
7169 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
7171 }
7172
7173 #[test]
7174 fn an_exclusive_flag_conflicts_with_clause_arguments() {
7175 let spec: Spec = r#"name "ex"
7176bin "ex"
7177flag "--dump" exclusive=#true
7178clause "tasks" separator=":::" {
7179 arg "<task>"
7180}
7181"#
7182 .parse()
7183 .unwrap();
7184
7185 let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
7186 assert!(err.to_string().contains("on its own"), "{err}");
7187 }
7188
7189 #[test]
7190 fn a_partial_parse_reports_the_next_implicit_clause_argument() {
7191 let spec: Spec = r#"name "ex"
7192bin "ex"
7193clause "tools" {
7194 arg "<tool@version>"
7195}
7196"#
7197 .parse()
7198 .unwrap();
7199
7200 let parsed = parse_partial(&spec, &input(&["ex"])).unwrap();
7201
7202 assert_eq!(
7203 parsed.next_arg.as_ref().map(|arg| arg.name.as_str()),
7204 Some("tool@version")
7205 );
7206 }
7207
7208 #[test]
7209 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
7210 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
7213 .parse()
7214 .unwrap();
7215
7216 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
7217 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
7218 }
7219
7220 #[test]
7221 fn an_exclusive_flag_bypasses_required_siblings() {
7222 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
7223 .parse()
7224 .unwrap();
7225
7226 parse(&spec, &input(&["ex", "--dump"]))
7227 .expect("exclusive is the command's requiredness escape");
7228 assert!(
7229 parse(
7230 &spec,
7231 &input(&["ex", "--dump", "--out", "somewhere", "target"])
7232 )
7233 .is_err()
7234 );
7235 }
7236
7237 #[test]
7238 fn an_environment_value_counts_for_an_exclusive_flag() {
7239 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
7240 .parse()
7241 .unwrap();
7242
7243 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
7244 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
7245 }
7246
7247 #[test]
7248 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
7249 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
7250 .parse()
7251 .unwrap();
7252
7253 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
7254 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
7255 }
7256
7257 #[test]
7258 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
7259 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7260 .parse()
7261 .unwrap();
7262
7263 parse(&spec, &input(&["ex", "run", "--clean"]))
7264 .expect("the child flag is alone within the child command");
7265 assert!(
7266 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7267 "the parent flag still conflicts with selecting the child"
7268 );
7269 }
7270
7271 #[test]
7272 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
7273 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7274 .parse()
7275 .unwrap();
7276
7277 parse(&spec, &input(&["ex", "run", "--clean"]))
7278 .expect("the child-local exclusive flag is alone inside the child command");
7279 assert!(
7280 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
7281 "the ancestor spelling still conflicts with selecting the child"
7282 );
7283 }
7284
7285 #[test]
7286 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
7287 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
7288 .parse()
7289 .unwrap();
7290
7291 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
7292 assert!(
7293 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
7294 "the distinct parent declaration is still company despite sharing a name"
7295 );
7296 }
7297
7298 #[test]
7299 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
7300 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7301 .parse()
7302 .unwrap();
7303
7304 parse(&spec, &input(&["ex", "run", "--clean"]))
7305 .expect("the child exclusive flag is valid alone");
7306 assert!(
7307 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7308 "merging with the inherited global must not discard child exclusivity"
7309 );
7310 }
7311
7312 #[test]
7313 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
7314 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7315 .parse()
7316 .unwrap();
7317
7318 parse(&spec, &input(&["ex", "run", "--clean"]))
7319 .expect("the typed long form belongs to the child declaration");
7320 assert!(
7321 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7322 "the inherited short form still belongs to the ancestor"
7323 );
7324 assert!(
7325 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
7326 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
7327 );
7328 }
7329
7330 #[test]
7331 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
7332 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
7333 .parse()
7334 .unwrap();
7335
7336 parse(&spec, &input(&["ex", "run", "--clean"]))
7337 .expect("the child's spelling does not activate the orphan ancestor alias");
7338 assert!(
7339 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7340 "the inherited short alias still belongs to the ancestor exclusive flag"
7341 );
7342 }
7343
7344 #[test]
7345 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
7346 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"
7347 .parse()
7348 .unwrap();
7349
7350 assert!(
7351 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
7352 "the inherited negated alias still belongs to the ancestor exclusive flag"
7353 );
7354 }
7355
7356 #[test]
7357 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
7358 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"
7363 .parse()
7364 .unwrap();
7365
7366 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
7367 assert!(
7368 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7369 "one unrelated alias collision cannot disown the child from its own flag"
7370 );
7371 }
7372
7373 #[test]
7374 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
7375 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7381 .parse()
7382 .unwrap();
7383 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
7384 .expect("the ancestor owns this occurrence, and it is not exclusive");
7385 assert!(
7386 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
7387 "after the subcommand word the child's declaration is in scope, and it is exclusive"
7388 );
7389
7390 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
7391 .parse()
7392 .unwrap();
7393 assert!(
7394 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
7395 "the same rule, with an exclusive ancestor: selecting the child is company for it"
7396 );
7397 }
7398
7399 #[test]
7400 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
7401 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
7405 .parse()
7406 .unwrap();
7407
7408 assert!(
7409 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
7410 "the orphan ancestor alias is still the ancestor's exclusive flag"
7411 );
7412 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
7413 .expect("the child's own spelling drops the exclusivity the child did not restate");
7414 }
7415
7416 #[test]
7417 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
7418 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
7422 .parse()
7423 .unwrap();
7424
7425 assert!(
7426 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
7427 "the child spelling is exclusive whatever it was typed beside"
7428 );
7429 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
7430 .expect("the ancestor's own spelling was never exclusive");
7431 }
7432
7433 #[test]
7434 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
7435 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"
7439 .parse()
7440 .unwrap();
7441
7442 assert!(
7443 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
7444 "the child added exclusivity the environment value has to honor"
7445 );
7446
7447 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"
7448 .parse()
7449 .unwrap();
7450
7451 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
7452 .expect("the child dropped the exclusivity, and the environment value follows it");
7453 }
7454
7455 #[test]
7456 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
7457 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"
7460 .parse()
7461 .unwrap();
7462
7463 parse(&spec, &input(&["ex", "run", "--clean"]))
7464 .expect("a merged child exclusive flag is still the command's requiredness escape");
7465 }
7466
7467 #[test]
7468 fn a_group_allows_one_member_and_refuses_two() {
7469 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
7470 .parse()
7471 .unwrap();
7472
7473 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
7475 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
7476
7477 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
7478 assert!(err.to_string().contains("group input"), "{err}");
7479 }
7480
7481 #[test]
7482 fn positional_selectors_work_in_conflicts_and_groups() {
7483 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
7484 .parse()
7485 .unwrap();
7486 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
7487 .expect("the flag alone is valid");
7488 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
7489 assert!(
7490 parse(
7491 &conflicts,
7492 &input(&["ex", "--from-file", "vars.env", "literal"])
7493 )
7494 .is_err()
7495 );
7496
7497 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
7498 .parse()
7499 .unwrap();
7500 assert!(
7501 parse(
7502 &positional_source,
7503 &input(&["ex", "--from-file", "vars.env", "literal"])
7504 )
7505 .is_err()
7506 );
7507
7508 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
7509 .parse()
7510 .unwrap();
7511 assert!(parse(&group, &input(&["ex"])).is_err());
7512 parse(&group, &input(&["ex", "target-name"]))
7513 .expect("a positional satisfies a required group");
7514 assert!(
7515 parse(
7516 &group,
7517 &input(&["ex", "--file", "input.txt", "target-name"])
7518 )
7519 .is_err()
7520 );
7521 }
7522
7523 #[test]
7524 fn a_required_group_needs_one_of_its_members() {
7525 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7526 .parse()
7527 .unwrap();
7528
7529 let err = parse(&spec, &input(&["ex"])).unwrap_err();
7530 assert!(err.to_string().contains("--file, --url"), "{err}");
7533 assert!(err.to_string().contains("input"), "{err}");
7534
7535 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
7536 }
7537
7538 #[test]
7539 fn a_multiple_group_only_polices_requiredness() {
7540 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
7543 .parse()
7544 .unwrap();
7545
7546 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
7547 assert!(parse(&spec, &input(&["ex"])).is_err());
7548 }
7549
7550 #[test]
7551 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
7552 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7558 .parse()
7559 .unwrap();
7560
7561 parse(&spec, &input(&["ex"])).expect("the default fills the group");
7562 parse(&spec, &input(&["ex", "--url", "u"]))
7563 .expect("the default must not conflict with the flag the user typed");
7564 }
7565
7566 #[test]
7567 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
7568 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
7571 .parse()
7572 .unwrap();
7573
7574 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
7575 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
7576
7577 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
7579 assert!(err.to_string().contains("group input"), "{err}");
7580 }
7581
7582 #[test]
7583 fn a_group_reads_the_environment_as_given() {
7584 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
7587 .parse()
7588 .unwrap();
7589
7590 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
7591 }
7592
7593 #[test]
7594 fn a_requirement_names_the_flag_that_is_missing() {
7595 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
7598 .parse()
7599 .unwrap();
7600
7601 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
7602 assert!(
7603 err.to_string().contains("format"),
7604 "the missing flag should be named: {err}"
7605 );
7606
7607 for words in [
7609 &["ex", "--out", "a.txt", "--format", "json"][..],
7610 &["ex", "--format", "json", "--out", "a.txt"][..],
7611 ] {
7612 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
7613 }
7614
7615 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
7618 }
7619
7620 #[test]
7621 fn a_value_activates_only_its_conditional_requirement() {
7622 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"
7623 .parse()
7624 .unwrap();
7625
7626 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
7627 .expect("an unrelated value requires nothing");
7628
7629 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
7630 assert!(key.to_string().contains("key"), "{key}");
7631 parse(
7632 &spec,
7633 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
7634 )
7635 .expect("the matching requirement is satisfied");
7636
7637 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
7638 assert!(token.to_string().contains("token"), "{token}");
7639 }
7640
7641 #[test]
7642 fn conditional_requirements_read_explicit_env_but_not_defaults() {
7643 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"
7644 .parse()
7645 .unwrap();
7646 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
7647 assert!(err.to_string().contains("key"), "{err}");
7648
7649 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"
7650 .parse()
7651 .unwrap();
7652 parse(&from_default, &input(&["ex"]))
7653 .expect("a default is not an explicit conditional value");
7654 }
7655
7656 #[test]
7657 fn command_line_values_override_env_for_conditional_requirements() {
7658 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
7659 .parse()
7660 .unwrap();
7661
7662 parse_with_env(
7663 &spec,
7664 &["ex", "--config", "ordinary.toml"],
7665 &[("EX_CONFIG", "special.toml")],
7666 )
7667 .expect("the command-line value takes precedence over the environment");
7668 }
7669
7670 #[test]
7671 fn conditional_requirements_normalize_boolean_env_values() {
7672 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
7673 .parse()
7674 .unwrap();
7675
7676 for value in ["1", "true", "True", "TRUE"] {
7677 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
7678 assert!(err.to_string().contains("key"), "{value}: {err}");
7679 }
7680 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
7681 .expect("a false environment value does not activate a true condition");
7682 }
7683
7684 #[test]
7685 fn a_default_satisfies_a_requirement() {
7686 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
7690 .parse()
7691 .unwrap();
7692
7693 parse(&spec, &input(&["ex", "--out", "a.txt"]))
7694 .expect("a defaulted flag is not a missing one");
7695 }
7696
7697 #[test]
7698 fn a_present_flag_binds_a_conditional_default() {
7699 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7700 .parse()
7701 .unwrap();
7702
7703 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
7704 assert_eq!(
7705 with.as_env().get("usage_bin_names").map(String::as_str),
7706 Some("true")
7707 );
7708
7709 let without = parse(&spec, &input(&["ex"])).unwrap();
7710 assert!(
7711 !without.as_env().contains_key("usage_bin_names"),
7712 "IsPresent does nothing when the selector is absent"
7713 );
7714 }
7715
7716 #[test]
7717 fn an_equals_condition_binds_a_conditional_default() {
7718 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
7719 .parse()
7720 .unwrap();
7721
7722 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
7723 assert_eq!(
7724 json.as_env().get("usage_style").map(String::as_str),
7725 Some("pretty")
7726 );
7727 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
7728 assert!(!yaml.as_env().contains_key("usage_style"));
7729 }
7730
7731 #[test]
7732 fn an_equals_condition_reads_a_negated_flag() {
7733 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
7734 .parse()
7735 .unwrap();
7736
7737 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
7738 assert_eq!(
7739 off.as_env().get("usage_pretty").map(String::as_str),
7740 Some("true")
7741 );
7742 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
7743 assert!(
7744 !on.as_env().contains_key("usage_pretty"),
7745 "--json is true, so when=false should miss"
7746 );
7747 }
7748
7749 #[test]
7750 fn the_first_matching_conditional_default_wins() {
7751 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"
7752 .parse()
7753 .unwrap();
7754
7755 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
7756 assert_eq!(
7757 out.as_env().get("usage_style").map(String::as_str),
7758 Some("compact")
7759 );
7760 }
7761
7762 #[test]
7763 fn argv_and_env_suppress_a_conditional_default() {
7764 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
7765 .parse()
7766 .unwrap();
7767
7768 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
7769 assert_eq!(
7770 from_env.as_env().get("usage_bin_names").map(String::as_str),
7771 Some("false"),
7772 "the target's environment wins over default_if"
7773 );
7774 }
7775
7776 #[test]
7777 fn a_sibling_env_activates_a_conditional_default() {
7778 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
7779 .parse()
7780 .unwrap();
7781
7782 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
7783 assert_eq!(
7784 out.as_env().get("usage_bin_names").map(String::as_str),
7785 Some("true")
7786 );
7787 }
7788
7789 #[test]
7790 fn a_default_does_not_activate_a_conditional_default() {
7791 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
7795 .parse()
7796 .unwrap();
7797
7798 let out = parse(&spec, &input(&["ex"])).unwrap();
7799 assert_eq!(
7800 out.as_env().get("usage_json").map(String::as_str),
7801 Some("true")
7802 );
7803 assert!(
7804 !out.as_env().contains_key("usage_pretty"),
7805 "a default is not an explicit value for default_if"
7806 );
7807 }
7808
7809 #[test]
7810 fn a_conditional_default_does_not_activate_requires_if() {
7811 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"
7812 .parse()
7813 .unwrap();
7814
7815 parse(&spec, &input(&["ex", "--json"]))
7816 .expect("a default_if value is not explicit for requires_if");
7817 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
7818 }
7819
7820 #[test]
7821 fn a_conditional_default_satisfies_a_requirement() {
7822 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
7823 .parse()
7824 .unwrap();
7825
7826 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
7827 .expect("default_if fills the required flag");
7828 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7829 }
7830
7831 #[test]
7832 fn an_environment_value_satisfies_a_requirement() {
7833 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
7834 .parse()
7835 .unwrap();
7836
7837 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
7838 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
7839 .expect("the environment supplies it");
7840 }
7841
7842 #[test]
7843 fn a_requirement_is_satisfied_by_a_short_form() {
7844 let spec: Spec =
7848 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
7849 .parse()
7850 .unwrap();
7851
7852 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
7853
7854 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
7855 assert!(err.to_string().contains("key"), "{err}");
7856 }
7857
7858 #[test]
7859 fn conflicting_flags_are_rejected_in_either_order() {
7860 let spec: Spec =
7864 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
7865 .parse()
7866 .unwrap();
7867
7868 for words in [
7869 &["ex", "--file", "a.txt", "--stdin"][..],
7870 &["ex", "--stdin", "--file", "a.txt"][..],
7871 ] {
7872 let err = parse(&spec, &input(words)).unwrap_err();
7873 assert!(
7874 err.to_string().contains("conflicts with --stdin"),
7875 "{words:?} should be refused: {err}"
7876 );
7877 }
7878
7879 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
7881 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
7882 }
7883
7884 #[test]
7885 fn unknown_flags_are_values_by_default() {
7886 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
7889 .parse()
7890 .unwrap();
7891 let out = parse(
7892 &spec,
7893 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
7894 )
7895 .unwrap();
7896 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
7897 assert_eq!(out.args[rest].to_string(), "--wat x");
7898 }
7899
7900 #[test]
7901 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
7902 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7903 .parse()
7904 .unwrap();
7905 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
7906 .expect("a repeat is a correction by default");
7907 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7908 assert_eq!(out.flags[jobs].to_string(), "2");
7909 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
7910 .expect("switches use the same default");
7911
7912 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
7913 .parse()
7914 .unwrap();
7915 for words in [
7916 &["ex", "--jobs", "1", "--jobs", "2"][..],
7917 &["ex", "--verbose", "--verbose"][..],
7918 ] {
7919 let err = parse(&strict, &input(words)).unwrap_err();
7920 assert!(
7921 err.to_string().contains("cannot be used multiple times"),
7922 "{err}"
7923 );
7924 }
7925
7926 let reparsed: Spec = strict.to_string().parse().unwrap();
7927 assert!(!reparsed.cmd.args_override_self);
7928 }
7929
7930 #[test]
7931 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
7932 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
7933 .parse()
7934 .unwrap();
7935
7936 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
7937 .expect("opposite forms override each other");
7938 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
7939 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
7940
7941 for words in [
7942 &["ex", "--color", "--color"][..],
7943 &["ex", "--no-color", "--no-color"][..],
7944 ] {
7945 let err = parse(&spec, &input(words)).unwrap_err();
7946 assert!(err.to_string().contains("cannot be used multiple times"));
7947 }
7948 }
7949
7950 #[test]
7951 fn strict_global_flags_may_repeat_across_command_levels() {
7952 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"
7953 .parse()
7954 .unwrap();
7955
7956 let out = parse(
7957 &spec,
7958 &input(&[
7959 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
7960 ]),
7961 )
7962 .expect("an inherited global is allowed once at each command level");
7963 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
7964 assert_eq!(out.flags[jobs].to_string(), "2");
7965
7966 for words in [
7967 &["ex", "--color", "--color", "run", "--no-color"][..],
7968 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
7969 ] {
7970 let err = parse(&spec, &input(words)).unwrap_err();
7971 assert!(err.to_string().contains("cannot be used multiple times"));
7972 }
7973 }
7974
7975 #[test]
7976 fn a_subcommand_can_negate_only_its_parents_requirements() {
7977 let base = r#"name "ex"
7978bin "ex"
7979flag "--config" required=#true
7980flag "--mode" requires="--config"
7981flag "--other"
7982arg "<input>"
7983group "source" "--config" "--other" required=#true
7984cmd "run" { flag "--child" required=#true }
7985"#;
7986 let strict: Spec = base.parse().unwrap();
7987 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
7988 let message = err.to_string();
7989 assert!(
7990 message.contains("input") || message.contains("config"),
7991 "{message}"
7992 );
7993
7994 let negated: Spec = base
7995 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
7996 .parse()
7997 .unwrap();
7998 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
7999 assert!(
8000 err.to_string().contains("child"),
8001 "the selected command keeps its own requirements: {err}"
8002 );
8003
8004 let mut child_optional = negated.clone();
8005 child_optional.cmd.subcommands["run"].flags[0].required = false;
8006 parse(&child_optional, &input(&["ex", "run"]))
8007 .expect("the child selection satisfies all parent requirements");
8008 parse(&child_optional, &input(&["ex", "--mode", "run"]))
8009 .expect("parent requires relationships are negated too");
8010 }
8011
8012 #[test]
8013 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
8014 let spec: Spec = r#"name "ex"
8015bin "ex"
8016args_conflicts_with_subcommands #true
8017flag "--verbose"
8018cmd "run"
8019"#
8020 .parse()
8021 .unwrap();
8022
8023 parse(&spec, &input(&["ex", "run"]))
8024 .expect("the subcommand is valid without a parent argument");
8025 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
8026 assert!(
8027 err.to_string().contains("cannot be used with arguments"),
8028 "{err}"
8029 );
8030 }
8031
8032 #[test]
8033 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
8034 let base = r#"name "ex"
8035bin "ex"
8036flag "--values <value>..."
8037cmd "run"
8038"#;
8039 let plain: Spec = base.parse().unwrap();
8040 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
8041 assert_eq!(out.cmd.name, "ex");
8042
8043 let precedence: Spec = base
8044 .replacen(
8045 "bin \"ex\"",
8046 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
8047 1,
8048 )
8049 .parse()
8050 .unwrap();
8051 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
8052 assert_eq!(out.cmd.name, "run");
8053 }
8054
8055 #[test]
8056 fn a_required_positional_can_follow_an_unfilled_optional_one() {
8057 let base = r#"name "ex"
8058bin "ex"
8059arg "[optional]"
8060arg "<required>"
8061"#;
8062 let plain: Spec = base.parse().unwrap();
8063 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
8064 assert!(err.to_string().contains("required"), "{err}");
8065
8066 let enabled: Spec = base
8067 .replacen(
8068 "bin \"ex\"",
8069 "bin \"ex\"\nallow_missing_positional #true",
8070 1,
8071 )
8072 .parse()
8073 .unwrap();
8074 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
8075 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
8076 let value = &out
8077 .args
8078 .iter()
8079 .find(|(arg, _)| arg.name == "required")
8080 .unwrap()
8081 .1;
8082 assert!(matches!(value, ParseValue::String(value) if value == "value"));
8083 }
8084
8085 #[test]
8086 fn sigil_args_do_not_block_optional_positional_skipping() {
8087 let spec: Spec = r#"name "ex"
8088bin "ex"
8089allow_missing_positional #true
8090arg "[optional]"
8091arg "[tool]" sigil="@"
8092arg "<required>"
8093"#
8094 .parse()
8095 .unwrap();
8096
8097 let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
8098 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
8099 let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
8100 let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
8101 assert_eq!(out.args[tool].to_string(), "node");
8102 assert_eq!(out.args[required].to_string(), "value");
8103 }
8104
8105 #[test]
8106 fn unknown_env_refuses_every_undeclared_variable_under_the_prefix() {
8107 let spec: Spec = r#"name "ex"
8108bin "ex"
8109unknown_env prefix="EX_" {
8110 allow "EX_LIBRARY"
8111}
8112flag "--port <port>" env="EX_PORT"
8113cmd "serve" {
8114 flag "--tls" deprecated_env="EX_SSL"
8115}
8116cmd "publish"
8117"#
8118 .parse()
8119 .unwrap();
8120 let env = |names: &[&str]| -> HashMap<String, String> {
8121 names.iter().map(|n| (n.to_string(), "1".into())).collect()
8122 };
8123 let argv = input(&["ex", "publish"]);
8124
8125 let out = Parser::new(&spec)
8128 .with_env(env(&["EX_PORT", "EX_SSL", "EX_LIBRARY", "OTHER"]))
8129 .parse(&argv)
8130 .unwrap();
8131 assert!(out.errors.is_empty());
8132
8133 let err = Parser::new(&spec)
8134 .with_env(env(&["EX_PROT", "EX_PORT", "EX_TSL"]))
8135 .parse(&argv)
8136 .unwrap_err()
8137 .to_string();
8138 assert_eq!(
8139 err,
8140 "Unknown environment variables under EX_: EX_PROT, EX_TSL"
8141 );
8142
8143 let err = Parser::new(&spec)
8145 .with_env(env(&["EX_PROT"]))
8146 .parse(&input(&["ex", "--help"]))
8147 .unwrap_err()
8148 .to_string();
8149 assert!(!err.contains("EX_PROT"), "{err}");
8150 assert!(err.contains("Usage:"), "{err}");
8151 }
8152
8153 #[test]
8154 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
8155 let spec: Spec =
8156 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
8157 .parse()
8158 .unwrap();
8159 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
8160 assert!(
8161 err.to_string().contains("--wat"),
8162 "the message should name the token: {err}"
8163 );
8164
8165 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
8168 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8169 assert_eq!(out.args[rest].to_string(), "-1");
8170
8171 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
8172 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8173 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8174 }
8175
8176 #[test]
8177 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
8178 let spec: Spec = r#"
8179name "ex"
8180bin "ex"
8181unknown_flags "error"
8182flag "-0 --print0" global=#true
8183cmd "run" {
8184 flag "--force"
8185}
8186"#
8187 .parse()
8188 .unwrap();
8189 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
8190 assert_eq!(out.cmd.name, "run");
8191 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
8192 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
8193 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
8194 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
8195 }
8196
8197 #[test]
8198 fn a_command_may_override_the_cli_wide_setting() {
8199 let spec: Spec = r#"
8201name "ex"
8202bin "ex"
8203unknown_flags "error"
8204cmd "exec" unknown_flags="value" {
8205 arg "[rest]..."
8206}
8207cmd "build" {
8208 arg "[rest]..."
8209}
8210"#
8211 .parse()
8212 .unwrap();
8213
8214 let out = parse(
8215 &spec,
8216 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
8217 )
8218 .unwrap();
8219 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
8220 assert_eq!(out.args[rest].to_string(), "--wat");
8221
8222 assert!(
8223 parse(
8224 &spec,
8225 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
8226 )
8227 .is_err(),
8228 "a command that says nothing inherits the CLI's choice"
8229 );
8230 }
8231
8232 #[test]
8233 fn the_setting_survives_a_round_trip() {
8234 let spec: Spec =
8235 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
8236 .parse()
8237 .unwrap();
8238 let reparsed: Spec = spec.to_string().parse().unwrap();
8239 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
8240 assert_eq!(
8241 reparsed.cmd.subcommands["x"].unknown_flags,
8242 Some(UnknownFlags::Value)
8243 );
8244 }
8245
8246 #[test]
8247 fn test_default_subcommand() {
8248 let run_cmd = SpecCommand::builder()
8250 .name("run")
8251 .arg(SpecArg::builder().name("task").build())
8252 .build();
8253 let mut cmd = SpecCommand::builder().name("test").build();
8254 cmd.subcommands.insert("run".to_string(), run_cmd);
8255
8256 let spec = Spec {
8257 name: "test".to_string(),
8258 bin: "test".to_string(),
8259 cmd,
8260 default_subcommand: Some("run".to_string()),
8261 ..Default::default()
8262 };
8263
8264 let input = vec!["test".to_string(), "mytask".to_string()];
8266 let parsed = parse(&spec, &input).unwrap();
8267
8268 assert_eq!(parsed.cmds.len(), 2);
8270 assert_eq!(parsed.cmds[1].name, "run");
8271
8272 assert_eq!(parsed.args.len(), 1);
8274 let arg = parsed.args.keys().next().unwrap();
8275 assert_eq!(arg.name, "task");
8276 let value = parsed.args.values().next().unwrap();
8277 assert_eq!(value.to_string(), "mytask");
8278 }
8279
8280 #[test]
8281 fn default_subcommand_outranks_root_sigil_arg() {
8282 let spec: Spec = r#"
8283name "test"
8284bin "test"
8285default_subcommand "run"
8286arg "[tools]..." sigil="+"
8287cmd "run" { arg "<task>" }
8288"#
8289 .parse()
8290 .unwrap();
8291
8292 let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
8293 assert_eq!(parsed.cmd.name, "run");
8294 let value = |name| {
8295 parsed
8296 .args
8297 .iter()
8298 .find(|(arg, _)| arg.name == name)
8299 .map(|(_, value)| value.to_string())
8300 .unwrap()
8301 };
8302 assert_eq!(value("tools"), "node");
8303 assert_eq!(value("task"), "node");
8304 }
8305
8306 #[test]
8307 fn test_default_subcommand_explicit_still_works() {
8308 let run_cmd = SpecCommand::builder()
8310 .name("run")
8311 .arg(SpecArg::builder().name("task").build())
8312 .build();
8313 let other_cmd = SpecCommand::builder()
8314 .name("other")
8315 .arg(SpecArg::builder().name("other_arg").build())
8316 .build();
8317 let mut cmd = SpecCommand::builder().name("test").build();
8318 cmd.subcommands.insert("run".to_string(), run_cmd);
8319 cmd.subcommands.insert("other".to_string(), other_cmd);
8320
8321 let spec = Spec {
8322 name: "test".to_string(),
8323 bin: "test".to_string(),
8324 cmd,
8325 default_subcommand: Some("run".to_string()),
8326 ..Default::default()
8327 };
8328
8329 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
8331 let parsed = parse(&spec, &input).unwrap();
8332
8333 assert_eq!(parsed.cmds.len(), 2);
8335 assert_eq!(parsed.cmds[1].name, "other");
8336 }
8337
8338 #[test]
8339 fn test_default_subcommand_applies_only_at_the_root() {
8340 let mut config_ls = SpecCommand::builder().name("ls").build();
8346 config_ls.args.push(SpecArg::builder().name("what").build());
8347 let mut config_cmd = SpecCommand::builder().name("config").build();
8348 config_cmd.subcommands.insert("ls".to_string(), config_ls);
8349
8350 let mut root_ls = SpecCommand::builder().name("ls").build();
8353 root_ls.args.push(SpecArg::builder().name("what").build());
8354 let mut cmd = SpecCommand::builder().name("ex").build();
8355 cmd.subcommands.insert("ls".to_string(), root_ls);
8356 cmd.subcommands.insert("config".to_string(), config_cmd);
8357
8358 let spec = Spec {
8359 name: "ex".to_string(),
8360 bin: "ex".to_string(),
8361 cmd,
8362 default_subcommand: Some("ls".to_string()),
8363 ..Default::default()
8364 };
8365
8366 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
8370 assert!(
8371 parse(&spec, &input).is_err(),
8372 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
8373 );
8374
8375 let input = vec!["ex".to_string(), "zzz".to_string()];
8377 let parsed = parse(&spec, &input).expect("the root's default applies");
8378 assert_eq!(
8379 parsed
8380 .cmds
8381 .iter()
8382 .map(|c| c.name.as_str())
8383 .collect::<Vec<_>>(),
8384 ["ex", "ls"]
8385 );
8386 assert_eq!(
8387 parsed.args.values().next().map(|v| v.to_string()),
8388 Some("zzz".to_string()),
8389 "and the word binds inside the command it reached"
8390 );
8391 }
8392
8393 #[test]
8394 fn test_default_subcommand_with_nested_subcommands() {
8395 let say_cmd = SpecCommand::builder()
8399 .name("say")
8400 .arg(SpecArg::builder().name("name").build())
8401 .build();
8402 let mut run_cmd = SpecCommand::builder().name("run").build();
8403 run_cmd.subcommands.insert("say".to_string(), say_cmd);
8404
8405 let mut cmd = SpecCommand::builder().name("test").build();
8406 cmd.subcommands.insert("run".to_string(), run_cmd);
8407
8408 let spec = Spec {
8409 name: "test".to_string(),
8410 bin: "test".to_string(),
8411 cmd,
8412 default_subcommand: Some("run".to_string()),
8413 ..Default::default()
8414 };
8415
8416 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
8418 let parsed = parse(&spec, &input).unwrap();
8419
8420 assert_eq!(parsed.cmds.len(), 3);
8422 assert_eq!(parsed.cmds[0].name, "test");
8423 assert_eq!(parsed.cmds[1].name, "run");
8424 assert_eq!(parsed.cmds[2].name, "say");
8425
8426 assert_eq!(parsed.args.len(), 1);
8428 let arg = parsed.args.keys().next().unwrap();
8429 assert_eq!(arg.name, "name");
8430 let value = parsed.args.values().next().unwrap();
8431 assert_eq!(value.to_string(), "hello");
8432 }
8433
8434 fn mounted_global_flag_spec() -> Spec {
8442 let task_cmd = SpecCommand::builder()
8443 .name("sample:run")
8444 .arg(
8445 SpecArg::builder()
8446 .name("profile")
8447 .choices(["alpha", "beta", "gamma"])
8448 .build(),
8449 )
8450 .build();
8451 let mut run_cmd = SpecCommand::builder()
8453 .name("run")
8454 .flag(
8455 SpecFlag::builder()
8456 .name("cd")
8457 .short('C')
8458 .long("cd")
8459 .arg(SpecArg::builder().name("dir").build())
8460 .global(false)
8461 .build(),
8462 )
8463 .build();
8464 run_cmd
8465 .subcommands
8466 .insert("sample:run".to_string(), task_cmd);
8467
8468 let mut cmd = SpecCommand::builder()
8469 .name("test")
8470 .flag(
8471 SpecFlag::builder()
8472 .name("cd")
8473 .short('C')
8474 .long("cd")
8475 .arg(SpecArg::builder().name("dir").build())
8476 .global(true)
8477 .build(),
8478 )
8479 .build();
8480 cmd.subcommands.insert("run".to_string(), run_cmd);
8481
8482 Spec {
8483 name: "test".to_string(),
8484 bin: "test".to_string(),
8485 cmd,
8486 ..Default::default()
8487 }
8488 }
8489
8490 #[test]
8491 fn test_prefix_global_flag_does_not_pollute_choices() {
8492 let spec = mounted_global_flag_spec();
8499
8500 for words in [
8503 &["test", "-C", "/tmp", "run", "sample:run"][..],
8504 &["test", "--cd=/tmp", "run", "sample:run"][..],
8506 ] {
8507 let parsed = parse_partial(&spec, &input(words)).unwrap();
8508 assert_eq!(
8509 parsed
8510 .cmds
8511 .iter()
8512 .map(|c| c.name.as_str())
8513 .collect::<Vec<_>>(),
8514 vec!["test", "run", "sample:run"],
8515 );
8516 assert!(
8518 parsed.args.is_empty(),
8519 "args should be empty, got {:?}",
8520 parsed.args
8521 );
8522
8523 let cd = parsed
8526 .available_flags
8527 .get("--cd")
8528 .expect("--cd should remain available after descending into the subcommand");
8529 assert!(cd.global, "--cd must stay global after descent");
8530 assert!(
8531 parsed.available_flags.get("-C").is_some_and(|f| f.global),
8532 "-C must stay global after descent",
8533 );
8534
8535 assert_eq!(
8539 parsed.as_env().get("usage_cd").map(String::as_str),
8540 Some("/tmp"),
8541 "global flag value must survive in as_env(), got {:?}",
8542 parsed.as_env(),
8543 );
8544 }
8545
8546 let parsed = parse_partial(
8548 &spec,
8549 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
8550 )
8551 .unwrap();
8552 assert_eq!(parsed.args.len(), 1);
8553 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8554
8555 assert_parse_err(
8557 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
8558 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8559 );
8560 }
8561
8562 fn mounted_orphan_short_spec() -> Spec {
8567 let task_cmd = SpecCommand::builder()
8568 .name("sample:run")
8569 .arg(
8570 SpecArg::builder()
8571 .name("profile")
8572 .choices(["alpha", "beta", "gamma"])
8573 .build(),
8574 )
8575 .build();
8576 let mut run_cmd = SpecCommand::builder()
8579 .name("run")
8580 .flag(
8581 SpecFlag::builder()
8582 .name("raw")
8583 .short('r')
8584 .long("raw")
8585 .global(false)
8586 .build(),
8587 )
8588 .flag(
8589 SpecFlag::builder()
8590 .name("force")
8591 .short('f')
8592 .long("force")
8593 .global(false)
8594 .build(),
8595 )
8596 .build();
8597 run_cmd
8598 .subcommands
8599 .insert("sample:run".to_string(), task_cmd);
8600
8601 let mut cmd = SpecCommand::builder()
8603 .name("test")
8604 .flag(
8605 SpecFlag::builder()
8606 .name("raw")
8607 .long("raw")
8608 .global(true)
8609 .build(),
8610 )
8611 .build();
8612 cmd.subcommands.insert("run".to_string(), run_cmd);
8613
8614 Spec {
8615 name: "test".to_string(),
8616 bin: "test".to_string(),
8617 cmd,
8618 ..Default::default()
8619 }
8620 }
8621
8622 #[test]
8623 fn test_orphan_short_alias_survives_merge() {
8624 let spec = mounted_orphan_short_spec();
8630
8631 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
8632 assert_eq!(
8633 parsed
8634 .cmds
8635 .iter()
8636 .map(|c| c.name.as_str())
8637 .collect::<Vec<_>>(),
8638 vec!["test", "run", "sample:run"],
8639 );
8640
8641 assert!(
8644 parsed.available_flags.get("-r").is_some_and(|f| f.global),
8645 "-r must be merged onto the inherited global flag and stay global after descent",
8646 );
8647 assert!(
8648 parsed
8649 .available_flags
8650 .get("--raw")
8651 .is_some_and(|f| f.global),
8652 "--raw must stay global after descent",
8653 );
8654
8655 assert!(
8657 parsed.args.is_empty(),
8658 "args should be empty, got {:?}",
8659 parsed.args
8660 );
8661
8662 assert_eq!(
8664 parsed.as_env().get("usage_raw").map(String::as_str),
8665 Some("true"),
8666 "merged short's value must survive in as_env(), got {:?}",
8667 parsed.as_env(),
8668 );
8669
8670 assert!(
8673 !parsed.available_flags.contains_key("-f"),
8674 "purely-local -f must not be promoted onto a global",
8675 );
8676 assert!(
8677 !parsed.available_flags.contains_key("--force"),
8678 "purely-local --force must not be promoted onto a global",
8679 );
8680
8681 let parsed =
8683 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
8684 assert_eq!(parsed.args.len(), 1);
8685 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
8686
8687 assert_parse_err(
8689 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
8690 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
8691 );
8692 }
8693
8694 #[test]
8695 fn test_orphan_short_does_not_clobber_unrelated_global() {
8696 let run_cmd = SpecCommand::builder()
8701 .name("run")
8702 .flag(
8703 SpecFlag::builder()
8704 .name("raw")
8705 .short('r')
8706 .long("raw")
8707 .global(false)
8708 .build(),
8709 )
8710 .build();
8711 let mut cmd = SpecCommand::builder()
8712 .name("test")
8713 .flag(
8714 SpecFlag::builder()
8715 .name("raw")
8716 .long("raw")
8717 .global(true)
8718 .build(),
8719 )
8720 .flag(
8721 SpecFlag::builder()
8722 .name("restrict")
8723 .short('r')
8724 .long("restrict")
8725 .global(true)
8726 .build(),
8727 )
8728 .build();
8729 cmd.subcommands.insert("run".to_string(), run_cmd);
8730 let spec = Spec {
8731 name: "test".to_string(),
8732 bin: "test".to_string(),
8733 cmd,
8734 ..Default::default()
8735 };
8736
8737 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8738 assert_eq!(
8740 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
8741 Some("restrict"),
8742 "-r must remain owned by the unrelated global it already belonged to",
8743 );
8744 assert!(
8746 parsed
8747 .available_flags
8748 .get("--raw")
8749 .is_some_and(|f| f.global)
8750 );
8751 assert!(
8752 parsed
8753 .available_flags
8754 .get("--restrict")
8755 .is_some_and(|f| f.global)
8756 );
8757 }
8758
8759 #[test]
8760 fn test_redeclared_global_aliases_share_one_flag() {
8761 let spec = r#"
8768flag "-y --yes" global=#true effect="write"
8769cmd "run" {
8770 flag "-y --yes --assume-yes"
8771}
8772"#
8773 .parse::<Spec>()
8774 .unwrap();
8775
8776 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8777
8778 for key in ["-y", "--yes", "--assume-yes"] {
8779 let flag = parsed
8780 .available_flags
8781 .get(key)
8782 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8783 assert!(flag.global, "{key} must stay global after the descent");
8784 assert_eq!(
8785 flag.long,
8786 vec!["yes".to_string(), "assume-yes".to_string()],
8787 "{key} must resolve to the flag carrying every alias",
8788 );
8789 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
8790 }
8791
8792 assert_eq!(
8794 unique_flags(parsed.available_flags.values()).count(),
8795 1,
8796 "all aliases must point at one flag object, got {:?}",
8797 parsed.available_flags,
8798 );
8799
8800 assert_eq!(
8802 parsed.available_flags["-y"].effect,
8803 Some(crate::SpecCommandEffect::Write),
8804 );
8805 }
8806
8807 #[test]
8808 fn test_redeclared_global_keeps_hidden_alias_metadata() {
8809 let spec = r#"
8810flag "--yes" global=#true {
8811 alias "-q" "--quietly" hide=#true
8812}
8813cmd "run" {
8814 flag "--yes --assume-yes" {
8815 alias "-s" "--secret" hide=#true
8816 }
8817}
8818"#
8819 .parse::<Spec>()
8820 .unwrap();
8821
8822 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8823 let merged = &parsed.available_flags["--yes"];
8824 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
8825 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
8826 for key in ["-q", "-s", "--quietly", "--secret"] {
8827 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8828 }
8829 }
8830
8831 #[test]
8832 fn test_redeclared_global_can_promote_hidden_aliases() {
8833 let spec = r#"
8834flag "--yes" global=#true {
8835 alias "-q" "--quietly" hide=#true
8836}
8837cmd "run" {
8838 flag "-q --yes --quietly"
8839}
8840"#
8841 .parse::<Spec>()
8842 .unwrap();
8843
8844 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8845 let merged = &parsed.available_flags["--yes"];
8846 assert!(merged.hidden_short_aliases.is_empty());
8847 assert!(merged.hidden_aliases.is_empty());
8848 for key in ["-q", "--quietly"] {
8849 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
8850 }
8851 }
8852
8853 #[test]
8854 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
8855 let spec = r#"
8861flag "-y --yes --confirm" global=#true
8862cmd "run" {
8863 flag "--yes --assume-yes"
8864}
8865"#
8866 .parse::<Spec>()
8867 .unwrap();
8868
8869 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8870
8871 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
8872 let flag = parsed
8873 .available_flags
8874 .get(key)
8875 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
8876 assert!(flag.global, "{key} must stay global after the descent");
8877 assert_eq!(
8878 flag.long,
8879 vec![
8880 "yes".to_string(),
8881 "confirm".to_string(),
8882 "assume-yes".to_string()
8883 ],
8884 "{key} must resolve to the flag carrying every alias",
8885 );
8886 }
8887
8888 assert_eq!(
8889 unique_flags(parsed.available_flags.values()).count(),
8890 1,
8891 "all aliases must point at one flag object, got {:?}",
8892 parsed.available_flags,
8893 );
8894 }
8895
8896 fn mounted_task_flag_spec() -> Spec {
8903 let mut task_cmd = SpecCommand::builder()
8904 .name("mytask")
8905 .flag(
8906 SpecFlag::builder()
8907 .name("env")
8908 .long("env")
8909 .arg(
8910 SpecArg::builder()
8911 .name("name")
8912 .choices(["dev", "stage", "prod"])
8913 .build(),
8914 )
8915 .global(false)
8916 .build(),
8917 )
8918 .flag(
8919 SpecFlag::builder()
8920 .name("bump")
8921 .long("bump")
8922 .arg(
8923 SpecArg::builder()
8924 .name("type")
8925 .choices(["auto", "major"])
8926 .build(),
8927 )
8928 .global(false)
8929 .build(),
8930 )
8931 .build();
8932 task_cmd.mounted = true;
8933
8934 let mut run_cmd = SpecCommand::builder()
8935 .name("run")
8936 .flag(
8937 SpecFlag::builder()
8938 .name("force")
8939 .short('f')
8940 .long("force")
8941 .global(false)
8942 .build(),
8943 )
8944 .build();
8945 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
8946
8947 let mut cmd = SpecCommand::builder()
8948 .name("test")
8949 .flag(
8950 SpecFlag::builder()
8951 .name("env")
8952 .short('E')
8953 .long("env")
8954 .arg(SpecArg::builder().name("ENV").build())
8955 .global(true)
8956 .build(),
8957 )
8958 .flag(
8959 SpecFlag::builder()
8960 .name("silent")
8961 .long("silent")
8962 .global(true)
8963 .build(),
8964 )
8965 .build();
8966 cmd.subcommands.insert("run".to_string(), run_cmd);
8967
8968 Spec {
8969 name: "test".to_string(),
8970 bin: "test".to_string(),
8971 cmd,
8972 ..Default::default()
8973 }
8974 }
8975
8976 #[test]
8977 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
8978 let deep = SpecCommand::builder().name("deep").build();
8984 let mut sub = SpecCommand::builder()
8985 .name("sub")
8986 .flag(
8988 SpecFlag::builder()
8989 .name("cd")
8990 .short('C')
8991 .long("cd")
8992 .arg(SpecArg::builder().name("dir").build())
8993 .global(false)
8994 .build(),
8995 )
8996 .build();
8997 sub.subcommands.insert("deep".to_string(), deep);
8998 let mut task = SpecCommand::builder()
8999 .name("task")
9000 .flag(
9001 SpecFlag::builder()
9002 .name("cd")
9003 .short('C')
9004 .long("cd")
9005 .arg(SpecArg::builder().name("dir").build())
9006 .global(true)
9007 .build(),
9008 )
9009 .build();
9010 task.subcommands.insert("sub".to_string(), sub);
9011 task.mark_mounted();
9012
9013 let mut run_cmd = SpecCommand::builder().name("run").build();
9014 run_cmd.subcommands.insert("task".to_string(), task);
9015 let mut cmd = SpecCommand::builder().name("test").build();
9016 cmd.subcommands.insert("run".to_string(), run_cmd);
9017 let spec = Spec {
9018 name: "test".to_string(),
9019 bin: "test".to_string(),
9020 cmd,
9021 ..Default::default()
9022 };
9023
9024 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
9025 assert!(
9026 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
9027 "the mounted program's own global must survive descents inside the mounted tree",
9028 );
9029 assert!(
9030 parsed.completion_flags().contains_key("--cd"),
9031 "and must still be offered there: it belongs to the mounted program",
9032 );
9033 assert!(
9034 parsed.completion_flags().contains_key("-C"),
9035 "including the short the nested command re-declared",
9036 );
9037 }
9038
9039 #[test]
9040 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
9041 let mut task = SpecCommand::builder()
9046 .name("task")
9047 .flag(
9048 SpecFlag::builder()
9049 .name("bump")
9050 .long("bump")
9051 .global(false)
9052 .build(),
9053 )
9054 .build();
9055 task.mark_mounted();
9056
9057 let mut run_cmd = SpecCommand::builder().name("run").build();
9058 run_cmd.subcommands.insert("task".to_string(), task);
9059 run_cmd.flags = vec![
9061 SpecFlag::builder()
9062 .name("tglobal")
9063 .long("tglobal")
9064 .global(true)
9065 .build(),
9066 SpecFlag::builder()
9067 .name("tlocal")
9068 .long("tlocal")
9069 .global(false)
9070 .build(),
9071 ];
9072 run_cmd.flags_from_mount = true;
9073
9074 let mut cmd = SpecCommand::builder()
9075 .name("test")
9076 .flag(
9077 SpecFlag::builder()
9078 .name("silent")
9079 .long("silent")
9080 .global(true)
9081 .build(),
9082 )
9083 .build();
9084 cmd.subcommands.insert("run".to_string(), run_cmd);
9085 let spec = Spec {
9086 name: "test".to_string(),
9087 bin: "test".to_string(),
9088 cmd,
9089 ..Default::default()
9090 };
9091
9092 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
9093 assert_eq!(
9094 parsed.completion_flags().keys().collect::<Vec<_>>(),
9095 vec!["--bump", "--tglobal"],
9096 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
9097 `--silent` does not, and the mount's non-global root flag is not inherited",
9098 );
9099 }
9100
9101 #[test]
9102 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
9103 let spec = mounted_task_flag_spec();
9108 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
9109
9110 assert!(parsed.available_flags.contains_key("--silent"));
9112 assert!(parsed.available_flags.contains_key("-E"));
9113 assert_eq!(
9115 parsed.completion_flags().keys().collect::<Vec<_>>(),
9116 vec!["--bump", "--env"],
9117 "only the mounted command's own flags may be offered",
9118 );
9119
9120 assert!(!parsed.available_flags.contains_key("--force"));
9122 }
9123
9124 #[test]
9125 fn test_mounted_cmd_flag_wins_over_inherited_global() {
9126 let spec = mounted_task_flag_spec();
9129 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
9130
9131 let awaiting = parsed
9132 .flag_awaiting_value
9133 .first()
9134 .expect("--env should await a value");
9135 assert_eq!(
9136 awaiting
9137 .arg
9138 .as_ref()
9139 .and_then(|a| a.choices.as_ref())
9140 .map(|c| c.choices.clone()),
9141 Some(vec![
9142 "dev".to_string(),
9143 "stage".to_string(),
9144 "prod".to_string()
9145 ]),
9146 "the mounted command's own --env must win over the inherited global",
9147 );
9148
9149 let parsed =
9152 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
9153 assert!(
9154 parsed.args.is_empty(),
9155 "prefix global tokens must not be consumed as positionals, got {:?}",
9156 parsed.args
9157 );
9158 assert_eq!(
9159 parsed.as_env().get("usage_env").map(String::as_str),
9160 Some("anything"),
9161 );
9162 }
9163
9164 #[test]
9165 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
9166 let spec = mounted_task_flag_spec();
9171 let parsed = parse_partial(
9172 &spec,
9173 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
9174 )
9175 .unwrap();
9176 assert!(
9177 parsed.errors.is_empty(),
9178 "prefix global value must not be validated against the mounted flag: {:?}",
9179 parsed
9180 .errors
9181 .iter()
9182 .map(|e| e.to_string())
9183 .collect::<Vec<_>>(),
9184 );
9185 assert_eq!(
9186 parsed.as_env().get("usage_env").map(String::as_str),
9187 Some("not-a-task-choice"),
9188 );
9189
9190 let parsed = parse_partial(
9192 &spec,
9193 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
9194 )
9195 .unwrap();
9196 assert!(parsed.errors.is_empty());
9197 assert_eq!(
9198 parsed.as_env().get("usage_env").map(String::as_str),
9199 Some("not-a-task-choice"),
9200 );
9201
9202 let parsed = parse_partial(
9205 &spec,
9206 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
9207 )
9208 .unwrap();
9209 let awaiting = parsed
9210 .flag_awaiting_value
9211 .first()
9212 .expect("--env should await a value");
9213 assert_eq!(
9214 awaiting
9215 .arg
9216 .as_ref()
9217 .and_then(|a| a.choices.as_ref())
9218 .map(|c| c.choices.clone()),
9219 Some(vec![
9220 "dev".to_string(),
9221 "stage".to_string(),
9222 "prod".to_string()
9223 ]),
9224 "the mounted command's --env must own the name after the mounted command",
9225 );
9226 }
9227
9228 #[test]
9229 fn test_non_global_flag_does_not_hide_subcommand() {
9230 let spec = mounted_task_flag_spec();
9234
9235 for words in [
9236 &["test", "run", "--force", "mytask"][..],
9238 &["test", "run", "-f", "mytask"][..],
9239 &["test", "-E", "prod", "run", "--force", "mytask"][..],
9241 ] {
9242 let parsed = parse_partial(&spec, &input(words)).unwrap();
9243 assert_eq!(
9244 parsed
9245 .cmds
9246 .iter()
9247 .map(|c| c.name.as_str())
9248 .collect::<Vec<_>>(),
9249 vec!["test", "run", "mytask"],
9250 "{words:?} should descend into the mounted command",
9251 );
9252 assert!(
9253 parsed.args.is_empty(),
9254 "{words:?} should not consume a positional, got {:?}",
9255 parsed.args,
9256 );
9257 assert_eq!(
9258 parsed.as_env().get("usage_force").map(String::as_str),
9259 Some("true"),
9260 "the non-global flag must still be recorded for {words:?}",
9261 );
9262 }
9263
9264 let mut run_cmd = SpecCommand::builder()
9267 .name("run")
9268 .flag(
9269 SpecFlag::builder()
9270 .name("output")
9271 .short('o')
9272 .long("output")
9273 .arg(SpecArg::builder().name("mode").build())
9274 .global(false)
9275 .build(),
9276 )
9277 .build();
9278 run_cmd.subcommands.insert(
9279 "task".to_string(),
9280 SpecCommand::builder().name("task").build(),
9281 );
9282 let mut cmd = SpecCommand::builder().name("test").build();
9283 cmd.subcommands.insert("run".to_string(), run_cmd);
9284 let spec = Spec {
9285 name: "test".to_string(),
9286 bin: "test".to_string(),
9287 cmd,
9288 ..Default::default()
9289 };
9290
9291 let parsed =
9292 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
9293 assert_eq!(
9294 parsed
9295 .cmds
9296 .iter()
9297 .map(|c| c.name.as_str())
9298 .collect::<Vec<_>>(),
9299 vec!["test", "run", "task"],
9300 );
9301 assert_eq!(
9302 parsed.as_env().get("usage_output").map(String::as_str),
9303 Some("quiet"),
9304 );
9305
9306 assert_parse_err(
9309 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
9310 "unexpected word: --nope",
9311 );
9312 }
9313
9314 #[test]
9315 fn test_non_mounted_subcommand_offers_inherited_globals() {
9316 let mut run_cmd = SpecCommand::builder().name("run").build();
9319 run_cmd.subcommands.insert(
9320 "nested".to_string(),
9321 SpecCommand::builder().name("nested").build(),
9322 );
9323 let mut cmd = SpecCommand::builder()
9324 .name("test")
9325 .flag(
9326 SpecFlag::builder()
9327 .name("silent")
9328 .long("silent")
9329 .global(true)
9330 .build(),
9331 )
9332 .build();
9333 cmd.subcommands.insert("run".to_string(), run_cmd);
9334 let spec = Spec {
9335 name: "test".to_string(),
9336 bin: "test".to_string(),
9337 cmd,
9338 ..Default::default()
9339 };
9340
9341 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
9342 assert_eq!(
9343 parsed.completion_flags().keys().collect::<Vec<_>>(),
9344 parsed.available_flags.keys().collect::<Vec<_>>(),
9345 );
9346 assert!(parsed.completion_flags().contains_key("--silent"));
9347 }
9348
9349 #[test]
9350 fn test_subcommand_alias_collision_keeps_last_owner() {
9351 let run_cmd = SpecCommand::builder()
9355 .name("run")
9356 .flag(
9357 SpecFlag::builder()
9358 .name("alpha")
9359 .short('x')
9360 .long("alpha")
9361 .global(false)
9362 .build(),
9363 )
9364 .flag(
9365 SpecFlag::builder()
9366 .name("beta")
9367 .short('x')
9368 .long("beta")
9369 .global(false)
9370 .build(),
9371 )
9372 .build();
9373 let mut cmd = SpecCommand::builder().name("test").build();
9374 cmd.subcommands.insert("run".to_string(), run_cmd);
9375 let spec = Spec {
9376 name: "test".to_string(),
9377 bin: "test".to_string(),
9378 cmd,
9379 ..Default::default()
9380 };
9381
9382 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
9383 assert_eq!(
9385 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
9386 Some("beta"),
9387 "the last-declared flag must keep a shared short alias",
9388 );
9389 assert_eq!(
9391 parsed
9392 .available_flags
9393 .get("--alpha")
9394 .map(|f| f.name.as_str()),
9395 Some("alpha"),
9396 );
9397 assert_eq!(
9398 parsed
9399 .available_flags
9400 .get("--beta")
9401 .map(|f| f.name.as_str()),
9402 Some("beta"),
9403 );
9404 }
9405
9406 #[test]
9407 fn test_default_subcommand_same_name_child() {
9408 let run_task = SpecCommand::builder()
9412 .name("run")
9413 .arg(SpecArg::builder().name("args").build())
9414 .build();
9415 let mut run_cmd = SpecCommand::builder().name("run").build();
9416 run_cmd.subcommands.insert("run".to_string(), run_task);
9417
9418 let mut cmd = SpecCommand::builder().name("test").build();
9419 cmd.subcommands.insert("run".to_string(), run_cmd);
9420
9421 let spec = Spec {
9422 name: "test".to_string(),
9423 bin: "test".to_string(),
9424 cmd,
9425 default_subcommand: Some("run".to_string()),
9426 ..Default::default()
9427 };
9428
9429 let input = vec!["test".to_string(), "run".to_string()];
9431 let parsed = parse(&spec, &input).unwrap();
9432
9433 assert_eq!(parsed.cmds.len(), 2);
9435 assert_eq!(parsed.cmds[0].name, "test");
9436 assert_eq!(parsed.cmds[1].name, "run");
9437
9438 let input = vec![
9440 "test".to_string(),
9441 "run".to_string(),
9442 "run".to_string(),
9443 "hello".to_string(),
9444 ];
9445 let parsed = parse(&spec, &input).unwrap();
9446
9447 assert_eq!(parsed.cmds.len(), 3);
9448 assert_eq!(parsed.cmds[0].name, "test");
9449 assert_eq!(parsed.cmds[1].name, "run");
9450 assert_eq!(parsed.cmds[2].name, "run");
9451 assert_eq!(parsed.args.len(), 1);
9452 let value = parsed.args.values().next().unwrap();
9453 assert_eq!(value.to_string(), "hello");
9454
9455 let mut run_cmd = SpecCommand::builder()
9459 .name("run")
9460 .arg(SpecArg::builder().name("task").build())
9461 .build();
9462 let run_task = SpecCommand::builder().name("run").build();
9463 run_cmd.subcommands.insert("run".to_string(), run_task);
9464
9465 let mut cmd = SpecCommand::builder().name("test").build();
9466 cmd.subcommands.insert("run".to_string(), run_cmd);
9467
9468 let spec = Spec {
9469 name: "test".to_string(),
9470 bin: "test".to_string(),
9471 cmd,
9472 default_subcommand: Some("run".to_string()),
9473 ..Default::default()
9474 };
9475
9476 let input = vec!["test".to_string(), "other".to_string()];
9477 let parsed = parse(&spec, &input).unwrap();
9478
9479 assert_eq!(parsed.cmds.len(), 2);
9482 assert_eq!(parsed.cmds[0].name, "test");
9483 assert_eq!(parsed.cmds[1].name, "run");
9484
9485 assert_eq!(parsed.args.len(), 1);
9487 let value = parsed.args.values().next().unwrap();
9488 assert_eq!(value.to_string(), "other");
9489 }
9490
9491 #[test]
9492 fn test_restart_token() {
9493 let run_cmd = SpecCommand::builder()
9495 .name("run")
9496 .arg(SpecArg::builder().name("task").build())
9497 .restart_token(":::".to_string())
9498 .build();
9499 let mut cmd = SpecCommand::builder().name("test").build();
9500 cmd.subcommands.insert("run".to_string(), run_cmd);
9501
9502 let spec = Spec {
9503 name: "test".to_string(),
9504 bin: "test".to_string(),
9505 cmd,
9506 ..Default::default()
9507 };
9508
9509 let input = vec![
9511 "test".to_string(),
9512 "run".to_string(),
9513 "task1".to_string(),
9514 ":::".to_string(),
9515 "task2".to_string(),
9516 ];
9517 let parsed = parse(&spec, &input).unwrap();
9518
9519 assert_eq!(parsed.args.len(), 1);
9521 let value = parsed.args.values().next().unwrap();
9522 assert_eq!(value.to_string(), "task2");
9523 }
9524
9525 #[test]
9526 fn test_restart_token_multiple() {
9527 let run_cmd = SpecCommand::builder()
9529 .name("run")
9530 .arg(SpecArg::builder().name("task").build())
9531 .restart_token(":::".to_string())
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 "task1".to_string(),
9548 ":::".to_string(),
9549 "task2".to_string(),
9550 ":::".to_string(),
9551 "task3".to_string(),
9552 ];
9553 let parsed = parse(&spec, &input).unwrap();
9554
9555 assert_eq!(parsed.args.len(), 1);
9557 let value = parsed.args.values().next().unwrap();
9558 assert_eq!(value.to_string(), "task3");
9559 }
9560
9561 #[test]
9562 fn test_restart_token_clears_flag_awaiting_value() {
9563 let run_cmd = SpecCommand::builder()
9565 .name("run")
9566 .arg(SpecArg::builder().name("task").build())
9567 .flag(
9568 SpecFlag::builder()
9569 .name("jobs")
9570 .long("jobs")
9571 .arg(SpecArg::builder().name("count").build())
9572 .build(),
9573 )
9574 .restart_token(":::".to_string())
9575 .build();
9576 let mut cmd = SpecCommand::builder().name("test").build();
9577 cmd.subcommands.insert("run".to_string(), run_cmd);
9578
9579 let spec = Spec {
9580 name: "test".to_string(),
9581 bin: "test".to_string(),
9582 cmd,
9583 ..Default::default()
9584 };
9585
9586 let input = vec![
9588 "test".to_string(),
9589 "run".to_string(),
9590 "task1".to_string(),
9591 "--jobs".to_string(),
9592 ":::".to_string(),
9593 "task2".to_string(),
9594 ];
9595 let parsed = parse(&spec, &input).unwrap();
9596
9597 assert_eq!(parsed.args.len(), 1);
9599 let value = parsed.args.values().next().unwrap();
9600 assert_eq!(value.to_string(), "task2");
9601 assert!(parsed.flag_awaiting_value.is_empty());
9603 }
9604
9605 #[test]
9606 fn test_restart_token_resets_double_dash() {
9607 let run_cmd = SpecCommand::builder()
9609 .name("run")
9610 .arg(SpecArg::builder().name("task").build())
9611 .arg(SpecArg::builder().name("extra_args").var(true).build())
9612 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
9613 .restart_token(":::".to_string())
9614 .build();
9615 let mut cmd = SpecCommand::builder().name("test").build();
9616 cmd.subcommands.insert("run".to_string(), run_cmd);
9617
9618 let spec = Spec {
9619 name: "test".to_string(),
9620 bin: "test".to_string(),
9621 cmd,
9622 ..Default::default()
9623 };
9624
9625 let input = vec![
9627 "test".to_string(),
9628 "run".to_string(),
9629 "task1".to_string(),
9630 "--".to_string(),
9631 "extra".to_string(),
9632 ":::".to_string(),
9633 "--verbose".to_string(),
9634 "task2".to_string(),
9635 ];
9636 let parsed = parse(&spec, &input).unwrap();
9637
9638 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9640 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9642 let value = parsed.args.get(task_arg).unwrap();
9643 assert_eq!(value.to_string(), "task2");
9644 }
9645
9646 #[test]
9647 fn test_double_dashes_without_preserve() {
9648 let run_cmd = SpecCommand::builder()
9653 .name("run")
9654 .arg(SpecArg::builder().name("args").var(true).build())
9655 .build();
9656 let mut cmd = SpecCommand::builder().name("test").build();
9657 cmd.subcommands.insert("run".to_string(), run_cmd);
9658
9659 let spec = Spec {
9660 name: "test".to_string(),
9661 bin: "test".to_string(),
9662 cmd,
9663 ..Default::default()
9664 };
9665
9666 let input = vec![
9668 "test".to_string(),
9669 "run".to_string(),
9670 "arg1".to_string(),
9671 "--".to_string(),
9672 "arg2".to_string(),
9673 "--".to_string(),
9674 "arg3".to_string(),
9675 ];
9676 let parsed = parse(&spec, &input).unwrap();
9677
9678 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9679 let value = parsed.args.get(args_arg).unwrap();
9680 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
9681 }
9682
9683 #[test]
9684 fn test_double_dashes_with_preserve() {
9685 let run_cmd = SpecCommand::builder()
9687 .name("run")
9688 .arg(
9689 SpecArg::builder()
9690 .name("args")
9691 .var(true)
9692 .double_dash(SpecDoubleDashChoices::Preserve)
9693 .build(),
9694 )
9695 .build();
9696 let mut cmd = SpecCommand::builder().name("test").build();
9697 cmd.subcommands.insert("run".to_string(), run_cmd);
9698
9699 let spec = Spec {
9700 name: "test".to_string(),
9701 bin: "test".to_string(),
9702 cmd,
9703 ..Default::default()
9704 };
9705
9706 let input = vec![
9708 "test".to_string(),
9709 "run".to_string(),
9710 "arg1".to_string(),
9711 "--".to_string(),
9712 "arg2".to_string(),
9713 "--".to_string(),
9714 "arg3".to_string(),
9715 ];
9716 let parsed = parse(&spec, &input).unwrap();
9717
9718 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9719 let value = parsed.args.get(args_arg).unwrap();
9720 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
9721 }
9722
9723 #[test]
9724 fn test_double_dashes_with_preserve_only_dashes() {
9725 let run_cmd = SpecCommand::builder()
9728 .name("run")
9729 .arg(
9730 SpecArg::builder()
9731 .name("args")
9732 .var(true)
9733 .double_dash(SpecDoubleDashChoices::Preserve)
9734 .build(),
9735 )
9736 .build();
9737 let mut cmd = SpecCommand::builder().name("test").build();
9738 cmd.subcommands.insert("run".to_string(), run_cmd);
9739
9740 let spec = Spec {
9741 name: "test".to_string(),
9742 bin: "test".to_string(),
9743 cmd,
9744 ..Default::default()
9745 };
9746
9747 let input = vec![
9749 "test".to_string(),
9750 "run".to_string(),
9751 "--".to_string(),
9752 "--".to_string(),
9753 ];
9754 let parsed = parse(&spec, &input).unwrap();
9755
9756 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
9757 let value = parsed.args.get(args_arg).unwrap();
9758 assert_eq!(value.to_string(), "-- --");
9759 }
9760
9761 #[test]
9762 fn test_double_dashes_with_preserve_multiple_args() {
9763 let run_cmd = SpecCommand::builder()
9765 .name("run")
9766 .arg(SpecArg::builder().name("task").build())
9767 .arg(
9768 SpecArg::builder()
9769 .name("extra_args")
9770 .var(true)
9771 .double_dash(SpecDoubleDashChoices::Preserve)
9772 .build(),
9773 )
9774 .build();
9775 let mut cmd = SpecCommand::builder().name("test").build();
9776 cmd.subcommands.insert("run".to_string(), run_cmd);
9777
9778 let spec = Spec {
9779 name: "test".to_string(),
9780 bin: "test".to_string(),
9781 cmd,
9782 ..Default::default()
9783 };
9784
9785 let input = vec![
9788 "test".to_string(),
9789 "run".to_string(),
9790 "task1".to_string(),
9791 "--".to_string(),
9792 "arg1".to_string(),
9793 "--".to_string(),
9794 "--foo".to_string(),
9795 ];
9796 let parsed = parse(&spec, &input).unwrap();
9797
9798 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
9799 let task_value = parsed.args.get(task_arg).unwrap();
9800 assert_eq!(task_value.to_string(), "task1");
9801
9802 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
9803 let extra_value = parsed.args.get(extra_arg).unwrap();
9804 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
9805 }
9806
9807 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
9808 let cmd = SpecCommand::builder().name("test").args(args).build();
9809 Spec {
9810 name: "test".to_string(),
9811 bin: "test".to_string(),
9812 cmd,
9813 ..Default::default()
9814 }
9815 }
9816
9817 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
9818 let arg = parsed
9819 .args
9820 .keys()
9821 .find(|a| a.name == name)
9822 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
9823 parsed.args.get(arg).unwrap().to_string()
9824 }
9825
9826 fn required_arg(name: &str) -> SpecArg {
9827 SpecArg::builder()
9828 .name(name)
9829 .var(true)
9830 .required(false)
9831 .double_dash(SpecDoubleDashChoices::Required)
9832 .build()
9833 }
9834
9835 #[test]
9836 fn test_double_dash_required_reports_error_once_for_variadic() {
9837 let spec = spec_with_args([required_arg("files")]);
9839
9840 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
9841
9842 assert!(parsed.args.is_empty());
9843 assert_eq!(parsed.errors.len(), 1);
9844 assert!(
9845 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
9846 );
9847 }
9848
9849 #[test]
9850 fn test_double_dash_required_suppresses_missing_arg() {
9851 let spec = spec_with_args([SpecArg::builder()
9853 .name("file")
9854 .required(true)
9855 .double_dash(SpecDoubleDashChoices::Required)
9856 .build()]);
9857
9858 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
9859
9860 assert_eq!(parsed.errors.len(), 1);
9861 assert!(matches!(
9862 &parsed.errors[0],
9863 UsageErr::ArgRequiresDoubleDash(_)
9864 ));
9865 assert_eq!(
9867 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
9868 Some("file")
9869 );
9870 assert!(!parsed.double_dash_seen);
9871 }
9872
9873 #[test]
9874 fn test_double_dash_routes_to_required_arg() {
9875 let spec = spec_with_args([
9878 SpecArg::builder()
9879 .name("tool")
9880 .var(true)
9881 .required(false)
9882 .build(),
9883 required_arg("command"),
9884 ]);
9885
9886 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
9887
9888 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9889 assert_eq!(arg_value(&parsed, "command"), "node app.js");
9890 assert!(parsed.double_dash_seen);
9891 }
9892
9893 #[test]
9894 fn test_double_dash_routes_with_gap_reports_missing_arg() {
9895 let spec = spec_with_args([
9898 SpecArg::builder()
9899 .name("tool")
9900 .var(true)
9901 .required(true)
9902 .build(),
9903 required_arg("command"),
9904 ]);
9905
9906 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
9907
9908 assert_eq!(arg_value(&parsed, "command"), "ls");
9909 assert!(parsed.args.keys().all(|a| a.name != "tool"));
9910 assert!(
9911 parsed
9912 .errors
9913 .iter()
9914 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool"))
9915 );
9916 }
9917
9918 #[test]
9919 fn test_double_dash_gap_applies_defaults() {
9920 let spec = spec_with_args([
9922 SpecArg::builder()
9923 .name("tool")
9924 .var(true)
9925 .required(false)
9926 .default_value("node@20")
9927 .build(),
9928 required_arg("command"),
9929 ]);
9930
9931 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
9932
9933 assert_eq!(arg_value(&parsed, "command"), "ls");
9934 assert_eq!(arg_value(&parsed, "tool"), "node@20");
9935 }
9936
9937 fn spec_with_restart_token_and_required_arg() -> Spec {
9938 let run_cmd = SpecCommand::builder()
9939 .name("run")
9940 .arg(SpecArg::builder().name("task").build())
9941 .arg(required_arg("run_args"))
9942 .restart_token(":::".to_string())
9943 .build();
9944 let mut cmd = SpecCommand::builder().name("test").build();
9945 cmd.subcommands.insert("run".to_string(), run_cmd);
9946 Spec {
9947 name: "test".to_string(),
9948 bin: "test".to_string(),
9949 cmd,
9950 ..Default::default()
9951 }
9952 }
9953
9954 #[test]
9955 fn test_double_dash_required_restart_token_resets_separator() {
9956 let spec = spec_with_restart_token_and_required_arg();
9958
9959 let parsed = parse_partial(
9960 &spec,
9961 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
9962 )
9963 .unwrap();
9964
9965 assert_eq!(arg_value(&parsed, "task"), "task2");
9966 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
9967 assert_eq!(
9969 parsed
9970 .errors
9971 .iter()
9972 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
9973 .count(),
9974 1
9975 );
9976 }
9977
9978 #[test]
9979 fn test_double_dash_required_restart_token_accepts_new_separator() {
9980 let spec = spec_with_restart_token_and_required_arg();
9981
9982 let parsed = parse(
9983 &spec,
9984 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
9985 )
9986 .unwrap();
9987
9988 assert_eq!(arg_value(&parsed, "task"), "task2");
9989 assert_eq!(arg_value(&parsed, "run_args"), "c");
9990 }
9991
9992 #[test]
9993 fn test_double_dash_preserve_is_not_a_separator() {
9994 let spec = spec_with_args([
9997 SpecArg::builder()
9998 .name("kept")
9999 .var(true)
10000 .var_max(1)
10001 .required(false)
10002 .double_dash(SpecDoubleDashChoices::Preserve)
10003 .build(),
10004 required_arg("rest"),
10005 ]);
10006
10007 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
10008
10009 assert_eq!(arg_value(&parsed, "kept"), "--");
10010 assert!(parsed.args.keys().all(|a| a.name != "rest"));
10011 assert!(!parsed.double_dash_seen);
10012 assert_eq!(parsed.errors.len(), 1);
10013 }
10014
10015 #[test]
10016 fn test_double_dash_required_does_not_bail_in_parse_partial() {
10017 let spec = spec_with_args([required_arg("file")]);
10019
10020 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
10021 assert!(parse(&spec, &input(&["test", "x"])).is_err());
10022 }
10023
10024 #[test]
10025 fn test_double_dash_without_required_arg_does_not_move_cursor() {
10026 let spec = spec_with_args([
10028 SpecArg::builder().name("first").required(false).build(),
10029 SpecArg::builder().name("second").required(false).build(),
10030 ]);
10031
10032 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
10033
10034 assert_eq!(arg_value(&parsed, "first"), "a");
10035 assert_eq!(arg_value(&parsed, "second"), "b");
10036 assert!(parsed.next_arg.is_none());
10037 }
10038
10039 #[test]
10040 fn test_parser_with_custom_env_for_required_arg() {
10041 let spec = spec_with_arg(
10042 SpecArg::builder()
10043 .name("name")
10044 .env("NAME")
10045 .required(true)
10046 .build(),
10047 );
10048 unsafe { std::env::remove_var("NAME") };
10050
10051 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
10052 .expect("parse should succeed with custom env");
10053 assert_eq!(parsed.args.len(), 1);
10054 assert_eq!(first_string_value(&parsed), "john");
10055 }
10056
10057 #[test]
10058 fn test_parser_with_custom_env_for_required_flag() {
10059 let spec = spec_with_flag(
10060 SpecFlag::builder()
10061 .long("name")
10062 .env("NAME")
10063 .required(true)
10064 .arg(SpecArg::builder().name("name").build())
10065 .build(),
10066 );
10067 unsafe { std::env::remove_var("NAME") };
10069
10070 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
10071 .expect("parse should succeed with custom env");
10072 assert_eq!(parsed.flags.len(), 1);
10073 assert_eq!(first_string_value(&parsed), "jane");
10074 }
10075
10076 #[test]
10077 fn test_flag_environment_fallbacks_preserve_declaration_order() {
10078 let spec = spec_with_flag(
10079 SpecFlag::builder()
10080 .long("name")
10081 .env("NAME")
10082 .env_fallback("OLD_NAME")
10083 .env_fallback("OLDER_NAME")
10084 .deprecated_env("DEPRECATED_NAME")
10085 .arg(SpecArg::builder().name("name").build())
10086 .build(),
10087 );
10088
10089 let parsed = parse_with_env(
10090 &spec,
10091 &["test"],
10092 &[
10093 ("NAME", "canonical"),
10094 ("OLD_NAME", "fallback"),
10095 ("DEPRECATED_NAME", "deprecated"),
10096 ],
10097 )
10098 .unwrap();
10099 assert_eq!(first_string_value(&parsed), "canonical");
10100
10101 let parsed = parse_with_env(
10102 &spec,
10103 &["test"],
10104 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
10105 )
10106 .unwrap();
10107 assert_eq!(first_string_value(&parsed), "old");
10108
10109 let parsed =
10110 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
10111 assert_eq!(first_string_value(&parsed), "deprecated");
10112 }
10113
10114 #[test]
10115 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
10116 let spec = spec_with_flag(
10117 SpecFlag::builder()
10118 .long("name")
10119 .env("NAME")
10120 .deprecated_env("DEPRECATED_NAME")
10121 .arg(SpecArg::builder().name("name").build())
10122 .build(),
10123 );
10124
10125 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
10127 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10128
10129 let parsed =
10130 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
10131 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10132 assert_eq!(
10133 parsed.warnings[0].kind,
10134 crate::warn::WarningKind::DeprecatedEnv
10135 );
10136 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
10137 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
10138 assert_eq!(first_string_value(&parsed), "deprecated");
10140 }
10141
10142 #[test]
10143 fn a_deprecated_flag_reports_only_when_it_was_used() {
10144 let spec = spec_with_flag(
10145 SpecFlag::builder()
10146 .long("output")
10147 .deprecated("use --out")
10148 .deprecated_remove_at("3.0.0")
10149 .arg(SpecArg::builder().name("output").build())
10150 .build(),
10151 );
10152
10153 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
10154 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10155
10156 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10157 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10158 assert_eq!(
10159 parsed.warnings[0].kind,
10160 crate::warn::WarningKind::DeprecatedFlag
10161 );
10162 assert_eq!(parsed.warnings[0].name, "--output");
10164 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
10165 assert_eq!(
10166 parsed.warnings[0].render(),
10167 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
10168 );
10169 }
10170
10171 #[test]
10172 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
10173 let flag = SpecFlag::builder()
10174 .long("output")
10175 .deprecated("use --out")
10176 .deprecated_warn_at("9.0.0")
10177 .arg(SpecArg::builder().name("output").build())
10178 .build();
10179 let mut spec = spec_with_flag(flag);
10180 spec.version = Some("2.0.0".to_string());
10181
10182 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10183 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
10184
10185 spec.version = Some("9.0.0".to_string());
10187 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
10188 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
10189 }
10190
10191 #[test]
10192 fn test_parser_with_custom_env_still_fails_when_missing() {
10193 let spec = spec_with_arg(
10194 SpecArg::builder()
10195 .name("name")
10196 .env("NAME")
10197 .required(true)
10198 .build(),
10199 );
10200 unsafe { std::env::remove_var("NAME") };
10202 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
10203 }
10204
10205 #[test]
10206 fn test_parser_does_not_treat_env_choice_value_as_help() {
10207 let spec = spec_with_arg(
10208 SpecArg::builder()
10209 .name("env")
10210 .env("CURRENT_ENV")
10211 .choices(["dev", "staging"])
10212 .required(false)
10213 .build(),
10214 );
10215
10216 assert_parse_err(
10217 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
10218 "Invalid choice for arg env: --help, expected one of dev, staging",
10219 );
10220 }
10221
10222 #[test]
10223 fn test_parser_does_not_treat_default_choice_value_as_help() {
10224 let spec = spec_with_flag(
10225 SpecFlag::builder()
10226 .long("env")
10227 .arg(
10228 SpecArg::builder()
10229 .name("env")
10230 .choices(["dev", "staging"])
10231 .build(),
10232 )
10233 .default_value("--help")
10234 .build(),
10235 );
10236
10237 assert_parse_err(
10238 parse_with_env(&spec, &["test"], &[]),
10239 "Invalid choice for option env: --help, expected one of dev, staging",
10240 );
10241 }
10242
10243 fn words(of: &[&str]) -> Vec<String> {
10245 of.iter().map(|s| s.to_string()).collect()
10246 }
10247
10248 #[test]
10249 fn a_command_that_needs_a_subcommand_says_so() {
10250 let spec: Spec = r#"
10254name "ex"
10255bin "ex"
10256cmd "gen" subcommand_required=#true {
10257 cmd "two" {}
10258 cmd "one" {}
10259 cmd "secret" hide=#true {}
10260 alias "g"
10261}
10262cmd "open" {
10263 cmd "sub" {}
10264}
10265"#
10266 .parse()
10267 .unwrap();
10268
10269 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
10270 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
10274
10275 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
10277 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
10278
10279 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
10281
10282 parse(&spec, &words(&["ex", "open"])).unwrap();
10285 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
10286 }
10287
10288 #[test]
10289 fn arg_required_else_help_observes_the_selected_commands_argv() {
10290 let spec: Spec = r#"
10291name "ex"
10292bin "ex"
10293flag "--verbose" global=#true
10294cmd "run" arg_required_else_help=#true {
10295 flag "--all"
10296}
10297"#
10298 .parse()
10299 .unwrap();
10300 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10301
10302 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
10303 assert!(err.to_string().contains("Usage: ex run"), "{err}");
10304
10305 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
10308 assert!(err.to_string().contains("Usage: ex run"), "{err}");
10309
10310 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
10311 }
10312
10313 #[test]
10314 fn empty_default_with_parent_flags_observes_child_argv() {
10315 let spec: Spec = r#"
10316name "ex"
10317bin "ex"
10318default_subcommand "run"
10319default_subcommand_on_empty #true
10320flag "--verbose" global=#true
10321cmd "run" arg_required_else_help=#true {}
10322"#
10323 .parse()
10324 .unwrap();
10325 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
10326 let err = parse(&spec, &words(&["ex", "--verbose"])).unwrap_err();
10327 assert!(err.to_string().contains("Usage: ex run"), "{err}");
10328 }
10329
10330 #[test]
10331 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
10332 let spec: Spec = r#"
10333name "ex"
10334bin "ex"
10335unknown_flags "error"
10336external_subcommand #true
10337cmd "install"
10338flag "-v --verbose" global=#true
10339"#
10340 .parse()
10341 .unwrap();
10342
10343 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
10344 assert_eq!(
10345 parsed.external,
10346 Some(vec!["foo".into(), "--help".into(), "bar".into()])
10347 );
10348 assert!(parsed.flags.is_empty());
10349
10350 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
10352 assert_eq!(parsed.cmd.name, "install");
10353 assert!(parsed.external.is_none());
10354
10355 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
10357 assert_eq!(
10358 parsed.external,
10359 Some(vec!["foo".into(), "--verbose".into()])
10360 );
10361 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
10362
10363 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
10365
10366 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
10370 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
10371 }
10372
10373 #[test]
10374 fn an_external_subcommand_satisfies_subcommand_required() {
10375 let mut spec: Spec = r#"
10376name "ex"
10377bin "ex"
10378external_subcommand #true
10379cmd "install"
10380"#
10381 .parse()
10382 .unwrap();
10383 spec.cmd.subcommand_required = true;
10384
10385 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
10386 assert!(parse(&spec, &input(&["ex"])).is_err());
10387 }
10388
10389 #[test]
10390 fn a_default_subcommand_outranks_an_external_one() {
10391 let spec: Spec = r#"
10392name "ex"
10393bin "ex"
10394default_subcommand "run"
10395external_subcommand #true
10396cmd "run" {
10397 arg "[task]"
10398}
10399"#
10400 .parse()
10401 .unwrap();
10402
10403 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
10404 assert_eq!(parsed.cmd.name, "run");
10405 assert!(parsed.external.is_none());
10406 assert_eq!(first_string_value(&parsed), "build");
10407 }
10408
10409 #[test]
10410 fn multicall_basename_strips_a_path_and_exe() {
10411 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
10412 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
10413 assert_eq!(multicall_basename("LS.EXE"), "LS");
10414 assert_eq!(multicall_basename("busybox"), "busybox");
10415 }
10416
10417 #[test]
10418 fn a_multicall_applet_is_the_first_word() {
10419 let spec: Spec = r#"
10420name "busybox"
10421bin "busybox"
10422multicall #true
10423cmd "ls" {
10424 arg "[ARGS]" var=#true
10425}
10426cmd "cat"
10427"#
10428 .parse()
10429 .unwrap();
10430
10431 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
10433 assert_eq!(parsed.cmd.name, "ls");
10434 match parsed.args.values().next() {
10435 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
10436 other => panic!("expected ARGS to collect -l, got {other:?}"),
10437 }
10438
10439 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
10441 assert_eq!(parsed.cmd.name, "ls");
10442
10443 let mut configured = spec.clone();
10445 configured.name = "BusyBox".to_string();
10446 configured.bin = "/opt/bin/busybox.exe".to_string();
10447 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
10448 assert_eq!(parsed.cmd.name, "ls");
10449
10450 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
10452 assert_eq!(parsed.cmd.name, "ls");
10453
10454 let mut plain = spec.clone();
10456 plain.multicall = false;
10457 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
10458 assert_eq!(parsed.cmd.name, "ls");
10459 }
10460
10461 #[test]
10462 fn a_multicall_unknown_applet_can_be_external() {
10463 let spec: Spec = r#"
10464name "busybox"
10465bin "busybox"
10466multicall #true
10467unknown_flags "error"
10468external_subcommand #true
10469cmd "ls"
10470"#
10471 .parse()
10472 .unwrap();
10473
10474 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
10475 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
10476
10477 let mut closed = spec.clone();
10478 closed.cmd.external_subcommand = false;
10479 assert!(parse(&closed, &input(&["wat"])).is_err());
10480 }
10481
10482 #[cfg(feature = "unstable_choices_env")]
10483 #[test]
10484 fn test_parser_arg_choices_from_custom_env() {
10485 let spec = spec_arg_choices_env("DEPLOY_ENVS");
10486
10487 let parsed =
10488 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
10489 assert_eq!(first_string_value(&parsed), "bar");
10490
10491 assert_parse_err(
10492 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
10493 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
10494 );
10495 assert_parse_err(
10496 parse_with_env(&spec, &["test", "prod"], &[]),
10497 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
10498 );
10499 }
10500
10501 #[cfg(feature = "unstable_choices_env")]
10502 #[test]
10503 fn test_parser_validates_flag_choices_from_custom_env() {
10504 let spec = spec_flag_choices_env("DEPLOY_ENVS");
10505 let parsed = parse_with_env(
10506 &spec,
10507 &["test", "--env", "baz"],
10508 &[("DEPLOY_ENVS", "foo,bar baz")],
10509 )
10510 .unwrap();
10511 assert_eq!(first_string_value(&parsed), "baz");
10512 }
10513
10514 #[cfg(feature = "unstable_choices_env")]
10515 #[test]
10516 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
10517 let arg_env_spec = spec_with_arg(
10518 SpecArg::builder()
10519 .name("env")
10520 .env("CURRENT_ENV")
10521 .choices_env("DEPLOY_ENVS")
10522 .build(),
10523 );
10524 assert_parse_err(
10525 parse_with_env(
10526 &arg_env_spec,
10527 &["test"],
10528 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
10529 ),
10530 "Invalid choice for arg env: prod, expected one of dev, staging",
10531 );
10532
10533 let flag_default_spec = spec_with_flag(
10534 SpecFlag::builder()
10535 .long("env")
10536 .arg(
10537 SpecArg::builder()
10538 .name("env")
10539 .choices_env("DEPLOY_ENVS")
10540 .build(),
10541 )
10542 .default_value("prod")
10543 .build(),
10544 );
10545 assert_parse_err(
10546 parse_with_env(
10547 &flag_default_spec,
10548 &["test"],
10549 &[("DEPLOY_ENVS", "dev,staging")],
10550 ),
10551 "Invalid choice for option env: prod, expected one of dev, staging",
10552 );
10553 }
10554
10555 #[test]
10556 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
10557 let spec: Spec = r#"
10558 flag "-v --verbose" var=#true
10559 arg "[database]" default="myapp_dev"
10560 arg "[args...]"
10561 "#
10562 .parse()
10563 .unwrap();
10564 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10565 .into_iter()
10566 .map(String::from)
10567 .collect();
10568 let parsed = parse(&spec, &input).unwrap();
10569 let env = parsed.as_env();
10570 assert_eq!(env.get("usage_database").unwrap(), "mydb");
10571 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
10572 }
10573
10574 #[test]
10575 fn test_variadic_arg_captures_unknown_flags() {
10576 let cmd = SpecCommand::builder()
10577 .name("test")
10578 .flag(SpecFlag::builder().short('v').long("verbose").build())
10579 .arg(SpecArg::builder().name("database").required(false).build())
10580 .arg(
10581 SpecArg::builder()
10582 .name("args")
10583 .required(false)
10584 .var(true)
10585 .build(),
10586 )
10587 .build();
10588 let spec = Spec {
10589 name: "test".to_string(),
10590 bin: "test".to_string(),
10591 cmd,
10592 ..Default::default()
10593 };
10594
10595 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
10597 .into_iter()
10598 .map(String::from)
10599 .collect();
10600 let parsed = parse(&spec, &input).unwrap();
10601 assert_eq!(parsed.args.len(), 2);
10602 let args_val = parsed
10603 .args
10604 .iter()
10605 .find(|(a, _)| a.name == "args")
10606 .unwrap()
10607 .1;
10608 match args_val {
10609 ParseValue::MultiString(v) => {
10610 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10611 }
10612 _ => panic!("Expected MultiString, got {:?}", args_val),
10613 }
10614 }
10615
10616 #[test]
10617 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
10618 let cmd = SpecCommand::builder()
10619 .name("test")
10620 .flag(SpecFlag::builder().short('v').long("verbose").build())
10621 .arg(SpecArg::builder().name("database").required(false).build())
10622 .arg(
10623 SpecArg::builder()
10624 .name("args")
10625 .required(false)
10626 .var(true)
10627 .build(),
10628 )
10629 .build();
10630 let spec = Spec {
10631 name: "test".to_string(),
10632 bin: "test".to_string(),
10633 cmd,
10634 ..Default::default()
10635 };
10636
10637 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
10639 .into_iter()
10640 .map(String::from)
10641 .collect();
10642 let parsed = parse(&spec, &input).unwrap();
10643 assert_eq!(parsed.args.len(), 2);
10644 let args_val = parsed
10645 .args
10646 .iter()
10647 .find(|(a, _)| a.name == "args")
10648 .unwrap()
10649 .1;
10650 match args_val {
10651 ParseValue::MultiString(v) => {
10652 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
10653 }
10654 _ => panic!("Expected MultiString, got {:?}", args_val),
10655 }
10656 }
10657
10658 #[test]
10659 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
10660 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
10663
10664 let input: Vec<String> = vec!["test", "--option=3"]
10667 .into_iter()
10668 .map(String::from)
10669 .collect();
10670 let parsed = parse(&spec, &input).unwrap();
10671 let env = parsed.as_env();
10672 assert_eq!(
10673 env.get("usage_other_args").map(String::as_str),
10674 Some("'--option=3'"),
10675 "expected a single --option=3 token, got {:?}",
10676 env.get("usage_other_args"),
10677 );
10678
10679 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
10681 .into_iter()
10682 .map(String::from)
10683 .collect();
10684 let parsed2 = parse(&spec, &input2).unwrap();
10685 let env2 = parsed2.as_env();
10686 assert_eq!(
10687 env2.get("usage_other_args").map(String::as_str),
10688 Some("'--foo=bar' '--baz=qux'"),
10689 "expected two intact tokens, got {:?}",
10690 env2.get("usage_other_args"),
10691 );
10692
10693 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
10695 .into_iter()
10696 .map(String::from)
10697 .collect();
10698 let parsed3 = parse(&spec, &input3).unwrap();
10699 let env3 = parsed3.as_env();
10700 assert_eq!(
10701 env3.get("usage_other_args").map(String::as_str),
10702 Some("positional1 '--option=3' positional2"),
10703 "expected positional args and intact flag token, got {:?}",
10704 env3.get("usage_other_args"),
10705 );
10706 }
10707
10708 #[test]
10709 fn test_allow_hyphen_values_consumes_short_flag_collision() {
10710 let spec = r#"
10711flag "-d --working-dir <DIR>"
10712flag "-a --args <ARGS>" allow_hyphen_values=#true
10713"#
10714 .parse::<Spec>()
10715 .unwrap();
10716
10717 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
10718
10719 assert_eq!(parsed.flags.len(), 1);
10720 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10721 }
10722
10723 #[test]
10724 fn test_allow_hyphen_values_consumes_embedded_long_value() {
10725 let spec = r#"
10726flag "-d --working-dir <DIR>"
10727flag "-a --args <ARGS>" allow_hyphen_values=#true
10728"#
10729 .parse::<Spec>()
10730 .unwrap();
10731
10732 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
10733
10734 assert_eq!(parsed.flags.len(), 1);
10735 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
10736 }
10737
10738 #[test]
10739 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
10740 let spec = r#"
10745flag "-a --args <ARGS>" allow_hyphen_values=#true
10746arg "[rest]..."
10747"#
10748 .parse::<Spec>()
10749 .unwrap();
10750
10751 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
10752
10753 assert_eq!(flag_string_value(&parsed, "args"), "--");
10754 let rest = parsed
10755 .args
10756 .values()
10757 .next()
10758 .expect("expected the word after the separator to reach the argument");
10759 assert_eq!(rest.to_string(), "-x");
10760 }
10761
10762 #[test]
10763 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
10764 let spec = r#"
10769flag "-a --args <ARGS>..." allow_hyphen_values=#true
10770"#
10771 .parse::<Spec>()
10772 .unwrap();
10773
10774 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
10775
10776 let flag = parsed
10777 .flags
10778 .keys()
10779 .find(|flag| flag.name == "args")
10780 .expect("expected args flag");
10781 match parsed.flags.get(flag).expect("expected args value") {
10782 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
10783 other => panic!("expected a list of values, got {other:?}"),
10784 }
10785 }
10786
10787 #[test]
10788 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
10789 let spec = r#"
10790flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
10791"#
10792 .parse::<Spec>()
10793 .unwrap();
10794
10795 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
10796
10797 let flag = parsed
10798 .flags
10799 .keys()
10800 .find(|flag| flag.name == "args")
10801 .expect("expected args flag");
10802 let value = parsed.flags.get(flag).expect("expected args value");
10803 match value {
10804 ParseValue::MultiString(values) => {
10805 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
10806 }
10807 _ => panic!("expected MultiString, got {value:?}"),
10808 }
10809 }
10810
10811 #[test]
10812 fn test_require_equals_accepts_attached_and_refuses_detached() {
10813 let spec = r#"
10814flag "--inspect <PORT>" require_equals=#true
10815"#
10816 .parse::<Spec>()
10817 .unwrap();
10818
10819 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
10820 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
10821
10822 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
10823 let msg = format!("{err}");
10824 assert!(
10825 msg.contains("requires an argument") || msg.contains("inspect"),
10826 "detached value must be refused: {msg}"
10827 );
10828 }
10829
10830 #[test]
10831 fn boolean_flags_can_accept_attached_values_when_enabled() {
10832 let spec: Spec = r#"
10833name "ex"
10834bin "ex"
10835flag "--color" negate="--no-color" bool_value=#true
10836arg "[rest]"
10837"#
10838 .parse()
10839 .unwrap();
10840
10841 for (token, expected) in [
10842 ("--color", true),
10843 ("--color=true", true),
10844 ("--color=false", false),
10845 ("--no-color", false),
10846 ("--no-color=false", true),
10847 ] {
10848 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
10849 assert!(
10850 matches!(
10851 parsed.flags.get(&spec.cmd.flags[0]),
10852 Some(ParseValue::Bool(value)) if *value == expected
10853 ),
10854 "{token}"
10855 );
10856 }
10857
10858 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
10859 assert!(matches!(
10860 parsed.args.get(&spec.cmd.args[0]),
10861 Some(ParseValue::String(value)) if value == "word"
10862 ));
10863 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
10864 assert!(err.to_string().contains("expected `true` or `false`"));
10865
10866 let strict: Spec = r#"
10867name "ex"
10868bin "ex"
10869args_override_self #false
10870flag "--color" negate="--no-color" bool_value=#true
10871"#
10872 .parse()
10873 .unwrap();
10874 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
10875 let parsed = parse(
10876 &strict,
10877 &input(&["ex", "--color=false", "--no-color=false"]),
10878 )
10879 .unwrap();
10880 assert!(matches!(
10881 parsed.flags.get(&strict.cmd.flags[0]),
10882 Some(ParseValue::Bool(true))
10883 ));
10884 }
10885
10886 #[test]
10887 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
10888 let spec = r#"
10889flag "-a --all"
10890flag "-i --inspect <PORT>" require_equals=#true
10891"#
10892 .parse::<Spec>()
10893 .unwrap();
10894
10895 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
10896 let msg = format!("{err}");
10897 assert!(
10898 msg.contains("requires an argument") || msg.contains("inspect"),
10899 "bundled short must refuse the following word: {msg}"
10900 );
10901 }
10902
10903 #[test]
10904 fn test_default_missing_binds_when_the_value_is_left_off() {
10905 let spec = r#"
10906flag "-c --color <WHEN>" default_missing="always"
10907flag "-v --verbose"
10908"#
10909 .parse::<Spec>()
10910 .unwrap();
10911
10912 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
10913 assert_eq!(flag_string_value(&parsed, "color"), "always");
10914
10915 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
10916 assert_eq!(flag_string_value(&parsed, "color"), "never");
10917
10918 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
10919 assert_eq!(flag_string_value(&parsed, "color"), "never");
10920
10921 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
10922 assert_eq!(flag_string_value(&parsed, "color"), "always");
10923 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10924
10925 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
10926 assert_eq!(flag_string_value(&parsed, "color"), "");
10927
10928 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
10929 assert_eq!(flag_string_value(&parsed, "color"), "never");
10930
10931 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
10932 assert_eq!(flag_string_value(&parsed, "color"), "always");
10933 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
10934 }
10935
10936 #[test]
10937 fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
10938 let spec = r#"
10939flag "--apps <N>"
10940flag "--jobs <N>" default_missing="default missing"
10941flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
10942"#
10943 .parse::<Spec>()
10944 .unwrap();
10945
10946 let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
10947 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10948
10949 let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
10950 assert!(
10951 err.to_string().contains("unexpected word: -1"),
10952 "default_missing must keep an unopted negative value separate: {err}"
10953 );
10954
10955 let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
10956 assert_eq!(flag_string_value(&parsed, "kids"), "-1");
10957
10958 let external_spec = r#"
10959external_subcommand #true
10960flag "--apps <N>"
10961"#
10962 .parse::<Spec>()
10963 .unwrap();
10964 let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
10965 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
10966 assert!(parsed.external.is_none());
10967
10968 for words in [
10969 &["test", "--apps", "--jobs", "1"][..],
10970 &["test", "--apps", "--kids", "-1"][..],
10971 ] {
10972 let err = parse(&spec, &input(words)).unwrap_err();
10973 let message = err.to_string();
10974 assert!(
10975 message.contains("--apps") && message.contains("requires an argument"),
10976 "the earlier flag must report its missing value for {words:?}: {message}"
10977 );
10978 }
10979 }
10980
10981 #[test]
10982 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
10983 let spec = r#"
10984flag "--bump [LEVEL]" value_optional=#true
10985flag "--verbose"
10986arg "[FILE]"
10987"#
10988 .parse::<Spec>()
10989 .unwrap();
10990
10991 let absent = parse(&spec, &input(&["test"])).unwrap();
10992 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
10993
10994 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
10995 let bump = bare
10996 .flags
10997 .iter()
10998 .find(|(flag, _)| flag.name == "bump")
10999 .map(|(_, value)| value)
11000 .unwrap();
11001 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
11002 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
11003 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
11004
11005 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
11006 assert_eq!(flag_string_value(&explicit, "bump"), "");
11007
11008 let corrected = parse(
11009 &spec,
11010 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
11011 )
11012 .unwrap();
11013 let bump = corrected
11014 .flags
11015 .iter()
11016 .find(|(flag, _)| flag.name == "bump")
11017 .map(|(_, value)| value)
11018 .unwrap();
11019 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
11020
11021 let collecting = r#"
11022flag "--tag [TAG]..." value_optional=#true
11023flag "--verbose"
11024"#
11025 .parse::<Spec>()
11026 .unwrap();
11027 let valued = parse(
11028 &collecting,
11029 &input(&["test", "--tag", "one", "two", "--verbose"]),
11030 )
11031 .unwrap();
11032 let tag = valued
11033 .flags
11034 .iter()
11035 .find(|(flag, _)| flag.name == "tag")
11036 .map(|(_, value)| value)
11037 .unwrap();
11038 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
11039 }
11040
11041 #[test]
11042 fn test_repeatable_bare_optional_values_count_each_occurrence() {
11043 let spec = r#"
11044flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
11045"#
11046 .parse::<Spec>()
11047 .unwrap();
11048
11049 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
11050 let tag = parsed
11051 .flags
11052 .iter()
11053 .find(|(flag, _)| flag.name == "tag")
11054 .map(|(_, value)| value)
11055 .unwrap();
11056 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
11057
11058 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
11059 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
11060 }
11061
11062 #[test]
11063 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
11064 let spec = r#"
11065flag "--tag [TAG]..." var=#true value_optional=#true
11066flag "--verbose"
11067"#
11068 .parse::<Spec>()
11069 .unwrap();
11070
11071 for argv in [
11072 &["test", "--tag", "one", "two"][..],
11073 &["test", "--tag", "one", "two", "--verbose"][..],
11074 &["test", "--tag", "one", "--tag", "two"][..],
11075 ] {
11076 let parsed = parse(&spec, &input(argv)).unwrap();
11077 let tag = parsed
11078 .flags
11079 .iter()
11080 .find(|(flag, _)| flag.name == "tag")
11081 .map(|(_, value)| value)
11082 .unwrap();
11083 assert!(
11084 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
11085 "argv={argv:?}: {tag:?}"
11086 );
11087 }
11088
11089 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
11090 let tag = bare
11091 .flags
11092 .iter()
11093 .find(|(flag, _)| flag.name == "tag")
11094 .map(|(_, value)| value)
11095 .unwrap();
11096 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
11097 }
11098
11099 #[test]
11100 fn test_default_missing_with_require_equals_refuses_the_following_word() {
11101 let spec = r#"
11102flag "--inspect <PORT>" require_equals=#true default_missing="9229"
11103arg "[rest]"
11104"#
11105 .parse::<Spec>()
11106 .unwrap();
11107
11108 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
11109 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
11110
11111 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
11112 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
11113
11114 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
11116 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
11117 assert_eq!(
11118 parsed
11119 .args
11120 .values()
11121 .next()
11122 .map(|v| v.to_string())
11123 .as_deref(),
11124 Some("80")
11125 );
11126
11127 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
11128 assert_eq!(flag_string_value(&parsed, "inspect"), "");
11129 }
11130
11131 #[test]
11132 fn test_default_missing_must_be_a_choice() {
11133 let spec = r#"
11134flag "--color <WHEN>" default_missing="always" {
11135 choices "auto" "always" "never"
11136}
11137"#
11138 .parse::<Spec>()
11139 .unwrap();
11140
11141 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
11142 assert_eq!(flag_string_value(&parsed, "color"), "always");
11143
11144 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
11145 assert_eq!(flag_string_value(&parsed, "color"), "never");
11146
11147 let spec = r#"
11148flag "--color <WHEN>" default_missing="wat" {
11149 choices "auto" "always" "never"
11150}
11151"#
11152 .parse::<Spec>()
11153 .unwrap();
11154
11155 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
11156 let msg = format!("{err}");
11157 assert!(
11158 msg.contains("Invalid choice for option color: wat"),
11159 "missing default has to pass choices the same way a typed value does: {msg}"
11160 );
11161
11162 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
11163 let msg = format!("{err}");
11164 assert!(
11165 msg.contains("Invalid choice for option color: wat"),
11166 "an attached value that is not a choice is still refused: {msg}"
11167 );
11168
11169 let spec = r#"
11170flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
11171 choices "9229" "80"
11172}
11173arg "[rest]"
11174"#
11175 .parse::<Spec>()
11176 .unwrap();
11177
11178 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
11179 let msg = format!("{err}");
11180 assert!(
11181 msg.contains("Invalid choice for option inspect: wat"),
11182 "require_equals still binds the missing string, so the error is the choice: {msg}"
11183 );
11184 }
11185
11186 #[test]
11187 fn test_hyphen_values_still_start_short_flag_parsing() {
11188 let spec = r#"
11189flag "-d --working-dir <DIR>"
11190flag "-a --args <ARGS>"
11191"#
11192 .parse::<Spec>()
11193 .unwrap();
11194
11195 let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
11196 let message = err.to_string();
11197 assert!(
11198 message.contains("--args") && message.contains("requires an argument"),
11199 "the recognized -d must leave the earlier -a missing: {message}"
11200 );
11201 }
11202
11203 mod available_flags {
11206 use super::*;
11207
11208 fn spec() -> Spec {
11209 r#"
11210bin "test"
11211flag "-v --verbose" global=#true
11212flag "--raw" global=#true effect="write"
11213flag "--local-only"
11214cmd "run" {
11215 flag "-r --raw"
11216 flag "-w --watch"
11217 cmd "once"
11218}
11219"#
11220 .parse::<Spec>()
11221 .unwrap()
11222 }
11223
11224 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
11225 let mut chain = vec![&spec.cmd];
11226 for segment in path {
11227 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
11228 }
11229 chain
11230 }
11231
11232 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
11233 let mut names: Vec<_> = available_flags(&chain(spec, path))
11234 .iter()
11235 .map(|f| f.name.clone())
11236 .collect();
11237 names.sort();
11238 names
11239 }
11240
11241 #[test]
11242 fn an_empty_chain_yields_nothing() {
11243 assert!(available_flags(&[]).is_empty());
11244 }
11245
11246 #[test]
11247 fn the_root_gets_its_own_flags() {
11248 let spec = spec();
11249 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
11250 }
11251
11252 #[test]
11253 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
11254 let spec = spec();
11255 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
11256 }
11257
11258 #[test]
11259 fn a_re_declared_global_is_listed_once() {
11260 let spec = r#"
11263bin "test"
11264flag "-y --yes" global=#true effect="write"
11265cmd "rm" {
11266 flag "-y --yes"
11267}
11268"#
11269 .parse::<Spec>()
11270 .unwrap();
11271 let flags = available_flags(&chain(&spec, &["rm"]));
11272 assert_eq!(flags.len(), 1, "{flags:?}");
11273 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
11274 }
11275
11276 #[test]
11277 fn a_re_declared_global_keeps_the_globals_declaration() {
11278 let spec = spec();
11282 let flags = available_flags(&chain(&spec, &["run"]));
11283 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
11284 assert!(raw.global);
11285 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
11286 assert_eq!(raw.short, ['r']);
11287 }
11288
11289 #[test]
11290 fn it_matches_what_a_parse_accepts() {
11291 let spec = spec();
11294 for path in [vec![], vec!["run"], vec!["run", "once"]] {
11295 let argv = std::iter::once("test".to_string())
11296 .chain(path.iter().map(|s| s.to_string()))
11297 .collect::<Vec<_>>();
11298 let parsed = parse_partial(&spec, &argv).unwrap();
11299
11300 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
11301 .map(|f| f.name.clone())
11302 .collect();
11303 from_parse.sort();
11304 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
11305 }
11306 }
11307 }
11308
11309 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
11314 parsed
11315 .tokens
11316 .iter()
11317 .find(|token| token.index == index)
11318 .unwrap_or_else(|| panic!("no token at {index}"))
11319 .roles
11320 .iter()
11321 .map(render_role)
11322 .collect()
11323 }
11324
11325 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
11326 parsed
11327 .flag_origins
11328 .iter()
11329 .find(|(f, _)| f.name == flag)
11330 .map(|(_, origins)| origins.clone())
11331 .unwrap_or_default()
11332 }
11333
11334 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
11335 let env = env
11336 .iter()
11337 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
11338 .collect();
11339 Parser::new(spec)
11340 .with_env(env)
11341 .explain(&input(words))
11342 .unwrap()
11343 }
11344
11345 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
11346 explain_with_env(spec, words, &[])
11347 }
11348
11349 #[test]
11350 fn an_attached_long_value_is_recorded_on_the_flag_token() {
11351 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11352 .parse()
11353 .unwrap();
11354
11355 let parsed = explain(&spec, &["ex", "--env=prod"]);
11356
11357 assert_eq!(roles(&parsed, 0), ["program"]);
11358 assert_eq!(
11359 roles(&parsed, 1),
11360 ["flag env as --env", "value of env = [\"prod\"], attached"]
11361 );
11362 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
11365 }
11366
11367 #[test]
11368 fn a_detached_long_value_is_recorded_on_its_own_token() {
11369 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
11370 .parse()
11371 .unwrap();
11372
11373 let parsed = explain(&spec, &["ex", "--env", "prod"]);
11374
11375 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
11376 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
11377 }
11378
11379 #[test]
11380 fn a_short_bundle_is_attributed_to_the_bundle_token() {
11381 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
11382 .parse()
11383 .unwrap();
11384
11385 let parsed = explain(&spec, &["ex", "-abj8"]);
11386
11387 assert_eq!(
11390 roles(&parsed, 1),
11391 [
11392 "flag a as -a",
11393 "flag b as -b",
11394 "flag j as -j",
11395 "value of j = [\"8\"], attached",
11396 ]
11397 );
11398 assert_eq!(parsed.tokens.len(), 2);
11399 }
11400
11401 #[test]
11402 fn a_delimiter_splits_one_token_into_several_values() {
11403 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
11404 .parse()
11405 .unwrap();
11406
11407 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
11408
11409 assert_eq!(
11410 roles(&parsed, 2),
11411 ["value of tags = [\"a\", \"b\", \"c\"]"],
11412 "the values meant, not the word typed"
11413 );
11414 }
11415
11416 #[test]
11417 fn a_separator_and_the_words_after_it_are_distinguished() {
11418 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
11419 .parse()
11420 .unwrap();
11421
11422 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
11423
11424 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
11425 assert_eq!(roles(&parsed, 2), ["separator"]);
11426 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
11428 }
11429
11430 #[test]
11431 fn a_second_separator_is_data() {
11432 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
11433 .parse()
11434 .unwrap();
11435
11436 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
11437
11438 assert_eq!(roles(&parsed, 1), ["separator"]);
11439 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
11440 }
11441
11442 #[test]
11443 fn an_unknown_flag_says_what_took_it() {
11444 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11445 .parse()
11446 .unwrap();
11447
11448 let parsed = explain(&spec, &["ex", "--wat"]);
11449
11450 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
11453 }
11454
11455 #[test]
11456 fn a_subcommand_word_is_not_a_positional() {
11457 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
11458 .parse()
11459 .unwrap();
11460
11461 let parsed = explain(&spec, &["ex", "build", "a"]);
11462
11463 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
11464 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
11465 }
11466
11467 #[test]
11468 fn a_multicall_applet_is_read_at_argv0() {
11469 let spec: Spec =
11470 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
11471 .parse()
11472 .unwrap();
11473
11474 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
11475
11476 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
11479 assert!(parsed.tokens[0].synthesized);
11480 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
11481 }
11482
11483 #[test]
11484 fn words_the_parse_never_reached_say_so() {
11485 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
11486 .parse()
11487 .unwrap();
11488
11489 let parsed = Parser::new(&spec)
11490 .explain(&input(&["ex", "--help", "a"]))
11491 .unwrap();
11492
11493 assert_eq!(roles(&parsed, 2), ["unread"]);
11494 }
11495
11496 #[test]
11497 fn an_env_origin_names_the_variable_that_fired() {
11498 let spec: Spec =
11499 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
11500 .parse()
11501 .unwrap();
11502
11503 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
11504 assert_eq!(
11505 origins(&primary, "token"),
11506 [ValueOrigin::Env("EX_TOKEN".to_string())]
11507 );
11508
11509 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
11512 assert_eq!(
11513 origins(&fallback, "token"),
11514 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
11515 );
11516 }
11517
11518 #[test]
11519 fn a_default_origin_is_recorded_for_flags_and_args() {
11520 let spec: Spec =
11521 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
11522 .parse()
11523 .unwrap();
11524
11525 let parsed = explain(&spec, &["ex"]);
11526
11527 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
11528 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
11529 assert_eq!(arg.name, "src");
11530 assert_eq!(origins, &[ValueOrigin::Default]);
11531 }
11532
11533 #[test]
11534 fn a_default_if_origin_carries_the_condition_that_fired() {
11535 let spec: Spec = r#"
11536name "ex"
11537bin "ex"
11538flag "--profile <p>"
11539flag "--strict" {
11540 default_if "--profile" "prod" "true"
11541}
11542 "#
11543 .parse()
11544 .unwrap();
11545
11546 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
11547
11548 assert_eq!(
11551 origins(&parsed, "strict"),
11552 [ValueOrigin::DefaultIf {
11553 selector: "--profile".to_string(),
11554 when: Some("prod".to_string()),
11555 }]
11556 );
11557 }
11558
11559 #[test]
11560 fn a_bare_optional_value_flag_records_default_missing() {
11561 let spec: Spec =
11562 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
11563 .parse()
11564 .unwrap();
11565
11566 let parsed = explain(&spec, &["ex", "--color", "-v"]);
11567
11568 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
11571 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11572 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
11573 }
11574
11575 #[test]
11576 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
11577 let spec: Spec =
11578 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
11579 .parse()
11580 .unwrap();
11581
11582 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
11583
11584 assert_eq!(
11586 roles(&parsed, 1),
11587 [
11588 "flag color as --color",
11589 "value of color = [\"red\"], attached"
11590 ]
11591 );
11592 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
11593 }
11594
11595 #[test]
11596 fn an_override_names_the_flag_that_did_it() {
11597 let spec: Spec =
11598 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
11599 .parse()
11600 .unwrap();
11601
11602 let parsed = explain(&spec, &["ex", "--loud"]);
11603
11604 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
11607 assert!(origins(&parsed, "quiet").is_empty());
11608 }
11609
11610 #[test]
11611 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
11612 let spec: Spec = r#"
11613name "ex"
11614bin "ex"
11615cmd "run" restart_token=":::" {
11616 arg "<task>" default="build"
11617}
11618 "#
11619 .parse()
11620 .unwrap();
11621
11622 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
11623
11624 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
11628 assert_eq!(roles(&parsed, 3), ["restart"]);
11631 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
11632 assert!(parsed.arg_origins.is_empty());
11633 }
11634
11635 #[test]
11636 fn a_value_terminator_says_which_run_it_ended() {
11637 let spec: Spec = r#"
11638name "ex"
11639bin "ex"
11640flag "--exec <cmd>..." value_terminator=";"
11641arg "<src>"
11642 "#
11643 .parse()
11644 .unwrap();
11645
11646 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
11647
11648 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
11649 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
11652 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
11653 }
11654
11655 #[test]
11656 fn an_args_value_terminator_says_which_run_it_ended() {
11657 let spec: Spec = r#"
11658name "ex"
11659bin "ex"
11660arg "<files>..." value_terminator=";"
11661arg "[dest]"
11662 "#
11663 .parse()
11664 .unwrap();
11665
11666 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
11667
11668 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
11669 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
11670 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
11671 }
11672
11673 #[test]
11674 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
11675 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
11676 .parse()
11677 .unwrap();
11678
11679 let parsed = Parser::new(&spec)
11680 .explain(&input(&["ex", "--env=prod"]))
11681 .unwrap();
11682
11683 assert!(
11686 Parser::new(&spec)
11687 .parse(&input(&["ex", "--env=prod"]))
11688 .is_err()
11689 );
11690 assert_eq!(
11691 roles(&parsed, 1),
11692 ["flag env as --env", "value of env = [\"prod\"], attached"]
11693 );
11694 assert!(
11695 parsed.errors.iter().any(|e| e.to_string().contains("src")),
11696 "{:?}",
11697 parsed.errors
11698 );
11699 }
11700
11701 #[test]
11702 fn an_external_subcommand_forwards_whole_tokens() {
11703 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
11704 .parse()
11705 .unwrap();
11706
11707 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
11708
11709 assert_eq!(roles(&parsed, 1), ["external"]);
11710 assert_eq!(roles(&parsed, 2), ["external"]);
11711 }
11712
11713 #[test]
11714 fn a_view_keeps_the_callers_argv_positions() {
11715 let spec: Spec = r#"
11716bin "ex"
11717view "runner" root="run"
11718cmd "run" {
11719 flag "--token <token>"
11720}
11721 "#
11722 .parse()
11723 .unwrap();
11724
11725 let parsed = explain(&spec, &["runner", "--token", "secret"]);
11726
11727 assert_eq!(roles(&parsed, 0), ["program"]);
11730 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
11731 }
11732
11733 #[test]
11734 fn empty_default_does_not_hide_a_missing_parent_flag_value() {
11735 let spec: Spec = r#"
11736name "ex"
11737bin "ex"
11738default_subcommand "run"
11739default_subcommand_on_empty #true
11740flag "--value <value>"
11741cmd "run" arg_required_else_help=#true
11742"#
11743 .parse()
11744 .unwrap();
11745 let err = Parser::new(&spec)
11746 .parse(&input(&["ex", "--value"]))
11747 .unwrap_err();
11748 assert!(err.to_string().contains("requires an argument"), "{err}");
11749 assert!(!err.to_string().contains("Usage: ex run"), "{err}");
11750 }
11751
11752 #[test]
11753 fn empty_default_honors_parent_args_conflict_policy() {
11754 let spec: Spec = r#"
11755name "ex"
11756bin "ex"
11757default_subcommand "run"
11758default_subcommand_on_empty #true
11759args_conflicts_with_subcommands #true
11760flag "--verbose"
11761cmd "run"
11762"#
11763 .parse()
11764 .unwrap();
11765 let err = Parser::new(&spec)
11766 .parse(&input(&["ex", "--verbose"]))
11767 .unwrap_err();
11768 assert!(
11769 err.to_string().contains("cannot be used with arguments"),
11770 "{err}"
11771 );
11772 }
11773
11774 #[test]
11775 fn the_parse_records_which_flags_a_mounted_program_declared() {
11776 let mut spec: Spec = r#"
11777bin ex
11778flag "-p --pick <tool>" global=#true
11779cmd run {
11780 mount run=tasks
11781}
11782"#
11783 .parse()
11784 .unwrap();
11785 let mounted = r#"
11786cmd outer {
11787 cmd inner {
11788 flag "-p --other <name>"
11789 }
11790}
11791cmd deep {
11792 flag "--pick <tool>" global=#true
11793 cmd leaf {
11794 flag "-p --pick <tool>"
11795 }
11796}
11797"#;
11798 spec.resolve_mount_outputs(&HashMap::from([("tasks".to_string(), mounted.to_string())]))
11799 .unwrap();
11800 let pending_is_mounted = |words: &[&str]| {
11801 let words: Vec<String> = words.iter().map(|word| word.to_string()).collect();
11802 let out = parse_partial(&spec, &words).unwrap();
11803 let flag = out
11804 .flag_awaiting_value
11805 .first()
11806 .expect("a flag awaiting its value");
11807 out.is_mounted_flag(flag)
11808 };
11809
11810 assert!(!pending_is_mounted(&["ex", "--pick"]));
11811 assert!(!pending_is_mounted(&["ex", "run", "outer", "inner", "-p"]));
11813 assert!(!pending_is_mounted(&[
11814 "ex", "run", "outer", "inner", "--pick"
11815 ]));
11816 assert!(pending_is_mounted(&[
11817 "ex", "run", "outer", "inner", "--other"
11818 ]));
11819 assert!(pending_is_mounted(&["ex", "run", "deep", "--pick"]));
11822 assert!(pending_is_mounted(&["ex", "run", "deep", "leaf", "--pick"]));
11823 assert!(!pending_is_mounted(&["ex", "run", "deep", "leaf", "-p"]));
11824 }
11825
11826 #[test]
11827 fn empty_default_mount_matches_explicit_selection() {
11828 let spec: Spec = r#"
11829name "ex"
11830bin "ex"
11831default_subcommand "run"
11832default_subcommand_on_empty #true
11833cmd "run" { mount run="discover" }
11834"#
11835 .parse()
11836 .unwrap();
11837 let mut mounts = HashMap::new();
11838 mounts.insert(
11839 "discover".to_string(),
11840 "name \"run\"\nbin \"run\"\nflag \"--mounted <mounted>\" default=\"yes\"\n".to_string(),
11841 );
11842 for argv in [
11843 vec!["ex".to_string()],
11844 vec!["ex".to_string(), "run".to_string()],
11845 ] {
11846 let out = Parser::new(&spec)
11847 .with_mount_outputs(mounts.clone())
11848 .parse(&argv)
11849 .unwrap();
11850 assert_eq!(out.cmds.last().unwrap().name, "run");
11851 assert_eq!(out.flags.values().next().unwrap().to_string(), "yes");
11852 }
11853 }
11854
11855 #[test]
11856 fn pending_parent_value_stays_pending_in_partial_parse() {
11857 let spec: Spec = r#"
11858name "ex"
11859bin "ex"
11860default_subcommand "run"
11861default_subcommand_on_empty #true
11862flag "--value <value>"
11863cmd "run" {}
11864"#
11865 .parse()
11866 .unwrap();
11867 let out = parse_partial(&spec, &input(&["ex", "--value"])).unwrap();
11868 assert_eq!(out.cmd.name, "ex");
11869 assert_eq!(out.flag_awaiting_value[0].name, "value");
11870 }
11871
11872 #[test]
11873 fn optional_default_missing_still_selects_empty_child() {
11874 let spec: Spec = r#"
11875name "ex"
11876bin "ex"
11877default_subcommand "run"
11878default_subcommand_on_empty #true
11879flag "--value <value>" default_missing="fallback"
11880cmd "run" {}
11881"#
11882 .parse()
11883 .unwrap();
11884 let out = Parser::new(&spec)
11885 .parse(&input(&["ex", "--value"]))
11886 .unwrap();
11887 assert_eq!(out.cmd.name, "run");
11888 assert_eq!(out.flags.values().next().unwrap().to_string(), "fallback");
11889 }
11890
11891 #[test]
11892 fn empty_default_resets_cursor_after_root_sigil() {
11893 let spec: Spec = r#"
11894name "ex"
11895bin "ex"
11896default_subcommand "run"
11897default_subcommand_on_empty #true
11898arg "[tool]..." sigil="@"
11899cmd "run" { arg "[path]" }
11900"#
11901 .parse()
11902 .unwrap();
11903 let out = parse_partial(&spec, &input(&["ex"])).unwrap();
11904 assert_eq!(out.cmd.name, "run");
11905 assert_eq!(out.next_arg.as_ref().unwrap().name, "path");
11906 }
11907
11908 #[test]
11909 fn root_clause_positional_prevents_empty_default() {
11910 let spec: Spec = r#"
11911name "ex"
11912bin "ex"
11913default_subcommand "run"
11914default_subcommand_on_empty #true
11915clause "tasks" {
11916 arg "<number>" allow_negative_numbers=#true
11917}
11918cmd "run" { arg "[task]" }
11919"#
11920 .parse()
11921 .unwrap();
11922 let out = Parser::new(&spec).parse(&input(&["ex", "-7"])).unwrap();
11923 assert!(
11924 out.cmds.iter().all(|cmd| cmd.name != "run"),
11925 "cmds={:?} args={:?}",
11926 out.cmds.iter().map(|c| &c.name).collect::<Vec<_>>(),
11927 out.args
11928 );
11929 let number = spec.cmd.clause.as_ref().unwrap().args.first().unwrap();
11930 assert_eq!(
11931 out.clauses["tasks"][0].get(number).unwrap().to_string(),
11932 "-7"
11933 );
11934 }
11935}