1use crate::miette::{self, bail};
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
6use std::fmt::{Debug, Display, Formatter};
7use std::sync::Arc;
8
9#[cfg(feature = "cli-help")]
10use crate::docs;
11use crate::error::UsageErr;
12use crate::spec::arg::SpecDoubleDashChoices;
13use crate::spec::unknown_flags::UnknownFlags;
14use crate::warn::Warning;
15use crate::{Spec, SpecArg, SpecChoices, SpecCommand, SpecFlag};
16
17fn merge_subcommand_flags(
35 available: &mut BTreeMap<String, Arc<SpecFlag>>,
36 new_flags: BTreeMap<String, Arc<SpecFlag>>,
37 crossing_mount: bool,
38) {
39 available.retain(|_, f| f.global);
41
42 if crossing_mount {
43 for (key, flag) in new_flags {
48 available.insert(key, flag);
49 }
50 return;
51 }
52
53 let mut merged_cache: HashMap<usize, Arc<SpecFlag>> = HashMap::new();
56 let mut merged_origin: HashMap<usize, usize> = HashMap::new();
60 fn origin_of(merged_origin: &HashMap<usize, usize>, flag: &Arc<SpecFlag>) -> usize {
62 let ptr = Arc::as_ptr(flag) as usize;
63 *merged_origin.get(&ptr).unwrap_or(&ptr)
64 }
65
66 for (key, flag) in new_flags {
71 if flag.global {
72 available.insert(key, flag);
74 continue;
75 }
76
77 let inherited_global = flag.long.iter().find_map(|l| {
86 available
87 .get(&format!("--{l}"))
88 .filter(|f| f.global)
89 .cloned()
90 });
91 if let Some(global_flag) = inherited_global {
92 let global_origin = origin_of(&merged_origin, &global_flag);
102 if available.get(&key).is_some_and(|existing| {
103 existing.global && origin_of(&merged_origin, existing) != global_origin
104 }) {
105 continue;
106 }
107 let merged = match merged_cache.get(&(Arc::as_ptr(&flag) as usize)) {
108 Some(merged) => merged.clone(),
109 None => {
110 let mut merged = (*global_flag).clone();
111 for s in &flag.short {
119 if !merged.short.contains(s) {
120 merged.short.push(*s);
121 }
122 }
123 for l in &flag.long {
124 if !merged.long.contains(l) {
125 merged.long.push(l.clone());
126 }
127 }
128 merged.hidden_short_aliases.retain(|alias| {
132 !flag.short.contains(alias) || flag.hidden_short_aliases.contains(alias)
133 });
134 merged.hidden_aliases.retain(|alias| {
135 !flag.long.contains(alias) || flag.hidden_aliases.contains(alias)
136 });
137 for s in &flag.hidden_short_aliases {
138 if !merged.hidden_short_aliases.contains(s) {
139 merged.hidden_short_aliases.push(*s);
140 }
141 }
142 for l in &flag.hidden_aliases {
143 if !merged.hidden_aliases.contains(l) {
144 merged.hidden_aliases.push(l.clone());
145 }
146 }
147 let merged = Arc::new(merged);
148 merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
149 merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
150 for existing in available.values_mut() {
156 if origin_of(&merged_origin, existing) == global_origin {
157 *existing = Arc::clone(&merged);
158 }
159 }
160 merged
161 }
162 };
163 available.insert(key, merged);
164 continue;
165 }
166
167 if available.contains_key(&key) {
171 continue;
172 }
173 available.insert(key, flag);
174 }
175}
176
177fn flag_keys(flag: &SpecFlag) -> Vec<String> {
180 let mut keys: Vec<String> = flag
181 .long
182 .iter()
183 .map(|l| format!("--{l}"))
184 .chain(flag.short.iter().map(|s| format!("-{s}")))
185 .collect();
186 if let Some(negate) = &flag.negate {
187 keys.push(negate.clone());
188 }
189 keys
190}
191
192fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
194 cmd.flags
195 .iter()
196 .flat_map(|f| {
197 let f = Arc::new(f.clone()); flag_keys(&f)
199 .into_iter()
200 .map(|key| (key, Arc::clone(&f)))
201 .collect::<Vec<_>>()
202 })
203 .collect()
204}
205
206fn unique_flags<'a>(
207 flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
208) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
209 let mut seen = HashSet::new();
210 flags
211 .into_iter()
212 .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
213}
214
215pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
228 let Some((root, rest)) = chain.split_first() else {
229 return vec![];
230 };
231 let mut available = gather_flags(root);
232 for cmd in rest {
233 merge_subcommand_flags(&mut available, gather_flags(cmd), false);
234 }
235
236 let mut seen_names = HashSet::new();
247 unique_flags(available.values())
248 .filter(|f| seen_names.insert(f.name.clone()))
249 .cloned()
250 .collect()
251}
252
253fn get_flag_key(word: &str) -> &str {
256 if word.starts_with("--") {
257 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
259 } else if let Some((end, _)) = word.char_indices().nth(2) {
260 &word[0..end]
263 } else {
264 word
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum ValueOrigin {
277 DefaultMissing,
282 Env(String),
288 Default,
294 DefaultIf {
298 selector: String,
299 when: Option<String>,
300 },
301}
302
303#[derive(Debug, Clone)]
308#[non_exhaustive]
309pub enum TokenRole {
310 Program,
312 Command { name: String },
314 Flag {
316 flag: Arc<SpecFlag>,
317 spelling: String,
318 negated: bool,
319 },
320 Value {
322 flag: Arc<SpecFlag>,
323 values: Vec<String>,
324 attached: bool,
327 },
328 Arg {
330 arg: Arc<SpecArg>,
331 values: Vec<String>,
332 },
333 Separator,
335 Builtin { spelling: String },
340 ValueTerminator { ends: String },
344 Restart,
348 UnknownFlag { bound_as: Option<Arc<SpecArg>> },
351 Refused { reason: String },
354 External,
356 Unread,
358 Sigil {
360 arg: Arc<SpecArg>,
361 sigil: String,
362 values: Vec<String>,
363 },
364 ClauseSeparator { name: String },
366}
367
368#[derive(Debug, Clone)]
370#[non_exhaustive]
371pub struct TokenBinding {
372 pub index: usize,
374 pub word: String,
375 pub synthesized: bool,
379 pub roles: Vec<TokenRole>,
380}
381
382#[non_exhaustive]
383pub struct ParseOutput {
384 pub cmd: SpecCommand,
385 pub cmds: Vec<SpecCommand>,
386 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
387 pub clauses: IndexMap<String, Vec<IndexMap<Arc<SpecArg>, ParseValue>>>,
389 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
390 pub tokens: Vec<TokenBinding>,
397 pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
403 pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
406 pub overridden_flags: BTreeMap<String, String>,
412 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
419 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
420 pub errors: Vec<UsageErr>,
421 pub warnings: Vec<Warning>,
425 pub next_arg: Option<Arc<SpecArg>>,
432 pub double_dash_seen: bool,
438 pub external: Option<Vec<String>>,
443}
444
445impl ParseOutput {
446 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
455 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
456 return self.available_flags.clone();
457 };
458 let start = match boundary.checked_sub(1) {
462 Some(prev) if self.cmds[prev].flags_from_mount => prev,
463 _ => boundary,
464 };
465 let mut offered = gather_flags(&self.cmds[start]);
469 for cmd in &self.cmds[start + 1..] {
470 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
471 }
472 offered
473 }
474}
475
476#[derive(Debug, Clone)]
477pub enum ParseValue {
478 Bool(bool),
479 String(String),
480 MultiBool(Vec<bool>),
481 MultiString(Vec<String>),
482}
483
484impl ParseValue {
485 pub fn try_as_bool(self) -> Option<bool> {
486 match self {
487 Self::Bool(value) => Some(value),
488 _ => None,
489 }
490 }
491
492 pub const fn try_as_bool_ref(&self) -> Option<&bool> {
493 match self {
494 Self::Bool(value) => Some(value),
495 _ => None,
496 }
497 }
498
499 pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
500 match self {
501 Self::Bool(value) => Some(value),
502 _ => None,
503 }
504 }
505
506 pub fn try_as_string(self) -> Option<String> {
507 match self {
508 Self::String(value) => Some(value),
509 _ => None,
510 }
511 }
512
513 pub const fn try_as_string_ref(&self) -> Option<&String> {
514 match self {
515 Self::String(value) => Some(value),
516 _ => None,
517 }
518 }
519
520 pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
521 match self {
522 Self::String(value) => Some(value),
523 _ => None,
524 }
525 }
526
527 pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
528 match self {
529 Self::MultiBool(value) => Some(value),
530 _ => None,
531 }
532 }
533
534 pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
535 match self {
536 Self::MultiBool(value) => Some(value),
537 _ => None,
538 }
539 }
540
541 pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
542 match self {
543 Self::MultiBool(value) => Some(value),
544 _ => None,
545 }
546 }
547
548 pub fn try_as_multi_string(self) -> Option<Vec<String>> {
549 match self {
550 Self::MultiString(value) => Some(value),
551 _ => None,
552 }
553 }
554
555 pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
556 match self {
557 Self::MultiString(value) => Some(value),
558 _ => None,
559 }
560 }
561
562 pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
563 match self {
564 Self::MultiString(value) => Some(value),
565 _ => None,
566 }
567 }
568}
569
570fn collect_deprecations(out: &mut ParseOutput) {
581 for cmd in out.cmds.iter().skip(1) {
582 if cmd.deprecated.is_none()
583 && cmd.deprecated_warn_at.is_none()
584 && cmd.deprecated_remove_at.is_none()
585 {
586 continue;
587 }
588 out.warnings.push(Warning::command(
589 cmd.name.clone(),
590 cmd.deprecated.clone(),
591 cmd.deprecated_warn_at.clone(),
592 cmd.deprecated_remove_at.clone(),
593 ));
594 }
595 for flag in out.flags.keys() {
596 if let Some(warning) = flag_deprecation(flag) {
597 out.warnings.push(warning);
598 }
599 }
600}
601
602fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
604 if flag.deprecated.is_none()
605 && flag.deprecated_warn_at.is_none()
606 && flag.deprecated_remove_at.is_none()
607 {
608 return None;
609 }
610 Some(Warning::flag(
611 flag_spelling(flag),
612 flag.deprecated.clone(),
613 flag.deprecated_warn_at.clone(),
614 flag.deprecated_remove_at.clone(),
615 ))
616}
617
618fn flag_spelling(flag: &SpecFlag) -> String {
621 flag.long
622 .first()
623 .map(|long| format!("--{long}"))
624 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
625 .unwrap_or_else(|| flag.name.clone())
626}
627
628fn flag_current_env(flag: &SpecFlag) -> Option<String> {
630 flag.env
631 .clone()
632 .or_else(|| flag.env_fallback.first().cloned())
633}
634
635fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
636 flag.deprecated_env.iter().any(|declared| declared == name)
637}
638
639fn arg_current_env(arg: &SpecArg) -> Option<String> {
641 arg.env
642 .clone()
643 .or_else(|| arg.env_fallback.first().cloned())
644}
645
646fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
647 arg.deprecated_env.iter().any(|declared| declared == name)
648}
649
650fn first_set_env<'a>(
657 mut names: impl Iterator<Item = &'a str>,
658 get_env: &impl Fn(&str) -> Option<String>,
659) -> Option<(&'a str, String)> {
660 names.find_map(|name| get_env(name).map(|value| (name, value)))
661}
662
663#[non_exhaustive]
683pub struct Parser<'a> {
684 spec: &'a Spec,
685 env: Option<HashMap<String, String>>,
686 mount_outputs: Option<HashMap<String, String>>,
687}
688
689impl<'a> Parser<'a> {
690 pub fn new(spec: &'a Spec) -> Self {
692 Self {
693 spec,
694 env: None,
695 mount_outputs: None,
696 }
697 }
698
699 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
704 self.env = Some(env);
705 self
706 }
707
708 pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
715 self.mount_outputs = Some(outputs);
716 self
717 }
718
719 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
723 let out = self.parse_collecting(input)?;
724 if let Some(err) = out
725 .errors
726 .iter()
727 .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
728 {
729 bail!("{err}");
730 }
731 if !out.errors.is_empty() {
732 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
733 }
734 Ok(out)
735 }
736
737 pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
748 self.parse_collecting(input)
749 }
750
751 pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
766 let mut trace = Trace::new(input);
767 match parse_partial_traced(
768 self.spec,
769 input,
770 self.env.as_ref(),
771 self.mount_outputs.as_ref(),
772 MountTiming::WhenAWordIsUnknown,
773 &mut trace,
774 ) {
775 Ok((out, _)) => Ok(out),
778 Err(_) => Err(trace.tokens),
779 }
780 }
781
782 fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
783 let custom_env = self.env.as_ref();
784 let (mut out, overridden_flags) = parse_partial_with_env(
785 self.spec,
786 input,
787 custom_env,
788 self.mount_outputs.as_ref(),
789 MountTiming::WhenAWordIsUnknown,
790 )?;
791 restore_current_clause(&mut out);
792 trace!("{out:?}");
793
794 while try_bind_default_missing(
800 &mut out.flags,
801 &mut out.flag_awaiting_value,
802 custom_env,
803 &mut out.flag_origins,
804 )? {}
805 if let Some(flag) = out.flag_awaiting_value.first() {
806 let token = flag
807 .long
808 .first()
809 .map(|l| format!("--{l}"))
810 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
811 .unwrap_or_else(|| flag.name.clone());
812 let rendered = input.join(" ");
813 let span = rendered
814 .rfind(&token)
815 .map(|at| (at, token.len()))
816 .unwrap_or((0, 0));
817 return Err(UsageErr::InvalidFlag {
818 token,
819 reason: "requires an argument".to_string(),
820 span: span.into(),
821 input: rendered,
822 }
823 .into());
824 }
825
826 collect_deprecations(&mut out);
830
831 let get_env = |key: &str| -> Option<String> {
832 if let Some(env_map) = custom_env {
833 env_map.get(key).cloned()
834 } else {
835 std::env::var(key).ok()
836 }
837 };
838
839 for arg in active_args(&out.cmd) {
844 if out.cmd.clause.is_some() {
847 break;
848 }
849 if out.args.contains_key(arg) {
850 continue;
851 }
852 if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
853 if arg_env_is_deprecated(arg, env_name) {
854 out.warnings
855 .push(Warning::env(env_name, arg_current_env(arg)));
856 }
857 let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
858 validate_choice_values(
859 ChoiceTarget::arg(arg),
860 &values,
861 arg.choices.as_ref(),
862 custom_env,
863 )?;
864 let parsed = if arg.var {
865 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
866 ParseValue::MultiString(values)
867 } else {
868 ParseValue::String(values.into_iter().next().unwrap_or_default())
869 };
870 out.args.insert(Arc::new(arg.clone()), parsed);
871 out.arg_origins
872 .entry(Arc::new(arg.clone()))
873 .or_default()
874 .push(ValueOrigin::Env(env_name.to_string()));
875 continue;
876 }
877 if !arg.default.is_empty() {
878 if arg.var {
880 let values = split_fallback_values(&arg.default, arg.delimiter);
881 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
882 validate_choice_values(
883 ChoiceTarget::arg(arg),
884 &values,
885 arg.choices.as_ref(),
886 custom_env,
887 )?;
888 out.args
890 .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
891 out.arg_origins
892 .entry(Arc::new(arg.clone()))
893 .or_default()
894 .push(ValueOrigin::Default);
895 } else {
896 validate_choice_value(
897 ChoiceTarget::arg(arg),
898 &arg.default[0],
899 arg.choices.as_ref(),
900 custom_env,
901 )?;
902 out.args.insert(
904 Arc::new(arg.clone()),
905 ParseValue::String(arg.default[0].clone()),
906 );
907 out.arg_origins
908 .entry(Arc::new(arg.clone()))
909 .or_default()
910 .push(ValueOrigin::Default);
911 }
912 }
913 }
914
915 let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
920 for flag in &flags {
921 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
922 continue;
923 }
924 if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
925 if let Some(warning) = flag_deprecation(flag) {
929 out.warnings.push(warning);
930 }
931 if flag_env_is_deprecated(flag, env_name) {
932 out.warnings
933 .push(Warning::env(env_name, flag_current_env(flag)));
934 }
935 if let Some(arg) = flag.arg.as_ref() {
936 let values =
937 split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
938 validate_choice_values(
939 ChoiceTarget::option(flag),
940 &values,
941 arg.choices.as_ref(),
942 custom_env,
943 )?;
944 let parsed = if flag.var || arg.var {
945 if flag.var {
946 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
947 }
948 if arg.var {
949 validate_flag_arg_fallback_count(
950 flag,
951 arg,
952 values.len(),
953 &mut out.errors,
954 );
955 }
956 ParseValue::MultiString(values)
957 } else {
958 ParseValue::String(values.into_iter().next().unwrap_or_default())
959 };
960 out.flags.insert(Arc::clone(flag), parsed);
961 } else {
962 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
963 out.flags
964 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
965 }
966 out.flag_origins
967 .entry(Arc::clone(flag))
968 .or_default()
969 .push(ValueOrigin::Env(env_name.to_string()));
970 }
971 }
972 let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
978 for flag in &flags {
979 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
980 continue;
981 }
982 if let Some(condition) = flag.default_if.iter().find(|condition| {
983 default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
984 }) {
985 from_default_if.push((Arc::clone(flag), condition.clone()));
986 }
987 }
988 for (flag, condition) in &from_default_if {
989 bind_flag_fallback(
993 flag,
994 std::slice::from_ref(&condition.value),
995 &mut out,
996 custom_env,
997 ValueOrigin::DefaultIf {
998 selector: condition.selector.clone(),
999 when: condition.when.clone(),
1000 },
1001 )?;
1002 }
1003 for flag in &flags {
1004 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
1005 continue;
1006 }
1007 if !flag.default.is_empty() {
1008 bind_flag_fallback(
1009 flag,
1010 &flag.default,
1011 &mut out,
1012 custom_env,
1013 ValueOrigin::Default,
1014 )?;
1015 continue;
1016 }
1017 if let Some(arg) = flag.arg.as_ref() {
1018 if !arg.default.is_empty() {
1019 bind_flag_fallback(
1020 flag,
1021 &arg.default,
1022 &mut out,
1023 custom_env,
1024 ValueOrigin::Default,
1025 )?;
1026 }
1027 }
1028 }
1029 for (arg, parsed) in &out.args {
1034 validate_expression(
1035 &arg.name,
1036 arg.validate.as_deref(),
1037 arg.validate_error.as_deref(),
1038 parsed,
1039 &mut out.errors,
1040 );
1041 }
1042 if let Some(clause) = &out.cmd.clause {
1043 let mut clause_errors = Vec::new();
1044 for (index, instance) in out
1045 .clauses
1046 .get(&clause.name)
1047 .into_iter()
1048 .flatten()
1049 .chain(std::iter::once(&out.args))
1050 .enumerate()
1051 {
1052 for arg in &clause.args {
1053 let Some(value) = instance.get(arg) else {
1054 if arg.required {
1055 clause_errors.push(UsageErr::MissingClauseArg {
1056 clause: clause.name.clone(),
1057 instance: index + 1,
1058 arg: arg.name.clone(),
1059 });
1060 }
1061 continue;
1062 };
1063 if let (true, ParseValue::MultiString(values)) = (arg.var, value) {
1064 if let Some(min) = arg.var_min {
1065 if values.len() < min {
1066 clause_errors.push(UsageErr::VarArgTooFew {
1067 name: format!(
1068 "{} instance {}: {}",
1069 clause.name,
1070 index + 1,
1071 arg.name
1072 ),
1073 min,
1074 got: values.len(),
1075 });
1076 }
1077 }
1078 if let Some(max) = arg.var_max {
1079 if values.len() > max {
1080 clause_errors.push(UsageErr::VarArgTooMany {
1081 name: format!(
1082 "{} instance {}: {}",
1083 clause.name,
1084 index + 1,
1085 arg.name
1086 ),
1087 max,
1088 got: values.len(),
1089 });
1090 }
1091 }
1092 }
1093 }
1094 }
1095 out.errors.extend(clause_errors);
1096 }
1097 for (flag, parsed) in &out.flags {
1098 if let Some(arg) = &flag.arg {
1099 validate_expression(
1100 &flag.name,
1101 arg.validate.as_deref(),
1102 arg.validate_error.as_deref(),
1103 parsed,
1104 &mut out.errors,
1105 );
1106 }
1107 }
1108 crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1111 finalize_current_clause(&mut out);
1112 Ok(out)
1113 }
1114}
1115
1116#[must_use = "parsing result should be used"]
1123pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1124 Parser::new(spec).parse(input)
1125}
1126
1127#[must_use = "parsing result should be used"]
1131pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1132 parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
1133}
1134
1135pub fn multicall_basename(argv0: &str) -> &str {
1138 let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1139 match name.get(name.len().saturating_sub(4)..) {
1140 Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1141 _ => name,
1142 }
1143}
1144
1145pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1150 let base = multicall_basename(argv0);
1151 if !name.is_empty() && base == multicall_basename(name) {
1152 return None;
1153 }
1154 if let Some(bin) = bin {
1155 if !bin.is_empty() && base == multicall_basename(bin) {
1156 return None;
1157 }
1158 }
1159 Some(base)
1160}
1161
1162#[derive(Clone, Copy, PartialEq, Eq)]
1171enum MountTiming {
1172 Eager,
1173 WhenAWordIsUnknown,
1174}
1175
1176struct Token {
1186 word: String,
1187 argv: usize,
1193 binding: Option<(Arc<SpecFlag>, usize)>,
1205}
1206
1207impl Token {
1208 fn new(word: String, argv: usize) -> Self {
1209 Self {
1210 word,
1211 argv,
1212 binding: None,
1213 }
1214 }
1215}
1216
1217struct Trace {
1223 tokens: Vec<TokenBinding>,
1224}
1225
1226impl Trace {
1227 fn new(input: &[String]) -> Self {
1228 Self {
1229 tokens: input
1230 .iter()
1231 .enumerate()
1232 .map(|(index, word)| TokenBinding {
1233 index,
1234 word: word.clone(),
1235 synthesized: false,
1236 roles: vec![],
1237 })
1238 .collect(),
1239 }
1240 }
1241
1242 fn record(&mut self, argv: usize, role: TokenRole) {
1243 if let Some(token) = self.tokens.get_mut(argv) {
1244 token.roles.push(role);
1245 }
1246 }
1247
1248 fn note_synthesized(&mut self, argv: usize) {
1250 if let Some(token) = self.tokens.get_mut(argv) {
1251 token.synthesized = true;
1252 }
1253 }
1254
1255 fn close(&mut self, unread: &VecDeque<Token>) {
1257 for token in unread {
1258 self.record(token.argv, TokenRole::Unread);
1259 }
1260 }
1261}
1262
1263fn parse_partial_with_env(
1264 spec: &Spec,
1265 input: &[String],
1266 custom_env: Option<&HashMap<String, String>>,
1267 mount_outputs: Option<&HashMap<String, String>>,
1268 mount_timing: MountTiming,
1269) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1270 let mut trace = Trace::new(input);
1271 parse_partial_traced(
1272 spec,
1273 input,
1274 custom_env,
1275 mount_outputs,
1276 mount_timing,
1277 &mut trace,
1278 )
1279}
1280
1281fn parse_partial_traced(
1288 spec: &Spec,
1289 input: &[String],
1290 custom_env: Option<&HashMap<String, String>>,
1291 mount_outputs: Option<&HashMap<String, String>>,
1292 mount_timing: MountTiming,
1293 trace: &mut Trace,
1294) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1295 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1296 let viewed = spec.for_view(view)?;
1297 return parse_partial_traced(
1298 &viewed,
1299 input,
1300 custom_env,
1301 mount_outputs,
1302 mount_timing,
1303 trace,
1304 );
1305 }
1306 trace!("parse_partial: {input:?}");
1307 let mut input = input
1308 .iter()
1309 .enumerate()
1310 .map(|(argv, word)| Token::new(word.clone(), argv))
1311 .collect::<VecDeque<_>>();
1312 let argv0 = input.pop_front();
1313 if let Some(argv0) = argv0.as_ref() {
1314 trace.record(argv0.argv, TokenRole::Program);
1315 }
1316 if spec.multicall {
1317 if let Some(raw) = argv0 {
1318 if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1319 trace.note_synthesized(raw.argv);
1323 input.push_front(Token::new(applet.to_string(), raw.argv));
1324 }
1325 }
1326 }
1327 let mut command_has_argv = !input.is_empty();
1331
1332 let mut out = ParseOutput {
1333 cmd: spec.cmd.clone(),
1334 cmds: vec![spec.cmd.clone()],
1335 args: IndexMap::new(),
1336 clauses: IndexMap::new(),
1337 flags: IndexMap::new(),
1338 tokens: vec![],
1339 flag_origins: IndexMap::new(),
1340 arg_origins: IndexMap::new(),
1341 overridden_flags: BTreeMap::new(),
1342 available_flags: gather_flags(&spec.cmd),
1343 flag_awaiting_value: vec![],
1344 errors: vec![],
1345 warnings: vec![],
1346 next_arg: None,
1347 double_dash_seen: false,
1348 external: None,
1349 };
1350 let mut overridden_flags = HashSet::new();
1353 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1358
1359 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1372 let mut command_arg_found = false;
1375 let mut variadic_flag_active = false;
1376 let mut idx = 0;
1377 let mut used_default_subcommand = false;
1380 let mut mounts_resolved = false;
1384 let default_outranks_mounts =
1394 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1395 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1396 {
1397 mounts_resolved = true;
1398 let mut mounted = out.cmd.clone();
1399 mounted.mount(&[], mount_outputs)?;
1400 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1401 if let Some(last) = out.cmds.last_mut() {
1402 *last = mounted.clone();
1403 }
1404 out.cmd = mounted;
1405 }
1406
1407 while idx < input.len() {
1408 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1418 default_accepts_word(&out.cmd, name, &input[idx].word)
1419 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1420 });
1421 if !mounts_resolved
1422 && !out.cmd.mounts.is_empty()
1423 && !default_catches_it
1424 && is_command_word(&input[idx].word)
1425 && !is_negative_number(&input[idx].word)
1426 && out.cmd.find_subcommand(&input[idx].word).is_none()
1427 {
1428 mounts_resolved = true;
1429 let mut mounted = out.cmd.clone();
1430 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1431 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1432 if let Some(last) = out.cmds.last_mut() {
1433 *last = mounted.clone();
1434 }
1435 out.cmd = mounted;
1436 }
1437 if variadic_flag_active
1438 && out.cmd.find_subcommand(&input[idx].word).is_some()
1439 && !out.cmd.subcommand_precedence_over_arg
1440 {
1441 break;
1442 }
1443 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1444 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1445 bail!(
1446 "subcommand '{}' cannot be used with arguments on its parent command",
1447 input[idx].word
1448 );
1449 }
1450 let mut subcommand = subcommand.clone();
1451 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1453 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1456 merge_subcommand_flags(
1457 &mut out.available_flags,
1458 gather_flags(&subcommand),
1459 crossing_mount,
1460 );
1461 let selected = input.remove(idx);
1463 if let Some(selected) = selected {
1464 trace.record(
1465 selected.argv,
1466 TokenRole::Command {
1467 name: subcommand.name.clone(),
1468 },
1469 );
1470 }
1471 command_has_argv = idx < input.len();
1472 out.cmds.push(subcommand.clone());
1473 out.cmd = subcommand.clone();
1474 mounts_resolved = true;
1476 prefix_flags.clear();
1477 command_arg_found = false;
1478 variadic_flag_active = false;
1479 } else if !is_command_word(&input[idx].word)
1482 || declared_numeric_short(&out.available_flags, &input[idx].word)
1483 {
1484 let word = input[idx].word.clone();
1486 let flag_key = get_flag_key(&word);
1487
1488 let is_bundle = word.starts_with("--")
1494 || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1495 if let Some(f) = out
1496 .available_flags
1497 .get(flag_key)
1498 .cloned()
1499 .filter(|_| is_bundle)
1500 {
1501 command_arg_found = true;
1502 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1503 input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1511 let mut forwarded = f.global.then(|| vec![word.clone()]);
1512 idx += 1;
1513
1514 if f.arg.is_some()
1517 && !word.contains('=')
1518 && idx < input.len()
1519 && accepts_detached_flag_value(&f, &input[idx].word)
1520 {
1521 if let Some(words) = forwarded.as_mut() {
1522 words.push(input[idx].word.clone());
1523 }
1524 idx += 1;
1525 }
1526 if let Some(words) = forwarded {
1527 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1528 prefix_flags.push((f, words));
1529 }
1530 } else {
1531 break;
1534 }
1535 } else {
1536 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1537 idx += 1;
1538 continue;
1539 }
1540 if !used_default_subcommand && out.cmds.len() == 1 {
1549 if let Some(default_name) = &spec.default_subcommand {
1550 if let Some(subcommand) = out
1551 .cmd
1552 .find_subcommand(default_name)
1553 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1554 {
1555 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1556 bail!(
1557 "subcommand '{}' cannot be used with arguments on its parent command",
1558 subcommand.name
1559 );
1560 }
1561 let mut subcommand = subcommand.clone();
1562 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1564 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1565 merge_subcommand_flags(
1566 &mut out.available_flags,
1567 gather_flags(&subcommand),
1568 crossing_mount,
1569 );
1570 out.cmds.push(subcommand.clone());
1571 out.cmd = subcommand.clone();
1572 command_has_argv = true;
1573 prefix_flags.clear();
1574 command_arg_found = false;
1575 variadic_flag_active = false;
1576 mounts_resolved = true;
1579 used_default_subcommand = true;
1580 continue;
1585 }
1586 }
1587 }
1588 if match_sigil_arg_chain(&out.cmds, &input[idx].word).is_some() {
1593 idx += 1;
1594 continue;
1595 }
1596 if out.cmd.external_subcommand {
1601 let rest: Vec<Token> = input.drain(idx..).collect();
1602 for token in &rest {
1603 trace.record(token.argv, TokenRole::External);
1604 }
1605 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1606 break;
1607 }
1608 break;
1610 }
1611 }
1612
1613 let mut next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1623 let mut enable_flags = true;
1624 let mut grouped_flag = false;
1625 let mut seen_double_dash = false;
1629 let mut restart_seen = false;
1632 let mut double_dash_violations: HashSet<String> = HashSet::new();
1635 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1640
1641 while !input.is_empty() {
1642 let token = input.pop_front().unwrap();
1643 let binding = token.binding;
1645 let argv = token.argv;
1646 let mut w = token.word;
1647 let attached_continuation = grouped_flag;
1652
1653 if !seen_double_dash {
1656 if let Some(clause) = out.cmd.clause.as_ref() {
1657 if w == clause.separator {
1658 let name = clause.name.clone();
1659 out.clauses
1660 .entry(name.clone())
1661 .or_default()
1662 .push(std::mem::take(&mut out.args));
1663 out.arg_origins.clear();
1664 trace.record(argv, TokenRole::ClauseSeparator { name });
1665 next_arg_idx = 0;
1666 out.flag_awaiting_value.clear();
1667 enable_flags = true;
1668 seen_double_dash = false;
1669 continue;
1670 }
1671 }
1672 }
1673
1674 if let Some(ref restart_token) = out.cmd.restart_token {
1677 if w == *restart_token {
1678 out.args.clear();
1683 out.arg_origins.clear();
1688 trace.record(argv, TokenRole::Restart);
1689 next_arg_idx = cursor_skip_sigils(&out.cmd, 0);
1690 restart_seen = true;
1691 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
1695 }
1696 }
1697
1698 if enable_flags
1707 && !attached_continuation
1708 && w.starts_with('-')
1709 && out
1710 .flag_awaiting_value
1711 .last()
1712 .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1713 {
1714 let should_return = bind_pending_flag_value(
1717 spec,
1718 &out.cmd,
1719 &mut out.errors,
1720 &mut out.flags,
1721 &mut out.flag_awaiting_value,
1722 &mut w,
1723 &mut input,
1724 custom_env,
1725 trace,
1726 argv,
1727 false,
1729 )?;
1730 if should_return {
1731 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1732 return Ok((out, overridden_flags));
1733 }
1734 continue;
1735 }
1736
1737 if enable_flags
1743 && !attached_continuation
1744 && !out.flag_awaiting_value.is_empty()
1745 && out.flag_awaiting_value.last().is_some_and(|flag| {
1746 (flag.default_missing.is_some() || flag.value_optional)
1747 && !accepts_detached_flag_value(flag, &w)
1748 })
1749 {
1750 try_bind_default_missing(
1751 &mut out.flags,
1752 &mut out.flag_awaiting_value,
1753 custom_env,
1754 &mut out.flag_origins,
1755 )?;
1756 }
1757
1758 if w == "--" && !seen_double_dash {
1764 enable_flags = false;
1765
1766 let should_preserve = active_args(&out.cmd)
1769 .get(next_arg_idx)
1770 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1771 .unwrap_or(false);
1772
1773 if should_preserve {
1774 } else {
1777 seen_double_dash = true;
1778 trace.record(argv, TokenRole::Separator);
1779
1780 let target = active_args(&out.cmd).iter().position(|arg| {
1786 arg.double_dash == SpecDoubleDashChoices::Required
1787 && !out.args.contains_key(arg)
1788 });
1789 if let Some(target) = target {
1790 if target > next_arg_idx {
1797 next_arg_idx = target;
1798 }
1799 }
1800 continue;
1801 }
1802 }
1803
1804 if enable_flags && w.starts_with("--") {
1806 grouped_flag = false;
1807 let split = w.split_once('=');
1811 let word = split.map(|(word, _)| word).unwrap_or(&w);
1812 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1813 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1814 let command_level = binding
1815 .as_ref()
1816 .map(|(_, level)| *level)
1817 .unwrap_or(out.cmds.len() - 1);
1818 parsed_flag_spellings
1819 .entry(Arc::as_ptr(f) as usize)
1820 .or_default()
1821 .insert(word.to_string());
1822 trace.record(
1825 argv,
1826 TokenRole::Flag {
1827 flag: Arc::clone(f),
1828 spelling: word.to_string(),
1829 negated: f.negate.as_deref() == Some(word),
1830 },
1831 );
1832 if f.action != crate::SpecFlagAction::Set {
1833 out.errors.push(render_action_err(spec, &out.cmd, f, word));
1834 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1835 return Ok((out, overridden_flags));
1836 }
1837 apply_flag_overrides(
1838 f,
1839 &out.available_flags,
1840 &mut out.flags,
1841 &mut out.flag_awaiting_value,
1842 &mut overridden_flags,
1843 &mut out.overridden_flags,
1844 );
1845 if let Some(pending) = out.flag_awaiting_value.first() {
1846 out.errors.push(render_missing_flag_value(pending, &w));
1847 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1848 return Ok((out, overridden_flags));
1849 }
1850 if f.arg.is_some() {
1856 record_scalar_flag_occurrence(
1857 &out.cmds,
1858 f,
1859 command_level,
1860 None,
1861 &mut scalar_occurrences,
1862 &mut out.errors,
1863 );
1864 let f = Arc::clone(f);
1865 out.flag_awaiting_value.push(Arc::clone(&f));
1866 if let Some((_, val)) = split {
1871 let mut val = val.to_string();
1875 let should_return = bind_pending_flag_value(
1876 spec,
1877 &out.cmd,
1878 &mut out.errors,
1879 &mut out.flags,
1880 &mut out.flag_awaiting_value,
1881 &mut val,
1882 &mut input,
1883 custom_env,
1884 trace,
1885 argv,
1886 true,
1889 )?;
1890 if should_return {
1891 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1892 return Ok((out, overridden_flags));
1893 }
1894 }
1895 } else if f.count {
1896 let arr = out
1897 .flags
1898 .entry(Arc::clone(f))
1899 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1900 .try_as_multi_bool_mut()
1901 .unwrap();
1902 arr.push(true);
1903 } else {
1904 let negate = f.negate.clone().unwrap_or_default();
1905 let negated_form = word == negate;
1906 let value = if f.bool_value {
1907 match split.map(|(_, value)| value) {
1908 Some("true") => !negated_form,
1909 Some("false") => negated_form,
1910 Some(value) => {
1911 out.errors.push(UsageErr::InvalidValue {
1912 name: f.name.clone(),
1913 value: value.to_string(),
1914 reason: "expected `true` or `false`".to_string(),
1915 });
1916 continue;
1917 }
1918 None => !negated_form,
1919 }
1920 } else {
1921 !negated_form
1922 };
1923 record_scalar_flag_occurrence(
1928 &out.cmds,
1929 f,
1930 command_level,
1931 Some(!negated_form),
1932 &mut scalar_occurrences,
1933 &mut out.errors,
1934 );
1935 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1936 }
1937 continue;
1938 }
1939 if is_help_arg(spec, &out.cmd, &w) {
1940 out.errors
1941 .push(render_help_err(spec, &out.cmd, w.len() > 2));
1942 trace.record(
1943 argv,
1944 TokenRole::Builtin {
1945 spelling: w.clone(),
1946 },
1947 );
1948 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1949 return Ok((out, overridden_flags));
1950 }
1951 if is_version_arg(spec, &out.cmds, &w) {
1952 out.errors.push(render_version_err(spec, w.len() > 2));
1953 trace.record(
1954 argv,
1955 TokenRole::Builtin {
1956 spelling: w.clone(),
1957 },
1958 );
1959 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1960 return Ok((out, overridden_flags));
1961 }
1962 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1963 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1964 trace.close(&input);
1965 return Err(refused.into());
1966 }
1967 }
1968
1969 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1976 let positional_negative_number = !declared_numeric_short
1977 && is_negative_number(&w)
1978 && active_args(&out.cmd)
1979 .get(next_arg_idx)
1980 .is_some_and(|arg| arg.allow_negative_numbers);
1981 if enable_flags
1982 && !grouped_flag
1983 && binding.is_none()
1986 && w.starts_with('-')
1987 && w.len() > 1
1988 && is_flag_like(&w)
1989 && !positional_negative_number
1990 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1991 {
1992 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1995 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1996 trace.close(&input);
1997 return Err(refused.into());
1998 }
1999 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
2000 let short = w.chars().nth(1).unwrap();
2001 if let Some(f) = binding
2002 .as_ref()
2003 .map(|(flag, _)| flag)
2004 .or_else(|| out.available_flags.get(&format!("-{short}")))
2005 {
2006 let command_level = binding
2007 .as_ref()
2008 .map(|(_, level)| *level)
2009 .unwrap_or(out.cmds.len() - 1);
2010 if f.action != crate::SpecFlagAction::Set {
2011 out.errors
2012 .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
2013 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2014 return Ok((out, overridden_flags));
2015 }
2016 parsed_flag_spellings
2017 .entry(Arc::as_ptr(f) as usize)
2018 .or_default()
2019 .insert(format!("-{short}"));
2020 trace.record(
2021 argv,
2022 TokenRole::Flag {
2023 flag: Arc::clone(f),
2024 spelling: format!("-{short}"),
2025 negated: false,
2027 },
2028 );
2029 apply_flag_overrides(
2030 f,
2031 &out.available_flags,
2032 &mut out.flags,
2033 &mut out.flag_awaiting_value,
2034 &mut overridden_flags,
2035 &mut out.overridden_flags,
2036 );
2037 if !attached_continuation {
2038 if let Some(pending) = out.flag_awaiting_value.first() {
2039 out.errors.push(render_missing_flag_value(pending, &w));
2040 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2041 return Ok((out, overridden_flags));
2042 }
2043 }
2044 let rest = &w[1 + short.len_utf8()..];
2045 if !rest.is_empty() {
2046 input.push_front(Token::new(format!("-{rest}"), argv));
2049 }
2050 grouped_flag = !rest.is_empty();
2054 if f.arg.is_some() {
2055 record_scalar_flag_occurrence(
2056 &out.cmds,
2057 f,
2058 command_level,
2059 None,
2060 &mut scalar_occurrences,
2061 &mut out.errors,
2062 );
2063 out.flag_awaiting_value.push(Arc::clone(f));
2064 } else if f.count {
2065 let arr = out
2066 .flags
2067 .entry(Arc::clone(f))
2068 .or_insert_with(|| ParseValue::MultiBool(vec![]))
2069 .try_as_multi_bool_mut()
2070 .unwrap();
2071 arr.push(true);
2072 } else {
2073 let negate = f.negate.clone().unwrap_or_default();
2074 let value = w != negate;
2075 record_scalar_flag_occurrence(
2076 &out.cmds,
2077 f,
2078 command_level,
2079 Some(value),
2080 &mut scalar_occurrences,
2081 &mut out.errors,
2082 );
2083 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
2084 }
2085 continue;
2086 }
2087 if let Some(err) = supplied_short(spec, &out.cmds, short) {
2091 out.errors.push(err);
2092 trace.record(
2093 argv,
2094 TokenRole::Builtin {
2095 spelling: format!("-{short}"),
2096 },
2097 );
2098 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2099 return Ok((out, overridden_flags));
2100 }
2101 if is_help_arg(spec, &out.cmd, &w) {
2102 out.errors
2103 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2104 trace.record(
2105 argv,
2106 TokenRole::Builtin {
2107 spelling: w.clone(),
2108 },
2109 );
2110 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2111 return Ok((out, overridden_flags));
2112 }
2113 if is_version_arg(spec, &out.cmds, &w) {
2114 out.errors.push(render_version_err(spec, w.len() > 2));
2115 trace.record(
2116 argv,
2117 TokenRole::Builtin {
2118 spelling: w.clone(),
2119 },
2120 );
2121 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2122 return Ok((out, overridden_flags));
2123 }
2124 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2125 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2126 trace.close(&input);
2127 return Err(refused.into());
2128 }
2129 if grouped_flag {
2130 grouped_flag = false;
2131 w.remove(0);
2132 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2136 w.remove(0);
2137 }
2138 }
2139 }
2140
2141 if enable_flags
2152 && !attached_continuation
2153 && !out.flag_awaiting_value.is_empty()
2154 && out
2155 .flag_awaiting_value
2156 .last()
2157 .is_some_and(|flag| flag.require_equals)
2158 {
2159 let flag = out.flag_awaiting_value.last().unwrap();
2160 let token = flag
2161 .long
2162 .first()
2163 .map(|l| format!("--{l}"))
2164 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2165 .unwrap_or_else(|| flag.name.clone());
2166 out.errors.push(UsageErr::InvalidFlag {
2167 token: token.clone(),
2168 reason: "requires an argument".to_string(),
2169 span: (0, 0).into(),
2170 input: format!("{token} {w}"),
2171 });
2172 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2173 return Ok((out, overridden_flags));
2174 }
2175 if enable_flags
2176 && !out.flag_awaiting_value.is_empty()
2177 && (attached_continuation
2178 || out
2179 .flag_awaiting_value
2180 .last()
2181 .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2182 {
2183 let should_return = bind_pending_flag_value(
2186 spec,
2187 &out.cmd,
2188 &mut out.errors,
2189 &mut out.flags,
2190 &mut out.flag_awaiting_value,
2191 &mut w,
2192 &mut input,
2193 custom_env,
2194 trace,
2195 argv,
2196 attached_continuation,
2197 )?;
2198 if should_return {
2199 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2200 return Ok((out, overridden_flags));
2201 }
2202 continue;
2203 }
2204
2205 if let Some((arg, sigil, value)) = (enable_flags && !restart_seen)
2206 .then(|| match_sigil_arg_chain(&out.cmds, &w))
2207 .flatten()
2208 {
2209 if value.is_empty() {
2210 out.errors.push(UsageErr::InvalidValue {
2211 name: arg.name.clone(),
2212 value: w.clone(),
2213 reason: format!("expected a value after sigil {sigil:?}"),
2214 });
2215 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2216 return Ok((out, overridden_flags));
2217 }
2218 let trailing_value = arg.double_dash == SpecDoubleDashChoices::Automatic;
2219 let suppress_trailing_delimiter =
2220 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2221 let delimiter = if suppress_trailing_delimiter && trailing_value {
2222 None
2223 } else {
2224 arg.delimiter
2225 };
2226 let parts = match delimiter {
2227 Some(delimiter) => value
2228 .split(delimiter)
2229 .map(str::to_string)
2230 .collect::<Vec<_>>(),
2231 None => vec![value.to_string()],
2232 };
2233 let mut refused = false;
2234 for part in &parts {
2235 if validate_choices(
2236 spec,
2237 &out.cmd,
2238 &mut out.errors,
2239 ChoiceTarget::arg(arg),
2240 part,
2241 arg.choices.as_ref(),
2242 custom_env,
2243 )? {
2244 refused = true;
2245 break;
2246 }
2247 }
2248 if refused {
2249 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2250 return Ok((out, overridden_flags));
2251 }
2252 trace.record(
2253 argv,
2254 TokenRole::Sigil {
2255 arg: Arc::new(arg.clone()),
2256 sigil: sigil.to_string(),
2257 values: parts.clone(),
2258 },
2259 );
2260 let key = Arc::new(arg.clone());
2261 if arg.var {
2262 let arr = out
2263 .args
2264 .entry(key)
2265 .or_insert_with(|| ParseValue::MultiString(vec![]))
2266 .try_as_multi_string_mut()
2267 .unwrap();
2268 arr.extend(parts);
2269 } else {
2270 out.args.insert(key, ParseValue::String(value.to_string()));
2271 }
2272 continue;
2273 }
2274
2275 if out.cmd.allow_missing_positional {
2276 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx);
2277 while let Some(current) = active_args(&out.cmd).get(next_arg_idx) {
2278 if current.required || out.args.contains_key(current) {
2279 break;
2280 }
2281 let required_after = active_args(&out.cmd)[next_arg_idx + 1..]
2282 .iter()
2283 .filter(|arg| arg.required && arg.sigil.is_none())
2284 .count();
2285 if required_after == 0 {
2286 break;
2287 }
2288 let remaining_values = 1 + input
2289 .iter()
2290 .filter(|token| {
2291 (!enable_flags || !is_flag_like(&token.word))
2292 && (!enable_flags
2293 || restart_seen
2294 || match_sigil_arg_chain(&out.cmds, &token.word).is_none())
2295 })
2296 .count();
2297 if remaining_values > required_after {
2298 break;
2299 }
2300 next_arg_idx = cursor_skip_sigils(&out.cmd, next_arg_idx + 1);
2301 }
2302 }
2303
2304 if let Some(arg) = active_args(&out.cmd).get(next_arg_idx) {
2305 if arg.var
2306 && out.args.contains_key(arg)
2307 && arg.value_terminator.as_deref() == Some(w.as_str())
2308 {
2309 trace.record(
2310 argv,
2311 TokenRole::ValueTerminator {
2312 ends: arg.name.clone(),
2313 },
2314 );
2315 next_arg_idx += 1;
2316 continue;
2317 }
2318 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2322 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2323 trace.record(
2324 argv,
2325 TokenRole::Refused {
2326 reason: format!("{} only accepts words after `--`", arg.name),
2327 },
2328 );
2329 continue;
2333 }
2334 let trailing_value =
2339 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2340 let suppress_trailing_delimiter =
2341 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2342 let delimiter = if suppress_trailing_delimiter && trailing_value {
2343 None
2344 } else {
2345 arg.delimiter
2346 };
2347 let parts: Vec<String> = match delimiter {
2348 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2349 None => vec![w.clone()],
2350 };
2351 let mut refused = false;
2352 for part in &parts {
2353 if validate_choices(
2354 spec,
2355 &out.cmd,
2356 &mut out.errors,
2357 ChoiceTarget::arg(arg),
2358 part,
2359 arg.choices.as_ref(),
2360 custom_env,
2361 )? {
2362 refused = true;
2363 break;
2364 }
2365 }
2366 if refused {
2367 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2368 return Ok((out, overridden_flags));
2369 }
2370 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2375 enable_flags = false;
2376 }
2377 let unknown_flag = enable_flags
2382 && !positional_negative_number
2383 && is_flag_like(&w)
2384 && binding.is_none();
2385 trace.record(
2386 argv,
2387 if unknown_flag {
2388 TokenRole::UnknownFlag {
2389 bound_as: Some(Arc::new(arg.clone())),
2390 }
2391 } else {
2392 TokenRole::Arg {
2393 arg: Arc::new(arg.clone()),
2394 values: parts.clone(),
2395 }
2396 },
2397 );
2398 if arg.var {
2399 let arr = out
2400 .args
2401 .entry(Arc::new(arg.clone()))
2402 .or_insert_with(|| ParseValue::MultiString(vec![]))
2403 .try_as_multi_string_mut()
2404 .unwrap();
2405 arr.extend(parts.iter().cloned());
2409 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2410 next_arg_idx += 1;
2411 }
2412 } else {
2413 out.args
2414 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2415 next_arg_idx += 1;
2416 }
2417 continue;
2418 }
2419 if is_help_arg(spec, &out.cmd, &w) {
2420 out.errors
2421 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2422 trace.record(
2423 argv,
2424 TokenRole::Builtin {
2425 spelling: w.clone(),
2426 },
2427 );
2428 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2429 return Ok((out, overridden_flags));
2430 }
2431 if is_version_arg(spec, &out.cmds, &w) {
2432 out.errors.push(render_version_err(spec, w.len() > 2));
2433 trace.record(
2434 argv,
2435 TokenRole::Builtin {
2436 spelling: w.clone(),
2437 },
2438 );
2439 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2440 return Ok((out, overridden_flags));
2441 }
2442 trace.record(
2443 argv,
2444 TokenRole::Refused {
2445 reason: "no declaration takes this word".to_string(),
2446 },
2447 );
2448 trace.close(&input);
2449 bail!("unexpected word: {w}");
2450 }
2451
2452 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2453 validate_clause_relationships(&mut out, &overridden_flags, custom_env);
2454
2455 let flag_was_parsed =
2460 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2461
2462 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2470 let declared: HashSet<String> = out
2471 .cmd
2472 .flags
2473 .iter()
2474 .filter(|declared| declared.name == flag.name)
2475 .flat_map(flag_keys)
2476 .collect();
2477 let speaks_for_this_flag = declared.iter().any(|spelling| {
2487 out.available_flags
2488 .get(spelling)
2489 .is_some_and(|available| Arc::ptr_eq(available, flag))
2490 });
2491 if speaks_for_this_flag {
2492 declared
2493 } else {
2494 HashSet::new()
2495 }
2496 };
2497
2498 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2506 let child = child_spellings(flag);
2507 let child_exclusive = !child.is_empty()
2508 && out
2509 .cmd
2510 .flags
2511 .iter()
2512 .any(|declared| declared.name == flag.name && declared.exclusive);
2513 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2514 Some(spellings) => (
2515 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2516 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2517 ),
2518 None => (child_exclusive, flag.exclusive && child.is_empty()),
2522 }
2523 };
2524
2525 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2526 let (child, ancestor) = exclusivity_in_play(flag);
2527 child || ancestor
2528 };
2529
2530 let exclusive_present =
2535 unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2536 exclusive_occurrence(flag)
2537 && !overridden_flags.contains(&flag.name)
2538 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2539 });
2540 let requirements_apply = |command_index: usize| {
2541 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2542 };
2543
2544 if out.cmd.arg_required_else_help && !command_has_argv {
2545 out.errors.push(render_help_err(spec, &out.cmd, false));
2546 }
2547
2548 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2554 let mut names: Vec<&str> = out
2555 .cmd
2556 .subcommands
2557 .iter()
2558 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2561 .map(|(name, _)| name.as_str())
2562 .collect();
2563 names.sort_unstable();
2564 out.errors.push(UsageErr::MissingSubcommand(
2565 out.cmd.name.clone(),
2566 names.join(", "),
2567 ));
2568 }
2569
2570 if !exclusive_present {
2573 for arg in out
2574 .cmds
2575 .iter()
2576 .enumerate()
2577 .filter(|(index, _)| requirements_apply(*index))
2578 .flat_map(|(_, cmd)| &cmd.args)
2579 {
2580 if out.args.contains_key(arg) {
2581 continue;
2582 }
2583 if double_dash_violations.contains(&arg.name) {
2585 continue;
2586 }
2587 let required_if = arg.required_if.iter().any(|selector| {
2588 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2589 });
2590 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2591 selector_explicit_has_value(
2592 &condition.selector,
2593 &condition.value,
2594 &out,
2595 &overridden_flags,
2596 custom_env,
2597 )
2598 });
2599 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2600 && arg.required_if_eq_all.iter().all(|condition| {
2601 selector_explicit_has_value(
2602 &condition.selector,
2603 &condition.value,
2604 &out,
2605 &overridden_flags,
2606 custom_env,
2607 )
2608 });
2609 let unless_any = arg.required_unless.iter().any(|selector| {
2610 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2611 });
2612 let unless_all = !arg.required_unless_all.is_empty()
2613 && arg.required_unless_all.iter().all(|selector| {
2614 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2615 });
2616 let required_unless = !(unless_any
2617 || unless_all
2618 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2619 if (arg.required
2620 || required_if
2621 || required_if_eq
2622 || required_if_eq_all
2623 || required_unless)
2624 && arg.default.is_empty()
2625 {
2626 let has_env = arg
2628 .env
2629 .as_ref()
2630 .is_some_and(|env_var| env_contains(custom_env, env_var));
2631 if !has_env {
2632 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2633 }
2634 }
2635 }
2636 }
2637
2638 for flag in unique_flags(out.available_flags.values()) {
2650 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2651 if !given || overridden_flags.contains(&flag.name) {
2652 continue;
2653 }
2654 for other in &flag.conflicts {
2655 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2656 out.errors.push(UsageErr::InvalidFlag {
2657 token: format!("--{}", flag.name),
2658 reason: format!("conflicts with {other}"),
2659 span: (0, 0).into(),
2660 input: format!("--{} {other}", flag.name),
2661 });
2662 }
2663 }
2664 let owner = out
2675 .cmds
2676 .iter()
2677 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2678 .unwrap_or(out.cmds.len() - 1);
2679 if !exclusive_present && requirements_apply(owner) {
2680 for other in &flag.requires {
2681 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2682 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2683 if other.starts_with('-') {
2684 out.errors.push(UsageErr::MissingFlag(name));
2685 } else {
2686 out.errors.push(UsageErr::MissingArg(name));
2687 }
2688 }
2689 }
2690 for condition in &flag.requires_if {
2691 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2692 && !selector_is_satisfied(
2693 &condition.requires,
2694 &out,
2695 &overridden_flags,
2696 custom_env,
2697 )
2698 {
2699 let name = selector_flag_name(&condition.requires, &out)
2700 .unwrap_or_else(|| condition.requires.clone());
2701 out.errors.push(UsageErr::MissingFlag(name));
2702 }
2703 }
2704 }
2705 }
2706
2707 for (command_index, arg) in out
2710 .cmds
2711 .iter()
2712 .enumerate()
2713 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2714 {
2715 let given = arg_is_explicit(arg, &out, custom_env);
2716 if !given {
2717 continue;
2718 }
2719 for other in &arg.conflicts {
2720 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2721 out.errors.push(UsageErr::InvalidFlag {
2722 token: arg.name.clone(),
2723 reason: format!("conflicts with {other}"),
2724 span: (0, 0).into(),
2725 input: format!("{} {other}", arg.name),
2726 });
2727 }
2728 }
2729 if !exclusive_present && requirements_apply(command_index) {
2730 for other in &arg.requires {
2731 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2732 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2733 if other.starts_with('-') {
2734 out.errors.push(UsageErr::MissingFlag(name));
2735 } else {
2736 out.errors.push(UsageErr::MissingArg(name));
2737 }
2738 }
2739 }
2740 }
2741 }
2742
2743 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2753 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2758 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2759 continue;
2760 }
2761 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2762 .find(|other| {
2763 !Arc::ptr_eq(other, flag)
2764 && !overridden_flags.contains(&other.name)
2765 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2766 })
2767 .map(|other| format!("--{}", other.name));
2768 let other_arg = active_args(&out.cmd).iter().find(|arg| {
2769 out.args.keys().any(|given| given.name == arg.name)
2770 || out
2771 .clauses
2772 .values()
2773 .flatten()
2774 .any(|instance| instance.keys().any(|given| given.name == arg.name))
2775 || arg
2776 .env
2777 .as_ref()
2778 .is_some_and(|env| env_contains(custom_env, env))
2779 });
2780 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2786 let selected_subcommand =
2787 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2788 let other = other_flag
2789 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2790 .or(selected_subcommand);
2791 if let Some(other) = other {
2792 out.errors.push(UsageErr::InvalidFlag {
2793 token: format!("--{}", flag.name),
2794 reason: format!("must be given on its own, and {other} was given too"),
2795 span: (0, 0).into(),
2796 input: format!("--{} {other}", flag.name),
2797 });
2798 }
2799 }
2800
2801 let mut group_errors: Vec<UsageErr> = Vec::new();
2810 for (command_index, group) in out
2811 .cmds
2812 .iter()
2813 .enumerate()
2814 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2815 {
2816 let mut given: Vec<&str> = Vec::new();
2822 let mut seen: Vec<String> = Vec::new();
2823 for selector in &group.members {
2824 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2825 continue;
2826 }
2827 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2828 if seen.contains(&name) {
2829 continue;
2830 }
2831 seen.push(name);
2832 given.push(selector.as_str());
2833 }
2834 if !group.multiple && given.len() > 1 {
2835 group_errors.push(UsageErr::InvalidFlag {
2836 token: given[1].to_string(),
2837 reason: format!("cannot be used with {} in group {}", given[0], group.name),
2838 span: (0, 0).into(),
2839 input: format!("{} {}", given[0], given[1]),
2840 });
2841 }
2842 let satisfied = group
2847 .members
2848 .iter()
2849 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2850 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2851 group_errors.push(UsageErr::MissingGroup {
2855 group: group.name.clone(),
2856 members: group.members.join(", "),
2857 });
2858 }
2859 }
2860 out.errors.extend(group_errors);
2861
2862 if !exclusive_present {
2863 for flag in unique_flags(out.available_flags.values()) {
2864 let owner = out
2865 .cmds
2866 .iter()
2867 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2868 .unwrap_or(out.cmds.len() - 1);
2869 if !requirements_apply(owner) {
2870 continue;
2871 }
2872 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2873 continue;
2874 }
2875 let has_default =
2876 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2877 let has_env = flag_has_env(flag, custom_env);
2878 let required_if = flag.required_if.iter().any(|selector| {
2879 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2880 });
2881 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2882 selector_explicit_has_value(
2883 &condition.selector,
2884 &condition.value,
2885 &out,
2886 &overridden_flags,
2887 custom_env,
2888 )
2889 });
2890 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2891 && flag.required_if_eq_all.iter().all(|condition| {
2892 selector_explicit_has_value(
2893 &condition.selector,
2894 &condition.value,
2895 &out,
2896 &overridden_flags,
2897 custom_env,
2898 )
2899 });
2900 let unless_any = flag.required_unless.iter().any(|selector| {
2901 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2902 });
2903 let unless_all = !flag.required_unless_all.is_empty()
2904 && flag.required_unless_all.iter().all(|selector| {
2905 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2906 });
2907 let required_unless = !(unless_any
2908 || unless_all
2909 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
2910 if (flag.required
2911 || required_if
2912 || required_if_eq
2913 || required_if_eq_all
2914 || required_unless)
2915 && !has_default
2916 && !has_env
2917 {
2918 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2919 }
2920 }
2921 }
2922
2923 for (arg, value) in &out.args {
2925 if arg.var {
2926 if let ParseValue::MultiString(values) = value {
2927 if let Some(min) = arg.var_min {
2928 if values.len() < min {
2929 out.errors.push(UsageErr::VarArgTooFew {
2930 name: arg.name.clone(),
2931 min,
2932 got: values.len(),
2933 });
2934 }
2935 }
2936 if let Some(max) = arg.var_max {
2937 if values.len() > max {
2938 out.errors.push(UsageErr::VarArgTooMany {
2939 name: arg.name.clone(),
2940 max,
2941 got: values.len(),
2942 });
2943 }
2944 }
2945 }
2946 }
2947 }
2948
2949 for flag in unique_flags(out.available_flags.values()) {
2953 if flag.var {
2954 let bound = match out.flags.get(flag) {
2955 Some(ParseValue::MultiString(values)) => values.len(),
2956 Some(ParseValue::MultiBool(values)) => values.len(),
2957 Some(_) => 1,
2958 None => 0,
2959 };
2960 let pending = out
2964 .flag_awaiting_value
2965 .iter()
2966 .filter(|pending| {
2967 Arc::ptr_eq(pending, flag)
2968 && (pending.value_optional || pending.default_missing.is_some())
2969 })
2970 .count();
2971 let count = bound + pending;
2972 if count == 0 {
2973 continue;
2974 }
2975 if let Some(min) = flag.var_min {
2976 if count < min {
2977 out.errors.push(UsageErr::VarFlagTooFew {
2978 name: flag.name.clone(),
2979 min,
2980 got: count,
2981 });
2982 }
2983 }
2984 if let Some(max) = flag.var_max {
2985 if count > max {
2986 out.errors.push(UsageErr::VarFlagTooMany {
2987 name: flag.name.clone(),
2988 max,
2989 got: count,
2990 });
2991 }
2992 }
2993 }
2994 }
2995
2996 Ok((out, overridden_flags))
2997}
2998
2999fn validate_expression(
3000 name: &str,
3001 expression: Option<&str>,
3002 message: Option<&str>,
3003 parsed: &ParseValue,
3004 errors: &mut Vec<UsageErr>,
3005) {
3006 let Some(expression) = expression else {
3007 return;
3008 };
3009 #[cfg(not(feature = "validation"))]
3010 let _ = expression;
3011 let values: &[String] = match parsed {
3012 ParseValue::String(value) => std::slice::from_ref(value),
3013 ParseValue::MultiString(values) => values,
3014 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
3015 };
3016 #[cfg(feature = "validation")]
3017 for value in values {
3018 let reason = match usage_validation::validate(expression, value) {
3019 Ok(true) => continue,
3020 Ok(false) => message
3021 .unwrap_or("does not satisfy the validation expression")
3022 .to_string(),
3023 Err(error) => format!("validation expression failed: {error}"),
3024 };
3025 errors.push(UsageErr::InvalidValue {
3026 name: name.to_string(),
3027 value: value.clone(),
3028 reason,
3029 });
3030 break;
3031 }
3032 #[cfg(not(feature = "validation"))]
3033 if let Some(value) = values.first() {
3034 let _ = message;
3035 errors.push(UsageErr::InvalidValue {
3036 name: name.to_string(),
3037 value: value.clone(),
3038 reason: "expression validation requires the `validation` feature".to_string(),
3039 });
3040 }
3041}
3042
3043#[cfg(all(test, not(feature = "validation")))]
3044mod optional_validation_tests {
3045 use crate::{parse, Spec};
3046
3047 #[test]
3048 fn validation_declarations_require_the_opt_in_runtime_feature() {
3049 let spec: Spec = r#"
3050name "ex"
3051bin "ex"
3052arg "<port>" validate="int(value) > 0"
3053 "#
3054 .parse()
3055 .unwrap();
3056 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
3057 assert!(
3058 error
3059 .to_string()
3060 .contains("requires the `validation` feature"),
3061 "{error:?}"
3062 );
3063 }
3064}
3065
3066fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
3067 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
3068}
3069
3070fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
3071 overrider
3072 .overrides
3073 .iter()
3074 .any(|selector| flag_matches_selector(overridden, selector))
3075}
3076
3077fn apply_prefix_flag_overrides(
3078 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3079 flag: Arc<SpecFlag>,
3080) {
3081 prefix_flags
3082 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
3083}
3084
3085fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
3086 prefix_flags
3087 .iter()
3088 .flat_map(|(_, words)| words.iter().cloned())
3089 .collect()
3090}
3091
3092fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
3093 match custom_env {
3094 Some(env) => env.contains_key(env_var),
3095 None => std::env::var(env_var).is_ok(),
3096 }
3097}
3098
3099fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
3100 flag.env_names()
3101 .any(|env_var| env_contains(custom_env, env_var))
3102}
3103
3104fn fallback_is_true(value: &str) -> bool {
3105 matches!(value, "1" | "true" | "True" | "TRUE")
3106}
3107
3108fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
3109 match delimiter {
3110 Some(delimiter) => values
3111 .iter()
3112 .flat_map(|value| value.split(delimiter).map(str::to_string))
3113 .collect(),
3114 None => values.to_vec(),
3115 }
3116}
3117
3118fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
3119 if let Some(min) = arg.var_min {
3120 if count < min {
3121 errors.push(UsageErr::VarArgTooFew {
3122 name: arg.name.clone(),
3123 min,
3124 got: count,
3125 });
3126 }
3127 }
3128 if let Some(max) = arg.var_max {
3129 if count > max {
3130 errors.push(UsageErr::VarArgTooMany {
3131 name: arg.name.clone(),
3132 max,
3133 got: count,
3134 });
3135 }
3136 }
3137}
3138
3139fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
3140 if let Some(min) = flag.var_min {
3141 if count < min {
3142 errors.push(UsageErr::VarFlagTooFew {
3143 name: flag.name.clone(),
3144 min,
3145 got: count,
3146 });
3147 }
3148 }
3149 if let Some(max) = flag.var_max {
3150 if count > max {
3151 errors.push(UsageErr::VarFlagTooMany {
3152 name: flag.name.clone(),
3153 max,
3154 got: count,
3155 });
3156 }
3157 }
3158}
3159
3160fn validate_flag_arg_fallback_count(
3161 flag: &SpecFlag,
3162 arg: &SpecArg,
3163 count: usize,
3164 errors: &mut Vec<UsageErr>,
3165) {
3166 if let Some(min) = arg.var_min {
3167 if count < min {
3168 errors.push(UsageErr::VarFlagTooFew {
3169 name: flag.name.clone(),
3170 min,
3171 got: count,
3172 });
3173 }
3174 }
3175 if let Some(max) = arg.var_max {
3176 if count > max {
3177 errors.push(UsageErr::VarFlagTooMany {
3178 name: flag.name.clone(),
3179 max,
3180 got: count,
3181 });
3182 }
3183 }
3184}
3185
3186fn bind_flag_fallback(
3189 flag: &Arc<SpecFlag>,
3190 values: &[String],
3191 out: &mut ParseOutput,
3192 custom_env: Option<&HashMap<String, String>>,
3193 origin: ValueOrigin,
3194) -> Result<(), miette::Error> {
3195 if values.is_empty() {
3196 return Ok(());
3197 }
3198 if let Some(arg) = flag.arg.as_ref() {
3199 let values = split_fallback_values(values, arg.delimiter);
3200 if flag.var || arg.var {
3201 if flag.var {
3202 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3203 }
3204 if arg.var {
3205 validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
3206 }
3207 validate_choice_values(
3208 ChoiceTarget::option(flag),
3209 &values,
3210 arg.choices.as_ref(),
3211 custom_env,
3212 )?;
3213 out.flags
3214 .insert(Arc::clone(flag), ParseValue::MultiString(values));
3215 } else {
3216 let value = values.into_iter().next().unwrap_or_default();
3217 validate_choice_value(
3218 ChoiceTarget::option(flag),
3219 &value,
3220 arg.choices.as_ref(),
3221 custom_env,
3222 )?;
3223 out.flags
3224 .insert(Arc::clone(flag), ParseValue::String(value));
3225 }
3226 } else if flag.var {
3227 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3228 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3229 out.flags
3230 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
3231 } else {
3232 out.flags.insert(
3233 Arc::clone(flag),
3234 ParseValue::Bool(fallback_is_true(&values[0])),
3235 );
3236 }
3237 out.flag_origins
3238 .entry(Arc::clone(flag))
3239 .or_default()
3240 .push(origin);
3241 Ok(())
3242}
3243
3244fn default_if_condition_matches(
3245 condition: &crate::SpecDefaultIf,
3246 out: &ParseOutput,
3247 overridden_flags: &HashSet<String>,
3248 custom_env: Option<&HashMap<String, String>>,
3249) -> bool {
3250 match &condition.when {
3251 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3252 Some(when) => {
3253 let Some(flag) = out
3254 .available_flags
3255 .values()
3256 .chain(out.flags.keys())
3257 .find(|flag| flag_matches_selector(flag, &condition.selector))
3258 else {
3259 return false;
3260 };
3261 if overridden_flags.contains(&flag.name) {
3262 return false;
3263 }
3264 explicit_flag_has_value(flag, when, out, custom_env)
3265 }
3266 }
3267}
3268
3269fn explicit_flag_has_value(
3275 flag: &SpecFlag,
3276 expected: &str,
3277 out: &ParseOutput,
3278 custom_env: Option<&HashMap<String, String>>,
3279) -> bool {
3280 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3281 ParseValue::Bool(value) => value.to_string() == expected,
3282 ParseValue::String(value) => value == expected,
3283 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3284 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3285 });
3286 if out.flags.contains_key(flag) {
3287 return parsed_matches;
3288 }
3289
3290 let value = flag.env_names().find_map(|env| match custom_env {
3291 Some(values) => values.get(env).cloned(),
3292 None => std::env::var(env).ok(),
3293 });
3294 value.is_some_and(
3295 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3296 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3297 None if flag.arg.is_none() => {
3298 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3299 }
3300 None => value == expected,
3301 },
3302 )
3303}
3304
3305fn parse_value_has(value: &ParseValue, expected: &str) -> bool {
3306 match value {
3307 ParseValue::Bool(value) => value.to_string() == expected,
3308 ParseValue::String(value) => value == expected,
3309 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3310 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3311 }
3312}
3313
3314fn validate_clause_relationships(
3315 out: &mut ParseOutput,
3316 overridden_flags: &HashSet<String>,
3317 custom_env: Option<&HashMap<String, String>>,
3318) {
3319 let Some(clause) = out.cmd.clause.as_ref() else {
3320 return;
3321 };
3322 let Some(instances) = out.clauses.get(&clause.name) else {
3323 return;
3324 };
3325 let flag_is_explicit = |selector: &str| {
3326 out.available_flags
3327 .values()
3328 .chain(out.flags.keys())
3329 .any(|flag| {
3330 flag_matches_selector(flag, selector)
3331 && !overridden_flags.contains(&flag.name)
3332 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3333 })
3334 };
3335 let flag_matches_value = |selector: &str, expected: &str| {
3336 out.available_flags
3337 .values()
3338 .chain(out.flags.keys())
3339 .find(|flag| flag_matches_selector(flag, selector))
3340 .is_some_and(|flag| {
3341 !overridden_flags.contains(&flag.name)
3342 && explicit_flag_has_value(flag, expected, out, custom_env)
3343 })
3344 };
3345 let flag_is_satisfied = |selector: &str| {
3346 out.available_flags
3347 .values()
3348 .chain(out.flags.keys())
3349 .any(|flag| flag_matches_selector(flag, selector))
3350 && selector_is_satisfied(selector, out, overridden_flags, custom_env)
3351 };
3352 let mut errors = Vec::new();
3353 for (instance_index, instance) in instances.iter().enumerate() {
3354 let arg_is_explicit = |selector: &str| {
3355 instance
3356 .keys()
3357 .any(|arg| !selector.starts_with('-') && arg.name == selector)
3358 };
3359 let selector_is_explicit =
3360 |selector: &str| flag_is_explicit(selector) || arg_is_explicit(selector);
3361 let selector_has_value = |selector: &str, expected: &str| {
3362 flag_matches_value(selector, expected)
3363 || instance.iter().any(|(arg, value)| {
3364 !selector.starts_with('-')
3365 && arg.name == selector
3366 && parse_value_has(value, expected)
3367 })
3368 };
3369 let selector_is_satisfied =
3370 |selector: &str| selector_is_explicit(selector) || flag_is_satisfied(selector);
3371 for arg in &clause.args {
3372 let given = instance.keys().any(|present| present.name == arg.name);
3373 if !given {
3374 let required_if = arg
3375 .required_if
3376 .iter()
3377 .any(|selector| selector_is_explicit(selector));
3378 let required_if_eq = arg
3379 .required_if_eq
3380 .iter()
3381 .any(|condition| selector_has_value(&condition.selector, &condition.value));
3382 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
3383 && arg
3384 .required_if_eq_all
3385 .iter()
3386 .all(|condition| selector_has_value(&condition.selector, &condition.value));
3387 let unless_any = arg
3388 .required_unless
3389 .iter()
3390 .any(|selector| selector_is_explicit(selector));
3391 let unless_all = !arg.required_unless_all.is_empty()
3392 && arg
3393 .required_unless_all
3394 .iter()
3395 .all(|selector| selector_is_explicit(selector));
3396 let required_unless = !(unless_any
3397 || unless_all
3398 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
3399 if required_if || required_if_eq || required_if_eq_all || required_unless {
3400 errors.push(UsageErr::MissingClauseArg {
3401 clause: clause.name.clone(),
3402 instance: instance_index + 1,
3403 arg: arg.name.clone(),
3404 });
3405 }
3406 continue;
3407 }
3408 for other in &arg.conflicts {
3409 if selector_is_explicit(other) {
3410 errors.push(UsageErr::InvalidFlag {
3411 token: arg.name.clone(),
3412 reason: format!("conflicts with {other}"),
3413 span: (0, 0).into(),
3414 input: format!("{} {other}", arg.name),
3415 });
3416 }
3417 }
3418 for other in &arg.requires {
3419 if selector_is_satisfied(other) {
3420 continue;
3421 }
3422 if other.starts_with('-') {
3423 let name = selector_flag_name(other, out).unwrap_or_else(|| other.clone());
3424 errors.push(UsageErr::MissingFlag(name));
3425 } else {
3426 errors.push(UsageErr::MissingClauseArg {
3427 clause: clause.name.clone(),
3428 instance: instance_index + 1,
3429 arg: other.clone(),
3430 });
3431 }
3432 }
3433 }
3434 }
3435 out.errors.extend(errors);
3436}
3437
3438fn selector_explicit_has_value(
3439 selector: &str,
3440 expected: &str,
3441 out: &ParseOutput,
3442 overridden_flags: &HashSet<String>,
3443 custom_env: Option<&HashMap<String, String>>,
3444) -> bool {
3445 if let Some(flag) = out
3446 .available_flags
3447 .values()
3448 .chain(out.flags.keys())
3449 .find(|flag| flag_matches_selector(flag, selector))
3450 {
3451 return !overridden_flags.contains(&flag.name)
3452 && explicit_flag_has_value(flag, expected, out, custom_env);
3453 }
3454 let Some(arg) = selector_arg(selector, out) else {
3455 return false;
3456 };
3457 let parsed = out
3458 .args
3459 .iter()
3460 .find(|(given, _)| given.name == arg.name)
3461 .map(|(_, value)| value)
3462 .or_else(|| {
3463 out.clauses.values().flatten().find_map(|instance| {
3464 instance
3465 .iter()
3466 .find(|(given, _)| given.name == arg.name)
3467 .map(|(_, value)| value)
3468 })
3469 });
3470 if let Some(value) = parsed {
3471 return match value {
3472 ParseValue::String(value) => value == expected,
3473 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3474 ParseValue::Bool(value) => value.to_string() == expected,
3475 ParseValue::MultiBool(values) => {
3476 values.iter().any(|value| value.to_string() == expected)
3477 }
3478 };
3479 }
3480 let value = arg.env_names().find_map(|env| match custom_env {
3481 Some(values) => values.get(env).cloned(),
3482 None => std::env::var(env).ok(),
3483 });
3484 value.is_some_and(|value| match arg.delimiter {
3485 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3486 None => value == expected,
3487 })
3488}
3489
3490fn selector_is_explicit(
3491 selector: &str,
3492 out: &ParseOutput,
3493 overridden_flags: &HashSet<String>,
3494 custom_env: Option<&HashMap<String, String>>,
3495) -> bool {
3496 let flag_is_explicit = out
3497 .available_flags
3498 .values()
3499 .chain(out.flags.keys())
3500 .any(|flag| {
3501 flag_matches_selector(flag, selector)
3502 && !overridden_flags.contains(&flag.name)
3503 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3504 });
3505 flag_is_explicit
3506 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3507}
3508
3509fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3515 out.available_flags
3516 .values()
3517 .chain(out.flags.keys())
3518 .find(|flag| flag_matches_selector(flag, selector))
3519 .map(|flag| flag.name.clone())
3520 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3521}
3522
3523fn selector_is_satisfied(
3533 selector: &str,
3534 out: &ParseOutput,
3535 overridden_flags: &HashSet<String>,
3536 custom_env: Option<&HashMap<String, String>>,
3537) -> bool {
3538 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3539 return true;
3540 }
3541 let flag_is_satisfied = out
3542 .available_flags
3543 .values()
3544 .chain(out.flags.keys())
3545 .filter(|flag| flag_matches_selector(flag, selector))
3546 .any(|flag| {
3547 !overridden_flags.contains(&flag.name)
3548 && (!flag.default.is_empty()
3549 || flag.arg.iter().any(|a| !a.default.is_empty())
3550 || flag.default_if.iter().any(|condition| {
3551 default_if_condition_matches(condition, out, overridden_flags, custom_env)
3552 }))
3553 });
3554 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3555}
3556
3557fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3558 if selector.starts_with('-') {
3561 return None;
3562 }
3563 out.cmds
3564 .iter()
3565 .flat_map(active_args)
3566 .find(|arg| arg.name == selector)
3567}
3568
3569fn arg_is_explicit(
3570 arg: &SpecArg,
3571 out: &ParseOutput,
3572 custom_env: Option<&HashMap<String, String>>,
3573) -> bool {
3574 out.args.keys().any(|given| given.name == arg.name)
3575 || out
3576 .clauses
3577 .values()
3578 .flatten()
3579 .any(|instance| instance.keys().any(|given| given.name == arg.name))
3580 || arg
3581 .env
3582 .as_ref()
3583 .is_some_and(|env| env_contains(custom_env, env))
3584}
3585
3586fn apply_flag_overrides(
3587 flag: &Arc<SpecFlag>,
3588 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3589 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3590 pending_flags: &mut Vec<Arc<SpecFlag>>,
3591 overridden_flags: &mut HashSet<String>,
3592 attributed: &mut BTreeMap<String, String>,
3596) {
3597 let overridden_names: HashSet<String> = available_flags
3598 .values()
3599 .chain(parsed_flags.keys())
3600 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3601 .map(|other| other.name.clone())
3602 .collect();
3603
3604 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3605 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3606 for name in &overridden_names {
3607 attributed.insert(name.clone(), flag.name.clone());
3608 }
3609 overridden_flags.extend(overridden_names);
3610 overridden_flags.remove(&flag.name);
3612 attributed.remove(&flag.name);
3613}
3614
3615#[cfg(feature = "cli-help")]
3616fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3617 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3618}
3619
3620#[cfg(feature = "cli-help")]
3621fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3622 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3623 if !out.is_empty() {
3624 out.push('\n');
3625 }
3626 out.push_str(&docs::cli::render_help(spec, cmd, true));
3627 let mut children: Vec<_> = cmd
3628 .subcommands
3629 .values()
3630 .filter(|child| !child.hide)
3631 .collect();
3632 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3633 for child in children {
3634 append(out, spec, child);
3635 }
3636 }
3637
3638 let mut out = String::new();
3639 append(&mut out, spec, cmd);
3640 UsageErr::Help(out)
3641}
3642
3643#[cfg(not(feature = "cli-help"))]
3644fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3645 UsageErr::Help("help".to_string())
3646}
3647
3648#[cfg(not(feature = "cli-help"))]
3649fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3650 UsageErr::Help("help".to_string())
3651}
3652
3653fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3656 let value = if long {
3657 spec.long_version.as_ref().or(spec.version.as_ref())
3658 } else {
3659 spec.version.as_ref().or(spec.long_version.as_ref())
3660 };
3661 UsageErr::Version(value.cloned().unwrap_or_default())
3662}
3663
3664fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3665 use crate::SpecFlagAction;
3666 match flag.action {
3667 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3668 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3669 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3670 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3671 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3672 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3673 }
3674}
3675
3676fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
3678 let token = flag
3679 .long
3680 .first()
3681 .map(|long| format!("--{long}"))
3682 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
3683 .unwrap_or_else(|| flag.name.clone());
3684 UsageErr::InvalidFlag {
3685 token: token.clone(),
3686 reason: "requires an argument".to_string(),
3687 span: (0, 0).into(),
3688 input: format!("{token} {following}"),
3689 }
3690}
3691
3692#[derive(Copy, Clone)]
3693struct ChoiceTarget<'a> {
3694 kind: &'a str,
3695 name: &'a str,
3696}
3697
3698impl<'a> ChoiceTarget<'a> {
3699 fn arg(arg: &'a SpecArg) -> Self {
3700 Self {
3701 kind: "arg",
3702 name: &arg.name,
3703 }
3704 }
3705
3706 fn option(flag: &'a SpecFlag) -> Self {
3707 Self {
3708 kind: "option",
3709 name: &flag.name,
3710 }
3711 }
3712}
3713
3714fn short_bundle_is_known(
3719 spec: &Spec,
3720 cmds: &[SpecCommand],
3721 available: &BTreeMap<String, Arc<SpecFlag>>,
3722 token: &str,
3723) -> bool {
3724 for c in token.chars().skip(1) {
3725 match available.get(&format!("-{c}")) {
3726 None if supplied_short(spec, cmds, c).is_some() => {}
3731 None => return false,
3732 Some(f) if f.arg.is_some() => return true,
3733 Some(_) => {}
3734 }
3735 }
3736 true
3737}
3738
3739fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3749 let cmd = cmds.last()?;
3750 match letter {
3751 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3752 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3753 _ => None,
3754 }
3755}
3756
3757fn reject_unknown_flag_if_asked(
3763 spec: &Spec,
3764 path: &[SpecCommand],
3765 token: &str,
3766) -> Result<(), UsageErr> {
3767 if !is_flag_like(token) {
3770 return Ok(());
3771 }
3772 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3773 return Ok(());
3774 }
3775 Err(UsageErr::InvalidFlag {
3776 token: token.to_string(),
3777 reason: "no such flag".to_string(),
3778 span: (0, 0).into(),
3779 input: token.to_string(),
3780 })
3781}
3782
3783fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3789 path.iter()
3790 .rev()
3791 .find_map(|cmd| cmd.unknown_flags)
3792 .or(spec.unknown_flags)
3793 .unwrap_or_default()
3794}
3795
3796fn is_flag_like(token: &str) -> bool {
3802 match token.strip_prefix('-') {
3803 None | Some("") => false,
3804 Some(_) => true,
3805 }
3806}
3807
3808fn is_negative_number(token: &str) -> bool {
3809 token.strip_prefix('-').is_some_and(is_number)
3810}
3811
3812fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
3818 !flag.require_equals
3819 && (!is_flag_like(token)
3820 || flag.allow_hyphen_values()
3821 || (is_negative_number(token)
3822 && (flag
3823 .arg
3824 .as_ref()
3825 .is_some_and(|arg| arg.allow_negative_numbers)
3826 || (flag.default_missing.is_none() && !flag.value_optional))))
3827}
3828
3829fn record_scalar_flag_occurrence(
3830 cmds: &[SpecCommand],
3831 flag: &Arc<SpecFlag>,
3832 command_level: usize,
3833 bool_value: Option<bool>,
3834 occurrences: &mut HashMap<(usize, usize), u8>,
3835 errors: &mut Vec<UsageErr>,
3836) {
3837 let strict = cmds
3838 .get(command_level)
3839 .is_some_and(|cmd| !cmd.args_override_self);
3840 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3841 if !strict || flag.count || collects_values {
3842 return;
3843 }
3844
3845 let bit = match bool_value {
3846 Some(false) if flag.negate.is_some() => 0b10,
3847 _ => 0b01,
3848 };
3849 let key = (Arc::as_ptr(flag) as usize, command_level);
3850 let seen = occurrences.entry(key).or_default();
3851 if *seen & bit != 0 {
3852 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3853 }
3854 *seen |= bit;
3855}
3856
3857fn is_command_word(token: &str) -> bool {
3865 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3866}
3867
3868fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3874 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3875}
3876
3877fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3883 !is_negative_number(token)
3884 || cmd
3885 .find_subcommand(default_name)
3886 .and_then(|default| default.args.first())
3887 .is_some_and(|arg| arg.allow_negative_numbers)
3888}
3889
3890fn is_number(rest: &str) -> bool {
3897 let (mantissa, exponent) = match rest.find(['e', 'E']) {
3898 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3899 None => (rest, None),
3900 };
3901
3902 let mut seen_digit = false;
3903 let mut seen_dot = false;
3904 for c in mantissa.chars() {
3905 match c {
3906 '0'..='9' => seen_digit = true,
3907 '.' if !seen_dot => seen_dot = true,
3908 _ => return false,
3909 }
3910 }
3911 if !seen_digit {
3912 return false;
3913 }
3914
3915 match exponent {
3916 None => true,
3917 Some(exp) => {
3918 let digits = exp
3919 .strip_prefix('+')
3920 .or_else(|| exp.strip_prefix('-'))
3921 .unwrap_or(exp);
3922 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3923 }
3924 }
3925}
3926
3927#[allow(clippy::too_many_arguments)]
3934fn bind_pending_flag_value(
3935 spec: &Spec,
3936 cmd: &SpecCommand,
3937 errors: &mut Vec<UsageErr>,
3938 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3939 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3940 word: &mut String,
3941 input: &mut VecDeque<Token>,
3942 custom_env: Option<&HashMap<String, String>>,
3943 trace: &mut Trace,
3944 argv: usize,
3948 attached: bool,
3949) -> miette::Result<bool> {
3950 let collecting = flag_awaiting_value
3954 .last()
3955 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3956 .cloned()
3957 .map(|flag| {
3958 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3959 (flag, carried)
3960 });
3961 let mut bound = vec![];
3962 let refused = drain_pending_flag_values(
3963 spec,
3964 cmd,
3965 errors,
3966 flags,
3967 flag_awaiting_value,
3968 word,
3969 custom_env,
3970 &mut bound,
3971 )?;
3972 for (flag, values) in bound {
3973 trace.record(
3974 argv,
3975 TokenRole::Value {
3976 flag,
3977 values,
3978 attached,
3979 },
3980 );
3981 }
3982 if refused {
3983 return Ok(true);
3984 }
3985 let Some((flag, carried)) = collecting else {
3986 return Ok(false);
3987 };
3988 collect_variadic_flag_values(
3989 spec,
3990 cmd,
3991 errors,
3992 flags,
3993 flag_awaiting_value,
3994 &flag,
3995 carried,
3996 input,
3997 custom_env,
3998 trace,
3999 )
4000}
4001
4002#[allow(clippy::too_many_arguments)]
4015fn collect_variadic_flag_values(
4016 spec: &Spec,
4017 cmd: &SpecCommand,
4018 errors: &mut Vec<UsageErr>,
4019 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4020 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4021 flag: &Arc<SpecFlag>,
4022 carried: usize,
4023 input: &mut VecDeque<Token>,
4024 custom_env: Option<&HashMap<String, String>>,
4025 trace: &mut Trace,
4026) -> miette::Result<bool> {
4027 let max = flag
4028 .arg
4029 .as_ref()
4030 .and_then(|arg| arg.var_max)
4031 .unwrap_or(usize::MAX);
4032 while flags
4033 .get(flag)
4034 .map(value_count)
4035 .unwrap_or(0)
4036 .saturating_sub(carried)
4037 < max
4038 {
4039 let Some(next) = input.front().map(|token| token.word.as_str()) else {
4040 break;
4041 };
4042 if flag
4043 .arg
4044 .as_ref()
4045 .and_then(|arg| arg.value_terminator.as_deref())
4046 == Some(next)
4047 {
4048 let terminator = input.pop_front().unwrap();
4049 trace.record(
4050 terminator.argv,
4051 TokenRole::ValueTerminator {
4052 ends: flag.name.clone(),
4053 },
4054 );
4055 break;
4056 }
4057 if next == "--"
4060 || (is_flag_like(next)
4061 && !(flag
4062 .arg
4063 .as_ref()
4064 .is_some_and(|arg| arg.allow_negative_numbers)
4065 && is_negative_number(next)))
4066 {
4067 break;
4068 }
4069 let taken = input.pop_front().unwrap();
4070 let argv = taken.argv;
4071 let mut word = taken.word;
4072 flag_awaiting_value.push(Arc::clone(flag));
4073 let mut bound = vec![];
4074 let refused = drain_pending_flag_values(
4075 spec,
4076 cmd,
4077 errors,
4078 flags,
4079 flag_awaiting_value,
4080 &mut word,
4081 custom_env,
4082 &mut bound,
4083 )?;
4084 for (flag, values) in bound {
4085 trace.record(
4088 argv,
4089 TokenRole::Value {
4090 flag,
4091 values,
4092 attached: false,
4093 },
4094 );
4095 }
4096 if refused {
4097 return Ok(true);
4098 }
4099 }
4100 let taken = flags
4109 .get(flag)
4110 .map(value_count)
4111 .unwrap_or(0)
4112 .saturating_sub(carried);
4113 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
4114 if taken < min {
4115 errors.push(UsageErr::VarFlagTooFew {
4116 name: flag.name.clone(),
4117 min,
4118 got: taken,
4119 });
4120 }
4121 }
4122 if taken > max {
4123 errors.push(UsageErr::VarFlagTooMany {
4124 name: flag.name.clone(),
4125 max,
4126 got: taken,
4127 });
4128 }
4129 Ok(false)
4130}
4131
4132fn value_count(value: &ParseValue) -> usize {
4134 match value {
4135 ParseValue::MultiString(values) => values.len(),
4136 ParseValue::MultiBool(values) => values.len(),
4137 _ => 1,
4138 }
4139}
4140
4141fn try_bind_default_missing(
4152 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4153 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4154 custom_env: Option<&HashMap<String, String>>,
4155 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
4156) -> miette::Result<bool> {
4157 let Some(flag) = flag_awaiting_value.last() else {
4158 return Ok(false);
4159 };
4160 let value = match flag.default_missing.clone() {
4161 Some(value) => value,
4162 None if flag.value_optional => {
4163 let flag = flag_awaiting_value.pop().unwrap();
4164 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
4168 origins
4169 .entry(Arc::clone(&flag))
4170 .or_default()
4171 .push(ValueOrigin::DefaultMissing);
4172 if flag.var {
4173 flags
4178 .entry(flag)
4179 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
4180 .try_as_multi_string_mut()
4181 .unwrap()
4182 .push(String::new());
4183 } else if variadic_value {
4184 flags
4187 .entry(flag)
4188 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
4189 } else {
4190 flags.insert(flag, ParseValue::MultiString(Vec::new()));
4194 }
4195 return Ok(true);
4196 }
4197 None => return Ok(false),
4198 };
4199 if let Some(arg) = flag.arg.as_ref() {
4200 validate_choice_value(
4201 ChoiceTarget::option(flag),
4202 &value,
4203 arg.choices.as_ref(),
4204 custom_env,
4205 )?;
4206 }
4207 let flag = flag_awaiting_value.pop().unwrap();
4208 origins
4209 .entry(Arc::clone(&flag))
4210 .or_default()
4211 .push(ValueOrigin::DefaultMissing);
4212 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
4213 if collecting {
4214 let arr = flags
4215 .entry(flag)
4216 .or_insert_with(|| ParseValue::MultiString(vec![]))
4217 .try_as_multi_string_mut()
4218 .unwrap();
4219 arr.push(value);
4220 } else {
4221 flags.insert(flag, ParseValue::String(value));
4222 }
4223 Ok(true)
4224}
4225
4226#[allow(clippy::too_many_arguments)]
4231fn drain_pending_flag_values(
4232 spec: &Spec,
4233 cmd: &SpecCommand,
4234 errors: &mut Vec<UsageErr>,
4235 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
4236 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
4237 word: &mut String,
4238 custom_env: Option<&HashMap<String, String>>,
4239 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
4240) -> miette::Result<bool> {
4241 while let Some(flag) = flag_awaiting_value.pop() {
4242 let arg = flag.arg.as_ref().unwrap();
4243 let parts: Vec<String> = match arg.delimiter {
4247 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
4248 None => vec![std::mem::take(word)],
4249 };
4250 for part in &parts {
4251 if validate_choices(
4252 spec,
4253 cmd,
4254 errors,
4255 ChoiceTarget::option(&flag),
4256 part,
4257 arg.choices.as_ref(),
4258 custom_env,
4259 )? {
4260 return Ok(true);
4261 }
4262 }
4263 word.clear();
4264 bound.push((Arc::clone(&flag), parts.clone()));
4265 if flag.var || arg.var {
4268 let arr = flags
4269 .entry(flag)
4270 .or_insert_with(|| ParseValue::MultiString(vec![]))
4271 .try_as_multi_string_mut()
4272 .unwrap();
4273 arr.extend(parts);
4274 } else {
4275 flags.insert(
4278 flag,
4279 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
4280 );
4281 }
4282 }
4283 Ok(false)
4284}
4285
4286fn choice_error(
4287 target: ChoiceTarget<'_>,
4288 value: &str,
4289 choices: Option<&SpecChoices>,
4290 custom_env: Option<&HashMap<String, String>>,
4291) -> Option<String> {
4292 let choices = choices?;
4293 if !choices.strict {
4294 return None;
4295 }
4296 let values = choices.values_with_env(custom_env);
4297 if choices.matches_with_env(value, custom_env) {
4298 return None;
4299 }
4300 if let Some(env) = choices.env() {
4301 if values.is_empty() {
4302 return Some(format!(
4303 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
4304 target.kind, target.name,
4305 ));
4306 }
4307 }
4308 Some(format!(
4309 "Invalid choice for {} {}: {value}, expected one of {}",
4310 target.kind,
4311 target.name,
4312 values.join(", ")
4313 ))
4314}
4315
4316fn validate_choices(
4317 spec: &Spec,
4318 cmd: &SpecCommand,
4319 errors: &mut Vec<UsageErr>,
4320 target: ChoiceTarget<'_>,
4321 value: &str,
4322 choices: Option<&SpecChoices>,
4323 custom_env: Option<&HashMap<String, String>>,
4324) -> miette::Result<bool> {
4325 if is_help_arg(spec, cmd, value)
4326 && choices
4327 .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
4328 {
4329 errors.push(render_help_err(spec, cmd, value.len() > 2));
4330 return Ok(true);
4331 }
4332
4333 if let Some(err) = choice_error(target, value, choices, custom_env) {
4334 bail!("{err}");
4335 }
4336 Ok(false)
4337}
4338
4339fn validate_choice_value(
4340 target: ChoiceTarget<'_>,
4341 value: &str,
4342 choices: Option<&SpecChoices>,
4343 custom_env: Option<&HashMap<String, String>>,
4344) -> miette::Result<()> {
4345 if let Some(err) = choice_error(target, value, choices, custom_env) {
4346 bail!("{err}");
4347 }
4348 Ok(())
4349}
4350
4351fn validate_choice_values(
4352 target: ChoiceTarget<'_>,
4353 values: &[String],
4354 choices: Option<&SpecChoices>,
4355 custom_env: Option<&HashMap<String, String>>,
4356) -> miette::Result<()> {
4357 for value in values {
4358 validate_choice_value(target, value, choices, custom_env)?;
4359 }
4360 Ok(())
4361}
4362
4363fn record_stop(
4370 out: &mut ParseOutput,
4371 next_arg_idx: usize,
4372 seen_double_dash: bool,
4373 trace: &mut Trace,
4374 unread: &VecDeque<Token>,
4375) {
4376 out.next_arg = out
4377 .cmd
4378 .args
4379 .get(cursor_skip_sigils(&out.cmd, next_arg_idx))
4380 .cloned()
4381 .map(Arc::new);
4382 out.double_dash_seen = seen_double_dash;
4383 finalize_current_clause(out);
4384 trace.close(unread);
4385 out.tokens = std::mem::take(&mut trace.tokens);
4386}
4387
4388fn finalize_current_clause(out: &mut ParseOutput) {
4389 let Some(clause) = out.cmd.clause.as_ref() else {
4390 return;
4391 };
4392 out.clauses
4393 .entry(clause.name.clone())
4394 .or_default()
4395 .push(std::mem::take(&mut out.args));
4396}
4397
4398fn restore_current_clause(out: &mut ParseOutput) {
4399 let Some(clause) = out.cmd.clause.as_ref() else {
4400 return;
4401 };
4402 if let Some(current) = out.clauses.get_mut(&clause.name).and_then(Vec::pop) {
4403 out.args = current;
4404 }
4405}
4406
4407fn cursor_skip_sigils(cmd: &SpecCommand, mut idx: usize) -> usize {
4408 while active_args(cmd)
4409 .get(idx)
4410 .is_some_and(|arg| arg.sigil.is_some())
4411 {
4412 idx += 1;
4413 }
4414 idx
4415}
4416
4417fn active_args(cmd: &SpecCommand) -> &[SpecArg] {
4418 cmd.clause
4419 .as_ref()
4420 .map(|clause| clause.args.as_slice())
4421 .unwrap_or(cmd.args.as_slice())
4422}
4423
4424fn match_sigil_arg<'a>(
4425 cmd: &'a SpecCommand,
4426 word: &'a str,
4427) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4428 cmd.args
4429 .iter()
4430 .filter_map(|arg| {
4431 let sigil = arg.sigil.as_deref()?;
4432 let value = word.strip_prefix(sigil)?;
4433 Some((arg, sigil, value))
4434 })
4435 .max_by_key(|(_, sigil, _)| sigil.len())
4436}
4437
4438fn match_sigil_arg_chain<'a>(
4439 cmds: &'a [SpecCommand],
4440 word: &'a str,
4441) -> Option<(&'a SpecArg, &'a str, &'a str)> {
4442 cmds.iter()
4443 .filter_map(|cmd| match_sigil_arg(cmd, word))
4444 .max_by_key(|(_, sigil, _)| sigil.len())
4445}
4446
4447fn report_double_dash_violation(
4454 arg: &SpecArg,
4455 errors: &mut Vec<UsageErr>,
4456 violations: &mut HashSet<String>,
4457) {
4458 if violations.insert(arg.name.clone()) {
4459 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4460 }
4461}
4462
4463fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
4478 (spec.version.is_some() || spec.long_version.is_some())
4479 && cmds.len() == 1
4480 && !spec.cmd.disable_version_flag
4481 && (w == "--version" || w == "-V")
4482}
4483
4484fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
4485 spec.disable_help != Some(true)
4486 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
4487 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
4488}
4489
4490impl ParseOutput {
4491 pub fn as_env(&self) -> BTreeMap<String, String> {
4492 let mut env = BTreeMap::new();
4493 for (flag, val) in &self.flags {
4494 let key = format!("usage_{}", crate::case::snake(&flag.name));
4495 let val = match val {
4496 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
4497 ParseValue::String(s) => s.clone(),
4498 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
4499 ParseValue::MultiString(s) => crate::shell_words::join(s),
4500 };
4501 env.insert(key, val);
4502 }
4503 for (arg, val) in &self.args {
4504 let key = format!("usage_{}", crate::case::snake(&arg.name));
4505 env.insert(key, val.to_string());
4506 }
4507 env
4508 }
4509}
4510
4511impl Display for ParseValue {
4512 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4513 match self {
4514 ParseValue::Bool(b) => write!(f, "{b}"),
4515 ParseValue::String(s) => write!(f, "{s}"),
4516 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
4517 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
4518 }
4519 }
4520}
4521
4522fn render_token(token: &TokenBinding) -> String {
4524 let roles = token.roles.iter().map(render_role).join(", ");
4525 let synthesized = if token.synthesized { " (read as)" } else { "" };
4526 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4527}
4528
4529fn render_role(role: &TokenRole) -> String {
4530 match role {
4531 TokenRole::Program => "program".to_string(),
4532 TokenRole::Command { name } => format!("subcommand {name}"),
4533 TokenRole::Flag {
4534 flag,
4535 spelling,
4536 negated,
4537 } => {
4538 let negated = if *negated { ", negated" } else { "" };
4539 format!("flag {} as {spelling}{negated}", flag.name)
4540 }
4541 TokenRole::Value {
4542 flag,
4543 values,
4544 attached,
4545 } => {
4546 let attached = if *attached { ", attached" } else { "" };
4547 format!("value of {} = {values:?}{attached}", flag.name)
4548 }
4549 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4550 TokenRole::Sigil { arg, sigil, values } => {
4551 format!("sigil arg {} ({sigil}) = {values:?}", arg.name)
4552 }
4553 TokenRole::Separator => "separator".to_string(),
4554 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4555 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4556 TokenRole::Restart => "restart".to_string(),
4557 TokenRole::ClauseSeparator { name } => format!("clause separator for {name}"),
4558 TokenRole::UnknownFlag { bound_as } => match bound_as {
4559 Some(arg) => format!("unknown flag, bound as {}", arg.name),
4560 None => "unknown flag".to_string(),
4561 },
4562 TokenRole::Refused { reason } => format!("refused: {reason}"),
4563 TokenRole::External => "external".to_string(),
4564 TokenRole::Unread => "unread".to_string(),
4565 }
4566}
4567
4568impl Debug for ParseOutput {
4569 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4570 f.debug_struct("ParseOutput")
4571 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4572 .field(
4573 "args",
4574 &self
4575 .args
4576 .iter()
4577 .map(|(a, w)| format!("{}: {w}", a.name))
4578 .collect_vec(),
4579 )
4580 .field("clauses", &self.clauses)
4581 .field(
4582 "available_flags",
4583 &self
4584 .available_flags
4585 .iter()
4586 .map(|(f, w)| format!("{f}: {w}"))
4587 .collect_vec(),
4588 )
4589 .field(
4590 "flags",
4591 &self
4592 .flags
4593 .iter()
4594 .map(|(f, w)| format!("{}: {w}", f.name))
4595 .collect_vec(),
4596 )
4597 .field("flag_awaiting_value", &self.flag_awaiting_value)
4598 .field("errors", &self.errors)
4599 .field("external", &self.external)
4600 .field(
4604 "tokens",
4605 &self.tokens.iter().map(render_token).collect_vec(),
4606 )
4607 .field(
4608 "origins",
4609 &self
4610 .flag_origins
4611 .iter()
4612 .map(|(f, o)| format!("{}: {o:?}", f.name))
4613 .chain(
4614 self.arg_origins
4615 .iter()
4616 .map(|(a, o)| format!("{}: {o:?}", a.name)),
4617 )
4618 .collect_vec(),
4619 )
4620 .field("overridden_flags", &self.overridden_flags)
4621 .finish()
4622 }
4623}
4624
4625#[cfg(test)]
4626mod tests {
4627 use super::*;
4628 use crate::SpecFlagAction;
4629
4630 fn input(words: &[&str]) -> Vec<String> {
4631 words.iter().map(|word| (*word).to_string()).collect()
4632 }
4633
4634 #[test]
4635 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4636 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4641 .parse()
4642 .unwrap();
4643
4644 for spelling in ["--version", "-V"] {
4645 let err = parse(&spec, &input(&["ex", spelling]))
4646 .expect_err("answering with a version ends the parse");
4647 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4648 }
4649 }
4650
4651 #[test]
4652 fn the_supplied_version_flag_is_the_roots_alone() {
4653 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4656 .parse()
4657 .unwrap();
4658
4659 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4660 assert_eq!(err.to_string(), "unexpected word: --version");
4661 }
4662
4663 #[test]
4664 fn no_declared_version_supplies_nothing() {
4665 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4668
4669 for spelling in ["--version", "-V"] {
4670 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4671 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4672 }
4673 }
4674
4675 #[test]
4676 fn disable_version_flag_removes_the_supplied_spellings() {
4677 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4678 .parse()
4679 .unwrap();
4680
4681 for spelling in ["--version", "-V"] {
4682 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4683 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4684 }
4685 }
4686
4687 #[test]
4688 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4689 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4692 .parse()
4693 .unwrap();
4694
4695 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4696 assert_eq!(out.flags.len(), 1);
4697
4698 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4699 assert_eq!(err.to_string(), "1.2.3");
4700 }
4701
4702 #[test]
4703 fn the_supplied_spellings_split_the_two_version_texts() {
4704 let spec: Spec =
4707 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4708 .parse()
4709 .unwrap();
4710
4711 assert_eq!(
4712 parse(&spec, &input(&["ex", "--version"]))
4713 .unwrap_err()
4714 .to_string(),
4715 "1.2.3 (abcdef)"
4716 );
4717 assert_eq!(
4718 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4719 "1.2.3"
4720 );
4721 }
4722
4723 #[test]
4724 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4725 let spec: Spec =
4730 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4731 .parse()
4732 .unwrap();
4733
4734 for token in ["-vh", "-hv"] {
4735 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4736 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4737 }
4738 for token in ["-vV", "-Vv"] {
4739 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4740 assert_eq!(err.to_string(), "1.2.3", "{token}");
4741 }
4742 }
4743
4744 #[test]
4745 fn a_bundled_help_letter_asks_for_the_short_page() {
4746 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"
4749 .parse()
4750 .unwrap();
4751
4752 let short = parse(&spec, &input(&["ex", "-vh"]))
4753 .unwrap_err()
4754 .to_string();
4755 let long = parse(&spec, &input(&["ex", "--help"]))
4756 .unwrap_err()
4757 .to_string();
4758 assert!(short.contains("Be loud"), "{short}");
4759 assert!(!short.contains("at length"), "{short}");
4760 assert!(long.contains("at length"), "{long}");
4761 }
4762
4763 #[test]
4764 fn the_bundled_version_letter_is_the_roots_alone() {
4765 let spec: Spec =
4767 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4768 .parse()
4769 .unwrap();
4770
4771 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4772 assert_eq!(err.to_string(), "unexpected word: -vV");
4773 }
4774
4775 #[test]
4776 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4777 let spec: Spec =
4780 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4781 .parse()
4782 .unwrap();
4783
4784 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4785 assert_eq!(out.flags.len(), 2);
4786 assert!(out
4787 .flags
4788 .iter()
4789 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4790 }
4791
4792 #[test]
4793 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4794 let spec: Spec =
4795 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4796 .parse()
4797 .unwrap();
4798
4799 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4800 assert_eq!(err.to_string(), "unexpected word: -vh");
4801 }
4802
4803 #[test]
4804 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4805 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4808 .parse()
4809 .unwrap();
4810
4811 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4812 assert!(out.flags.is_empty(), "{:?}", out.flags);
4813 assert_eq!(out.args.len(), 1);
4814 }
4815
4816 fn spec_with_arg(arg: SpecArg) -> Spec {
4817 let cmd = SpecCommand::builder().name("test").arg(arg).build();
4818 Spec {
4819 name: "test".to_string(),
4820 bin: "test".to_string(),
4821 cmd,
4822 ..Default::default()
4823 }
4824 }
4825
4826 fn spec_with_flag(flag: SpecFlag) -> Spec {
4827 let cmd = SpecCommand::builder().name("test").flag(flag).build();
4828 Spec {
4829 name: "test".to_string(),
4830 bin: "test".to_string(),
4831 cmd,
4832 ..Default::default()
4833 }
4834 }
4835
4836 fn parse_with_env(
4837 spec: &Spec,
4838 words: &[&str],
4839 env: &[(&str, &str)],
4840 ) -> Result<ParseOutput, miette::Error> {
4841 let env = env
4842 .iter()
4843 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4844 .collect();
4845 Parser::new(spec).with_env(env).parse(&input(words))
4846 }
4847
4848 fn first_string_value(parsed: &ParseOutput) -> &str {
4849 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4850 return value;
4851 }
4852 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4853 return value;
4854 }
4855 panic!("expected first parsed value to be ParseValue::String");
4856 }
4857
4858 #[test]
4859 fn custom_environment_parser_dispatches_executable_views() {
4860 let spec: Spec = r#"
4861bin "ex"
4862view "runner" root="run"
4863cmd "run" {
4864 flag "--token <token>" env="TOKEN"
4865}
4866 "#
4867 .parse()
4868 .unwrap();
4869 let parsed = Parser::new(&spec)
4870 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4871 .parse(&input(&["runner"]))
4872 .unwrap();
4873
4874 assert_eq!(parsed.cmd.name, "runner");
4875 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4876 && matches!(value, ParseValue::String(value) if value == "secret")));
4877 }
4878
4879 #[test]
4880 fn an_executable_view_keeps_the_hosts_version_action() {
4881 let spec: Spec = r#"
4882bin "ex"
4883version "1.2.3"
4884flag "-V --version" action="version"
4885flag "--verbose" global=#true
4886view "runner" root="run" globals=#true
4887cmd "run"
4888 "#
4889 .parse()
4890 .unwrap();
4891
4892 let error = Parser::new(&spec)
4893 .parse(&input(&["runner", "--version"]))
4894 .expect_err("the host version action should answer before view projection");
4895 assert_eq!(error.to_string(), "1.2.3");
4896
4897 let error = Parser::new(&spec)
4898 .parse(&input(&["runner", "--verbose", "--version"]))
4899 .expect_err("the host version action should remain after a carried global");
4900 assert_eq!(error.to_string(), "1.2.3");
4901 }
4902
4903 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4904 let flag = parsed
4905 .flags
4906 .keys()
4907 .find(|flag| flag.name == name)
4908 .unwrap_or_else(|| panic!("expected flag {name}"));
4909 let value = parsed
4910 .flags
4911 .get(flag)
4912 .unwrap_or_else(|| panic!("expected value for flag {name}"));
4913 match value {
4914 ParseValue::String(value) => value,
4915 _ => panic!("expected flag {name} to be ParseValue::String"),
4916 }
4917 }
4918
4919 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4920 let err = result.expect_err("expected parser error");
4921 assert_eq!(format!("{err}"), expected);
4922 }
4923
4924 #[test]
4925 fn a_short_version_action_falls_back_to_the_long_version() {
4926 let flag = SpecFlag::builder()
4927 .short('R')
4928 .action(SpecFlagAction::Version)
4929 .build();
4930 let spec = Spec {
4931 name: "test".to_string(),
4932 bin: "test".to_string(),
4933 long_version: Some("1.2.3\ncommit abc123".to_string()),
4934 ..Default::default()
4935 };
4936 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4937 panic!("expected version action")
4938 };
4939 assert_eq!(version, "1.2.3\ncommit abc123");
4940 }
4941
4942 #[cfg(feature = "unstable_choices_env")]
4943 fn spec_arg_choices_env(key: &str) -> Spec {
4944 spec_with_arg(
4945 SpecArg::builder()
4946 .name("env")
4947 .choices_env(key)
4948 .required(false)
4949 .build(),
4950 )
4951 }
4952
4953 #[cfg(feature = "unstable_choices_env")]
4954 fn spec_flag_choices_env(key: &str) -> Spec {
4955 spec_with_flag(
4956 SpecFlag::builder()
4957 .long("env")
4958 .arg(SpecArg::builder().name("env").choices_env(key).build())
4959 .build(),
4960 )
4961 }
4962
4963 #[test]
4964 fn test_parse() {
4965 let cmd = SpecCommand::builder()
4966 .name("test")
4967 .arg(SpecArg::builder().name("arg").build())
4968 .flag(SpecFlag::builder().long("flag").build())
4969 .build();
4970 let spec = Spec {
4971 name: "test".to_string(),
4972 bin: "test".to_string(),
4973 cmd,
4974 ..Default::default()
4975 };
4976 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4977 let parsed = parse(&spec, &input).unwrap();
4978 assert_eq!(parsed.cmds.len(), 1);
4979 assert_eq!(parsed.cmds[0].name, "test");
4980 assert_eq!(parsed.args.len(), 1);
4981 assert_eq!(parsed.flags.len(), 1);
4982 assert_eq!(parsed.available_flags.len(), 1);
4983 }
4984
4985 #[test]
4986 fn test_flag_overrides_last_occurrence_wins() {
4987 let spec: Spec = r#"
4988flag "--stdin" default=#true
4989flag "--file <file>" overrides="--stdin"
4990 "#
4991 .parse()
4992 .unwrap();
4993
4994 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4995 assert_eq!(file_wins.flags.len(), 1);
4996 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4997 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4998
4999 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
5000 assert_eq!(stdin_wins.flags.len(), 1);
5001 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
5002 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
5003 }
5004
5005 #[test]
5006 fn test_flag_override_clears_pending_value() {
5007 let spec: Spec = r#"
5008flag "--file <file>" overrides="--stdin"
5009flag "--stdin"
5010arg "[input]"
5011 "#
5012 .parse()
5013 .unwrap();
5014
5015 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
5016 assert_eq!(parsed.flags.len(), 1);
5017 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
5018 assert_eq!(first_string_value(&parsed), "input.txt");
5019 }
5020
5021 #[cfg(unix)]
5022 #[test]
5023 fn a_mount_on_the_root_discovers_subcommands() {
5024 let spec: Spec = r#"
5028name "ex"
5029bin "ex"
5030cmd "declared"
5031mount run="echo 'cmd \"discovered\"'"
5032"#
5033 .parse()
5034 .unwrap();
5035
5036 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5037 assert_eq!(out.cmd.name, "discovered");
5038 }
5039
5040 #[test]
5041 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
5042 let spec: Spec = r#"
5043name "ex"
5044bin "ex"
5045mount run="this command must never run"
5046cmd "declared"
5047"#
5048 .parse()
5049 .unwrap();
5050
5051 Parser::new(&spec)
5052 .with_mount_outputs(HashMap::new())
5053 .parse(&input(&["ex", "declared"]))
5054 .expect("a declared command does not resolve the mount");
5055
5056 let error = Parser::new(&spec)
5057 .with_mount_outputs(HashMap::new())
5058 .parse(&input(&["ex", "discovered"]))
5059 .unwrap_err();
5060 assert!(
5061 error
5062 .to_string()
5063 .contains("No injected output was provided for mount command"),
5064 "{error}"
5065 );
5066 }
5067
5068 #[cfg(unix)]
5069 #[test]
5070 fn completion_sees_root_mounted_commands_with_nothing_typed() {
5071 let spec: Spec = r#"
5075name "ex"
5076bin "ex"
5077cmd "declared"
5078mount run="echo 'cmd \"discovered\"'"
5079"#
5080 .parse()
5081 .unwrap();
5082
5083 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5084 assert!(
5085 out.cmd.subcommands.contains_key("discovered"),
5086 "a completion should see mounted commands; got {:?}",
5087 out.cmd.subcommands.keys().collect::<Vec<_>>()
5088 );
5089 }
5090
5091 #[cfg(unix)]
5092 #[test]
5093 fn completion_and_execution_agree_about_discovery() {
5094 let spec: Spec = r#"
5098name "ex"
5099bin "ex"
5100default_subcommand "run"
5101cmd "run" {
5102 arg "<task>"
5103}
5104mount run="exit 1"
5105"#
5106 .parse()
5107 .unwrap();
5108
5109 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
5110 assert!(
5111 !out.cmd.subcommands.contains_key("discovered"),
5112 "a completion must not offer what execution will not route"
5113 );
5114
5115 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5116 assert_eq!(out.cmd.name, "run");
5117 }
5118
5119 #[cfg(unix)]
5120 #[test]
5121 fn a_default_subcommand_outranks_discovery() {
5122 let spec: Spec = r#"
5125name "ex"
5126bin "ex"
5127default_subcommand "run"
5128cmd "run" {
5129 arg "<task>"
5130}
5131mount run="exit 1"
5132"#
5133 .parse()
5134 .unwrap();
5135
5136 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5137 assert_eq!(out.cmd.name, "run");
5138 }
5139
5140 #[cfg(unix)]
5141 #[test]
5142 fn a_mount_may_ask_to_outrank_the_default() {
5143 let spec: Spec = r#"
5146name "ex"
5147bin "ex"
5148default_subcommand "run"
5149cmd "run" {
5150 arg "<task>"
5151}
5152mount run="echo 'cmd \"discovered\"'" overrides_default=#true
5153"#
5154 .parse()
5155 .unwrap();
5156
5157 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
5158 assert_eq!(out.cmd.name, "discovered");
5159
5160 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
5162 assert_eq!(out.cmd.name, "run");
5163 }
5164
5165 #[cfg(unix)]
5166 #[test]
5167 fn a_flag_does_not_run_the_mount() {
5168 let spec: Spec = r#"
5172name "ex"
5173bin "ex"
5174flag "--verbose"
5175cmd "declared"
5176mount run="exit 1"
5177"#
5178 .parse()
5179 .unwrap();
5180
5181 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
5182 assert_eq!(out.cmd.name, "ex");
5183 }
5184
5185 #[cfg(unix)]
5186 #[test]
5187 fn a_declared_subcommand_does_not_run_the_mount() {
5188 let spec: Spec = r#"
5192name "ex"
5193bin "ex"
5194cmd "declared"
5195mount run="exit 1"
5196"#
5197 .parse()
5198 .unwrap();
5199
5200 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
5201 assert_eq!(out.cmd.name, "declared");
5202 }
5203
5204 #[test]
5205 fn a_root_mount_survives_being_written_out() {
5206 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
5207 .parse()
5208 .unwrap();
5209 assert_eq!(spec.cmd.mounts.len(), 1);
5210
5211 let reparsed: Spec = spec.to_string().parse().unwrap();
5212 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
5213 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
5214 }
5215
5216 #[test]
5217 fn test_mount_prefix_applies_flag_overrides() {
5218 let stdin = Arc::new(
5219 SpecFlag::builder()
5220 .name("stdin")
5221 .long("stdin")
5222 .global(true)
5223 .build(),
5224 );
5225 let file = Arc::new(
5226 SpecFlag::builder()
5227 .name("file")
5228 .long("file")
5229 .arg(SpecArg::builder().name("file").build())
5230 .global(true)
5231 .overrides_with(vec!["--stdin".to_string()])
5232 .build(),
5233 );
5234 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
5235
5236 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
5237 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
5238
5239 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
5240 }
5241
5242 #[test]
5243 fn test_flag_override_suppresses_env_value() {
5244 let spec: Spec = r#"
5245flag "--stdin" env="USE_STDIN"
5246flag "--file <file>" overrides="--stdin"
5247 "#
5248 .parse()
5249 .unwrap();
5250
5251 let parsed = parse_with_env(
5252 &spec,
5253 &["test", "--file", "input.txt"],
5254 &[("USE_STDIN", "true")],
5255 )
5256 .unwrap();
5257 assert_eq!(parsed.flags.len(), 1);
5258 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5259 }
5260
5261 #[test]
5262 fn test_flag_override_suppresses_required_check() {
5263 let spec: Spec = r#"
5264flag "--stdin" required=#true
5265flag "--file <file>" overrides="--stdin"
5266 "#
5267 .parse()
5268 .unwrap();
5269
5270 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5271 assert_eq!(parsed.flags.len(), 1);
5272 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
5273 }
5274
5275 #[test]
5276 fn test_flag_required_if() {
5277 let spec: Spec = r#"
5278flag "--dir <dir>"
5279flag "--file <file>" required_if="--dir"
5280 "#
5281 .parse()
5282 .unwrap();
5283
5284 parse(&spec, &input(&["test"])).unwrap();
5285 assert_parse_err(
5286 parse(&spec, &input(&["test", "--dir", "src"])),
5287 "Missing required flag: --file <file>",
5288 );
5289 parse(
5290 &spec,
5291 &input(&["test", "--dir", "src", "--file", "input.txt"]),
5292 )
5293 .unwrap();
5294 }
5295
5296 #[test]
5297 fn test_flag_required_unless() {
5298 let spec: Spec = r#"
5299flag "--stdin"
5300flag "--file <file>" required_unless="--stdin"
5301 "#
5302 .parse()
5303 .unwrap();
5304
5305 assert_parse_err(
5306 parse(&spec, &input(&["test"])),
5307 "Missing required flag: --file <file>",
5308 );
5309 parse(&spec, &input(&["test", "--stdin"])).unwrap();
5310 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
5311 }
5312
5313 #[test]
5314 fn complete_required_relationship_truth_tables() {
5315 let spec: Spec = r#"
5316name "ex"
5317bin "ex"
5318flag "--mode <mode>"
5319flag "--scope <scope>"
5320flag "--token <token>" {
5321 required_if_eq "--mode" "remote"
5322}
5323flag "--approval <approval>" {
5324 required_if_eq_all "--mode" "remote" "--scope" "global"
5325}
5326flag "--input <input>" {
5327 required_unless "--stdin" "--file"
5328}
5329flag "--checksum <checksum>" {
5330 required_unless_all "--stdin" "--file"
5331}
5332flag "--stdin"
5333flag "--file <file>"
5334arg "[request]" {
5335 requires "--mode" "--scope"
5336}
5337"#
5338 .parse()
5339 .unwrap();
5340 let parse_args = |args: &[&str]| {
5341 parse(
5342 &spec,
5343 &args
5344 .iter()
5345 .map(|arg| (*arg).to_string())
5346 .collect::<Vec<_>>(),
5347 )
5348 };
5349
5350 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
5351 assert!(parse_args(&[
5352 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
5353 ])
5354 .is_err());
5355 parse_args(&[
5356 "ex",
5357 "--mode",
5358 "remote",
5359 "--token",
5360 "secret",
5361 "--scope",
5362 "global",
5363 "--approval",
5364 "yes",
5365 "--stdin",
5366 "--file",
5367 "in",
5368 ])
5369 .unwrap();
5370 parse_args(&[
5371 "ex",
5372 "--mode",
5373 "local",
5374 "--scope",
5375 "project",
5376 "--stdin",
5377 "--checksum",
5378 "sum",
5379 "request.json",
5380 ])
5381 .unwrap();
5382
5383 let reparsed: Spec = spec.to_string().parse().unwrap();
5384 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
5385 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
5386 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
5387 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
5388 }
5389
5390 #[test]
5391 fn test_conditional_requirements_treat_env_as_explicit() {
5392 let spec: Spec = r#"
5393flag "--dir <dir>" env="INPUT_DIR"
5394flag "--stdin" env="USE_STDIN"
5395flag "--file <file>" required_if="--dir" required_unless="--stdin"
5396 "#
5397 .parse()
5398 .unwrap();
5399
5400 assert_parse_err(
5401 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5402 "Missing required flag: --file <file>",
5403 );
5404 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5405 }
5406
5407 #[test]
5408 fn test_custom_env_does_not_fall_back_to_process_env() {
5409 assert!(std::env::var("PATH").is_ok());
5410 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5411
5412 assert_parse_err(
5413 parse_with_env(&spec, &["test"], &[]),
5414 "Missing required flag: --file <file>",
5415 );
5416 }
5417
5418 #[test]
5419 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5420 let spec: Spec = r#"
5421flag "--dir <dir>" default="src"
5422flag "--file <file>" required_if="--dir"
5423 "#
5424 .parse()
5425 .unwrap();
5426
5427 parse(&spec, &input(&["test"])).unwrap();
5428 }
5429
5430 #[test]
5431 fn test_conditional_requirements_see_overridden_flags_as_absent() {
5432 let spec: Spec = r#"
5433flag "--stdin"
5434flag "--dir <dir>" overrides="--stdin"
5435flag "--file <file>" required_unless="--stdin"
5436 "#
5437 .parse()
5438 .unwrap();
5439
5440 assert_parse_err(
5441 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5442 "Missing required flag: --file <file>",
5443 );
5444 }
5445
5446 #[test]
5447 fn short_flag_is_one_character_not_one_byte() {
5448 let spec = spec_with_flag(
5452 SpecFlag::builder()
5453 .short('磨')
5454 .long("polish")
5455 .arg(SpecArg::builder().name("opt").build())
5456 .build(),
5457 );
5458 let attached = Parser::new(&spec)
5459 .parse(&input(&["test", "-磨VALUE"]))
5460 .unwrap();
5461 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5462 let detached = Parser::new(&spec)
5463 .parse(&input(&["test", "-磨", "V"]))
5464 .unwrap();
5465 assert_eq!(flag_string_value(&detached, "polish"), "V");
5466 }
5467
5468 #[test]
5469 fn test_as_env() {
5470 let cmd = SpecCommand::builder()
5471 .name("test")
5472 .arg(SpecArg::builder().name("arg").build())
5473 .flag(SpecFlag::builder().long("flag").build())
5474 .flag(
5475 SpecFlag::builder()
5476 .long("force")
5477 .negate("--no-force")
5478 .build(),
5479 )
5480 .build();
5481 let spec = Spec {
5482 name: "test".to_string(),
5483 bin: "test".to_string(),
5484 cmd,
5485 ..Default::default()
5486 };
5487 let input = vec![
5488 "test".to_string(),
5489 "--flag".to_string(),
5490 "--no-force".to_string(),
5491 ];
5492 let parsed = parse(&spec, &input).unwrap();
5493 let env = parsed.as_env();
5494 assert_eq!(env.len(), 2);
5495 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
5496 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
5497 }
5498
5499 #[test]
5500 fn test_arg_env_var() {
5501 let cmd = SpecCommand::builder()
5502 .name("test")
5503 .arg(
5504 SpecArg::builder()
5505 .name("input")
5506 .env("TEST_ARG_INPUT")
5507 .required(true)
5508 .build(),
5509 )
5510 .build();
5511 let spec = Spec {
5512 name: "test".to_string(),
5513 bin: "test".to_string(),
5514 cmd,
5515 ..Default::default()
5516 };
5517
5518 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
5520
5521 let input = vec!["test".to_string()];
5522 let parsed = parse(&spec, &input).unwrap();
5523
5524 assert_eq!(parsed.args.len(), 1);
5525 let arg = parsed.args.keys().next().unwrap();
5526 assert_eq!(arg.name, "input");
5527 let value = parsed.args.values().next().unwrap();
5528 assert_eq!(value.to_string(), "test_file.txt");
5529
5530 std::env::remove_var("TEST_ARG_INPUT");
5532 }
5533
5534 #[test]
5535 fn test_flag_env_var_with_arg() {
5536 let cmd = SpecCommand::builder()
5537 .name("test")
5538 .flag(
5539 SpecFlag::builder()
5540 .long("output")
5541 .env("TEST_FLAG_OUTPUT")
5542 .arg(SpecArg::builder().name("file").build())
5543 .build(),
5544 )
5545 .build();
5546 let spec = Spec {
5547 name: "test".to_string(),
5548 bin: "test".to_string(),
5549 cmd,
5550 ..Default::default()
5551 };
5552
5553 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5555
5556 let input = vec!["test".to_string()];
5557 let parsed = parse(&spec, &input).unwrap();
5558
5559 assert_eq!(parsed.flags.len(), 1);
5560 let flag = parsed.flags.keys().next().unwrap();
5561 assert_eq!(flag.name, "output");
5562 let value = parsed.flags.values().next().unwrap();
5563 assert_eq!(value.to_string(), "output.txt");
5564
5565 std::env::remove_var("TEST_FLAG_OUTPUT");
5567 }
5568
5569 #[test]
5570 fn test_flag_env_var_boolean() {
5571 let cmd = SpecCommand::builder()
5572 .name("test")
5573 .flag(
5574 SpecFlag::builder()
5575 .long("verbose")
5576 .env("TEST_FLAG_VERBOSE")
5577 .build(),
5578 )
5579 .build();
5580 let spec = Spec {
5581 name: "test".to_string(),
5582 bin: "test".to_string(),
5583 cmd,
5584 ..Default::default()
5585 };
5586
5587 std::env::set_var("TEST_FLAG_VERBOSE", "true");
5589
5590 let input = vec!["test".to_string()];
5591 let parsed = parse(&spec, &input).unwrap();
5592
5593 assert_eq!(parsed.flags.len(), 1);
5594 let flag = parsed.flags.keys().next().unwrap();
5595 assert_eq!(flag.name, "verbose");
5596 let value = parsed.flags.values().next().unwrap();
5597 assert_eq!(value.to_string(), "true");
5598
5599 std::env::remove_var("TEST_FLAG_VERBOSE");
5601 }
5602
5603 #[test]
5604 fn test_env_var_precedence() {
5605 let cmd = SpecCommand::builder()
5607 .name("test")
5608 .arg(
5609 SpecArg::builder()
5610 .name("input")
5611 .env("TEST_PRECEDENCE_INPUT")
5612 .required(true)
5613 .build(),
5614 )
5615 .build();
5616 let spec = Spec {
5617 name: "test".to_string(),
5618 bin: "test".to_string(),
5619 cmd,
5620 ..Default::default()
5621 };
5622
5623 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5625
5626 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5627 let parsed = parse(&spec, &input).unwrap();
5628
5629 assert_eq!(parsed.args.len(), 1);
5630 let value = parsed.args.values().next().unwrap();
5631 assert_eq!(value.to_string(), "cli_file.txt");
5633
5634 std::env::remove_var("TEST_PRECEDENCE_INPUT");
5636 }
5637
5638 #[test]
5639 fn test_flag_var_true_with_single_default() {
5640 let cmd = SpecCommand::builder()
5642 .name("test")
5643 .flag(
5644 SpecFlag::builder()
5645 .long("foo")
5646 .var(true)
5647 .arg(SpecArg::builder().name("foo").build())
5648 .default_value("bar")
5649 .build(),
5650 )
5651 .build();
5652 let spec = Spec {
5653 name: "test".to_string(),
5654 bin: "test".to_string(),
5655 cmd,
5656 ..Default::default()
5657 };
5658
5659 let input = vec!["test".to_string()];
5661 let parsed = parse(&spec, &input).unwrap();
5662
5663 assert_eq!(parsed.flags.len(), 1);
5664 let flag = parsed.flags.keys().next().unwrap();
5665 assert_eq!(flag.name, "foo");
5666 let value = parsed.flags.values().next().unwrap();
5667 match value {
5669 ParseValue::MultiString(v) => {
5670 assert_eq!(v.len(), 1);
5671 assert_eq!(v[0], "bar");
5672 }
5673 _ => panic!("Expected MultiString, got {:?}", value),
5674 }
5675 }
5676
5677 #[test]
5678 fn test_flag_var_true_with_multiple_defaults() {
5679 let cmd = SpecCommand::builder()
5681 .name("test")
5682 .flag(
5683 SpecFlag::builder()
5684 .long("foo")
5685 .var(true)
5686 .arg(SpecArg::builder().name("foo").build())
5687 .default_values(["xyz", "bar"])
5688 .build(),
5689 )
5690 .build();
5691 let spec = Spec {
5692 name: "test".to_string(),
5693 bin: "test".to_string(),
5694 cmd,
5695 ..Default::default()
5696 };
5697
5698 let input = vec!["test".to_string()];
5700 let parsed = parse(&spec, &input).unwrap();
5701
5702 assert_eq!(parsed.flags.len(), 1);
5703 let value = parsed.flags.values().next().unwrap();
5704 match value {
5706 ParseValue::MultiString(v) => {
5707 assert_eq!(v.len(), 2);
5708 assert_eq!(v[0], "xyz");
5709 assert_eq!(v[1], "bar");
5710 }
5711 _ => panic!("Expected MultiString, got {:?}", value),
5712 }
5713 }
5714
5715 #[test]
5716 fn test_flag_var_false_with_default_remains_string() {
5717 let cmd = SpecCommand::builder()
5719 .name("test")
5720 .flag(
5721 SpecFlag::builder()
5722 .long("foo")
5723 .var(false) .arg(SpecArg::builder().name("foo").build())
5725 .default_value("bar")
5726 .build(),
5727 )
5728 .build();
5729 let spec = Spec {
5730 name: "test".to_string(),
5731 bin: "test".to_string(),
5732 cmd,
5733 ..Default::default()
5734 };
5735
5736 let input = vec!["test".to_string()];
5738 let parsed = parse(&spec, &input).unwrap();
5739
5740 assert_eq!(parsed.flags.len(), 1);
5741 let value = parsed.flags.values().next().unwrap();
5742 match value {
5744 ParseValue::String(s) => {
5745 assert_eq!(s, "bar");
5746 }
5747 _ => panic!("Expected String, got {:?}", value),
5748 }
5749 }
5750
5751 #[test]
5752 fn test_arg_var_true_with_single_default() {
5753 let cmd = SpecCommand::builder()
5755 .name("test")
5756 .arg(
5757 SpecArg::builder()
5758 .name("files")
5759 .var(true)
5760 .default_value("default.txt")
5761 .required(false)
5762 .build(),
5763 )
5764 .build();
5765 let spec = Spec {
5766 name: "test".to_string(),
5767 bin: "test".to_string(),
5768 cmd,
5769 ..Default::default()
5770 };
5771
5772 let input = vec!["test".to_string()];
5774 let parsed = parse(&spec, &input).unwrap();
5775
5776 assert_eq!(parsed.args.len(), 1);
5777 let value = parsed.args.values().next().unwrap();
5778 match value {
5780 ParseValue::MultiString(v) => {
5781 assert_eq!(v.len(), 1);
5782 assert_eq!(v[0], "default.txt");
5783 }
5784 _ => panic!("Expected MultiString, got {:?}", value),
5785 }
5786 }
5787
5788 #[test]
5789 fn test_arg_var_true_with_multiple_defaults() {
5790 let cmd = SpecCommand::builder()
5792 .name("test")
5793 .arg(
5794 SpecArg::builder()
5795 .name("files")
5796 .var(true)
5797 .default_values(["file1.txt", "file2.txt"])
5798 .required(false)
5799 .build(),
5800 )
5801 .build();
5802 let spec = Spec {
5803 name: "test".to_string(),
5804 bin: "test".to_string(),
5805 cmd,
5806 ..Default::default()
5807 };
5808
5809 let input = vec!["test".to_string()];
5811 let parsed = parse(&spec, &input).unwrap();
5812
5813 assert_eq!(parsed.args.len(), 1);
5814 let value = parsed.args.values().next().unwrap();
5815 match value {
5817 ParseValue::MultiString(v) => {
5818 assert_eq!(v.len(), 2);
5819 assert_eq!(v[0], "file1.txt");
5820 assert_eq!(v[1], "file2.txt");
5821 }
5822 _ => panic!("Expected MultiString, got {:?}", value),
5823 }
5824 }
5825
5826 #[test]
5827 fn test_arg_var_false_with_default_remains_string() {
5828 let cmd = SpecCommand::builder()
5830 .name("test")
5831 .arg(
5832 SpecArg::builder()
5833 .name("file")
5834 .var(false)
5835 .default_value("default.txt")
5836 .required(false)
5837 .build(),
5838 )
5839 .build();
5840 let spec = Spec {
5841 name: "test".to_string(),
5842 bin: "test".to_string(),
5843 cmd,
5844 ..Default::default()
5845 };
5846
5847 let input = vec!["test".to_string()];
5849 let parsed = parse(&spec, &input).unwrap();
5850
5851 assert_eq!(parsed.args.len(), 1);
5852 let value = parsed.args.values().next().unwrap();
5853 match value {
5855 ParseValue::String(s) => {
5856 assert_eq!(s, "default.txt");
5857 }
5858 _ => panic!("Expected String, got {:?}", value),
5859 }
5860 }
5861
5862 #[test]
5863 fn test_scalar_defaults_validate_only_first_default_choice() {
5864 let specs = [
5865 spec_with_arg(
5866 SpecArg::builder()
5867 .name("env")
5868 .var(false)
5869 .default_values(["dev", "prod"])
5870 .choices(["dev"])
5871 .required(false)
5872 .build(),
5873 ),
5874 spec_with_flag(
5875 SpecFlag::builder()
5876 .long("env")
5877 .arg(
5878 SpecArg::builder()
5879 .name("env")
5880 .default_values(["dev", "prod"])
5881 .choices(["dev"])
5882 .build(),
5883 )
5884 .build(),
5885 ),
5886 ];
5887
5888 for spec in specs {
5889 let parsed = parse(&spec, &input(&["test"])).unwrap();
5890 assert_eq!(first_string_value(&parsed), "dev");
5891 }
5892 }
5893
5894 #[test]
5895 fn a_delimiter_turns_one_word_into_several_values() {
5896 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5897 .parse()
5898 .unwrap();
5899
5900 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5901 let multi = |value: &ParseValue| match value {
5902 ParseValue::MultiString(values) => values.clone(),
5903 other => panic!("expected several values, got {other:?}"),
5904 };
5905 let tags = parsed
5906 .flags
5907 .iter()
5908 .find(|(f, _)| f.name == "tags")
5909 .map(|(_, v)| v)
5910 .unwrap();
5911 assert_eq!(multi(tags), vec!["a", "b", "c"]);
5912 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5913 }
5914
5915 #[test]
5916 fn a_positional_splits_before_its_choices_are_asked() {
5917 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
5921 .parse()
5922 .unwrap();
5923
5924 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5925
5926 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5927 let message = err.to_string();
5928 assert!(message.contains("nowhere"), "{message}");
5929 assert!(
5930 !message.contains("src:nowhere"),
5931 "the bad half should be named, not the whole word: {message}"
5932 );
5933 }
5934
5935 #[test]
5936 fn a_split_value_is_counted_and_judged_as_values() {
5937 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
5941 .parse()
5942 .unwrap();
5943
5944 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5945 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5946 assert!(err.to_string().contains("staging"), "{err}");
5947 assert!(
5948 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5949 "three values should breach var_max=2"
5950 );
5951 }
5952
5953 #[test]
5954 fn a_split_bound_counts_one_occurrence_at_a_time() {
5955 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
5960 .parse()
5961 .unwrap();
5962
5963 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5964 assert!(
5965 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5966 "three values out of one word is still three values"
5967 );
5968 parse(
5971 &spec,
5972 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5973 )
5974 .expect("two per occurrence, twice, is within the bound");
5975 }
5976
5977 #[test]
5978 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5979 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5980 .parse()
5981 .unwrap();
5982
5983 parse(
5984 &spec,
5985 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5986 )
5987 .expect("each occurrence satisfies the bound independently");
5988
5989 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5990 assert!(
5991 error
5992 .to_string()
5993 .contains("requires at least 2 value(s), got 1"),
5994 "{error:?}"
5995 );
5996 }
5997
5998 #[test]
5999 fn an_exclusive_flag_has_to_be_alone() {
6000 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
6001 .parse()
6002 .unwrap();
6003
6004 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
6005
6006 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
6008 assert!(err.to_string().contains("on its own"), "{err}");
6009
6010 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
6013 assert!(err.to_string().contains("on its own"), "{err}");
6014
6015 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
6017 }
6018
6019 #[test]
6020 fn an_exclusive_flag_conflicts_with_clause_arguments() {
6021 let spec: Spec = r#"name "ex"
6022bin "ex"
6023flag "--dump" exclusive=#true
6024clause "tasks" separator=":::" {
6025 arg "<task>"
6026}
6027"#
6028 .parse()
6029 .unwrap();
6030
6031 let err = parse(&spec, &input(&["ex", "--dump", "lint"])).unwrap_err();
6032 assert!(err.to_string().contains("on its own"), "{err}");
6033 }
6034
6035 #[test]
6036 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
6037 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
6040 .parse()
6041 .unwrap();
6042
6043 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
6044 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
6045 }
6046
6047 #[test]
6048 fn an_exclusive_flag_bypasses_required_siblings() {
6049 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
6050 .parse()
6051 .unwrap();
6052
6053 parse(&spec, &input(&["ex", "--dump"]))
6054 .expect("exclusive is the command's requiredness escape");
6055 assert!(parse(
6056 &spec,
6057 &input(&["ex", "--dump", "--out", "somewhere", "target"])
6058 )
6059 .is_err());
6060 }
6061
6062 #[test]
6063 fn an_environment_value_counts_for_an_exclusive_flag() {
6064 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
6065 .parse()
6066 .unwrap();
6067
6068 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
6069 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
6070 }
6071
6072 #[test]
6073 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
6074 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
6075 .parse()
6076 .unwrap();
6077
6078 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
6079 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
6080 }
6081
6082 #[test]
6083 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
6084 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6085 .parse()
6086 .unwrap();
6087
6088 parse(&spec, &input(&["ex", "run", "--clean"]))
6089 .expect("the child flag is alone within the child command");
6090 assert!(
6091 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6092 "the parent flag still conflicts with selecting the child"
6093 );
6094 }
6095
6096 #[test]
6097 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
6098 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6099 .parse()
6100 .unwrap();
6101
6102 parse(&spec, &input(&["ex", "run", "--clean"]))
6103 .expect("the child-local exclusive flag is alone inside the child command");
6104 assert!(
6105 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
6106 "the ancestor spelling still conflicts with selecting the child"
6107 );
6108 }
6109
6110 #[test]
6111 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
6112 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
6113 .parse()
6114 .unwrap();
6115
6116 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
6117 assert!(
6118 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
6119 "the distinct parent declaration is still company despite sharing a name"
6120 );
6121 }
6122
6123 #[test]
6124 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
6125 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6126 .parse()
6127 .unwrap();
6128
6129 parse(&spec, &input(&["ex", "run", "--clean"]))
6130 .expect("the child exclusive flag is valid alone");
6131 assert!(
6132 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6133 "merging with the inherited global must not discard child exclusivity"
6134 );
6135 }
6136
6137 #[test]
6138 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
6139 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6140 .parse()
6141 .unwrap();
6142
6143 parse(&spec, &input(&["ex", "run", "--clean"]))
6144 .expect("the typed long form belongs to the child declaration");
6145 assert!(
6146 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6147 "the inherited short form still belongs to the ancestor"
6148 );
6149 assert!(
6150 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
6151 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
6152 );
6153 }
6154
6155 #[test]
6156 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
6157 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
6158 .parse()
6159 .unwrap();
6160
6161 parse(&spec, &input(&["ex", "run", "--clean"]))
6162 .expect("the child's spelling does not activate the orphan ancestor alias");
6163 assert!(
6164 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6165 "the inherited short alias still belongs to the ancestor exclusive flag"
6166 );
6167 }
6168
6169 #[test]
6170 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
6171 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"
6172 .parse()
6173 .unwrap();
6174
6175 assert!(
6176 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
6177 "the inherited negated alias still belongs to the ancestor exclusive flag"
6178 );
6179 }
6180
6181 #[test]
6182 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
6183 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"
6188 .parse()
6189 .unwrap();
6190
6191 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
6192 assert!(
6193 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6194 "one unrelated alias collision cannot disown the child from its own flag"
6195 );
6196 }
6197
6198 #[test]
6199 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
6200 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6206 .parse()
6207 .unwrap();
6208 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
6209 .expect("the ancestor owns this occurrence, and it is not exclusive");
6210 assert!(
6211 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
6212 "after the subcommand word the child's declaration is in scope, and it is exclusive"
6213 );
6214
6215 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
6216 .parse()
6217 .unwrap();
6218 assert!(
6219 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
6220 "the same rule, with an exclusive ancestor: selecting the child is company for it"
6221 );
6222 }
6223
6224 #[test]
6225 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
6226 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
6230 .parse()
6231 .unwrap();
6232
6233 assert!(
6234 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
6235 "the orphan ancestor alias is still the ancestor's exclusive flag"
6236 );
6237 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
6238 .expect("the child's own spelling drops the exclusivity the child did not restate");
6239 }
6240
6241 #[test]
6242 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
6243 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
6247 .parse()
6248 .unwrap();
6249
6250 assert!(
6251 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
6252 "the child spelling is exclusive whatever it was typed beside"
6253 );
6254 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
6255 .expect("the ancestor's own spelling was never exclusive");
6256 }
6257
6258 #[test]
6259 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
6260 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"
6264 .parse()
6265 .unwrap();
6266
6267 assert!(
6268 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
6269 "the child added exclusivity the environment value has to honor"
6270 );
6271
6272 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"
6273 .parse()
6274 .unwrap();
6275
6276 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
6277 .expect("the child dropped the exclusivity, and the environment value follows it");
6278 }
6279
6280 #[test]
6281 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
6282 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"
6285 .parse()
6286 .unwrap();
6287
6288 parse(&spec, &input(&["ex", "run", "--clean"]))
6289 .expect("a merged child exclusive flag is still the command's requiredness escape");
6290 }
6291
6292 #[test]
6293 fn a_group_allows_one_member_and_refuses_two() {
6294 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
6295 .parse()
6296 .unwrap();
6297
6298 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
6300 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
6301
6302 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
6303 assert!(err.to_string().contains("group input"), "{err}");
6304 }
6305
6306 #[test]
6307 fn positional_selectors_work_in_conflicts_and_groups() {
6308 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
6309 .parse()
6310 .unwrap();
6311 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
6312 .expect("the flag alone is valid");
6313 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
6314 assert!(parse(
6315 &conflicts,
6316 &input(&["ex", "--from-file", "vars.env", "literal"])
6317 )
6318 .is_err());
6319
6320 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
6321 .parse()
6322 .unwrap();
6323 assert!(parse(
6324 &positional_source,
6325 &input(&["ex", "--from-file", "vars.env", "literal"])
6326 )
6327 .is_err());
6328
6329 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
6330 .parse()
6331 .unwrap();
6332 assert!(parse(&group, &input(&["ex"])).is_err());
6333 parse(&group, &input(&["ex", "target-name"]))
6334 .expect("a positional satisfies a required group");
6335 assert!(parse(
6336 &group,
6337 &input(&["ex", "--file", "input.txt", "target-name"])
6338 )
6339 .is_err());
6340 }
6341
6342 #[test]
6343 fn a_required_group_needs_one_of_its_members() {
6344 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6345 .parse()
6346 .unwrap();
6347
6348 let err = parse(&spec, &input(&["ex"])).unwrap_err();
6349 assert!(err.to_string().contains("--file, --url"), "{err}");
6352 assert!(err.to_string().contains("input"), "{err}");
6353
6354 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
6355 }
6356
6357 #[test]
6358 fn a_multiple_group_only_polices_requiredness() {
6359 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
6362 .parse()
6363 .unwrap();
6364
6365 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
6366 assert!(parse(&spec, &input(&["ex"])).is_err());
6367 }
6368
6369 #[test]
6370 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
6371 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6377 .parse()
6378 .unwrap();
6379
6380 parse(&spec, &input(&["ex"])).expect("the default fills the group");
6381 parse(&spec, &input(&["ex", "--url", "u"]))
6382 .expect("the default must not conflict with the flag the user typed");
6383 }
6384
6385 #[test]
6386 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
6387 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
6390 .parse()
6391 .unwrap();
6392
6393 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
6394 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
6395
6396 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
6398 assert!(err.to_string().contains("group input"), "{err}");
6399 }
6400
6401 #[test]
6402 fn a_group_reads_the_environment_as_given() {
6403 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
6406 .parse()
6407 .unwrap();
6408
6409 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
6410 }
6411
6412 #[test]
6413 fn a_requirement_names_the_flag_that_is_missing() {
6414 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6417 .parse()
6418 .unwrap();
6419
6420 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6421 assert!(
6422 err.to_string().contains("format"),
6423 "the missing flag should be named: {err}"
6424 );
6425
6426 for words in [
6428 &["ex", "--out", "a.txt", "--format", "json"][..],
6429 &["ex", "--format", "json", "--out", "a.txt"][..],
6430 ] {
6431 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6432 }
6433
6434 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6437 }
6438
6439 #[test]
6440 fn a_value_activates_only_its_conditional_requirement() {
6441 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"
6442 .parse()
6443 .unwrap();
6444
6445 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6446 .expect("an unrelated value requires nothing");
6447
6448 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6449 assert!(key.to_string().contains("key"), "{key}");
6450 parse(
6451 &spec,
6452 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6453 )
6454 .expect("the matching requirement is satisfied");
6455
6456 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6457 assert!(token.to_string().contains("token"), "{token}");
6458 }
6459
6460 #[test]
6461 fn conditional_requirements_read_explicit_env_but_not_defaults() {
6462 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"
6463 .parse()
6464 .unwrap();
6465 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6466 assert!(err.to_string().contains("key"), "{err}");
6467
6468 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"
6469 .parse()
6470 .unwrap();
6471 parse(&from_default, &input(&["ex"]))
6472 .expect("a default is not an explicit conditional value");
6473 }
6474
6475 #[test]
6476 fn command_line_values_override_env_for_conditional_requirements() {
6477 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6478 .parse()
6479 .unwrap();
6480
6481 parse_with_env(
6482 &spec,
6483 &["ex", "--config", "ordinary.toml"],
6484 &[("EX_CONFIG", "special.toml")],
6485 )
6486 .expect("the command-line value takes precedence over the environment");
6487 }
6488
6489 #[test]
6490 fn conditional_requirements_normalize_boolean_env_values() {
6491 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
6492 .parse()
6493 .unwrap();
6494
6495 for value in ["1", "true", "True", "TRUE"] {
6496 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
6497 assert!(err.to_string().contains("key"), "{value}: {err}");
6498 }
6499 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
6500 .expect("a false environment value does not activate a true condition");
6501 }
6502
6503 #[test]
6504 fn a_default_satisfies_a_requirement() {
6505 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
6509 .parse()
6510 .unwrap();
6511
6512 parse(&spec, &input(&["ex", "--out", "a.txt"]))
6513 .expect("a defaulted flag is not a missing one");
6514 }
6515
6516 #[test]
6517 fn a_present_flag_binds_a_conditional_default() {
6518 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6519 .parse()
6520 .unwrap();
6521
6522 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
6523 assert_eq!(
6524 with.as_env().get("usage_bin_names").map(String::as_str),
6525 Some("true")
6526 );
6527
6528 let without = parse(&spec, &input(&["ex"])).unwrap();
6529 assert!(
6530 !without.as_env().contains_key("usage_bin_names"),
6531 "IsPresent does nothing when the selector is absent"
6532 );
6533 }
6534
6535 #[test]
6536 fn an_equals_condition_binds_a_conditional_default() {
6537 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
6538 .parse()
6539 .unwrap();
6540
6541 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6542 assert_eq!(
6543 json.as_env().get("usage_style").map(String::as_str),
6544 Some("pretty")
6545 );
6546 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6547 assert!(!yaml.as_env().contains_key("usage_style"));
6548 }
6549
6550 #[test]
6551 fn an_equals_condition_reads_a_negated_flag() {
6552 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6553 .parse()
6554 .unwrap();
6555
6556 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6557 assert_eq!(
6558 off.as_env().get("usage_pretty").map(String::as_str),
6559 Some("true")
6560 );
6561 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6562 assert!(
6563 !on.as_env().contains_key("usage_pretty"),
6564 "--json is true, so when=false should miss"
6565 );
6566 }
6567
6568 #[test]
6569 fn the_first_matching_conditional_default_wins() {
6570 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"
6571 .parse()
6572 .unwrap();
6573
6574 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6575 assert_eq!(
6576 out.as_env().get("usage_style").map(String::as_str),
6577 Some("compact")
6578 );
6579 }
6580
6581 #[test]
6582 fn argv_and_env_suppress_a_conditional_default() {
6583 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6584 .parse()
6585 .unwrap();
6586
6587 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6588 assert_eq!(
6589 from_env.as_env().get("usage_bin_names").map(String::as_str),
6590 Some("false"),
6591 "the target's environment wins over default_if"
6592 );
6593 }
6594
6595 #[test]
6596 fn a_sibling_env_activates_a_conditional_default() {
6597 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6598 .parse()
6599 .unwrap();
6600
6601 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6602 assert_eq!(
6603 out.as_env().get("usage_bin_names").map(String::as_str),
6604 Some("true")
6605 );
6606 }
6607
6608 #[test]
6609 fn a_default_does_not_activate_a_conditional_default() {
6610 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6614 .parse()
6615 .unwrap();
6616
6617 let out = parse(&spec, &input(&["ex"])).unwrap();
6618 assert_eq!(
6619 out.as_env().get("usage_json").map(String::as_str),
6620 Some("true")
6621 );
6622 assert!(
6623 !out.as_env().contains_key("usage_pretty"),
6624 "a default is not an explicit value for default_if"
6625 );
6626 }
6627
6628 #[test]
6629 fn a_conditional_default_does_not_activate_requires_if() {
6630 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"
6631 .parse()
6632 .unwrap();
6633
6634 parse(&spec, &input(&["ex", "--json"]))
6635 .expect("a default_if value is not explicit for requires_if");
6636 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6637 }
6638
6639 #[test]
6640 fn a_conditional_default_satisfies_a_requirement() {
6641 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6642 .parse()
6643 .unwrap();
6644
6645 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6646 .expect("default_if fills the required flag");
6647 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6648 }
6649
6650 #[test]
6651 fn an_environment_value_satisfies_a_requirement() {
6652 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6653 .parse()
6654 .unwrap();
6655
6656 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6657 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6658 .expect("the environment supplies it");
6659 }
6660
6661 #[test]
6662 fn a_requirement_is_satisfied_by_a_short_form() {
6663 let spec: Spec =
6667 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6668 .parse()
6669 .unwrap();
6670
6671 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6672
6673 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6674 assert!(err.to_string().contains("key"), "{err}");
6675 }
6676
6677 #[test]
6678 fn conflicting_flags_are_rejected_in_either_order() {
6679 let spec: Spec =
6683 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6684 .parse()
6685 .unwrap();
6686
6687 for words in [
6688 &["ex", "--file", "a.txt", "--stdin"][..],
6689 &["ex", "--stdin", "--file", "a.txt"][..],
6690 ] {
6691 let err = parse(&spec, &input(words)).unwrap_err();
6692 assert!(
6693 err.to_string().contains("conflicts with --stdin"),
6694 "{words:?} should be refused: {err}"
6695 );
6696 }
6697
6698 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6700 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6701 }
6702
6703 #[test]
6704 fn unknown_flags_are_values_by_default() {
6705 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6708 .parse()
6709 .unwrap();
6710 let out = parse(
6711 &spec,
6712 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6713 )
6714 .unwrap();
6715 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6716 assert_eq!(out.args[rest].to_string(), "--wat x");
6717 }
6718
6719 #[test]
6720 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6721 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6722 .parse()
6723 .unwrap();
6724 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6725 .expect("a repeat is a correction by default");
6726 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6727 assert_eq!(out.flags[jobs].to_string(), "2");
6728 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6729 .expect("switches use the same default");
6730
6731 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6732 .parse()
6733 .unwrap();
6734 for words in [
6735 &["ex", "--jobs", "1", "--jobs", "2"][..],
6736 &["ex", "--verbose", "--verbose"][..],
6737 ] {
6738 let err = parse(&strict, &input(words)).unwrap_err();
6739 assert!(
6740 err.to_string().contains("cannot be used multiple times"),
6741 "{err}"
6742 );
6743 }
6744
6745 let reparsed: Spec = strict.to_string().parse().unwrap();
6746 assert!(!reparsed.cmd.args_override_self);
6747 }
6748
6749 #[test]
6750 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6751 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6752 .parse()
6753 .unwrap();
6754
6755 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6756 .expect("opposite forms override each other");
6757 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6758 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6759
6760 for words in [
6761 &["ex", "--color", "--color"][..],
6762 &["ex", "--no-color", "--no-color"][..],
6763 ] {
6764 let err = parse(&spec, &input(words)).unwrap_err();
6765 assert!(err.to_string().contains("cannot be used multiple times"));
6766 }
6767 }
6768
6769 #[test]
6770 fn strict_global_flags_may_repeat_across_command_levels() {
6771 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"
6772 .parse()
6773 .unwrap();
6774
6775 let out = parse(
6776 &spec,
6777 &input(&[
6778 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6779 ]),
6780 )
6781 .expect("an inherited global is allowed once at each command level");
6782 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6783 assert_eq!(out.flags[jobs].to_string(), "2");
6784
6785 for words in [
6786 &["ex", "--color", "--color", "run", "--no-color"][..],
6787 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6788 ] {
6789 let err = parse(&spec, &input(words)).unwrap_err();
6790 assert!(err.to_string().contains("cannot be used multiple times"));
6791 }
6792 }
6793
6794 #[test]
6795 fn a_subcommand_can_negate_only_its_parents_requirements() {
6796 let base = r#"name "ex"
6797bin "ex"
6798flag "--config" required=#true
6799flag "--mode" requires="--config"
6800flag "--other"
6801arg "<input>"
6802group "source" "--config" "--other" required=#true
6803cmd "run" { flag "--child" required=#true }
6804"#;
6805 let strict: Spec = base.parse().unwrap();
6806 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6807 let message = err.to_string();
6808 assert!(
6809 message.contains("input") || message.contains("config"),
6810 "{message}"
6811 );
6812
6813 let negated: Spec = base
6814 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6815 .parse()
6816 .unwrap();
6817 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6818 assert!(
6819 err.to_string().contains("child"),
6820 "the selected command keeps its own requirements: {err}"
6821 );
6822
6823 let mut child_optional = negated.clone();
6824 child_optional.cmd.subcommands["run"].flags[0].required = false;
6825 parse(&child_optional, &input(&["ex", "run"]))
6826 .expect("the child selection satisfies all parent requirements");
6827 parse(&child_optional, &input(&["ex", "--mode", "run"]))
6828 .expect("parent requires relationships are negated too");
6829 }
6830
6831 #[test]
6832 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6833 let spec: Spec = r#"name "ex"
6834bin "ex"
6835args_conflicts_with_subcommands #true
6836flag "--verbose"
6837cmd "run"
6838"#
6839 .parse()
6840 .unwrap();
6841
6842 parse(&spec, &input(&["ex", "run"]))
6843 .expect("the subcommand is valid without a parent argument");
6844 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6845 assert!(
6846 err.to_string().contains("cannot be used with arguments"),
6847 "{err}"
6848 );
6849 }
6850
6851 #[test]
6852 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6853 let base = r#"name "ex"
6854bin "ex"
6855flag "--values <value>..."
6856cmd "run"
6857"#;
6858 let plain: Spec = base.parse().unwrap();
6859 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6860 assert_eq!(out.cmd.name, "ex");
6861
6862 let precedence: Spec = base
6863 .replacen(
6864 "bin \"ex\"",
6865 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6866 1,
6867 )
6868 .parse()
6869 .unwrap();
6870 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6871 assert_eq!(out.cmd.name, "run");
6872 }
6873
6874 #[test]
6875 fn a_required_positional_can_follow_an_unfilled_optional_one() {
6876 let base = r#"name "ex"
6877bin "ex"
6878arg "[optional]"
6879arg "<required>"
6880"#;
6881 let plain: Spec = base.parse().unwrap();
6882 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6883 assert!(err.to_string().contains("required"), "{err}");
6884
6885 let enabled: Spec = base
6886 .replacen(
6887 "bin \"ex\"",
6888 "bin \"ex\"\nallow_missing_positional #true",
6889 1,
6890 )
6891 .parse()
6892 .unwrap();
6893 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6894 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6895 let value = &out
6896 .args
6897 .iter()
6898 .find(|(arg, _)| arg.name == "required")
6899 .unwrap()
6900 .1;
6901 assert!(matches!(value, ParseValue::String(value) if value == "value"));
6902 }
6903
6904 #[test]
6905 fn sigil_args_do_not_block_optional_positional_skipping() {
6906 let spec: Spec = r#"name "ex"
6907bin "ex"
6908allow_missing_positional #true
6909arg "[optional]"
6910arg "[tool]" sigil="@"
6911arg "<required>"
6912"#
6913 .parse()
6914 .unwrap();
6915
6916 let out = parse(&spec, &input(&["ex", "@node", "value"])).unwrap();
6917 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6918 let tool = out.args.keys().find(|arg| arg.name == "tool").unwrap();
6919 let required = out.args.keys().find(|arg| arg.name == "required").unwrap();
6920 assert_eq!(out.args[tool].to_string(), "node");
6921 assert_eq!(out.args[required].to_string(), "value");
6922 }
6923
6924 #[test]
6925 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6926 let spec: Spec =
6927 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6928 .parse()
6929 .unwrap();
6930 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6931 assert!(
6932 err.to_string().contains("--wat"),
6933 "the message should name the token: {err}"
6934 );
6935
6936 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6939 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6940 assert_eq!(out.args[rest].to_string(), "-1");
6941
6942 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6943 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6944 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6945 }
6946
6947 #[test]
6948 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6949 let spec: Spec = r#"
6950name "ex"
6951bin "ex"
6952unknown_flags "error"
6953flag "-0 --print0" global=#true
6954cmd "run" {
6955 flag "--force"
6956}
6957"#
6958 .parse()
6959 .unwrap();
6960 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6961 assert_eq!(out.cmd.name, "run");
6962 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6963 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6964 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6965 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6966 }
6967
6968 #[test]
6969 fn a_command_may_override_the_cli_wide_setting() {
6970 let spec: Spec = r#"
6972name "ex"
6973bin "ex"
6974unknown_flags "error"
6975cmd "exec" unknown_flags="value" {
6976 arg "[rest]..."
6977}
6978cmd "build" {
6979 arg "[rest]..."
6980}
6981"#
6982 .parse()
6983 .unwrap();
6984
6985 let out = parse(
6986 &spec,
6987 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6988 )
6989 .unwrap();
6990 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6991 assert_eq!(out.args[rest].to_string(), "--wat");
6992
6993 assert!(
6994 parse(
6995 &spec,
6996 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6997 )
6998 .is_err(),
6999 "a command that says nothing inherits the CLI's choice"
7000 );
7001 }
7002
7003 #[test]
7004 fn the_setting_survives_a_round_trip() {
7005 let spec: Spec =
7006 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
7007 .parse()
7008 .unwrap();
7009 let reparsed: Spec = spec.to_string().parse().unwrap();
7010 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
7011 assert_eq!(
7012 reparsed.cmd.subcommands["x"].unknown_flags,
7013 Some(UnknownFlags::Value)
7014 );
7015 }
7016
7017 #[test]
7018 fn test_default_subcommand() {
7019 let run_cmd = SpecCommand::builder()
7021 .name("run")
7022 .arg(SpecArg::builder().name("task").build())
7023 .build();
7024 let mut cmd = SpecCommand::builder().name("test").build();
7025 cmd.subcommands.insert("run".to_string(), run_cmd);
7026
7027 let spec = Spec {
7028 name: "test".to_string(),
7029 bin: "test".to_string(),
7030 cmd,
7031 default_subcommand: Some("run".to_string()),
7032 ..Default::default()
7033 };
7034
7035 let input = vec!["test".to_string(), "mytask".to_string()];
7037 let parsed = parse(&spec, &input).unwrap();
7038
7039 assert_eq!(parsed.cmds.len(), 2);
7041 assert_eq!(parsed.cmds[1].name, "run");
7042
7043 assert_eq!(parsed.args.len(), 1);
7045 let arg = parsed.args.keys().next().unwrap();
7046 assert_eq!(arg.name, "task");
7047 let value = parsed.args.values().next().unwrap();
7048 assert_eq!(value.to_string(), "mytask");
7049 }
7050
7051 #[test]
7052 fn default_subcommand_outranks_root_sigil_arg() {
7053 let spec: Spec = r#"
7054name "test"
7055bin "test"
7056default_subcommand "run"
7057arg "[tools]..." sigil="+"
7058cmd "run" { arg "<task>" }
7059"#
7060 .parse()
7061 .unwrap();
7062
7063 let parsed = parse(&spec, &input(&["test", "+node", "node"])).unwrap();
7064 assert_eq!(parsed.cmd.name, "run");
7065 let value = |name| {
7066 parsed
7067 .args
7068 .iter()
7069 .find(|(arg, _)| arg.name == name)
7070 .map(|(_, value)| value.to_string())
7071 .unwrap()
7072 };
7073 assert_eq!(value("tools"), "node");
7074 assert_eq!(value("task"), "node");
7075 }
7076
7077 #[test]
7078 fn test_default_subcommand_explicit_still_works() {
7079 let run_cmd = SpecCommand::builder()
7081 .name("run")
7082 .arg(SpecArg::builder().name("task").build())
7083 .build();
7084 let other_cmd = SpecCommand::builder()
7085 .name("other")
7086 .arg(SpecArg::builder().name("other_arg").build())
7087 .build();
7088 let mut cmd = SpecCommand::builder().name("test").build();
7089 cmd.subcommands.insert("run".to_string(), run_cmd);
7090 cmd.subcommands.insert("other".to_string(), other_cmd);
7091
7092 let spec = Spec {
7093 name: "test".to_string(),
7094 bin: "test".to_string(),
7095 cmd,
7096 default_subcommand: Some("run".to_string()),
7097 ..Default::default()
7098 };
7099
7100 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
7102 let parsed = parse(&spec, &input).unwrap();
7103
7104 assert_eq!(parsed.cmds.len(), 2);
7106 assert_eq!(parsed.cmds[1].name, "other");
7107 }
7108
7109 #[test]
7110 fn test_default_subcommand_applies_only_at_the_root() {
7111 let mut config_ls = SpecCommand::builder().name("ls").build();
7117 config_ls.args.push(SpecArg::builder().name("what").build());
7118 let mut config_cmd = SpecCommand::builder().name("config").build();
7119 config_cmd.subcommands.insert("ls".to_string(), config_ls);
7120
7121 let mut root_ls = SpecCommand::builder().name("ls").build();
7124 root_ls.args.push(SpecArg::builder().name("what").build());
7125 let mut cmd = SpecCommand::builder().name("ex").build();
7126 cmd.subcommands.insert("ls".to_string(), root_ls);
7127 cmd.subcommands.insert("config".to_string(), config_cmd);
7128
7129 let spec = Spec {
7130 name: "ex".to_string(),
7131 bin: "ex".to_string(),
7132 cmd,
7133 default_subcommand: Some("ls".to_string()),
7134 ..Default::default()
7135 };
7136
7137 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
7141 assert!(
7142 parse(&spec, &input).is_err(),
7143 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
7144 );
7145
7146 let input = vec!["ex".to_string(), "zzz".to_string()];
7148 let parsed = parse(&spec, &input).expect("the root's default applies");
7149 assert_eq!(
7150 parsed
7151 .cmds
7152 .iter()
7153 .map(|c| c.name.as_str())
7154 .collect::<Vec<_>>(),
7155 ["ex", "ls"]
7156 );
7157 assert_eq!(
7158 parsed.args.values().next().map(|v| v.to_string()),
7159 Some("zzz".to_string()),
7160 "and the word binds inside the command it reached"
7161 );
7162 }
7163
7164 #[test]
7165 fn test_default_subcommand_with_nested_subcommands() {
7166 let say_cmd = SpecCommand::builder()
7170 .name("say")
7171 .arg(SpecArg::builder().name("name").build())
7172 .build();
7173 let mut run_cmd = SpecCommand::builder().name("run").build();
7174 run_cmd.subcommands.insert("say".to_string(), say_cmd);
7175
7176 let mut cmd = SpecCommand::builder().name("test").build();
7177 cmd.subcommands.insert("run".to_string(), run_cmd);
7178
7179 let spec = Spec {
7180 name: "test".to_string(),
7181 bin: "test".to_string(),
7182 cmd,
7183 default_subcommand: Some("run".to_string()),
7184 ..Default::default()
7185 };
7186
7187 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
7189 let parsed = parse(&spec, &input).unwrap();
7190
7191 assert_eq!(parsed.cmds.len(), 3);
7193 assert_eq!(parsed.cmds[0].name, "test");
7194 assert_eq!(parsed.cmds[1].name, "run");
7195 assert_eq!(parsed.cmds[2].name, "say");
7196
7197 assert_eq!(parsed.args.len(), 1);
7199 let arg = parsed.args.keys().next().unwrap();
7200 assert_eq!(arg.name, "name");
7201 let value = parsed.args.values().next().unwrap();
7202 assert_eq!(value.to_string(), "hello");
7203 }
7204
7205 fn mounted_global_flag_spec() -> Spec {
7213 let task_cmd = SpecCommand::builder()
7214 .name("sample:run")
7215 .arg(
7216 SpecArg::builder()
7217 .name("profile")
7218 .choices(["alpha", "beta", "gamma"])
7219 .build(),
7220 )
7221 .build();
7222 let mut run_cmd = SpecCommand::builder()
7224 .name("run")
7225 .flag(
7226 SpecFlag::builder()
7227 .name("cd")
7228 .short('C')
7229 .long("cd")
7230 .arg(SpecArg::builder().name("dir").build())
7231 .global(false)
7232 .build(),
7233 )
7234 .build();
7235 run_cmd
7236 .subcommands
7237 .insert("sample:run".to_string(), task_cmd);
7238
7239 let mut cmd = SpecCommand::builder()
7240 .name("test")
7241 .flag(
7242 SpecFlag::builder()
7243 .name("cd")
7244 .short('C')
7245 .long("cd")
7246 .arg(SpecArg::builder().name("dir").build())
7247 .global(true)
7248 .build(),
7249 )
7250 .build();
7251 cmd.subcommands.insert("run".to_string(), run_cmd);
7252
7253 Spec {
7254 name: "test".to_string(),
7255 bin: "test".to_string(),
7256 cmd,
7257 ..Default::default()
7258 }
7259 }
7260
7261 #[test]
7262 fn test_prefix_global_flag_does_not_pollute_choices() {
7263 let spec = mounted_global_flag_spec();
7270
7271 for words in [
7274 &["test", "-C", "/tmp", "run", "sample:run"][..],
7275 &["test", "--cd=/tmp", "run", "sample:run"][..],
7277 ] {
7278 let parsed = parse_partial(&spec, &input(words)).unwrap();
7279 assert_eq!(
7280 parsed
7281 .cmds
7282 .iter()
7283 .map(|c| c.name.as_str())
7284 .collect::<Vec<_>>(),
7285 vec!["test", "run", "sample:run"],
7286 );
7287 assert!(
7289 parsed.args.is_empty(),
7290 "args should be empty, got {:?}",
7291 parsed.args
7292 );
7293
7294 let cd = parsed
7297 .available_flags
7298 .get("--cd")
7299 .expect("--cd should remain available after descending into the subcommand");
7300 assert!(cd.global, "--cd must stay global after descent");
7301 assert!(
7302 parsed.available_flags.get("-C").is_some_and(|f| f.global),
7303 "-C must stay global after descent",
7304 );
7305
7306 assert_eq!(
7310 parsed.as_env().get("usage_cd").map(String::as_str),
7311 Some("/tmp"),
7312 "global flag value must survive in as_env(), got {:?}",
7313 parsed.as_env(),
7314 );
7315 }
7316
7317 let parsed = parse_partial(
7319 &spec,
7320 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
7321 )
7322 .unwrap();
7323 assert_eq!(parsed.args.len(), 1);
7324 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7325
7326 assert_parse_err(
7328 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
7329 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7330 );
7331 }
7332
7333 fn mounted_orphan_short_spec() -> Spec {
7338 let task_cmd = SpecCommand::builder()
7339 .name("sample:run")
7340 .arg(
7341 SpecArg::builder()
7342 .name("profile")
7343 .choices(["alpha", "beta", "gamma"])
7344 .build(),
7345 )
7346 .build();
7347 let mut run_cmd = SpecCommand::builder()
7350 .name("run")
7351 .flag(
7352 SpecFlag::builder()
7353 .name("raw")
7354 .short('r')
7355 .long("raw")
7356 .global(false)
7357 .build(),
7358 )
7359 .flag(
7360 SpecFlag::builder()
7361 .name("force")
7362 .short('f')
7363 .long("force")
7364 .global(false)
7365 .build(),
7366 )
7367 .build();
7368 run_cmd
7369 .subcommands
7370 .insert("sample:run".to_string(), task_cmd);
7371
7372 let mut cmd = SpecCommand::builder()
7374 .name("test")
7375 .flag(
7376 SpecFlag::builder()
7377 .name("raw")
7378 .long("raw")
7379 .global(true)
7380 .build(),
7381 )
7382 .build();
7383 cmd.subcommands.insert("run".to_string(), run_cmd);
7384
7385 Spec {
7386 name: "test".to_string(),
7387 bin: "test".to_string(),
7388 cmd,
7389 ..Default::default()
7390 }
7391 }
7392
7393 #[test]
7394 fn test_orphan_short_alias_survives_merge() {
7395 let spec = mounted_orphan_short_spec();
7401
7402 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
7403 assert_eq!(
7404 parsed
7405 .cmds
7406 .iter()
7407 .map(|c| c.name.as_str())
7408 .collect::<Vec<_>>(),
7409 vec!["test", "run", "sample:run"],
7410 );
7411
7412 assert!(
7415 parsed.available_flags.get("-r").is_some_and(|f| f.global),
7416 "-r must be merged onto the inherited global flag and stay global after descent",
7417 );
7418 assert!(
7419 parsed
7420 .available_flags
7421 .get("--raw")
7422 .is_some_and(|f| f.global),
7423 "--raw must stay global after descent",
7424 );
7425
7426 assert!(
7428 parsed.args.is_empty(),
7429 "args should be empty, got {:?}",
7430 parsed.args
7431 );
7432
7433 assert_eq!(
7435 parsed.as_env().get("usage_raw").map(String::as_str),
7436 Some("true"),
7437 "merged short's value must survive in as_env(), got {:?}",
7438 parsed.as_env(),
7439 );
7440
7441 assert!(
7444 !parsed.available_flags.contains_key("-f"),
7445 "purely-local -f must not be promoted onto a global",
7446 );
7447 assert!(
7448 !parsed.available_flags.contains_key("--force"),
7449 "purely-local --force must not be promoted onto a global",
7450 );
7451
7452 let parsed =
7454 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
7455 assert_eq!(parsed.args.len(), 1);
7456 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
7457
7458 assert_parse_err(
7460 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
7461 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7462 );
7463 }
7464
7465 #[test]
7466 fn test_orphan_short_does_not_clobber_unrelated_global() {
7467 let run_cmd = SpecCommand::builder()
7472 .name("run")
7473 .flag(
7474 SpecFlag::builder()
7475 .name("raw")
7476 .short('r')
7477 .long("raw")
7478 .global(false)
7479 .build(),
7480 )
7481 .build();
7482 let mut cmd = SpecCommand::builder()
7483 .name("test")
7484 .flag(
7485 SpecFlag::builder()
7486 .name("raw")
7487 .long("raw")
7488 .global(true)
7489 .build(),
7490 )
7491 .flag(
7492 SpecFlag::builder()
7493 .name("restrict")
7494 .short('r')
7495 .long("restrict")
7496 .global(true)
7497 .build(),
7498 )
7499 .build();
7500 cmd.subcommands.insert("run".to_string(), run_cmd);
7501 let spec = Spec {
7502 name: "test".to_string(),
7503 bin: "test".to_string(),
7504 cmd,
7505 ..Default::default()
7506 };
7507
7508 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7509 assert_eq!(
7511 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
7512 Some("restrict"),
7513 "-r must remain owned by the unrelated global it already belonged to",
7514 );
7515 assert!(parsed
7517 .available_flags
7518 .get("--raw")
7519 .is_some_and(|f| f.global));
7520 assert!(parsed
7521 .available_flags
7522 .get("--restrict")
7523 .is_some_and(|f| f.global));
7524 }
7525
7526 #[test]
7527 fn test_redeclared_global_aliases_share_one_flag() {
7528 let spec = r#"
7535flag "-y --yes" global=#true effect="write"
7536cmd "run" {
7537 flag "-y --yes --assume-yes"
7538}
7539"#
7540 .parse::<Spec>()
7541 .unwrap();
7542
7543 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7544
7545 for key in ["-y", "--yes", "--assume-yes"] {
7546 let flag = parsed
7547 .available_flags
7548 .get(key)
7549 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7550 assert!(flag.global, "{key} must stay global after the descent");
7551 assert_eq!(
7552 flag.long,
7553 vec!["yes".to_string(), "assume-yes".to_string()],
7554 "{key} must resolve to the flag carrying every alias",
7555 );
7556 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
7557 }
7558
7559 assert_eq!(
7561 unique_flags(parsed.available_flags.values()).count(),
7562 1,
7563 "all aliases must point at one flag object, got {:?}",
7564 parsed.available_flags,
7565 );
7566
7567 assert_eq!(
7569 parsed.available_flags["-y"].effect,
7570 Some(crate::SpecCommandEffect::Write),
7571 );
7572 }
7573
7574 #[test]
7575 fn test_redeclared_global_keeps_hidden_alias_metadata() {
7576 let spec = r#"
7577flag "--yes" global=#true {
7578 alias "-q" "--quietly" hide=#true
7579}
7580cmd "run" {
7581 flag "--yes --assume-yes" {
7582 alias "-s" "--secret" hide=#true
7583 }
7584}
7585"#
7586 .parse::<Spec>()
7587 .unwrap();
7588
7589 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7590 let merged = &parsed.available_flags["--yes"];
7591 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7592 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7593 for key in ["-q", "-s", "--quietly", "--secret"] {
7594 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7595 }
7596 }
7597
7598 #[test]
7599 fn test_redeclared_global_can_promote_hidden_aliases() {
7600 let spec = r#"
7601flag "--yes" global=#true {
7602 alias "-q" "--quietly" hide=#true
7603}
7604cmd "run" {
7605 flag "-q --yes --quietly"
7606}
7607"#
7608 .parse::<Spec>()
7609 .unwrap();
7610
7611 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7612 let merged = &parsed.available_flags["--yes"];
7613 assert!(merged.hidden_short_aliases.is_empty());
7614 assert!(merged.hidden_aliases.is_empty());
7615 for key in ["-q", "--quietly"] {
7616 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7617 }
7618 }
7619
7620 #[test]
7621 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7622 let spec = r#"
7628flag "-y --yes --confirm" global=#true
7629cmd "run" {
7630 flag "--yes --assume-yes"
7631}
7632"#
7633 .parse::<Spec>()
7634 .unwrap();
7635
7636 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7637
7638 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7639 let flag = parsed
7640 .available_flags
7641 .get(key)
7642 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7643 assert!(flag.global, "{key} must stay global after the descent");
7644 assert_eq!(
7645 flag.long,
7646 vec![
7647 "yes".to_string(),
7648 "confirm".to_string(),
7649 "assume-yes".to_string()
7650 ],
7651 "{key} must resolve to the flag carrying every alias",
7652 );
7653 }
7654
7655 assert_eq!(
7656 unique_flags(parsed.available_flags.values()).count(),
7657 1,
7658 "all aliases must point at one flag object, got {:?}",
7659 parsed.available_flags,
7660 );
7661 }
7662
7663 fn mounted_task_flag_spec() -> Spec {
7670 let mut task_cmd = SpecCommand::builder()
7671 .name("mytask")
7672 .flag(
7673 SpecFlag::builder()
7674 .name("env")
7675 .long("env")
7676 .arg(
7677 SpecArg::builder()
7678 .name("name")
7679 .choices(["dev", "stage", "prod"])
7680 .build(),
7681 )
7682 .global(false)
7683 .build(),
7684 )
7685 .flag(
7686 SpecFlag::builder()
7687 .name("bump")
7688 .long("bump")
7689 .arg(
7690 SpecArg::builder()
7691 .name("type")
7692 .choices(["auto", "major"])
7693 .build(),
7694 )
7695 .global(false)
7696 .build(),
7697 )
7698 .build();
7699 task_cmd.mounted = true;
7700
7701 let mut run_cmd = SpecCommand::builder()
7702 .name("run")
7703 .flag(
7704 SpecFlag::builder()
7705 .name("force")
7706 .short('f')
7707 .long("force")
7708 .global(false)
7709 .build(),
7710 )
7711 .build();
7712 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7713
7714 let mut cmd = SpecCommand::builder()
7715 .name("test")
7716 .flag(
7717 SpecFlag::builder()
7718 .name("env")
7719 .short('E')
7720 .long("env")
7721 .arg(SpecArg::builder().name("ENV").build())
7722 .global(true)
7723 .build(),
7724 )
7725 .flag(
7726 SpecFlag::builder()
7727 .name("silent")
7728 .long("silent")
7729 .global(true)
7730 .build(),
7731 )
7732 .build();
7733 cmd.subcommands.insert("run".to_string(), run_cmd);
7734
7735 Spec {
7736 name: "test".to_string(),
7737 bin: "test".to_string(),
7738 cmd,
7739 ..Default::default()
7740 }
7741 }
7742
7743 #[test]
7744 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7745 let deep = SpecCommand::builder().name("deep").build();
7751 let mut sub = SpecCommand::builder()
7752 .name("sub")
7753 .flag(
7755 SpecFlag::builder()
7756 .name("cd")
7757 .short('C')
7758 .long("cd")
7759 .arg(SpecArg::builder().name("dir").build())
7760 .global(false)
7761 .build(),
7762 )
7763 .build();
7764 sub.subcommands.insert("deep".to_string(), deep);
7765 let mut task = SpecCommand::builder()
7766 .name("task")
7767 .flag(
7768 SpecFlag::builder()
7769 .name("cd")
7770 .short('C')
7771 .long("cd")
7772 .arg(SpecArg::builder().name("dir").build())
7773 .global(true)
7774 .build(),
7775 )
7776 .build();
7777 task.subcommands.insert("sub".to_string(), sub);
7778 task.mark_mounted();
7779
7780 let mut run_cmd = SpecCommand::builder().name("run").build();
7781 run_cmd.subcommands.insert("task".to_string(), task);
7782 let mut cmd = SpecCommand::builder().name("test").build();
7783 cmd.subcommands.insert("run".to_string(), run_cmd);
7784 let spec = Spec {
7785 name: "test".to_string(),
7786 bin: "test".to_string(),
7787 cmd,
7788 ..Default::default()
7789 };
7790
7791 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7792 assert!(
7793 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7794 "the mounted program's own global must survive descents inside the mounted tree",
7795 );
7796 assert!(
7797 parsed.completion_flags().contains_key("--cd"),
7798 "and must still be offered there: it belongs to the mounted program",
7799 );
7800 assert!(
7801 parsed.completion_flags().contains_key("-C"),
7802 "including the short the nested command re-declared",
7803 );
7804 }
7805
7806 #[test]
7807 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7808 let mut task = SpecCommand::builder()
7813 .name("task")
7814 .flag(
7815 SpecFlag::builder()
7816 .name("bump")
7817 .long("bump")
7818 .global(false)
7819 .build(),
7820 )
7821 .build();
7822 task.mark_mounted();
7823
7824 let mut run_cmd = SpecCommand::builder().name("run").build();
7825 run_cmd.subcommands.insert("task".to_string(), task);
7826 run_cmd.flags = vec![
7828 SpecFlag::builder()
7829 .name("tglobal")
7830 .long("tglobal")
7831 .global(true)
7832 .build(),
7833 SpecFlag::builder()
7834 .name("tlocal")
7835 .long("tlocal")
7836 .global(false)
7837 .build(),
7838 ];
7839 run_cmd.flags_from_mount = true;
7840
7841 let mut cmd = SpecCommand::builder()
7842 .name("test")
7843 .flag(
7844 SpecFlag::builder()
7845 .name("silent")
7846 .long("silent")
7847 .global(true)
7848 .build(),
7849 )
7850 .build();
7851 cmd.subcommands.insert("run".to_string(), run_cmd);
7852 let spec = Spec {
7853 name: "test".to_string(),
7854 bin: "test".to_string(),
7855 cmd,
7856 ..Default::default()
7857 };
7858
7859 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7860 assert_eq!(
7861 parsed.completion_flags().keys().collect::<Vec<_>>(),
7862 vec!["--bump", "--tglobal"],
7863 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7864 `--silent` does not, and the mount's non-global root flag is not inherited",
7865 );
7866 }
7867
7868 #[test]
7869 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7870 let spec = mounted_task_flag_spec();
7875 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7876
7877 assert!(parsed.available_flags.contains_key("--silent"));
7879 assert!(parsed.available_flags.contains_key("-E"));
7880 assert_eq!(
7882 parsed.completion_flags().keys().collect::<Vec<_>>(),
7883 vec!["--bump", "--env"],
7884 "only the mounted command's own flags may be offered",
7885 );
7886
7887 assert!(!parsed.available_flags.contains_key("--force"));
7889 }
7890
7891 #[test]
7892 fn test_mounted_cmd_flag_wins_over_inherited_global() {
7893 let spec = mounted_task_flag_spec();
7896 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7897
7898 let awaiting = parsed
7899 .flag_awaiting_value
7900 .first()
7901 .expect("--env should await a value");
7902 assert_eq!(
7903 awaiting
7904 .arg
7905 .as_ref()
7906 .and_then(|a| a.choices.as_ref())
7907 .map(|c| c.choices.clone()),
7908 Some(vec![
7909 "dev".to_string(),
7910 "stage".to_string(),
7911 "prod".to_string()
7912 ]),
7913 "the mounted command's own --env must win over the inherited global",
7914 );
7915
7916 let parsed =
7919 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7920 assert!(
7921 parsed.args.is_empty(),
7922 "prefix global tokens must not be consumed as positionals, got {:?}",
7923 parsed.args
7924 );
7925 assert_eq!(
7926 parsed.as_env().get("usage_env").map(String::as_str),
7927 Some("anything"),
7928 );
7929 }
7930
7931 #[test]
7932 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7933 let spec = mounted_task_flag_spec();
7938 let parsed = parse_partial(
7939 &spec,
7940 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7941 )
7942 .unwrap();
7943 assert!(
7944 parsed.errors.is_empty(),
7945 "prefix global value must not be validated against the mounted flag: {:?}",
7946 parsed
7947 .errors
7948 .iter()
7949 .map(|e| e.to_string())
7950 .collect::<Vec<_>>(),
7951 );
7952 assert_eq!(
7953 parsed.as_env().get("usage_env").map(String::as_str),
7954 Some("not-a-task-choice"),
7955 );
7956
7957 let parsed = parse_partial(
7959 &spec,
7960 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7961 )
7962 .unwrap();
7963 assert!(parsed.errors.is_empty());
7964 assert_eq!(
7965 parsed.as_env().get("usage_env").map(String::as_str),
7966 Some("not-a-task-choice"),
7967 );
7968
7969 let parsed = parse_partial(
7972 &spec,
7973 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7974 )
7975 .unwrap();
7976 let awaiting = parsed
7977 .flag_awaiting_value
7978 .first()
7979 .expect("--env should await a value");
7980 assert_eq!(
7981 awaiting
7982 .arg
7983 .as_ref()
7984 .and_then(|a| a.choices.as_ref())
7985 .map(|c| c.choices.clone()),
7986 Some(vec![
7987 "dev".to_string(),
7988 "stage".to_string(),
7989 "prod".to_string()
7990 ]),
7991 "the mounted command's --env must own the name after the mounted command",
7992 );
7993 }
7994
7995 #[test]
7996 fn test_non_global_flag_does_not_hide_subcommand() {
7997 let spec = mounted_task_flag_spec();
8001
8002 for words in [
8003 &["test", "run", "--force", "mytask"][..],
8005 &["test", "run", "-f", "mytask"][..],
8006 &["test", "-E", "prod", "run", "--force", "mytask"][..],
8008 ] {
8009 let parsed = parse_partial(&spec, &input(words)).unwrap();
8010 assert_eq!(
8011 parsed
8012 .cmds
8013 .iter()
8014 .map(|c| c.name.as_str())
8015 .collect::<Vec<_>>(),
8016 vec!["test", "run", "mytask"],
8017 "{words:?} should descend into the mounted command",
8018 );
8019 assert!(
8020 parsed.args.is_empty(),
8021 "{words:?} should not consume a positional, got {:?}",
8022 parsed.args,
8023 );
8024 assert_eq!(
8025 parsed.as_env().get("usage_force").map(String::as_str),
8026 Some("true"),
8027 "the non-global flag must still be recorded for {words:?}",
8028 );
8029 }
8030
8031 let mut run_cmd = SpecCommand::builder()
8034 .name("run")
8035 .flag(
8036 SpecFlag::builder()
8037 .name("output")
8038 .short('o')
8039 .long("output")
8040 .arg(SpecArg::builder().name("mode").build())
8041 .global(false)
8042 .build(),
8043 )
8044 .build();
8045 run_cmd.subcommands.insert(
8046 "task".to_string(),
8047 SpecCommand::builder().name("task").build(),
8048 );
8049 let mut cmd = SpecCommand::builder().name("test").build();
8050 cmd.subcommands.insert("run".to_string(), run_cmd);
8051 let spec = Spec {
8052 name: "test".to_string(),
8053 bin: "test".to_string(),
8054 cmd,
8055 ..Default::default()
8056 };
8057
8058 let parsed =
8059 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
8060 assert_eq!(
8061 parsed
8062 .cmds
8063 .iter()
8064 .map(|c| c.name.as_str())
8065 .collect::<Vec<_>>(),
8066 vec!["test", "run", "task"],
8067 );
8068 assert_eq!(
8069 parsed.as_env().get("usage_output").map(String::as_str),
8070 Some("quiet"),
8071 );
8072
8073 assert_parse_err(
8076 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
8077 "unexpected word: --nope",
8078 );
8079 }
8080
8081 #[test]
8082 fn test_non_mounted_subcommand_offers_inherited_globals() {
8083 let mut run_cmd = SpecCommand::builder().name("run").build();
8086 run_cmd.subcommands.insert(
8087 "nested".to_string(),
8088 SpecCommand::builder().name("nested").build(),
8089 );
8090 let mut cmd = SpecCommand::builder()
8091 .name("test")
8092 .flag(
8093 SpecFlag::builder()
8094 .name("silent")
8095 .long("silent")
8096 .global(true)
8097 .build(),
8098 )
8099 .build();
8100 cmd.subcommands.insert("run".to_string(), run_cmd);
8101 let spec = Spec {
8102 name: "test".to_string(),
8103 bin: "test".to_string(),
8104 cmd,
8105 ..Default::default()
8106 };
8107
8108 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
8109 assert_eq!(
8110 parsed.completion_flags().keys().collect::<Vec<_>>(),
8111 parsed.available_flags.keys().collect::<Vec<_>>(),
8112 );
8113 assert!(parsed.completion_flags().contains_key("--silent"));
8114 }
8115
8116 #[test]
8117 fn test_subcommand_alias_collision_keeps_last_owner() {
8118 let run_cmd = SpecCommand::builder()
8122 .name("run")
8123 .flag(
8124 SpecFlag::builder()
8125 .name("alpha")
8126 .short('x')
8127 .long("alpha")
8128 .global(false)
8129 .build(),
8130 )
8131 .flag(
8132 SpecFlag::builder()
8133 .name("beta")
8134 .short('x')
8135 .long("beta")
8136 .global(false)
8137 .build(),
8138 )
8139 .build();
8140 let mut cmd = SpecCommand::builder().name("test").build();
8141 cmd.subcommands.insert("run".to_string(), run_cmd);
8142 let spec = Spec {
8143 name: "test".to_string(),
8144 bin: "test".to_string(),
8145 cmd,
8146 ..Default::default()
8147 };
8148
8149 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
8150 assert_eq!(
8152 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
8153 Some("beta"),
8154 "the last-declared flag must keep a shared short alias",
8155 );
8156 assert_eq!(
8158 parsed
8159 .available_flags
8160 .get("--alpha")
8161 .map(|f| f.name.as_str()),
8162 Some("alpha"),
8163 );
8164 assert_eq!(
8165 parsed
8166 .available_flags
8167 .get("--beta")
8168 .map(|f| f.name.as_str()),
8169 Some("beta"),
8170 );
8171 }
8172
8173 #[test]
8174 fn test_default_subcommand_same_name_child() {
8175 let run_task = SpecCommand::builder()
8179 .name("run")
8180 .arg(SpecArg::builder().name("args").build())
8181 .build();
8182 let mut run_cmd = SpecCommand::builder().name("run").build();
8183 run_cmd.subcommands.insert("run".to_string(), run_task);
8184
8185 let mut cmd = SpecCommand::builder().name("test").build();
8186 cmd.subcommands.insert("run".to_string(), run_cmd);
8187
8188 let spec = Spec {
8189 name: "test".to_string(),
8190 bin: "test".to_string(),
8191 cmd,
8192 default_subcommand: Some("run".to_string()),
8193 ..Default::default()
8194 };
8195
8196 let input = vec!["test".to_string(), "run".to_string()];
8198 let parsed = parse(&spec, &input).unwrap();
8199
8200 assert_eq!(parsed.cmds.len(), 2);
8202 assert_eq!(parsed.cmds[0].name, "test");
8203 assert_eq!(parsed.cmds[1].name, "run");
8204
8205 let input = vec![
8207 "test".to_string(),
8208 "run".to_string(),
8209 "run".to_string(),
8210 "hello".to_string(),
8211 ];
8212 let parsed = parse(&spec, &input).unwrap();
8213
8214 assert_eq!(parsed.cmds.len(), 3);
8215 assert_eq!(parsed.cmds[0].name, "test");
8216 assert_eq!(parsed.cmds[1].name, "run");
8217 assert_eq!(parsed.cmds[2].name, "run");
8218 assert_eq!(parsed.args.len(), 1);
8219 let value = parsed.args.values().next().unwrap();
8220 assert_eq!(value.to_string(), "hello");
8221
8222 let mut run_cmd = SpecCommand::builder()
8226 .name("run")
8227 .arg(SpecArg::builder().name("task").build())
8228 .build();
8229 let run_task = SpecCommand::builder().name("run").build();
8230 run_cmd.subcommands.insert("run".to_string(), run_task);
8231
8232 let mut cmd = SpecCommand::builder().name("test").build();
8233 cmd.subcommands.insert("run".to_string(), run_cmd);
8234
8235 let spec = Spec {
8236 name: "test".to_string(),
8237 bin: "test".to_string(),
8238 cmd,
8239 default_subcommand: Some("run".to_string()),
8240 ..Default::default()
8241 };
8242
8243 let input = vec!["test".to_string(), "other".to_string()];
8244 let parsed = parse(&spec, &input).unwrap();
8245
8246 assert_eq!(parsed.cmds.len(), 2);
8249 assert_eq!(parsed.cmds[0].name, "test");
8250 assert_eq!(parsed.cmds[1].name, "run");
8251
8252 assert_eq!(parsed.args.len(), 1);
8254 let value = parsed.args.values().next().unwrap();
8255 assert_eq!(value.to_string(), "other");
8256 }
8257
8258 #[test]
8259 fn test_restart_token() {
8260 let run_cmd = SpecCommand::builder()
8262 .name("run")
8263 .arg(SpecArg::builder().name("task").build())
8264 .restart_token(":::".to_string())
8265 .build();
8266 let mut cmd = SpecCommand::builder().name("test").build();
8267 cmd.subcommands.insert("run".to_string(), run_cmd);
8268
8269 let spec = Spec {
8270 name: "test".to_string(),
8271 bin: "test".to_string(),
8272 cmd,
8273 ..Default::default()
8274 };
8275
8276 let input = vec![
8278 "test".to_string(),
8279 "run".to_string(),
8280 "task1".to_string(),
8281 ":::".to_string(),
8282 "task2".to_string(),
8283 ];
8284 let parsed = parse(&spec, &input).unwrap();
8285
8286 assert_eq!(parsed.args.len(), 1);
8288 let value = parsed.args.values().next().unwrap();
8289 assert_eq!(value.to_string(), "task2");
8290 }
8291
8292 #[test]
8293 fn test_restart_token_multiple() {
8294 let run_cmd = SpecCommand::builder()
8296 .name("run")
8297 .arg(SpecArg::builder().name("task").build())
8298 .restart_token(":::".to_string())
8299 .build();
8300 let mut cmd = SpecCommand::builder().name("test").build();
8301 cmd.subcommands.insert("run".to_string(), run_cmd);
8302
8303 let spec = Spec {
8304 name: "test".to_string(),
8305 bin: "test".to_string(),
8306 cmd,
8307 ..Default::default()
8308 };
8309
8310 let input = vec![
8312 "test".to_string(),
8313 "run".to_string(),
8314 "task1".to_string(),
8315 ":::".to_string(),
8316 "task2".to_string(),
8317 ":::".to_string(),
8318 "task3".to_string(),
8319 ];
8320 let parsed = parse(&spec, &input).unwrap();
8321
8322 assert_eq!(parsed.args.len(), 1);
8324 let value = parsed.args.values().next().unwrap();
8325 assert_eq!(value.to_string(), "task3");
8326 }
8327
8328 #[test]
8329 fn test_restart_token_clears_flag_awaiting_value() {
8330 let run_cmd = SpecCommand::builder()
8332 .name("run")
8333 .arg(SpecArg::builder().name("task").build())
8334 .flag(
8335 SpecFlag::builder()
8336 .name("jobs")
8337 .long("jobs")
8338 .arg(SpecArg::builder().name("count").build())
8339 .build(),
8340 )
8341 .restart_token(":::".to_string())
8342 .build();
8343 let mut cmd = SpecCommand::builder().name("test").build();
8344 cmd.subcommands.insert("run".to_string(), run_cmd);
8345
8346 let spec = Spec {
8347 name: "test".to_string(),
8348 bin: "test".to_string(),
8349 cmd,
8350 ..Default::default()
8351 };
8352
8353 let input = vec![
8355 "test".to_string(),
8356 "run".to_string(),
8357 "task1".to_string(),
8358 "--jobs".to_string(),
8359 ":::".to_string(),
8360 "task2".to_string(),
8361 ];
8362 let parsed = parse(&spec, &input).unwrap();
8363
8364 assert_eq!(parsed.args.len(), 1);
8366 let value = parsed.args.values().next().unwrap();
8367 assert_eq!(value.to_string(), "task2");
8368 assert!(parsed.flag_awaiting_value.is_empty());
8370 }
8371
8372 #[test]
8373 fn test_restart_token_resets_double_dash() {
8374 let run_cmd = SpecCommand::builder()
8376 .name("run")
8377 .arg(SpecArg::builder().name("task").build())
8378 .arg(SpecArg::builder().name("extra_args").var(true).build())
8379 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
8380 .restart_token(":::".to_string())
8381 .build();
8382 let mut cmd = SpecCommand::builder().name("test").build();
8383 cmd.subcommands.insert("run".to_string(), run_cmd);
8384
8385 let spec = Spec {
8386 name: "test".to_string(),
8387 bin: "test".to_string(),
8388 cmd,
8389 ..Default::default()
8390 };
8391
8392 let input = vec![
8394 "test".to_string(),
8395 "run".to_string(),
8396 "task1".to_string(),
8397 "--".to_string(),
8398 "extra".to_string(),
8399 ":::".to_string(),
8400 "--verbose".to_string(),
8401 "task2".to_string(),
8402 ];
8403 let parsed = parse(&spec, &input).unwrap();
8404
8405 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
8407 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8409 let value = parsed.args.get(task_arg).unwrap();
8410 assert_eq!(value.to_string(), "task2");
8411 }
8412
8413 #[test]
8414 fn test_double_dashes_without_preserve() {
8415 let run_cmd = SpecCommand::builder()
8420 .name("run")
8421 .arg(SpecArg::builder().name("args").var(true).build())
8422 .build();
8423 let mut cmd = SpecCommand::builder().name("test").build();
8424 cmd.subcommands.insert("run".to_string(), run_cmd);
8425
8426 let spec = Spec {
8427 name: "test".to_string(),
8428 bin: "test".to_string(),
8429 cmd,
8430 ..Default::default()
8431 };
8432
8433 let input = vec![
8435 "test".to_string(),
8436 "run".to_string(),
8437 "arg1".to_string(),
8438 "--".to_string(),
8439 "arg2".to_string(),
8440 "--".to_string(),
8441 "arg3".to_string(),
8442 ];
8443 let parsed = parse(&spec, &input).unwrap();
8444
8445 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8446 let value = parsed.args.get(args_arg).unwrap();
8447 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
8448 }
8449
8450 #[test]
8451 fn test_double_dashes_with_preserve() {
8452 let run_cmd = SpecCommand::builder()
8454 .name("run")
8455 .arg(
8456 SpecArg::builder()
8457 .name("args")
8458 .var(true)
8459 .double_dash(SpecDoubleDashChoices::Preserve)
8460 .build(),
8461 )
8462 .build();
8463 let mut cmd = SpecCommand::builder().name("test").build();
8464 cmd.subcommands.insert("run".to_string(), run_cmd);
8465
8466 let spec = Spec {
8467 name: "test".to_string(),
8468 bin: "test".to_string(),
8469 cmd,
8470 ..Default::default()
8471 };
8472
8473 let input = vec![
8475 "test".to_string(),
8476 "run".to_string(),
8477 "arg1".to_string(),
8478 "--".to_string(),
8479 "arg2".to_string(),
8480 "--".to_string(),
8481 "arg3".to_string(),
8482 ];
8483 let parsed = parse(&spec, &input).unwrap();
8484
8485 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8486 let value = parsed.args.get(args_arg).unwrap();
8487 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
8488 }
8489
8490 #[test]
8491 fn test_double_dashes_with_preserve_only_dashes() {
8492 let run_cmd = SpecCommand::builder()
8495 .name("run")
8496 .arg(
8497 SpecArg::builder()
8498 .name("args")
8499 .var(true)
8500 .double_dash(SpecDoubleDashChoices::Preserve)
8501 .build(),
8502 )
8503 .build();
8504 let mut cmd = SpecCommand::builder().name("test").build();
8505 cmd.subcommands.insert("run".to_string(), run_cmd);
8506
8507 let spec = Spec {
8508 name: "test".to_string(),
8509 bin: "test".to_string(),
8510 cmd,
8511 ..Default::default()
8512 };
8513
8514 let input = vec![
8516 "test".to_string(),
8517 "run".to_string(),
8518 "--".to_string(),
8519 "--".to_string(),
8520 ];
8521 let parsed = parse(&spec, &input).unwrap();
8522
8523 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8524 let value = parsed.args.get(args_arg).unwrap();
8525 assert_eq!(value.to_string(), "-- --");
8526 }
8527
8528 #[test]
8529 fn test_double_dashes_with_preserve_multiple_args() {
8530 let run_cmd = SpecCommand::builder()
8532 .name("run")
8533 .arg(SpecArg::builder().name("task").build())
8534 .arg(
8535 SpecArg::builder()
8536 .name("extra_args")
8537 .var(true)
8538 .double_dash(SpecDoubleDashChoices::Preserve)
8539 .build(),
8540 )
8541 .build();
8542 let mut cmd = SpecCommand::builder().name("test").build();
8543 cmd.subcommands.insert("run".to_string(), run_cmd);
8544
8545 let spec = Spec {
8546 name: "test".to_string(),
8547 bin: "test".to_string(),
8548 cmd,
8549 ..Default::default()
8550 };
8551
8552 let input = vec![
8555 "test".to_string(),
8556 "run".to_string(),
8557 "task1".to_string(),
8558 "--".to_string(),
8559 "arg1".to_string(),
8560 "--".to_string(),
8561 "--foo".to_string(),
8562 ];
8563 let parsed = parse(&spec, &input).unwrap();
8564
8565 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8566 let task_value = parsed.args.get(task_arg).unwrap();
8567 assert_eq!(task_value.to_string(), "task1");
8568
8569 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
8570 let extra_value = parsed.args.get(extra_arg).unwrap();
8571 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
8572 }
8573
8574 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
8575 let cmd = SpecCommand::builder().name("test").args(args).build();
8576 Spec {
8577 name: "test".to_string(),
8578 bin: "test".to_string(),
8579 cmd,
8580 ..Default::default()
8581 }
8582 }
8583
8584 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8585 let arg = parsed
8586 .args
8587 .keys()
8588 .find(|a| a.name == name)
8589 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8590 parsed.args.get(arg).unwrap().to_string()
8591 }
8592
8593 fn required_arg(name: &str) -> SpecArg {
8594 SpecArg::builder()
8595 .name(name)
8596 .var(true)
8597 .required(false)
8598 .double_dash(SpecDoubleDashChoices::Required)
8599 .build()
8600 }
8601
8602 #[test]
8603 fn test_double_dash_required_reports_error_once_for_variadic() {
8604 let spec = spec_with_args([required_arg("files")]);
8606
8607 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8608
8609 assert!(parsed.args.is_empty());
8610 assert_eq!(parsed.errors.len(), 1);
8611 assert!(
8612 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8613 );
8614 }
8615
8616 #[test]
8617 fn test_double_dash_required_suppresses_missing_arg() {
8618 let spec = spec_with_args([SpecArg::builder()
8620 .name("file")
8621 .required(true)
8622 .double_dash(SpecDoubleDashChoices::Required)
8623 .build()]);
8624
8625 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8626
8627 assert_eq!(parsed.errors.len(), 1);
8628 assert!(matches!(
8629 &parsed.errors[0],
8630 UsageErr::ArgRequiresDoubleDash(_)
8631 ));
8632 assert_eq!(
8634 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8635 Some("file")
8636 );
8637 assert!(!parsed.double_dash_seen);
8638 }
8639
8640 #[test]
8641 fn test_double_dash_routes_to_required_arg() {
8642 let spec = spec_with_args([
8645 SpecArg::builder()
8646 .name("tool")
8647 .var(true)
8648 .required(false)
8649 .build(),
8650 required_arg("command"),
8651 ]);
8652
8653 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8654
8655 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8656 assert_eq!(arg_value(&parsed, "command"), "node app.js");
8657 assert!(parsed.double_dash_seen);
8658 }
8659
8660 #[test]
8661 fn test_double_dash_routes_with_gap_reports_missing_arg() {
8662 let spec = spec_with_args([
8665 SpecArg::builder()
8666 .name("tool")
8667 .var(true)
8668 .required(true)
8669 .build(),
8670 required_arg("command"),
8671 ]);
8672
8673 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8674
8675 assert_eq!(arg_value(&parsed, "command"), "ls");
8676 assert!(parsed.args.keys().all(|a| a.name != "tool"));
8677 assert!(parsed
8678 .errors
8679 .iter()
8680 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8681 }
8682
8683 #[test]
8684 fn test_double_dash_gap_applies_defaults() {
8685 let spec = spec_with_args([
8687 SpecArg::builder()
8688 .name("tool")
8689 .var(true)
8690 .required(false)
8691 .default_value("node@20")
8692 .build(),
8693 required_arg("command"),
8694 ]);
8695
8696 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8697
8698 assert_eq!(arg_value(&parsed, "command"), "ls");
8699 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8700 }
8701
8702 fn spec_with_restart_token_and_required_arg() -> Spec {
8703 let run_cmd = SpecCommand::builder()
8704 .name("run")
8705 .arg(SpecArg::builder().name("task").build())
8706 .arg(required_arg("run_args"))
8707 .restart_token(":::".to_string())
8708 .build();
8709 let mut cmd = SpecCommand::builder().name("test").build();
8710 cmd.subcommands.insert("run".to_string(), run_cmd);
8711 Spec {
8712 name: "test".to_string(),
8713 bin: "test".to_string(),
8714 cmd,
8715 ..Default::default()
8716 }
8717 }
8718
8719 #[test]
8720 fn test_double_dash_required_restart_token_resets_separator() {
8721 let spec = spec_with_restart_token_and_required_arg();
8723
8724 let parsed = parse_partial(
8725 &spec,
8726 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8727 )
8728 .unwrap();
8729
8730 assert_eq!(arg_value(&parsed, "task"), "task2");
8731 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8732 assert_eq!(
8734 parsed
8735 .errors
8736 .iter()
8737 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8738 .count(),
8739 1
8740 );
8741 }
8742
8743 #[test]
8744 fn test_double_dash_required_restart_token_accepts_new_separator() {
8745 let spec = spec_with_restart_token_and_required_arg();
8746
8747 let parsed = parse(
8748 &spec,
8749 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8750 )
8751 .unwrap();
8752
8753 assert_eq!(arg_value(&parsed, "task"), "task2");
8754 assert_eq!(arg_value(&parsed, "run_args"), "c");
8755 }
8756
8757 #[test]
8758 fn test_double_dash_preserve_is_not_a_separator() {
8759 let spec = spec_with_args([
8762 SpecArg::builder()
8763 .name("kept")
8764 .var(true)
8765 .var_max(1)
8766 .required(false)
8767 .double_dash(SpecDoubleDashChoices::Preserve)
8768 .build(),
8769 required_arg("rest"),
8770 ]);
8771
8772 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8773
8774 assert_eq!(arg_value(&parsed, "kept"), "--");
8775 assert!(parsed.args.keys().all(|a| a.name != "rest"));
8776 assert!(!parsed.double_dash_seen);
8777 assert_eq!(parsed.errors.len(), 1);
8778 }
8779
8780 #[test]
8781 fn test_double_dash_required_does_not_bail_in_parse_partial() {
8782 let spec = spec_with_args([required_arg("file")]);
8784
8785 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8786 assert!(parse(&spec, &input(&["test", "x"])).is_err());
8787 }
8788
8789 #[test]
8790 fn test_double_dash_without_required_arg_does_not_move_cursor() {
8791 let spec = spec_with_args([
8793 SpecArg::builder().name("first").required(false).build(),
8794 SpecArg::builder().name("second").required(false).build(),
8795 ]);
8796
8797 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8798
8799 assert_eq!(arg_value(&parsed, "first"), "a");
8800 assert_eq!(arg_value(&parsed, "second"), "b");
8801 assert!(parsed.next_arg.is_none());
8802 }
8803
8804 #[test]
8805 fn test_parser_with_custom_env_for_required_arg() {
8806 let spec = spec_with_arg(
8807 SpecArg::builder()
8808 .name("name")
8809 .env("NAME")
8810 .required(true)
8811 .build(),
8812 );
8813 std::env::remove_var("NAME");
8814
8815 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8816 .expect("parse should succeed with custom env");
8817 assert_eq!(parsed.args.len(), 1);
8818 assert_eq!(first_string_value(&parsed), "john");
8819 }
8820
8821 #[test]
8822 fn test_parser_with_custom_env_for_required_flag() {
8823 let spec = spec_with_flag(
8824 SpecFlag::builder()
8825 .long("name")
8826 .env("NAME")
8827 .required(true)
8828 .arg(SpecArg::builder().name("name").build())
8829 .build(),
8830 );
8831 std::env::remove_var("NAME");
8832
8833 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8834 .expect("parse should succeed with custom env");
8835 assert_eq!(parsed.flags.len(), 1);
8836 assert_eq!(first_string_value(&parsed), "jane");
8837 }
8838
8839 #[test]
8840 fn test_flag_environment_fallbacks_preserve_declaration_order() {
8841 let spec = spec_with_flag(
8842 SpecFlag::builder()
8843 .long("name")
8844 .env("NAME")
8845 .env_fallback("OLD_NAME")
8846 .env_fallback("OLDER_NAME")
8847 .deprecated_env("DEPRECATED_NAME")
8848 .arg(SpecArg::builder().name("name").build())
8849 .build(),
8850 );
8851
8852 let parsed = parse_with_env(
8853 &spec,
8854 &["test"],
8855 &[
8856 ("NAME", "canonical"),
8857 ("OLD_NAME", "fallback"),
8858 ("DEPRECATED_NAME", "deprecated"),
8859 ],
8860 )
8861 .unwrap();
8862 assert_eq!(first_string_value(&parsed), "canonical");
8863
8864 let parsed = parse_with_env(
8865 &spec,
8866 &["test"],
8867 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8868 )
8869 .unwrap();
8870 assert_eq!(first_string_value(&parsed), "old");
8871
8872 let parsed =
8873 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8874 assert_eq!(first_string_value(&parsed), "deprecated");
8875 }
8876
8877 #[test]
8878 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8879 let spec = spec_with_flag(
8880 SpecFlag::builder()
8881 .long("name")
8882 .env("NAME")
8883 .deprecated_env("DEPRECATED_NAME")
8884 .arg(SpecArg::builder().name("name").build())
8885 .build(),
8886 );
8887
8888 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8890 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8891
8892 let parsed =
8893 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8894 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8895 assert_eq!(
8896 parsed.warnings[0].kind,
8897 crate::warn::WarningKind::DeprecatedEnv
8898 );
8899 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8900 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8901 assert_eq!(first_string_value(&parsed), "deprecated");
8903 }
8904
8905 #[test]
8906 fn a_deprecated_flag_reports_only_when_it_was_used() {
8907 let spec = spec_with_flag(
8908 SpecFlag::builder()
8909 .long("output")
8910 .deprecated("use --out")
8911 .deprecated_remove_at("3.0.0")
8912 .arg(SpecArg::builder().name("output").build())
8913 .build(),
8914 );
8915
8916 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8917 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8918
8919 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8920 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8921 assert_eq!(
8922 parsed.warnings[0].kind,
8923 crate::warn::WarningKind::DeprecatedFlag
8924 );
8925 assert_eq!(parsed.warnings[0].name, "--output");
8927 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8928 assert_eq!(
8929 parsed.warnings[0].render(),
8930 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8931 );
8932 }
8933
8934 #[test]
8935 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8936 let flag = SpecFlag::builder()
8937 .long("output")
8938 .deprecated("use --out")
8939 .deprecated_warn_at("9.0.0")
8940 .arg(SpecArg::builder().name("output").build())
8941 .build();
8942 let mut spec = spec_with_flag(flag);
8943 spec.version = Some("2.0.0".to_string());
8944
8945 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8946 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8947
8948 spec.version = Some("9.0.0".to_string());
8950 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8951 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8952 }
8953
8954 #[test]
8955 fn test_parser_with_custom_env_still_fails_when_missing() {
8956 let spec = spec_with_arg(
8957 SpecArg::builder()
8958 .name("name")
8959 .env("NAME")
8960 .required(true)
8961 .build(),
8962 );
8963 std::env::remove_var("NAME");
8964 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8965 }
8966
8967 #[test]
8968 fn test_parser_does_not_treat_env_choice_value_as_help() {
8969 let spec = spec_with_arg(
8970 SpecArg::builder()
8971 .name("env")
8972 .env("CURRENT_ENV")
8973 .choices(["dev", "staging"])
8974 .required(false)
8975 .build(),
8976 );
8977
8978 assert_parse_err(
8979 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8980 "Invalid choice for arg env: --help, expected one of dev, staging",
8981 );
8982 }
8983
8984 #[test]
8985 fn test_parser_does_not_treat_default_choice_value_as_help() {
8986 let spec = spec_with_flag(
8987 SpecFlag::builder()
8988 .long("env")
8989 .arg(
8990 SpecArg::builder()
8991 .name("env")
8992 .choices(["dev", "staging"])
8993 .build(),
8994 )
8995 .default_value("--help")
8996 .build(),
8997 );
8998
8999 assert_parse_err(
9000 parse_with_env(&spec, &["test"], &[]),
9001 "Invalid choice for option env: --help, expected one of dev, staging",
9002 );
9003 }
9004
9005 fn words(of: &[&str]) -> Vec<String> {
9007 of.iter().map(|s| s.to_string()).collect()
9008 }
9009
9010 #[test]
9011 fn a_command_that_needs_a_subcommand_says_so() {
9012 let spec: Spec = r#"
9016name "ex"
9017bin "ex"
9018cmd "gen" subcommand_required=#true {
9019 cmd "two" {}
9020 cmd "one" {}
9021 cmd "secret" hide=#true {}
9022 alias "g"
9023}
9024cmd "open" {
9025 cmd "sub" {}
9026}
9027"#
9028 .parse()
9029 .unwrap();
9030
9031 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
9032 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
9036
9037 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
9039 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
9040
9041 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
9043
9044 parse(&spec, &words(&["ex", "open"])).unwrap();
9047 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
9048 }
9049
9050 #[test]
9051 fn arg_required_else_help_observes_the_selected_commands_argv() {
9052 let spec: Spec = r#"
9053name "ex"
9054bin "ex"
9055flag "--verbose" global=#true
9056cmd "run" arg_required_else_help=#true {
9057 flag "--all"
9058}
9059"#
9060 .parse()
9061 .unwrap();
9062 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
9063
9064 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
9065 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9066
9067 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
9070 assert!(err.to_string().contains("Usage: ex run"), "{err}");
9071
9072 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
9073 }
9074
9075 #[test]
9076 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
9077 let spec: Spec = r#"
9078name "ex"
9079bin "ex"
9080unknown_flags "error"
9081external_subcommand #true
9082cmd "install"
9083flag "-v --verbose" global=#true
9084"#
9085 .parse()
9086 .unwrap();
9087
9088 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
9089 assert_eq!(
9090 parsed.external,
9091 Some(vec!["foo".into(), "--help".into(), "bar".into()])
9092 );
9093 assert!(parsed.flags.is_empty());
9094
9095 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
9097 assert_eq!(parsed.cmd.name, "install");
9098 assert!(parsed.external.is_none());
9099
9100 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
9102 assert_eq!(
9103 parsed.external,
9104 Some(vec!["foo".into(), "--verbose".into()])
9105 );
9106 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
9107
9108 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
9110
9111 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
9115 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
9116 }
9117
9118 #[test]
9119 fn an_external_subcommand_satisfies_subcommand_required() {
9120 let mut spec: Spec = r#"
9121name "ex"
9122bin "ex"
9123external_subcommand #true
9124cmd "install"
9125"#
9126 .parse()
9127 .unwrap();
9128 spec.cmd.subcommand_required = true;
9129
9130 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
9131 assert!(parse(&spec, &input(&["ex"])).is_err());
9132 }
9133
9134 #[test]
9135 fn a_default_subcommand_outranks_an_external_one() {
9136 let spec: Spec = r#"
9137name "ex"
9138bin "ex"
9139default_subcommand "run"
9140external_subcommand #true
9141cmd "run" {
9142 arg "[task]"
9143}
9144"#
9145 .parse()
9146 .unwrap();
9147
9148 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
9149 assert_eq!(parsed.cmd.name, "run");
9150 assert!(parsed.external.is_none());
9151 assert_eq!(first_string_value(&parsed), "build");
9152 }
9153
9154 #[test]
9155 fn multicall_basename_strips_a_path_and_exe() {
9156 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
9157 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
9158 assert_eq!(multicall_basename("LS.EXE"), "LS");
9159 assert_eq!(multicall_basename("busybox"), "busybox");
9160 }
9161
9162 #[test]
9163 fn a_multicall_applet_is_the_first_word() {
9164 let spec: Spec = r#"
9165name "busybox"
9166bin "busybox"
9167multicall #true
9168cmd "ls" {
9169 arg "[ARGS]" var=#true
9170}
9171cmd "cat"
9172"#
9173 .parse()
9174 .unwrap();
9175
9176 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
9178 assert_eq!(parsed.cmd.name, "ls");
9179 match parsed.args.values().next() {
9180 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
9181 other => panic!("expected ARGS to collect -l, got {other:?}"),
9182 }
9183
9184 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
9186 assert_eq!(parsed.cmd.name, "ls");
9187
9188 let mut configured = spec.clone();
9190 configured.name = "BusyBox".to_string();
9191 configured.bin = "/opt/bin/busybox.exe".to_string();
9192 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
9193 assert_eq!(parsed.cmd.name, "ls");
9194
9195 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
9197 assert_eq!(parsed.cmd.name, "ls");
9198
9199 let mut plain = spec.clone();
9201 plain.multicall = false;
9202 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
9203 assert_eq!(parsed.cmd.name, "ls");
9204 }
9205
9206 #[test]
9207 fn a_multicall_unknown_applet_can_be_external() {
9208 let spec: Spec = r#"
9209name "busybox"
9210bin "busybox"
9211multicall #true
9212unknown_flags "error"
9213external_subcommand #true
9214cmd "ls"
9215"#
9216 .parse()
9217 .unwrap();
9218
9219 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
9220 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
9221
9222 let mut closed = spec.clone();
9223 closed.cmd.external_subcommand = false;
9224 assert!(parse(&closed, &input(&["wat"])).is_err());
9225 }
9226
9227 #[cfg(feature = "unstable_choices_env")]
9228 #[test]
9229 fn test_parser_arg_choices_from_custom_env() {
9230 let spec = spec_arg_choices_env("DEPLOY_ENVS");
9231
9232 let parsed =
9233 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
9234 assert_eq!(first_string_value(&parsed), "bar");
9235
9236 assert_parse_err(
9237 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
9238 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
9239 );
9240 assert_parse_err(
9241 parse_with_env(&spec, &["test", "prod"], &[]),
9242 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
9243 );
9244 }
9245
9246 #[cfg(feature = "unstable_choices_env")]
9247 #[test]
9248 fn test_parser_validates_flag_choices_from_custom_env() {
9249 let spec = spec_flag_choices_env("DEPLOY_ENVS");
9250 let parsed = parse_with_env(
9251 &spec,
9252 &["test", "--env", "baz"],
9253 &[("DEPLOY_ENVS", "foo,bar baz")],
9254 )
9255 .unwrap();
9256 assert_eq!(first_string_value(&parsed), "baz");
9257 }
9258
9259 #[cfg(feature = "unstable_choices_env")]
9260 #[test]
9261 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
9262 let arg_env_spec = spec_with_arg(
9263 SpecArg::builder()
9264 .name("env")
9265 .env("CURRENT_ENV")
9266 .choices_env("DEPLOY_ENVS")
9267 .build(),
9268 );
9269 assert_parse_err(
9270 parse_with_env(
9271 &arg_env_spec,
9272 &["test"],
9273 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
9274 ),
9275 "Invalid choice for arg env: prod, expected one of dev, staging",
9276 );
9277
9278 let flag_default_spec = spec_with_flag(
9279 SpecFlag::builder()
9280 .long("env")
9281 .arg(
9282 SpecArg::builder()
9283 .name("env")
9284 .choices_env("DEPLOY_ENVS")
9285 .build(),
9286 )
9287 .default_value("prod")
9288 .build(),
9289 );
9290 assert_parse_err(
9291 parse_with_env(
9292 &flag_default_spec,
9293 &["test"],
9294 &[("DEPLOY_ENVS", "dev,staging")],
9295 ),
9296 "Invalid choice for option env: prod, expected one of dev, staging",
9297 );
9298 }
9299
9300 #[test]
9301 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
9302 let spec: Spec = r#"
9303 flag "-v --verbose" var=#true
9304 arg "[database]" default="myapp_dev"
9305 arg "[args...]"
9306 "#
9307 .parse()
9308 .unwrap();
9309 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9310 .into_iter()
9311 .map(String::from)
9312 .collect();
9313 let parsed = parse(&spec, &input).unwrap();
9314 let env = parsed.as_env();
9315 assert_eq!(env.get("usage_database").unwrap(), "mydb");
9316 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
9317 }
9318
9319 #[test]
9320 fn test_variadic_arg_captures_unknown_flags() {
9321 let cmd = SpecCommand::builder()
9322 .name("test")
9323 .flag(SpecFlag::builder().short('v').long("verbose").build())
9324 .arg(SpecArg::builder().name("database").required(false).build())
9325 .arg(
9326 SpecArg::builder()
9327 .name("args")
9328 .required(false)
9329 .var(true)
9330 .build(),
9331 )
9332 .build();
9333 let spec = Spec {
9334 name: "test".to_string(),
9335 bin: "test".to_string(),
9336 cmd,
9337 ..Default::default()
9338 };
9339
9340 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
9342 .into_iter()
9343 .map(String::from)
9344 .collect();
9345 let parsed = parse(&spec, &input).unwrap();
9346 assert_eq!(parsed.args.len(), 2);
9347 let args_val = parsed
9348 .args
9349 .iter()
9350 .find(|(a, _)| a.name == "args")
9351 .unwrap()
9352 .1;
9353 match args_val {
9354 ParseValue::MultiString(v) => {
9355 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9356 }
9357 _ => panic!("Expected MultiString, got {:?}", args_val),
9358 }
9359 }
9360
9361 #[test]
9362 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
9363 let cmd = SpecCommand::builder()
9364 .name("test")
9365 .flag(SpecFlag::builder().short('v').long("verbose").build())
9366 .arg(SpecArg::builder().name("database").required(false).build())
9367 .arg(
9368 SpecArg::builder()
9369 .name("args")
9370 .required(false)
9371 .var(true)
9372 .build(),
9373 )
9374 .build();
9375 let spec = Spec {
9376 name: "test".to_string(),
9377 bin: "test".to_string(),
9378 cmd,
9379 ..Default::default()
9380 };
9381
9382 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
9384 .into_iter()
9385 .map(String::from)
9386 .collect();
9387 let parsed = parse(&spec, &input).unwrap();
9388 assert_eq!(parsed.args.len(), 2);
9389 let args_val = parsed
9390 .args
9391 .iter()
9392 .find(|(a, _)| a.name == "args")
9393 .unwrap()
9394 .1;
9395 match args_val {
9396 ParseValue::MultiString(v) => {
9397 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
9398 }
9399 _ => panic!("Expected MultiString, got {:?}", args_val),
9400 }
9401 }
9402
9403 #[test]
9404 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
9405 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
9408
9409 let input: Vec<String> = vec!["test", "--option=3"]
9412 .into_iter()
9413 .map(String::from)
9414 .collect();
9415 let parsed = parse(&spec, &input).unwrap();
9416 let env = parsed.as_env();
9417 assert_eq!(
9418 env.get("usage_other_args").map(String::as_str),
9419 Some("'--option=3'"),
9420 "expected a single --option=3 token, got {:?}",
9421 env.get("usage_other_args"),
9422 );
9423
9424 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
9426 .into_iter()
9427 .map(String::from)
9428 .collect();
9429 let parsed2 = parse(&spec, &input2).unwrap();
9430 let env2 = parsed2.as_env();
9431 assert_eq!(
9432 env2.get("usage_other_args").map(String::as_str),
9433 Some("'--foo=bar' '--baz=qux'"),
9434 "expected two intact tokens, got {:?}",
9435 env2.get("usage_other_args"),
9436 );
9437
9438 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
9440 .into_iter()
9441 .map(String::from)
9442 .collect();
9443 let parsed3 = parse(&spec, &input3).unwrap();
9444 let env3 = parsed3.as_env();
9445 assert_eq!(
9446 env3.get("usage_other_args").map(String::as_str),
9447 Some("positional1 '--option=3' positional2"),
9448 "expected positional args and intact flag token, got {:?}",
9449 env3.get("usage_other_args"),
9450 );
9451 }
9452
9453 #[test]
9454 fn test_allow_hyphen_values_consumes_short_flag_collision() {
9455 let spec = r#"
9456flag "-d --working-dir <DIR>"
9457flag "-a --args <ARGS>" allow_hyphen_values=#true
9458"#
9459 .parse::<Spec>()
9460 .unwrap();
9461
9462 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9463
9464 assert_eq!(parsed.flags.len(), 1);
9465 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9466 }
9467
9468 #[test]
9469 fn test_allow_hyphen_values_consumes_embedded_long_value() {
9470 let spec = r#"
9471flag "-d --working-dir <DIR>"
9472flag "-a --args <ARGS>" allow_hyphen_values=#true
9473"#
9474 .parse::<Spec>()
9475 .unwrap();
9476
9477 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
9478
9479 assert_eq!(parsed.flags.len(), 1);
9480 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9481 }
9482
9483 #[test]
9484 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
9485 let spec = r#"
9490flag "-a --args <ARGS>" allow_hyphen_values=#true
9491arg "[rest]..."
9492"#
9493 .parse::<Spec>()
9494 .unwrap();
9495
9496 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
9497
9498 assert_eq!(flag_string_value(&parsed, "args"), "--");
9499 let rest = parsed
9500 .args
9501 .values()
9502 .next()
9503 .expect("expected the word after the separator to reach the argument");
9504 assert_eq!(rest.to_string(), "-x");
9505 }
9506
9507 #[test]
9508 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
9509 let spec = r#"
9514flag "-a --args <ARGS>..." allow_hyphen_values=#true
9515"#
9516 .parse::<Spec>()
9517 .unwrap();
9518
9519 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
9520
9521 let flag = parsed
9522 .flags
9523 .keys()
9524 .find(|flag| flag.name == "args")
9525 .expect("expected args flag");
9526 match parsed.flags.get(flag).expect("expected args value") {
9527 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
9528 other => panic!("expected a list of values, got {other:?}"),
9529 }
9530 }
9531
9532 #[test]
9533 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
9534 let spec = r#"
9535flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
9536"#
9537 .parse::<Spec>()
9538 .unwrap();
9539
9540 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
9541
9542 let flag = parsed
9543 .flags
9544 .keys()
9545 .find(|flag| flag.name == "args")
9546 .expect("expected args flag");
9547 let value = parsed.flags.get(flag).expect("expected args value");
9548 match value {
9549 ParseValue::MultiString(values) => {
9550 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
9551 }
9552 _ => panic!("expected MultiString, got {value:?}"),
9553 }
9554 }
9555
9556 #[test]
9557 fn test_require_equals_accepts_attached_and_refuses_detached() {
9558 let spec = r#"
9559flag "--inspect <PORT>" require_equals=#true
9560"#
9561 .parse::<Spec>()
9562 .unwrap();
9563
9564 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
9565 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9566
9567 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
9568 let msg = format!("{err}");
9569 assert!(
9570 msg.contains("requires an argument") || msg.contains("inspect"),
9571 "detached value must be refused: {msg}"
9572 );
9573 }
9574
9575 #[test]
9576 fn boolean_flags_can_accept_attached_values_when_enabled() {
9577 let spec: Spec = r#"
9578name "ex"
9579bin "ex"
9580flag "--color" negate="--no-color" bool_value=#true
9581arg "[rest]"
9582"#
9583 .parse()
9584 .unwrap();
9585
9586 for (token, expected) in [
9587 ("--color", true),
9588 ("--color=true", true),
9589 ("--color=false", false),
9590 ("--no-color", false),
9591 ("--no-color=false", true),
9592 ] {
9593 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9594 assert!(
9595 matches!(
9596 parsed.flags.get(&spec.cmd.flags[0]),
9597 Some(ParseValue::Bool(value)) if *value == expected
9598 ),
9599 "{token}"
9600 );
9601 }
9602
9603 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9604 assert!(matches!(
9605 parsed.args.get(&spec.cmd.args[0]),
9606 Some(ParseValue::String(value)) if value == "word"
9607 ));
9608 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9609 assert!(err.to_string().contains("expected `true` or `false`"));
9610
9611 let strict: Spec = r#"
9612name "ex"
9613bin "ex"
9614args_override_self #false
9615flag "--color" negate="--no-color" bool_value=#true
9616"#
9617 .parse()
9618 .unwrap();
9619 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9620 let parsed = parse(
9621 &strict,
9622 &input(&["ex", "--color=false", "--no-color=false"]),
9623 )
9624 .unwrap();
9625 assert!(matches!(
9626 parsed.flags.get(&strict.cmd.flags[0]),
9627 Some(ParseValue::Bool(true))
9628 ));
9629 }
9630
9631 #[test]
9632 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9633 let spec = r#"
9634flag "-a --all"
9635flag "-i --inspect <PORT>" require_equals=#true
9636"#
9637 .parse::<Spec>()
9638 .unwrap();
9639
9640 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9641 let msg = format!("{err}");
9642 assert!(
9643 msg.contains("requires an argument") || msg.contains("inspect"),
9644 "bundled short must refuse the following word: {msg}"
9645 );
9646 }
9647
9648 #[test]
9649 fn test_default_missing_binds_when_the_value_is_left_off() {
9650 let spec = r#"
9651flag "-c --color <WHEN>" default_missing="always"
9652flag "-v --verbose"
9653"#
9654 .parse::<Spec>()
9655 .unwrap();
9656
9657 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9658 assert_eq!(flag_string_value(&parsed, "color"), "always");
9659
9660 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9661 assert_eq!(flag_string_value(&parsed, "color"), "never");
9662
9663 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9664 assert_eq!(flag_string_value(&parsed, "color"), "never");
9665
9666 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9667 assert_eq!(flag_string_value(&parsed, "color"), "always");
9668 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9669
9670 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9671 assert_eq!(flag_string_value(&parsed, "color"), "");
9672
9673 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9674 assert_eq!(flag_string_value(&parsed, "color"), "never");
9675
9676 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9677 assert_eq!(flag_string_value(&parsed, "color"), "always");
9678 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9679 }
9680
9681 #[test]
9682 fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
9683 let spec = r#"
9684flag "--apps <N>"
9685flag "--jobs <N>" default_missing="default missing"
9686flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
9687"#
9688 .parse::<Spec>()
9689 .unwrap();
9690
9691 let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
9692 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9693
9694 let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
9695 assert!(
9696 err.to_string().contains("unexpected word: -1"),
9697 "default_missing must keep an unopted negative value separate: {err}"
9698 );
9699
9700 let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
9701 assert_eq!(flag_string_value(&parsed, "kids"), "-1");
9702
9703 let external_spec = r#"
9704external_subcommand #true
9705flag "--apps <N>"
9706"#
9707 .parse::<Spec>()
9708 .unwrap();
9709 let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
9710 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9711 assert!(parsed.external.is_none());
9712
9713 for words in [
9714 &["test", "--apps", "--jobs", "1"][..],
9715 &["test", "--apps", "--kids", "-1"][..],
9716 ] {
9717 let err = parse(&spec, &input(words)).unwrap_err();
9718 let message = err.to_string();
9719 assert!(
9720 message.contains("--apps") && message.contains("requires an argument"),
9721 "the earlier flag must report its missing value for {words:?}: {message}"
9722 );
9723 }
9724 }
9725
9726 #[test]
9727 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9728 let spec = r#"
9729flag "--bump [LEVEL]" value_optional=#true
9730flag "--verbose"
9731arg "[FILE]"
9732"#
9733 .parse::<Spec>()
9734 .unwrap();
9735
9736 let absent = parse(&spec, &input(&["test"])).unwrap();
9737 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9738
9739 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9740 let bump = bare
9741 .flags
9742 .iter()
9743 .find(|(flag, _)| flag.name == "bump")
9744 .map(|(_, value)| value)
9745 .unwrap();
9746 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9747 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9748 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9749
9750 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9751 assert_eq!(flag_string_value(&explicit, "bump"), "");
9752
9753 let corrected = parse(
9754 &spec,
9755 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9756 )
9757 .unwrap();
9758 let bump = corrected
9759 .flags
9760 .iter()
9761 .find(|(flag, _)| flag.name == "bump")
9762 .map(|(_, value)| value)
9763 .unwrap();
9764 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9765
9766 let collecting = r#"
9767flag "--tag [TAG]..." value_optional=#true
9768flag "--verbose"
9769"#
9770 .parse::<Spec>()
9771 .unwrap();
9772 let valued = parse(
9773 &collecting,
9774 &input(&["test", "--tag", "one", "two", "--verbose"]),
9775 )
9776 .unwrap();
9777 let tag = valued
9778 .flags
9779 .iter()
9780 .find(|(flag, _)| flag.name == "tag")
9781 .map(|(_, value)| value)
9782 .unwrap();
9783 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9784 }
9785
9786 #[test]
9787 fn test_repeatable_bare_optional_values_count_each_occurrence() {
9788 let spec = r#"
9789flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9790"#
9791 .parse::<Spec>()
9792 .unwrap();
9793
9794 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9795 let tag = parsed
9796 .flags
9797 .iter()
9798 .find(|(flag, _)| flag.name == "tag")
9799 .map(|(_, value)| value)
9800 .unwrap();
9801 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9802
9803 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9804 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9805 }
9806
9807 #[test]
9808 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9809 let spec = r#"
9810flag "--tag [TAG]..." var=#true value_optional=#true
9811flag "--verbose"
9812"#
9813 .parse::<Spec>()
9814 .unwrap();
9815
9816 for argv in [
9817 &["test", "--tag", "one", "two"][..],
9818 &["test", "--tag", "one", "two", "--verbose"][..],
9819 &["test", "--tag", "one", "--tag", "two"][..],
9820 ] {
9821 let parsed = parse(&spec, &input(argv)).unwrap();
9822 let tag = parsed
9823 .flags
9824 .iter()
9825 .find(|(flag, _)| flag.name == "tag")
9826 .map(|(_, value)| value)
9827 .unwrap();
9828 assert!(
9829 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9830 "argv={argv:?}: {tag:?}"
9831 );
9832 }
9833
9834 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9835 let tag = bare
9836 .flags
9837 .iter()
9838 .find(|(flag, _)| flag.name == "tag")
9839 .map(|(_, value)| value)
9840 .unwrap();
9841 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9842 }
9843
9844 #[test]
9845 fn test_default_missing_with_require_equals_refuses_the_following_word() {
9846 let spec = r#"
9847flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9848arg "[rest]"
9849"#
9850 .parse::<Spec>()
9851 .unwrap();
9852
9853 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9854 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9855
9856 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9857 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9858
9859 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9861 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9862 assert_eq!(
9863 parsed
9864 .args
9865 .values()
9866 .next()
9867 .map(|v| v.to_string())
9868 .as_deref(),
9869 Some("80")
9870 );
9871
9872 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9873 assert_eq!(flag_string_value(&parsed, "inspect"), "");
9874 }
9875
9876 #[test]
9877 fn test_default_missing_must_be_a_choice() {
9878 let spec = r#"
9879flag "--color <WHEN>" default_missing="always" {
9880 choices "auto" "always" "never"
9881}
9882"#
9883 .parse::<Spec>()
9884 .unwrap();
9885
9886 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9887 assert_eq!(flag_string_value(&parsed, "color"), "always");
9888
9889 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9890 assert_eq!(flag_string_value(&parsed, "color"), "never");
9891
9892 let spec = r#"
9893flag "--color <WHEN>" default_missing="wat" {
9894 choices "auto" "always" "never"
9895}
9896"#
9897 .parse::<Spec>()
9898 .unwrap();
9899
9900 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9901 let msg = format!("{err}");
9902 assert!(
9903 msg.contains("Invalid choice for option color: wat"),
9904 "missing default has to pass choices the same way a typed value does: {msg}"
9905 );
9906
9907 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9908 let msg = format!("{err}");
9909 assert!(
9910 msg.contains("Invalid choice for option color: wat"),
9911 "an attached value that is not a choice is still refused: {msg}"
9912 );
9913
9914 let spec = r#"
9915flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9916 choices "9229" "80"
9917}
9918arg "[rest]"
9919"#
9920 .parse::<Spec>()
9921 .unwrap();
9922
9923 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9924 let msg = format!("{err}");
9925 assert!(
9926 msg.contains("Invalid choice for option inspect: wat"),
9927 "require_equals still binds the missing string, so the error is the choice: {msg}"
9928 );
9929 }
9930
9931 #[test]
9932 fn test_hyphen_values_still_start_short_flag_parsing() {
9933 let spec = r#"
9934flag "-d --working-dir <DIR>"
9935flag "-a --args <ARGS>"
9936"#
9937 .parse::<Spec>()
9938 .unwrap();
9939
9940 let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
9941 let message = err.to_string();
9942 assert!(
9943 message.contains("--args") && message.contains("requires an argument"),
9944 "the recognized -d must leave the earlier -a missing: {message}"
9945 );
9946 }
9947
9948 mod available_flags {
9951 use super::*;
9952
9953 fn spec() -> Spec {
9954 r#"
9955bin "test"
9956flag "-v --verbose" global=#true
9957flag "--raw" global=#true effect="write"
9958flag "--local-only"
9959cmd "run" {
9960 flag "-r --raw"
9961 flag "-w --watch"
9962 cmd "once"
9963}
9964"#
9965 .parse::<Spec>()
9966 .unwrap()
9967 }
9968
9969 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9970 let mut chain = vec![&spec.cmd];
9971 for segment in path {
9972 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9973 }
9974 chain
9975 }
9976
9977 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9978 let mut names: Vec<_> = available_flags(&chain(spec, path))
9979 .iter()
9980 .map(|f| f.name.clone())
9981 .collect();
9982 names.sort();
9983 names
9984 }
9985
9986 #[test]
9987 fn an_empty_chain_yields_nothing() {
9988 assert!(available_flags(&[]).is_empty());
9989 }
9990
9991 #[test]
9992 fn the_root_gets_its_own_flags() {
9993 let spec = spec();
9994 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9995 }
9996
9997 #[test]
9998 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9999 let spec = spec();
10000 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
10001 }
10002
10003 #[test]
10004 fn a_re_declared_global_is_listed_once() {
10005 let spec = r#"
10008bin "test"
10009flag "-y --yes" global=#true effect="write"
10010cmd "rm" {
10011 flag "-y --yes"
10012}
10013"#
10014 .parse::<Spec>()
10015 .unwrap();
10016 let flags = available_flags(&chain(&spec, &["rm"]));
10017 assert_eq!(flags.len(), 1, "{flags:?}");
10018 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
10019 }
10020
10021 #[test]
10022 fn a_re_declared_global_keeps_the_globals_declaration() {
10023 let spec = spec();
10027 let flags = available_flags(&chain(&spec, &["run"]));
10028 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
10029 assert!(raw.global);
10030 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
10031 assert_eq!(raw.short, ['r']);
10032 }
10033
10034 #[test]
10035 fn it_matches_what_a_parse_accepts() {
10036 let spec = spec();
10039 for path in [vec![], vec!["run"], vec!["run", "once"]] {
10040 let argv = std::iter::once("test".to_string())
10041 .chain(path.iter().map(|s| s.to_string()))
10042 .collect::<Vec<_>>();
10043 let parsed = parse_partial(&spec, &argv).unwrap();
10044
10045 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
10046 .map(|f| f.name.clone())
10047 .collect();
10048 from_parse.sort();
10049 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
10050 }
10051 }
10052 }
10053
10054 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
10059 parsed
10060 .tokens
10061 .iter()
10062 .find(|token| token.index == index)
10063 .unwrap_or_else(|| panic!("no token at {index}"))
10064 .roles
10065 .iter()
10066 .map(render_role)
10067 .collect()
10068 }
10069
10070 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
10071 parsed
10072 .flag_origins
10073 .iter()
10074 .find(|(f, _)| f.name == flag)
10075 .map(|(_, origins)| origins.clone())
10076 .unwrap_or_default()
10077 }
10078
10079 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
10080 let env = env
10081 .iter()
10082 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
10083 .collect();
10084 Parser::new(spec)
10085 .with_env(env)
10086 .explain(&input(words))
10087 .unwrap()
10088 }
10089
10090 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
10091 explain_with_env(spec, words, &[])
10092 }
10093
10094 #[test]
10095 fn an_attached_long_value_is_recorded_on_the_flag_token() {
10096 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10097 .parse()
10098 .unwrap();
10099
10100 let parsed = explain(&spec, &["ex", "--env=prod"]);
10101
10102 assert_eq!(roles(&parsed, 0), ["program"]);
10103 assert_eq!(
10104 roles(&parsed, 1),
10105 ["flag env as --env", "value of env = [\"prod\"], attached"]
10106 );
10107 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
10110 }
10111
10112 #[test]
10113 fn a_detached_long_value_is_recorded_on_its_own_token() {
10114 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
10115 .parse()
10116 .unwrap();
10117
10118 let parsed = explain(&spec, &["ex", "--env", "prod"]);
10119
10120 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
10121 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
10122 }
10123
10124 #[test]
10125 fn a_short_bundle_is_attributed_to_the_bundle_token() {
10126 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
10127 .parse()
10128 .unwrap();
10129
10130 let parsed = explain(&spec, &["ex", "-abj8"]);
10131
10132 assert_eq!(
10135 roles(&parsed, 1),
10136 [
10137 "flag a as -a",
10138 "flag b as -b",
10139 "flag j as -j",
10140 "value of j = [\"8\"], attached",
10141 ]
10142 );
10143 assert_eq!(parsed.tokens.len(), 2);
10144 }
10145
10146 #[test]
10147 fn a_delimiter_splits_one_token_into_several_values() {
10148 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
10149 .parse()
10150 .unwrap();
10151
10152 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
10153
10154 assert_eq!(
10155 roles(&parsed, 2),
10156 ["value of tags = [\"a\", \"b\", \"c\"]"],
10157 "the values meant, not the word typed"
10158 );
10159 }
10160
10161 #[test]
10162 fn a_separator_and_the_words_after_it_are_distinguished() {
10163 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
10164 .parse()
10165 .unwrap();
10166
10167 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
10168
10169 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
10170 assert_eq!(roles(&parsed, 2), ["separator"]);
10171 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
10173 }
10174
10175 #[test]
10176 fn a_second_separator_is_data() {
10177 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
10178 .parse()
10179 .unwrap();
10180
10181 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
10182
10183 assert_eq!(roles(&parsed, 1), ["separator"]);
10184 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
10185 }
10186
10187 #[test]
10188 fn an_unknown_flag_says_what_took_it() {
10189 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10190 .parse()
10191 .unwrap();
10192
10193 let parsed = explain(&spec, &["ex", "--wat"]);
10194
10195 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
10198 }
10199
10200 #[test]
10201 fn a_subcommand_word_is_not_a_positional() {
10202 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
10203 .parse()
10204 .unwrap();
10205
10206 let parsed = explain(&spec, &["ex", "build", "a"]);
10207
10208 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
10209 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
10210 }
10211
10212 #[test]
10213 fn a_multicall_applet_is_read_at_argv0() {
10214 let spec: Spec =
10215 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
10216 .parse()
10217 .unwrap();
10218
10219 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
10220
10221 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
10224 assert!(parsed.tokens[0].synthesized);
10225 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
10226 }
10227
10228 #[test]
10229 fn words_the_parse_never_reached_say_so() {
10230 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
10231 .parse()
10232 .unwrap();
10233
10234 let parsed = Parser::new(&spec)
10235 .explain(&input(&["ex", "--help", "a"]))
10236 .unwrap();
10237
10238 assert_eq!(roles(&parsed, 2), ["unread"]);
10239 }
10240
10241 #[test]
10242 fn an_env_origin_names_the_variable_that_fired() {
10243 let spec: Spec =
10244 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
10245 .parse()
10246 .unwrap();
10247
10248 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
10249 assert_eq!(
10250 origins(&primary, "token"),
10251 [ValueOrigin::Env("EX_TOKEN".to_string())]
10252 );
10253
10254 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
10257 assert_eq!(
10258 origins(&fallback, "token"),
10259 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
10260 );
10261 }
10262
10263 #[test]
10264 fn a_default_origin_is_recorded_for_flags_and_args() {
10265 let spec: Spec =
10266 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
10267 .parse()
10268 .unwrap();
10269
10270 let parsed = explain(&spec, &["ex"]);
10271
10272 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
10273 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
10274 assert_eq!(arg.name, "src");
10275 assert_eq!(origins, &[ValueOrigin::Default]);
10276 }
10277
10278 #[test]
10279 fn a_default_if_origin_carries_the_condition_that_fired() {
10280 let spec: Spec = r#"
10281name "ex"
10282bin "ex"
10283flag "--profile <p>"
10284flag "--strict" {
10285 default_if "--profile" "prod" "true"
10286}
10287 "#
10288 .parse()
10289 .unwrap();
10290
10291 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
10292
10293 assert_eq!(
10296 origins(&parsed, "strict"),
10297 [ValueOrigin::DefaultIf {
10298 selector: "--profile".to_string(),
10299 when: Some("prod".to_string()),
10300 }]
10301 );
10302 }
10303
10304 #[test]
10305 fn a_bare_optional_value_flag_records_default_missing() {
10306 let spec: Spec =
10307 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
10308 .parse()
10309 .unwrap();
10310
10311 let parsed = explain(&spec, &["ex", "--color", "-v"]);
10312
10313 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
10316 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10317 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
10318 }
10319
10320 #[test]
10321 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
10322 let spec: Spec =
10323 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
10324 .parse()
10325 .unwrap();
10326
10327 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
10328
10329 assert_eq!(
10331 roles(&parsed, 1),
10332 [
10333 "flag color as --color",
10334 "value of color = [\"red\"], attached"
10335 ]
10336 );
10337 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
10338 }
10339
10340 #[test]
10341 fn an_override_names_the_flag_that_did_it() {
10342 let spec: Spec =
10343 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
10344 .parse()
10345 .unwrap();
10346
10347 let parsed = explain(&spec, &["ex", "--loud"]);
10348
10349 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
10352 assert!(origins(&parsed, "quiet").is_empty());
10353 }
10354
10355 #[test]
10356 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
10357 let spec: Spec = r#"
10358name "ex"
10359bin "ex"
10360cmd "run" restart_token=":::" {
10361 arg "<task>" default="build"
10362}
10363 "#
10364 .parse()
10365 .unwrap();
10366
10367 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
10368
10369 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
10373 assert_eq!(roles(&parsed, 3), ["restart"]);
10376 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
10377 assert!(parsed.arg_origins.is_empty());
10378 }
10379
10380 #[test]
10381 fn a_value_terminator_says_which_run_it_ended() {
10382 let spec: Spec = r#"
10383name "ex"
10384bin "ex"
10385flag "--exec <cmd>..." value_terminator=";"
10386arg "<src>"
10387 "#
10388 .parse()
10389 .unwrap();
10390
10391 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
10392
10393 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
10394 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
10397 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
10398 }
10399
10400 #[test]
10401 fn an_args_value_terminator_says_which_run_it_ended() {
10402 let spec: Spec = r#"
10403name "ex"
10404bin "ex"
10405arg "<files>..." value_terminator=";"
10406arg "[dest]"
10407 "#
10408 .parse()
10409 .unwrap();
10410
10411 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
10412
10413 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
10414 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
10415 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
10416 }
10417
10418 #[test]
10419 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
10420 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
10421 .parse()
10422 .unwrap();
10423
10424 let parsed = Parser::new(&spec)
10425 .explain(&input(&["ex", "--env=prod"]))
10426 .unwrap();
10427
10428 assert!(Parser::new(&spec)
10431 .parse(&input(&["ex", "--env=prod"]))
10432 .is_err());
10433 assert_eq!(
10434 roles(&parsed, 1),
10435 ["flag env as --env", "value of env = [\"prod\"], attached"]
10436 );
10437 assert!(
10438 parsed.errors.iter().any(|e| e.to_string().contains("src")),
10439 "{:?}",
10440 parsed.errors
10441 );
10442 }
10443
10444 #[test]
10445 fn an_external_subcommand_forwards_whole_tokens() {
10446 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
10447 .parse()
10448 .unwrap();
10449
10450 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
10451
10452 assert_eq!(roles(&parsed, 1), ["external"]);
10453 assert_eq!(roles(&parsed, 2), ["external"]);
10454 }
10455
10456 #[test]
10457 fn a_view_keeps_the_callers_argv_positions() {
10458 let spec: Spec = r#"
10459bin "ex"
10460view "runner" root="run"
10461cmd "run" {
10462 flag "--token <token>"
10463}
10464 "#
10465 .parse()
10466 .unwrap();
10467
10468 let parsed = explain(&spec, &["runner", "--token", "secret"]);
10469
10470 assert_eq!(roles(&parsed, 0), ["program"]);
10473 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
10474 }
10475}