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}
359
360#[derive(Debug, Clone)]
362#[non_exhaustive]
363pub struct TokenBinding {
364 pub index: usize,
366 pub word: String,
367 pub synthesized: bool,
371 pub roles: Vec<TokenRole>,
372}
373
374#[non_exhaustive]
375pub struct ParseOutput {
376 pub cmd: SpecCommand,
377 pub cmds: Vec<SpecCommand>,
378 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
379 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
380 pub tokens: Vec<TokenBinding>,
387 pub flag_origins: IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
393 pub arg_origins: IndexMap<Arc<SpecArg>, Vec<ValueOrigin>>,
396 pub overridden_flags: BTreeMap<String, String>,
402 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
409 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
410 pub errors: Vec<UsageErr>,
411 pub warnings: Vec<Warning>,
415 pub next_arg: Option<Arc<SpecArg>>,
422 pub double_dash_seen: bool,
428 pub external: Option<Vec<String>>,
433}
434
435impl ParseOutput {
436 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
445 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
446 return self.available_flags.clone();
447 };
448 let start = match boundary.checked_sub(1) {
452 Some(prev) if self.cmds[prev].flags_from_mount => prev,
453 _ => boundary,
454 };
455 let mut offered = gather_flags(&self.cmds[start]);
459 for cmd in &self.cmds[start + 1..] {
460 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
461 }
462 offered
463 }
464}
465
466#[derive(Debug, Clone)]
467pub enum ParseValue {
468 Bool(bool),
469 String(String),
470 MultiBool(Vec<bool>),
471 MultiString(Vec<String>),
472}
473
474impl ParseValue {
475 pub fn try_as_bool(self) -> Option<bool> {
476 match self {
477 Self::Bool(value) => Some(value),
478 _ => None,
479 }
480 }
481
482 pub const fn try_as_bool_ref(&self) -> Option<&bool> {
483 match self {
484 Self::Bool(value) => Some(value),
485 _ => None,
486 }
487 }
488
489 pub fn try_as_bool_mut(&mut self) -> Option<&mut bool> {
490 match self {
491 Self::Bool(value) => Some(value),
492 _ => None,
493 }
494 }
495
496 pub fn try_as_string(self) -> Option<String> {
497 match self {
498 Self::String(value) => Some(value),
499 _ => None,
500 }
501 }
502
503 pub const fn try_as_string_ref(&self) -> Option<&String> {
504 match self {
505 Self::String(value) => Some(value),
506 _ => None,
507 }
508 }
509
510 pub fn try_as_string_mut(&mut self) -> Option<&mut String> {
511 match self {
512 Self::String(value) => Some(value),
513 _ => None,
514 }
515 }
516
517 pub fn try_as_multi_bool(self) -> Option<Vec<bool>> {
518 match self {
519 Self::MultiBool(value) => Some(value),
520 _ => None,
521 }
522 }
523
524 pub const fn try_as_multi_bool_ref(&self) -> Option<&Vec<bool>> {
525 match self {
526 Self::MultiBool(value) => Some(value),
527 _ => None,
528 }
529 }
530
531 pub fn try_as_multi_bool_mut(&mut self) -> Option<&mut Vec<bool>> {
532 match self {
533 Self::MultiBool(value) => Some(value),
534 _ => None,
535 }
536 }
537
538 pub fn try_as_multi_string(self) -> Option<Vec<String>> {
539 match self {
540 Self::MultiString(value) => Some(value),
541 _ => None,
542 }
543 }
544
545 pub const fn try_as_multi_string_ref(&self) -> Option<&Vec<String>> {
546 match self {
547 Self::MultiString(value) => Some(value),
548 _ => None,
549 }
550 }
551
552 pub fn try_as_multi_string_mut(&mut self) -> Option<&mut Vec<String>> {
553 match self {
554 Self::MultiString(value) => Some(value),
555 _ => None,
556 }
557 }
558}
559
560fn collect_deprecations(out: &mut ParseOutput) {
571 for cmd in out.cmds.iter().skip(1) {
572 if cmd.deprecated.is_none()
573 && cmd.deprecated_warn_at.is_none()
574 && cmd.deprecated_remove_at.is_none()
575 {
576 continue;
577 }
578 out.warnings.push(Warning::command(
579 cmd.name.clone(),
580 cmd.deprecated.clone(),
581 cmd.deprecated_warn_at.clone(),
582 cmd.deprecated_remove_at.clone(),
583 ));
584 }
585 for flag in out.flags.keys() {
586 if let Some(warning) = flag_deprecation(flag) {
587 out.warnings.push(warning);
588 }
589 }
590}
591
592fn flag_deprecation(flag: &SpecFlag) -> Option<Warning> {
594 if flag.deprecated.is_none()
595 && flag.deprecated_warn_at.is_none()
596 && flag.deprecated_remove_at.is_none()
597 {
598 return None;
599 }
600 Some(Warning::flag(
601 flag_spelling(flag),
602 flag.deprecated.clone(),
603 flag.deprecated_warn_at.clone(),
604 flag.deprecated_remove_at.clone(),
605 ))
606}
607
608fn flag_spelling(flag: &SpecFlag) -> String {
611 flag.long
612 .first()
613 .map(|long| format!("--{long}"))
614 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
615 .unwrap_or_else(|| flag.name.clone())
616}
617
618fn flag_current_env(flag: &SpecFlag) -> Option<String> {
620 flag.env
621 .clone()
622 .or_else(|| flag.env_fallback.first().cloned())
623}
624
625fn flag_env_is_deprecated(flag: &SpecFlag, name: &str) -> bool {
626 flag.deprecated_env.iter().any(|declared| declared == name)
627}
628
629fn arg_current_env(arg: &SpecArg) -> Option<String> {
631 arg.env
632 .clone()
633 .or_else(|| arg.env_fallback.first().cloned())
634}
635
636fn arg_env_is_deprecated(arg: &SpecArg, name: &str) -> bool {
637 arg.deprecated_env.iter().any(|declared| declared == name)
638}
639
640fn first_set_env<'a>(
647 mut names: impl Iterator<Item = &'a str>,
648 get_env: &impl Fn(&str) -> Option<String>,
649) -> Option<(&'a str, String)> {
650 names.find_map(|name| get_env(name).map(|value| (name, value)))
651}
652
653#[non_exhaustive]
673pub struct Parser<'a> {
674 spec: &'a Spec,
675 env: Option<HashMap<String, String>>,
676 mount_outputs: Option<HashMap<String, String>>,
677}
678
679impl<'a> Parser<'a> {
680 pub fn new(spec: &'a Spec) -> Self {
682 Self {
683 spec,
684 env: None,
685 mount_outputs: None,
686 }
687 }
688
689 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
694 self.env = Some(env);
695 self
696 }
697
698 pub fn with_mount_outputs(mut self, outputs: HashMap<String, String>) -> Self {
705 self.mount_outputs = Some(outputs);
706 self
707 }
708
709 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
713 let out = self.parse_collecting(input)?;
714 if let Some(err) = out
715 .errors
716 .iter()
717 .find(|e| matches!(e, UsageErr::Help(_) | UsageErr::Version(_)))
718 {
719 bail!("{err}");
720 }
721 if !out.errors.is_empty() {
722 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
723 }
724 Ok(out)
725 }
726
727 pub fn explain(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
738 self.parse_collecting(input)
739 }
740
741 pub fn explain_refused(self, input: &[String]) -> Result<ParseOutput, Vec<TokenBinding>> {
756 let mut trace = Trace::new(input);
757 match parse_partial_traced(
758 self.spec,
759 input,
760 self.env.as_ref(),
761 self.mount_outputs.as_ref(),
762 MountTiming::WhenAWordIsUnknown,
763 &mut trace,
764 ) {
765 Ok((out, _)) => Ok(out),
768 Err(_) => Err(trace.tokens),
769 }
770 }
771
772 fn parse_collecting(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
773 let custom_env = self.env.as_ref();
774 let (mut out, overridden_flags) = parse_partial_with_env(
775 self.spec,
776 input,
777 custom_env,
778 self.mount_outputs.as_ref(),
779 MountTiming::WhenAWordIsUnknown,
780 )?;
781 trace!("{out:?}");
782
783 while try_bind_default_missing(
789 &mut out.flags,
790 &mut out.flag_awaiting_value,
791 custom_env,
792 &mut out.flag_origins,
793 )? {}
794 if let Some(flag) = out.flag_awaiting_value.first() {
795 let token = flag
796 .long
797 .first()
798 .map(|l| format!("--{l}"))
799 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
800 .unwrap_or_else(|| flag.name.clone());
801 let rendered = input.join(" ");
802 let span = rendered
803 .rfind(&token)
804 .map(|at| (at, token.len()))
805 .unwrap_or((0, 0));
806 return Err(UsageErr::InvalidFlag {
807 token,
808 reason: "requires an argument".to_string(),
809 span: span.into(),
810 input: rendered,
811 }
812 .into());
813 }
814
815 collect_deprecations(&mut out);
819
820 let get_env = |key: &str| -> Option<String> {
821 if let Some(env_map) = custom_env {
822 env_map.get(key).cloned()
823 } else {
824 std::env::var(key).ok()
825 }
826 };
827
828 for arg in out.cmd.args.iter() {
833 if out.args.contains_key(arg) {
834 continue;
835 }
836 if let Some((env_name, env_value)) = first_set_env(arg.env_names(), &get_env) {
837 if arg_env_is_deprecated(arg, env_name) {
838 out.warnings
839 .push(Warning::env(env_name, arg_current_env(arg)));
840 }
841 let values = split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
842 validate_choice_values(
843 ChoiceTarget::arg(arg),
844 &values,
845 arg.choices.as_ref(),
846 custom_env,
847 )?;
848 let parsed = if arg.var {
849 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
850 ParseValue::MultiString(values)
851 } else {
852 ParseValue::String(values.into_iter().next().unwrap_or_default())
853 };
854 out.args.insert(Arc::new(arg.clone()), parsed);
855 out.arg_origins
856 .entry(Arc::new(arg.clone()))
857 .or_default()
858 .push(ValueOrigin::Env(env_name.to_string()));
859 continue;
860 }
861 if !arg.default.is_empty() {
862 if arg.var {
864 let values = split_fallback_values(&arg.default, arg.delimiter);
865 validate_arg_fallback_count(arg, values.len(), &mut out.errors);
866 validate_choice_values(
867 ChoiceTarget::arg(arg),
868 &values,
869 arg.choices.as_ref(),
870 custom_env,
871 )?;
872 out.args
874 .insert(Arc::new(arg.clone()), ParseValue::MultiString(values));
875 out.arg_origins
876 .entry(Arc::new(arg.clone()))
877 .or_default()
878 .push(ValueOrigin::Default);
879 } else {
880 validate_choice_value(
881 ChoiceTarget::arg(arg),
882 &arg.default[0],
883 arg.choices.as_ref(),
884 custom_env,
885 )?;
886 out.args.insert(
888 Arc::new(arg.clone()),
889 ParseValue::String(arg.default[0].clone()),
890 );
891 out.arg_origins
892 .entry(Arc::new(arg.clone()))
893 .or_default()
894 .push(ValueOrigin::Default);
895 }
896 }
897 }
898
899 let flags: Vec<Arc<SpecFlag>> = out.available_flags.values().cloned().collect();
904 for flag in &flags {
905 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
906 continue;
907 }
908 if let Some((env_name, env_value)) = first_set_env(flag.env_names(), &get_env) {
909 if let Some(warning) = flag_deprecation(flag) {
913 out.warnings.push(warning);
914 }
915 if flag_env_is_deprecated(flag, env_name) {
916 out.warnings
917 .push(Warning::env(env_name, flag_current_env(flag)));
918 }
919 if let Some(arg) = flag.arg.as_ref() {
920 let values =
921 split_fallback_values(std::slice::from_ref(&env_value), arg.delimiter);
922 validate_choice_values(
923 ChoiceTarget::option(flag),
924 &values,
925 arg.choices.as_ref(),
926 custom_env,
927 )?;
928 let parsed = if flag.var || arg.var {
929 if flag.var {
930 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
931 }
932 if arg.var {
933 validate_flag_arg_fallback_count(
934 flag,
935 arg,
936 values.len(),
937 &mut out.errors,
938 );
939 }
940 ParseValue::MultiString(values)
941 } else {
942 ParseValue::String(values.into_iter().next().unwrap_or_default())
943 };
944 out.flags.insert(Arc::clone(flag), parsed);
945 } else {
946 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
947 out.flags
948 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
949 }
950 out.flag_origins
951 .entry(Arc::clone(flag))
952 .or_default()
953 .push(ValueOrigin::Env(env_name.to_string()));
954 }
955 }
956 let mut from_default_if: Vec<(Arc<SpecFlag>, crate::SpecDefaultIf)> = Vec::new();
962 for flag in &flags {
963 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
964 continue;
965 }
966 if let Some(condition) = flag.default_if.iter().find(|condition| {
967 default_if_condition_matches(condition, &out, &overridden_flags, custom_env)
968 }) {
969 from_default_if.push((Arc::clone(flag), condition.clone()));
970 }
971 }
972 for (flag, condition) in &from_default_if {
973 bind_flag_fallback(
977 flag,
978 std::slice::from_ref(&condition.value),
979 &mut out,
980 custom_env,
981 ValueOrigin::DefaultIf {
982 selector: condition.selector.clone(),
983 when: condition.when.clone(),
984 },
985 )?;
986 }
987 for flag in &flags {
988 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
989 continue;
990 }
991 if !flag.default.is_empty() {
992 bind_flag_fallback(
993 flag,
994 &flag.default,
995 &mut out,
996 custom_env,
997 ValueOrigin::Default,
998 )?;
999 continue;
1000 }
1001 if let Some(arg) = flag.arg.as_ref() {
1002 if !arg.default.is_empty() {
1003 bind_flag_fallback(
1004 flag,
1005 &arg.default,
1006 &mut out,
1007 custom_env,
1008 ValueOrigin::Default,
1009 )?;
1010 }
1011 }
1012 }
1013 for (arg, parsed) in &out.args {
1018 validate_expression(
1019 &arg.name,
1020 arg.validate.as_deref(),
1021 arg.validate_error.as_deref(),
1022 parsed,
1023 &mut out.errors,
1024 );
1025 }
1026 for (flag, parsed) in &out.flags {
1027 if let Some(arg) = &flag.arg {
1028 validate_expression(
1029 &flag.name,
1030 arg.validate.as_deref(),
1031 arg.validate_error.as_deref(),
1032 parsed,
1033 &mut out.errors,
1034 );
1035 }
1036 }
1037 crate::warn::retain_reached(&mut out.warnings, self.spec.version.as_deref());
1040 Ok(out)
1041 }
1042}
1043
1044#[must_use = "parsing result should be used"]
1051pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1052 Parser::new(spec).parse(input)
1053}
1054
1055#[must_use = "parsing result should be used"]
1059pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
1060 parse_partial_with_env(spec, input, None, None, MountTiming::Eager).map(|(out, _)| out)
1061}
1062
1063pub fn multicall_basename(argv0: &str) -> &str {
1066 let name = argv0.rsplit(['/', '\\']).next().unwrap_or(argv0);
1067 match name.get(name.len().saturating_sub(4)..) {
1068 Some(ext) if ext.eq_ignore_ascii_case(".exe") => &name[..name.len() - 4],
1069 _ => name,
1070 }
1071}
1072
1073pub fn multicall_applet<'a>(argv0: &'a str, name: &str, bin: Option<&str>) -> Option<&'a str> {
1078 let base = multicall_basename(argv0);
1079 if !name.is_empty() && base == multicall_basename(name) {
1080 return None;
1081 }
1082 if let Some(bin) = bin {
1083 if !bin.is_empty() && base == multicall_basename(bin) {
1084 return None;
1085 }
1086 }
1087 Some(base)
1088}
1089
1090#[derive(Clone, Copy, PartialEq, Eq)]
1099enum MountTiming {
1100 Eager,
1101 WhenAWordIsUnknown,
1102}
1103
1104struct Token {
1114 word: String,
1115 argv: usize,
1121 binding: Option<(Arc<SpecFlag>, usize)>,
1133}
1134
1135impl Token {
1136 fn new(word: String, argv: usize) -> Self {
1137 Self {
1138 word,
1139 argv,
1140 binding: None,
1141 }
1142 }
1143}
1144
1145struct Trace {
1151 tokens: Vec<TokenBinding>,
1152}
1153
1154impl Trace {
1155 fn new(input: &[String]) -> Self {
1156 Self {
1157 tokens: input
1158 .iter()
1159 .enumerate()
1160 .map(|(index, word)| TokenBinding {
1161 index,
1162 word: word.clone(),
1163 synthesized: false,
1164 roles: vec![],
1165 })
1166 .collect(),
1167 }
1168 }
1169
1170 fn record(&mut self, argv: usize, role: TokenRole) {
1171 if let Some(token) = self.tokens.get_mut(argv) {
1172 token.roles.push(role);
1173 }
1174 }
1175
1176 fn note_synthesized(&mut self, argv: usize) {
1178 if let Some(token) = self.tokens.get_mut(argv) {
1179 token.synthesized = true;
1180 }
1181 }
1182
1183 fn close(&mut self, unread: &VecDeque<Token>) {
1185 for token in unread {
1186 self.record(token.argv, TokenRole::Unread);
1187 }
1188 }
1189}
1190
1191fn parse_partial_with_env(
1192 spec: &Spec,
1193 input: &[String],
1194 custom_env: Option<&HashMap<String, String>>,
1195 mount_outputs: Option<&HashMap<String, String>>,
1196 mount_timing: MountTiming,
1197) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1198 let mut trace = Trace::new(input);
1199 parse_partial_traced(
1200 spec,
1201 input,
1202 custom_env,
1203 mount_outputs,
1204 mount_timing,
1205 &mut trace,
1206 )
1207}
1208
1209fn parse_partial_traced(
1216 spec: &Spec,
1217 input: &[String],
1218 custom_env: Option<&HashMap<String, String>>,
1219 mount_outputs: Option<&HashMap<String, String>>,
1220 mount_timing: MountTiming,
1221 trace: &mut Trace,
1222) -> Result<(ParseOutput, HashSet<String>), miette::Error> {
1223 if let Some(view) = input.first().and_then(|argv0| spec.view_for_program(argv0)) {
1224 let viewed = spec.for_view(view)?;
1225 return parse_partial_traced(
1226 &viewed,
1227 input,
1228 custom_env,
1229 mount_outputs,
1230 mount_timing,
1231 trace,
1232 );
1233 }
1234 trace!("parse_partial: {input:?}");
1235 let mut input = input
1236 .iter()
1237 .enumerate()
1238 .map(|(argv, word)| Token::new(word.clone(), argv))
1239 .collect::<VecDeque<_>>();
1240 let argv0 = input.pop_front();
1241 if let Some(argv0) = argv0.as_ref() {
1242 trace.record(argv0.argv, TokenRole::Program);
1243 }
1244 if spec.multicall {
1245 if let Some(raw) = argv0 {
1246 if let Some(applet) = multicall_applet(&raw.word, &spec.name, Some(spec.bin.as_str())) {
1247 trace.note_synthesized(raw.argv);
1251 input.push_front(Token::new(applet.to_string(), raw.argv));
1252 }
1253 }
1254 }
1255 let mut command_has_argv = !input.is_empty();
1259
1260 let mut out = ParseOutput {
1261 cmd: spec.cmd.clone(),
1262 cmds: vec![spec.cmd.clone()],
1263 args: IndexMap::new(),
1264 flags: IndexMap::new(),
1265 tokens: vec![],
1266 flag_origins: IndexMap::new(),
1267 arg_origins: IndexMap::new(),
1268 overridden_flags: BTreeMap::new(),
1269 available_flags: gather_flags(&spec.cmd),
1270 flag_awaiting_value: vec![],
1271 errors: vec![],
1272 warnings: vec![],
1273 next_arg: None,
1274 double_dash_seen: false,
1275 external: None,
1276 };
1277 let mut overridden_flags = HashSet::new();
1280 let mut parsed_flag_spellings: HashMap<usize, HashSet<String>> = HashMap::new();
1285
1286 let mut prefix_flags: Vec<(Arc<SpecFlag>, Vec<String>)> = vec![];
1299 let mut command_arg_found = false;
1302 let mut variadic_flag_active = false;
1303 let mut idx = 0;
1304 let mut used_default_subcommand = false;
1307 let mut mounts_resolved = false;
1311 let default_outranks_mounts =
1321 spec.default_subcommand.is_some() && !out.cmd.mounts.iter().any(|m| m.overrides_default);
1322 if mount_timing == MountTiming::Eager && !default_outranks_mounts && !out.cmd.mounts.is_empty()
1323 {
1324 mounts_resolved = true;
1325 let mut mounted = out.cmd.clone();
1326 mounted.mount(&[], mount_outputs)?;
1327 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1328 if let Some(last) = out.cmds.last_mut() {
1329 *last = mounted.clone();
1330 }
1331 out.cmd = mounted;
1332 }
1333
1334 while idx < input.len() {
1335 let default_catches_it = spec.default_subcommand.as_deref().is_some_and(|name| {
1345 default_accepts_word(&out.cmd, name, &input[idx].word)
1346 && !out.cmd.mounts.iter().any(|m| m.overrides_default)
1347 });
1348 if !mounts_resolved
1349 && !out.cmd.mounts.is_empty()
1350 && !default_catches_it
1351 && is_command_word(&input[idx].word)
1352 && !is_negative_number(&input[idx].word)
1353 && out.cmd.find_subcommand(&input[idx].word).is_none()
1354 {
1355 mounts_resolved = true;
1356 let mut mounted = out.cmd.clone();
1357 mounted.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1358 merge_subcommand_flags(&mut out.available_flags, gather_flags(&mounted), false);
1359 if let Some(last) = out.cmds.last_mut() {
1360 *last = mounted.clone();
1361 }
1362 out.cmd = mounted;
1363 }
1364 if variadic_flag_active
1365 && out.cmd.find_subcommand(&input[idx].word).is_some()
1366 && !out.cmd.subcommand_precedence_over_arg
1367 {
1368 break;
1369 }
1370 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx].word) {
1371 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1372 bail!(
1373 "subcommand '{}' cannot be used with arguments on its parent command",
1374 input[idx].word
1375 );
1376 }
1377 let mut subcommand = subcommand.clone();
1378 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1380 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1383 merge_subcommand_flags(
1384 &mut out.available_flags,
1385 gather_flags(&subcommand),
1386 crossing_mount,
1387 );
1388 let selected = input.remove(idx);
1390 if let Some(selected) = selected {
1391 trace.record(
1392 selected.argv,
1393 TokenRole::Command {
1394 name: subcommand.name.clone(),
1395 },
1396 );
1397 }
1398 command_has_argv = idx < input.len();
1399 out.cmds.push(subcommand.clone());
1400 out.cmd = subcommand.clone();
1401 mounts_resolved = true;
1403 prefix_flags.clear();
1404 command_arg_found = false;
1405 variadic_flag_active = false;
1406 } else if !is_command_word(&input[idx].word)
1409 || declared_numeric_short(&out.available_flags, &input[idx].word)
1410 {
1411 let word = input[idx].word.clone();
1413 let flag_key = get_flag_key(&word);
1414
1415 let is_bundle = word.starts_with("--")
1421 || short_bundle_is_known(spec, &out.cmds, &out.available_flags, &word);
1422 if let Some(f) = out
1423 .available_flags
1424 .get(flag_key)
1425 .cloned()
1426 .filter(|_| is_bundle)
1427 {
1428 command_arg_found = true;
1429 variadic_flag_active = f.arg.as_ref().is_some_and(|arg| arg.var);
1430 input[idx].binding = Some((Arc::clone(&f), out.cmds.len() - 1));
1438 let mut forwarded = f.global.then(|| vec![word.clone()]);
1439 idx += 1;
1440
1441 if f.arg.is_some()
1444 && !word.contains('=')
1445 && idx < input.len()
1446 && (!is_flag_like(&input[idx].word)
1447 || (f.arg.as_ref().is_some_and(|arg| arg.allow_negative_numbers)
1448 && is_negative_number(&input[idx].word)))
1449 {
1450 if let Some(words) = forwarded.as_mut() {
1451 words.push(input[idx].word.clone());
1452 }
1453 idx += 1;
1454 }
1455 if let Some(words) = forwarded {
1456 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1457 prefix_flags.push((f, words));
1458 }
1459 } else {
1460 break;
1463 }
1464 } else {
1465 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1466 idx += 1;
1467 continue;
1468 }
1469 if !used_default_subcommand && out.cmds.len() == 1 {
1478 if let Some(default_name) = &spec.default_subcommand {
1479 if let Some(subcommand) = out
1480 .cmd
1481 .find_subcommand(default_name)
1482 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1483 {
1484 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1485 bail!(
1486 "subcommand '{}' cannot be used with arguments on its parent command",
1487 subcommand.name
1488 );
1489 }
1490 let mut subcommand = subcommand.clone();
1491 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1493 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1494 merge_subcommand_flags(
1495 &mut out.available_flags,
1496 gather_flags(&subcommand),
1497 crossing_mount,
1498 );
1499 out.cmds.push(subcommand.clone());
1500 out.cmd = subcommand.clone();
1501 command_has_argv = true;
1502 prefix_flags.clear();
1503 command_arg_found = false;
1504 variadic_flag_active = false;
1505 mounts_resolved = true;
1508 used_default_subcommand = true;
1509 continue;
1514 }
1515 }
1516 }
1517 if out.cmd.external_subcommand {
1522 let rest: Vec<Token> = input.drain(idx..).collect();
1523 for token in &rest {
1524 trace.record(token.argv, TokenRole::External);
1525 }
1526 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1527 break;
1528 }
1529 break;
1531 }
1532 }
1533
1534 let mut next_arg_idx: usize = 0;
1544 let mut enable_flags = true;
1545 let mut grouped_flag = false;
1546 let mut seen_double_dash = false;
1550 let mut double_dash_violations: HashSet<String> = HashSet::new();
1553 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1558
1559 while !input.is_empty() {
1560 let token = input.pop_front().unwrap();
1561 let binding = token.binding;
1563 let argv = token.argv;
1564 let mut w = token.word;
1565 let attached_continuation = grouped_flag;
1570
1571 if let Some(ref restart_token) = out.cmd.restart_token {
1574 if w == *restart_token {
1575 out.args.clear();
1580 out.arg_origins.clear();
1585 trace.record(argv, TokenRole::Restart);
1586 next_arg_idx = 0;
1587 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
1591 }
1592 }
1593
1594 if enable_flags
1603 && w.starts_with('-')
1604 && out.flag_awaiting_value.last().is_some_and(|flag| {
1605 !flag.require_equals
1606 && (flag.allow_hyphen_values()
1607 || (flag
1608 .arg
1609 .as_ref()
1610 .is_some_and(|arg| arg.allow_negative_numbers)
1611 && is_negative_number(&w)))
1612 })
1613 {
1614 let should_return = bind_pending_flag_value(
1617 spec,
1618 &out.cmd,
1619 &mut out.errors,
1620 &mut out.flags,
1621 &mut out.flag_awaiting_value,
1622 &mut w,
1623 &mut input,
1624 custom_env,
1625 trace,
1626 argv,
1627 false,
1629 )?;
1630 if should_return {
1631 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1632 return Ok((out, overridden_flags));
1633 }
1634 continue;
1635 }
1636
1637 if enable_flags
1643 && !attached_continuation
1644 && !out.flag_awaiting_value.is_empty()
1645 && out.flag_awaiting_value.last().is_some_and(|flag| {
1646 (flag.default_missing.is_some() || flag.value_optional)
1647 && (flag.require_equals
1648 || (is_flag_like(&w)
1649 && !flag.allow_hyphen_values()
1650 && !(flag
1651 .arg
1652 .as_ref()
1653 .is_some_and(|arg| arg.allow_negative_numbers)
1654 && is_negative_number(&w))))
1655 })
1656 {
1657 try_bind_default_missing(
1658 &mut out.flags,
1659 &mut out.flag_awaiting_value,
1660 custom_env,
1661 &mut out.flag_origins,
1662 )?;
1663 }
1664
1665 if w == "--" && !seen_double_dash {
1671 enable_flags = false;
1672
1673 let should_preserve = out
1676 .cmd
1677 .args
1678 .get(next_arg_idx)
1679 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1680 .unwrap_or(false);
1681
1682 if should_preserve {
1683 } else {
1686 seen_double_dash = true;
1687 trace.record(argv, TokenRole::Separator);
1688
1689 let target = out.cmd.args.iter().position(|arg| {
1695 arg.double_dash == SpecDoubleDashChoices::Required
1696 && !out.args.contains_key(arg)
1697 });
1698 if let Some(target) = target {
1699 if target > next_arg_idx {
1706 next_arg_idx = target;
1707 }
1708 }
1709 continue;
1710 }
1711 }
1712
1713 if enable_flags && w.starts_with("--") {
1715 grouped_flag = false;
1716 let split = w.split_once('=');
1720 let word = split.map(|(word, _)| word).unwrap_or(&w);
1721 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1722 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1723 let command_level = binding
1724 .as_ref()
1725 .map(|(_, level)| *level)
1726 .unwrap_or(out.cmds.len() - 1);
1727 parsed_flag_spellings
1728 .entry(Arc::as_ptr(f) as usize)
1729 .or_default()
1730 .insert(word.to_string());
1731 trace.record(
1734 argv,
1735 TokenRole::Flag {
1736 flag: Arc::clone(f),
1737 spelling: word.to_string(),
1738 negated: f.negate.as_deref() == Some(word),
1739 },
1740 );
1741 if f.action != crate::SpecFlagAction::Set {
1742 out.errors.push(render_action_err(spec, &out.cmd, f, word));
1743 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1744 return Ok((out, overridden_flags));
1745 }
1746 apply_flag_overrides(
1747 f,
1748 &out.available_flags,
1749 &mut out.flags,
1750 &mut out.flag_awaiting_value,
1751 &mut overridden_flags,
1752 &mut out.overridden_flags,
1753 );
1754 if f.arg.is_some() {
1760 record_scalar_flag_occurrence(
1761 &out.cmds,
1762 f,
1763 command_level,
1764 None,
1765 &mut scalar_occurrences,
1766 &mut out.errors,
1767 );
1768 let f = Arc::clone(f);
1769 out.flag_awaiting_value.push(Arc::clone(&f));
1770 if let Some((_, val)) = split {
1775 let mut val = val.to_string();
1779 let should_return = bind_pending_flag_value(
1780 spec,
1781 &out.cmd,
1782 &mut out.errors,
1783 &mut out.flags,
1784 &mut out.flag_awaiting_value,
1785 &mut val,
1786 &mut input,
1787 custom_env,
1788 trace,
1789 argv,
1790 true,
1793 )?;
1794 if should_return {
1795 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1796 return Ok((out, overridden_flags));
1797 }
1798 }
1799 } else if f.count {
1800 let arr = out
1801 .flags
1802 .entry(Arc::clone(f))
1803 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1804 .try_as_multi_bool_mut()
1805 .unwrap();
1806 arr.push(true);
1807 } else {
1808 let negate = f.negate.clone().unwrap_or_default();
1809 let negated_form = word == negate;
1810 let value = if f.bool_value {
1811 match split.map(|(_, value)| value) {
1812 Some("true") => !negated_form,
1813 Some("false") => negated_form,
1814 Some(value) => {
1815 out.errors.push(UsageErr::InvalidValue {
1816 name: f.name.clone(),
1817 value: value.to_string(),
1818 reason: "expected `true` or `false`".to_string(),
1819 });
1820 continue;
1821 }
1822 None => !negated_form,
1823 }
1824 } else {
1825 !negated_form
1826 };
1827 record_scalar_flag_occurrence(
1832 &out.cmds,
1833 f,
1834 command_level,
1835 Some(!negated_form),
1836 &mut scalar_occurrences,
1837 &mut out.errors,
1838 );
1839 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1840 }
1841 continue;
1842 }
1843 if is_help_arg(spec, &out.cmd, &w) {
1844 out.errors
1845 .push(render_help_err(spec, &out.cmd, w.len() > 2));
1846 trace.record(
1847 argv,
1848 TokenRole::Builtin {
1849 spelling: w.clone(),
1850 },
1851 );
1852 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1853 return Ok((out, overridden_flags));
1854 }
1855 if is_version_arg(spec, &out.cmds, &w) {
1856 out.errors.push(render_version_err(spec, w.len() > 2));
1857 trace.record(
1858 argv,
1859 TokenRole::Builtin {
1860 spelling: w.clone(),
1861 },
1862 );
1863 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1864 return Ok((out, overridden_flags));
1865 }
1866 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1867 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1868 trace.close(&input);
1869 return Err(refused.into());
1870 }
1871 }
1872
1873 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1880 let positional_negative_number = !declared_numeric_short
1881 && is_negative_number(&w)
1882 && out
1883 .cmd
1884 .args
1885 .get(next_arg_idx)
1886 .is_some_and(|arg| arg.allow_negative_numbers);
1887 if enable_flags
1888 && !grouped_flag
1889 && binding.is_none()
1892 && w.starts_with('-')
1893 && w.len() > 1
1894 && is_flag_like(&w)
1895 && !positional_negative_number
1896 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1897 {
1898 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1901 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1902 trace.close(&input);
1903 return Err(refused.into());
1904 }
1905 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
1906 let short = w.chars().nth(1).unwrap();
1907 if let Some(f) = binding
1908 .as_ref()
1909 .map(|(flag, _)| flag)
1910 .or_else(|| out.available_flags.get(&format!("-{short}")))
1911 {
1912 let command_level = binding
1913 .as_ref()
1914 .map(|(_, level)| *level)
1915 .unwrap_or(out.cmds.len() - 1);
1916 if f.action != crate::SpecFlagAction::Set {
1917 out.errors
1918 .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
1919 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1920 return Ok((out, overridden_flags));
1921 }
1922 parsed_flag_spellings
1923 .entry(Arc::as_ptr(f) as usize)
1924 .or_default()
1925 .insert(format!("-{short}"));
1926 trace.record(
1927 argv,
1928 TokenRole::Flag {
1929 flag: Arc::clone(f),
1930 spelling: format!("-{short}"),
1931 negated: false,
1933 },
1934 );
1935 apply_flag_overrides(
1936 f,
1937 &out.available_flags,
1938 &mut out.flags,
1939 &mut out.flag_awaiting_value,
1940 &mut overridden_flags,
1941 &mut out.overridden_flags,
1942 );
1943 let rest = &w[1 + short.len_utf8()..];
1944 if !rest.is_empty() {
1945 input.push_front(Token::new(format!("-{rest}"), argv));
1948 }
1949 grouped_flag = !rest.is_empty();
1953 if f.arg.is_some() {
1954 record_scalar_flag_occurrence(
1955 &out.cmds,
1956 f,
1957 command_level,
1958 None,
1959 &mut scalar_occurrences,
1960 &mut out.errors,
1961 );
1962 out.flag_awaiting_value.push(Arc::clone(f));
1963 } else if f.count {
1964 let arr = out
1965 .flags
1966 .entry(Arc::clone(f))
1967 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1968 .try_as_multi_bool_mut()
1969 .unwrap();
1970 arr.push(true);
1971 } else {
1972 let negate = f.negate.clone().unwrap_or_default();
1973 let value = w != negate;
1974 record_scalar_flag_occurrence(
1975 &out.cmds,
1976 f,
1977 command_level,
1978 Some(value),
1979 &mut scalar_occurrences,
1980 &mut out.errors,
1981 );
1982 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1983 }
1984 continue;
1985 }
1986 if let Some(err) = supplied_short(spec, &out.cmds, short) {
1990 out.errors.push(err);
1991 trace.record(
1992 argv,
1993 TokenRole::Builtin {
1994 spelling: format!("-{short}"),
1995 },
1996 );
1997 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1998 return Ok((out, overridden_flags));
1999 }
2000 if is_help_arg(spec, &out.cmd, &w) {
2001 out.errors
2002 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2003 trace.record(
2004 argv,
2005 TokenRole::Builtin {
2006 spelling: w.clone(),
2007 },
2008 );
2009 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2010 return Ok((out, overridden_flags));
2011 }
2012 if is_version_arg(spec, &out.cmds, &w) {
2013 out.errors.push(render_version_err(spec, w.len() > 2));
2014 trace.record(
2015 argv,
2016 TokenRole::Builtin {
2017 spelling: w.clone(),
2018 },
2019 );
2020 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2021 return Ok((out, overridden_flags));
2022 }
2023 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2024 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2025 trace.close(&input);
2026 return Err(refused.into());
2027 }
2028 if grouped_flag {
2029 grouped_flag = false;
2030 w.remove(0);
2031 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2035 w.remove(0);
2036 }
2037 }
2038 }
2039
2040 if enable_flags
2051 && !attached_continuation
2052 && !out.flag_awaiting_value.is_empty()
2053 && out
2054 .flag_awaiting_value
2055 .last()
2056 .is_some_and(|flag| flag.require_equals)
2057 {
2058 let flag = out.flag_awaiting_value.last().unwrap();
2059 let token = flag
2060 .long
2061 .first()
2062 .map(|l| format!("--{l}"))
2063 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2064 .unwrap_or_else(|| flag.name.clone());
2065 out.errors.push(UsageErr::InvalidFlag {
2066 token: token.clone(),
2067 reason: "requires an argument".to_string(),
2068 span: (0, 0).into(),
2069 input: format!("{token} {w}"),
2070 });
2071 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2072 return Ok((out, overridden_flags));
2073 }
2074 if enable_flags && !out.flag_awaiting_value.is_empty() {
2075 let should_return = bind_pending_flag_value(
2078 spec,
2079 &out.cmd,
2080 &mut out.errors,
2081 &mut out.flags,
2082 &mut out.flag_awaiting_value,
2083 &mut w,
2084 &mut input,
2085 custom_env,
2086 trace,
2087 argv,
2088 attached_continuation,
2089 )?;
2090 if should_return {
2091 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2092 return Ok((out, overridden_flags));
2093 }
2094 continue;
2095 }
2096
2097 if out.cmd.allow_missing_positional {
2098 while let Some(current) = out.cmd.args.get(next_arg_idx) {
2099 if current.required || out.args.contains_key(current) {
2100 break;
2101 }
2102 let required_after = out.cmd.args[next_arg_idx + 1..]
2103 .iter()
2104 .filter(|arg| arg.required)
2105 .count();
2106 if required_after == 0 {
2107 break;
2108 }
2109 let remaining_values = 1 + input
2110 .iter()
2111 .filter(|token| !enable_flags || !is_flag_like(&token.word))
2112 .count();
2113 if remaining_values > required_after {
2114 break;
2115 }
2116 next_arg_idx += 1;
2117 }
2118 }
2119
2120 if let Some(arg) = out.cmd.args.get(next_arg_idx) {
2121 if arg.var
2122 && out.args.contains_key(arg)
2123 && arg.value_terminator.as_deref() == Some(w.as_str())
2124 {
2125 trace.record(
2126 argv,
2127 TokenRole::ValueTerminator {
2128 ends: arg.name.clone(),
2129 },
2130 );
2131 next_arg_idx += 1;
2132 continue;
2133 }
2134 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2138 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2139 trace.record(
2140 argv,
2141 TokenRole::Refused {
2142 reason: format!("{} only accepts words after `--`", arg.name),
2143 },
2144 );
2145 continue;
2149 }
2150 let trailing_value =
2155 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2156 let suppress_trailing_delimiter =
2157 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2158 let delimiter = if suppress_trailing_delimiter && trailing_value {
2159 None
2160 } else {
2161 arg.delimiter
2162 };
2163 let parts: Vec<String> = match delimiter {
2164 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2165 None => vec![w.clone()],
2166 };
2167 let mut refused = false;
2168 for part in &parts {
2169 if validate_choices(
2170 spec,
2171 &out.cmd,
2172 &mut out.errors,
2173 ChoiceTarget::arg(arg),
2174 part,
2175 arg.choices.as_ref(),
2176 custom_env,
2177 )? {
2178 refused = true;
2179 break;
2180 }
2181 }
2182 if refused {
2183 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2184 return Ok((out, overridden_flags));
2185 }
2186 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2191 enable_flags = false;
2192 }
2193 let unknown_flag = enable_flags
2198 && !positional_negative_number
2199 && is_flag_like(&w)
2200 && binding.is_none();
2201 trace.record(
2202 argv,
2203 if unknown_flag {
2204 TokenRole::UnknownFlag {
2205 bound_as: Some(Arc::new(arg.clone())),
2206 }
2207 } else {
2208 TokenRole::Arg {
2209 arg: Arc::new(arg.clone()),
2210 values: parts.clone(),
2211 }
2212 },
2213 );
2214 if arg.var {
2215 let arr = out
2216 .args
2217 .entry(Arc::new(arg.clone()))
2218 .or_insert_with(|| ParseValue::MultiString(vec![]))
2219 .try_as_multi_string_mut()
2220 .unwrap();
2221 arr.extend(parts.iter().cloned());
2225 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2226 next_arg_idx += 1;
2227 }
2228 } else {
2229 out.args
2230 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2231 next_arg_idx += 1;
2232 }
2233 continue;
2234 }
2235 if is_help_arg(spec, &out.cmd, &w) {
2236 out.errors
2237 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2238 trace.record(
2239 argv,
2240 TokenRole::Builtin {
2241 spelling: w.clone(),
2242 },
2243 );
2244 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2245 return Ok((out, overridden_flags));
2246 }
2247 if is_version_arg(spec, &out.cmds, &w) {
2248 out.errors.push(render_version_err(spec, w.len() > 2));
2249 trace.record(
2250 argv,
2251 TokenRole::Builtin {
2252 spelling: w.clone(),
2253 },
2254 );
2255 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2256 return Ok((out, overridden_flags));
2257 }
2258 trace.record(
2259 argv,
2260 TokenRole::Refused {
2261 reason: "no declaration takes this word".to_string(),
2262 },
2263 );
2264 trace.close(&input);
2265 bail!("unexpected word: {w}");
2266 }
2267
2268 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2269
2270 let flag_was_parsed =
2275 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2276
2277 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2285 let declared: HashSet<String> = out
2286 .cmd
2287 .flags
2288 .iter()
2289 .filter(|declared| declared.name == flag.name)
2290 .flat_map(flag_keys)
2291 .collect();
2292 let speaks_for_this_flag = declared.iter().any(|spelling| {
2302 out.available_flags
2303 .get(spelling)
2304 .is_some_and(|available| Arc::ptr_eq(available, flag))
2305 });
2306 if speaks_for_this_flag {
2307 declared
2308 } else {
2309 HashSet::new()
2310 }
2311 };
2312
2313 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2321 let child = child_spellings(flag);
2322 let child_exclusive = !child.is_empty()
2323 && out
2324 .cmd
2325 .flags
2326 .iter()
2327 .any(|declared| declared.name == flag.name && declared.exclusive);
2328 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2329 Some(spellings) => (
2330 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2331 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2332 ),
2333 None => (child_exclusive, flag.exclusive && child.is_empty()),
2337 }
2338 };
2339
2340 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2341 let (child, ancestor) = exclusivity_in_play(flag);
2342 child || ancestor
2343 };
2344
2345 let exclusive_present =
2350 unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2351 exclusive_occurrence(flag)
2352 && !overridden_flags.contains(&flag.name)
2353 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2354 });
2355 let requirements_apply = |command_index: usize| {
2356 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2357 };
2358
2359 if out.cmd.arg_required_else_help && !command_has_argv {
2360 out.errors.push(render_help_err(spec, &out.cmd, false));
2361 }
2362
2363 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2369 let mut names: Vec<&str> = out
2370 .cmd
2371 .subcommands
2372 .iter()
2373 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2376 .map(|(name, _)| name.as_str())
2377 .collect();
2378 names.sort_unstable();
2379 out.errors.push(UsageErr::MissingSubcommand(
2380 out.cmd.name.clone(),
2381 names.join(", "),
2382 ));
2383 }
2384
2385 if !exclusive_present {
2388 for arg in out
2389 .cmds
2390 .iter()
2391 .enumerate()
2392 .filter(|(index, _)| requirements_apply(*index))
2393 .flat_map(|(_, cmd)| &cmd.args)
2394 {
2395 if out.args.contains_key(arg) {
2396 continue;
2397 }
2398 if double_dash_violations.contains(&arg.name) {
2400 continue;
2401 }
2402 let required_if = arg.required_if.iter().any(|selector| {
2403 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2404 });
2405 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2406 selector_explicit_has_value(
2407 &condition.selector,
2408 &condition.value,
2409 &out,
2410 &overridden_flags,
2411 custom_env,
2412 )
2413 });
2414 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2415 && arg.required_if_eq_all.iter().all(|condition| {
2416 selector_explicit_has_value(
2417 &condition.selector,
2418 &condition.value,
2419 &out,
2420 &overridden_flags,
2421 custom_env,
2422 )
2423 });
2424 let unless_any = arg.required_unless.iter().any(|selector| {
2425 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2426 });
2427 let unless_all = !arg.required_unless_all.is_empty()
2428 && arg.required_unless_all.iter().all(|selector| {
2429 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2430 });
2431 let required_unless = (!arg.required_unless.is_empty()
2432 || !arg.required_unless_all.is_empty())
2433 && !(unless_any || unless_all);
2434 if (arg.required
2435 || required_if
2436 || required_if_eq
2437 || required_if_eq_all
2438 || required_unless)
2439 && arg.default.is_empty()
2440 {
2441 let has_env = arg
2443 .env
2444 .as_ref()
2445 .is_some_and(|env_var| env_contains(custom_env, env_var));
2446 if !has_env {
2447 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2448 }
2449 }
2450 }
2451 }
2452
2453 for flag in unique_flags(out.available_flags.values()) {
2465 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2466 if !given || overridden_flags.contains(&flag.name) {
2467 continue;
2468 }
2469 for other in &flag.conflicts {
2470 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2471 out.errors.push(UsageErr::InvalidFlag {
2472 token: format!("--{}", flag.name),
2473 reason: format!("conflicts with {other}"),
2474 span: (0, 0).into(),
2475 input: format!("--{} {other}", flag.name),
2476 });
2477 }
2478 }
2479 let owner = out
2490 .cmds
2491 .iter()
2492 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2493 .unwrap_or(out.cmds.len() - 1);
2494 if !exclusive_present && requirements_apply(owner) {
2495 for other in &flag.requires {
2496 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2497 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2498 if other.starts_with('-') {
2499 out.errors.push(UsageErr::MissingFlag(name));
2500 } else {
2501 out.errors.push(UsageErr::MissingArg(name));
2502 }
2503 }
2504 }
2505 for condition in &flag.requires_if {
2506 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2507 && !selector_is_satisfied(
2508 &condition.requires,
2509 &out,
2510 &overridden_flags,
2511 custom_env,
2512 )
2513 {
2514 let name = selector_flag_name(&condition.requires, &out)
2515 .unwrap_or_else(|| condition.requires.clone());
2516 out.errors.push(UsageErr::MissingFlag(name));
2517 }
2518 }
2519 }
2520 }
2521
2522 for (command_index, arg) in out
2525 .cmds
2526 .iter()
2527 .enumerate()
2528 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2529 {
2530 let given = arg_is_explicit(arg, &out, custom_env);
2531 if !given {
2532 continue;
2533 }
2534 for other in &arg.conflicts {
2535 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2536 out.errors.push(UsageErr::InvalidFlag {
2537 token: arg.name.clone(),
2538 reason: format!("conflicts with {other}"),
2539 span: (0, 0).into(),
2540 input: format!("{} {other}", arg.name),
2541 });
2542 }
2543 }
2544 if !exclusive_present && requirements_apply(command_index) {
2545 for other in &arg.requires {
2546 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2547 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2548 if other.starts_with('-') {
2549 out.errors.push(UsageErr::MissingFlag(name));
2550 } else {
2551 out.errors.push(UsageErr::MissingArg(name));
2552 }
2553 }
2554 }
2555 }
2556 }
2557
2558 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2568 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2573 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2574 continue;
2575 }
2576 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2577 .find(|other| {
2578 !Arc::ptr_eq(other, flag)
2579 && !overridden_flags.contains(&other.name)
2580 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2581 })
2582 .map(|other| format!("--{}", other.name));
2583 let other_arg = out.cmd.args.iter().find(|arg| {
2584 out.args.keys().any(|given| given.name == arg.name)
2585 || arg
2586 .env
2587 .as_ref()
2588 .is_some_and(|env| env_contains(custom_env, env))
2589 });
2590 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2596 let selected_subcommand =
2597 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2598 let other = other_flag
2599 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2600 .or(selected_subcommand);
2601 if let Some(other) = other {
2602 out.errors.push(UsageErr::InvalidFlag {
2603 token: format!("--{}", flag.name),
2604 reason: format!("must be given on its own, and {other} was given too"),
2605 span: (0, 0).into(),
2606 input: format!("--{} {other}", flag.name),
2607 });
2608 }
2609 }
2610
2611 let mut group_errors: Vec<UsageErr> = Vec::new();
2620 for (command_index, group) in out
2621 .cmds
2622 .iter()
2623 .enumerate()
2624 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2625 {
2626 let mut given: Vec<&str> = Vec::new();
2632 let mut seen: Vec<String> = Vec::new();
2633 for selector in &group.members {
2634 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2635 continue;
2636 }
2637 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2638 if seen.contains(&name) {
2639 continue;
2640 }
2641 seen.push(name);
2642 given.push(selector.as_str());
2643 }
2644 if !group.multiple && given.len() > 1 {
2645 group_errors.push(UsageErr::InvalidFlag {
2646 token: given[1].to_string(),
2647 reason: format!("cannot be used with {} in group {}", given[0], group.name),
2648 span: (0, 0).into(),
2649 input: format!("{} {}", given[0], given[1]),
2650 });
2651 }
2652 let satisfied = group
2657 .members
2658 .iter()
2659 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2660 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2661 group_errors.push(UsageErr::MissingGroup {
2665 group: group.name.clone(),
2666 members: group.members.join(", "),
2667 });
2668 }
2669 }
2670 out.errors.extend(group_errors);
2671
2672 if !exclusive_present {
2673 for flag in unique_flags(out.available_flags.values()) {
2674 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 !requirements_apply(owner) {
2680 continue;
2681 }
2682 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2683 continue;
2684 }
2685 let has_default =
2686 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2687 let has_env = flag_has_env(flag, custom_env);
2688 let required_if = flag.required_if.iter().any(|selector| {
2689 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2690 });
2691 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2692 selector_explicit_has_value(
2693 &condition.selector,
2694 &condition.value,
2695 &out,
2696 &overridden_flags,
2697 custom_env,
2698 )
2699 });
2700 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2701 && flag.required_if_eq_all.iter().all(|condition| {
2702 selector_explicit_has_value(
2703 &condition.selector,
2704 &condition.value,
2705 &out,
2706 &overridden_flags,
2707 custom_env,
2708 )
2709 });
2710 let unless_any = flag.required_unless.iter().any(|selector| {
2711 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2712 });
2713 let unless_all = !flag.required_unless_all.is_empty()
2714 && flag.required_unless_all.iter().all(|selector| {
2715 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2716 });
2717 let required_unless = (!flag.required_unless.is_empty()
2718 || !flag.required_unless_all.is_empty())
2719 && !(unless_any || unless_all);
2720 if (flag.required
2721 || required_if
2722 || required_if_eq
2723 || required_if_eq_all
2724 || required_unless)
2725 && !has_default
2726 && !has_env
2727 {
2728 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2729 }
2730 }
2731 }
2732
2733 for (arg, value) in &out.args {
2735 if arg.var {
2736 if let ParseValue::MultiString(values) = value {
2737 if let Some(min) = arg.var_min {
2738 if values.len() < min {
2739 out.errors.push(UsageErr::VarArgTooFew {
2740 name: arg.name.clone(),
2741 min,
2742 got: values.len(),
2743 });
2744 }
2745 }
2746 if let Some(max) = arg.var_max {
2747 if values.len() > max {
2748 out.errors.push(UsageErr::VarArgTooMany {
2749 name: arg.name.clone(),
2750 max,
2751 got: values.len(),
2752 });
2753 }
2754 }
2755 }
2756 }
2757 }
2758
2759 for flag in unique_flags(out.available_flags.values()) {
2763 if flag.var {
2764 let bound = match out.flags.get(flag) {
2765 Some(ParseValue::MultiString(values)) => values.len(),
2766 Some(ParseValue::MultiBool(values)) => values.len(),
2767 Some(_) => 1,
2768 None => 0,
2769 };
2770 let pending = out
2774 .flag_awaiting_value
2775 .iter()
2776 .filter(|pending| {
2777 Arc::ptr_eq(pending, flag)
2778 && (pending.value_optional || pending.default_missing.is_some())
2779 })
2780 .count();
2781 let count = bound + pending;
2782 if count == 0 {
2783 continue;
2784 }
2785 if let Some(min) = flag.var_min {
2786 if count < min {
2787 out.errors.push(UsageErr::VarFlagTooFew {
2788 name: flag.name.clone(),
2789 min,
2790 got: count,
2791 });
2792 }
2793 }
2794 if let Some(max) = flag.var_max {
2795 if count > max {
2796 out.errors.push(UsageErr::VarFlagTooMany {
2797 name: flag.name.clone(),
2798 max,
2799 got: count,
2800 });
2801 }
2802 }
2803 }
2804 }
2805
2806 Ok((out, overridden_flags))
2807}
2808
2809fn validate_expression(
2810 name: &str,
2811 expression: Option<&str>,
2812 message: Option<&str>,
2813 parsed: &ParseValue,
2814 errors: &mut Vec<UsageErr>,
2815) {
2816 let Some(expression) = expression else {
2817 return;
2818 };
2819 #[cfg(not(feature = "validation"))]
2820 let _ = expression;
2821 let values: &[String] = match parsed {
2822 ParseValue::String(value) => std::slice::from_ref(value),
2823 ParseValue::MultiString(values) => values,
2824 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
2825 };
2826 #[cfg(feature = "validation")]
2827 for value in values {
2828 let reason = match usage_validation::validate(expression, value) {
2829 Ok(true) => continue,
2830 Ok(false) => message
2831 .unwrap_or("does not satisfy the validation expression")
2832 .to_string(),
2833 Err(error) => format!("validation expression failed: {error}"),
2834 };
2835 errors.push(UsageErr::InvalidValue {
2836 name: name.to_string(),
2837 value: value.clone(),
2838 reason,
2839 });
2840 break;
2841 }
2842 #[cfg(not(feature = "validation"))]
2843 if let Some(value) = values.first() {
2844 let _ = message;
2845 errors.push(UsageErr::InvalidValue {
2846 name: name.to_string(),
2847 value: value.clone(),
2848 reason: "expression validation requires the `validation` feature".to_string(),
2849 });
2850 }
2851}
2852
2853#[cfg(all(test, not(feature = "validation")))]
2854mod optional_validation_tests {
2855 use crate::{parse, Spec};
2856
2857 #[test]
2858 fn validation_declarations_require_the_opt_in_runtime_feature() {
2859 let spec: Spec = r#"
2860name "ex"
2861bin "ex"
2862arg "<port>" validate="int(value) > 0"
2863 "#
2864 .parse()
2865 .unwrap();
2866 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
2867 assert!(
2868 error
2869 .to_string()
2870 .contains("requires the `validation` feature"),
2871 "{error:?}"
2872 );
2873 }
2874}
2875
2876fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
2877 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
2878}
2879
2880fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
2881 overrider
2882 .overrides
2883 .iter()
2884 .any(|selector| flag_matches_selector(overridden, selector))
2885}
2886
2887fn apply_prefix_flag_overrides(
2888 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
2889 flag: Arc<SpecFlag>,
2890) {
2891 prefix_flags
2892 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
2893}
2894
2895fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
2896 prefix_flags
2897 .iter()
2898 .flat_map(|(_, words)| words.iter().cloned())
2899 .collect()
2900}
2901
2902fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
2903 match custom_env {
2904 Some(env) => env.contains_key(env_var),
2905 None => std::env::var(env_var).is_ok(),
2906 }
2907}
2908
2909fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
2910 flag.env_names()
2911 .any(|env_var| env_contains(custom_env, env_var))
2912}
2913
2914fn fallback_is_true(value: &str) -> bool {
2915 matches!(value, "1" | "true" | "True" | "TRUE")
2916}
2917
2918fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
2919 match delimiter {
2920 Some(delimiter) => values
2921 .iter()
2922 .flat_map(|value| value.split(delimiter).map(str::to_string))
2923 .collect(),
2924 None => values.to_vec(),
2925 }
2926}
2927
2928fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
2929 if let Some(min) = arg.var_min {
2930 if count < min {
2931 errors.push(UsageErr::VarArgTooFew {
2932 name: arg.name.clone(),
2933 min,
2934 got: count,
2935 });
2936 }
2937 }
2938 if let Some(max) = arg.var_max {
2939 if count > max {
2940 errors.push(UsageErr::VarArgTooMany {
2941 name: arg.name.clone(),
2942 max,
2943 got: count,
2944 });
2945 }
2946 }
2947}
2948
2949fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
2950 if let Some(min) = flag.var_min {
2951 if count < min {
2952 errors.push(UsageErr::VarFlagTooFew {
2953 name: flag.name.clone(),
2954 min,
2955 got: count,
2956 });
2957 }
2958 }
2959 if let Some(max) = flag.var_max {
2960 if count > max {
2961 errors.push(UsageErr::VarFlagTooMany {
2962 name: flag.name.clone(),
2963 max,
2964 got: count,
2965 });
2966 }
2967 }
2968}
2969
2970fn validate_flag_arg_fallback_count(
2971 flag: &SpecFlag,
2972 arg: &SpecArg,
2973 count: usize,
2974 errors: &mut Vec<UsageErr>,
2975) {
2976 if let Some(min) = arg.var_min {
2977 if count < min {
2978 errors.push(UsageErr::VarFlagTooFew {
2979 name: flag.name.clone(),
2980 min,
2981 got: count,
2982 });
2983 }
2984 }
2985 if let Some(max) = arg.var_max {
2986 if count > max {
2987 errors.push(UsageErr::VarFlagTooMany {
2988 name: flag.name.clone(),
2989 max,
2990 got: count,
2991 });
2992 }
2993 }
2994}
2995
2996fn bind_flag_fallback(
2999 flag: &Arc<SpecFlag>,
3000 values: &[String],
3001 out: &mut ParseOutput,
3002 custom_env: Option<&HashMap<String, String>>,
3003 origin: ValueOrigin,
3004) -> Result<(), miette::Error> {
3005 if values.is_empty() {
3006 return Ok(());
3007 }
3008 if let Some(arg) = flag.arg.as_ref() {
3009 let values = split_fallback_values(values, arg.delimiter);
3010 if flag.var || arg.var {
3011 if flag.var {
3012 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3013 }
3014 if arg.var {
3015 validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
3016 }
3017 validate_choice_values(
3018 ChoiceTarget::option(flag),
3019 &values,
3020 arg.choices.as_ref(),
3021 custom_env,
3022 )?;
3023 out.flags
3024 .insert(Arc::clone(flag), ParseValue::MultiString(values));
3025 } else {
3026 let value = values.into_iter().next().unwrap_or_default();
3027 validate_choice_value(
3028 ChoiceTarget::option(flag),
3029 &value,
3030 arg.choices.as_ref(),
3031 custom_env,
3032 )?;
3033 out.flags
3034 .insert(Arc::clone(flag), ParseValue::String(value));
3035 }
3036 } else if flag.var {
3037 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3038 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3039 out.flags
3040 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
3041 } else {
3042 out.flags.insert(
3043 Arc::clone(flag),
3044 ParseValue::Bool(fallback_is_true(&values[0])),
3045 );
3046 }
3047 out.flag_origins
3048 .entry(Arc::clone(flag))
3049 .or_default()
3050 .push(origin);
3051 Ok(())
3052}
3053
3054fn default_if_condition_matches(
3055 condition: &crate::SpecDefaultIf,
3056 out: &ParseOutput,
3057 overridden_flags: &HashSet<String>,
3058 custom_env: Option<&HashMap<String, String>>,
3059) -> bool {
3060 match &condition.when {
3061 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3062 Some(when) => {
3063 let Some(flag) = out
3064 .available_flags
3065 .values()
3066 .chain(out.flags.keys())
3067 .find(|flag| flag_matches_selector(flag, &condition.selector))
3068 else {
3069 return false;
3070 };
3071 if overridden_flags.contains(&flag.name) {
3072 return false;
3073 }
3074 explicit_flag_has_value(flag, when, out, custom_env)
3075 }
3076 }
3077}
3078
3079fn explicit_flag_has_value(
3085 flag: &SpecFlag,
3086 expected: &str,
3087 out: &ParseOutput,
3088 custom_env: Option<&HashMap<String, String>>,
3089) -> bool {
3090 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3091 ParseValue::Bool(value) => value.to_string() == expected,
3092 ParseValue::String(value) => value == expected,
3093 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3094 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3095 });
3096 if out.flags.contains_key(flag) {
3097 return parsed_matches;
3098 }
3099
3100 let value = flag.env_names().find_map(|env| match custom_env {
3101 Some(values) => values.get(env).cloned(),
3102 None => std::env::var(env).ok(),
3103 });
3104 value.is_some_and(
3105 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3106 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3107 None if flag.arg.is_none() => {
3108 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3109 }
3110 None => value == expected,
3111 },
3112 )
3113}
3114
3115fn selector_explicit_has_value(
3116 selector: &str,
3117 expected: &str,
3118 out: &ParseOutput,
3119 overridden_flags: &HashSet<String>,
3120 custom_env: Option<&HashMap<String, String>>,
3121) -> bool {
3122 if let Some(flag) = out
3123 .available_flags
3124 .values()
3125 .chain(out.flags.keys())
3126 .find(|flag| flag_matches_selector(flag, selector))
3127 {
3128 return !overridden_flags.contains(&flag.name)
3129 && explicit_flag_has_value(flag, expected, out, custom_env);
3130 }
3131 let Some(arg) = selector_arg(selector, out) else {
3132 return false;
3133 };
3134 let parsed = out
3135 .args
3136 .iter()
3137 .find(|(given, _)| given.name == arg.name)
3138 .map(|(_, value)| value);
3139 if let Some(value) = parsed {
3140 return match value {
3141 ParseValue::String(value) => value == expected,
3142 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3143 ParseValue::Bool(value) => value.to_string() == expected,
3144 ParseValue::MultiBool(values) => {
3145 values.iter().any(|value| value.to_string() == expected)
3146 }
3147 };
3148 }
3149 let value = arg.env_names().find_map(|env| match custom_env {
3150 Some(values) => values.get(env).cloned(),
3151 None => std::env::var(env).ok(),
3152 });
3153 value.is_some_and(|value| match arg.delimiter {
3154 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3155 None => value == expected,
3156 })
3157}
3158
3159fn selector_is_explicit(
3160 selector: &str,
3161 out: &ParseOutput,
3162 overridden_flags: &HashSet<String>,
3163 custom_env: Option<&HashMap<String, String>>,
3164) -> bool {
3165 let flag_is_explicit = out
3166 .available_flags
3167 .values()
3168 .chain(out.flags.keys())
3169 .any(|flag| {
3170 flag_matches_selector(flag, selector)
3171 && !overridden_flags.contains(&flag.name)
3172 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3173 });
3174 flag_is_explicit
3175 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3176}
3177
3178fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3184 out.available_flags
3185 .values()
3186 .chain(out.flags.keys())
3187 .find(|flag| flag_matches_selector(flag, selector))
3188 .map(|flag| flag.name.clone())
3189 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3190}
3191
3192fn selector_is_satisfied(
3202 selector: &str,
3203 out: &ParseOutput,
3204 overridden_flags: &HashSet<String>,
3205 custom_env: Option<&HashMap<String, String>>,
3206) -> bool {
3207 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3208 return true;
3209 }
3210 let flag_is_satisfied = out
3211 .available_flags
3212 .values()
3213 .chain(out.flags.keys())
3214 .filter(|flag| flag_matches_selector(flag, selector))
3215 .any(|flag| {
3216 !overridden_flags.contains(&flag.name)
3217 && (!flag.default.is_empty()
3218 || flag.arg.iter().any(|a| !a.default.is_empty())
3219 || flag.default_if.iter().any(|condition| {
3220 default_if_condition_matches(condition, out, overridden_flags, custom_env)
3221 }))
3222 });
3223 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3224}
3225
3226fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3227 if selector.starts_with('-') {
3230 return None;
3231 }
3232 out.cmds
3233 .iter()
3234 .flat_map(|cmd| &cmd.args)
3235 .find(|arg| arg.name == selector)
3236}
3237
3238fn arg_is_explicit(
3239 arg: &SpecArg,
3240 out: &ParseOutput,
3241 custom_env: Option<&HashMap<String, String>>,
3242) -> bool {
3243 out.args.keys().any(|given| given.name == arg.name)
3244 || arg
3245 .env
3246 .as_ref()
3247 .is_some_and(|env| env_contains(custom_env, env))
3248}
3249
3250fn apply_flag_overrides(
3251 flag: &Arc<SpecFlag>,
3252 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3253 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3254 pending_flags: &mut Vec<Arc<SpecFlag>>,
3255 overridden_flags: &mut HashSet<String>,
3256 attributed: &mut BTreeMap<String, String>,
3260) {
3261 let overridden_names: HashSet<String> = available_flags
3262 .values()
3263 .chain(parsed_flags.keys())
3264 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3265 .map(|other| other.name.clone())
3266 .collect();
3267
3268 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3269 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3270 for name in &overridden_names {
3271 attributed.insert(name.clone(), flag.name.clone());
3272 }
3273 overridden_flags.extend(overridden_names);
3274 overridden_flags.remove(&flag.name);
3276 attributed.remove(&flag.name);
3277}
3278
3279#[cfg(feature = "cli-help")]
3280fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3281 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3282}
3283
3284#[cfg(feature = "cli-help")]
3285fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3286 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3287 if !out.is_empty() {
3288 out.push('\n');
3289 }
3290 out.push_str(&docs::cli::render_help(spec, cmd, true));
3291 let mut children: Vec<_> = cmd
3292 .subcommands
3293 .values()
3294 .filter(|child| !child.hide)
3295 .collect();
3296 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3297 for child in children {
3298 append(out, spec, child);
3299 }
3300 }
3301
3302 let mut out = String::new();
3303 append(&mut out, spec, cmd);
3304 UsageErr::Help(out)
3305}
3306
3307#[cfg(not(feature = "cli-help"))]
3308fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3309 UsageErr::Help("help".to_string())
3310}
3311
3312#[cfg(not(feature = "cli-help"))]
3313fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3314 UsageErr::Help("help".to_string())
3315}
3316
3317fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3320 let value = if long {
3321 spec.long_version.as_ref().or(spec.version.as_ref())
3322 } else {
3323 spec.version.as_ref().or(spec.long_version.as_ref())
3324 };
3325 UsageErr::Version(value.cloned().unwrap_or_default())
3326}
3327
3328fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3329 use crate::SpecFlagAction;
3330 match flag.action {
3331 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3332 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3333 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3334 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3335 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3336 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3337 }
3338}
3339
3340#[derive(Copy, Clone)]
3341struct ChoiceTarget<'a> {
3342 kind: &'a str,
3343 name: &'a str,
3344}
3345
3346impl<'a> ChoiceTarget<'a> {
3347 fn arg(arg: &'a SpecArg) -> Self {
3348 Self {
3349 kind: "arg",
3350 name: &arg.name,
3351 }
3352 }
3353
3354 fn option(flag: &'a SpecFlag) -> Self {
3355 Self {
3356 kind: "option",
3357 name: &flag.name,
3358 }
3359 }
3360}
3361
3362fn short_bundle_is_known(
3367 spec: &Spec,
3368 cmds: &[SpecCommand],
3369 available: &BTreeMap<String, Arc<SpecFlag>>,
3370 token: &str,
3371) -> bool {
3372 for c in token.chars().skip(1) {
3373 match available.get(&format!("-{c}")) {
3374 None if supplied_short(spec, cmds, c).is_some() => {}
3379 None => return false,
3380 Some(f) if f.arg.is_some() => return true,
3381 Some(_) => {}
3382 }
3383 }
3384 true
3385}
3386
3387fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3397 let cmd = cmds.last()?;
3398 match letter {
3399 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3400 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3401 _ => None,
3402 }
3403}
3404
3405fn reject_unknown_flag_if_asked(
3411 spec: &Spec,
3412 path: &[SpecCommand],
3413 token: &str,
3414) -> Result<(), UsageErr> {
3415 if !is_flag_like(token) {
3418 return Ok(());
3419 }
3420 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3421 return Ok(());
3422 }
3423 Err(UsageErr::InvalidFlag {
3424 token: token.to_string(),
3425 reason: "no such flag".to_string(),
3426 span: (0, 0).into(),
3427 input: token.to_string(),
3428 })
3429}
3430
3431fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3437 path.iter()
3438 .rev()
3439 .find_map(|cmd| cmd.unknown_flags)
3440 .or(spec.unknown_flags)
3441 .unwrap_or_default()
3442}
3443
3444fn is_flag_like(token: &str) -> bool {
3450 match token.strip_prefix('-') {
3451 None | Some("") => false,
3452 Some(_) => true,
3453 }
3454}
3455
3456fn is_negative_number(token: &str) -> bool {
3457 token.strip_prefix('-').is_some_and(is_number)
3458}
3459
3460fn record_scalar_flag_occurrence(
3461 cmds: &[SpecCommand],
3462 flag: &Arc<SpecFlag>,
3463 command_level: usize,
3464 bool_value: Option<bool>,
3465 occurrences: &mut HashMap<(usize, usize), u8>,
3466 errors: &mut Vec<UsageErr>,
3467) {
3468 let strict = cmds
3469 .get(command_level)
3470 .is_some_and(|cmd| !cmd.args_override_self);
3471 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3472 if !strict || flag.count || collects_values {
3473 return;
3474 }
3475
3476 let bit = match bool_value {
3477 Some(false) if flag.negate.is_some() => 0b10,
3478 _ => 0b01,
3479 };
3480 let key = (Arc::as_ptr(flag) as usize, command_level);
3481 let seen = occurrences.entry(key).or_default();
3482 if *seen & bit != 0 {
3483 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3484 }
3485 *seen |= bit;
3486}
3487
3488fn is_command_word(token: &str) -> bool {
3496 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3497}
3498
3499fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3505 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3506}
3507
3508fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3514 !is_negative_number(token)
3515 || cmd
3516 .find_subcommand(default_name)
3517 .and_then(|default| default.args.first())
3518 .is_some_and(|arg| arg.allow_negative_numbers)
3519}
3520
3521fn is_number(rest: &str) -> bool {
3528 let (mantissa, exponent) = match rest.find(['e', 'E']) {
3529 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3530 None => (rest, None),
3531 };
3532
3533 let mut seen_digit = false;
3534 let mut seen_dot = false;
3535 for c in mantissa.chars() {
3536 match c {
3537 '0'..='9' => seen_digit = true,
3538 '.' if !seen_dot => seen_dot = true,
3539 _ => return false,
3540 }
3541 }
3542 if !seen_digit {
3543 return false;
3544 }
3545
3546 match exponent {
3547 None => true,
3548 Some(exp) => {
3549 let digits = exp
3550 .strip_prefix('+')
3551 .or_else(|| exp.strip_prefix('-'))
3552 .unwrap_or(exp);
3553 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3554 }
3555 }
3556}
3557
3558#[allow(clippy::too_many_arguments)]
3565fn bind_pending_flag_value(
3566 spec: &Spec,
3567 cmd: &SpecCommand,
3568 errors: &mut Vec<UsageErr>,
3569 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3570 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3571 word: &mut String,
3572 input: &mut VecDeque<Token>,
3573 custom_env: Option<&HashMap<String, String>>,
3574 trace: &mut Trace,
3575 argv: usize,
3579 attached: bool,
3580) -> miette::Result<bool> {
3581 let collecting = flag_awaiting_value
3585 .last()
3586 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3587 .cloned()
3588 .map(|flag| {
3589 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3590 (flag, carried)
3591 });
3592 let mut bound = vec![];
3593 let refused = drain_pending_flag_values(
3594 spec,
3595 cmd,
3596 errors,
3597 flags,
3598 flag_awaiting_value,
3599 word,
3600 custom_env,
3601 &mut bound,
3602 )?;
3603 for (flag, values) in bound {
3604 trace.record(
3605 argv,
3606 TokenRole::Value {
3607 flag,
3608 values,
3609 attached,
3610 },
3611 );
3612 }
3613 if refused {
3614 return Ok(true);
3615 }
3616 let Some((flag, carried)) = collecting else {
3617 return Ok(false);
3618 };
3619 collect_variadic_flag_values(
3620 spec,
3621 cmd,
3622 errors,
3623 flags,
3624 flag_awaiting_value,
3625 &flag,
3626 carried,
3627 input,
3628 custom_env,
3629 trace,
3630 )
3631}
3632
3633#[allow(clippy::too_many_arguments)]
3646fn collect_variadic_flag_values(
3647 spec: &Spec,
3648 cmd: &SpecCommand,
3649 errors: &mut Vec<UsageErr>,
3650 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3651 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3652 flag: &Arc<SpecFlag>,
3653 carried: usize,
3654 input: &mut VecDeque<Token>,
3655 custom_env: Option<&HashMap<String, String>>,
3656 trace: &mut Trace,
3657) -> miette::Result<bool> {
3658 let max = flag
3659 .arg
3660 .as_ref()
3661 .and_then(|arg| arg.var_max)
3662 .unwrap_or(usize::MAX);
3663 while flags
3664 .get(flag)
3665 .map(value_count)
3666 .unwrap_or(0)
3667 .saturating_sub(carried)
3668 < max
3669 {
3670 let Some(next) = input.front().map(|token| token.word.as_str()) else {
3671 break;
3672 };
3673 if flag
3674 .arg
3675 .as_ref()
3676 .and_then(|arg| arg.value_terminator.as_deref())
3677 == Some(next)
3678 {
3679 let terminator = input.pop_front().unwrap();
3680 trace.record(
3681 terminator.argv,
3682 TokenRole::ValueTerminator {
3683 ends: flag.name.clone(),
3684 },
3685 );
3686 break;
3687 }
3688 if next == "--"
3691 || (is_flag_like(next)
3692 && !(flag
3693 .arg
3694 .as_ref()
3695 .is_some_and(|arg| arg.allow_negative_numbers)
3696 && is_negative_number(next)))
3697 {
3698 break;
3699 }
3700 let taken = input.pop_front().unwrap();
3701 let argv = taken.argv;
3702 let mut word = taken.word;
3703 flag_awaiting_value.push(Arc::clone(flag));
3704 let mut bound = vec![];
3705 let refused = drain_pending_flag_values(
3706 spec,
3707 cmd,
3708 errors,
3709 flags,
3710 flag_awaiting_value,
3711 &mut word,
3712 custom_env,
3713 &mut bound,
3714 )?;
3715 for (flag, values) in bound {
3716 trace.record(
3719 argv,
3720 TokenRole::Value {
3721 flag,
3722 values,
3723 attached: false,
3724 },
3725 );
3726 }
3727 if refused {
3728 return Ok(true);
3729 }
3730 }
3731 let taken = flags
3740 .get(flag)
3741 .map(value_count)
3742 .unwrap_or(0)
3743 .saturating_sub(carried);
3744 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
3745 if taken < min {
3746 errors.push(UsageErr::VarFlagTooFew {
3747 name: flag.name.clone(),
3748 min,
3749 got: taken,
3750 });
3751 }
3752 }
3753 if taken > max {
3754 errors.push(UsageErr::VarFlagTooMany {
3755 name: flag.name.clone(),
3756 max,
3757 got: taken,
3758 });
3759 }
3760 Ok(false)
3761}
3762
3763fn value_count(value: &ParseValue) -> usize {
3765 match value {
3766 ParseValue::MultiString(values) => values.len(),
3767 ParseValue::MultiBool(values) => values.len(),
3768 _ => 1,
3769 }
3770}
3771
3772fn try_bind_default_missing(
3783 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3784 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3785 custom_env: Option<&HashMap<String, String>>,
3786 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
3787) -> miette::Result<bool> {
3788 let Some(flag) = flag_awaiting_value.last() else {
3789 return Ok(false);
3790 };
3791 let value = match flag.default_missing.clone() {
3792 Some(value) => value,
3793 None if flag.value_optional => {
3794 let flag = flag_awaiting_value.pop().unwrap();
3795 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
3799 origins
3800 .entry(Arc::clone(&flag))
3801 .or_default()
3802 .push(ValueOrigin::DefaultMissing);
3803 if flag.var {
3804 flags
3809 .entry(flag)
3810 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
3811 .try_as_multi_string_mut()
3812 .unwrap()
3813 .push(String::new());
3814 } else if variadic_value {
3815 flags
3818 .entry(flag)
3819 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
3820 } else {
3821 flags.insert(flag, ParseValue::MultiString(Vec::new()));
3825 }
3826 return Ok(true);
3827 }
3828 None => return Ok(false),
3829 };
3830 if let Some(arg) = flag.arg.as_ref() {
3831 validate_choice_value(
3832 ChoiceTarget::option(flag),
3833 &value,
3834 arg.choices.as_ref(),
3835 custom_env,
3836 )?;
3837 }
3838 let flag = flag_awaiting_value.pop().unwrap();
3839 origins
3840 .entry(Arc::clone(&flag))
3841 .or_default()
3842 .push(ValueOrigin::DefaultMissing);
3843 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3844 if collecting {
3845 let arr = flags
3846 .entry(flag)
3847 .or_insert_with(|| ParseValue::MultiString(vec![]))
3848 .try_as_multi_string_mut()
3849 .unwrap();
3850 arr.push(value);
3851 } else {
3852 flags.insert(flag, ParseValue::String(value));
3853 }
3854 Ok(true)
3855}
3856
3857#[allow(clippy::too_many_arguments)]
3862fn drain_pending_flag_values(
3863 spec: &Spec,
3864 cmd: &SpecCommand,
3865 errors: &mut Vec<UsageErr>,
3866 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3867 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3868 word: &mut String,
3869 custom_env: Option<&HashMap<String, String>>,
3870 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3871) -> miette::Result<bool> {
3872 while let Some(flag) = flag_awaiting_value.pop() {
3873 let arg = flag.arg.as_ref().unwrap();
3874 let parts: Vec<String> = match arg.delimiter {
3878 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
3879 None => vec![std::mem::take(word)],
3880 };
3881 for part in &parts {
3882 if validate_choices(
3883 spec,
3884 cmd,
3885 errors,
3886 ChoiceTarget::option(&flag),
3887 part,
3888 arg.choices.as_ref(),
3889 custom_env,
3890 )? {
3891 return Ok(true);
3892 }
3893 }
3894 word.clear();
3895 bound.push((Arc::clone(&flag), parts.clone()));
3896 if flag.var || arg.var {
3899 let arr = flags
3900 .entry(flag)
3901 .or_insert_with(|| ParseValue::MultiString(vec![]))
3902 .try_as_multi_string_mut()
3903 .unwrap();
3904 arr.extend(parts);
3905 } else {
3906 flags.insert(
3909 flag,
3910 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
3911 );
3912 }
3913 }
3914 Ok(false)
3915}
3916
3917fn choice_error(
3918 target: ChoiceTarget<'_>,
3919 value: &str,
3920 choices: Option<&SpecChoices>,
3921 custom_env: Option<&HashMap<String, String>>,
3922) -> Option<String> {
3923 let choices = choices?;
3924 if !choices.strict {
3925 return None;
3926 }
3927 let values = choices.values_with_env(custom_env);
3928 if choices.matches_with_env(value, custom_env) {
3929 return None;
3930 }
3931 if let Some(env) = choices.env() {
3932 if values.is_empty() {
3933 return Some(format!(
3934 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
3935 target.kind, target.name,
3936 ));
3937 }
3938 }
3939 Some(format!(
3940 "Invalid choice for {} {}: {value}, expected one of {}",
3941 target.kind,
3942 target.name,
3943 values.join(", ")
3944 ))
3945}
3946
3947fn validate_choices(
3948 spec: &Spec,
3949 cmd: &SpecCommand,
3950 errors: &mut Vec<UsageErr>,
3951 target: ChoiceTarget<'_>,
3952 value: &str,
3953 choices: Option<&SpecChoices>,
3954 custom_env: Option<&HashMap<String, String>>,
3955) -> miette::Result<bool> {
3956 if is_help_arg(spec, cmd, value)
3957 && choices
3958 .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
3959 {
3960 errors.push(render_help_err(spec, cmd, value.len() > 2));
3961 return Ok(true);
3962 }
3963
3964 if let Some(err) = choice_error(target, value, choices, custom_env) {
3965 bail!("{err}");
3966 }
3967 Ok(false)
3968}
3969
3970fn validate_choice_value(
3971 target: ChoiceTarget<'_>,
3972 value: &str,
3973 choices: Option<&SpecChoices>,
3974 custom_env: Option<&HashMap<String, String>>,
3975) -> miette::Result<()> {
3976 if let Some(err) = choice_error(target, value, choices, custom_env) {
3977 bail!("{err}");
3978 }
3979 Ok(())
3980}
3981
3982fn validate_choice_values(
3983 target: ChoiceTarget<'_>,
3984 values: &[String],
3985 choices: Option<&SpecChoices>,
3986 custom_env: Option<&HashMap<String, String>>,
3987) -> miette::Result<()> {
3988 for value in values {
3989 validate_choice_value(target, value, choices, custom_env)?;
3990 }
3991 Ok(())
3992}
3993
3994fn record_stop(
4001 out: &mut ParseOutput,
4002 next_arg_idx: usize,
4003 seen_double_dash: bool,
4004 trace: &mut Trace,
4005 unread: &VecDeque<Token>,
4006) {
4007 out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
4008 out.double_dash_seen = seen_double_dash;
4009 trace.close(unread);
4010 out.tokens = std::mem::take(&mut trace.tokens);
4011}
4012
4013fn report_double_dash_violation(
4020 arg: &SpecArg,
4021 errors: &mut Vec<UsageErr>,
4022 violations: &mut HashSet<String>,
4023) {
4024 if violations.insert(arg.name.clone()) {
4025 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4026 }
4027}
4028
4029fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
4044 (spec.version.is_some() || spec.long_version.is_some())
4045 && cmds.len() == 1
4046 && !spec.cmd.disable_version_flag
4047 && (w == "--version" || w == "-V")
4048}
4049
4050fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
4051 spec.disable_help != Some(true)
4052 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
4053 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
4054}
4055
4056impl ParseOutput {
4057 pub fn as_env(&self) -> BTreeMap<String, String> {
4058 let mut env = BTreeMap::new();
4059 for (flag, val) in &self.flags {
4060 let key = format!("usage_{}", crate::case::snake(&flag.name));
4061 let val = match val {
4062 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
4063 ParseValue::String(s) => s.clone(),
4064 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
4065 ParseValue::MultiString(s) => crate::shell_words::join(s),
4066 };
4067 env.insert(key, val);
4068 }
4069 for (arg, val) in &self.args {
4070 let key = format!("usage_{}", crate::case::snake(&arg.name));
4071 env.insert(key, val.to_string());
4072 }
4073 env
4074 }
4075}
4076
4077impl Display for ParseValue {
4078 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4079 match self {
4080 ParseValue::Bool(b) => write!(f, "{b}"),
4081 ParseValue::String(s) => write!(f, "{s}"),
4082 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
4083 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
4084 }
4085 }
4086}
4087
4088fn render_token(token: &TokenBinding) -> String {
4090 let roles = token.roles.iter().map(render_role).join(", ");
4091 let synthesized = if token.synthesized { " (read as)" } else { "" };
4092 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4093}
4094
4095fn render_role(role: &TokenRole) -> String {
4096 match role {
4097 TokenRole::Program => "program".to_string(),
4098 TokenRole::Command { name } => format!("subcommand {name}"),
4099 TokenRole::Flag {
4100 flag,
4101 spelling,
4102 negated,
4103 } => {
4104 let negated = if *negated { ", negated" } else { "" };
4105 format!("flag {} as {spelling}{negated}", flag.name)
4106 }
4107 TokenRole::Value {
4108 flag,
4109 values,
4110 attached,
4111 } => {
4112 let attached = if *attached { ", attached" } else { "" };
4113 format!("value of {} = {values:?}{attached}", flag.name)
4114 }
4115 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4116 TokenRole::Separator => "separator".to_string(),
4117 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4118 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4119 TokenRole::Restart => "restart".to_string(),
4120 TokenRole::UnknownFlag { bound_as } => match bound_as {
4121 Some(arg) => format!("unknown flag, bound as {}", arg.name),
4122 None => "unknown flag".to_string(),
4123 },
4124 TokenRole::Refused { reason } => format!("refused: {reason}"),
4125 TokenRole::External => "external".to_string(),
4126 TokenRole::Unread => "unread".to_string(),
4127 }
4128}
4129
4130impl Debug for ParseOutput {
4131 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4132 f.debug_struct("ParseOutput")
4133 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4134 .field(
4135 "args",
4136 &self
4137 .args
4138 .iter()
4139 .map(|(a, w)| format!("{}: {w}", a.name))
4140 .collect_vec(),
4141 )
4142 .field(
4143 "available_flags",
4144 &self
4145 .available_flags
4146 .iter()
4147 .map(|(f, w)| format!("{f}: {w}"))
4148 .collect_vec(),
4149 )
4150 .field(
4151 "flags",
4152 &self
4153 .flags
4154 .iter()
4155 .map(|(f, w)| format!("{}: {w}", f.name))
4156 .collect_vec(),
4157 )
4158 .field("flag_awaiting_value", &self.flag_awaiting_value)
4159 .field("errors", &self.errors)
4160 .field("external", &self.external)
4161 .field(
4165 "tokens",
4166 &self.tokens.iter().map(render_token).collect_vec(),
4167 )
4168 .field(
4169 "origins",
4170 &self
4171 .flag_origins
4172 .iter()
4173 .map(|(f, o)| format!("{}: {o:?}", f.name))
4174 .chain(
4175 self.arg_origins
4176 .iter()
4177 .map(|(a, o)| format!("{}: {o:?}", a.name)),
4178 )
4179 .collect_vec(),
4180 )
4181 .field("overridden_flags", &self.overridden_flags)
4182 .finish()
4183 }
4184}
4185
4186#[cfg(test)]
4187mod tests {
4188 use super::*;
4189 use crate::SpecFlagAction;
4190
4191 fn input(words: &[&str]) -> Vec<String> {
4192 words.iter().map(|word| (*word).to_string()).collect()
4193 }
4194
4195 #[test]
4196 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4197 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4202 .parse()
4203 .unwrap();
4204
4205 for spelling in ["--version", "-V"] {
4206 let err = parse(&spec, &input(&["ex", spelling]))
4207 .expect_err("answering with a version ends the parse");
4208 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4209 }
4210 }
4211
4212 #[test]
4213 fn the_supplied_version_flag_is_the_roots_alone() {
4214 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4217 .parse()
4218 .unwrap();
4219
4220 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4221 assert_eq!(err.to_string(), "unexpected word: --version");
4222 }
4223
4224 #[test]
4225 fn no_declared_version_supplies_nothing() {
4226 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4229
4230 for spelling in ["--version", "-V"] {
4231 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4232 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4233 }
4234 }
4235
4236 #[test]
4237 fn disable_version_flag_removes_the_supplied_spellings() {
4238 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4239 .parse()
4240 .unwrap();
4241
4242 for spelling in ["--version", "-V"] {
4243 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4244 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4245 }
4246 }
4247
4248 #[test]
4249 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4250 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4253 .parse()
4254 .unwrap();
4255
4256 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4257 assert_eq!(out.flags.len(), 1);
4258
4259 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4260 assert_eq!(err.to_string(), "1.2.3");
4261 }
4262
4263 #[test]
4264 fn the_supplied_spellings_split_the_two_version_texts() {
4265 let spec: Spec =
4268 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4269 .parse()
4270 .unwrap();
4271
4272 assert_eq!(
4273 parse(&spec, &input(&["ex", "--version"]))
4274 .unwrap_err()
4275 .to_string(),
4276 "1.2.3 (abcdef)"
4277 );
4278 assert_eq!(
4279 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4280 "1.2.3"
4281 );
4282 }
4283
4284 #[test]
4285 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4286 let spec: Spec =
4291 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4292 .parse()
4293 .unwrap();
4294
4295 for token in ["-vh", "-hv"] {
4296 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4297 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4298 }
4299 for token in ["-vV", "-Vv"] {
4300 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4301 assert_eq!(err.to_string(), "1.2.3", "{token}");
4302 }
4303 }
4304
4305 #[test]
4306 fn a_bundled_help_letter_asks_for_the_short_page() {
4307 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"
4310 .parse()
4311 .unwrap();
4312
4313 let short = parse(&spec, &input(&["ex", "-vh"]))
4314 .unwrap_err()
4315 .to_string();
4316 let long = parse(&spec, &input(&["ex", "--help"]))
4317 .unwrap_err()
4318 .to_string();
4319 assert!(short.contains("Be loud"), "{short}");
4320 assert!(!short.contains("at length"), "{short}");
4321 assert!(long.contains("at length"), "{long}");
4322 }
4323
4324 #[test]
4325 fn the_bundled_version_letter_is_the_roots_alone() {
4326 let spec: Spec =
4328 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4329 .parse()
4330 .unwrap();
4331
4332 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4333 assert_eq!(err.to_string(), "unexpected word: -vV");
4334 }
4335
4336 #[test]
4337 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4338 let spec: Spec =
4341 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4342 .parse()
4343 .unwrap();
4344
4345 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4346 assert_eq!(out.flags.len(), 2);
4347 assert!(out
4348 .flags
4349 .iter()
4350 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4351 }
4352
4353 #[test]
4354 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4355 let spec: Spec =
4356 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4357 .parse()
4358 .unwrap();
4359
4360 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4361 assert_eq!(err.to_string(), "unexpected word: -vh");
4362 }
4363
4364 #[test]
4365 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4366 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4369 .parse()
4370 .unwrap();
4371
4372 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4373 assert!(out.flags.is_empty(), "{:?}", out.flags);
4374 assert_eq!(out.args.len(), 1);
4375 }
4376
4377 fn spec_with_arg(arg: SpecArg) -> Spec {
4378 let cmd = SpecCommand::builder().name("test").arg(arg).build();
4379 Spec {
4380 name: "test".to_string(),
4381 bin: "test".to_string(),
4382 cmd,
4383 ..Default::default()
4384 }
4385 }
4386
4387 fn spec_with_flag(flag: SpecFlag) -> Spec {
4388 let cmd = SpecCommand::builder().name("test").flag(flag).build();
4389 Spec {
4390 name: "test".to_string(),
4391 bin: "test".to_string(),
4392 cmd,
4393 ..Default::default()
4394 }
4395 }
4396
4397 fn parse_with_env(
4398 spec: &Spec,
4399 words: &[&str],
4400 env: &[(&str, &str)],
4401 ) -> Result<ParseOutput, miette::Error> {
4402 let env = env
4403 .iter()
4404 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4405 .collect();
4406 Parser::new(spec).with_env(env).parse(&input(words))
4407 }
4408
4409 fn first_string_value(parsed: &ParseOutput) -> &str {
4410 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4411 return value;
4412 }
4413 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4414 return value;
4415 }
4416 panic!("expected first parsed value to be ParseValue::String");
4417 }
4418
4419 #[test]
4420 fn custom_environment_parser_dispatches_executable_views() {
4421 let spec: Spec = r#"
4422bin "ex"
4423view "runner" root="run"
4424cmd "run" {
4425 flag "--token <token>" env="TOKEN"
4426}
4427 "#
4428 .parse()
4429 .unwrap();
4430 let parsed = Parser::new(&spec)
4431 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4432 .parse(&input(&["runner"]))
4433 .unwrap();
4434
4435 assert_eq!(parsed.cmd.name, "runner");
4436 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4437 && matches!(value, ParseValue::String(value) if value == "secret")));
4438 }
4439
4440 #[test]
4441 fn an_executable_view_keeps_the_hosts_version_action() {
4442 let spec: Spec = r#"
4443bin "ex"
4444version "1.2.3"
4445flag "-V --version" action="version"
4446flag "--verbose" global=#true
4447view "runner" root="run" globals=#true
4448cmd "run"
4449 "#
4450 .parse()
4451 .unwrap();
4452
4453 let error = Parser::new(&spec)
4454 .parse(&input(&["runner", "--version"]))
4455 .expect_err("the host version action should answer before view projection");
4456 assert_eq!(error.to_string(), "1.2.3");
4457
4458 let error = Parser::new(&spec)
4459 .parse(&input(&["runner", "--verbose", "--version"]))
4460 .expect_err("the host version action should remain after a carried global");
4461 assert_eq!(error.to_string(), "1.2.3");
4462 }
4463
4464 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4465 let flag = parsed
4466 .flags
4467 .keys()
4468 .find(|flag| flag.name == name)
4469 .unwrap_or_else(|| panic!("expected flag {name}"));
4470 let value = parsed
4471 .flags
4472 .get(flag)
4473 .unwrap_or_else(|| panic!("expected value for flag {name}"));
4474 match value {
4475 ParseValue::String(value) => value,
4476 _ => panic!("expected flag {name} to be ParseValue::String"),
4477 }
4478 }
4479
4480 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4481 let err = result.expect_err("expected parser error");
4482 assert_eq!(format!("{err}"), expected);
4483 }
4484
4485 #[test]
4486 fn a_short_version_action_falls_back_to_the_long_version() {
4487 let flag = SpecFlag::builder()
4488 .short('R')
4489 .action(SpecFlagAction::Version)
4490 .build();
4491 let spec = Spec {
4492 name: "test".to_string(),
4493 bin: "test".to_string(),
4494 long_version: Some("1.2.3\ncommit abc123".to_string()),
4495 ..Default::default()
4496 };
4497 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4498 panic!("expected version action")
4499 };
4500 assert_eq!(version, "1.2.3\ncommit abc123");
4501 }
4502
4503 #[cfg(feature = "unstable_choices_env")]
4504 fn spec_arg_choices_env(key: &str) -> Spec {
4505 spec_with_arg(
4506 SpecArg::builder()
4507 .name("env")
4508 .choices_env(key)
4509 .required(false)
4510 .build(),
4511 )
4512 }
4513
4514 #[cfg(feature = "unstable_choices_env")]
4515 fn spec_flag_choices_env(key: &str) -> Spec {
4516 spec_with_flag(
4517 SpecFlag::builder()
4518 .long("env")
4519 .arg(SpecArg::builder().name("env").choices_env(key).build())
4520 .build(),
4521 )
4522 }
4523
4524 #[test]
4525 fn test_parse() {
4526 let cmd = SpecCommand::builder()
4527 .name("test")
4528 .arg(SpecArg::builder().name("arg").build())
4529 .flag(SpecFlag::builder().long("flag").build())
4530 .build();
4531 let spec = Spec {
4532 name: "test".to_string(),
4533 bin: "test".to_string(),
4534 cmd,
4535 ..Default::default()
4536 };
4537 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4538 let parsed = parse(&spec, &input).unwrap();
4539 assert_eq!(parsed.cmds.len(), 1);
4540 assert_eq!(parsed.cmds[0].name, "test");
4541 assert_eq!(parsed.args.len(), 1);
4542 assert_eq!(parsed.flags.len(), 1);
4543 assert_eq!(parsed.available_flags.len(), 1);
4544 }
4545
4546 #[test]
4547 fn test_flag_overrides_last_occurrence_wins() {
4548 let spec: Spec = r#"
4549flag "--stdin" default=#true
4550flag "--file <file>" overrides="--stdin"
4551 "#
4552 .parse()
4553 .unwrap();
4554
4555 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4556 assert_eq!(file_wins.flags.len(), 1);
4557 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4558 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4559
4560 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
4561 assert_eq!(stdin_wins.flags.len(), 1);
4562 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
4563 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
4564 }
4565
4566 #[test]
4567 fn test_flag_override_clears_pending_value() {
4568 let spec: Spec = r#"
4569flag "--file <file>" overrides="--stdin"
4570flag "--stdin"
4571arg "[input]"
4572 "#
4573 .parse()
4574 .unwrap();
4575
4576 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
4577 assert_eq!(parsed.flags.len(), 1);
4578 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
4579 assert_eq!(first_string_value(&parsed), "input.txt");
4580 }
4581
4582 #[cfg(unix)]
4583 #[test]
4584 fn a_mount_on_the_root_discovers_subcommands() {
4585 let spec: Spec = r#"
4589name "ex"
4590bin "ex"
4591cmd "declared"
4592mount run="echo 'cmd \"discovered\"'"
4593"#
4594 .parse()
4595 .unwrap();
4596
4597 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4598 assert_eq!(out.cmd.name, "discovered");
4599 }
4600
4601 #[test]
4602 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
4603 let spec: Spec = r#"
4604name "ex"
4605bin "ex"
4606mount run="this command must never run"
4607cmd "declared"
4608"#
4609 .parse()
4610 .unwrap();
4611
4612 Parser::new(&spec)
4613 .with_mount_outputs(HashMap::new())
4614 .parse(&input(&["ex", "declared"]))
4615 .expect("a declared command does not resolve the mount");
4616
4617 let error = Parser::new(&spec)
4618 .with_mount_outputs(HashMap::new())
4619 .parse(&input(&["ex", "discovered"]))
4620 .unwrap_err();
4621 assert!(
4622 error
4623 .to_string()
4624 .contains("No injected output was provided for mount command"),
4625 "{error}"
4626 );
4627 }
4628
4629 #[cfg(unix)]
4630 #[test]
4631 fn completion_sees_root_mounted_commands_with_nothing_typed() {
4632 let spec: Spec = r#"
4636name "ex"
4637bin "ex"
4638cmd "declared"
4639mount run="echo 'cmd \"discovered\"'"
4640"#
4641 .parse()
4642 .unwrap();
4643
4644 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4645 assert!(
4646 out.cmd.subcommands.contains_key("discovered"),
4647 "a completion should see mounted commands; got {:?}",
4648 out.cmd.subcommands.keys().collect::<Vec<_>>()
4649 );
4650 }
4651
4652 #[cfg(unix)]
4653 #[test]
4654 fn completion_and_execution_agree_about_discovery() {
4655 let spec: Spec = r#"
4659name "ex"
4660bin "ex"
4661default_subcommand "run"
4662cmd "run" {
4663 arg "<task>"
4664}
4665mount run="exit 1"
4666"#
4667 .parse()
4668 .unwrap();
4669
4670 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4671 assert!(
4672 !out.cmd.subcommands.contains_key("discovered"),
4673 "a completion must not offer what execution will not route"
4674 );
4675
4676 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4677 assert_eq!(out.cmd.name, "run");
4678 }
4679
4680 #[cfg(unix)]
4681 #[test]
4682 fn a_default_subcommand_outranks_discovery() {
4683 let spec: Spec = r#"
4686name "ex"
4687bin "ex"
4688default_subcommand "run"
4689cmd "run" {
4690 arg "<task>"
4691}
4692mount run="exit 1"
4693"#
4694 .parse()
4695 .unwrap();
4696
4697 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4698 assert_eq!(out.cmd.name, "run");
4699 }
4700
4701 #[cfg(unix)]
4702 #[test]
4703 fn a_mount_may_ask_to_outrank_the_default() {
4704 let spec: Spec = r#"
4707name "ex"
4708bin "ex"
4709default_subcommand "run"
4710cmd "run" {
4711 arg "<task>"
4712}
4713mount run="echo 'cmd \"discovered\"'" overrides_default=#true
4714"#
4715 .parse()
4716 .unwrap();
4717
4718 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4719 assert_eq!(out.cmd.name, "discovered");
4720
4721 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4723 assert_eq!(out.cmd.name, "run");
4724 }
4725
4726 #[cfg(unix)]
4727 #[test]
4728 fn a_flag_does_not_run_the_mount() {
4729 let spec: Spec = r#"
4733name "ex"
4734bin "ex"
4735flag "--verbose"
4736cmd "declared"
4737mount run="exit 1"
4738"#
4739 .parse()
4740 .unwrap();
4741
4742 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
4743 assert_eq!(out.cmd.name, "ex");
4744 }
4745
4746 #[cfg(unix)]
4747 #[test]
4748 fn a_declared_subcommand_does_not_run_the_mount() {
4749 let spec: Spec = r#"
4753name "ex"
4754bin "ex"
4755cmd "declared"
4756mount run="exit 1"
4757"#
4758 .parse()
4759 .unwrap();
4760
4761 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
4762 assert_eq!(out.cmd.name, "declared");
4763 }
4764
4765 #[test]
4766 fn a_root_mount_survives_being_written_out() {
4767 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
4768 .parse()
4769 .unwrap();
4770 assert_eq!(spec.cmd.mounts.len(), 1);
4771
4772 let reparsed: Spec = spec.to_string().parse().unwrap();
4773 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
4774 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
4775 }
4776
4777 #[test]
4778 fn test_mount_prefix_applies_flag_overrides() {
4779 let stdin = Arc::new(
4780 SpecFlag::builder()
4781 .name("stdin")
4782 .long("stdin")
4783 .global(true)
4784 .build(),
4785 );
4786 let file = Arc::new(
4787 SpecFlag::builder()
4788 .name("file")
4789 .long("file")
4790 .arg(SpecArg::builder().name("file").build())
4791 .global(true)
4792 .overrides_with(vec!["--stdin".to_string()])
4793 .build(),
4794 );
4795 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
4796
4797 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
4798 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
4799
4800 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
4801 }
4802
4803 #[test]
4804 fn test_flag_override_suppresses_env_value() {
4805 let spec: Spec = r#"
4806flag "--stdin" env="USE_STDIN"
4807flag "--file <file>" overrides="--stdin"
4808 "#
4809 .parse()
4810 .unwrap();
4811
4812 let parsed = parse_with_env(
4813 &spec,
4814 &["test", "--file", "input.txt"],
4815 &[("USE_STDIN", "true")],
4816 )
4817 .unwrap();
4818 assert_eq!(parsed.flags.len(), 1);
4819 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4820 }
4821
4822 #[test]
4823 fn test_flag_override_suppresses_required_check() {
4824 let spec: Spec = r#"
4825flag "--stdin" required=#true
4826flag "--file <file>" overrides="--stdin"
4827 "#
4828 .parse()
4829 .unwrap();
4830
4831 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4832 assert_eq!(parsed.flags.len(), 1);
4833 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4834 }
4835
4836 #[test]
4837 fn test_flag_required_if() {
4838 let spec: Spec = r#"
4839flag "--dir <dir>"
4840flag "--file <file>" required_if="--dir"
4841 "#
4842 .parse()
4843 .unwrap();
4844
4845 parse(&spec, &input(&["test"])).unwrap();
4846 assert_parse_err(
4847 parse(&spec, &input(&["test", "--dir", "src"])),
4848 "Missing required flag: --file <file>",
4849 );
4850 parse(
4851 &spec,
4852 &input(&["test", "--dir", "src", "--file", "input.txt"]),
4853 )
4854 .unwrap();
4855 }
4856
4857 #[test]
4858 fn test_flag_required_unless() {
4859 let spec: Spec = r#"
4860flag "--stdin"
4861flag "--file <file>" required_unless="--stdin"
4862 "#
4863 .parse()
4864 .unwrap();
4865
4866 assert_parse_err(
4867 parse(&spec, &input(&["test"])),
4868 "Missing required flag: --file <file>",
4869 );
4870 parse(&spec, &input(&["test", "--stdin"])).unwrap();
4871 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4872 }
4873
4874 #[test]
4875 fn complete_required_relationship_truth_tables() {
4876 let spec: Spec = r#"
4877name "ex"
4878bin "ex"
4879flag "--mode <mode>"
4880flag "--scope <scope>"
4881flag "--token <token>" {
4882 required_if_eq "--mode" "remote"
4883}
4884flag "--approval <approval>" {
4885 required_if_eq_all "--mode" "remote" "--scope" "global"
4886}
4887flag "--input <input>" {
4888 required_unless "--stdin" "--file"
4889}
4890flag "--checksum <checksum>" {
4891 required_unless_all "--stdin" "--file"
4892}
4893flag "--stdin"
4894flag "--file <file>"
4895arg "[request]" {
4896 requires "--mode" "--scope"
4897}
4898"#
4899 .parse()
4900 .unwrap();
4901 let parse_args = |args: &[&str]| {
4902 parse(
4903 &spec,
4904 &args
4905 .iter()
4906 .map(|arg| (*arg).to_string())
4907 .collect::<Vec<_>>(),
4908 )
4909 };
4910
4911 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
4912 assert!(parse_args(&[
4913 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
4914 ])
4915 .is_err());
4916 parse_args(&[
4917 "ex",
4918 "--mode",
4919 "remote",
4920 "--token",
4921 "secret",
4922 "--scope",
4923 "global",
4924 "--approval",
4925 "yes",
4926 "--stdin",
4927 "--file",
4928 "in",
4929 ])
4930 .unwrap();
4931 parse_args(&[
4932 "ex",
4933 "--mode",
4934 "local",
4935 "--scope",
4936 "project",
4937 "--stdin",
4938 "--checksum",
4939 "sum",
4940 "request.json",
4941 ])
4942 .unwrap();
4943
4944 let reparsed: Spec = spec.to_string().parse().unwrap();
4945 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
4946 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
4947 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
4948 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
4949 }
4950
4951 #[test]
4952 fn test_conditional_requirements_treat_env_as_explicit() {
4953 let spec: Spec = r#"
4954flag "--dir <dir>" env="INPUT_DIR"
4955flag "--stdin" env="USE_STDIN"
4956flag "--file <file>" required_if="--dir" required_unless="--stdin"
4957 "#
4958 .parse()
4959 .unwrap();
4960
4961 assert_parse_err(
4962 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
4963 "Missing required flag: --file <file>",
4964 );
4965 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
4966 }
4967
4968 #[test]
4969 fn test_custom_env_does_not_fall_back_to_process_env() {
4970 assert!(std::env::var("PATH").is_ok());
4971 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
4972
4973 assert_parse_err(
4974 parse_with_env(&spec, &["test"], &[]),
4975 "Missing required flag: --file <file>",
4976 );
4977 }
4978
4979 #[test]
4980 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
4981 let spec: Spec = r#"
4982flag "--dir <dir>" default="src"
4983flag "--file <file>" required_if="--dir"
4984 "#
4985 .parse()
4986 .unwrap();
4987
4988 parse(&spec, &input(&["test"])).unwrap();
4989 }
4990
4991 #[test]
4992 fn test_conditional_requirements_see_overridden_flags_as_absent() {
4993 let spec: Spec = r#"
4994flag "--stdin"
4995flag "--dir <dir>" overrides="--stdin"
4996flag "--file <file>" required_unless="--stdin"
4997 "#
4998 .parse()
4999 .unwrap();
5000
5001 assert_parse_err(
5002 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5003 "Missing required flag: --file <file>",
5004 );
5005 }
5006
5007 #[test]
5008 fn short_flag_is_one_character_not_one_byte() {
5009 let spec = spec_with_flag(
5013 SpecFlag::builder()
5014 .short('磨')
5015 .long("polish")
5016 .arg(SpecArg::builder().name("opt").build())
5017 .build(),
5018 );
5019 let attached = Parser::new(&spec)
5020 .parse(&input(&["test", "-磨VALUE"]))
5021 .unwrap();
5022 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5023 let detached = Parser::new(&spec)
5024 .parse(&input(&["test", "-磨", "V"]))
5025 .unwrap();
5026 assert_eq!(flag_string_value(&detached, "polish"), "V");
5027 }
5028
5029 #[test]
5030 fn test_as_env() {
5031 let cmd = SpecCommand::builder()
5032 .name("test")
5033 .arg(SpecArg::builder().name("arg").build())
5034 .flag(SpecFlag::builder().long("flag").build())
5035 .flag(
5036 SpecFlag::builder()
5037 .long("force")
5038 .negate("--no-force")
5039 .build(),
5040 )
5041 .build();
5042 let spec = Spec {
5043 name: "test".to_string(),
5044 bin: "test".to_string(),
5045 cmd,
5046 ..Default::default()
5047 };
5048 let input = vec![
5049 "test".to_string(),
5050 "--flag".to_string(),
5051 "--no-force".to_string(),
5052 ];
5053 let parsed = parse(&spec, &input).unwrap();
5054 let env = parsed.as_env();
5055 assert_eq!(env.len(), 2);
5056 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
5057 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
5058 }
5059
5060 #[test]
5061 fn test_arg_env_var() {
5062 let cmd = SpecCommand::builder()
5063 .name("test")
5064 .arg(
5065 SpecArg::builder()
5066 .name("input")
5067 .env("TEST_ARG_INPUT")
5068 .required(true)
5069 .build(),
5070 )
5071 .build();
5072 let spec = Spec {
5073 name: "test".to_string(),
5074 bin: "test".to_string(),
5075 cmd,
5076 ..Default::default()
5077 };
5078
5079 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
5081
5082 let input = vec!["test".to_string()];
5083 let parsed = parse(&spec, &input).unwrap();
5084
5085 assert_eq!(parsed.args.len(), 1);
5086 let arg = parsed.args.keys().next().unwrap();
5087 assert_eq!(arg.name, "input");
5088 let value = parsed.args.values().next().unwrap();
5089 assert_eq!(value.to_string(), "test_file.txt");
5090
5091 std::env::remove_var("TEST_ARG_INPUT");
5093 }
5094
5095 #[test]
5096 fn test_flag_env_var_with_arg() {
5097 let cmd = SpecCommand::builder()
5098 .name("test")
5099 .flag(
5100 SpecFlag::builder()
5101 .long("output")
5102 .env("TEST_FLAG_OUTPUT")
5103 .arg(SpecArg::builder().name("file").build())
5104 .build(),
5105 )
5106 .build();
5107 let spec = Spec {
5108 name: "test".to_string(),
5109 bin: "test".to_string(),
5110 cmd,
5111 ..Default::default()
5112 };
5113
5114 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5116
5117 let input = vec!["test".to_string()];
5118 let parsed = parse(&spec, &input).unwrap();
5119
5120 assert_eq!(parsed.flags.len(), 1);
5121 let flag = parsed.flags.keys().next().unwrap();
5122 assert_eq!(flag.name, "output");
5123 let value = parsed.flags.values().next().unwrap();
5124 assert_eq!(value.to_string(), "output.txt");
5125
5126 std::env::remove_var("TEST_FLAG_OUTPUT");
5128 }
5129
5130 #[test]
5131 fn test_flag_env_var_boolean() {
5132 let cmd = SpecCommand::builder()
5133 .name("test")
5134 .flag(
5135 SpecFlag::builder()
5136 .long("verbose")
5137 .env("TEST_FLAG_VERBOSE")
5138 .build(),
5139 )
5140 .build();
5141 let spec = Spec {
5142 name: "test".to_string(),
5143 bin: "test".to_string(),
5144 cmd,
5145 ..Default::default()
5146 };
5147
5148 std::env::set_var("TEST_FLAG_VERBOSE", "true");
5150
5151 let input = vec!["test".to_string()];
5152 let parsed = parse(&spec, &input).unwrap();
5153
5154 assert_eq!(parsed.flags.len(), 1);
5155 let flag = parsed.flags.keys().next().unwrap();
5156 assert_eq!(flag.name, "verbose");
5157 let value = parsed.flags.values().next().unwrap();
5158 assert_eq!(value.to_string(), "true");
5159
5160 std::env::remove_var("TEST_FLAG_VERBOSE");
5162 }
5163
5164 #[test]
5165 fn test_env_var_precedence() {
5166 let cmd = SpecCommand::builder()
5168 .name("test")
5169 .arg(
5170 SpecArg::builder()
5171 .name("input")
5172 .env("TEST_PRECEDENCE_INPUT")
5173 .required(true)
5174 .build(),
5175 )
5176 .build();
5177 let spec = Spec {
5178 name: "test".to_string(),
5179 bin: "test".to_string(),
5180 cmd,
5181 ..Default::default()
5182 };
5183
5184 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5186
5187 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5188 let parsed = parse(&spec, &input).unwrap();
5189
5190 assert_eq!(parsed.args.len(), 1);
5191 let value = parsed.args.values().next().unwrap();
5192 assert_eq!(value.to_string(), "cli_file.txt");
5194
5195 std::env::remove_var("TEST_PRECEDENCE_INPUT");
5197 }
5198
5199 #[test]
5200 fn test_flag_var_true_with_single_default() {
5201 let cmd = SpecCommand::builder()
5203 .name("test")
5204 .flag(
5205 SpecFlag::builder()
5206 .long("foo")
5207 .var(true)
5208 .arg(SpecArg::builder().name("foo").build())
5209 .default_value("bar")
5210 .build(),
5211 )
5212 .build();
5213 let spec = Spec {
5214 name: "test".to_string(),
5215 bin: "test".to_string(),
5216 cmd,
5217 ..Default::default()
5218 };
5219
5220 let input = vec!["test".to_string()];
5222 let parsed = parse(&spec, &input).unwrap();
5223
5224 assert_eq!(parsed.flags.len(), 1);
5225 let flag = parsed.flags.keys().next().unwrap();
5226 assert_eq!(flag.name, "foo");
5227 let value = parsed.flags.values().next().unwrap();
5228 match value {
5230 ParseValue::MultiString(v) => {
5231 assert_eq!(v.len(), 1);
5232 assert_eq!(v[0], "bar");
5233 }
5234 _ => panic!("Expected MultiString, got {:?}", value),
5235 }
5236 }
5237
5238 #[test]
5239 fn test_flag_var_true_with_multiple_defaults() {
5240 let cmd = SpecCommand::builder()
5242 .name("test")
5243 .flag(
5244 SpecFlag::builder()
5245 .long("foo")
5246 .var(true)
5247 .arg(SpecArg::builder().name("foo").build())
5248 .default_values(["xyz", "bar"])
5249 .build(),
5250 )
5251 .build();
5252 let spec = Spec {
5253 name: "test".to_string(),
5254 bin: "test".to_string(),
5255 cmd,
5256 ..Default::default()
5257 };
5258
5259 let input = vec!["test".to_string()];
5261 let parsed = parse(&spec, &input).unwrap();
5262
5263 assert_eq!(parsed.flags.len(), 1);
5264 let value = parsed.flags.values().next().unwrap();
5265 match value {
5267 ParseValue::MultiString(v) => {
5268 assert_eq!(v.len(), 2);
5269 assert_eq!(v[0], "xyz");
5270 assert_eq!(v[1], "bar");
5271 }
5272 _ => panic!("Expected MultiString, got {:?}", value),
5273 }
5274 }
5275
5276 #[test]
5277 fn test_flag_var_false_with_default_remains_string() {
5278 let cmd = SpecCommand::builder()
5280 .name("test")
5281 .flag(
5282 SpecFlag::builder()
5283 .long("foo")
5284 .var(false) .arg(SpecArg::builder().name("foo").build())
5286 .default_value("bar")
5287 .build(),
5288 )
5289 .build();
5290 let spec = Spec {
5291 name: "test".to_string(),
5292 bin: "test".to_string(),
5293 cmd,
5294 ..Default::default()
5295 };
5296
5297 let input = vec!["test".to_string()];
5299 let parsed = parse(&spec, &input).unwrap();
5300
5301 assert_eq!(parsed.flags.len(), 1);
5302 let value = parsed.flags.values().next().unwrap();
5303 match value {
5305 ParseValue::String(s) => {
5306 assert_eq!(s, "bar");
5307 }
5308 _ => panic!("Expected String, got {:?}", value),
5309 }
5310 }
5311
5312 #[test]
5313 fn test_arg_var_true_with_single_default() {
5314 let cmd = SpecCommand::builder()
5316 .name("test")
5317 .arg(
5318 SpecArg::builder()
5319 .name("files")
5320 .var(true)
5321 .default_value("default.txt")
5322 .required(false)
5323 .build(),
5324 )
5325 .build();
5326 let spec = Spec {
5327 name: "test".to_string(),
5328 bin: "test".to_string(),
5329 cmd,
5330 ..Default::default()
5331 };
5332
5333 let input = vec!["test".to_string()];
5335 let parsed = parse(&spec, &input).unwrap();
5336
5337 assert_eq!(parsed.args.len(), 1);
5338 let value = parsed.args.values().next().unwrap();
5339 match value {
5341 ParseValue::MultiString(v) => {
5342 assert_eq!(v.len(), 1);
5343 assert_eq!(v[0], "default.txt");
5344 }
5345 _ => panic!("Expected MultiString, got {:?}", value),
5346 }
5347 }
5348
5349 #[test]
5350 fn test_arg_var_true_with_multiple_defaults() {
5351 let cmd = SpecCommand::builder()
5353 .name("test")
5354 .arg(
5355 SpecArg::builder()
5356 .name("files")
5357 .var(true)
5358 .default_values(["file1.txt", "file2.txt"])
5359 .required(false)
5360 .build(),
5361 )
5362 .build();
5363 let spec = Spec {
5364 name: "test".to_string(),
5365 bin: "test".to_string(),
5366 cmd,
5367 ..Default::default()
5368 };
5369
5370 let input = vec!["test".to_string()];
5372 let parsed = parse(&spec, &input).unwrap();
5373
5374 assert_eq!(parsed.args.len(), 1);
5375 let value = parsed.args.values().next().unwrap();
5376 match value {
5378 ParseValue::MultiString(v) => {
5379 assert_eq!(v.len(), 2);
5380 assert_eq!(v[0], "file1.txt");
5381 assert_eq!(v[1], "file2.txt");
5382 }
5383 _ => panic!("Expected MultiString, got {:?}", value),
5384 }
5385 }
5386
5387 #[test]
5388 fn test_arg_var_false_with_default_remains_string() {
5389 let cmd = SpecCommand::builder()
5391 .name("test")
5392 .arg(
5393 SpecArg::builder()
5394 .name("file")
5395 .var(false)
5396 .default_value("default.txt")
5397 .required(false)
5398 .build(),
5399 )
5400 .build();
5401 let spec = Spec {
5402 name: "test".to_string(),
5403 bin: "test".to_string(),
5404 cmd,
5405 ..Default::default()
5406 };
5407
5408 let input = vec!["test".to_string()];
5410 let parsed = parse(&spec, &input).unwrap();
5411
5412 assert_eq!(parsed.args.len(), 1);
5413 let value = parsed.args.values().next().unwrap();
5414 match value {
5416 ParseValue::String(s) => {
5417 assert_eq!(s, "default.txt");
5418 }
5419 _ => panic!("Expected String, got {:?}", value),
5420 }
5421 }
5422
5423 #[test]
5424 fn test_scalar_defaults_validate_only_first_default_choice() {
5425 let specs = [
5426 spec_with_arg(
5427 SpecArg::builder()
5428 .name("env")
5429 .var(false)
5430 .default_values(["dev", "prod"])
5431 .choices(["dev"])
5432 .required(false)
5433 .build(),
5434 ),
5435 spec_with_flag(
5436 SpecFlag::builder()
5437 .long("env")
5438 .arg(
5439 SpecArg::builder()
5440 .name("env")
5441 .default_values(["dev", "prod"])
5442 .choices(["dev"])
5443 .build(),
5444 )
5445 .build(),
5446 ),
5447 ];
5448
5449 for spec in specs {
5450 let parsed = parse(&spec, &input(&["test"])).unwrap();
5451 assert_eq!(first_string_value(&parsed), "dev");
5452 }
5453 }
5454
5455 #[test]
5456 fn a_delimiter_turns_one_word_into_several_values() {
5457 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5458 .parse()
5459 .unwrap();
5460
5461 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5462 let multi = |value: &ParseValue| match value {
5463 ParseValue::MultiString(values) => values.clone(),
5464 other => panic!("expected several values, got {other:?}"),
5465 };
5466 let tags = parsed
5467 .flags
5468 .iter()
5469 .find(|(f, _)| f.name == "tags")
5470 .map(|(_, v)| v)
5471 .unwrap();
5472 assert_eq!(multi(tags), vec!["a", "b", "c"]);
5473 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5474 }
5475
5476 #[test]
5477 fn a_positional_splits_before_its_choices_are_asked() {
5478 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
5482 .parse()
5483 .unwrap();
5484
5485 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5486
5487 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5488 let message = err.to_string();
5489 assert!(message.contains("nowhere"), "{message}");
5490 assert!(
5491 !message.contains("src:nowhere"),
5492 "the bad half should be named, not the whole word: {message}"
5493 );
5494 }
5495
5496 #[test]
5497 fn a_split_value_is_counted_and_judged_as_values() {
5498 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
5502 .parse()
5503 .unwrap();
5504
5505 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5506 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5507 assert!(err.to_string().contains("staging"), "{err}");
5508 assert!(
5509 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5510 "three values should breach var_max=2"
5511 );
5512 }
5513
5514 #[test]
5515 fn a_split_bound_counts_one_occurrence_at_a_time() {
5516 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
5521 .parse()
5522 .unwrap();
5523
5524 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5525 assert!(
5526 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5527 "three values out of one word is still three values"
5528 );
5529 parse(
5532 &spec,
5533 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5534 )
5535 .expect("two per occurrence, twice, is within the bound");
5536 }
5537
5538 #[test]
5539 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5540 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5541 .parse()
5542 .unwrap();
5543
5544 parse(
5545 &spec,
5546 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5547 )
5548 .expect("each occurrence satisfies the bound independently");
5549
5550 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5551 assert!(
5552 error
5553 .to_string()
5554 .contains("requires at least 2 value(s), got 1"),
5555 "{error:?}"
5556 );
5557 }
5558
5559 #[test]
5560 fn an_exclusive_flag_has_to_be_alone() {
5561 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
5562 .parse()
5563 .unwrap();
5564
5565 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
5566
5567 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
5569 assert!(err.to_string().contains("on its own"), "{err}");
5570
5571 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
5574 assert!(err.to_string().contains("on its own"), "{err}");
5575
5576 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
5578 }
5579
5580 #[test]
5581 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
5582 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
5585 .parse()
5586 .unwrap();
5587
5588 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
5589 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
5590 }
5591
5592 #[test]
5593 fn an_exclusive_flag_bypasses_required_siblings() {
5594 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
5595 .parse()
5596 .unwrap();
5597
5598 parse(&spec, &input(&["ex", "--dump"]))
5599 .expect("exclusive is the command's requiredness escape");
5600 assert!(parse(
5601 &spec,
5602 &input(&["ex", "--dump", "--out", "somewhere", "target"])
5603 )
5604 .is_err());
5605 }
5606
5607 #[test]
5608 fn an_environment_value_counts_for_an_exclusive_flag() {
5609 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
5610 .parse()
5611 .unwrap();
5612
5613 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
5614 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
5615 }
5616
5617 #[test]
5618 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
5619 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
5620 .parse()
5621 .unwrap();
5622
5623 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
5624 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
5625 }
5626
5627 #[test]
5628 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
5629 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5630 .parse()
5631 .unwrap();
5632
5633 parse(&spec, &input(&["ex", "run", "--clean"]))
5634 .expect("the child flag is alone within the child command");
5635 assert!(
5636 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5637 "the parent flag still conflicts with selecting the child"
5638 );
5639 }
5640
5641 #[test]
5642 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
5643 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5644 .parse()
5645 .unwrap();
5646
5647 parse(&spec, &input(&["ex", "run", "--clean"]))
5648 .expect("the child-local exclusive flag is alone inside the child command");
5649 assert!(
5650 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5651 "the ancestor spelling still conflicts with selecting the child"
5652 );
5653 }
5654
5655 #[test]
5656 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
5657 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
5658 .parse()
5659 .unwrap();
5660
5661 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
5662 assert!(
5663 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
5664 "the distinct parent declaration is still company despite sharing a name"
5665 );
5666 }
5667
5668 #[test]
5669 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
5670 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5671 .parse()
5672 .unwrap();
5673
5674 parse(&spec, &input(&["ex", "run", "--clean"]))
5675 .expect("the child exclusive flag is valid alone");
5676 assert!(
5677 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5678 "merging with the inherited global must not discard child exclusivity"
5679 );
5680 }
5681
5682 #[test]
5683 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
5684 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5685 .parse()
5686 .unwrap();
5687
5688 parse(&spec, &input(&["ex", "run", "--clean"]))
5689 .expect("the typed long form belongs to the child declaration");
5690 assert!(
5691 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5692 "the inherited short form still belongs to the ancestor"
5693 );
5694 assert!(
5695 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
5696 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
5697 );
5698 }
5699
5700 #[test]
5701 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
5702 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
5703 .parse()
5704 .unwrap();
5705
5706 parse(&spec, &input(&["ex", "run", "--clean"]))
5707 .expect("the child's spelling does not activate the orphan ancestor alias");
5708 assert!(
5709 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5710 "the inherited short alias still belongs to the ancestor exclusive flag"
5711 );
5712 }
5713
5714 #[test]
5715 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
5716 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"
5717 .parse()
5718 .unwrap();
5719
5720 assert!(
5721 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
5722 "the inherited negated alias still belongs to the ancestor exclusive flag"
5723 );
5724 }
5725
5726 #[test]
5727 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
5728 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"
5733 .parse()
5734 .unwrap();
5735
5736 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
5737 assert!(
5738 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5739 "one unrelated alias collision cannot disown the child from its own flag"
5740 );
5741 }
5742
5743 #[test]
5744 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
5745 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5751 .parse()
5752 .unwrap();
5753 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
5754 .expect("the ancestor owns this occurrence, and it is not exclusive");
5755 assert!(
5756 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5757 "after the subcommand word the child's declaration is in scope, and it is exclusive"
5758 );
5759
5760 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5761 .parse()
5762 .unwrap();
5763 assert!(
5764 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
5765 "the same rule, with an exclusive ancestor: selecting the child is company for it"
5766 );
5767 }
5768
5769 #[test]
5770 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
5771 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
5775 .parse()
5776 .unwrap();
5777
5778 assert!(
5779 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5780 "the orphan ancestor alias is still the ancestor's exclusive flag"
5781 );
5782 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
5783 .expect("the child's own spelling drops the exclusivity the child did not restate");
5784 }
5785
5786 #[test]
5787 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
5788 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5792 .parse()
5793 .unwrap();
5794
5795 assert!(
5796 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
5797 "the child spelling is exclusive whatever it was typed beside"
5798 );
5799 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
5800 .expect("the ancestor's own spelling was never exclusive");
5801 }
5802
5803 #[test]
5804 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
5805 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"
5809 .parse()
5810 .unwrap();
5811
5812 assert!(
5813 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
5814 "the child added exclusivity the environment value has to honor"
5815 );
5816
5817 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"
5818 .parse()
5819 .unwrap();
5820
5821 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
5822 .expect("the child dropped the exclusivity, and the environment value follows it");
5823 }
5824
5825 #[test]
5826 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
5827 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"
5830 .parse()
5831 .unwrap();
5832
5833 parse(&spec, &input(&["ex", "run", "--clean"]))
5834 .expect("a merged child exclusive flag is still the command's requiredness escape");
5835 }
5836
5837 #[test]
5838 fn a_group_allows_one_member_and_refuses_two() {
5839 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
5840 .parse()
5841 .unwrap();
5842
5843 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
5845 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
5846
5847 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
5848 assert!(err.to_string().contains("group input"), "{err}");
5849 }
5850
5851 #[test]
5852 fn positional_selectors_work_in_conflicts_and_groups() {
5853 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
5854 .parse()
5855 .unwrap();
5856 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
5857 .expect("the flag alone is valid");
5858 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
5859 assert!(parse(
5860 &conflicts,
5861 &input(&["ex", "--from-file", "vars.env", "literal"])
5862 )
5863 .is_err());
5864
5865 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
5866 .parse()
5867 .unwrap();
5868 assert!(parse(
5869 &positional_source,
5870 &input(&["ex", "--from-file", "vars.env", "literal"])
5871 )
5872 .is_err());
5873
5874 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
5875 .parse()
5876 .unwrap();
5877 assert!(parse(&group, &input(&["ex"])).is_err());
5878 parse(&group, &input(&["ex", "target-name"]))
5879 .expect("a positional satisfies a required group");
5880 assert!(parse(
5881 &group,
5882 &input(&["ex", "--file", "input.txt", "target-name"])
5883 )
5884 .is_err());
5885 }
5886
5887 #[test]
5888 fn a_required_group_needs_one_of_its_members() {
5889 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5890 .parse()
5891 .unwrap();
5892
5893 let err = parse(&spec, &input(&["ex"])).unwrap_err();
5894 assert!(err.to_string().contains("--file, --url"), "{err}");
5897 assert!(err.to_string().contains("input"), "{err}");
5898
5899 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
5900 }
5901
5902 #[test]
5903 fn a_multiple_group_only_polices_requiredness() {
5904 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
5907 .parse()
5908 .unwrap();
5909
5910 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
5911 assert!(parse(&spec, &input(&["ex"])).is_err());
5912 }
5913
5914 #[test]
5915 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
5916 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5922 .parse()
5923 .unwrap();
5924
5925 parse(&spec, &input(&["ex"])).expect("the default fills the group");
5926 parse(&spec, &input(&["ex", "--url", "u"]))
5927 .expect("the default must not conflict with the flag the user typed");
5928 }
5929
5930 #[test]
5931 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
5932 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
5935 .parse()
5936 .unwrap();
5937
5938 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
5939 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
5940
5941 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
5943 assert!(err.to_string().contains("group input"), "{err}");
5944 }
5945
5946 #[test]
5947 fn a_group_reads_the_environment_as_given() {
5948 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5951 .parse()
5952 .unwrap();
5953
5954 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
5955 }
5956
5957 #[test]
5958 fn a_requirement_names_the_flag_that_is_missing() {
5959 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
5962 .parse()
5963 .unwrap();
5964
5965 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
5966 assert!(
5967 err.to_string().contains("format"),
5968 "the missing flag should be named: {err}"
5969 );
5970
5971 for words in [
5973 &["ex", "--out", "a.txt", "--format", "json"][..],
5974 &["ex", "--format", "json", "--out", "a.txt"][..],
5975 ] {
5976 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
5977 }
5978
5979 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
5982 }
5983
5984 #[test]
5985 fn a_value_activates_only_its_conditional_requirement() {
5986 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"
5987 .parse()
5988 .unwrap();
5989
5990 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
5991 .expect("an unrelated value requires nothing");
5992
5993 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
5994 assert!(key.to_string().contains("key"), "{key}");
5995 parse(
5996 &spec,
5997 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
5998 )
5999 .expect("the matching requirement is satisfied");
6000
6001 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6002 assert!(token.to_string().contains("token"), "{token}");
6003 }
6004
6005 #[test]
6006 fn conditional_requirements_read_explicit_env_but_not_defaults() {
6007 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"
6008 .parse()
6009 .unwrap();
6010 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6011 assert!(err.to_string().contains("key"), "{err}");
6012
6013 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"
6014 .parse()
6015 .unwrap();
6016 parse(&from_default, &input(&["ex"]))
6017 .expect("a default is not an explicit conditional value");
6018 }
6019
6020 #[test]
6021 fn command_line_values_override_env_for_conditional_requirements() {
6022 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6023 .parse()
6024 .unwrap();
6025
6026 parse_with_env(
6027 &spec,
6028 &["ex", "--config", "ordinary.toml"],
6029 &[("EX_CONFIG", "special.toml")],
6030 )
6031 .expect("the command-line value takes precedence over the environment");
6032 }
6033
6034 #[test]
6035 fn conditional_requirements_normalize_boolean_env_values() {
6036 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
6037 .parse()
6038 .unwrap();
6039
6040 for value in ["1", "true", "True", "TRUE"] {
6041 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
6042 assert!(err.to_string().contains("key"), "{value}: {err}");
6043 }
6044 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
6045 .expect("a false environment value does not activate a true condition");
6046 }
6047
6048 #[test]
6049 fn a_default_satisfies_a_requirement() {
6050 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
6054 .parse()
6055 .unwrap();
6056
6057 parse(&spec, &input(&["ex", "--out", "a.txt"]))
6058 .expect("a defaulted flag is not a missing one");
6059 }
6060
6061 #[test]
6062 fn a_present_flag_binds_a_conditional_default() {
6063 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6064 .parse()
6065 .unwrap();
6066
6067 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
6068 assert_eq!(
6069 with.as_env().get("usage_bin_names").map(String::as_str),
6070 Some("true")
6071 );
6072
6073 let without = parse(&spec, &input(&["ex"])).unwrap();
6074 assert!(
6075 !without.as_env().contains_key("usage_bin_names"),
6076 "IsPresent does nothing when the selector is absent"
6077 );
6078 }
6079
6080 #[test]
6081 fn an_equals_condition_binds_a_conditional_default() {
6082 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
6083 .parse()
6084 .unwrap();
6085
6086 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6087 assert_eq!(
6088 json.as_env().get("usage_style").map(String::as_str),
6089 Some("pretty")
6090 );
6091 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6092 assert!(!yaml.as_env().contains_key("usage_style"));
6093 }
6094
6095 #[test]
6096 fn an_equals_condition_reads_a_negated_flag() {
6097 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6098 .parse()
6099 .unwrap();
6100
6101 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6102 assert_eq!(
6103 off.as_env().get("usage_pretty").map(String::as_str),
6104 Some("true")
6105 );
6106 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6107 assert!(
6108 !on.as_env().contains_key("usage_pretty"),
6109 "--json is true, so when=false should miss"
6110 );
6111 }
6112
6113 #[test]
6114 fn the_first_matching_conditional_default_wins() {
6115 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"
6116 .parse()
6117 .unwrap();
6118
6119 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6120 assert_eq!(
6121 out.as_env().get("usage_style").map(String::as_str),
6122 Some("compact")
6123 );
6124 }
6125
6126 #[test]
6127 fn argv_and_env_suppress_a_conditional_default() {
6128 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6129 .parse()
6130 .unwrap();
6131
6132 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6133 assert_eq!(
6134 from_env.as_env().get("usage_bin_names").map(String::as_str),
6135 Some("false"),
6136 "the target's environment wins over default_if"
6137 );
6138 }
6139
6140 #[test]
6141 fn a_sibling_env_activates_a_conditional_default() {
6142 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6143 .parse()
6144 .unwrap();
6145
6146 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6147 assert_eq!(
6148 out.as_env().get("usage_bin_names").map(String::as_str),
6149 Some("true")
6150 );
6151 }
6152
6153 #[test]
6154 fn a_default_does_not_activate_a_conditional_default() {
6155 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6159 .parse()
6160 .unwrap();
6161
6162 let out = parse(&spec, &input(&["ex"])).unwrap();
6163 assert_eq!(
6164 out.as_env().get("usage_json").map(String::as_str),
6165 Some("true")
6166 );
6167 assert!(
6168 !out.as_env().contains_key("usage_pretty"),
6169 "a default is not an explicit value for default_if"
6170 );
6171 }
6172
6173 #[test]
6174 fn a_conditional_default_does_not_activate_requires_if() {
6175 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"
6176 .parse()
6177 .unwrap();
6178
6179 parse(&spec, &input(&["ex", "--json"]))
6180 .expect("a default_if value is not explicit for requires_if");
6181 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6182 }
6183
6184 #[test]
6185 fn a_conditional_default_satisfies_a_requirement() {
6186 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6187 .parse()
6188 .unwrap();
6189
6190 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6191 .expect("default_if fills the required flag");
6192 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6193 }
6194
6195 #[test]
6196 fn an_environment_value_satisfies_a_requirement() {
6197 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6198 .parse()
6199 .unwrap();
6200
6201 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6202 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6203 .expect("the environment supplies it");
6204 }
6205
6206 #[test]
6207 fn a_requirement_is_satisfied_by_a_short_form() {
6208 let spec: Spec =
6212 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6213 .parse()
6214 .unwrap();
6215
6216 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6217
6218 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6219 assert!(err.to_string().contains("key"), "{err}");
6220 }
6221
6222 #[test]
6223 fn conflicting_flags_are_rejected_in_either_order() {
6224 let spec: Spec =
6228 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6229 .parse()
6230 .unwrap();
6231
6232 for words in [
6233 &["ex", "--file", "a.txt", "--stdin"][..],
6234 &["ex", "--stdin", "--file", "a.txt"][..],
6235 ] {
6236 let err = parse(&spec, &input(words)).unwrap_err();
6237 assert!(
6238 err.to_string().contains("conflicts with --stdin"),
6239 "{words:?} should be refused: {err}"
6240 );
6241 }
6242
6243 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6245 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6246 }
6247
6248 #[test]
6249 fn unknown_flags_are_values_by_default() {
6250 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6253 .parse()
6254 .unwrap();
6255 let out = parse(
6256 &spec,
6257 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6258 )
6259 .unwrap();
6260 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6261 assert_eq!(out.args[rest].to_string(), "--wat x");
6262 }
6263
6264 #[test]
6265 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6266 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6267 .parse()
6268 .unwrap();
6269 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6270 .expect("a repeat is a correction by default");
6271 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6272 assert_eq!(out.flags[jobs].to_string(), "2");
6273 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6274 .expect("switches use the same default");
6275
6276 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6277 .parse()
6278 .unwrap();
6279 for words in [
6280 &["ex", "--jobs", "1", "--jobs", "2"][..],
6281 &["ex", "--verbose", "--verbose"][..],
6282 ] {
6283 let err = parse(&strict, &input(words)).unwrap_err();
6284 assert!(
6285 err.to_string().contains("cannot be used multiple times"),
6286 "{err}"
6287 );
6288 }
6289
6290 let reparsed: Spec = strict.to_string().parse().unwrap();
6291 assert!(!reparsed.cmd.args_override_self);
6292 }
6293
6294 #[test]
6295 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6296 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6297 .parse()
6298 .unwrap();
6299
6300 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6301 .expect("opposite forms override each other");
6302 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6303 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6304
6305 for words in [
6306 &["ex", "--color", "--color"][..],
6307 &["ex", "--no-color", "--no-color"][..],
6308 ] {
6309 let err = parse(&spec, &input(words)).unwrap_err();
6310 assert!(err.to_string().contains("cannot be used multiple times"));
6311 }
6312 }
6313
6314 #[test]
6315 fn strict_global_flags_may_repeat_across_command_levels() {
6316 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"
6317 .parse()
6318 .unwrap();
6319
6320 let out = parse(
6321 &spec,
6322 &input(&[
6323 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6324 ]),
6325 )
6326 .expect("an inherited global is allowed once at each command level");
6327 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6328 assert_eq!(out.flags[jobs].to_string(), "2");
6329
6330 for words in [
6331 &["ex", "--color", "--color", "run", "--no-color"][..],
6332 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6333 ] {
6334 let err = parse(&spec, &input(words)).unwrap_err();
6335 assert!(err.to_string().contains("cannot be used multiple times"));
6336 }
6337 }
6338
6339 #[test]
6340 fn a_subcommand_can_negate_only_its_parents_requirements() {
6341 let base = r#"name "ex"
6342bin "ex"
6343flag "--config" required=#true
6344flag "--mode" requires="--config"
6345flag "--other"
6346arg "<input>"
6347group "source" "--config" "--other" required=#true
6348cmd "run" { flag "--child" required=#true }
6349"#;
6350 let strict: Spec = base.parse().unwrap();
6351 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6352 let message = err.to_string();
6353 assert!(
6354 message.contains("input") || message.contains("config"),
6355 "{message}"
6356 );
6357
6358 let negated: Spec = base
6359 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6360 .parse()
6361 .unwrap();
6362 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6363 assert!(
6364 err.to_string().contains("child"),
6365 "the selected command keeps its own requirements: {err}"
6366 );
6367
6368 let mut child_optional = negated.clone();
6369 child_optional.cmd.subcommands["run"].flags[0].required = false;
6370 parse(&child_optional, &input(&["ex", "run"]))
6371 .expect("the child selection satisfies all parent requirements");
6372 parse(&child_optional, &input(&["ex", "--mode", "run"]))
6373 .expect("parent requires relationships are negated too");
6374 }
6375
6376 #[test]
6377 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6378 let spec: Spec = r#"name "ex"
6379bin "ex"
6380args_conflicts_with_subcommands #true
6381flag "--verbose"
6382cmd "run"
6383"#
6384 .parse()
6385 .unwrap();
6386
6387 parse(&spec, &input(&["ex", "run"]))
6388 .expect("the subcommand is valid without a parent argument");
6389 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6390 assert!(
6391 err.to_string().contains("cannot be used with arguments"),
6392 "{err}"
6393 );
6394 }
6395
6396 #[test]
6397 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6398 let base = r#"name "ex"
6399bin "ex"
6400flag "--values <value>..."
6401cmd "run"
6402"#;
6403 let plain: Spec = base.parse().unwrap();
6404 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6405 assert_eq!(out.cmd.name, "ex");
6406
6407 let precedence: Spec = base
6408 .replacen(
6409 "bin \"ex\"",
6410 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6411 1,
6412 )
6413 .parse()
6414 .unwrap();
6415 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6416 assert_eq!(out.cmd.name, "run");
6417 }
6418
6419 #[test]
6420 fn a_required_positional_can_follow_an_unfilled_optional_one() {
6421 let base = r#"name "ex"
6422bin "ex"
6423arg "[optional]"
6424arg "<required>"
6425"#;
6426 let plain: Spec = base.parse().unwrap();
6427 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6428 assert!(err.to_string().contains("required"), "{err}");
6429
6430 let enabled: Spec = base
6431 .replacen(
6432 "bin \"ex\"",
6433 "bin \"ex\"\nallow_missing_positional #true",
6434 1,
6435 )
6436 .parse()
6437 .unwrap();
6438 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6439 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6440 let value = &out
6441 .args
6442 .iter()
6443 .find(|(arg, _)| arg.name == "required")
6444 .unwrap()
6445 .1;
6446 assert!(matches!(value, ParseValue::String(value) if value == "value"));
6447 }
6448
6449 #[test]
6450 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6451 let spec: Spec =
6452 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6453 .parse()
6454 .unwrap();
6455 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6456 assert!(
6457 err.to_string().contains("--wat"),
6458 "the message should name the token: {err}"
6459 );
6460
6461 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6464 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6465 assert_eq!(out.args[rest].to_string(), "-1");
6466
6467 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6468 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6469 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6470 }
6471
6472 #[test]
6473 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6474 let spec: Spec = r#"
6475name "ex"
6476bin "ex"
6477unknown_flags "error"
6478flag "-0 --print0" global=#true
6479cmd "run" {
6480 flag "--force"
6481}
6482"#
6483 .parse()
6484 .unwrap();
6485 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6486 assert_eq!(out.cmd.name, "run");
6487 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6488 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6489 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6490 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6491 }
6492
6493 #[test]
6494 fn a_command_may_override_the_cli_wide_setting() {
6495 let spec: Spec = r#"
6497name "ex"
6498bin "ex"
6499unknown_flags "error"
6500cmd "exec" unknown_flags="value" {
6501 arg "[rest]..."
6502}
6503cmd "build" {
6504 arg "[rest]..."
6505}
6506"#
6507 .parse()
6508 .unwrap();
6509
6510 let out = parse(
6511 &spec,
6512 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6513 )
6514 .unwrap();
6515 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6516 assert_eq!(out.args[rest].to_string(), "--wat");
6517
6518 assert!(
6519 parse(
6520 &spec,
6521 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6522 )
6523 .is_err(),
6524 "a command that says nothing inherits the CLI's choice"
6525 );
6526 }
6527
6528 #[test]
6529 fn the_setting_survives_a_round_trip() {
6530 let spec: Spec =
6531 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
6532 .parse()
6533 .unwrap();
6534 let reparsed: Spec = spec.to_string().parse().unwrap();
6535 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
6536 assert_eq!(
6537 reparsed.cmd.subcommands["x"].unknown_flags,
6538 Some(UnknownFlags::Value)
6539 );
6540 }
6541
6542 #[test]
6543 fn test_default_subcommand() {
6544 let run_cmd = SpecCommand::builder()
6546 .name("run")
6547 .arg(SpecArg::builder().name("task").build())
6548 .build();
6549 let mut cmd = SpecCommand::builder().name("test").build();
6550 cmd.subcommands.insert("run".to_string(), run_cmd);
6551
6552 let spec = Spec {
6553 name: "test".to_string(),
6554 bin: "test".to_string(),
6555 cmd,
6556 default_subcommand: Some("run".to_string()),
6557 ..Default::default()
6558 };
6559
6560 let input = vec!["test".to_string(), "mytask".to_string()];
6562 let parsed = parse(&spec, &input).unwrap();
6563
6564 assert_eq!(parsed.cmds.len(), 2);
6566 assert_eq!(parsed.cmds[1].name, "run");
6567
6568 assert_eq!(parsed.args.len(), 1);
6570 let arg = parsed.args.keys().next().unwrap();
6571 assert_eq!(arg.name, "task");
6572 let value = parsed.args.values().next().unwrap();
6573 assert_eq!(value.to_string(), "mytask");
6574 }
6575
6576 #[test]
6577 fn test_default_subcommand_explicit_still_works() {
6578 let run_cmd = SpecCommand::builder()
6580 .name("run")
6581 .arg(SpecArg::builder().name("task").build())
6582 .build();
6583 let other_cmd = SpecCommand::builder()
6584 .name("other")
6585 .arg(SpecArg::builder().name("other_arg").build())
6586 .build();
6587 let mut cmd = SpecCommand::builder().name("test").build();
6588 cmd.subcommands.insert("run".to_string(), run_cmd);
6589 cmd.subcommands.insert("other".to_string(), other_cmd);
6590
6591 let spec = Spec {
6592 name: "test".to_string(),
6593 bin: "test".to_string(),
6594 cmd,
6595 default_subcommand: Some("run".to_string()),
6596 ..Default::default()
6597 };
6598
6599 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
6601 let parsed = parse(&spec, &input).unwrap();
6602
6603 assert_eq!(parsed.cmds.len(), 2);
6605 assert_eq!(parsed.cmds[1].name, "other");
6606 }
6607
6608 #[test]
6609 fn test_default_subcommand_applies_only_at_the_root() {
6610 let mut config_ls = SpecCommand::builder().name("ls").build();
6616 config_ls.args.push(SpecArg::builder().name("what").build());
6617 let mut config_cmd = SpecCommand::builder().name("config").build();
6618 config_cmd.subcommands.insert("ls".to_string(), config_ls);
6619
6620 let mut root_ls = SpecCommand::builder().name("ls").build();
6623 root_ls.args.push(SpecArg::builder().name("what").build());
6624 let mut cmd = SpecCommand::builder().name("ex").build();
6625 cmd.subcommands.insert("ls".to_string(), root_ls);
6626 cmd.subcommands.insert("config".to_string(), config_cmd);
6627
6628 let spec = Spec {
6629 name: "ex".to_string(),
6630 bin: "ex".to_string(),
6631 cmd,
6632 default_subcommand: Some("ls".to_string()),
6633 ..Default::default()
6634 };
6635
6636 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
6640 assert!(
6641 parse(&spec, &input).is_err(),
6642 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
6643 );
6644
6645 let input = vec!["ex".to_string(), "zzz".to_string()];
6647 let parsed = parse(&spec, &input).expect("the root's default applies");
6648 assert_eq!(
6649 parsed
6650 .cmds
6651 .iter()
6652 .map(|c| c.name.as_str())
6653 .collect::<Vec<_>>(),
6654 ["ex", "ls"]
6655 );
6656 assert_eq!(
6657 parsed.args.values().next().map(|v| v.to_string()),
6658 Some("zzz".to_string()),
6659 "and the word binds inside the command it reached"
6660 );
6661 }
6662
6663 #[test]
6664 fn test_default_subcommand_with_nested_subcommands() {
6665 let say_cmd = SpecCommand::builder()
6669 .name("say")
6670 .arg(SpecArg::builder().name("name").build())
6671 .build();
6672 let mut run_cmd = SpecCommand::builder().name("run").build();
6673 run_cmd.subcommands.insert("say".to_string(), say_cmd);
6674
6675 let mut cmd = SpecCommand::builder().name("test").build();
6676 cmd.subcommands.insert("run".to_string(), run_cmd);
6677
6678 let spec = Spec {
6679 name: "test".to_string(),
6680 bin: "test".to_string(),
6681 cmd,
6682 default_subcommand: Some("run".to_string()),
6683 ..Default::default()
6684 };
6685
6686 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
6688 let parsed = parse(&spec, &input).unwrap();
6689
6690 assert_eq!(parsed.cmds.len(), 3);
6692 assert_eq!(parsed.cmds[0].name, "test");
6693 assert_eq!(parsed.cmds[1].name, "run");
6694 assert_eq!(parsed.cmds[2].name, "say");
6695
6696 assert_eq!(parsed.args.len(), 1);
6698 let arg = parsed.args.keys().next().unwrap();
6699 assert_eq!(arg.name, "name");
6700 let value = parsed.args.values().next().unwrap();
6701 assert_eq!(value.to_string(), "hello");
6702 }
6703
6704 fn mounted_global_flag_spec() -> Spec {
6712 let task_cmd = SpecCommand::builder()
6713 .name("sample:run")
6714 .arg(
6715 SpecArg::builder()
6716 .name("profile")
6717 .choices(["alpha", "beta", "gamma"])
6718 .build(),
6719 )
6720 .build();
6721 let mut run_cmd = SpecCommand::builder()
6723 .name("run")
6724 .flag(
6725 SpecFlag::builder()
6726 .name("cd")
6727 .short('C')
6728 .long("cd")
6729 .arg(SpecArg::builder().name("dir").build())
6730 .global(false)
6731 .build(),
6732 )
6733 .build();
6734 run_cmd
6735 .subcommands
6736 .insert("sample:run".to_string(), task_cmd);
6737
6738 let mut cmd = SpecCommand::builder()
6739 .name("test")
6740 .flag(
6741 SpecFlag::builder()
6742 .name("cd")
6743 .short('C')
6744 .long("cd")
6745 .arg(SpecArg::builder().name("dir").build())
6746 .global(true)
6747 .build(),
6748 )
6749 .build();
6750 cmd.subcommands.insert("run".to_string(), run_cmd);
6751
6752 Spec {
6753 name: "test".to_string(),
6754 bin: "test".to_string(),
6755 cmd,
6756 ..Default::default()
6757 }
6758 }
6759
6760 #[test]
6761 fn test_prefix_global_flag_does_not_pollute_choices() {
6762 let spec = mounted_global_flag_spec();
6769
6770 for words in [
6773 &["test", "-C", "/tmp", "run", "sample:run"][..],
6774 &["test", "--cd=/tmp", "run", "sample:run"][..],
6776 ] {
6777 let parsed = parse_partial(&spec, &input(words)).unwrap();
6778 assert_eq!(
6779 parsed
6780 .cmds
6781 .iter()
6782 .map(|c| c.name.as_str())
6783 .collect::<Vec<_>>(),
6784 vec!["test", "run", "sample:run"],
6785 );
6786 assert!(
6788 parsed.args.is_empty(),
6789 "args should be empty, got {:?}",
6790 parsed.args
6791 );
6792
6793 let cd = parsed
6796 .available_flags
6797 .get("--cd")
6798 .expect("--cd should remain available after descending into the subcommand");
6799 assert!(cd.global, "--cd must stay global after descent");
6800 assert!(
6801 parsed.available_flags.get("-C").is_some_and(|f| f.global),
6802 "-C must stay global after descent",
6803 );
6804
6805 assert_eq!(
6809 parsed.as_env().get("usage_cd").map(String::as_str),
6810 Some("/tmp"),
6811 "global flag value must survive in as_env(), got {:?}",
6812 parsed.as_env(),
6813 );
6814 }
6815
6816 let parsed = parse_partial(
6818 &spec,
6819 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
6820 )
6821 .unwrap();
6822 assert_eq!(parsed.args.len(), 1);
6823 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6824
6825 assert_parse_err(
6827 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
6828 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6829 );
6830 }
6831
6832 fn mounted_orphan_short_spec() -> Spec {
6837 let task_cmd = SpecCommand::builder()
6838 .name("sample:run")
6839 .arg(
6840 SpecArg::builder()
6841 .name("profile")
6842 .choices(["alpha", "beta", "gamma"])
6843 .build(),
6844 )
6845 .build();
6846 let mut run_cmd = SpecCommand::builder()
6849 .name("run")
6850 .flag(
6851 SpecFlag::builder()
6852 .name("raw")
6853 .short('r')
6854 .long("raw")
6855 .global(false)
6856 .build(),
6857 )
6858 .flag(
6859 SpecFlag::builder()
6860 .name("force")
6861 .short('f')
6862 .long("force")
6863 .global(false)
6864 .build(),
6865 )
6866 .build();
6867 run_cmd
6868 .subcommands
6869 .insert("sample:run".to_string(), task_cmd);
6870
6871 let mut cmd = SpecCommand::builder()
6873 .name("test")
6874 .flag(
6875 SpecFlag::builder()
6876 .name("raw")
6877 .long("raw")
6878 .global(true)
6879 .build(),
6880 )
6881 .build();
6882 cmd.subcommands.insert("run".to_string(), run_cmd);
6883
6884 Spec {
6885 name: "test".to_string(),
6886 bin: "test".to_string(),
6887 cmd,
6888 ..Default::default()
6889 }
6890 }
6891
6892 #[test]
6893 fn test_orphan_short_alias_survives_merge() {
6894 let spec = mounted_orphan_short_spec();
6900
6901 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
6902 assert_eq!(
6903 parsed
6904 .cmds
6905 .iter()
6906 .map(|c| c.name.as_str())
6907 .collect::<Vec<_>>(),
6908 vec!["test", "run", "sample:run"],
6909 );
6910
6911 assert!(
6914 parsed.available_flags.get("-r").is_some_and(|f| f.global),
6915 "-r must be merged onto the inherited global flag and stay global after descent",
6916 );
6917 assert!(
6918 parsed
6919 .available_flags
6920 .get("--raw")
6921 .is_some_and(|f| f.global),
6922 "--raw must stay global after descent",
6923 );
6924
6925 assert!(
6927 parsed.args.is_empty(),
6928 "args should be empty, got {:?}",
6929 parsed.args
6930 );
6931
6932 assert_eq!(
6934 parsed.as_env().get("usage_raw").map(String::as_str),
6935 Some("true"),
6936 "merged short's value must survive in as_env(), got {:?}",
6937 parsed.as_env(),
6938 );
6939
6940 assert!(
6943 !parsed.available_flags.contains_key("-f"),
6944 "purely-local -f must not be promoted onto a global",
6945 );
6946 assert!(
6947 !parsed.available_flags.contains_key("--force"),
6948 "purely-local --force must not be promoted onto a global",
6949 );
6950
6951 let parsed =
6953 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
6954 assert_eq!(parsed.args.len(), 1);
6955 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6956
6957 assert_parse_err(
6959 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
6960 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6961 );
6962 }
6963
6964 #[test]
6965 fn test_orphan_short_does_not_clobber_unrelated_global() {
6966 let run_cmd = SpecCommand::builder()
6971 .name("run")
6972 .flag(
6973 SpecFlag::builder()
6974 .name("raw")
6975 .short('r')
6976 .long("raw")
6977 .global(false)
6978 .build(),
6979 )
6980 .build();
6981 let mut cmd = SpecCommand::builder()
6982 .name("test")
6983 .flag(
6984 SpecFlag::builder()
6985 .name("raw")
6986 .long("raw")
6987 .global(true)
6988 .build(),
6989 )
6990 .flag(
6991 SpecFlag::builder()
6992 .name("restrict")
6993 .short('r')
6994 .long("restrict")
6995 .global(true)
6996 .build(),
6997 )
6998 .build();
6999 cmd.subcommands.insert("run".to_string(), run_cmd);
7000 let spec = Spec {
7001 name: "test".to_string(),
7002 bin: "test".to_string(),
7003 cmd,
7004 ..Default::default()
7005 };
7006
7007 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7008 assert_eq!(
7010 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
7011 Some("restrict"),
7012 "-r must remain owned by the unrelated global it already belonged to",
7013 );
7014 assert!(parsed
7016 .available_flags
7017 .get("--raw")
7018 .is_some_and(|f| f.global));
7019 assert!(parsed
7020 .available_flags
7021 .get("--restrict")
7022 .is_some_and(|f| f.global));
7023 }
7024
7025 #[test]
7026 fn test_redeclared_global_aliases_share_one_flag() {
7027 let spec = r#"
7034flag "-y --yes" global=#true effect="write"
7035cmd "run" {
7036 flag "-y --yes --assume-yes"
7037}
7038"#
7039 .parse::<Spec>()
7040 .unwrap();
7041
7042 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7043
7044 for key in ["-y", "--yes", "--assume-yes"] {
7045 let flag = parsed
7046 .available_flags
7047 .get(key)
7048 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7049 assert!(flag.global, "{key} must stay global after the descent");
7050 assert_eq!(
7051 flag.long,
7052 vec!["yes".to_string(), "assume-yes".to_string()],
7053 "{key} must resolve to the flag carrying every alias",
7054 );
7055 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
7056 }
7057
7058 assert_eq!(
7060 unique_flags(parsed.available_flags.values()).count(),
7061 1,
7062 "all aliases must point at one flag object, got {:?}",
7063 parsed.available_flags,
7064 );
7065
7066 assert_eq!(
7068 parsed.available_flags["-y"].effect,
7069 Some(crate::SpecCommandEffect::Write),
7070 );
7071 }
7072
7073 #[test]
7074 fn test_redeclared_global_keeps_hidden_alias_metadata() {
7075 let spec = r#"
7076flag "--yes" global=#true {
7077 alias "-q" "--quietly" hide=#true
7078}
7079cmd "run" {
7080 flag "--yes --assume-yes" {
7081 alias "-s" "--secret" hide=#true
7082 }
7083}
7084"#
7085 .parse::<Spec>()
7086 .unwrap();
7087
7088 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7089 let merged = &parsed.available_flags["--yes"];
7090 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7091 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7092 for key in ["-q", "-s", "--quietly", "--secret"] {
7093 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7094 }
7095 }
7096
7097 #[test]
7098 fn test_redeclared_global_can_promote_hidden_aliases() {
7099 let spec = r#"
7100flag "--yes" global=#true {
7101 alias "-q" "--quietly" hide=#true
7102}
7103cmd "run" {
7104 flag "-q --yes --quietly"
7105}
7106"#
7107 .parse::<Spec>()
7108 .unwrap();
7109
7110 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7111 let merged = &parsed.available_flags["--yes"];
7112 assert!(merged.hidden_short_aliases.is_empty());
7113 assert!(merged.hidden_aliases.is_empty());
7114 for key in ["-q", "--quietly"] {
7115 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7116 }
7117 }
7118
7119 #[test]
7120 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7121 let spec = r#"
7127flag "-y --yes --confirm" global=#true
7128cmd "run" {
7129 flag "--yes --assume-yes"
7130}
7131"#
7132 .parse::<Spec>()
7133 .unwrap();
7134
7135 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7136
7137 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7138 let flag = parsed
7139 .available_flags
7140 .get(key)
7141 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7142 assert!(flag.global, "{key} must stay global after the descent");
7143 assert_eq!(
7144 flag.long,
7145 vec![
7146 "yes".to_string(),
7147 "confirm".to_string(),
7148 "assume-yes".to_string()
7149 ],
7150 "{key} must resolve to the flag carrying every alias",
7151 );
7152 }
7153
7154 assert_eq!(
7155 unique_flags(parsed.available_flags.values()).count(),
7156 1,
7157 "all aliases must point at one flag object, got {:?}",
7158 parsed.available_flags,
7159 );
7160 }
7161
7162 fn mounted_task_flag_spec() -> Spec {
7169 let mut task_cmd = SpecCommand::builder()
7170 .name("mytask")
7171 .flag(
7172 SpecFlag::builder()
7173 .name("env")
7174 .long("env")
7175 .arg(
7176 SpecArg::builder()
7177 .name("name")
7178 .choices(["dev", "stage", "prod"])
7179 .build(),
7180 )
7181 .global(false)
7182 .build(),
7183 )
7184 .flag(
7185 SpecFlag::builder()
7186 .name("bump")
7187 .long("bump")
7188 .arg(
7189 SpecArg::builder()
7190 .name("type")
7191 .choices(["auto", "major"])
7192 .build(),
7193 )
7194 .global(false)
7195 .build(),
7196 )
7197 .build();
7198 task_cmd.mounted = true;
7199
7200 let mut run_cmd = SpecCommand::builder()
7201 .name("run")
7202 .flag(
7203 SpecFlag::builder()
7204 .name("force")
7205 .short('f')
7206 .long("force")
7207 .global(false)
7208 .build(),
7209 )
7210 .build();
7211 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7212
7213 let mut cmd = SpecCommand::builder()
7214 .name("test")
7215 .flag(
7216 SpecFlag::builder()
7217 .name("env")
7218 .short('E')
7219 .long("env")
7220 .arg(SpecArg::builder().name("ENV").build())
7221 .global(true)
7222 .build(),
7223 )
7224 .flag(
7225 SpecFlag::builder()
7226 .name("silent")
7227 .long("silent")
7228 .global(true)
7229 .build(),
7230 )
7231 .build();
7232 cmd.subcommands.insert("run".to_string(), run_cmd);
7233
7234 Spec {
7235 name: "test".to_string(),
7236 bin: "test".to_string(),
7237 cmd,
7238 ..Default::default()
7239 }
7240 }
7241
7242 #[test]
7243 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7244 let deep = SpecCommand::builder().name("deep").build();
7250 let mut sub = SpecCommand::builder()
7251 .name("sub")
7252 .flag(
7254 SpecFlag::builder()
7255 .name("cd")
7256 .short('C')
7257 .long("cd")
7258 .arg(SpecArg::builder().name("dir").build())
7259 .global(false)
7260 .build(),
7261 )
7262 .build();
7263 sub.subcommands.insert("deep".to_string(), deep);
7264 let mut task = SpecCommand::builder()
7265 .name("task")
7266 .flag(
7267 SpecFlag::builder()
7268 .name("cd")
7269 .short('C')
7270 .long("cd")
7271 .arg(SpecArg::builder().name("dir").build())
7272 .global(true)
7273 .build(),
7274 )
7275 .build();
7276 task.subcommands.insert("sub".to_string(), sub);
7277 task.mark_mounted();
7278
7279 let mut run_cmd = SpecCommand::builder().name("run").build();
7280 run_cmd.subcommands.insert("task".to_string(), task);
7281 let mut cmd = SpecCommand::builder().name("test").build();
7282 cmd.subcommands.insert("run".to_string(), run_cmd);
7283 let spec = Spec {
7284 name: "test".to_string(),
7285 bin: "test".to_string(),
7286 cmd,
7287 ..Default::default()
7288 };
7289
7290 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7291 assert!(
7292 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7293 "the mounted program's own global must survive descents inside the mounted tree",
7294 );
7295 assert!(
7296 parsed.completion_flags().contains_key("--cd"),
7297 "and must still be offered there: it belongs to the mounted program",
7298 );
7299 assert!(
7300 parsed.completion_flags().contains_key("-C"),
7301 "including the short the nested command re-declared",
7302 );
7303 }
7304
7305 #[test]
7306 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7307 let mut task = SpecCommand::builder()
7312 .name("task")
7313 .flag(
7314 SpecFlag::builder()
7315 .name("bump")
7316 .long("bump")
7317 .global(false)
7318 .build(),
7319 )
7320 .build();
7321 task.mark_mounted();
7322
7323 let mut run_cmd = SpecCommand::builder().name("run").build();
7324 run_cmd.subcommands.insert("task".to_string(), task);
7325 run_cmd.flags = vec![
7327 SpecFlag::builder()
7328 .name("tglobal")
7329 .long("tglobal")
7330 .global(true)
7331 .build(),
7332 SpecFlag::builder()
7333 .name("tlocal")
7334 .long("tlocal")
7335 .global(false)
7336 .build(),
7337 ];
7338 run_cmd.flags_from_mount = true;
7339
7340 let mut cmd = SpecCommand::builder()
7341 .name("test")
7342 .flag(
7343 SpecFlag::builder()
7344 .name("silent")
7345 .long("silent")
7346 .global(true)
7347 .build(),
7348 )
7349 .build();
7350 cmd.subcommands.insert("run".to_string(), run_cmd);
7351 let spec = Spec {
7352 name: "test".to_string(),
7353 bin: "test".to_string(),
7354 cmd,
7355 ..Default::default()
7356 };
7357
7358 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7359 assert_eq!(
7360 parsed.completion_flags().keys().collect::<Vec<_>>(),
7361 vec!["--bump", "--tglobal"],
7362 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7363 `--silent` does not, and the mount's non-global root flag is not inherited",
7364 );
7365 }
7366
7367 #[test]
7368 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7369 let spec = mounted_task_flag_spec();
7374 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7375
7376 assert!(parsed.available_flags.contains_key("--silent"));
7378 assert!(parsed.available_flags.contains_key("-E"));
7379 assert_eq!(
7381 parsed.completion_flags().keys().collect::<Vec<_>>(),
7382 vec!["--bump", "--env"],
7383 "only the mounted command's own flags may be offered",
7384 );
7385
7386 assert!(!parsed.available_flags.contains_key("--force"));
7388 }
7389
7390 #[test]
7391 fn test_mounted_cmd_flag_wins_over_inherited_global() {
7392 let spec = mounted_task_flag_spec();
7395 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7396
7397 let awaiting = parsed
7398 .flag_awaiting_value
7399 .first()
7400 .expect("--env should await a value");
7401 assert_eq!(
7402 awaiting
7403 .arg
7404 .as_ref()
7405 .and_then(|a| a.choices.as_ref())
7406 .map(|c| c.choices.clone()),
7407 Some(vec![
7408 "dev".to_string(),
7409 "stage".to_string(),
7410 "prod".to_string()
7411 ]),
7412 "the mounted command's own --env must win over the inherited global",
7413 );
7414
7415 let parsed =
7418 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7419 assert!(
7420 parsed.args.is_empty(),
7421 "prefix global tokens must not be consumed as positionals, got {:?}",
7422 parsed.args
7423 );
7424 assert_eq!(
7425 parsed.as_env().get("usage_env").map(String::as_str),
7426 Some("anything"),
7427 );
7428 }
7429
7430 #[test]
7431 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7432 let spec = mounted_task_flag_spec();
7437 let parsed = parse_partial(
7438 &spec,
7439 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7440 )
7441 .unwrap();
7442 assert!(
7443 parsed.errors.is_empty(),
7444 "prefix global value must not be validated against the mounted flag: {:?}",
7445 parsed
7446 .errors
7447 .iter()
7448 .map(|e| e.to_string())
7449 .collect::<Vec<_>>(),
7450 );
7451 assert_eq!(
7452 parsed.as_env().get("usage_env").map(String::as_str),
7453 Some("not-a-task-choice"),
7454 );
7455
7456 let parsed = parse_partial(
7458 &spec,
7459 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7460 )
7461 .unwrap();
7462 assert!(parsed.errors.is_empty());
7463 assert_eq!(
7464 parsed.as_env().get("usage_env").map(String::as_str),
7465 Some("not-a-task-choice"),
7466 );
7467
7468 let parsed = parse_partial(
7471 &spec,
7472 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7473 )
7474 .unwrap();
7475 let awaiting = parsed
7476 .flag_awaiting_value
7477 .first()
7478 .expect("--env should await a value");
7479 assert_eq!(
7480 awaiting
7481 .arg
7482 .as_ref()
7483 .and_then(|a| a.choices.as_ref())
7484 .map(|c| c.choices.clone()),
7485 Some(vec![
7486 "dev".to_string(),
7487 "stage".to_string(),
7488 "prod".to_string()
7489 ]),
7490 "the mounted command's --env must own the name after the mounted command",
7491 );
7492 }
7493
7494 #[test]
7495 fn test_non_global_flag_does_not_hide_subcommand() {
7496 let spec = mounted_task_flag_spec();
7500
7501 for words in [
7502 &["test", "run", "--force", "mytask"][..],
7504 &["test", "run", "-f", "mytask"][..],
7505 &["test", "-E", "prod", "run", "--force", "mytask"][..],
7507 ] {
7508 let parsed = parse_partial(&spec, &input(words)).unwrap();
7509 assert_eq!(
7510 parsed
7511 .cmds
7512 .iter()
7513 .map(|c| c.name.as_str())
7514 .collect::<Vec<_>>(),
7515 vec!["test", "run", "mytask"],
7516 "{words:?} should descend into the mounted command",
7517 );
7518 assert!(
7519 parsed.args.is_empty(),
7520 "{words:?} should not consume a positional, got {:?}",
7521 parsed.args,
7522 );
7523 assert_eq!(
7524 parsed.as_env().get("usage_force").map(String::as_str),
7525 Some("true"),
7526 "the non-global flag must still be recorded for {words:?}",
7527 );
7528 }
7529
7530 let mut run_cmd = SpecCommand::builder()
7533 .name("run")
7534 .flag(
7535 SpecFlag::builder()
7536 .name("output")
7537 .short('o')
7538 .long("output")
7539 .arg(SpecArg::builder().name("mode").build())
7540 .global(false)
7541 .build(),
7542 )
7543 .build();
7544 run_cmd.subcommands.insert(
7545 "task".to_string(),
7546 SpecCommand::builder().name("task").build(),
7547 );
7548 let mut cmd = SpecCommand::builder().name("test").build();
7549 cmd.subcommands.insert("run".to_string(), run_cmd);
7550 let spec = Spec {
7551 name: "test".to_string(),
7552 bin: "test".to_string(),
7553 cmd,
7554 ..Default::default()
7555 };
7556
7557 let parsed =
7558 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
7559 assert_eq!(
7560 parsed
7561 .cmds
7562 .iter()
7563 .map(|c| c.name.as_str())
7564 .collect::<Vec<_>>(),
7565 vec!["test", "run", "task"],
7566 );
7567 assert_eq!(
7568 parsed.as_env().get("usage_output").map(String::as_str),
7569 Some("quiet"),
7570 );
7571
7572 assert_parse_err(
7575 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
7576 "unexpected word: --nope",
7577 );
7578 }
7579
7580 #[test]
7581 fn test_non_mounted_subcommand_offers_inherited_globals() {
7582 let mut run_cmd = SpecCommand::builder().name("run").build();
7585 run_cmd.subcommands.insert(
7586 "nested".to_string(),
7587 SpecCommand::builder().name("nested").build(),
7588 );
7589 let mut cmd = SpecCommand::builder()
7590 .name("test")
7591 .flag(
7592 SpecFlag::builder()
7593 .name("silent")
7594 .long("silent")
7595 .global(true)
7596 .build(),
7597 )
7598 .build();
7599 cmd.subcommands.insert("run".to_string(), run_cmd);
7600 let spec = Spec {
7601 name: "test".to_string(),
7602 bin: "test".to_string(),
7603 cmd,
7604 ..Default::default()
7605 };
7606
7607 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
7608 assert_eq!(
7609 parsed.completion_flags().keys().collect::<Vec<_>>(),
7610 parsed.available_flags.keys().collect::<Vec<_>>(),
7611 );
7612 assert!(parsed.completion_flags().contains_key("--silent"));
7613 }
7614
7615 #[test]
7616 fn test_subcommand_alias_collision_keeps_last_owner() {
7617 let run_cmd = SpecCommand::builder()
7621 .name("run")
7622 .flag(
7623 SpecFlag::builder()
7624 .name("alpha")
7625 .short('x')
7626 .long("alpha")
7627 .global(false)
7628 .build(),
7629 )
7630 .flag(
7631 SpecFlag::builder()
7632 .name("beta")
7633 .short('x')
7634 .long("beta")
7635 .global(false)
7636 .build(),
7637 )
7638 .build();
7639 let mut cmd = SpecCommand::builder().name("test").build();
7640 cmd.subcommands.insert("run".to_string(), run_cmd);
7641 let spec = Spec {
7642 name: "test".to_string(),
7643 bin: "test".to_string(),
7644 cmd,
7645 ..Default::default()
7646 };
7647
7648 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7649 assert_eq!(
7651 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
7652 Some("beta"),
7653 "the last-declared flag must keep a shared short alias",
7654 );
7655 assert_eq!(
7657 parsed
7658 .available_flags
7659 .get("--alpha")
7660 .map(|f| f.name.as_str()),
7661 Some("alpha"),
7662 );
7663 assert_eq!(
7664 parsed
7665 .available_flags
7666 .get("--beta")
7667 .map(|f| f.name.as_str()),
7668 Some("beta"),
7669 );
7670 }
7671
7672 #[test]
7673 fn test_default_subcommand_same_name_child() {
7674 let run_task = SpecCommand::builder()
7678 .name("run")
7679 .arg(SpecArg::builder().name("args").build())
7680 .build();
7681 let mut run_cmd = SpecCommand::builder().name("run").build();
7682 run_cmd.subcommands.insert("run".to_string(), run_task);
7683
7684 let mut cmd = SpecCommand::builder().name("test").build();
7685 cmd.subcommands.insert("run".to_string(), run_cmd);
7686
7687 let spec = Spec {
7688 name: "test".to_string(),
7689 bin: "test".to_string(),
7690 cmd,
7691 default_subcommand: Some("run".to_string()),
7692 ..Default::default()
7693 };
7694
7695 let input = vec!["test".to_string(), "run".to_string()];
7697 let parsed = parse(&spec, &input).unwrap();
7698
7699 assert_eq!(parsed.cmds.len(), 2);
7701 assert_eq!(parsed.cmds[0].name, "test");
7702 assert_eq!(parsed.cmds[1].name, "run");
7703
7704 let input = vec![
7706 "test".to_string(),
7707 "run".to_string(),
7708 "run".to_string(),
7709 "hello".to_string(),
7710 ];
7711 let parsed = parse(&spec, &input).unwrap();
7712
7713 assert_eq!(parsed.cmds.len(), 3);
7714 assert_eq!(parsed.cmds[0].name, "test");
7715 assert_eq!(parsed.cmds[1].name, "run");
7716 assert_eq!(parsed.cmds[2].name, "run");
7717 assert_eq!(parsed.args.len(), 1);
7718 let value = parsed.args.values().next().unwrap();
7719 assert_eq!(value.to_string(), "hello");
7720
7721 let mut run_cmd = SpecCommand::builder()
7725 .name("run")
7726 .arg(SpecArg::builder().name("task").build())
7727 .build();
7728 let run_task = SpecCommand::builder().name("run").build();
7729 run_cmd.subcommands.insert("run".to_string(), run_task);
7730
7731 let mut cmd = SpecCommand::builder().name("test").build();
7732 cmd.subcommands.insert("run".to_string(), run_cmd);
7733
7734 let spec = Spec {
7735 name: "test".to_string(),
7736 bin: "test".to_string(),
7737 cmd,
7738 default_subcommand: Some("run".to_string()),
7739 ..Default::default()
7740 };
7741
7742 let input = vec!["test".to_string(), "other".to_string()];
7743 let parsed = parse(&spec, &input).unwrap();
7744
7745 assert_eq!(parsed.cmds.len(), 2);
7748 assert_eq!(parsed.cmds[0].name, "test");
7749 assert_eq!(parsed.cmds[1].name, "run");
7750
7751 assert_eq!(parsed.args.len(), 1);
7753 let value = parsed.args.values().next().unwrap();
7754 assert_eq!(value.to_string(), "other");
7755 }
7756
7757 #[test]
7758 fn test_restart_token() {
7759 let run_cmd = SpecCommand::builder()
7761 .name("run")
7762 .arg(SpecArg::builder().name("task").build())
7763 .restart_token(":::".to_string())
7764 .build();
7765 let mut cmd = SpecCommand::builder().name("test").build();
7766 cmd.subcommands.insert("run".to_string(), run_cmd);
7767
7768 let spec = Spec {
7769 name: "test".to_string(),
7770 bin: "test".to_string(),
7771 cmd,
7772 ..Default::default()
7773 };
7774
7775 let input = vec![
7777 "test".to_string(),
7778 "run".to_string(),
7779 "task1".to_string(),
7780 ":::".to_string(),
7781 "task2".to_string(),
7782 ];
7783 let parsed = parse(&spec, &input).unwrap();
7784
7785 assert_eq!(parsed.args.len(), 1);
7787 let value = parsed.args.values().next().unwrap();
7788 assert_eq!(value.to_string(), "task2");
7789 }
7790
7791 #[test]
7792 fn test_restart_token_multiple() {
7793 let run_cmd = SpecCommand::builder()
7795 .name("run")
7796 .arg(SpecArg::builder().name("task").build())
7797 .restart_token(":::".to_string())
7798 .build();
7799 let mut cmd = SpecCommand::builder().name("test").build();
7800 cmd.subcommands.insert("run".to_string(), run_cmd);
7801
7802 let spec = Spec {
7803 name: "test".to_string(),
7804 bin: "test".to_string(),
7805 cmd,
7806 ..Default::default()
7807 };
7808
7809 let input = vec![
7811 "test".to_string(),
7812 "run".to_string(),
7813 "task1".to_string(),
7814 ":::".to_string(),
7815 "task2".to_string(),
7816 ":::".to_string(),
7817 "task3".to_string(),
7818 ];
7819 let parsed = parse(&spec, &input).unwrap();
7820
7821 assert_eq!(parsed.args.len(), 1);
7823 let value = parsed.args.values().next().unwrap();
7824 assert_eq!(value.to_string(), "task3");
7825 }
7826
7827 #[test]
7828 fn test_restart_token_clears_flag_awaiting_value() {
7829 let run_cmd = SpecCommand::builder()
7831 .name("run")
7832 .arg(SpecArg::builder().name("task").build())
7833 .flag(
7834 SpecFlag::builder()
7835 .name("jobs")
7836 .long("jobs")
7837 .arg(SpecArg::builder().name("count").build())
7838 .build(),
7839 )
7840 .restart_token(":::".to_string())
7841 .build();
7842 let mut cmd = SpecCommand::builder().name("test").build();
7843 cmd.subcommands.insert("run".to_string(), run_cmd);
7844
7845 let spec = Spec {
7846 name: "test".to_string(),
7847 bin: "test".to_string(),
7848 cmd,
7849 ..Default::default()
7850 };
7851
7852 let input = vec![
7854 "test".to_string(),
7855 "run".to_string(),
7856 "task1".to_string(),
7857 "--jobs".to_string(),
7858 ":::".to_string(),
7859 "task2".to_string(),
7860 ];
7861 let parsed = parse(&spec, &input).unwrap();
7862
7863 assert_eq!(parsed.args.len(), 1);
7865 let value = parsed.args.values().next().unwrap();
7866 assert_eq!(value.to_string(), "task2");
7867 assert!(parsed.flag_awaiting_value.is_empty());
7869 }
7870
7871 #[test]
7872 fn test_restart_token_resets_double_dash() {
7873 let run_cmd = SpecCommand::builder()
7875 .name("run")
7876 .arg(SpecArg::builder().name("task").build())
7877 .arg(SpecArg::builder().name("extra_args").var(true).build())
7878 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
7879 .restart_token(":::".to_string())
7880 .build();
7881 let mut cmd = SpecCommand::builder().name("test").build();
7882 cmd.subcommands.insert("run".to_string(), run_cmd);
7883
7884 let spec = Spec {
7885 name: "test".to_string(),
7886 bin: "test".to_string(),
7887 cmd,
7888 ..Default::default()
7889 };
7890
7891 let input = vec![
7893 "test".to_string(),
7894 "run".to_string(),
7895 "task1".to_string(),
7896 "--".to_string(),
7897 "extra".to_string(),
7898 ":::".to_string(),
7899 "--verbose".to_string(),
7900 "task2".to_string(),
7901 ];
7902 let parsed = parse(&spec, &input).unwrap();
7903
7904 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
7906 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7908 let value = parsed.args.get(task_arg).unwrap();
7909 assert_eq!(value.to_string(), "task2");
7910 }
7911
7912 #[test]
7913 fn test_double_dashes_without_preserve() {
7914 let run_cmd = SpecCommand::builder()
7919 .name("run")
7920 .arg(SpecArg::builder().name("args").var(true).build())
7921 .build();
7922 let mut cmd = SpecCommand::builder().name("test").build();
7923 cmd.subcommands.insert("run".to_string(), run_cmd);
7924
7925 let spec = Spec {
7926 name: "test".to_string(),
7927 bin: "test".to_string(),
7928 cmd,
7929 ..Default::default()
7930 };
7931
7932 let input = vec![
7934 "test".to_string(),
7935 "run".to_string(),
7936 "arg1".to_string(),
7937 "--".to_string(),
7938 "arg2".to_string(),
7939 "--".to_string(),
7940 "arg3".to_string(),
7941 ];
7942 let parsed = parse(&spec, &input).unwrap();
7943
7944 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7945 let value = parsed.args.get(args_arg).unwrap();
7946 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
7947 }
7948
7949 #[test]
7950 fn test_double_dashes_with_preserve() {
7951 let run_cmd = SpecCommand::builder()
7953 .name("run")
7954 .arg(
7955 SpecArg::builder()
7956 .name("args")
7957 .var(true)
7958 .double_dash(SpecDoubleDashChoices::Preserve)
7959 .build(),
7960 )
7961 .build();
7962 let mut cmd = SpecCommand::builder().name("test").build();
7963 cmd.subcommands.insert("run".to_string(), run_cmd);
7964
7965 let spec = Spec {
7966 name: "test".to_string(),
7967 bin: "test".to_string(),
7968 cmd,
7969 ..Default::default()
7970 };
7971
7972 let input = vec![
7974 "test".to_string(),
7975 "run".to_string(),
7976 "arg1".to_string(),
7977 "--".to_string(),
7978 "arg2".to_string(),
7979 "--".to_string(),
7980 "arg3".to_string(),
7981 ];
7982 let parsed = parse(&spec, &input).unwrap();
7983
7984 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7985 let value = parsed.args.get(args_arg).unwrap();
7986 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
7987 }
7988
7989 #[test]
7990 fn test_double_dashes_with_preserve_only_dashes() {
7991 let run_cmd = SpecCommand::builder()
7994 .name("run")
7995 .arg(
7996 SpecArg::builder()
7997 .name("args")
7998 .var(true)
7999 .double_dash(SpecDoubleDashChoices::Preserve)
8000 .build(),
8001 )
8002 .build();
8003 let mut cmd = SpecCommand::builder().name("test").build();
8004 cmd.subcommands.insert("run".to_string(), run_cmd);
8005
8006 let spec = Spec {
8007 name: "test".to_string(),
8008 bin: "test".to_string(),
8009 cmd,
8010 ..Default::default()
8011 };
8012
8013 let input = vec![
8015 "test".to_string(),
8016 "run".to_string(),
8017 "--".to_string(),
8018 "--".to_string(),
8019 ];
8020 let parsed = parse(&spec, &input).unwrap();
8021
8022 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8023 let value = parsed.args.get(args_arg).unwrap();
8024 assert_eq!(value.to_string(), "-- --");
8025 }
8026
8027 #[test]
8028 fn test_double_dashes_with_preserve_multiple_args() {
8029 let run_cmd = SpecCommand::builder()
8031 .name("run")
8032 .arg(SpecArg::builder().name("task").build())
8033 .arg(
8034 SpecArg::builder()
8035 .name("extra_args")
8036 .var(true)
8037 .double_dash(SpecDoubleDashChoices::Preserve)
8038 .build(),
8039 )
8040 .build();
8041 let mut cmd = SpecCommand::builder().name("test").build();
8042 cmd.subcommands.insert("run".to_string(), run_cmd);
8043
8044 let spec = Spec {
8045 name: "test".to_string(),
8046 bin: "test".to_string(),
8047 cmd,
8048 ..Default::default()
8049 };
8050
8051 let input = vec![
8054 "test".to_string(),
8055 "run".to_string(),
8056 "task1".to_string(),
8057 "--".to_string(),
8058 "arg1".to_string(),
8059 "--".to_string(),
8060 "--foo".to_string(),
8061 ];
8062 let parsed = parse(&spec, &input).unwrap();
8063
8064 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8065 let task_value = parsed.args.get(task_arg).unwrap();
8066 assert_eq!(task_value.to_string(), "task1");
8067
8068 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
8069 let extra_value = parsed.args.get(extra_arg).unwrap();
8070 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
8071 }
8072
8073 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
8074 let cmd = SpecCommand::builder().name("test").args(args).build();
8075 Spec {
8076 name: "test".to_string(),
8077 bin: "test".to_string(),
8078 cmd,
8079 ..Default::default()
8080 }
8081 }
8082
8083 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8084 let arg = parsed
8085 .args
8086 .keys()
8087 .find(|a| a.name == name)
8088 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8089 parsed.args.get(arg).unwrap().to_string()
8090 }
8091
8092 fn required_arg(name: &str) -> SpecArg {
8093 SpecArg::builder()
8094 .name(name)
8095 .var(true)
8096 .required(false)
8097 .double_dash(SpecDoubleDashChoices::Required)
8098 .build()
8099 }
8100
8101 #[test]
8102 fn test_double_dash_required_reports_error_once_for_variadic() {
8103 let spec = spec_with_args([required_arg("files")]);
8105
8106 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8107
8108 assert!(parsed.args.is_empty());
8109 assert_eq!(parsed.errors.len(), 1);
8110 assert!(
8111 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8112 );
8113 }
8114
8115 #[test]
8116 fn test_double_dash_required_suppresses_missing_arg() {
8117 let spec = spec_with_args([SpecArg::builder()
8119 .name("file")
8120 .required(true)
8121 .double_dash(SpecDoubleDashChoices::Required)
8122 .build()]);
8123
8124 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8125
8126 assert_eq!(parsed.errors.len(), 1);
8127 assert!(matches!(
8128 &parsed.errors[0],
8129 UsageErr::ArgRequiresDoubleDash(_)
8130 ));
8131 assert_eq!(
8133 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8134 Some("file")
8135 );
8136 assert!(!parsed.double_dash_seen);
8137 }
8138
8139 #[test]
8140 fn test_double_dash_routes_to_required_arg() {
8141 let spec = spec_with_args([
8144 SpecArg::builder()
8145 .name("tool")
8146 .var(true)
8147 .required(false)
8148 .build(),
8149 required_arg("command"),
8150 ]);
8151
8152 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8153
8154 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8155 assert_eq!(arg_value(&parsed, "command"), "node app.js");
8156 assert!(parsed.double_dash_seen);
8157 }
8158
8159 #[test]
8160 fn test_double_dash_routes_with_gap_reports_missing_arg() {
8161 let spec = spec_with_args([
8164 SpecArg::builder()
8165 .name("tool")
8166 .var(true)
8167 .required(true)
8168 .build(),
8169 required_arg("command"),
8170 ]);
8171
8172 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8173
8174 assert_eq!(arg_value(&parsed, "command"), "ls");
8175 assert!(parsed.args.keys().all(|a| a.name != "tool"));
8176 assert!(parsed
8177 .errors
8178 .iter()
8179 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8180 }
8181
8182 #[test]
8183 fn test_double_dash_gap_applies_defaults() {
8184 let spec = spec_with_args([
8186 SpecArg::builder()
8187 .name("tool")
8188 .var(true)
8189 .required(false)
8190 .default_value("node@20")
8191 .build(),
8192 required_arg("command"),
8193 ]);
8194
8195 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8196
8197 assert_eq!(arg_value(&parsed, "command"), "ls");
8198 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8199 }
8200
8201 fn spec_with_restart_token_and_required_arg() -> Spec {
8202 let run_cmd = SpecCommand::builder()
8203 .name("run")
8204 .arg(SpecArg::builder().name("task").build())
8205 .arg(required_arg("run_args"))
8206 .restart_token(":::".to_string())
8207 .build();
8208 let mut cmd = SpecCommand::builder().name("test").build();
8209 cmd.subcommands.insert("run".to_string(), run_cmd);
8210 Spec {
8211 name: "test".to_string(),
8212 bin: "test".to_string(),
8213 cmd,
8214 ..Default::default()
8215 }
8216 }
8217
8218 #[test]
8219 fn test_double_dash_required_restart_token_resets_separator() {
8220 let spec = spec_with_restart_token_and_required_arg();
8222
8223 let parsed = parse_partial(
8224 &spec,
8225 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8226 )
8227 .unwrap();
8228
8229 assert_eq!(arg_value(&parsed, "task"), "task2");
8230 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8231 assert_eq!(
8233 parsed
8234 .errors
8235 .iter()
8236 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8237 .count(),
8238 1
8239 );
8240 }
8241
8242 #[test]
8243 fn test_double_dash_required_restart_token_accepts_new_separator() {
8244 let spec = spec_with_restart_token_and_required_arg();
8245
8246 let parsed = parse(
8247 &spec,
8248 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8249 )
8250 .unwrap();
8251
8252 assert_eq!(arg_value(&parsed, "task"), "task2");
8253 assert_eq!(arg_value(&parsed, "run_args"), "c");
8254 }
8255
8256 #[test]
8257 fn test_double_dash_preserve_is_not_a_separator() {
8258 let spec = spec_with_args([
8261 SpecArg::builder()
8262 .name("kept")
8263 .var(true)
8264 .var_max(1)
8265 .required(false)
8266 .double_dash(SpecDoubleDashChoices::Preserve)
8267 .build(),
8268 required_arg("rest"),
8269 ]);
8270
8271 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8272
8273 assert_eq!(arg_value(&parsed, "kept"), "--");
8274 assert!(parsed.args.keys().all(|a| a.name != "rest"));
8275 assert!(!parsed.double_dash_seen);
8276 assert_eq!(parsed.errors.len(), 1);
8277 }
8278
8279 #[test]
8280 fn test_double_dash_required_does_not_bail_in_parse_partial() {
8281 let spec = spec_with_args([required_arg("file")]);
8283
8284 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8285 assert!(parse(&spec, &input(&["test", "x"])).is_err());
8286 }
8287
8288 #[test]
8289 fn test_double_dash_without_required_arg_does_not_move_cursor() {
8290 let spec = spec_with_args([
8292 SpecArg::builder().name("first").required(false).build(),
8293 SpecArg::builder().name("second").required(false).build(),
8294 ]);
8295
8296 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8297
8298 assert_eq!(arg_value(&parsed, "first"), "a");
8299 assert_eq!(arg_value(&parsed, "second"), "b");
8300 assert!(parsed.next_arg.is_none());
8301 }
8302
8303 #[test]
8304 fn test_parser_with_custom_env_for_required_arg() {
8305 let spec = spec_with_arg(
8306 SpecArg::builder()
8307 .name("name")
8308 .env("NAME")
8309 .required(true)
8310 .build(),
8311 );
8312 std::env::remove_var("NAME");
8313
8314 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8315 .expect("parse should succeed with custom env");
8316 assert_eq!(parsed.args.len(), 1);
8317 assert_eq!(first_string_value(&parsed), "john");
8318 }
8319
8320 #[test]
8321 fn test_parser_with_custom_env_for_required_flag() {
8322 let spec = spec_with_flag(
8323 SpecFlag::builder()
8324 .long("name")
8325 .env("NAME")
8326 .required(true)
8327 .arg(SpecArg::builder().name("name").build())
8328 .build(),
8329 );
8330 std::env::remove_var("NAME");
8331
8332 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8333 .expect("parse should succeed with custom env");
8334 assert_eq!(parsed.flags.len(), 1);
8335 assert_eq!(first_string_value(&parsed), "jane");
8336 }
8337
8338 #[test]
8339 fn test_flag_environment_fallbacks_preserve_declaration_order() {
8340 let spec = spec_with_flag(
8341 SpecFlag::builder()
8342 .long("name")
8343 .env("NAME")
8344 .env_fallback("OLD_NAME")
8345 .env_fallback("OLDER_NAME")
8346 .deprecated_env("DEPRECATED_NAME")
8347 .arg(SpecArg::builder().name("name").build())
8348 .build(),
8349 );
8350
8351 let parsed = parse_with_env(
8352 &spec,
8353 &["test"],
8354 &[
8355 ("NAME", "canonical"),
8356 ("OLD_NAME", "fallback"),
8357 ("DEPRECATED_NAME", "deprecated"),
8358 ],
8359 )
8360 .unwrap();
8361 assert_eq!(first_string_value(&parsed), "canonical");
8362
8363 let parsed = parse_with_env(
8364 &spec,
8365 &["test"],
8366 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8367 )
8368 .unwrap();
8369 assert_eq!(first_string_value(&parsed), "old");
8370
8371 let parsed =
8372 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8373 assert_eq!(first_string_value(&parsed), "deprecated");
8374 }
8375
8376 #[test]
8377 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8378 let spec = spec_with_flag(
8379 SpecFlag::builder()
8380 .long("name")
8381 .env("NAME")
8382 .deprecated_env("DEPRECATED_NAME")
8383 .arg(SpecArg::builder().name("name").build())
8384 .build(),
8385 );
8386
8387 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8389 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8390
8391 let parsed =
8392 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8393 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8394 assert_eq!(
8395 parsed.warnings[0].kind,
8396 crate::warn::WarningKind::DeprecatedEnv
8397 );
8398 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8399 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8400 assert_eq!(first_string_value(&parsed), "deprecated");
8402 }
8403
8404 #[test]
8405 fn a_deprecated_flag_reports_only_when_it_was_used() {
8406 let spec = spec_with_flag(
8407 SpecFlag::builder()
8408 .long("output")
8409 .deprecated("use --out")
8410 .deprecated_remove_at("3.0.0")
8411 .arg(SpecArg::builder().name("output").build())
8412 .build(),
8413 );
8414
8415 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8416 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8417
8418 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8419 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8420 assert_eq!(
8421 parsed.warnings[0].kind,
8422 crate::warn::WarningKind::DeprecatedFlag
8423 );
8424 assert_eq!(parsed.warnings[0].name, "--output");
8426 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8427 assert_eq!(
8428 parsed.warnings[0].render(),
8429 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8430 );
8431 }
8432
8433 #[test]
8434 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8435 let flag = SpecFlag::builder()
8436 .long("output")
8437 .deprecated("use --out")
8438 .deprecated_warn_at("9.0.0")
8439 .arg(SpecArg::builder().name("output").build())
8440 .build();
8441 let mut spec = spec_with_flag(flag);
8442 spec.version = Some("2.0.0".to_string());
8443
8444 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8445 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8446
8447 spec.version = Some("9.0.0".to_string());
8449 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8450 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8451 }
8452
8453 #[test]
8454 fn test_parser_with_custom_env_still_fails_when_missing() {
8455 let spec = spec_with_arg(
8456 SpecArg::builder()
8457 .name("name")
8458 .env("NAME")
8459 .required(true)
8460 .build(),
8461 );
8462 std::env::remove_var("NAME");
8463 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8464 }
8465
8466 #[test]
8467 fn test_parser_does_not_treat_env_choice_value_as_help() {
8468 let spec = spec_with_arg(
8469 SpecArg::builder()
8470 .name("env")
8471 .env("CURRENT_ENV")
8472 .choices(["dev", "staging"])
8473 .required(false)
8474 .build(),
8475 );
8476
8477 assert_parse_err(
8478 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8479 "Invalid choice for arg env: --help, expected one of dev, staging",
8480 );
8481 }
8482
8483 #[test]
8484 fn test_parser_does_not_treat_default_choice_value_as_help() {
8485 let spec = spec_with_flag(
8486 SpecFlag::builder()
8487 .long("env")
8488 .arg(
8489 SpecArg::builder()
8490 .name("env")
8491 .choices(["dev", "staging"])
8492 .build(),
8493 )
8494 .default_value("--help")
8495 .build(),
8496 );
8497
8498 assert_parse_err(
8499 parse_with_env(&spec, &["test"], &[]),
8500 "Invalid choice for option env: --help, expected one of dev, staging",
8501 );
8502 }
8503
8504 fn words(of: &[&str]) -> Vec<String> {
8506 of.iter().map(|s| s.to_string()).collect()
8507 }
8508
8509 #[test]
8510 fn a_command_that_needs_a_subcommand_says_so() {
8511 let spec: Spec = r#"
8515name "ex"
8516bin "ex"
8517cmd "gen" subcommand_required=#true {
8518 cmd "two" {}
8519 cmd "one" {}
8520 cmd "secret" hide=#true {}
8521 alias "g"
8522}
8523cmd "open" {
8524 cmd "sub" {}
8525}
8526"#
8527 .parse()
8528 .unwrap();
8529
8530 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
8531 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
8535
8536 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
8538 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
8539
8540 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
8542
8543 parse(&spec, &words(&["ex", "open"])).unwrap();
8546 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
8547 }
8548
8549 #[test]
8550 fn arg_required_else_help_observes_the_selected_commands_argv() {
8551 let spec: Spec = r#"
8552name "ex"
8553bin "ex"
8554flag "--verbose" global=#true
8555cmd "run" arg_required_else_help=#true {
8556 flag "--all"
8557}
8558"#
8559 .parse()
8560 .unwrap();
8561 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
8562
8563 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
8564 assert!(err.to_string().contains("Usage: ex run"), "{err}");
8565
8566 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
8569 assert!(err.to_string().contains("Usage: ex run"), "{err}");
8570
8571 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
8572 }
8573
8574 #[test]
8575 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
8576 let spec: Spec = r#"
8577name "ex"
8578bin "ex"
8579unknown_flags "error"
8580external_subcommand #true
8581cmd "install"
8582flag "-v --verbose" global=#true
8583"#
8584 .parse()
8585 .unwrap();
8586
8587 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
8588 assert_eq!(
8589 parsed.external,
8590 Some(vec!["foo".into(), "--help".into(), "bar".into()])
8591 );
8592 assert!(parsed.flags.is_empty());
8593
8594 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
8596 assert_eq!(parsed.cmd.name, "install");
8597 assert!(parsed.external.is_none());
8598
8599 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
8601 assert_eq!(
8602 parsed.external,
8603 Some(vec!["foo".into(), "--verbose".into()])
8604 );
8605 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
8606
8607 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
8609
8610 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
8614 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
8615 }
8616
8617 #[test]
8618 fn an_external_subcommand_satisfies_subcommand_required() {
8619 let mut spec: Spec = r#"
8620name "ex"
8621bin "ex"
8622external_subcommand #true
8623cmd "install"
8624"#
8625 .parse()
8626 .unwrap();
8627 spec.cmd.subcommand_required = true;
8628
8629 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
8630 assert!(parse(&spec, &input(&["ex"])).is_err());
8631 }
8632
8633 #[test]
8634 fn a_default_subcommand_outranks_an_external_one() {
8635 let spec: Spec = r#"
8636name "ex"
8637bin "ex"
8638default_subcommand "run"
8639external_subcommand #true
8640cmd "run" {
8641 arg "[task]"
8642}
8643"#
8644 .parse()
8645 .unwrap();
8646
8647 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
8648 assert_eq!(parsed.cmd.name, "run");
8649 assert!(parsed.external.is_none());
8650 assert_eq!(first_string_value(&parsed), "build");
8651 }
8652
8653 #[test]
8654 fn multicall_basename_strips_a_path_and_exe() {
8655 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
8656 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
8657 assert_eq!(multicall_basename("LS.EXE"), "LS");
8658 assert_eq!(multicall_basename("busybox"), "busybox");
8659 }
8660
8661 #[test]
8662 fn a_multicall_applet_is_the_first_word() {
8663 let spec: Spec = r#"
8664name "busybox"
8665bin "busybox"
8666multicall #true
8667cmd "ls" {
8668 arg "[ARGS]" var=#true
8669}
8670cmd "cat"
8671"#
8672 .parse()
8673 .unwrap();
8674
8675 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
8677 assert_eq!(parsed.cmd.name, "ls");
8678 match parsed.args.values().next() {
8679 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
8680 other => panic!("expected ARGS to collect -l, got {other:?}"),
8681 }
8682
8683 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
8685 assert_eq!(parsed.cmd.name, "ls");
8686
8687 let mut configured = spec.clone();
8689 configured.name = "BusyBox".to_string();
8690 configured.bin = "/opt/bin/busybox.exe".to_string();
8691 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
8692 assert_eq!(parsed.cmd.name, "ls");
8693
8694 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
8696 assert_eq!(parsed.cmd.name, "ls");
8697
8698 let mut plain = spec.clone();
8700 plain.multicall = false;
8701 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
8702 assert_eq!(parsed.cmd.name, "ls");
8703 }
8704
8705 #[test]
8706 fn a_multicall_unknown_applet_can_be_external() {
8707 let spec: Spec = r#"
8708name "busybox"
8709bin "busybox"
8710multicall #true
8711unknown_flags "error"
8712external_subcommand #true
8713cmd "ls"
8714"#
8715 .parse()
8716 .unwrap();
8717
8718 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
8719 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
8720
8721 let mut closed = spec.clone();
8722 closed.cmd.external_subcommand = false;
8723 assert!(parse(&closed, &input(&["wat"])).is_err());
8724 }
8725
8726 #[cfg(feature = "unstable_choices_env")]
8727 #[test]
8728 fn test_parser_arg_choices_from_custom_env() {
8729 let spec = spec_arg_choices_env("DEPLOY_ENVS");
8730
8731 let parsed =
8732 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
8733 assert_eq!(first_string_value(&parsed), "bar");
8734
8735 assert_parse_err(
8736 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
8737 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
8738 );
8739 assert_parse_err(
8740 parse_with_env(&spec, &["test", "prod"], &[]),
8741 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
8742 );
8743 }
8744
8745 #[cfg(feature = "unstable_choices_env")]
8746 #[test]
8747 fn test_parser_validates_flag_choices_from_custom_env() {
8748 let spec = spec_flag_choices_env("DEPLOY_ENVS");
8749 let parsed = parse_with_env(
8750 &spec,
8751 &["test", "--env", "baz"],
8752 &[("DEPLOY_ENVS", "foo,bar baz")],
8753 )
8754 .unwrap();
8755 assert_eq!(first_string_value(&parsed), "baz");
8756 }
8757
8758 #[cfg(feature = "unstable_choices_env")]
8759 #[test]
8760 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
8761 let arg_env_spec = spec_with_arg(
8762 SpecArg::builder()
8763 .name("env")
8764 .env("CURRENT_ENV")
8765 .choices_env("DEPLOY_ENVS")
8766 .build(),
8767 );
8768 assert_parse_err(
8769 parse_with_env(
8770 &arg_env_spec,
8771 &["test"],
8772 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
8773 ),
8774 "Invalid choice for arg env: prod, expected one of dev, staging",
8775 );
8776
8777 let flag_default_spec = spec_with_flag(
8778 SpecFlag::builder()
8779 .long("env")
8780 .arg(
8781 SpecArg::builder()
8782 .name("env")
8783 .choices_env("DEPLOY_ENVS")
8784 .build(),
8785 )
8786 .default_value("prod")
8787 .build(),
8788 );
8789 assert_parse_err(
8790 parse_with_env(
8791 &flag_default_spec,
8792 &["test"],
8793 &[("DEPLOY_ENVS", "dev,staging")],
8794 ),
8795 "Invalid choice for option env: prod, expected one of dev, staging",
8796 );
8797 }
8798
8799 #[test]
8800 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
8801 let spec: Spec = r#"
8802 flag "-v --verbose" var=#true
8803 arg "[database]" default="myapp_dev"
8804 arg "[args...]"
8805 "#
8806 .parse()
8807 .unwrap();
8808 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8809 .into_iter()
8810 .map(String::from)
8811 .collect();
8812 let parsed = parse(&spec, &input).unwrap();
8813 let env = parsed.as_env();
8814 assert_eq!(env.get("usage_database").unwrap(), "mydb");
8815 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
8816 }
8817
8818 #[test]
8819 fn test_variadic_arg_captures_unknown_flags() {
8820 let cmd = SpecCommand::builder()
8821 .name("test")
8822 .flag(SpecFlag::builder().short('v').long("verbose").build())
8823 .arg(SpecArg::builder().name("database").required(false).build())
8824 .arg(
8825 SpecArg::builder()
8826 .name("args")
8827 .required(false)
8828 .var(true)
8829 .build(),
8830 )
8831 .build();
8832 let spec = Spec {
8833 name: "test".to_string(),
8834 bin: "test".to_string(),
8835 cmd,
8836 ..Default::default()
8837 };
8838
8839 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8841 .into_iter()
8842 .map(String::from)
8843 .collect();
8844 let parsed = parse(&spec, &input).unwrap();
8845 assert_eq!(parsed.args.len(), 2);
8846 let args_val = parsed
8847 .args
8848 .iter()
8849 .find(|(a, _)| a.name == "args")
8850 .unwrap()
8851 .1;
8852 match args_val {
8853 ParseValue::MultiString(v) => {
8854 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8855 }
8856 _ => panic!("Expected MultiString, got {:?}", args_val),
8857 }
8858 }
8859
8860 #[test]
8861 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
8862 let cmd = SpecCommand::builder()
8863 .name("test")
8864 .flag(SpecFlag::builder().short('v').long("verbose").build())
8865 .arg(SpecArg::builder().name("database").required(false).build())
8866 .arg(
8867 SpecArg::builder()
8868 .name("args")
8869 .required(false)
8870 .var(true)
8871 .build(),
8872 )
8873 .build();
8874 let spec = Spec {
8875 name: "test".to_string(),
8876 bin: "test".to_string(),
8877 cmd,
8878 ..Default::default()
8879 };
8880
8881 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
8883 .into_iter()
8884 .map(String::from)
8885 .collect();
8886 let parsed = parse(&spec, &input).unwrap();
8887 assert_eq!(parsed.args.len(), 2);
8888 let args_val = parsed
8889 .args
8890 .iter()
8891 .find(|(a, _)| a.name == "args")
8892 .unwrap()
8893 .1;
8894 match args_val {
8895 ParseValue::MultiString(v) => {
8896 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8897 }
8898 _ => panic!("Expected MultiString, got {:?}", args_val),
8899 }
8900 }
8901
8902 #[test]
8903 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
8904 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
8907
8908 let input: Vec<String> = vec!["test", "--option=3"]
8911 .into_iter()
8912 .map(String::from)
8913 .collect();
8914 let parsed = parse(&spec, &input).unwrap();
8915 let env = parsed.as_env();
8916 assert_eq!(
8917 env.get("usage_other_args").map(String::as_str),
8918 Some("'--option=3'"),
8919 "expected a single --option=3 token, got {:?}",
8920 env.get("usage_other_args"),
8921 );
8922
8923 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
8925 .into_iter()
8926 .map(String::from)
8927 .collect();
8928 let parsed2 = parse(&spec, &input2).unwrap();
8929 let env2 = parsed2.as_env();
8930 assert_eq!(
8931 env2.get("usage_other_args").map(String::as_str),
8932 Some("'--foo=bar' '--baz=qux'"),
8933 "expected two intact tokens, got {:?}",
8934 env2.get("usage_other_args"),
8935 );
8936
8937 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
8939 .into_iter()
8940 .map(String::from)
8941 .collect();
8942 let parsed3 = parse(&spec, &input3).unwrap();
8943 let env3 = parsed3.as_env();
8944 assert_eq!(
8945 env3.get("usage_other_args").map(String::as_str),
8946 Some("positional1 '--option=3' positional2"),
8947 "expected positional args and intact flag token, got {:?}",
8948 env3.get("usage_other_args"),
8949 );
8950 }
8951
8952 #[test]
8953 fn test_allow_hyphen_values_consumes_short_flag_collision() {
8954 let spec = r#"
8955flag "-d --working-dir <DIR>"
8956flag "-a --args <ARGS>" allow_hyphen_values=#true
8957"#
8958 .parse::<Spec>()
8959 .unwrap();
8960
8961 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
8962
8963 assert_eq!(parsed.flags.len(), 1);
8964 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8965 }
8966
8967 #[test]
8968 fn test_allow_hyphen_values_consumes_embedded_long_value() {
8969 let spec = r#"
8970flag "-d --working-dir <DIR>"
8971flag "-a --args <ARGS>" allow_hyphen_values=#true
8972"#
8973 .parse::<Spec>()
8974 .unwrap();
8975
8976 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
8977
8978 assert_eq!(parsed.flags.len(), 1);
8979 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
8980 }
8981
8982 #[test]
8983 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
8984 let spec = r#"
8989flag "-a --args <ARGS>" allow_hyphen_values=#true
8990arg "[rest]..."
8991"#
8992 .parse::<Spec>()
8993 .unwrap();
8994
8995 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
8996
8997 assert_eq!(flag_string_value(&parsed, "args"), "--");
8998 let rest = parsed
8999 .args
9000 .values()
9001 .next()
9002 .expect("expected the word after the separator to reach the argument");
9003 assert_eq!(rest.to_string(), "-x");
9004 }
9005
9006 #[test]
9007 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
9008 let spec = r#"
9013flag "-a --args <ARGS>..." allow_hyphen_values=#true
9014"#
9015 .parse::<Spec>()
9016 .unwrap();
9017
9018 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
9019
9020 let flag = parsed
9021 .flags
9022 .keys()
9023 .find(|flag| flag.name == "args")
9024 .expect("expected args flag");
9025 match parsed.flags.get(flag).expect("expected args value") {
9026 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
9027 other => panic!("expected a list of values, got {other:?}"),
9028 }
9029 }
9030
9031 #[test]
9032 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
9033 let spec = r#"
9034flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
9035"#
9036 .parse::<Spec>()
9037 .unwrap();
9038
9039 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
9040
9041 let flag = parsed
9042 .flags
9043 .keys()
9044 .find(|flag| flag.name == "args")
9045 .expect("expected args flag");
9046 let value = parsed.flags.get(flag).expect("expected args value");
9047 match value {
9048 ParseValue::MultiString(values) => {
9049 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
9050 }
9051 _ => panic!("expected MultiString, got {value:?}"),
9052 }
9053 }
9054
9055 #[test]
9056 fn test_require_equals_accepts_attached_and_refuses_detached() {
9057 let spec = r#"
9058flag "--inspect <PORT>" require_equals=#true
9059"#
9060 .parse::<Spec>()
9061 .unwrap();
9062
9063 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
9064 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9065
9066 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
9067 let msg = format!("{err}");
9068 assert!(
9069 msg.contains("requires an argument") || msg.contains("inspect"),
9070 "detached value must be refused: {msg}"
9071 );
9072 }
9073
9074 #[test]
9075 fn boolean_flags_can_accept_attached_values_when_enabled() {
9076 let spec: Spec = r#"
9077name "ex"
9078bin "ex"
9079flag "--color" negate="--no-color" bool_value=#true
9080arg "[rest]"
9081"#
9082 .parse()
9083 .unwrap();
9084
9085 for (token, expected) in [
9086 ("--color", true),
9087 ("--color=true", true),
9088 ("--color=false", false),
9089 ("--no-color", false),
9090 ("--no-color=false", true),
9091 ] {
9092 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9093 assert!(
9094 matches!(
9095 parsed.flags.get(&spec.cmd.flags[0]),
9096 Some(ParseValue::Bool(value)) if *value == expected
9097 ),
9098 "{token}"
9099 );
9100 }
9101
9102 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9103 assert!(matches!(
9104 parsed.args.get(&spec.cmd.args[0]),
9105 Some(ParseValue::String(value)) if value == "word"
9106 ));
9107 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9108 assert!(err.to_string().contains("expected `true` or `false`"));
9109
9110 let strict: Spec = r#"
9111name "ex"
9112bin "ex"
9113args_override_self #false
9114flag "--color" negate="--no-color" bool_value=#true
9115"#
9116 .parse()
9117 .unwrap();
9118 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9119 let parsed = parse(
9120 &strict,
9121 &input(&["ex", "--color=false", "--no-color=false"]),
9122 )
9123 .unwrap();
9124 assert!(matches!(
9125 parsed.flags.get(&strict.cmd.flags[0]),
9126 Some(ParseValue::Bool(true))
9127 ));
9128 }
9129
9130 #[test]
9131 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9132 let spec = r#"
9133flag "-a --all"
9134flag "-i --inspect <PORT>" require_equals=#true
9135"#
9136 .parse::<Spec>()
9137 .unwrap();
9138
9139 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9140 let msg = format!("{err}");
9141 assert!(
9142 msg.contains("requires an argument") || msg.contains("inspect"),
9143 "bundled short must refuse the following word: {msg}"
9144 );
9145 }
9146
9147 #[test]
9148 fn test_default_missing_binds_when_the_value_is_left_off() {
9149 let spec = r#"
9150flag "-c --color <WHEN>" default_missing="always"
9151flag "-v --verbose"
9152"#
9153 .parse::<Spec>()
9154 .unwrap();
9155
9156 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9157 assert_eq!(flag_string_value(&parsed, "color"), "always");
9158
9159 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9160 assert_eq!(flag_string_value(&parsed, "color"), "never");
9161
9162 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9163 assert_eq!(flag_string_value(&parsed, "color"), "never");
9164
9165 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9166 assert_eq!(flag_string_value(&parsed, "color"), "always");
9167 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9168
9169 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9170 assert_eq!(flag_string_value(&parsed, "color"), "");
9171
9172 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9173 assert_eq!(flag_string_value(&parsed, "color"), "never");
9174
9175 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9176 assert_eq!(flag_string_value(&parsed, "color"), "always");
9177 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9178 }
9179
9180 #[test]
9181 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9182 let spec = r#"
9183flag "--bump [LEVEL]" value_optional=#true
9184flag "--verbose"
9185arg "[FILE]"
9186"#
9187 .parse::<Spec>()
9188 .unwrap();
9189
9190 let absent = parse(&spec, &input(&["test"])).unwrap();
9191 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9192
9193 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9194 let bump = bare
9195 .flags
9196 .iter()
9197 .find(|(flag, _)| flag.name == "bump")
9198 .map(|(_, value)| value)
9199 .unwrap();
9200 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9201 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9202 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9203
9204 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9205 assert_eq!(flag_string_value(&explicit, "bump"), "");
9206
9207 let corrected = parse(
9208 &spec,
9209 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9210 )
9211 .unwrap();
9212 let bump = corrected
9213 .flags
9214 .iter()
9215 .find(|(flag, _)| flag.name == "bump")
9216 .map(|(_, value)| value)
9217 .unwrap();
9218 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9219
9220 let collecting = r#"
9221flag "--tag [TAG]..." value_optional=#true
9222flag "--verbose"
9223"#
9224 .parse::<Spec>()
9225 .unwrap();
9226 let valued = parse(
9227 &collecting,
9228 &input(&["test", "--tag", "one", "two", "--verbose"]),
9229 )
9230 .unwrap();
9231 let tag = valued
9232 .flags
9233 .iter()
9234 .find(|(flag, _)| flag.name == "tag")
9235 .map(|(_, value)| value)
9236 .unwrap();
9237 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9238 }
9239
9240 #[test]
9241 fn test_repeatable_bare_optional_values_count_each_occurrence() {
9242 let spec = r#"
9243flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9244"#
9245 .parse::<Spec>()
9246 .unwrap();
9247
9248 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9249 let tag = parsed
9250 .flags
9251 .iter()
9252 .find(|(flag, _)| flag.name == "tag")
9253 .map(|(_, value)| value)
9254 .unwrap();
9255 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9256
9257 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9258 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9259 }
9260
9261 #[test]
9262 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9263 let spec = r#"
9264flag "--tag [TAG]..." var=#true value_optional=#true
9265flag "--verbose"
9266"#
9267 .parse::<Spec>()
9268 .unwrap();
9269
9270 for argv in [
9271 &["test", "--tag", "one", "two"][..],
9272 &["test", "--tag", "one", "two", "--verbose"][..],
9273 &["test", "--tag", "one", "--tag", "two"][..],
9274 ] {
9275 let parsed = parse(&spec, &input(argv)).unwrap();
9276 let tag = parsed
9277 .flags
9278 .iter()
9279 .find(|(flag, _)| flag.name == "tag")
9280 .map(|(_, value)| value)
9281 .unwrap();
9282 assert!(
9283 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9284 "argv={argv:?}: {tag:?}"
9285 );
9286 }
9287
9288 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9289 let tag = bare
9290 .flags
9291 .iter()
9292 .find(|(flag, _)| flag.name == "tag")
9293 .map(|(_, value)| value)
9294 .unwrap();
9295 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9296 }
9297
9298 #[test]
9299 fn test_default_missing_with_require_equals_refuses_the_following_word() {
9300 let spec = r#"
9301flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9302arg "[rest]"
9303"#
9304 .parse::<Spec>()
9305 .unwrap();
9306
9307 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9308 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9309
9310 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9311 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9312
9313 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9315 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9316 assert_eq!(
9317 parsed
9318 .args
9319 .values()
9320 .next()
9321 .map(|v| v.to_string())
9322 .as_deref(),
9323 Some("80")
9324 );
9325
9326 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9327 assert_eq!(flag_string_value(&parsed, "inspect"), "");
9328 }
9329
9330 #[test]
9331 fn test_default_missing_must_be_a_choice() {
9332 let spec = r#"
9333flag "--color <WHEN>" default_missing="always" {
9334 choices "auto" "always" "never"
9335}
9336"#
9337 .parse::<Spec>()
9338 .unwrap();
9339
9340 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9341 assert_eq!(flag_string_value(&parsed, "color"), "always");
9342
9343 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9344 assert_eq!(flag_string_value(&parsed, "color"), "never");
9345
9346 let spec = r#"
9347flag "--color <WHEN>" default_missing="wat" {
9348 choices "auto" "always" "never"
9349}
9350"#
9351 .parse::<Spec>()
9352 .unwrap();
9353
9354 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9355 let msg = format!("{err}");
9356 assert!(
9357 msg.contains("Invalid choice for option color: wat"),
9358 "missing default has to pass choices the same way a typed value does: {msg}"
9359 );
9360
9361 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9362 let msg = format!("{err}");
9363 assert!(
9364 msg.contains("Invalid choice for option color: wat"),
9365 "an attached value that is not a choice is still refused: {msg}"
9366 );
9367
9368 let spec = r#"
9369flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9370 choices "9229" "80"
9371}
9372arg "[rest]"
9373"#
9374 .parse::<Spec>()
9375 .unwrap();
9376
9377 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9378 let msg = format!("{err}");
9379 assert!(
9380 msg.contains("Invalid choice for option inspect: wat"),
9381 "require_equals still binds the missing string, so the error is the choice: {msg}"
9382 );
9383 }
9384
9385 #[test]
9386 fn test_hyphen_values_still_default_to_short_flag_parsing() {
9387 let spec = r#"
9388flag "-d --working-dir <DIR>"
9389flag "-a --args <ARGS>"
9390"#
9391 .parse::<Spec>()
9392 .unwrap();
9393
9394 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9395
9396 assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
9397 }
9398
9399 mod available_flags {
9402 use super::*;
9403
9404 fn spec() -> Spec {
9405 r#"
9406bin "test"
9407flag "-v --verbose" global=#true
9408flag "--raw" global=#true effect="write"
9409flag "--local-only"
9410cmd "run" {
9411 flag "-r --raw"
9412 flag "-w --watch"
9413 cmd "once"
9414}
9415"#
9416 .parse::<Spec>()
9417 .unwrap()
9418 }
9419
9420 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9421 let mut chain = vec![&spec.cmd];
9422 for segment in path {
9423 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9424 }
9425 chain
9426 }
9427
9428 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9429 let mut names: Vec<_> = available_flags(&chain(spec, path))
9430 .iter()
9431 .map(|f| f.name.clone())
9432 .collect();
9433 names.sort();
9434 names
9435 }
9436
9437 #[test]
9438 fn an_empty_chain_yields_nothing() {
9439 assert!(available_flags(&[]).is_empty());
9440 }
9441
9442 #[test]
9443 fn the_root_gets_its_own_flags() {
9444 let spec = spec();
9445 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9446 }
9447
9448 #[test]
9449 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9450 let spec = spec();
9451 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
9452 }
9453
9454 #[test]
9455 fn a_re_declared_global_is_listed_once() {
9456 let spec = r#"
9459bin "test"
9460flag "-y --yes" global=#true effect="write"
9461cmd "rm" {
9462 flag "-y --yes"
9463}
9464"#
9465 .parse::<Spec>()
9466 .unwrap();
9467 let flags = available_flags(&chain(&spec, &["rm"]));
9468 assert_eq!(flags.len(), 1, "{flags:?}");
9469 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
9470 }
9471
9472 #[test]
9473 fn a_re_declared_global_keeps_the_globals_declaration() {
9474 let spec = spec();
9478 let flags = available_flags(&chain(&spec, &["run"]));
9479 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
9480 assert!(raw.global);
9481 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
9482 assert_eq!(raw.short, ['r']);
9483 }
9484
9485 #[test]
9486 fn it_matches_what_a_parse_accepts() {
9487 let spec = spec();
9490 for path in [vec![], vec!["run"], vec!["run", "once"]] {
9491 let argv = std::iter::once("test".to_string())
9492 .chain(path.iter().map(|s| s.to_string()))
9493 .collect::<Vec<_>>();
9494 let parsed = parse_partial(&spec, &argv).unwrap();
9495
9496 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
9497 .map(|f| f.name.clone())
9498 .collect();
9499 from_parse.sort();
9500 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
9501 }
9502 }
9503 }
9504
9505 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
9510 parsed
9511 .tokens
9512 .iter()
9513 .find(|token| token.index == index)
9514 .unwrap_or_else(|| panic!("no token at {index}"))
9515 .roles
9516 .iter()
9517 .map(render_role)
9518 .collect()
9519 }
9520
9521 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
9522 parsed
9523 .flag_origins
9524 .iter()
9525 .find(|(f, _)| f.name == flag)
9526 .map(|(_, origins)| origins.clone())
9527 .unwrap_or_default()
9528 }
9529
9530 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
9531 let env = env
9532 .iter()
9533 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
9534 .collect();
9535 Parser::new(spec)
9536 .with_env(env)
9537 .explain(&input(words))
9538 .unwrap()
9539 }
9540
9541 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
9542 explain_with_env(spec, words, &[])
9543 }
9544
9545 #[test]
9546 fn an_attached_long_value_is_recorded_on_the_flag_token() {
9547 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9548 .parse()
9549 .unwrap();
9550
9551 let parsed = explain(&spec, &["ex", "--env=prod"]);
9552
9553 assert_eq!(roles(&parsed, 0), ["program"]);
9554 assert_eq!(
9555 roles(&parsed, 1),
9556 ["flag env as --env", "value of env = [\"prod\"], attached"]
9557 );
9558 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
9561 }
9562
9563 #[test]
9564 fn a_detached_long_value_is_recorded_on_its_own_token() {
9565 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9566 .parse()
9567 .unwrap();
9568
9569 let parsed = explain(&spec, &["ex", "--env", "prod"]);
9570
9571 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
9572 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
9573 }
9574
9575 #[test]
9576 fn a_short_bundle_is_attributed_to_the_bundle_token() {
9577 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
9578 .parse()
9579 .unwrap();
9580
9581 let parsed = explain(&spec, &["ex", "-abj8"]);
9582
9583 assert_eq!(
9586 roles(&parsed, 1),
9587 [
9588 "flag a as -a",
9589 "flag b as -b",
9590 "flag j as -j",
9591 "value of j = [\"8\"], attached",
9592 ]
9593 );
9594 assert_eq!(parsed.tokens.len(), 2);
9595 }
9596
9597 #[test]
9598 fn a_delimiter_splits_one_token_into_several_values() {
9599 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
9600 .parse()
9601 .unwrap();
9602
9603 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
9604
9605 assert_eq!(
9606 roles(&parsed, 2),
9607 ["value of tags = [\"a\", \"b\", \"c\"]"],
9608 "the values meant, not the word typed"
9609 );
9610 }
9611
9612 #[test]
9613 fn a_separator_and_the_words_after_it_are_distinguished() {
9614 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
9615 .parse()
9616 .unwrap();
9617
9618 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
9619
9620 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
9621 assert_eq!(roles(&parsed, 2), ["separator"]);
9622 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
9624 }
9625
9626 #[test]
9627 fn a_second_separator_is_data() {
9628 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
9629 .parse()
9630 .unwrap();
9631
9632 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
9633
9634 assert_eq!(roles(&parsed, 1), ["separator"]);
9635 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
9636 }
9637
9638 #[test]
9639 fn an_unknown_flag_says_what_took_it() {
9640 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9641 .parse()
9642 .unwrap();
9643
9644 let parsed = explain(&spec, &["ex", "--wat"]);
9645
9646 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
9649 }
9650
9651 #[test]
9652 fn a_subcommand_word_is_not_a_positional() {
9653 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
9654 .parse()
9655 .unwrap();
9656
9657 let parsed = explain(&spec, &["ex", "build", "a"]);
9658
9659 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
9660 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
9661 }
9662
9663 #[test]
9664 fn a_multicall_applet_is_read_at_argv0() {
9665 let spec: Spec =
9666 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
9667 .parse()
9668 .unwrap();
9669
9670 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
9671
9672 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
9675 assert!(parsed.tokens[0].synthesized);
9676 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
9677 }
9678
9679 #[test]
9680 fn words_the_parse_never_reached_say_so() {
9681 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9682 .parse()
9683 .unwrap();
9684
9685 let parsed = Parser::new(&spec)
9686 .explain(&input(&["ex", "--help", "a"]))
9687 .unwrap();
9688
9689 assert_eq!(roles(&parsed, 2), ["unread"]);
9690 }
9691
9692 #[test]
9693 fn an_env_origin_names_the_variable_that_fired() {
9694 let spec: Spec =
9695 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
9696 .parse()
9697 .unwrap();
9698
9699 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
9700 assert_eq!(
9701 origins(&primary, "token"),
9702 [ValueOrigin::Env("EX_TOKEN".to_string())]
9703 );
9704
9705 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
9708 assert_eq!(
9709 origins(&fallback, "token"),
9710 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
9711 );
9712 }
9713
9714 #[test]
9715 fn a_default_origin_is_recorded_for_flags_and_args() {
9716 let spec: Spec =
9717 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
9718 .parse()
9719 .unwrap();
9720
9721 let parsed = explain(&spec, &["ex"]);
9722
9723 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
9724 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
9725 assert_eq!(arg.name, "src");
9726 assert_eq!(origins, &[ValueOrigin::Default]);
9727 }
9728
9729 #[test]
9730 fn a_default_if_origin_carries_the_condition_that_fired() {
9731 let spec: Spec = r#"
9732name "ex"
9733bin "ex"
9734flag "--profile <p>"
9735flag "--strict" {
9736 default_if "--profile" "prod" "true"
9737}
9738 "#
9739 .parse()
9740 .unwrap();
9741
9742 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
9743
9744 assert_eq!(
9747 origins(&parsed, "strict"),
9748 [ValueOrigin::DefaultIf {
9749 selector: "--profile".to_string(),
9750 when: Some("prod".to_string()),
9751 }]
9752 );
9753 }
9754
9755 #[test]
9756 fn a_bare_optional_value_flag_records_default_missing() {
9757 let spec: Spec =
9758 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
9759 .parse()
9760 .unwrap();
9761
9762 let parsed = explain(&spec, &["ex", "--color", "-v"]);
9763
9764 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
9767 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9768 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
9769 }
9770
9771 #[test]
9772 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
9773 let spec: Spec =
9774 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
9775 .parse()
9776 .unwrap();
9777
9778 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
9779
9780 assert_eq!(
9782 roles(&parsed, 1),
9783 [
9784 "flag color as --color",
9785 "value of color = [\"red\"], attached"
9786 ]
9787 );
9788 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9789 }
9790
9791 #[test]
9792 fn an_override_names_the_flag_that_did_it() {
9793 let spec: Spec =
9794 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
9795 .parse()
9796 .unwrap();
9797
9798 let parsed = explain(&spec, &["ex", "--loud"]);
9799
9800 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
9803 assert!(origins(&parsed, "quiet").is_empty());
9804 }
9805
9806 #[test]
9807 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
9808 let spec: Spec = r#"
9809name "ex"
9810bin "ex"
9811cmd "run" restart_token=":::" {
9812 arg "<task>" default="build"
9813}
9814 "#
9815 .parse()
9816 .unwrap();
9817
9818 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
9819
9820 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
9824 assert_eq!(roles(&parsed, 3), ["restart"]);
9827 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
9828 assert!(parsed.arg_origins.is_empty());
9829 }
9830
9831 #[test]
9832 fn a_value_terminator_says_which_run_it_ended() {
9833 let spec: Spec = r#"
9834name "ex"
9835bin "ex"
9836flag "--exec <cmd>..." value_terminator=";"
9837arg "<src>"
9838 "#
9839 .parse()
9840 .unwrap();
9841
9842 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
9843
9844 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
9845 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
9848 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
9849 }
9850
9851 #[test]
9852 fn an_args_value_terminator_says_which_run_it_ended() {
9853 let spec: Spec = r#"
9854name "ex"
9855bin "ex"
9856arg "<files>..." value_terminator=";"
9857arg "[dest]"
9858 "#
9859 .parse()
9860 .unwrap();
9861
9862 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
9863
9864 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
9865 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
9866 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
9867 }
9868
9869 #[test]
9870 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
9871 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
9872 .parse()
9873 .unwrap();
9874
9875 let parsed = Parser::new(&spec)
9876 .explain(&input(&["ex", "--env=prod"]))
9877 .unwrap();
9878
9879 assert!(Parser::new(&spec)
9882 .parse(&input(&["ex", "--env=prod"]))
9883 .is_err());
9884 assert_eq!(
9885 roles(&parsed, 1),
9886 ["flag env as --env", "value of env = [\"prod\"], attached"]
9887 );
9888 assert!(
9889 parsed.errors.iter().any(|e| e.to_string().contains("src")),
9890 "{:?}",
9891 parsed.errors
9892 );
9893 }
9894
9895 #[test]
9896 fn an_external_subcommand_forwards_whole_tokens() {
9897 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
9898 .parse()
9899 .unwrap();
9900
9901 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
9902
9903 assert_eq!(roles(&parsed, 1), ["external"]);
9904 assert_eq!(roles(&parsed, 2), ["external"]);
9905 }
9906
9907 #[test]
9908 fn a_view_keeps_the_callers_argv_positions() {
9909 let spec: Spec = r#"
9910bin "ex"
9911view "runner" root="run"
9912cmd "run" {
9913 flag "--token <token>"
9914}
9915 "#
9916 .parse()
9917 .unwrap();
9918
9919 let parsed = explain(&spec, &["runner", "--token", "secret"]);
9920
9921 assert_eq!(roles(&parsed, 0), ["program"]);
9924 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
9925 }
9926}