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