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 && accepts_detached_flag_value(&f, &input[idx].word)
1447 {
1448 if let Some(words) = forwarded.as_mut() {
1449 words.push(input[idx].word.clone());
1450 }
1451 idx += 1;
1452 }
1453 if let Some(words) = forwarded {
1454 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&f));
1455 prefix_flags.push((f, words));
1456 }
1457 } else {
1458 break;
1461 }
1462 } else {
1463 if variadic_flag_active && out.cmd.subcommand_precedence_over_arg {
1464 idx += 1;
1465 continue;
1466 }
1467 if !used_default_subcommand && out.cmds.len() == 1 {
1476 if let Some(default_name) = &spec.default_subcommand {
1477 if let Some(subcommand) = out
1478 .cmd
1479 .find_subcommand(default_name)
1480 .filter(|_| default_accepts_word(&out.cmd, default_name, &input[idx].word))
1481 {
1482 if out.cmd.args_conflicts_with_subcommands && command_arg_found {
1483 bail!(
1484 "subcommand '{}' cannot be used with arguments on its parent command",
1485 subcommand.name
1486 );
1487 }
1488 let mut subcommand = subcommand.clone();
1489 subcommand.mount(&mount_prefix_words(&prefix_flags), mount_outputs)?;
1491 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
1492 merge_subcommand_flags(
1493 &mut out.available_flags,
1494 gather_flags(&subcommand),
1495 crossing_mount,
1496 );
1497 out.cmds.push(subcommand.clone());
1498 out.cmd = subcommand.clone();
1499 command_has_argv = true;
1500 prefix_flags.clear();
1501 command_arg_found = false;
1502 variadic_flag_active = false;
1503 mounts_resolved = true;
1506 used_default_subcommand = true;
1507 continue;
1512 }
1513 }
1514 }
1515 if out.cmd.external_subcommand {
1520 let rest: Vec<Token> = input.drain(idx..).collect();
1521 for token in &rest {
1522 trace.record(token.argv, TokenRole::External);
1523 }
1524 out.external = Some(rest.into_iter().map(|t| t.word).collect());
1525 break;
1526 }
1527 break;
1529 }
1530 }
1531
1532 let mut next_arg_idx: usize = 0;
1542 let mut enable_flags = true;
1543 let mut grouped_flag = false;
1544 let mut seen_double_dash = false;
1548 let mut double_dash_violations: HashSet<String> = HashSet::new();
1551 let mut scalar_occurrences: HashMap<(usize, usize), u8> = HashMap::new();
1556
1557 while !input.is_empty() {
1558 let token = input.pop_front().unwrap();
1559 let binding = token.binding;
1561 let argv = token.argv;
1562 let mut w = token.word;
1563 let attached_continuation = grouped_flag;
1568
1569 if let Some(ref restart_token) = out.cmd.restart_token {
1572 if w == *restart_token {
1573 out.args.clear();
1578 out.arg_origins.clear();
1583 trace.record(argv, TokenRole::Restart);
1584 next_arg_idx = 0;
1585 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
1589 }
1590 }
1591
1592 if enable_flags
1601 && !attached_continuation
1602 && w.starts_with('-')
1603 && out
1604 .flag_awaiting_value
1605 .last()
1606 .is_some_and(|flag| accepts_detached_flag_value(flag, &w))
1607 {
1608 let should_return = bind_pending_flag_value(
1611 spec,
1612 &out.cmd,
1613 &mut out.errors,
1614 &mut out.flags,
1615 &mut out.flag_awaiting_value,
1616 &mut w,
1617 &mut input,
1618 custom_env,
1619 trace,
1620 argv,
1621 false,
1623 )?;
1624 if should_return {
1625 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1626 return Ok((out, overridden_flags));
1627 }
1628 continue;
1629 }
1630
1631 if enable_flags
1637 && !attached_continuation
1638 && !out.flag_awaiting_value.is_empty()
1639 && out.flag_awaiting_value.last().is_some_and(|flag| {
1640 (flag.default_missing.is_some() || flag.value_optional)
1641 && !accepts_detached_flag_value(flag, &w)
1642 })
1643 {
1644 try_bind_default_missing(
1645 &mut out.flags,
1646 &mut out.flag_awaiting_value,
1647 custom_env,
1648 &mut out.flag_origins,
1649 )?;
1650 }
1651
1652 if w == "--" && !seen_double_dash {
1658 enable_flags = false;
1659
1660 let should_preserve = out
1663 .cmd
1664 .args
1665 .get(next_arg_idx)
1666 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
1667 .unwrap_or(false);
1668
1669 if should_preserve {
1670 } else {
1673 seen_double_dash = true;
1674 trace.record(argv, TokenRole::Separator);
1675
1676 let target = out.cmd.args.iter().position(|arg| {
1682 arg.double_dash == SpecDoubleDashChoices::Required
1683 && !out.args.contains_key(arg)
1684 });
1685 if let Some(target) = target {
1686 if target > next_arg_idx {
1693 next_arg_idx = target;
1694 }
1695 }
1696 continue;
1697 }
1698 }
1699
1700 if enable_flags && w.starts_with("--") {
1702 grouped_flag = false;
1703 let split = w.split_once('=');
1707 let word = split.map(|(word, _)| word).unwrap_or(&w);
1708 let bound_flag = binding.as_ref().map(|(flag, _)| flag);
1709 if let Some(f) = bound_flag.or_else(|| out.available_flags.get(word)) {
1710 let command_level = binding
1711 .as_ref()
1712 .map(|(_, level)| *level)
1713 .unwrap_or(out.cmds.len() - 1);
1714 parsed_flag_spellings
1715 .entry(Arc::as_ptr(f) as usize)
1716 .or_default()
1717 .insert(word.to_string());
1718 trace.record(
1721 argv,
1722 TokenRole::Flag {
1723 flag: Arc::clone(f),
1724 spelling: word.to_string(),
1725 negated: f.negate.as_deref() == Some(word),
1726 },
1727 );
1728 if f.action != crate::SpecFlagAction::Set {
1729 out.errors.push(render_action_err(spec, &out.cmd, f, word));
1730 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1731 return Ok((out, overridden_flags));
1732 }
1733 apply_flag_overrides(
1734 f,
1735 &out.available_flags,
1736 &mut out.flags,
1737 &mut out.flag_awaiting_value,
1738 &mut overridden_flags,
1739 &mut out.overridden_flags,
1740 );
1741 if let Some(pending) = out.flag_awaiting_value.first() {
1742 out.errors.push(render_missing_flag_value(pending, &w));
1743 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1744 return Ok((out, overridden_flags));
1745 }
1746 if f.arg.is_some() {
1752 record_scalar_flag_occurrence(
1753 &out.cmds,
1754 f,
1755 command_level,
1756 None,
1757 &mut scalar_occurrences,
1758 &mut out.errors,
1759 );
1760 let f = Arc::clone(f);
1761 out.flag_awaiting_value.push(Arc::clone(&f));
1762 if let Some((_, val)) = split {
1767 let mut val = val.to_string();
1771 let should_return = bind_pending_flag_value(
1772 spec,
1773 &out.cmd,
1774 &mut out.errors,
1775 &mut out.flags,
1776 &mut out.flag_awaiting_value,
1777 &mut val,
1778 &mut input,
1779 custom_env,
1780 trace,
1781 argv,
1782 true,
1785 )?;
1786 if should_return {
1787 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1788 return Ok((out, overridden_flags));
1789 }
1790 }
1791 } else if f.count {
1792 let arr = out
1793 .flags
1794 .entry(Arc::clone(f))
1795 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1796 .try_as_multi_bool_mut()
1797 .unwrap();
1798 arr.push(true);
1799 } else {
1800 let negate = f.negate.clone().unwrap_or_default();
1801 let negated_form = word == negate;
1802 let value = if f.bool_value {
1803 match split.map(|(_, value)| value) {
1804 Some("true") => !negated_form,
1805 Some("false") => negated_form,
1806 Some(value) => {
1807 out.errors.push(UsageErr::InvalidValue {
1808 name: f.name.clone(),
1809 value: value.to_string(),
1810 reason: "expected `true` or `false`".to_string(),
1811 });
1812 continue;
1813 }
1814 None => !negated_form,
1815 }
1816 } else {
1817 !negated_form
1818 };
1819 record_scalar_flag_occurrence(
1824 &out.cmds,
1825 f,
1826 command_level,
1827 Some(!negated_form),
1828 &mut scalar_occurrences,
1829 &mut out.errors,
1830 );
1831 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1832 }
1833 continue;
1834 }
1835 if is_help_arg(spec, &out.cmd, &w) {
1836 out.errors
1837 .push(render_help_err(spec, &out.cmd, w.len() > 2));
1838 trace.record(
1839 argv,
1840 TokenRole::Builtin {
1841 spelling: w.clone(),
1842 },
1843 );
1844 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1845 return Ok((out, overridden_flags));
1846 }
1847 if is_version_arg(spec, &out.cmds, &w) {
1848 out.errors.push(render_version_err(spec, w.len() > 2));
1849 trace.record(
1850 argv,
1851 TokenRole::Builtin {
1852 spelling: w.clone(),
1853 },
1854 );
1855 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1856 return Ok((out, overridden_flags));
1857 }
1858 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1859 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1860 trace.close(&input);
1861 return Err(refused.into());
1862 }
1863 }
1864
1865 let declared_numeric_short = declared_numeric_short(&out.available_flags, &w);
1872 let positional_negative_number = !declared_numeric_short
1873 && is_negative_number(&w)
1874 && out
1875 .cmd
1876 .args
1877 .get(next_arg_idx)
1878 .is_some_and(|arg| arg.allow_negative_numbers);
1879 if enable_flags
1880 && !grouped_flag
1881 && binding.is_none()
1884 && w.starts_with('-')
1885 && w.len() > 1
1886 && is_flag_like(&w)
1887 && !positional_negative_number
1888 && !short_bundle_is_known(spec, &out.cmds, &out.available_flags, &w)
1889 {
1890 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
1893 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
1894 trace.close(&input);
1895 return Err(refused.into());
1896 }
1897 } else if enable_flags && !positional_negative_number && w.starts_with('-') && w.len() > 1 {
1898 let short = w.chars().nth(1).unwrap();
1899 if let Some(f) = binding
1900 .as_ref()
1901 .map(|(flag, _)| flag)
1902 .or_else(|| out.available_flags.get(&format!("-{short}")))
1903 {
1904 let command_level = binding
1905 .as_ref()
1906 .map(|(_, level)| *level)
1907 .unwrap_or(out.cmds.len() - 1);
1908 if f.action != crate::SpecFlagAction::Set {
1909 out.errors
1910 .push(render_action_err(spec, &out.cmd, f, &format!("-{short}")));
1911 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1912 return Ok((out, overridden_flags));
1913 }
1914 parsed_flag_spellings
1915 .entry(Arc::as_ptr(f) as usize)
1916 .or_default()
1917 .insert(format!("-{short}"));
1918 trace.record(
1919 argv,
1920 TokenRole::Flag {
1921 flag: Arc::clone(f),
1922 spelling: format!("-{short}"),
1923 negated: false,
1925 },
1926 );
1927 apply_flag_overrides(
1928 f,
1929 &out.available_flags,
1930 &mut out.flags,
1931 &mut out.flag_awaiting_value,
1932 &mut overridden_flags,
1933 &mut out.overridden_flags,
1934 );
1935 if !attached_continuation {
1936 if let Some(pending) = out.flag_awaiting_value.first() {
1937 out.errors.push(render_missing_flag_value(pending, &w));
1938 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1939 return Ok((out, overridden_flags));
1940 }
1941 }
1942 let rest = &w[1 + short.len_utf8()..];
1943 if !rest.is_empty() {
1944 input.push_front(Token::new(format!("-{rest}"), argv));
1947 }
1948 grouped_flag = !rest.is_empty();
1952 if f.arg.is_some() {
1953 record_scalar_flag_occurrence(
1954 &out.cmds,
1955 f,
1956 command_level,
1957 None,
1958 &mut scalar_occurrences,
1959 &mut out.errors,
1960 );
1961 out.flag_awaiting_value.push(Arc::clone(f));
1962 } else if f.count {
1963 let arr = out
1964 .flags
1965 .entry(Arc::clone(f))
1966 .or_insert_with(|| ParseValue::MultiBool(vec![]))
1967 .try_as_multi_bool_mut()
1968 .unwrap();
1969 arr.push(true);
1970 } else {
1971 let negate = f.negate.clone().unwrap_or_default();
1972 let value = w != negate;
1973 record_scalar_flag_occurrence(
1974 &out.cmds,
1975 f,
1976 command_level,
1977 Some(value),
1978 &mut scalar_occurrences,
1979 &mut out.errors,
1980 );
1981 out.flags.insert(Arc::clone(f), ParseValue::Bool(value));
1982 }
1983 continue;
1984 }
1985 if let Some(err) = supplied_short(spec, &out.cmds, short) {
1989 out.errors.push(err);
1990 trace.record(
1991 argv,
1992 TokenRole::Builtin {
1993 spelling: format!("-{short}"),
1994 },
1995 );
1996 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
1997 return Ok((out, overridden_flags));
1998 }
1999 if is_help_arg(spec, &out.cmd, &w) {
2000 out.errors
2001 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2002 trace.record(
2003 argv,
2004 TokenRole::Builtin {
2005 spelling: w.clone(),
2006 },
2007 );
2008 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2009 return Ok((out, overridden_flags));
2010 }
2011 if is_version_arg(spec, &out.cmds, &w) {
2012 out.errors.push(render_version_err(spec, w.len() > 2));
2013 trace.record(
2014 argv,
2015 TokenRole::Builtin {
2016 spelling: w.clone(),
2017 },
2018 );
2019 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2020 return Ok((out, overridden_flags));
2021 }
2022 if let Err(refused) = reject_unknown_flag_if_asked(spec, &out.cmds, &w) {
2023 trace.record(argv, TokenRole::UnknownFlag { bound_as: None });
2024 trace.close(&input);
2025 return Err(refused.into());
2026 }
2027 if grouped_flag {
2028 grouped_flag = false;
2029 w.remove(0);
2030 if !out.flag_awaiting_value.is_empty() && w.starts_with('=') {
2034 w.remove(0);
2035 }
2036 }
2037 }
2038
2039 if enable_flags
2050 && !attached_continuation
2051 && !out.flag_awaiting_value.is_empty()
2052 && out
2053 .flag_awaiting_value
2054 .last()
2055 .is_some_and(|flag| flag.require_equals)
2056 {
2057 let flag = out.flag_awaiting_value.last().unwrap();
2058 let token = flag
2059 .long
2060 .first()
2061 .map(|l| format!("--{l}"))
2062 .or_else(|| flag.short.first().map(|s| format!("-{s}")))
2063 .unwrap_or_else(|| flag.name.clone());
2064 out.errors.push(UsageErr::InvalidFlag {
2065 token: token.clone(),
2066 reason: "requires an argument".to_string(),
2067 span: (0, 0).into(),
2068 input: format!("{token} {w}"),
2069 });
2070 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2071 return Ok((out, overridden_flags));
2072 }
2073 if enable_flags
2074 && !out.flag_awaiting_value.is_empty()
2075 && (attached_continuation
2076 || out
2077 .flag_awaiting_value
2078 .last()
2079 .is_some_and(|flag| accepts_detached_flag_value(flag, &w)))
2080 {
2081 let should_return = bind_pending_flag_value(
2084 spec,
2085 &out.cmd,
2086 &mut out.errors,
2087 &mut out.flags,
2088 &mut out.flag_awaiting_value,
2089 &mut w,
2090 &mut input,
2091 custom_env,
2092 trace,
2093 argv,
2094 attached_continuation,
2095 )?;
2096 if should_return {
2097 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2098 return Ok((out, overridden_flags));
2099 }
2100 continue;
2101 }
2102
2103 if out.cmd.allow_missing_positional {
2104 while let Some(current) = out.cmd.args.get(next_arg_idx) {
2105 if current.required || out.args.contains_key(current) {
2106 break;
2107 }
2108 let required_after = out.cmd.args[next_arg_idx + 1..]
2109 .iter()
2110 .filter(|arg| arg.required)
2111 .count();
2112 if required_after == 0 {
2113 break;
2114 }
2115 let remaining_values = 1 + input
2116 .iter()
2117 .filter(|token| !enable_flags || !is_flag_like(&token.word))
2118 .count();
2119 if remaining_values > required_after {
2120 break;
2121 }
2122 next_arg_idx += 1;
2123 }
2124 }
2125
2126 if let Some(arg) = out.cmd.args.get(next_arg_idx) {
2127 if arg.var
2128 && out.args.contains_key(arg)
2129 && arg.value_terminator.as_deref() == Some(w.as_str())
2130 {
2131 trace.record(
2132 argv,
2133 TokenRole::ValueTerminator {
2134 ends: arg.name.clone(),
2135 },
2136 );
2137 next_arg_idx += 1;
2138 continue;
2139 }
2140 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
2144 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
2145 trace.record(
2146 argv,
2147 TokenRole::Refused {
2148 reason: format!("{} only accepts words after `--`", arg.name),
2149 },
2150 );
2151 continue;
2155 }
2156 let trailing_value =
2161 seen_double_dash || arg.double_dash == SpecDoubleDashChoices::Automatic;
2162 let suppress_trailing_delimiter =
2163 out.cmds.iter().any(|cmd| cmd.dont_delimit_trailing_values);
2164 let delimiter = if suppress_trailing_delimiter && trailing_value {
2165 None
2166 } else {
2167 arg.delimiter
2168 };
2169 let parts: Vec<String> = match delimiter {
2170 Some(delimiter) => w.split(delimiter).map(str::to_string).collect(),
2171 None => vec![w.clone()],
2172 };
2173 let mut refused = false;
2174 for part in &parts {
2175 if validate_choices(
2176 spec,
2177 &out.cmd,
2178 &mut out.errors,
2179 ChoiceTarget::arg(arg),
2180 part,
2181 arg.choices.as_ref(),
2182 custom_env,
2183 )? {
2184 refused = true;
2185 break;
2186 }
2187 }
2188 if refused {
2189 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2190 return Ok((out, overridden_flags));
2191 }
2192 if arg.double_dash == SpecDoubleDashChoices::Automatic {
2197 enable_flags = false;
2198 }
2199 let unknown_flag = enable_flags
2204 && !positional_negative_number
2205 && is_flag_like(&w)
2206 && binding.is_none();
2207 trace.record(
2208 argv,
2209 if unknown_flag {
2210 TokenRole::UnknownFlag {
2211 bound_as: Some(Arc::new(arg.clone())),
2212 }
2213 } else {
2214 TokenRole::Arg {
2215 arg: Arc::new(arg.clone()),
2216 values: parts.clone(),
2217 }
2218 },
2219 );
2220 if arg.var {
2221 let arr = out
2222 .args
2223 .entry(Arc::new(arg.clone()))
2224 .or_insert_with(|| ParseValue::MultiString(vec![]))
2225 .try_as_multi_string_mut()
2226 .unwrap();
2227 arr.extend(parts.iter().cloned());
2231 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
2232 next_arg_idx += 1;
2233 }
2234 } else {
2235 out.args
2236 .insert(Arc::new(arg.clone()), ParseValue::String(w));
2237 next_arg_idx += 1;
2238 }
2239 continue;
2240 }
2241 if is_help_arg(spec, &out.cmd, &w) {
2242 out.errors
2243 .push(render_help_err(spec, &out.cmd, w.len() > 2));
2244 trace.record(
2245 argv,
2246 TokenRole::Builtin {
2247 spelling: w.clone(),
2248 },
2249 );
2250 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2251 return Ok((out, overridden_flags));
2252 }
2253 if is_version_arg(spec, &out.cmds, &w) {
2254 out.errors.push(render_version_err(spec, w.len() > 2));
2255 trace.record(
2256 argv,
2257 TokenRole::Builtin {
2258 spelling: w.clone(),
2259 },
2260 );
2261 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2262 return Ok((out, overridden_flags));
2263 }
2264 trace.record(
2265 argv,
2266 TokenRole::Refused {
2267 reason: "no declaration takes this word".to_string(),
2268 },
2269 );
2270 trace.close(&input);
2271 bail!("unexpected word: {w}");
2272 }
2273
2274 record_stop(&mut out, next_arg_idx, seen_double_dash, trace, &input);
2275
2276 let flag_was_parsed =
2281 |flag: &Arc<SpecFlag>| parsed_flag_spellings.contains_key(&(Arc::as_ptr(flag) as usize));
2282
2283 let child_spellings = |flag: &Arc<SpecFlag>| -> HashSet<String> {
2291 let declared: HashSet<String> = out
2292 .cmd
2293 .flags
2294 .iter()
2295 .filter(|declared| declared.name == flag.name)
2296 .flat_map(flag_keys)
2297 .collect();
2298 let speaks_for_this_flag = declared.iter().any(|spelling| {
2308 out.available_flags
2309 .get(spelling)
2310 .is_some_and(|available| Arc::ptr_eq(available, flag))
2311 });
2312 if speaks_for_this_flag {
2313 declared
2314 } else {
2315 HashSet::new()
2316 }
2317 };
2318
2319 let exclusivity_in_play = |flag: &Arc<SpecFlag>| -> (bool, bool) {
2327 let child = child_spellings(flag);
2328 let child_exclusive = !child.is_empty()
2329 && out
2330 .cmd
2331 .flags
2332 .iter()
2333 .any(|declared| declared.name == flag.name && declared.exclusive);
2334 match parsed_flag_spellings.get(&(Arc::as_ptr(flag) as usize)) {
2335 Some(spellings) => (
2336 child_exclusive && spellings.iter().any(|s| child.contains(s)),
2337 flag.exclusive && spellings.iter().any(|s| !child.contains(s)),
2338 ),
2339 None => (child_exclusive, flag.exclusive && child.is_empty()),
2343 }
2344 };
2345
2346 let exclusive_occurrence = |flag: &Arc<SpecFlag>| {
2347 let (child, ancestor) = exclusivity_in_play(flag);
2348 child || ancestor
2349 };
2350
2351 let exclusive_present =
2356 unique_flags(out.available_flags.values().chain(out.flags.keys())).any(|flag| {
2357 exclusive_occurrence(flag)
2358 && !overridden_flags.contains(&flag.name)
2359 && (flag_was_parsed(flag) || flag_has_env(flag, custom_env))
2360 });
2361 let requirements_apply = |command_index: usize| {
2362 command_index + 1 == out.cmds.len() || !out.cmds[command_index].subcommand_negates_reqs
2363 };
2364
2365 if out.cmd.arg_required_else_help && !command_has_argv {
2366 out.errors.push(render_help_err(spec, &out.cmd, false));
2367 }
2368
2369 if out.cmd.subcommand_required && !out.cmd.subcommands.is_empty() && out.external.is_none() {
2375 let mut names: Vec<&str> = out
2376 .cmd
2377 .subcommands
2378 .iter()
2379 .filter(|(name, sub)| sub.name == **name && !sub.hide)
2382 .map(|(name, _)| name.as_str())
2383 .collect();
2384 names.sort_unstable();
2385 out.errors.push(UsageErr::MissingSubcommand(
2386 out.cmd.name.clone(),
2387 names.join(", "),
2388 ));
2389 }
2390
2391 if !exclusive_present {
2394 for arg in out
2395 .cmds
2396 .iter()
2397 .enumerate()
2398 .filter(|(index, _)| requirements_apply(*index))
2399 .flat_map(|(_, cmd)| &cmd.args)
2400 {
2401 if out.args.contains_key(arg) {
2402 continue;
2403 }
2404 if double_dash_violations.contains(&arg.name) {
2406 continue;
2407 }
2408 let required_if = arg.required_if.iter().any(|selector| {
2409 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2410 });
2411 let required_if_eq = arg.required_if_eq.iter().any(|condition| {
2412 selector_explicit_has_value(
2413 &condition.selector,
2414 &condition.value,
2415 &out,
2416 &overridden_flags,
2417 custom_env,
2418 )
2419 });
2420 let required_if_eq_all = !arg.required_if_eq_all.is_empty()
2421 && arg.required_if_eq_all.iter().all(|condition| {
2422 selector_explicit_has_value(
2423 &condition.selector,
2424 &condition.value,
2425 &out,
2426 &overridden_flags,
2427 custom_env,
2428 )
2429 });
2430 let unless_any = arg.required_unless.iter().any(|selector| {
2431 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2432 });
2433 let unless_all = !arg.required_unless_all.is_empty()
2434 && arg.required_unless_all.iter().all(|selector| {
2435 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2436 });
2437 let required_unless = !(unless_any
2438 || unless_all
2439 || (arg.required_unless.is_empty() && arg.required_unless_all.is_empty()));
2440 if (arg.required
2441 || required_if
2442 || required_if_eq
2443 || required_if_eq_all
2444 || required_unless)
2445 && arg.default.is_empty()
2446 {
2447 let has_env = arg
2449 .env
2450 .as_ref()
2451 .is_some_and(|env_var| env_contains(custom_env, env_var));
2452 if !has_env {
2453 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
2454 }
2455 }
2456 }
2457 }
2458
2459 for flag in unique_flags(out.available_flags.values()) {
2471 let given = out.flags.contains_key(flag) || flag_has_env(flag, custom_env);
2472 if !given || overridden_flags.contains(&flag.name) {
2473 continue;
2474 }
2475 for other in &flag.conflicts {
2476 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2477 out.errors.push(UsageErr::InvalidFlag {
2478 token: format!("--{}", flag.name),
2479 reason: format!("conflicts with {other}"),
2480 span: (0, 0).into(),
2481 input: format!("--{} {other}", flag.name),
2482 });
2483 }
2484 }
2485 let owner = out
2496 .cmds
2497 .iter()
2498 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2499 .unwrap_or(out.cmds.len() - 1);
2500 if !exclusive_present && requirements_apply(owner) {
2501 for other in &flag.requires {
2502 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2503 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2504 if other.starts_with('-') {
2505 out.errors.push(UsageErr::MissingFlag(name));
2506 } else {
2507 out.errors.push(UsageErr::MissingArg(name));
2508 }
2509 }
2510 }
2511 for condition in &flag.requires_if {
2512 if explicit_flag_has_value(flag, &condition.value, &out, custom_env)
2513 && !selector_is_satisfied(
2514 &condition.requires,
2515 &out,
2516 &overridden_flags,
2517 custom_env,
2518 )
2519 {
2520 let name = selector_flag_name(&condition.requires, &out)
2521 .unwrap_or_else(|| condition.requires.clone());
2522 out.errors.push(UsageErr::MissingFlag(name));
2523 }
2524 }
2525 }
2526 }
2527
2528 for (command_index, arg) in out
2531 .cmds
2532 .iter()
2533 .enumerate()
2534 .flat_map(|(index, cmd)| cmd.args.iter().map(move |arg| (index, arg)))
2535 {
2536 let given = arg_is_explicit(arg, &out, custom_env);
2537 if !given {
2538 continue;
2539 }
2540 for other in &arg.conflicts {
2541 if selector_is_explicit(other, &out, &overridden_flags, custom_env) {
2542 out.errors.push(UsageErr::InvalidFlag {
2543 token: arg.name.clone(),
2544 reason: format!("conflicts with {other}"),
2545 span: (0, 0).into(),
2546 input: format!("{} {other}", arg.name),
2547 });
2548 }
2549 }
2550 if !exclusive_present && requirements_apply(command_index) {
2551 for other in &arg.requires {
2552 if !selector_is_satisfied(other, &out, &overridden_flags, custom_env) {
2553 let name = selector_flag_name(other, &out).unwrap_or_else(|| other.clone());
2554 if other.starts_with('-') {
2555 out.errors.push(UsageErr::MissingFlag(name));
2556 } else {
2557 out.errors.push(UsageErr::MissingArg(name));
2558 }
2559 }
2560 }
2561 }
2562 }
2563
2564 for flag in unique_flags(out.available_flags.values().chain(out.flags.keys())) {
2574 let given = flag_was_parsed(flag) || flag_has_env(flag, custom_env);
2579 if !exclusive_occurrence(flag) || !given || overridden_flags.contains(&flag.name) {
2580 continue;
2581 }
2582 let other_flag = unique_flags(out.available_flags.values().chain(out.flags.keys()))
2583 .find(|other| {
2584 !Arc::ptr_eq(other, flag)
2585 && !overridden_flags.contains(&other.name)
2586 && (flag_was_parsed(other) || flag_has_env(other, custom_env))
2587 })
2588 .map(|other| format!("--{}", other.name));
2589 let other_arg = out.cmd.args.iter().find(|arg| {
2590 out.args.keys().any(|given| given.name == arg.name)
2591 || arg
2592 .env
2593 .as_ref()
2594 .is_some_and(|env| env_contains(custom_env, env))
2595 });
2596 let (_, ancestor_exclusivity) = exclusivity_in_play(flag);
2602 let selected_subcommand =
2603 (out.cmds.len() > 1 && ancestor_exclusivity).then(|| out.cmd.name.clone());
2604 let other = other_flag
2605 .or_else(|| other_arg.map(|arg| format!("<{}>", arg.name)))
2606 .or(selected_subcommand);
2607 if let Some(other) = other {
2608 out.errors.push(UsageErr::InvalidFlag {
2609 token: format!("--{}", flag.name),
2610 reason: format!("must be given on its own, and {other} was given too"),
2611 span: (0, 0).into(),
2612 input: format!("--{} {other}", flag.name),
2613 });
2614 }
2615 }
2616
2617 let mut group_errors: Vec<UsageErr> = Vec::new();
2626 for (command_index, group) in out
2627 .cmds
2628 .iter()
2629 .enumerate()
2630 .flat_map(|(index, cmd)| cmd.groups.iter().map(move |group| (index, group)))
2631 {
2632 let mut given: Vec<&str> = Vec::new();
2638 let mut seen: Vec<String> = Vec::new();
2639 for selector in &group.members {
2640 if !selector_is_explicit(selector, &out, &overridden_flags, custom_env) {
2641 continue;
2642 }
2643 let name = selector_flag_name(selector, &out).unwrap_or_else(|| selector.clone());
2644 if seen.contains(&name) {
2645 continue;
2646 }
2647 seen.push(name);
2648 given.push(selector.as_str());
2649 }
2650 if !group.multiple && given.len() > 1 {
2651 group_errors.push(UsageErr::InvalidFlag {
2652 token: given[1].to_string(),
2653 reason: format!("cannot be used with {} in group {}", given[0], group.name),
2654 span: (0, 0).into(),
2655 input: format!("{} {}", given[0], given[1]),
2656 });
2657 }
2658 let satisfied = group
2663 .members
2664 .iter()
2665 .any(|selector| selector_is_satisfied(selector, &out, &overridden_flags, custom_env));
2666 if group.required && requirements_apply(command_index) && !satisfied && !exclusive_present {
2667 group_errors.push(UsageErr::MissingGroup {
2671 group: group.name.clone(),
2672 members: group.members.join(", "),
2673 });
2674 }
2675 }
2676 out.errors.extend(group_errors);
2677
2678 if !exclusive_present {
2679 for flag in unique_flags(out.available_flags.values()) {
2680 let owner = out
2681 .cmds
2682 .iter()
2683 .rposition(|cmd| cmd.flags.iter().any(|declared| declared.name == flag.name))
2684 .unwrap_or(out.cmds.len() - 1);
2685 if !requirements_apply(owner) {
2686 continue;
2687 }
2688 if out.flags.contains_key(flag) || overridden_flags.contains(&flag.name) {
2689 continue;
2690 }
2691 let has_default =
2692 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
2693 let has_env = flag_has_env(flag, custom_env);
2694 let required_if = flag.required_if.iter().any(|selector| {
2695 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2696 });
2697 let required_if_eq = flag.required_if_eq.iter().any(|condition| {
2698 selector_explicit_has_value(
2699 &condition.selector,
2700 &condition.value,
2701 &out,
2702 &overridden_flags,
2703 custom_env,
2704 )
2705 });
2706 let required_if_eq_all = !flag.required_if_eq_all.is_empty()
2707 && flag.required_if_eq_all.iter().all(|condition| {
2708 selector_explicit_has_value(
2709 &condition.selector,
2710 &condition.value,
2711 &out,
2712 &overridden_flags,
2713 custom_env,
2714 )
2715 });
2716 let unless_any = flag.required_unless.iter().any(|selector| {
2717 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2718 });
2719 let unless_all = !flag.required_unless_all.is_empty()
2720 && flag.required_unless_all.iter().all(|selector| {
2721 selector_is_explicit(selector, &out, &overridden_flags, custom_env)
2722 });
2723 let required_unless = !(unless_any
2724 || unless_all
2725 || (flag.required_unless.is_empty() && flag.required_unless_all.is_empty()));
2726 if (flag.required
2727 || required_if
2728 || required_if_eq
2729 || required_if_eq_all
2730 || required_unless)
2731 && !has_default
2732 && !has_env
2733 {
2734 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
2735 }
2736 }
2737 }
2738
2739 for (arg, value) in &out.args {
2741 if arg.var {
2742 if let ParseValue::MultiString(values) = value {
2743 if let Some(min) = arg.var_min {
2744 if values.len() < min {
2745 out.errors.push(UsageErr::VarArgTooFew {
2746 name: arg.name.clone(),
2747 min,
2748 got: values.len(),
2749 });
2750 }
2751 }
2752 if let Some(max) = arg.var_max {
2753 if values.len() > max {
2754 out.errors.push(UsageErr::VarArgTooMany {
2755 name: arg.name.clone(),
2756 max,
2757 got: values.len(),
2758 });
2759 }
2760 }
2761 }
2762 }
2763 }
2764
2765 for flag in unique_flags(out.available_flags.values()) {
2769 if flag.var {
2770 let bound = match out.flags.get(flag) {
2771 Some(ParseValue::MultiString(values)) => values.len(),
2772 Some(ParseValue::MultiBool(values)) => values.len(),
2773 Some(_) => 1,
2774 None => 0,
2775 };
2776 let pending = out
2780 .flag_awaiting_value
2781 .iter()
2782 .filter(|pending| {
2783 Arc::ptr_eq(pending, flag)
2784 && (pending.value_optional || pending.default_missing.is_some())
2785 })
2786 .count();
2787 let count = bound + pending;
2788 if count == 0 {
2789 continue;
2790 }
2791 if let Some(min) = flag.var_min {
2792 if count < min {
2793 out.errors.push(UsageErr::VarFlagTooFew {
2794 name: flag.name.clone(),
2795 min,
2796 got: count,
2797 });
2798 }
2799 }
2800 if let Some(max) = flag.var_max {
2801 if count > max {
2802 out.errors.push(UsageErr::VarFlagTooMany {
2803 name: flag.name.clone(),
2804 max,
2805 got: count,
2806 });
2807 }
2808 }
2809 }
2810 }
2811
2812 Ok((out, overridden_flags))
2813}
2814
2815fn validate_expression(
2816 name: &str,
2817 expression: Option<&str>,
2818 message: Option<&str>,
2819 parsed: &ParseValue,
2820 errors: &mut Vec<UsageErr>,
2821) {
2822 let Some(expression) = expression else {
2823 return;
2824 };
2825 #[cfg(not(feature = "validation"))]
2826 let _ = expression;
2827 let values: &[String] = match parsed {
2828 ParseValue::String(value) => std::slice::from_ref(value),
2829 ParseValue::MultiString(values) => values,
2830 ParseValue::Bool(_) | ParseValue::MultiBool(_) => return,
2831 };
2832 #[cfg(feature = "validation")]
2833 for value in values {
2834 let reason = match usage_validation::validate(expression, value) {
2835 Ok(true) => continue,
2836 Ok(false) => message
2837 .unwrap_or("does not satisfy the validation expression")
2838 .to_string(),
2839 Err(error) => format!("validation expression failed: {error}"),
2840 };
2841 errors.push(UsageErr::InvalidValue {
2842 name: name.to_string(),
2843 value: value.clone(),
2844 reason,
2845 });
2846 break;
2847 }
2848 #[cfg(not(feature = "validation"))]
2849 if let Some(value) = values.first() {
2850 let _ = message;
2851 errors.push(UsageErr::InvalidValue {
2852 name: name.to_string(),
2853 value: value.clone(),
2854 reason: "expression validation requires the `validation` feature".to_string(),
2855 });
2856 }
2857}
2858
2859#[cfg(all(test, not(feature = "validation")))]
2860mod optional_validation_tests {
2861 use crate::{parse, Spec};
2862
2863 #[test]
2864 fn validation_declarations_require_the_opt_in_runtime_feature() {
2865 let spec: Spec = r#"
2866name "ex"
2867bin "ex"
2868arg "<port>" validate="int(value) > 0"
2869 "#
2870 .parse()
2871 .unwrap();
2872 let error = parse(&spec, &["ex".to_string(), "1".to_string()]).unwrap_err();
2873 assert!(
2874 error
2875 .to_string()
2876 .contains("requires the `validation` feature"),
2877 "{error:?}"
2878 );
2879 }
2880}
2881
2882fn flag_matches_selector(flag: &SpecFlag, selector: &str) -> bool {
2883 flag.name == selector || flag_keys(flag).iter().any(|key| key == selector)
2884}
2885
2886fn flags_override(overrider: &SpecFlag, overridden: &SpecFlag) -> bool {
2887 overrider
2888 .overrides
2889 .iter()
2890 .any(|selector| flag_matches_selector(overridden, selector))
2891}
2892
2893fn apply_prefix_flag_overrides(
2894 prefix_flags: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
2895 flag: Arc<SpecFlag>,
2896) {
2897 prefix_flags
2898 .retain(|(other, _)| !(flags_override(&flag, other) || flags_override(other, &flag)));
2899}
2900
2901fn mount_prefix_words(prefix_flags: &[(Arc<SpecFlag>, Vec<String>)]) -> Vec<String> {
2902 prefix_flags
2903 .iter()
2904 .flat_map(|(_, words)| words.iter().cloned())
2905 .collect()
2906}
2907
2908fn env_contains(custom_env: Option<&HashMap<String, String>>, env_var: &str) -> bool {
2909 match custom_env {
2910 Some(env) => env.contains_key(env_var),
2911 None => std::env::var(env_var).is_ok(),
2912 }
2913}
2914
2915fn flag_has_env(flag: &SpecFlag, custom_env: Option<&HashMap<String, String>>) -> bool {
2916 flag.env_names()
2917 .any(|env_var| env_contains(custom_env, env_var))
2918}
2919
2920fn fallback_is_true(value: &str) -> bool {
2921 matches!(value, "1" | "true" | "True" | "TRUE")
2922}
2923
2924fn split_fallback_values(values: &[String], delimiter: Option<char>) -> Vec<String> {
2925 match delimiter {
2926 Some(delimiter) => values
2927 .iter()
2928 .flat_map(|value| value.split(delimiter).map(str::to_string))
2929 .collect(),
2930 None => values.to_vec(),
2931 }
2932}
2933
2934fn validate_arg_fallback_count(arg: &SpecArg, count: usize, errors: &mut Vec<UsageErr>) {
2935 if let Some(min) = arg.var_min {
2936 if count < min {
2937 errors.push(UsageErr::VarArgTooFew {
2938 name: arg.name.clone(),
2939 min,
2940 got: count,
2941 });
2942 }
2943 }
2944 if let Some(max) = arg.var_max {
2945 if count > max {
2946 errors.push(UsageErr::VarArgTooMany {
2947 name: arg.name.clone(),
2948 max,
2949 got: count,
2950 });
2951 }
2952 }
2953}
2954
2955fn validate_flag_fallback_count(flag: &SpecFlag, count: usize, errors: &mut Vec<UsageErr>) {
2956 if let Some(min) = flag.var_min {
2957 if count < min {
2958 errors.push(UsageErr::VarFlagTooFew {
2959 name: flag.name.clone(),
2960 min,
2961 got: count,
2962 });
2963 }
2964 }
2965 if let Some(max) = flag.var_max {
2966 if count > max {
2967 errors.push(UsageErr::VarFlagTooMany {
2968 name: flag.name.clone(),
2969 max,
2970 got: count,
2971 });
2972 }
2973 }
2974}
2975
2976fn validate_flag_arg_fallback_count(
2977 flag: &SpecFlag,
2978 arg: &SpecArg,
2979 count: usize,
2980 errors: &mut Vec<UsageErr>,
2981) {
2982 if let Some(min) = arg.var_min {
2983 if count < min {
2984 errors.push(UsageErr::VarFlagTooFew {
2985 name: flag.name.clone(),
2986 min,
2987 got: count,
2988 });
2989 }
2990 }
2991 if let Some(max) = arg.var_max {
2992 if count > max {
2993 errors.push(UsageErr::VarFlagTooMany {
2994 name: flag.name.clone(),
2995 max,
2996 got: count,
2997 });
2998 }
2999 }
3000}
3001
3002fn bind_flag_fallback(
3005 flag: &Arc<SpecFlag>,
3006 values: &[String],
3007 out: &mut ParseOutput,
3008 custom_env: Option<&HashMap<String, String>>,
3009 origin: ValueOrigin,
3010) -> Result<(), miette::Error> {
3011 if values.is_empty() {
3012 return Ok(());
3013 }
3014 if let Some(arg) = flag.arg.as_ref() {
3015 let values = split_fallback_values(values, arg.delimiter);
3016 if flag.var || arg.var {
3017 if flag.var {
3018 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3019 }
3020 if arg.var {
3021 validate_flag_arg_fallback_count(flag, arg, values.len(), &mut out.errors);
3022 }
3023 validate_choice_values(
3024 ChoiceTarget::option(flag),
3025 &values,
3026 arg.choices.as_ref(),
3027 custom_env,
3028 )?;
3029 out.flags
3030 .insert(Arc::clone(flag), ParseValue::MultiString(values));
3031 } else {
3032 let value = values.into_iter().next().unwrap_or_default();
3033 validate_choice_value(
3034 ChoiceTarget::option(flag),
3035 &value,
3036 arg.choices.as_ref(),
3037 custom_env,
3038 )?;
3039 out.flags
3040 .insert(Arc::clone(flag), ParseValue::String(value));
3041 }
3042 } else if flag.var {
3043 validate_flag_fallback_count(flag, values.len(), &mut out.errors);
3044 let bools: Vec<bool> = values.iter().map(|s| fallback_is_true(s)).collect();
3045 out.flags
3046 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
3047 } else {
3048 out.flags.insert(
3049 Arc::clone(flag),
3050 ParseValue::Bool(fallback_is_true(&values[0])),
3051 );
3052 }
3053 out.flag_origins
3054 .entry(Arc::clone(flag))
3055 .or_default()
3056 .push(origin);
3057 Ok(())
3058}
3059
3060fn default_if_condition_matches(
3061 condition: &crate::SpecDefaultIf,
3062 out: &ParseOutput,
3063 overridden_flags: &HashSet<String>,
3064 custom_env: Option<&HashMap<String, String>>,
3065) -> bool {
3066 match &condition.when {
3067 None => selector_is_explicit(&condition.selector, out, overridden_flags, custom_env),
3068 Some(when) => {
3069 let Some(flag) = out
3070 .available_flags
3071 .values()
3072 .chain(out.flags.keys())
3073 .find(|flag| flag_matches_selector(flag, &condition.selector))
3074 else {
3075 return false;
3076 };
3077 if overridden_flags.contains(&flag.name) {
3078 return false;
3079 }
3080 explicit_flag_has_value(flag, when, out, custom_env)
3081 }
3082 }
3083}
3084
3085fn explicit_flag_has_value(
3091 flag: &SpecFlag,
3092 expected: &str,
3093 out: &ParseOutput,
3094 custom_env: Option<&HashMap<String, String>>,
3095) -> bool {
3096 let parsed_matches = out.flags.get(flag).is_some_and(|value| match value {
3097 ParseValue::Bool(value) => value.to_string() == expected,
3098 ParseValue::String(value) => value == expected,
3099 ParseValue::MultiBool(values) => values.iter().any(|value| value.to_string() == expected),
3100 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3101 });
3102 if out.flags.contains_key(flag) {
3103 return parsed_matches;
3104 }
3105
3106 let value = flag.env_names().find_map(|env| match custom_env {
3107 Some(values) => values.get(env).cloned(),
3108 None => std::env::var(env).ok(),
3109 });
3110 value.is_some_and(
3111 |value| match flag.arg.as_ref().and_then(|arg| arg.delimiter) {
3112 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3113 None if flag.arg.is_none() => {
3114 matches!(value.as_str(), "1" | "true" | "True" | "TRUE").to_string() == expected
3115 }
3116 None => value == expected,
3117 },
3118 )
3119}
3120
3121fn selector_explicit_has_value(
3122 selector: &str,
3123 expected: &str,
3124 out: &ParseOutput,
3125 overridden_flags: &HashSet<String>,
3126 custom_env: Option<&HashMap<String, String>>,
3127) -> bool {
3128 if let Some(flag) = out
3129 .available_flags
3130 .values()
3131 .chain(out.flags.keys())
3132 .find(|flag| flag_matches_selector(flag, selector))
3133 {
3134 return !overridden_flags.contains(&flag.name)
3135 && explicit_flag_has_value(flag, expected, out, custom_env);
3136 }
3137 let Some(arg) = selector_arg(selector, out) else {
3138 return false;
3139 };
3140 let parsed = out
3141 .args
3142 .iter()
3143 .find(|(given, _)| given.name == arg.name)
3144 .map(|(_, value)| value);
3145 if let Some(value) = parsed {
3146 return match value {
3147 ParseValue::String(value) => value == expected,
3148 ParseValue::MultiString(values) => values.iter().any(|value| value == expected),
3149 ParseValue::Bool(value) => value.to_string() == expected,
3150 ParseValue::MultiBool(values) => {
3151 values.iter().any(|value| value.to_string() == expected)
3152 }
3153 };
3154 }
3155 let value = arg.env_names().find_map(|env| match custom_env {
3156 Some(values) => values.get(env).cloned(),
3157 None => std::env::var(env).ok(),
3158 });
3159 value.is_some_and(|value| match arg.delimiter {
3160 Some(delimiter) => value.split(delimiter).any(|value| value == expected),
3161 None => value == expected,
3162 })
3163}
3164
3165fn selector_is_explicit(
3166 selector: &str,
3167 out: &ParseOutput,
3168 overridden_flags: &HashSet<String>,
3169 custom_env: Option<&HashMap<String, String>>,
3170) -> bool {
3171 let flag_is_explicit = out
3172 .available_flags
3173 .values()
3174 .chain(out.flags.keys())
3175 .any(|flag| {
3176 flag_matches_selector(flag, selector)
3177 && !overridden_flags.contains(&flag.name)
3178 && (out.flags.contains_key(flag) || flag_has_env(flag, custom_env))
3179 });
3180 flag_is_explicit
3181 || selector_arg(selector, out).is_some_and(|arg| arg_is_explicit(arg, out, custom_env))
3182}
3183
3184fn selector_flag_name(selector: &str, out: &ParseOutput) -> Option<String> {
3190 out.available_flags
3191 .values()
3192 .chain(out.flags.keys())
3193 .find(|flag| flag_matches_selector(flag, selector))
3194 .map(|flag| flag.name.clone())
3195 .or_else(|| selector_arg(selector, out).map(|arg| arg.name.clone()))
3196}
3197
3198fn selector_is_satisfied(
3208 selector: &str,
3209 out: &ParseOutput,
3210 overridden_flags: &HashSet<String>,
3211 custom_env: Option<&HashMap<String, String>>,
3212) -> bool {
3213 if selector_is_explicit(selector, out, overridden_flags, custom_env) {
3214 return true;
3215 }
3216 let flag_is_satisfied = out
3217 .available_flags
3218 .values()
3219 .chain(out.flags.keys())
3220 .filter(|flag| flag_matches_selector(flag, selector))
3221 .any(|flag| {
3222 !overridden_flags.contains(&flag.name)
3223 && (!flag.default.is_empty()
3224 || flag.arg.iter().any(|a| !a.default.is_empty())
3225 || flag.default_if.iter().any(|condition| {
3226 default_if_condition_matches(condition, out, overridden_flags, custom_env)
3227 }))
3228 });
3229 flag_is_satisfied || selector_arg(selector, out).is_some_and(|arg| !arg.default.is_empty())
3230}
3231
3232fn selector_arg<'a>(selector: &str, out: &'a ParseOutput) -> Option<&'a SpecArg> {
3233 if selector.starts_with('-') {
3236 return None;
3237 }
3238 out.cmds
3239 .iter()
3240 .flat_map(|cmd| &cmd.args)
3241 .find(|arg| arg.name == selector)
3242}
3243
3244fn arg_is_explicit(
3245 arg: &SpecArg,
3246 out: &ParseOutput,
3247 custom_env: Option<&HashMap<String, String>>,
3248) -> bool {
3249 out.args.keys().any(|given| given.name == arg.name)
3250 || arg
3251 .env
3252 .as_ref()
3253 .is_some_and(|env| env_contains(custom_env, env))
3254}
3255
3256fn apply_flag_overrides(
3257 flag: &Arc<SpecFlag>,
3258 available_flags: &BTreeMap<String, Arc<SpecFlag>>,
3259 parsed_flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3260 pending_flags: &mut Vec<Arc<SpecFlag>>,
3261 overridden_flags: &mut HashSet<String>,
3262 attributed: &mut BTreeMap<String, String>,
3266) {
3267 let overridden_names: HashSet<String> = available_flags
3268 .values()
3269 .chain(parsed_flags.keys())
3270 .filter(|other| flags_override(flag, other) || flags_override(other, flag))
3271 .map(|other| other.name.clone())
3272 .collect();
3273
3274 parsed_flags.retain(|parsed, _| !overridden_names.contains(&parsed.name));
3275 pending_flags.retain(|pending| !overridden_names.contains(&pending.name));
3276 for name in &overridden_names {
3277 attributed.insert(name.clone(), flag.name.clone());
3278 }
3279 overridden_flags.extend(overridden_names);
3280 overridden_flags.remove(&flag.name);
3282 attributed.remove(&flag.name);
3283}
3284
3285#[cfg(feature = "cli-help")]
3286fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
3287 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
3288}
3289
3290#[cfg(feature = "cli-help")]
3291fn render_help_all_err(spec: &Spec, cmd: &SpecCommand) -> UsageErr {
3292 fn append(out: &mut String, spec: &Spec, cmd: &SpecCommand) {
3293 if !out.is_empty() {
3294 out.push('\n');
3295 }
3296 out.push_str(&docs::cli::render_help(spec, cmd, true));
3297 let mut children: Vec<_> = cmd
3298 .subcommands
3299 .values()
3300 .filter(|child| !child.hide)
3301 .collect();
3302 children.sort_by_key(|child| (child.display_order.unwrap_or(999), child.name.as_str()));
3303 for child in children {
3304 append(out, spec, child);
3305 }
3306 }
3307
3308 let mut out = String::new();
3309 append(&mut out, spec, cmd);
3310 UsageErr::Help(out)
3311}
3312
3313#[cfg(not(feature = "cli-help"))]
3314fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
3315 UsageErr::Help("help".to_string())
3316}
3317
3318#[cfg(not(feature = "cli-help"))]
3319fn render_help_all_err(_spec: &Spec, _cmd: &SpecCommand) -> UsageErr {
3320 UsageErr::Help("help".to_string())
3321}
3322
3323fn render_version_err(spec: &Spec, long: bool) -> UsageErr {
3326 let value = if long {
3327 spec.long_version.as_ref().or(spec.version.as_ref())
3328 } else {
3329 spec.version.as_ref().or(spec.long_version.as_ref())
3330 };
3331 UsageErr::Version(value.cloned().unwrap_or_default())
3332}
3333
3334fn render_action_err(spec: &Spec, cmd: &SpecCommand, flag: &SpecFlag, spelling: &str) -> UsageErr {
3335 use crate::SpecFlagAction;
3336 match flag.action {
3337 SpecFlagAction::Help => render_help_err(spec, cmd, spelling.starts_with("--")),
3338 SpecFlagAction::HelpShort => render_help_err(spec, cmd, false),
3339 SpecFlagAction::HelpLong => render_help_err(spec, cmd, true),
3340 SpecFlagAction::HelpAll => render_help_all_err(spec, cmd),
3341 SpecFlagAction::Version => render_version_err(spec, spelling.starts_with("--")),
3342 SpecFlagAction::Set => unreachable!("binding actions are handled before this helper"),
3343 }
3344}
3345
3346fn render_missing_flag_value(flag: &SpecFlag, following: &str) -> UsageErr {
3348 let token = flag
3349 .long
3350 .first()
3351 .map(|long| format!("--{long}"))
3352 .or_else(|| flag.short.first().map(|short| format!("-{short}")))
3353 .unwrap_or_else(|| flag.name.clone());
3354 UsageErr::InvalidFlag {
3355 token: token.clone(),
3356 reason: "requires an argument".to_string(),
3357 span: (0, 0).into(),
3358 input: format!("{token} {following}"),
3359 }
3360}
3361
3362#[derive(Copy, Clone)]
3363struct ChoiceTarget<'a> {
3364 kind: &'a str,
3365 name: &'a str,
3366}
3367
3368impl<'a> ChoiceTarget<'a> {
3369 fn arg(arg: &'a SpecArg) -> Self {
3370 Self {
3371 kind: "arg",
3372 name: &arg.name,
3373 }
3374 }
3375
3376 fn option(flag: &'a SpecFlag) -> Self {
3377 Self {
3378 kind: "option",
3379 name: &flag.name,
3380 }
3381 }
3382}
3383
3384fn short_bundle_is_known(
3389 spec: &Spec,
3390 cmds: &[SpecCommand],
3391 available: &BTreeMap<String, Arc<SpecFlag>>,
3392 token: &str,
3393) -> bool {
3394 for c in token.chars().skip(1) {
3395 match available.get(&format!("-{c}")) {
3396 None if supplied_short(spec, cmds, c).is_some() => {}
3401 None => return false,
3402 Some(f) if f.arg.is_some() => return true,
3403 Some(_) => {}
3404 }
3405 }
3406 true
3407}
3408
3409fn supplied_short(spec: &Spec, cmds: &[SpecCommand], letter: char) -> Option<UsageErr> {
3419 let cmd = cmds.last()?;
3420 match letter {
3421 'h' if is_help_arg(spec, cmd, "-h") => Some(render_help_err(spec, cmd, false)),
3422 'V' if is_version_arg(spec, cmds, "-V") => Some(render_version_err(spec, false)),
3423 _ => None,
3424 }
3425}
3426
3427fn reject_unknown_flag_if_asked(
3433 spec: &Spec,
3434 path: &[SpecCommand],
3435 token: &str,
3436) -> Result<(), UsageErr> {
3437 if !is_flag_like(token) {
3440 return Ok(());
3441 }
3442 if effective_unknown_flags(spec, path) != UnknownFlags::Error {
3443 return Ok(());
3444 }
3445 Err(UsageErr::InvalidFlag {
3446 token: token.to_string(),
3447 reason: "no such flag".to_string(),
3448 span: (0, 0).into(),
3449 input: token.to_string(),
3450 })
3451}
3452
3453fn effective_unknown_flags(spec: &Spec, path: &[SpecCommand]) -> UnknownFlags {
3459 path.iter()
3460 .rev()
3461 .find_map(|cmd| cmd.unknown_flags)
3462 .or(spec.unknown_flags)
3463 .unwrap_or_default()
3464}
3465
3466fn is_flag_like(token: &str) -> bool {
3472 match token.strip_prefix('-') {
3473 None | Some("") => false,
3474 Some(_) => true,
3475 }
3476}
3477
3478fn is_negative_number(token: &str) -> bool {
3479 token.strip_prefix('-').is_some_and(is_number)
3480}
3481
3482fn accepts_detached_flag_value(flag: &SpecFlag, token: &str) -> bool {
3488 !flag.require_equals
3489 && (!is_flag_like(token)
3490 || flag.allow_hyphen_values()
3491 || (is_negative_number(token)
3492 && (flag
3493 .arg
3494 .as_ref()
3495 .is_some_and(|arg| arg.allow_negative_numbers)
3496 || (flag.default_missing.is_none() && !flag.value_optional))))
3497}
3498
3499fn record_scalar_flag_occurrence(
3500 cmds: &[SpecCommand],
3501 flag: &Arc<SpecFlag>,
3502 command_level: usize,
3503 bool_value: Option<bool>,
3504 occurrences: &mut HashMap<(usize, usize), u8>,
3505 errors: &mut Vec<UsageErr>,
3506) {
3507 let strict = cmds
3508 .get(command_level)
3509 .is_some_and(|cmd| !cmd.args_override_self);
3510 let collects_values = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3511 if !strict || flag.count || collects_values {
3512 return;
3513 }
3514
3515 let bit = match bool_value {
3516 Some(false) if flag.negate.is_some() => 0b10,
3517 _ => 0b01,
3518 };
3519 let key = (Arc::as_ptr(flag) as usize, command_level);
3520 let seen = occurrences.entry(key).or_default();
3521 if *seen & bit != 0 {
3522 errors.push(UsageErr::DuplicateFlag(flag.name.clone()));
3523 }
3524 *seen |= bit;
3525}
3526
3527fn is_command_word(token: &str) -> bool {
3535 (!is_flag_like(token) || is_negative_number(token)) && token != "-"
3536}
3537
3538fn declared_numeric_short(available_flags: &BTreeMap<String, Arc<SpecFlag>>, token: &str) -> bool {
3544 token.len() == 2 && token.as_bytes()[1].is_ascii_digit() && available_flags.contains_key(token)
3545}
3546
3547fn default_accepts_word(cmd: &SpecCommand, default_name: &str, token: &str) -> bool {
3553 !is_negative_number(token)
3554 || cmd
3555 .find_subcommand(default_name)
3556 .and_then(|default| default.args.first())
3557 .is_some_and(|arg| arg.allow_negative_numbers)
3558}
3559
3560fn is_number(rest: &str) -> bool {
3567 let (mantissa, exponent) = match rest.find(['e', 'E']) {
3568 Some(at) => (&rest[..at], Some(&rest[at + 1..])),
3569 None => (rest, None),
3570 };
3571
3572 let mut seen_digit = false;
3573 let mut seen_dot = false;
3574 for c in mantissa.chars() {
3575 match c {
3576 '0'..='9' => seen_digit = true,
3577 '.' if !seen_dot => seen_dot = true,
3578 _ => return false,
3579 }
3580 }
3581 if !seen_digit {
3582 return false;
3583 }
3584
3585 match exponent {
3586 None => true,
3587 Some(exp) => {
3588 let digits = exp
3589 .strip_prefix('+')
3590 .or_else(|| exp.strip_prefix('-'))
3591 .unwrap_or(exp);
3592 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
3593 }
3594 }
3595}
3596
3597#[allow(clippy::too_many_arguments)]
3604fn bind_pending_flag_value(
3605 spec: &Spec,
3606 cmd: &SpecCommand,
3607 errors: &mut Vec<UsageErr>,
3608 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3609 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3610 word: &mut String,
3611 input: &mut VecDeque<Token>,
3612 custom_env: Option<&HashMap<String, String>>,
3613 trace: &mut Trace,
3614 argv: usize,
3618 attached: bool,
3619) -> miette::Result<bool> {
3620 let collecting = flag_awaiting_value
3624 .last()
3625 .filter(|flag| flag.arg.as_ref().is_some_and(|arg| arg.var))
3626 .cloned()
3627 .map(|flag| {
3628 let carried = flags.get(&flag).map(value_count).unwrap_or(0);
3629 (flag, carried)
3630 });
3631 let mut bound = vec![];
3632 let refused = drain_pending_flag_values(
3633 spec,
3634 cmd,
3635 errors,
3636 flags,
3637 flag_awaiting_value,
3638 word,
3639 custom_env,
3640 &mut bound,
3641 )?;
3642 for (flag, values) in bound {
3643 trace.record(
3644 argv,
3645 TokenRole::Value {
3646 flag,
3647 values,
3648 attached,
3649 },
3650 );
3651 }
3652 if refused {
3653 return Ok(true);
3654 }
3655 let Some((flag, carried)) = collecting else {
3656 return Ok(false);
3657 };
3658 collect_variadic_flag_values(
3659 spec,
3660 cmd,
3661 errors,
3662 flags,
3663 flag_awaiting_value,
3664 &flag,
3665 carried,
3666 input,
3667 custom_env,
3668 trace,
3669 )
3670}
3671
3672#[allow(clippy::too_many_arguments)]
3685fn collect_variadic_flag_values(
3686 spec: &Spec,
3687 cmd: &SpecCommand,
3688 errors: &mut Vec<UsageErr>,
3689 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3690 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3691 flag: &Arc<SpecFlag>,
3692 carried: usize,
3693 input: &mut VecDeque<Token>,
3694 custom_env: Option<&HashMap<String, String>>,
3695 trace: &mut Trace,
3696) -> miette::Result<bool> {
3697 let max = flag
3698 .arg
3699 .as_ref()
3700 .and_then(|arg| arg.var_max)
3701 .unwrap_or(usize::MAX);
3702 while flags
3703 .get(flag)
3704 .map(value_count)
3705 .unwrap_or(0)
3706 .saturating_sub(carried)
3707 < max
3708 {
3709 let Some(next) = input.front().map(|token| token.word.as_str()) else {
3710 break;
3711 };
3712 if flag
3713 .arg
3714 .as_ref()
3715 .and_then(|arg| arg.value_terminator.as_deref())
3716 == Some(next)
3717 {
3718 let terminator = input.pop_front().unwrap();
3719 trace.record(
3720 terminator.argv,
3721 TokenRole::ValueTerminator {
3722 ends: flag.name.clone(),
3723 },
3724 );
3725 break;
3726 }
3727 if next == "--"
3730 || (is_flag_like(next)
3731 && !(flag
3732 .arg
3733 .as_ref()
3734 .is_some_and(|arg| arg.allow_negative_numbers)
3735 && is_negative_number(next)))
3736 {
3737 break;
3738 }
3739 let taken = input.pop_front().unwrap();
3740 let argv = taken.argv;
3741 let mut word = taken.word;
3742 flag_awaiting_value.push(Arc::clone(flag));
3743 let mut bound = vec![];
3744 let refused = drain_pending_flag_values(
3745 spec,
3746 cmd,
3747 errors,
3748 flags,
3749 flag_awaiting_value,
3750 &mut word,
3751 custom_env,
3752 &mut bound,
3753 )?;
3754 for (flag, values) in bound {
3755 trace.record(
3758 argv,
3759 TokenRole::Value {
3760 flag,
3761 values,
3762 attached: false,
3763 },
3764 );
3765 }
3766 if refused {
3767 return Ok(true);
3768 }
3769 }
3770 let taken = flags
3779 .get(flag)
3780 .map(value_count)
3781 .unwrap_or(0)
3782 .saturating_sub(carried);
3783 if let Some(min) = flag.arg.as_ref().and_then(|arg| arg.var_min) {
3784 if taken < min {
3785 errors.push(UsageErr::VarFlagTooFew {
3786 name: flag.name.clone(),
3787 min,
3788 got: taken,
3789 });
3790 }
3791 }
3792 if taken > max {
3793 errors.push(UsageErr::VarFlagTooMany {
3794 name: flag.name.clone(),
3795 max,
3796 got: taken,
3797 });
3798 }
3799 Ok(false)
3800}
3801
3802fn value_count(value: &ParseValue) -> usize {
3804 match value {
3805 ParseValue::MultiString(values) => values.len(),
3806 ParseValue::MultiBool(values) => values.len(),
3807 _ => 1,
3808 }
3809}
3810
3811fn try_bind_default_missing(
3822 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3823 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3824 custom_env: Option<&HashMap<String, String>>,
3825 origins: &mut IndexMap<Arc<SpecFlag>, Vec<ValueOrigin>>,
3826) -> miette::Result<bool> {
3827 let Some(flag) = flag_awaiting_value.last() else {
3828 return Ok(false);
3829 };
3830 let value = match flag.default_missing.clone() {
3831 Some(value) => value,
3832 None if flag.value_optional => {
3833 let flag = flag_awaiting_value.pop().unwrap();
3834 let variadic_value = flag.arg.as_ref().is_some_and(|arg| arg.var);
3838 origins
3839 .entry(Arc::clone(&flag))
3840 .or_default()
3841 .push(ValueOrigin::DefaultMissing);
3842 if flag.var {
3843 flags
3848 .entry(flag)
3849 .or_insert_with(|| ParseValue::MultiString(Vec::new()))
3850 .try_as_multi_string_mut()
3851 .unwrap()
3852 .push(String::new());
3853 } else if variadic_value {
3854 flags
3857 .entry(flag)
3858 .or_insert_with(|| ParseValue::MultiString(Vec::new()));
3859 } else {
3860 flags.insert(flag, ParseValue::MultiString(Vec::new()));
3864 }
3865 return Ok(true);
3866 }
3867 None => return Ok(false),
3868 };
3869 if let Some(arg) = flag.arg.as_ref() {
3870 validate_choice_value(
3871 ChoiceTarget::option(flag),
3872 &value,
3873 arg.choices.as_ref(),
3874 custom_env,
3875 )?;
3876 }
3877 let flag = flag_awaiting_value.pop().unwrap();
3878 origins
3879 .entry(Arc::clone(&flag))
3880 .or_default()
3881 .push(ValueOrigin::DefaultMissing);
3882 let collecting = flag.var || flag.arg.as_ref().is_some_and(|arg| arg.var);
3883 if collecting {
3884 let arr = flags
3885 .entry(flag)
3886 .or_insert_with(|| ParseValue::MultiString(vec![]))
3887 .try_as_multi_string_mut()
3888 .unwrap();
3889 arr.push(value);
3890 } else {
3891 flags.insert(flag, ParseValue::String(value));
3892 }
3893 Ok(true)
3894}
3895
3896#[allow(clippy::too_many_arguments)]
3901fn drain_pending_flag_values(
3902 spec: &Spec,
3903 cmd: &SpecCommand,
3904 errors: &mut Vec<UsageErr>,
3905 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
3906 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
3907 word: &mut String,
3908 custom_env: Option<&HashMap<String, String>>,
3909 bound: &mut Vec<(Arc<SpecFlag>, Vec<String>)>,
3910) -> miette::Result<bool> {
3911 while let Some(flag) = flag_awaiting_value.pop() {
3912 let arg = flag.arg.as_ref().unwrap();
3913 let parts: Vec<String> = match arg.delimiter {
3917 Some(delimiter) => word.split(delimiter).map(str::to_string).collect(),
3918 None => vec![std::mem::take(word)],
3919 };
3920 for part in &parts {
3921 if validate_choices(
3922 spec,
3923 cmd,
3924 errors,
3925 ChoiceTarget::option(&flag),
3926 part,
3927 arg.choices.as_ref(),
3928 custom_env,
3929 )? {
3930 return Ok(true);
3931 }
3932 }
3933 word.clear();
3934 bound.push((Arc::clone(&flag), parts.clone()));
3935 if flag.var || arg.var {
3938 let arr = flags
3939 .entry(flag)
3940 .or_insert_with(|| ParseValue::MultiString(vec![]))
3941 .try_as_multi_string_mut()
3942 .unwrap();
3943 arr.extend(parts);
3944 } else {
3945 flags.insert(
3948 flag,
3949 ParseValue::String(parts.into_iter().next().unwrap_or_default()),
3950 );
3951 }
3952 }
3953 Ok(false)
3954}
3955
3956fn choice_error(
3957 target: ChoiceTarget<'_>,
3958 value: &str,
3959 choices: Option<&SpecChoices>,
3960 custom_env: Option<&HashMap<String, String>>,
3961) -> Option<String> {
3962 let choices = choices?;
3963 if !choices.strict {
3964 return None;
3965 }
3966 let values = choices.values_with_env(custom_env);
3967 if choices.matches_with_env(value, custom_env) {
3968 return None;
3969 }
3970 if let Some(env) = choices.env() {
3971 if values.is_empty() {
3972 return Some(format!(
3973 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
3974 target.kind, target.name,
3975 ));
3976 }
3977 }
3978 Some(format!(
3979 "Invalid choice for {} {}: {value}, expected one of {}",
3980 target.kind,
3981 target.name,
3982 values.join(", ")
3983 ))
3984}
3985
3986fn validate_choices(
3987 spec: &Spec,
3988 cmd: &SpecCommand,
3989 errors: &mut Vec<UsageErr>,
3990 target: ChoiceTarget<'_>,
3991 value: &str,
3992 choices: Option<&SpecChoices>,
3993 custom_env: Option<&HashMap<String, String>>,
3994) -> miette::Result<bool> {
3995 if is_help_arg(spec, cmd, value)
3996 && choices
3997 .is_some_and(|choices| choices.strict && !choices.matches_with_env(value, custom_env))
3998 {
3999 errors.push(render_help_err(spec, cmd, value.len() > 2));
4000 return Ok(true);
4001 }
4002
4003 if let Some(err) = choice_error(target, value, choices, custom_env) {
4004 bail!("{err}");
4005 }
4006 Ok(false)
4007}
4008
4009fn validate_choice_value(
4010 target: ChoiceTarget<'_>,
4011 value: &str,
4012 choices: Option<&SpecChoices>,
4013 custom_env: Option<&HashMap<String, String>>,
4014) -> miette::Result<()> {
4015 if let Some(err) = choice_error(target, value, choices, custom_env) {
4016 bail!("{err}");
4017 }
4018 Ok(())
4019}
4020
4021fn validate_choice_values(
4022 target: ChoiceTarget<'_>,
4023 values: &[String],
4024 choices: Option<&SpecChoices>,
4025 custom_env: Option<&HashMap<String, String>>,
4026) -> miette::Result<()> {
4027 for value in values {
4028 validate_choice_value(target, value, choices, custom_env)?;
4029 }
4030 Ok(())
4031}
4032
4033fn record_stop(
4040 out: &mut ParseOutput,
4041 next_arg_idx: usize,
4042 seen_double_dash: bool,
4043 trace: &mut Trace,
4044 unread: &VecDeque<Token>,
4045) {
4046 out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
4047 out.double_dash_seen = seen_double_dash;
4048 trace.close(unread);
4049 out.tokens = std::mem::take(&mut trace.tokens);
4050}
4051
4052fn report_double_dash_violation(
4059 arg: &SpecArg,
4060 errors: &mut Vec<UsageErr>,
4061 violations: &mut HashSet<String>,
4062) {
4063 if violations.insert(arg.name.clone()) {
4064 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
4065 }
4066}
4067
4068fn is_version_arg(spec: &Spec, cmds: &[SpecCommand], w: &str) -> bool {
4083 (spec.version.is_some() || spec.long_version.is_some())
4084 && cmds.len() == 1
4085 && !spec.cmd.disable_version_flag
4086 && (w == "--version" || w == "-V")
4087}
4088
4089fn is_help_arg(spec: &Spec, cmd: &SpecCommand, w: &str) -> bool {
4090 spec.disable_help != Some(true)
4091 && (((w == "--help" || w == "-h" || w == "-?") && !cmd.disable_help_flag)
4092 || (w == "help" && !cmd.disable_help_subcommand && cmd.subcommands.is_empty()))
4093}
4094
4095impl ParseOutput {
4096 pub fn as_env(&self) -> BTreeMap<String, String> {
4097 let mut env = BTreeMap::new();
4098 for (flag, val) in &self.flags {
4099 let key = format!("usage_{}", crate::case::snake(&flag.name));
4100 let val = match val {
4101 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
4102 ParseValue::String(s) => s.clone(),
4103 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
4104 ParseValue::MultiString(s) => crate::shell_words::join(s),
4105 };
4106 env.insert(key, val);
4107 }
4108 for (arg, val) in &self.args {
4109 let key = format!("usage_{}", crate::case::snake(&arg.name));
4110 env.insert(key, val.to_string());
4111 }
4112 env
4113 }
4114}
4115
4116impl Display for ParseValue {
4117 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4118 match self {
4119 ParseValue::Bool(b) => write!(f, "{b}"),
4120 ParseValue::String(s) => write!(f, "{s}"),
4121 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
4122 ParseValue::MultiString(s) => write!(f, "{}", crate::shell_words::join(s)),
4123 }
4124 }
4125}
4126
4127fn render_token(token: &TokenBinding) -> String {
4129 let roles = token.roles.iter().map(render_role).join(", ");
4130 let synthesized = if token.synthesized { " (read as)" } else { "" };
4131 format!("[{}] {}{synthesized}: {roles}", token.index, token.word)
4132}
4133
4134fn render_role(role: &TokenRole) -> String {
4135 match role {
4136 TokenRole::Program => "program".to_string(),
4137 TokenRole::Command { name } => format!("subcommand {name}"),
4138 TokenRole::Flag {
4139 flag,
4140 spelling,
4141 negated,
4142 } => {
4143 let negated = if *negated { ", negated" } else { "" };
4144 format!("flag {} as {spelling}{negated}", flag.name)
4145 }
4146 TokenRole::Value {
4147 flag,
4148 values,
4149 attached,
4150 } => {
4151 let attached = if *attached { ", attached" } else { "" };
4152 format!("value of {} = {values:?}{attached}", flag.name)
4153 }
4154 TokenRole::Arg { arg, values } => format!("arg {} = {values:?}", arg.name),
4155 TokenRole::Separator => "separator".to_string(),
4156 TokenRole::Builtin { spelling } => format!("built-in {spelling}"),
4157 TokenRole::ValueTerminator { ends } => format!("value terminator, ends {ends}"),
4158 TokenRole::Restart => "restart".to_string(),
4159 TokenRole::UnknownFlag { bound_as } => match bound_as {
4160 Some(arg) => format!("unknown flag, bound as {}", arg.name),
4161 None => "unknown flag".to_string(),
4162 },
4163 TokenRole::Refused { reason } => format!("refused: {reason}"),
4164 TokenRole::External => "external".to_string(),
4165 TokenRole::Unread => "unread".to_string(),
4166 }
4167}
4168
4169impl Debug for ParseOutput {
4170 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4171 f.debug_struct("ParseOutput")
4172 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
4173 .field(
4174 "args",
4175 &self
4176 .args
4177 .iter()
4178 .map(|(a, w)| format!("{}: {w}", a.name))
4179 .collect_vec(),
4180 )
4181 .field(
4182 "available_flags",
4183 &self
4184 .available_flags
4185 .iter()
4186 .map(|(f, w)| format!("{f}: {w}"))
4187 .collect_vec(),
4188 )
4189 .field(
4190 "flags",
4191 &self
4192 .flags
4193 .iter()
4194 .map(|(f, w)| format!("{}: {w}", f.name))
4195 .collect_vec(),
4196 )
4197 .field("flag_awaiting_value", &self.flag_awaiting_value)
4198 .field("errors", &self.errors)
4199 .field("external", &self.external)
4200 .field(
4204 "tokens",
4205 &self.tokens.iter().map(render_token).collect_vec(),
4206 )
4207 .field(
4208 "origins",
4209 &self
4210 .flag_origins
4211 .iter()
4212 .map(|(f, o)| format!("{}: {o:?}", f.name))
4213 .chain(
4214 self.arg_origins
4215 .iter()
4216 .map(|(a, o)| format!("{}: {o:?}", a.name)),
4217 )
4218 .collect_vec(),
4219 )
4220 .field("overridden_flags", &self.overridden_flags)
4221 .finish()
4222 }
4223}
4224
4225#[cfg(test)]
4226mod tests {
4227 use super::*;
4228 use crate::SpecFlagAction;
4229
4230 fn input(words: &[&str]) -> Vec<String> {
4231 words.iter().map(|word| (*word).to_string()).collect()
4232 }
4233
4234 #[test]
4235 fn a_declared_version_supplies_the_flag_the_help_page_lists() {
4236 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4241 .parse()
4242 .unwrap();
4243
4244 for spelling in ["--version", "-V"] {
4245 let err = parse(&spec, &input(&["ex", spelling]))
4246 .expect_err("answering with a version ends the parse");
4247 assert_eq!(err.to_string(), "1.2.3", "{spelling}");
4248 }
4249 }
4250
4251 #[test]
4252 fn the_supplied_version_flag_is_the_roots_alone() {
4253 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ncmd \"run\"\n"
4256 .parse()
4257 .unwrap();
4258
4259 let err = parse(&spec, &input(&["ex", "run", "--version"])).unwrap_err();
4260 assert_eq!(err.to_string(), "unexpected word: --version");
4261 }
4262
4263 #[test]
4264 fn no_declared_version_supplies_nothing() {
4265 let spec: Spec = "name \"ex\"\nbin \"ex\"\n".parse().unwrap();
4268
4269 for spelling in ["--version", "-V"] {
4270 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4271 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4272 }
4273 }
4274
4275 #[test]
4276 fn disable_version_flag_removes_the_supplied_spellings() {
4277 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\ndisable_version_flag #true\n"
4278 .parse()
4279 .unwrap();
4280
4281 for spelling in ["--version", "-V"] {
4282 let err = parse(&spec, &input(&["ex", spelling])).unwrap_err();
4283 assert_eq!(err.to_string(), format!("unexpected word: {spelling}"));
4284 }
4285 }
4286
4287 #[test]
4288 fn a_spelling_the_spec_spends_elsewhere_keeps_its_meaning() {
4289 let spec: Spec = "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-V --verbose\"\n"
4292 .parse()
4293 .unwrap();
4294
4295 let out = parse(&spec, &input(&["ex", "-V"])).expect("-V is the CLI's own flag");
4296 assert_eq!(out.flags.len(), 1);
4297
4298 let err = parse(&spec, &input(&["ex", "--version"])).unwrap_err();
4299 assert_eq!(err.to_string(), "1.2.3");
4300 }
4301
4302 #[test]
4303 fn the_supplied_spellings_split_the_two_version_texts() {
4304 let spec: Spec =
4307 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nlong_version \"1.2.3 (abcdef)\"\n"
4308 .parse()
4309 .unwrap();
4310
4311 assert_eq!(
4312 parse(&spec, &input(&["ex", "--version"]))
4313 .unwrap_err()
4314 .to_string(),
4315 "1.2.3 (abcdef)"
4316 );
4317 assert_eq!(
4318 parse(&spec, &input(&["ex", "-V"])).unwrap_err().to_string(),
4319 "1.2.3"
4320 );
4321 }
4322
4323 #[test]
4324 fn a_supplied_short_is_a_letter_a_bundle_may_contain() {
4325 let spec: Spec =
4330 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\ncmd \"run\"\n"
4331 .parse()
4332 .unwrap();
4333
4334 for token in ["-vh", "-hv"] {
4335 let err = parse(&spec, &input(&["ex", token])).expect_err("help ends the parse");
4336 assert!(err.to_string().starts_with("ex 1.2.3"), "{token}: {err}");
4337 }
4338 for token in ["-vV", "-Vv"] {
4339 let err = parse(&spec, &input(&["ex", token])).expect_err("a version ends it too");
4340 assert_eq!(err.to_string(), "1.2.3", "{token}");
4341 }
4342 }
4343
4344 #[test]
4345 fn a_bundled_help_letter_asks_for_the_short_page() {
4346 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"
4349 .parse()
4350 .unwrap();
4351
4352 let short = parse(&spec, &input(&["ex", "-vh"]))
4353 .unwrap_err()
4354 .to_string();
4355 let long = parse(&spec, &input(&["ex", "--help"]))
4356 .unwrap_err()
4357 .to_string();
4358 assert!(short.contains("Be loud"), "{short}");
4359 assert!(!short.contains("at length"), "{short}");
4360 assert!(long.contains("at length"), "{long}");
4361 }
4362
4363 #[test]
4364 fn the_bundled_version_letter_is_the_roots_alone() {
4365 let spec: Spec =
4367 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\" global=#true\ncmd \"run\"\n"
4368 .parse()
4369 .unwrap();
4370
4371 let err = parse(&spec, &input(&["ex", "run", "-vV"])).unwrap_err();
4372 assert_eq!(err.to_string(), "unexpected word: -vV");
4373 }
4374
4375 #[test]
4376 fn a_declared_letter_keeps_its_meaning_inside_a_bundle() {
4377 let spec: Spec =
4380 "name \"ex\"\nbin \"ex\"\nversion \"1.2.3\"\nflag \"-v --verbose\"\nflag \"-h --host <host>\"\n"
4381 .parse()
4382 .unwrap();
4383
4384 let out = parse(&spec, &input(&["ex", "-vhlocal"])).expect("a bundle and its value");
4385 assert_eq!(out.flags.len(), 2);
4386 assert!(out
4387 .flags
4388 .iter()
4389 .any(|(flag, value)| flag.name == "host" && value.to_string() == "local"));
4390 }
4391
4392 #[test]
4393 fn disabling_help_takes_the_letter_back_out_of_the_bundle() {
4394 let spec: Spec =
4395 "name \"ex\"\nbin \"ex\"\ndisable_help_flag #true\nflag \"-v --verbose\"\n"
4396 .parse()
4397 .unwrap();
4398
4399 let err = parse(&spec, &input(&["ex", "-vh"])).unwrap_err();
4400 assert_eq!(err.to_string(), "unexpected word: -vh");
4401 }
4402
4403 #[test]
4404 fn a_letter_nothing_supplies_still_refuses_the_whole_bundle() {
4405 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a --all\"\narg \"[file]\"\n"
4408 .parse()
4409 .unwrap();
4410
4411 let out = parse(&spec, &input(&["ex", "-az"])).expect("it falls through to the argument");
4412 assert!(out.flags.is_empty(), "{:?}", out.flags);
4413 assert_eq!(out.args.len(), 1);
4414 }
4415
4416 fn spec_with_arg(arg: SpecArg) -> Spec {
4417 let cmd = SpecCommand::builder().name("test").arg(arg).build();
4418 Spec {
4419 name: "test".to_string(),
4420 bin: "test".to_string(),
4421 cmd,
4422 ..Default::default()
4423 }
4424 }
4425
4426 fn spec_with_flag(flag: SpecFlag) -> Spec {
4427 let cmd = SpecCommand::builder().name("test").flag(flag).build();
4428 Spec {
4429 name: "test".to_string(),
4430 bin: "test".to_string(),
4431 cmd,
4432 ..Default::default()
4433 }
4434 }
4435
4436 fn parse_with_env(
4437 spec: &Spec,
4438 words: &[&str],
4439 env: &[(&str, &str)],
4440 ) -> Result<ParseOutput, miette::Error> {
4441 let env = env
4442 .iter()
4443 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
4444 .collect();
4445 Parser::new(spec).with_env(env).parse(&input(words))
4446 }
4447
4448 fn first_string_value(parsed: &ParseOutput) -> &str {
4449 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
4450 return value;
4451 }
4452 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
4453 return value;
4454 }
4455 panic!("expected first parsed value to be ParseValue::String");
4456 }
4457
4458 #[test]
4459 fn custom_environment_parser_dispatches_executable_views() {
4460 let spec: Spec = r#"
4461bin "ex"
4462view "runner" root="run"
4463cmd "run" {
4464 flag "--token <token>" env="TOKEN"
4465}
4466 "#
4467 .parse()
4468 .unwrap();
4469 let parsed = Parser::new(&spec)
4470 .with_env([("TOKEN".to_string(), "secret".to_string())].into())
4471 .parse(&input(&["runner"]))
4472 .unwrap();
4473
4474 assert_eq!(parsed.cmd.name, "runner");
4475 assert!(parsed.flags.iter().any(|(flag, value)| flag.name == "token"
4476 && matches!(value, ParseValue::String(value) if value == "secret")));
4477 }
4478
4479 #[test]
4480 fn an_executable_view_keeps_the_hosts_version_action() {
4481 let spec: Spec = r#"
4482bin "ex"
4483version "1.2.3"
4484flag "-V --version" action="version"
4485flag "--verbose" global=#true
4486view "runner" root="run" globals=#true
4487cmd "run"
4488 "#
4489 .parse()
4490 .unwrap();
4491
4492 let error = Parser::new(&spec)
4493 .parse(&input(&["runner", "--version"]))
4494 .expect_err("the host version action should answer before view projection");
4495 assert_eq!(error.to_string(), "1.2.3");
4496
4497 let error = Parser::new(&spec)
4498 .parse(&input(&["runner", "--verbose", "--version"]))
4499 .expect_err("the host version action should remain after a carried global");
4500 assert_eq!(error.to_string(), "1.2.3");
4501 }
4502
4503 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
4504 let flag = parsed
4505 .flags
4506 .keys()
4507 .find(|flag| flag.name == name)
4508 .unwrap_or_else(|| panic!("expected flag {name}"));
4509 let value = parsed
4510 .flags
4511 .get(flag)
4512 .unwrap_or_else(|| panic!("expected value for flag {name}"));
4513 match value {
4514 ParseValue::String(value) => value,
4515 _ => panic!("expected flag {name} to be ParseValue::String"),
4516 }
4517 }
4518
4519 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
4520 let err = result.expect_err("expected parser error");
4521 assert_eq!(format!("{err}"), expected);
4522 }
4523
4524 #[test]
4525 fn a_short_version_action_falls_back_to_the_long_version() {
4526 let flag = SpecFlag::builder()
4527 .short('R')
4528 .action(SpecFlagAction::Version)
4529 .build();
4530 let spec = Spec {
4531 name: "test".to_string(),
4532 bin: "test".to_string(),
4533 long_version: Some("1.2.3\ncommit abc123".to_string()),
4534 ..Default::default()
4535 };
4536 let UsageErr::Version(version) = render_action_err(&spec, &spec.cmd, &flag, "-R") else {
4537 panic!("expected version action")
4538 };
4539 assert_eq!(version, "1.2.3\ncommit abc123");
4540 }
4541
4542 #[cfg(feature = "unstable_choices_env")]
4543 fn spec_arg_choices_env(key: &str) -> Spec {
4544 spec_with_arg(
4545 SpecArg::builder()
4546 .name("env")
4547 .choices_env(key)
4548 .required(false)
4549 .build(),
4550 )
4551 }
4552
4553 #[cfg(feature = "unstable_choices_env")]
4554 fn spec_flag_choices_env(key: &str) -> Spec {
4555 spec_with_flag(
4556 SpecFlag::builder()
4557 .long("env")
4558 .arg(SpecArg::builder().name("env").choices_env(key).build())
4559 .build(),
4560 )
4561 }
4562
4563 #[test]
4564 fn test_parse() {
4565 let cmd = SpecCommand::builder()
4566 .name("test")
4567 .arg(SpecArg::builder().name("arg").build())
4568 .flag(SpecFlag::builder().long("flag").build())
4569 .build();
4570 let spec = Spec {
4571 name: "test".to_string(),
4572 bin: "test".to_string(),
4573 cmd,
4574 ..Default::default()
4575 };
4576 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
4577 let parsed = parse(&spec, &input).unwrap();
4578 assert_eq!(parsed.cmds.len(), 1);
4579 assert_eq!(parsed.cmds[0].name, "test");
4580 assert_eq!(parsed.args.len(), 1);
4581 assert_eq!(parsed.flags.len(), 1);
4582 assert_eq!(parsed.available_flags.len(), 1);
4583 }
4584
4585 #[test]
4586 fn test_flag_overrides_last_occurrence_wins() {
4587 let spec: Spec = r#"
4588flag "--stdin" default=#true
4589flag "--file <file>" overrides="--stdin"
4590 "#
4591 .parse()
4592 .unwrap();
4593
4594 let file_wins = parse(&spec, &input(&["test", "--stdin", "--file", "input.txt"])).unwrap();
4595 assert_eq!(file_wins.flags.len(), 1);
4596 assert_eq!(flag_string_value(&file_wins, "file"), "input.txt");
4597 assert!(!file_wins.flags.keys().any(|flag| flag.name == "stdin"));
4598
4599 let stdin_wins = parse(&spec, &input(&["test", "--file", "input.txt", "--stdin"])).unwrap();
4600 assert_eq!(stdin_wins.flags.len(), 1);
4601 assert!(stdin_wins.flags.keys().any(|flag| flag.name == "stdin"));
4602 assert!(!stdin_wins.flags.keys().any(|flag| flag.name == "file"));
4603 }
4604
4605 #[test]
4606 fn test_flag_override_clears_pending_value() {
4607 let spec: Spec = r#"
4608flag "--file <file>" overrides="--stdin"
4609flag "--stdin"
4610arg "[input]"
4611 "#
4612 .parse()
4613 .unwrap();
4614
4615 let parsed = parse(&spec, &input(&["test", "--file", "--stdin", "input.txt"])).unwrap();
4616 assert_eq!(parsed.flags.len(), 1);
4617 assert!(parsed.flags.keys().any(|flag| flag.name == "stdin"));
4618 assert_eq!(first_string_value(&parsed), "input.txt");
4619 }
4620
4621 #[cfg(unix)]
4622 #[test]
4623 fn a_mount_on_the_root_discovers_subcommands() {
4624 let spec: Spec = r#"
4628name "ex"
4629bin "ex"
4630cmd "declared"
4631mount run="echo 'cmd \"discovered\"'"
4632"#
4633 .parse()
4634 .unwrap();
4635
4636 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4637 assert_eq!(out.cmd.name, "discovered");
4638 }
4639
4640 #[test]
4641 fn injected_mount_outputs_are_complete_and_never_fall_back_to_processes() {
4642 let spec: Spec = r#"
4643name "ex"
4644bin "ex"
4645mount run="this command must never run"
4646cmd "declared"
4647"#
4648 .parse()
4649 .unwrap();
4650
4651 Parser::new(&spec)
4652 .with_mount_outputs(HashMap::new())
4653 .parse(&input(&["ex", "declared"]))
4654 .expect("a declared command does not resolve the mount");
4655
4656 let error = Parser::new(&spec)
4657 .with_mount_outputs(HashMap::new())
4658 .parse(&input(&["ex", "discovered"]))
4659 .unwrap_err();
4660 assert!(
4661 error
4662 .to_string()
4663 .contains("No injected output was provided for mount command"),
4664 "{error}"
4665 );
4666 }
4667
4668 #[cfg(unix)]
4669 #[test]
4670 fn completion_sees_root_mounted_commands_with_nothing_typed() {
4671 let spec: Spec = r#"
4675name "ex"
4676bin "ex"
4677cmd "declared"
4678mount run="echo 'cmd \"discovered\"'"
4679"#
4680 .parse()
4681 .unwrap();
4682
4683 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4684 assert!(
4685 out.cmd.subcommands.contains_key("discovered"),
4686 "a completion should see mounted commands; got {:?}",
4687 out.cmd.subcommands.keys().collect::<Vec<_>>()
4688 );
4689 }
4690
4691 #[cfg(unix)]
4692 #[test]
4693 fn completion_and_execution_agree_about_discovery() {
4694 let spec: Spec = r#"
4698name "ex"
4699bin "ex"
4700default_subcommand "run"
4701cmd "run" {
4702 arg "<task>"
4703}
4704mount run="exit 1"
4705"#
4706 .parse()
4707 .unwrap();
4708
4709 let out = parse_partial(&spec, &["ex".to_string()]).unwrap();
4710 assert!(
4711 !out.cmd.subcommands.contains_key("discovered"),
4712 "a completion must not offer what execution will not route"
4713 );
4714
4715 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4716 assert_eq!(out.cmd.name, "run");
4717 }
4718
4719 #[cfg(unix)]
4720 #[test]
4721 fn a_default_subcommand_outranks_discovery() {
4722 let spec: Spec = r#"
4725name "ex"
4726bin "ex"
4727default_subcommand "run"
4728cmd "run" {
4729 arg "<task>"
4730}
4731mount run="exit 1"
4732"#
4733 .parse()
4734 .unwrap();
4735
4736 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4737 assert_eq!(out.cmd.name, "run");
4738 }
4739
4740 #[cfg(unix)]
4741 #[test]
4742 fn a_mount_may_ask_to_outrank_the_default() {
4743 let spec: Spec = r#"
4746name "ex"
4747bin "ex"
4748default_subcommand "run"
4749cmd "run" {
4750 arg "<task>"
4751}
4752mount run="echo 'cmd \"discovered\"'" overrides_default=#true
4753"#
4754 .parse()
4755 .unwrap();
4756
4757 let out = parse(&spec, &["ex".to_string(), "discovered".to_string()]).unwrap();
4758 assert_eq!(out.cmd.name, "discovered");
4759
4760 let out = parse(&spec, &["ex".to_string(), "mytask".to_string()]).unwrap();
4762 assert_eq!(out.cmd.name, "run");
4763 }
4764
4765 #[cfg(unix)]
4766 #[test]
4767 fn a_flag_does_not_run_the_mount() {
4768 let spec: Spec = r#"
4772name "ex"
4773bin "ex"
4774flag "--verbose"
4775cmd "declared"
4776mount run="exit 1"
4777"#
4778 .parse()
4779 .unwrap();
4780
4781 let out = parse(&spec, &["ex".to_string(), "--verbose".to_string()]).unwrap();
4782 assert_eq!(out.cmd.name, "ex");
4783 }
4784
4785 #[cfg(unix)]
4786 #[test]
4787 fn a_declared_subcommand_does_not_run_the_mount() {
4788 let spec: Spec = r#"
4792name "ex"
4793bin "ex"
4794cmd "declared"
4795mount run="exit 1"
4796"#
4797 .parse()
4798 .unwrap();
4799
4800 let out = parse(&spec, &["ex".to_string(), "declared".to_string()]).unwrap();
4801 assert_eq!(out.cmd.name, "declared");
4802 }
4803
4804 #[test]
4805 fn a_root_mount_survives_being_written_out() {
4806 let spec: Spec = "name \"ex\"\nbin \"ex\"\nmount run=\"ex plugins --usage\"\n"
4807 .parse()
4808 .unwrap();
4809 assert_eq!(spec.cmd.mounts.len(), 1);
4810
4811 let reparsed: Spec = spec.to_string().parse().unwrap();
4812 assert_eq!(reparsed.cmd.mounts.len(), 1, "written:\n{spec}");
4813 assert_eq!(reparsed.cmd.mounts[0].run, "ex plugins --usage");
4814 }
4815
4816 #[test]
4817 fn test_mount_prefix_applies_flag_overrides() {
4818 let stdin = Arc::new(
4819 SpecFlag::builder()
4820 .name("stdin")
4821 .long("stdin")
4822 .global(true)
4823 .build(),
4824 );
4825 let file = Arc::new(
4826 SpecFlag::builder()
4827 .name("file")
4828 .long("file")
4829 .arg(SpecArg::builder().name("file").build())
4830 .global(true)
4831 .overrides_with(vec!["--stdin".to_string()])
4832 .build(),
4833 );
4834 let mut prefix_flags = vec![(stdin, vec!["--stdin".to_string()])];
4835
4836 apply_prefix_flag_overrides(&mut prefix_flags, Arc::clone(&file));
4837 prefix_flags.push((file, vec!["--file".to_string(), "input.txt".to_string()]));
4838
4839 assert_eq!(mount_prefix_words(&prefix_flags), ["--file", "input.txt"]);
4840 }
4841
4842 #[test]
4843 fn test_flag_override_suppresses_env_value() {
4844 let spec: Spec = r#"
4845flag "--stdin" env="USE_STDIN"
4846flag "--file <file>" overrides="--stdin"
4847 "#
4848 .parse()
4849 .unwrap();
4850
4851 let parsed = parse_with_env(
4852 &spec,
4853 &["test", "--file", "input.txt"],
4854 &[("USE_STDIN", "true")],
4855 )
4856 .unwrap();
4857 assert_eq!(parsed.flags.len(), 1);
4858 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4859 }
4860
4861 #[test]
4862 fn test_flag_override_suppresses_required_check() {
4863 let spec: Spec = r#"
4864flag "--stdin" required=#true
4865flag "--file <file>" overrides="--stdin"
4866 "#
4867 .parse()
4868 .unwrap();
4869
4870 let parsed = parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4871 assert_eq!(parsed.flags.len(), 1);
4872 assert_eq!(flag_string_value(&parsed, "file"), "input.txt");
4873 }
4874
4875 #[test]
4876 fn test_flag_required_if() {
4877 let spec: Spec = r#"
4878flag "--dir <dir>"
4879flag "--file <file>" required_if="--dir"
4880 "#
4881 .parse()
4882 .unwrap();
4883
4884 parse(&spec, &input(&["test"])).unwrap();
4885 assert_parse_err(
4886 parse(&spec, &input(&["test", "--dir", "src"])),
4887 "Missing required flag: --file <file>",
4888 );
4889 parse(
4890 &spec,
4891 &input(&["test", "--dir", "src", "--file", "input.txt"]),
4892 )
4893 .unwrap();
4894 }
4895
4896 #[test]
4897 fn test_flag_required_unless() {
4898 let spec: Spec = r#"
4899flag "--stdin"
4900flag "--file <file>" required_unless="--stdin"
4901 "#
4902 .parse()
4903 .unwrap();
4904
4905 assert_parse_err(
4906 parse(&spec, &input(&["test"])),
4907 "Missing required flag: --file <file>",
4908 );
4909 parse(&spec, &input(&["test", "--stdin"])).unwrap();
4910 parse(&spec, &input(&["test", "--file", "input.txt"])).unwrap();
4911 }
4912
4913 #[test]
4914 fn complete_required_relationship_truth_tables() {
4915 let spec: Spec = r#"
4916name "ex"
4917bin "ex"
4918flag "--mode <mode>"
4919flag "--scope <scope>"
4920flag "--token <token>" {
4921 required_if_eq "--mode" "remote"
4922}
4923flag "--approval <approval>" {
4924 required_if_eq_all "--mode" "remote" "--scope" "global"
4925}
4926flag "--input <input>" {
4927 required_unless "--stdin" "--file"
4928}
4929flag "--checksum <checksum>" {
4930 required_unless_all "--stdin" "--file"
4931}
4932flag "--stdin"
4933flag "--file <file>"
4934arg "[request]" {
4935 requires "--mode" "--scope"
4936}
4937"#
4938 .parse()
4939 .unwrap();
4940 let parse_args = |args: &[&str]| {
4941 parse(
4942 &spec,
4943 &args
4944 .iter()
4945 .map(|arg| (*arg).to_string())
4946 .collect::<Vec<_>>(),
4947 )
4948 };
4949
4950 assert!(parse_args(&["ex", "--mode", "remote", "--stdin"]).is_err());
4951 assert!(parse_args(&[
4952 "ex", "--mode", "remote", "--token", "secret", "--scope", "global", "--stdin",
4953 ])
4954 .is_err());
4955 parse_args(&[
4956 "ex",
4957 "--mode",
4958 "remote",
4959 "--token",
4960 "secret",
4961 "--scope",
4962 "global",
4963 "--approval",
4964 "yes",
4965 "--stdin",
4966 "--file",
4967 "in",
4968 ])
4969 .unwrap();
4970 parse_args(&[
4971 "ex",
4972 "--mode",
4973 "local",
4974 "--scope",
4975 "project",
4976 "--stdin",
4977 "--checksum",
4978 "sum",
4979 "request.json",
4980 ])
4981 .unwrap();
4982
4983 let reparsed: Spec = spec.to_string().parse().unwrap();
4984 assert_eq!(reparsed.cmd.flags[2].required_if_eq.len(), 1);
4985 assert_eq!(reparsed.cmd.flags[3].required_if_eq_all.len(), 2);
4986 assert_eq!(reparsed.cmd.flags[5].required_unless_all.len(), 2);
4987 assert_eq!(reparsed.cmd.args[0].requires.len(), 2);
4988 }
4989
4990 #[test]
4991 fn test_conditional_requirements_treat_env_as_explicit() {
4992 let spec: Spec = r#"
4993flag "--dir <dir>" env="INPUT_DIR"
4994flag "--stdin" env="USE_STDIN"
4995flag "--file <file>" required_if="--dir" required_unless="--stdin"
4996 "#
4997 .parse()
4998 .unwrap();
4999
5000 assert_parse_err(
5001 parse_with_env(&spec, &["test"], &[("INPUT_DIR", "src")]),
5002 "Missing required flag: --file <file>",
5003 );
5004 parse_with_env(&spec, &["test"], &[("USE_STDIN", "true")]).unwrap();
5005 }
5006
5007 #[test]
5008 fn test_custom_env_does_not_fall_back_to_process_env() {
5009 assert!(std::env::var("PATH").is_ok());
5010 let spec: Spec = r#"flag "--file <file>" env="PATH" required=#true"#.parse().unwrap();
5011
5012 assert_parse_err(
5013 parse_with_env(&spec, &["test"], &[]),
5014 "Missing required flag: --file <file>",
5015 );
5016 }
5017
5018 #[test]
5019 fn test_conditional_requirements_ignore_defaults_on_condition_flags() {
5020 let spec: Spec = r#"
5021flag "--dir <dir>" default="src"
5022flag "--file <file>" required_if="--dir"
5023 "#
5024 .parse()
5025 .unwrap();
5026
5027 parse(&spec, &input(&["test"])).unwrap();
5028 }
5029
5030 #[test]
5031 fn test_conditional_requirements_see_overridden_flags_as_absent() {
5032 let spec: Spec = r#"
5033flag "--stdin"
5034flag "--dir <dir>" overrides="--stdin"
5035flag "--file <file>" required_unless="--stdin"
5036 "#
5037 .parse()
5038 .unwrap();
5039
5040 assert_parse_err(
5041 parse(&spec, &input(&["test", "--stdin", "--dir", "src"])),
5042 "Missing required flag: --file <file>",
5043 );
5044 }
5045
5046 #[test]
5047 fn short_flag_is_one_character_not_one_byte() {
5048 let spec = spec_with_flag(
5052 SpecFlag::builder()
5053 .short('磨')
5054 .long("polish")
5055 .arg(SpecArg::builder().name("opt").build())
5056 .build(),
5057 );
5058 let attached = Parser::new(&spec)
5059 .parse(&input(&["test", "-磨VALUE"]))
5060 .unwrap();
5061 assert_eq!(flag_string_value(&attached, "polish"), "VALUE");
5062 let detached = Parser::new(&spec)
5063 .parse(&input(&["test", "-磨", "V"]))
5064 .unwrap();
5065 assert_eq!(flag_string_value(&detached, "polish"), "V");
5066 }
5067
5068 #[test]
5069 fn test_as_env() {
5070 let cmd = SpecCommand::builder()
5071 .name("test")
5072 .arg(SpecArg::builder().name("arg").build())
5073 .flag(SpecFlag::builder().long("flag").build())
5074 .flag(
5075 SpecFlag::builder()
5076 .long("force")
5077 .negate("--no-force")
5078 .build(),
5079 )
5080 .build();
5081 let spec = Spec {
5082 name: "test".to_string(),
5083 bin: "test".to_string(),
5084 cmd,
5085 ..Default::default()
5086 };
5087 let input = vec![
5088 "test".to_string(),
5089 "--flag".to_string(),
5090 "--no-force".to_string(),
5091 ];
5092 let parsed = parse(&spec, &input).unwrap();
5093 let env = parsed.as_env();
5094 assert_eq!(env.len(), 2);
5095 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
5096 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
5097 }
5098
5099 #[test]
5100 fn test_arg_env_var() {
5101 let cmd = SpecCommand::builder()
5102 .name("test")
5103 .arg(
5104 SpecArg::builder()
5105 .name("input")
5106 .env("TEST_ARG_INPUT")
5107 .required(true)
5108 .build(),
5109 )
5110 .build();
5111 let spec = Spec {
5112 name: "test".to_string(),
5113 bin: "test".to_string(),
5114 cmd,
5115 ..Default::default()
5116 };
5117
5118 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
5120
5121 let input = vec!["test".to_string()];
5122 let parsed = parse(&spec, &input).unwrap();
5123
5124 assert_eq!(parsed.args.len(), 1);
5125 let arg = parsed.args.keys().next().unwrap();
5126 assert_eq!(arg.name, "input");
5127 let value = parsed.args.values().next().unwrap();
5128 assert_eq!(value.to_string(), "test_file.txt");
5129
5130 std::env::remove_var("TEST_ARG_INPUT");
5132 }
5133
5134 #[test]
5135 fn test_flag_env_var_with_arg() {
5136 let cmd = SpecCommand::builder()
5137 .name("test")
5138 .flag(
5139 SpecFlag::builder()
5140 .long("output")
5141 .env("TEST_FLAG_OUTPUT")
5142 .arg(SpecArg::builder().name("file").build())
5143 .build(),
5144 )
5145 .build();
5146 let spec = Spec {
5147 name: "test".to_string(),
5148 bin: "test".to_string(),
5149 cmd,
5150 ..Default::default()
5151 };
5152
5153 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
5155
5156 let input = vec!["test".to_string()];
5157 let parsed = parse(&spec, &input).unwrap();
5158
5159 assert_eq!(parsed.flags.len(), 1);
5160 let flag = parsed.flags.keys().next().unwrap();
5161 assert_eq!(flag.name, "output");
5162 let value = parsed.flags.values().next().unwrap();
5163 assert_eq!(value.to_string(), "output.txt");
5164
5165 std::env::remove_var("TEST_FLAG_OUTPUT");
5167 }
5168
5169 #[test]
5170 fn test_flag_env_var_boolean() {
5171 let cmd = SpecCommand::builder()
5172 .name("test")
5173 .flag(
5174 SpecFlag::builder()
5175 .long("verbose")
5176 .env("TEST_FLAG_VERBOSE")
5177 .build(),
5178 )
5179 .build();
5180 let spec = Spec {
5181 name: "test".to_string(),
5182 bin: "test".to_string(),
5183 cmd,
5184 ..Default::default()
5185 };
5186
5187 std::env::set_var("TEST_FLAG_VERBOSE", "true");
5189
5190 let input = vec!["test".to_string()];
5191 let parsed = parse(&spec, &input).unwrap();
5192
5193 assert_eq!(parsed.flags.len(), 1);
5194 let flag = parsed.flags.keys().next().unwrap();
5195 assert_eq!(flag.name, "verbose");
5196 let value = parsed.flags.values().next().unwrap();
5197 assert_eq!(value.to_string(), "true");
5198
5199 std::env::remove_var("TEST_FLAG_VERBOSE");
5201 }
5202
5203 #[test]
5204 fn test_env_var_precedence() {
5205 let cmd = SpecCommand::builder()
5207 .name("test")
5208 .arg(
5209 SpecArg::builder()
5210 .name("input")
5211 .env("TEST_PRECEDENCE_INPUT")
5212 .required(true)
5213 .build(),
5214 )
5215 .build();
5216 let spec = Spec {
5217 name: "test".to_string(),
5218 bin: "test".to_string(),
5219 cmd,
5220 ..Default::default()
5221 };
5222
5223 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
5225
5226 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
5227 let parsed = parse(&spec, &input).unwrap();
5228
5229 assert_eq!(parsed.args.len(), 1);
5230 let value = parsed.args.values().next().unwrap();
5231 assert_eq!(value.to_string(), "cli_file.txt");
5233
5234 std::env::remove_var("TEST_PRECEDENCE_INPUT");
5236 }
5237
5238 #[test]
5239 fn test_flag_var_true_with_single_default() {
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_value("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 flag = parsed.flags.keys().next().unwrap();
5265 assert_eq!(flag.name, "foo");
5266 let value = parsed.flags.values().next().unwrap();
5267 match value {
5269 ParseValue::MultiString(v) => {
5270 assert_eq!(v.len(), 1);
5271 assert_eq!(v[0], "bar");
5272 }
5273 _ => panic!("Expected MultiString, got {:?}", value),
5274 }
5275 }
5276
5277 #[test]
5278 fn test_flag_var_true_with_multiple_defaults() {
5279 let cmd = SpecCommand::builder()
5281 .name("test")
5282 .flag(
5283 SpecFlag::builder()
5284 .long("foo")
5285 .var(true)
5286 .arg(SpecArg::builder().name("foo").build())
5287 .default_values(["xyz", "bar"])
5288 .build(),
5289 )
5290 .build();
5291 let spec = Spec {
5292 name: "test".to_string(),
5293 bin: "test".to_string(),
5294 cmd,
5295 ..Default::default()
5296 };
5297
5298 let input = vec!["test".to_string()];
5300 let parsed = parse(&spec, &input).unwrap();
5301
5302 assert_eq!(parsed.flags.len(), 1);
5303 let value = parsed.flags.values().next().unwrap();
5304 match value {
5306 ParseValue::MultiString(v) => {
5307 assert_eq!(v.len(), 2);
5308 assert_eq!(v[0], "xyz");
5309 assert_eq!(v[1], "bar");
5310 }
5311 _ => panic!("Expected MultiString, got {:?}", value),
5312 }
5313 }
5314
5315 #[test]
5316 fn test_flag_var_false_with_default_remains_string() {
5317 let cmd = SpecCommand::builder()
5319 .name("test")
5320 .flag(
5321 SpecFlag::builder()
5322 .long("foo")
5323 .var(false) .arg(SpecArg::builder().name("foo").build())
5325 .default_value("bar")
5326 .build(),
5327 )
5328 .build();
5329 let spec = Spec {
5330 name: "test".to_string(),
5331 bin: "test".to_string(),
5332 cmd,
5333 ..Default::default()
5334 };
5335
5336 let input = vec!["test".to_string()];
5338 let parsed = parse(&spec, &input).unwrap();
5339
5340 assert_eq!(parsed.flags.len(), 1);
5341 let value = parsed.flags.values().next().unwrap();
5342 match value {
5344 ParseValue::String(s) => {
5345 assert_eq!(s, "bar");
5346 }
5347 _ => panic!("Expected String, got {:?}", value),
5348 }
5349 }
5350
5351 #[test]
5352 fn test_arg_var_true_with_single_default() {
5353 let cmd = SpecCommand::builder()
5355 .name("test")
5356 .arg(
5357 SpecArg::builder()
5358 .name("files")
5359 .var(true)
5360 .default_value("default.txt")
5361 .required(false)
5362 .build(),
5363 )
5364 .build();
5365 let spec = Spec {
5366 name: "test".to_string(),
5367 bin: "test".to_string(),
5368 cmd,
5369 ..Default::default()
5370 };
5371
5372 let input = vec!["test".to_string()];
5374 let parsed = parse(&spec, &input).unwrap();
5375
5376 assert_eq!(parsed.args.len(), 1);
5377 let value = parsed.args.values().next().unwrap();
5378 match value {
5380 ParseValue::MultiString(v) => {
5381 assert_eq!(v.len(), 1);
5382 assert_eq!(v[0], "default.txt");
5383 }
5384 _ => panic!("Expected MultiString, got {:?}", value),
5385 }
5386 }
5387
5388 #[test]
5389 fn test_arg_var_true_with_multiple_defaults() {
5390 let cmd = SpecCommand::builder()
5392 .name("test")
5393 .arg(
5394 SpecArg::builder()
5395 .name("files")
5396 .var(true)
5397 .default_values(["file1.txt", "file2.txt"])
5398 .required(false)
5399 .build(),
5400 )
5401 .build();
5402 let spec = Spec {
5403 name: "test".to_string(),
5404 bin: "test".to_string(),
5405 cmd,
5406 ..Default::default()
5407 };
5408
5409 let input = vec!["test".to_string()];
5411 let parsed = parse(&spec, &input).unwrap();
5412
5413 assert_eq!(parsed.args.len(), 1);
5414 let value = parsed.args.values().next().unwrap();
5415 match value {
5417 ParseValue::MultiString(v) => {
5418 assert_eq!(v.len(), 2);
5419 assert_eq!(v[0], "file1.txt");
5420 assert_eq!(v[1], "file2.txt");
5421 }
5422 _ => panic!("Expected MultiString, got {:?}", value),
5423 }
5424 }
5425
5426 #[test]
5427 fn test_arg_var_false_with_default_remains_string() {
5428 let cmd = SpecCommand::builder()
5430 .name("test")
5431 .arg(
5432 SpecArg::builder()
5433 .name("file")
5434 .var(false)
5435 .default_value("default.txt")
5436 .required(false)
5437 .build(),
5438 )
5439 .build();
5440 let spec = Spec {
5441 name: "test".to_string(),
5442 bin: "test".to_string(),
5443 cmd,
5444 ..Default::default()
5445 };
5446
5447 let input = vec!["test".to_string()];
5449 let parsed = parse(&spec, &input).unwrap();
5450
5451 assert_eq!(parsed.args.len(), 1);
5452 let value = parsed.args.values().next().unwrap();
5453 match value {
5455 ParseValue::String(s) => {
5456 assert_eq!(s, "default.txt");
5457 }
5458 _ => panic!("Expected String, got {:?}", value),
5459 }
5460 }
5461
5462 #[test]
5463 fn test_scalar_defaults_validate_only_first_default_choice() {
5464 let specs = [
5465 spec_with_arg(
5466 SpecArg::builder()
5467 .name("env")
5468 .var(false)
5469 .default_values(["dev", "prod"])
5470 .choices(["dev"])
5471 .required(false)
5472 .build(),
5473 ),
5474 spec_with_flag(
5475 SpecFlag::builder()
5476 .long("env")
5477 .arg(
5478 SpecArg::builder()
5479 .name("env")
5480 .default_values(["dev", "prod"])
5481 .choices(["dev"])
5482 .build(),
5483 )
5484 .build(),
5485 ),
5486 ];
5487
5488 for spec in specs {
5489 let parsed = parse(&spec, &input(&["test"])).unwrap();
5490 assert_eq!(first_string_value(&parsed), "dev");
5491 }
5492 }
5493
5494 #[test]
5495 fn a_delimiter_turns_one_word_into_several_values() {
5496 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tag>\" var=#true delimiter=\",\"\narg \"[files]...\" var=#true delimiter=\":\"\n"
5497 .parse()
5498 .unwrap();
5499
5500 let parsed = parse(&spec, &input(&["ex", "--tags", "a,b,c", "x:y"])).unwrap();
5501 let multi = |value: &ParseValue| match value {
5502 ParseValue::MultiString(values) => values.clone(),
5503 other => panic!("expected several values, got {other:?}"),
5504 };
5505 let tags = parsed
5506 .flags
5507 .iter()
5508 .find(|(f, _)| f.name == "tags")
5509 .map(|(_, v)| v)
5510 .unwrap();
5511 assert_eq!(multi(tags), vec!["a", "b", "c"]);
5512 assert_eq!(multi(parsed.args.values().next().unwrap()), vec!["x", "y"]);
5513 }
5514
5515 #[test]
5516 fn a_positional_splits_before_its_choices_are_asked() {
5517 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[paths]...\" var=#true delimiter=\":\" {\n choices \"src\" \"docs\"\n}\n"
5521 .parse()
5522 .unwrap();
5523
5524 parse(&spec, &input(&["ex", "src:docs"])).expect("both halves are choices");
5525
5526 let err = parse(&spec, &input(&["ex", "src:nowhere"])).unwrap_err();
5527 let message = err.to_string();
5528 assert!(message.contains("nowhere"), "{message}");
5529 assert!(
5530 !message.contains("src:nowhere"),
5531 "the bad half should be named, not the whole word: {message}"
5532 );
5533 }
5534
5535 #[test]
5536 fn a_split_value_is_counted_and_judged_as_values() {
5537 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <e>\" var=#true delimiter=\",\" var_max=2 {\n choices \"dev\" \"prod\"\n}\n"
5541 .parse()
5542 .unwrap();
5543
5544 parse(&spec, &input(&["ex", "--env", "dev,prod"])).expect("two values, both allowed");
5545 let err = parse(&spec, &input(&["ex", "--env", "dev,staging"])).unwrap_err();
5546 assert!(err.to_string().contains("staging"), "{err}");
5547 assert!(
5548 parse(&spec, &input(&["ex", "--env", "dev,prod,dev"])).is_err(),
5549 "three values should breach var_max=2"
5550 );
5551 }
5552
5553 #[test]
5554 fn a_split_bound_counts_one_occurrence_at_a_time() {
5555 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--include <pattern>...\" delimiter=\",\" {\n arg \"<pattern>...\" var=#true var_max=2\n}\n"
5560 .parse()
5561 .unwrap();
5562
5563 parse(&spec, &input(&["ex", "--include", "a,b"])).expect("exactly the bound is fine");
5564 assert!(
5565 parse(&spec, &input(&["ex", "--include", "a,b,c"])).is_err(),
5566 "three values out of one word is still three values"
5567 );
5568 parse(
5571 &spec,
5572 &input(&["ex", "--include", "a,b", "--include", "c,d"]),
5573 )
5574 .expect("two per occurrence, twice, is within the bound");
5575 }
5576
5577 #[test]
5578 fn a_nested_minimum_is_checked_once_per_flag_occurrence() {
5579 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pair <value>...\" {\n arg \"<value>...\" var=#true var_min=2 var_max=2\n}\n"
5580 .parse()
5581 .unwrap();
5582
5583 parse(
5584 &spec,
5585 &input(&["ex", "--pair", "a", "b", "--pair", "c", "d"]),
5586 )
5587 .expect("each occurrence satisfies the bound independently");
5588
5589 let error = parse(&spec, &input(&["ex", "--pair", "a", "--pair", "b", "c"])).unwrap_err();
5590 assert!(
5591 error
5592 .to_string()
5593 .contains("requires at least 2 value(s), got 1"),
5594 "{error:?}"
5595 );
5596 }
5597
5598 #[test]
5599 fn an_exclusive_flag_has_to_be_alone() {
5600 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--verbose\"\narg \"[target]\"\n"
5601 .parse()
5602 .unwrap();
5603
5604 parse(&spec, &input(&["ex", "--dump"])).expect("alone is the point");
5605
5606 let err = parse(&spec, &input(&["ex", "--dump", "--verbose"])).unwrap_err();
5608 assert!(err.to_string().contains("on its own"), "{err}");
5609
5610 let err = parse(&spec, &input(&["ex", "--dump", "t"])).unwrap_err();
5613 assert!(err.to_string().contains("on its own"), "{err}");
5614
5615 parse(&spec, &input(&["ex", "--verbose", "t"])).expect("without it, nothing changes");
5617 }
5618
5619 #[test]
5620 fn an_exclusive_flag_is_not_disturbed_by_a_default() {
5621 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--jobs <n>\" default=\"4\"\n"
5624 .parse()
5625 .unwrap();
5626
5627 parse(&spec, &input(&["ex", "--dump"])).expect("a default is nobody saying anything");
5628 assert!(parse(&spec, &input(&["ex", "--dump", "--jobs", "8"])).is_err());
5629 }
5630
5631 #[test]
5632 fn an_exclusive_flag_bypasses_required_siblings() {
5633 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" required=#true\narg \"<target>\"\n"
5634 .parse()
5635 .unwrap();
5636
5637 parse(&spec, &input(&["ex", "--dump"]))
5638 .expect("exclusive is the command's requiredness escape");
5639 assert!(parse(
5640 &spec,
5641 &input(&["ex", "--dump", "--out", "somewhere", "target"])
5642 )
5643 .is_err());
5644 }
5645
5646 #[test]
5647 fn an_environment_value_counts_for_an_exclusive_flag() {
5648 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--dump\" exclusive=#true\nflag \"--out <path>\" env=\"EX_OUT\"\n"
5649 .parse()
5650 .unwrap();
5651
5652 assert!(parse_with_env(&spec, &["ex", "--dump"], &[("EX_OUT", "somewhere")]).is_err());
5653 parse_with_env(&spec, &["ex", "--dump"], &[]).expect("without the value it is alone");
5654 }
5655
5656 #[test]
5657 fn a_selected_subcommand_counts_for_an_ancestor_exclusive_flag() {
5658 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--version\" global=#true exclusive=#true\ncmd \"run\"\n"
5659 .parse()
5660 .unwrap();
5661
5662 parse(&spec, &input(&["ex", "--version"])).expect("alone is allowed");
5663 assert!(parse(&spec, &input(&["ex", "--version", "run"])).is_err());
5664 }
5665
5666 #[test]
5667 fn a_child_exclusive_flag_is_not_mistaken_for_a_same_named_parent_flag() {
5668 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5669 .parse()
5670 .unwrap();
5671
5672 parse(&spec, &input(&["ex", "run", "--clean"]))
5673 .expect("the child flag is alone within the child command");
5674 assert!(
5675 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5676 "the parent flag still conflicts with selecting the child"
5677 );
5678 }
5679
5680 #[test]
5681 fn a_child_local_exclusive_redeclaration_belongs_to_the_child() {
5682 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5683 .parse()
5684 .unwrap();
5685
5686 parse(&spec, &input(&["ex", "run", "--clean"]))
5687 .expect("the child-local exclusive flag is alone inside the child command");
5688 assert!(
5689 parse(&spec, &input(&["ex", "--clean", "run"])).is_err(),
5690 "the ancestor spelling still conflicts with selecting the child"
5691 );
5692 }
5693
5694 #[test]
5695 fn a_same_named_parent_flag_is_company_for_a_child_exclusive_flag() {
5696 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" global=#true exclusive=#true\n}\n"
5697 .parse()
5698 .unwrap();
5699
5700 parse(&spec, &input(&["ex", "run", "--clean"])).expect("the child exclusive flag is alone");
5701 assert!(
5702 parse(&spec, &input(&["ex", "--clean", "run", "--clean"])).is_err(),
5703 "the distinct parent declaration is still company despite sharing a name"
5704 );
5705 }
5706
5707 #[test]
5708 fn a_local_child_redeclaration_keeps_its_exclusivity_when_merged() {
5709 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5710 .parse()
5711 .unwrap();
5712
5713 parse(&spec, &input(&["ex", "run", "--clean"]))
5714 .expect("the child exclusive flag is valid alone");
5715 assert!(
5716 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5717 "merging with the inherited global must not discard child exclusivity"
5718 );
5719 }
5720
5721 #[test]
5722 fn an_orphan_parent_alias_does_not_disown_a_child_local_exclusive_flag() {
5723 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5724 .parse()
5725 .unwrap();
5726
5727 parse(&spec, &input(&["ex", "run", "--clean"]))
5728 .expect("the typed long form belongs to the child declaration");
5729 assert!(
5730 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5731 "the inherited short form still belongs to the ancestor"
5732 );
5733 assert!(
5734 parse(&spec, &input(&["ex", "run", "-c", "--clean"])).is_err(),
5735 "a child spelling cannot mask the ancestor-exclusive occurrence on the same merged flag"
5736 );
5737 }
5738
5739 #[test]
5740 fn an_inherited_alias_keeps_its_ancestor_exclusivity() {
5741 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" global=#true\n}\n"
5742 .parse()
5743 .unwrap();
5744
5745 parse(&spec, &input(&["ex", "run", "--clean"]))
5746 .expect("the child's spelling does not activate the orphan ancestor alias");
5747 assert!(
5748 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5749 "the inherited short alias still belongs to the ancestor exclusive flag"
5750 );
5751 }
5752
5753 #[test]
5754 fn an_inherited_negated_alias_keeps_its_ancestor_exclusivity() {
5755 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"
5756 .parse()
5757 .unwrap();
5758
5759 assert!(
5760 parse(&spec, &input(&["ex", "run", "--no-clean"])).is_err(),
5761 "the inherited negated alias still belongs to the ancestor exclusive flag"
5762 );
5763 }
5764
5765 #[test]
5766 fn a_colliding_alias_does_not_disown_the_child_from_the_rest() {
5767 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"
5772 .parse()
5773 .unwrap();
5774
5775 parse(&spec, &input(&["ex", "run", "--clean"])).expect("alone is allowed");
5776 assert!(
5777 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5778 "one unrelated alias collision cannot disown the child from its own flag"
5779 );
5780 }
5781
5782 #[test]
5783 fn a_local_child_declaration_is_not_in_scope_before_the_subcommand() {
5784 let quiet: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5790 .parse()
5791 .unwrap();
5792 parse(&quiet, &input(&["ex", "--clean", "run", "--verbose"]))
5793 .expect("the ancestor owns this occurrence, and it is not exclusive");
5794 assert!(
5795 parse(&quiet, &input(&["ex", "run", "--clean", "--verbose"])).is_err(),
5796 "after the subcommand word the child's declaration is in scope, and it is exclusive"
5797 );
5798
5799 let loud: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n}\n"
5800 .parse()
5801 .unwrap();
5802 assert!(
5803 parse(&loud, &input(&["ex", "--clean", "run"])).is_err(),
5804 "the same rule, with an exclusive ancestor: selecting the child is company for it"
5805 );
5806 }
5807
5808 #[test]
5809 fn an_orphan_ancestor_alias_keeps_its_exclusivity_past_a_plain_child_redeclaration() {
5810 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true exclusive=#true\ncmd \"run\" {\n flag \"--clean\"\n flag \"--verbose\"\n}\n"
5814 .parse()
5815 .unwrap();
5816
5817 assert!(
5818 parse(&spec, &input(&["ex", "run", "-c"])).is_err(),
5819 "the orphan ancestor alias is still the ancestor's exclusive flag"
5820 );
5821 parse(&spec, &input(&["ex", "run", "--clean", "--verbose"]))
5822 .expect("the child's own spelling drops the exclusivity the child did not restate");
5823 }
5824
5825 #[test]
5826 fn a_child_spelling_carries_its_exclusivity_even_beside_an_ancestor_spelling() {
5827 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-c --clean\" global=#true\ncmd \"run\" {\n flag \"--clean\" exclusive=#true\n flag \"--verbose\"\n}\n"
5831 .parse()
5832 .unwrap();
5833
5834 assert!(
5835 parse(&spec, &input(&["ex", "run", "-c", "--clean", "--verbose"])).is_err(),
5836 "the child spelling is exclusive whatever it was typed beside"
5837 );
5838 parse(&spec, &input(&["ex", "run", "-c", "--verbose"]))
5839 .expect("the ancestor's own spelling was never exclusive");
5840 }
5841
5842 #[test]
5843 fn an_environment_value_takes_the_exclusivity_of_the_declaration_in_scope() {
5844 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"
5848 .parse()
5849 .unwrap();
5850
5851 assert!(
5852 parse_with_env(&added, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")]).is_err(),
5853 "the child added exclusivity the environment value has to honor"
5854 );
5855
5856 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"
5857 .parse()
5858 .unwrap();
5859
5860 parse_with_env(&dropped, &["ex", "run", "--verbose"], &[("EX_CLEAN", "1")])
5861 .expect("the child dropped the exclusivity, and the environment value follows it");
5862 }
5863
5864 #[test]
5865 fn a_merged_child_exclusive_flag_still_escapes_requiredness() {
5866 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"
5869 .parse()
5870 .unwrap();
5871
5872 parse(&spec, &input(&["ex", "run", "--clean"]))
5873 .expect("a merged child exclusive flag is still the command's requiredness escape");
5874 }
5875
5876 #[test]
5877 fn a_group_allows_one_member_and_refuses_two() {
5878 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\nflag \"--stdin\"\ngroup \"input\" \"--file\" \"--url\" \"--stdin\"\n"
5879 .parse()
5880 .unwrap();
5881
5882 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one member is fine");
5884 parse(&spec, &input(&["ex"])).expect("a group that is not required asks for nothing");
5885
5886 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--stdin"])).unwrap_err();
5887 assert!(err.to_string().contains("group input"), "{err}");
5888 }
5889
5890 #[test]
5891 fn positional_selectors_work_in_conflicts_and_groups() {
5892 let conflicts: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\" conflicts=\"value\"\narg \"[value]\"\n"
5893 .parse()
5894 .unwrap();
5895 parse(&conflicts, &input(&["ex", "--from-file", "vars.env"]))
5896 .expect("the flag alone is valid");
5897 parse(&conflicts, &input(&["ex", "literal"])).expect("the positional alone is valid");
5898 assert!(parse(
5899 &conflicts,
5900 &input(&["ex", "--from-file", "vars.env", "literal"])
5901 )
5902 .is_err());
5903
5904 let positional_source: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--from-file <path>\"\narg \"[value]\" conflicts=\"--from-file\"\n"
5905 .parse()
5906 .unwrap();
5907 assert!(parse(
5908 &positional_source,
5909 &input(&["ex", "--from-file", "vars.env", "literal"])
5910 )
5911 .is_err());
5912
5913 let group: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <path>\"\narg \"[target]\"\ngroup \"input\" \"--file\" \"target\" required=#true\n"
5914 .parse()
5915 .unwrap();
5916 assert!(parse(&group, &input(&["ex"])).is_err());
5917 parse(&group, &input(&["ex", "target-name"]))
5918 .expect("a positional satisfies a required group");
5919 assert!(parse(
5920 &group,
5921 &input(&["ex", "--file", "input.txt", "target-name"])
5922 )
5923 .is_err());
5924 }
5925
5926 #[test]
5927 fn a_required_group_needs_one_of_its_members() {
5928 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5929 .parse()
5930 .unwrap();
5931
5932 let err = parse(&spec, &input(&["ex"])).unwrap_err();
5933 assert!(err.to_string().contains("--file, --url"), "{err}");
5936 assert!(err.to_string().contains("input"), "{err}");
5937
5938 parse(&spec, &input(&["ex", "--url", "u"])).expect("one member satisfies it");
5939 }
5940
5941 #[test]
5942 fn a_multiple_group_only_polices_requiredness() {
5943 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--a\"\nflag \"--b\"\ngroup \"any\" \"--a\" \"--b\" required=#true multiple=#true\n"
5946 .parse()
5947 .unwrap();
5948
5949 parse(&spec, &input(&["ex", "--a", "--b"])).expect("multiple allows both");
5950 assert!(parse(&spec, &input(&["ex"])).is_err());
5951 }
5952
5953 #[test]
5954 fn a_group_reads_a_default_for_requiredness_and_not_for_exclusivity() {
5955 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" default=\"a.txt\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5961 .parse()
5962 .unwrap();
5963
5964 parse(&spec, &input(&["ex"])).expect("the default fills the group");
5965 parse(&spec, &input(&["ex", "--url", "u"]))
5966 .expect("the default must not conflict with the flag the user typed");
5967 }
5968
5969 #[test]
5970 fn a_group_naming_two_spellings_of_one_flag_is_not_a_conflict() {
5971 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-f --file <f>\"\nflag \"--url <u>\"\ngroup \"input\" \"-f\" \"--file\" \"--url\"\n"
5974 .parse()
5975 .unwrap();
5976
5977 parse(&spec, &input(&["ex", "--file", "a.txt"])).expect("one flag is one member");
5978 parse(&spec, &input(&["ex", "-f", "a.txt"])).expect("either spelling, still one member");
5979
5980 let err = parse(&spec, &input(&["ex", "--file", "a.txt", "--url", "u"])).unwrap_err();
5982 assert!(err.to_string().contains("group input"), "{err}");
5983 }
5984
5985 #[test]
5986 fn a_group_reads_the_environment_as_given() {
5987 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" env=\"EX_FILE\"\nflag \"--url <u>\"\ngroup \"input\" \"--file\" \"--url\" required=#true\n"
5990 .parse()
5991 .unwrap();
5992
5993 parse_with_env(&spec, &["ex"], &[("EX_FILE", "a.txt")]).expect("the environment fills it");
5994 }
5995
5996 #[test]
5997 fn a_requirement_names_the_flag_that_is_missing() {
5998 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\"\n"
6001 .parse()
6002 .unwrap();
6003
6004 let err = parse(&spec, &input(&["ex", "--out", "a.txt"])).unwrap_err();
6005 assert!(
6006 err.to_string().contains("format"),
6007 "the missing flag should be named: {err}"
6008 );
6009
6010 for words in [
6012 &["ex", "--out", "a.txt", "--format", "json"][..],
6013 &["ex", "--format", "json", "--out", "a.txt"][..],
6014 ] {
6015 parse(&spec, &input(words)).unwrap_or_else(|e| panic!("{words:?}: {e}"));
6016 }
6017
6018 parse(&spec, &input(&["ex"])).expect("a bare invocation requires nothing");
6021 }
6022
6023 #[test]
6024 fn a_value_activates_only_its_conditional_requirement() {
6025 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"
6026 .parse()
6027 .unwrap();
6028
6029 parse(&spec, &input(&["ex", "--config", "ordinary.toml"]))
6030 .expect("an unrelated value requires nothing");
6031
6032 let key = parse(&spec, &input(&["ex", "--config", "special.toml"])).unwrap_err();
6033 assert!(key.to_string().contains("key"), "{key}");
6034 parse(
6035 &spec,
6036 &input(&["ex", "--config", "special.toml", "--key", "secret"]),
6037 )
6038 .expect("the matching requirement is satisfied");
6039
6040 let token = parse(&spec, &input(&["ex", "--config", "remote.toml"])).unwrap_err();
6041 assert!(token.to_string().contains("token"), "{token}");
6042 }
6043
6044 #[test]
6045 fn conditional_requirements_read_explicit_env_but_not_defaults() {
6046 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"
6047 .parse()
6048 .unwrap();
6049 let err = parse_with_env(&from_env, &["ex"], &[("EX_CONFIG", "special.toml")]).unwrap_err();
6050 assert!(err.to_string().contains("key"), "{err}");
6051
6052 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"
6053 .parse()
6054 .unwrap();
6055 parse(&from_default, &input(&["ex"]))
6056 .expect("a default is not an explicit conditional value");
6057 }
6058
6059 #[test]
6060 fn command_line_values_override_env_for_conditional_requirements() {
6061 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--config <file>\" env=\"EX_CONFIG\" {\n requires_if \"special.toml\" \"--key\"\n}\nflag \"--key <key>\"\n"
6062 .parse()
6063 .unwrap();
6064
6065 parse_with_env(
6066 &spec,
6067 &["ex", "--config", "ordinary.toml"],
6068 &[("EX_CONFIG", "special.toml")],
6069 )
6070 .expect("the command-line value takes precedence over the environment");
6071 }
6072
6073 #[test]
6074 fn conditional_requirements_normalize_boolean_env_values() {
6075 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--feature\" env=\"EX_FEATURE\" {\n requires_if \"true\" \"--key\"\n}\nflag \"--key <key>\"\n"
6076 .parse()
6077 .unwrap();
6078
6079 for value in ["1", "true", "True", "TRUE"] {
6080 let err = parse_with_env(&spec, &["ex"], &[("EX_FEATURE", value)]).unwrap_err();
6081 assert!(err.to_string().contains("key"), "{value}: {err}");
6082 }
6083 parse_with_env(&spec, &["ex"], &[("EX_FEATURE", "false")])
6084 .expect("a false environment value does not activate a true condition");
6085 }
6086
6087 #[test]
6088 fn a_default_satisfies_a_requirement() {
6089 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" default=\"json\"\n"
6093 .parse()
6094 .unwrap();
6095
6096 parse(&spec, &input(&["ex", "--out", "a.txt"]))
6097 .expect("a defaulted flag is not a missing one");
6098 }
6099
6100 #[test]
6101 fn a_present_flag_binds_a_conditional_default() {
6102 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6103 .parse()
6104 .unwrap();
6105
6106 let with = parse(&spec, &input(&["ex", "--json"])).unwrap();
6107 assert_eq!(
6108 with.as_env().get("usage_bin_names").map(String::as_str),
6109 Some("true")
6110 );
6111
6112 let without = parse(&spec, &input(&["ex"])).unwrap();
6113 assert!(
6114 !without.as_env().contains_key("usage_bin_names"),
6115 "IsPresent does nothing when the selector is absent"
6116 );
6117 }
6118
6119 #[test]
6120 fn an_equals_condition_binds_a_conditional_default() {
6121 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--style <s>\" {\n default_if \"--output\" \"json\" \"pretty\"\n}\nflag \"--output <fmt>\"\n"
6122 .parse()
6123 .unwrap();
6124
6125 let json = parse(&spec, &input(&["ex", "--output", "json"])).unwrap();
6126 assert_eq!(
6127 json.as_env().get("usage_style").map(String::as_str),
6128 Some("pretty")
6129 );
6130 let yaml = parse(&spec, &input(&["ex", "--output", "yaml"])).unwrap();
6131 assert!(!yaml.as_env().contains_key("usage_style"));
6132 }
6133
6134 #[test]
6135 fn an_equals_condition_reads_a_negated_flag() {
6136 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"false\" \"true\"\n}\nflag \"--json\" negate=\"--no-json\"\n"
6137 .parse()
6138 .unwrap();
6139
6140 let off = parse(&spec, &input(&["ex", "--no-json"])).unwrap();
6141 assert_eq!(
6142 off.as_env().get("usage_pretty").map(String::as_str),
6143 Some("true")
6144 );
6145 let on = parse(&spec, &input(&["ex", "--json"])).unwrap();
6146 assert!(
6147 !on.as_env().contains_key("usage_pretty"),
6148 "--json is true, so when=false should miss"
6149 );
6150 }
6151
6152 #[test]
6153 fn the_first_matching_conditional_default_wins() {
6154 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"
6155 .parse()
6156 .unwrap();
6157
6158 let out = parse(&spec, &input(&["ex", "--json", "--pretty"])).unwrap();
6159 assert_eq!(
6160 out.as_env().get("usage_style").map(String::as_str),
6161 Some("compact")
6162 );
6163 }
6164
6165 #[test]
6166 fn argv_and_env_suppress_a_conditional_default() {
6167 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" env=\"EX_BIN\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\"\n"
6168 .parse()
6169 .unwrap();
6170
6171 let from_env = parse_with_env(&spec, &["ex", "--json"], &[("EX_BIN", "false")]).unwrap();
6172 assert_eq!(
6173 from_env.as_env().get("usage_bin_names").map(String::as_str),
6174 Some("false"),
6175 "the target's environment wins over default_if"
6176 );
6177 }
6178
6179 #[test]
6180 fn a_sibling_env_activates_a_conditional_default() {
6181 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--bin-names\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" env=\"EX_JSON\"\n"
6182 .parse()
6183 .unwrap();
6184
6185 let out = parse_with_env(&spec, &["ex"], &[("EX_JSON", "1")]).unwrap();
6186 assert_eq!(
6187 out.as_env().get("usage_bin_names").map(String::as_str),
6188 Some("true")
6189 );
6190 }
6191
6192 #[test]
6193 fn a_default_does_not_activate_a_conditional_default() {
6194 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--pretty\" {\n default_if \"--json\" \"true\"\n}\nflag \"--json\" default=#true\n"
6198 .parse()
6199 .unwrap();
6200
6201 let out = parse(&spec, &input(&["ex"])).unwrap();
6202 assert_eq!(
6203 out.as_env().get("usage_json").map(String::as_str),
6204 Some("true")
6205 );
6206 assert!(
6207 !out.as_env().contains_key("usage_pretty"),
6208 "a default is not an explicit value for default_if"
6209 );
6210 }
6211
6212 #[test]
6213 fn a_conditional_default_does_not_activate_requires_if() {
6214 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"
6215 .parse()
6216 .unwrap();
6217
6218 parse(&spec, &input(&["ex", "--json"]))
6219 .expect("a default_if value is not explicit for requires_if");
6220 assert!(parse(&spec, &input(&["ex", "--format", "json"])).is_err());
6221 }
6222
6223 #[test]
6224 fn a_conditional_default_satisfies_a_requirement() {
6225 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" {\n default_if \"--json\" \"json\"\n}\nflag \"--json\"\n"
6226 .parse()
6227 .unwrap();
6228
6229 parse(&spec, &input(&["ex", "--out", "a.txt", "--json"]))
6230 .expect("default_if fills the required flag");
6231 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6232 }
6233
6234 #[test]
6235 fn an_environment_value_satisfies_a_requirement() {
6236 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--out <p>\" requires=\"--format\"\nflag \"--format <f>\" env=\"EX_FORMAT\"\n"
6237 .parse()
6238 .unwrap();
6239
6240 assert!(parse(&spec, &input(&["ex", "--out", "a.txt"])).is_err());
6241 parse_with_env(&spec, &["ex", "--out", "a.txt"], &[("EX_FORMAT", "json")])
6242 .expect("the environment supplies it");
6243 }
6244
6245 #[test]
6246 fn a_requirement_is_satisfied_by_a_short_form() {
6247 let spec: Spec =
6251 "name \"ex\"\nbin \"ex\"\nflag \"--sign\" requires=\"-k\"\nflag \"-k --key <k>\"\n"
6252 .parse()
6253 .unwrap();
6254
6255 parse(&spec, &input(&["ex", "--sign", "--key", "x"])).expect("--key satisfies -k");
6256
6257 let err = parse(&spec, &input(&["ex", "--sign"])).unwrap_err();
6258 assert!(err.to_string().contains("key"), "{err}");
6259 }
6260
6261 #[test]
6262 fn conflicting_flags_are_rejected_in_either_order() {
6263 let spec: Spec =
6267 "name \"ex\"\nbin \"ex\"\nflag \"--file <f>\" conflicts=\"--stdin\"\nflag \"--stdin\"\n"
6268 .parse()
6269 .unwrap();
6270
6271 for words in [
6272 &["ex", "--file", "a.txt", "--stdin"][..],
6273 &["ex", "--stdin", "--file", "a.txt"][..],
6274 ] {
6275 let err = parse(&spec, &input(words)).unwrap_err();
6276 assert!(
6277 err.to_string().contains("conflicts with --stdin"),
6278 "{words:?} should be refused: {err}"
6279 );
6280 }
6281
6282 parse(&spec, &input(&["ex", "--stdin"])).unwrap();
6284 parse(&spec, &input(&["ex", "--file", "a.txt"])).unwrap();
6285 }
6286
6287 #[test]
6288 fn unknown_flags_are_values_by_default() {
6289 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--force\"\narg \"[rest]...\"\n"
6292 .parse()
6293 .unwrap();
6294 let out = parse(
6295 &spec,
6296 &["ex".to_string(), "--wat".to_string(), "x".to_string()],
6297 )
6298 .unwrap();
6299 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6300 assert_eq!(out.args[rest].to_string(), "--wat x");
6301 }
6302
6303 #[test]
6304 fn repeated_scalar_flags_override_by_default_and_can_be_strict() {
6305 let permissive: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6306 .parse()
6307 .unwrap();
6308 let out = parse(&permissive, &input(&["ex", "--jobs", "1", "--jobs", "2"]))
6309 .expect("a repeat is a correction by default");
6310 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6311 assert_eq!(out.flags[jobs].to_string(), "2");
6312 parse(&permissive, &input(&["ex", "--verbose", "--verbose"]))
6313 .expect("switches use the same default");
6314
6315 let strict: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--jobs <n>\"\nflag \"--verbose\"\n"
6316 .parse()
6317 .unwrap();
6318 for words in [
6319 &["ex", "--jobs", "1", "--jobs", "2"][..],
6320 &["ex", "--verbose", "--verbose"][..],
6321 ] {
6322 let err = parse(&strict, &input(words)).unwrap_err();
6323 assert!(
6324 err.to_string().contains("cannot be used multiple times"),
6325 "{err}"
6326 );
6327 }
6328
6329 let reparsed: Spec = strict.to_string().parse().unwrap();
6330 assert!(!reparsed.cmd.args_override_self);
6331 }
6332
6333 #[test]
6334 fn strict_negated_flags_allow_opposite_forms_but_reject_the_same_form() {
6335 let spec: Spec = "name \"ex\"\nbin \"ex\"\nargs_override_self #false\nflag \"--color\" negate=\"--no-color\"\n"
6336 .parse()
6337 .unwrap();
6338
6339 let out = parse(&spec, &input(&["ex", "--color", "--no-color"]))
6340 .expect("opposite forms override each other");
6341 let color = out.flags.keys().find(|f| f.name == "color").unwrap();
6342 assert!(matches!(out.flags[color], ParseValue::Bool(false)));
6343
6344 for words in [
6345 &["ex", "--color", "--color"][..],
6346 &["ex", "--no-color", "--no-color"][..],
6347 ] {
6348 let err = parse(&spec, &input(words)).unwrap_err();
6349 assert!(err.to_string().contains("cannot be used multiple times"));
6350 }
6351 }
6352
6353 #[test]
6354 fn strict_global_flags_may_repeat_across_command_levels() {
6355 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"
6356 .parse()
6357 .unwrap();
6358
6359 let out = parse(
6360 &spec,
6361 &input(&[
6362 "ex", "--color", "--jobs", "1", "run", "--color", "--jobs", "2",
6363 ]),
6364 )
6365 .expect("an inherited global is allowed once at each command level");
6366 let jobs = out.flags.keys().find(|f| f.name == "jobs").unwrap();
6367 assert_eq!(out.flags[jobs].to_string(), "2");
6368
6369 for words in [
6370 &["ex", "--color", "--color", "run", "--no-color"][..],
6371 &["ex", "--jobs", "1", "run", "--jobs", "2", "--jobs", "3"][..],
6372 ] {
6373 let err = parse(&spec, &input(words)).unwrap_err();
6374 assert!(err.to_string().contains("cannot be used multiple times"));
6375 }
6376 }
6377
6378 #[test]
6379 fn a_subcommand_can_negate_only_its_parents_requirements() {
6380 let base = r#"name "ex"
6381bin "ex"
6382flag "--config" required=#true
6383flag "--mode" requires="--config"
6384flag "--other"
6385arg "<input>"
6386group "source" "--config" "--other" required=#true
6387cmd "run" { flag "--child" required=#true }
6388"#;
6389 let strict: Spec = base.parse().unwrap();
6390 let err = parse(&strict, &input(&["ex", "run"])).unwrap_err();
6391 let message = err.to_string();
6392 assert!(
6393 message.contains("input") || message.contains("config"),
6394 "{message}"
6395 );
6396
6397 let negated: Spec = base
6398 .replacen("bin \"ex\"", "bin \"ex\"\nsubcommand_negates_reqs #true", 1)
6399 .parse()
6400 .unwrap();
6401 let err = parse(&negated, &input(&["ex", "run"])).unwrap_err();
6402 assert!(
6403 err.to_string().contains("child"),
6404 "the selected command keeps its own requirements: {err}"
6405 );
6406
6407 let mut child_optional = negated.clone();
6408 child_optional.cmd.subcommands["run"].flags[0].required = false;
6409 parse(&child_optional, &input(&["ex", "run"]))
6410 .expect("the child selection satisfies all parent requirements");
6411 parse(&child_optional, &input(&["ex", "--mode", "run"]))
6412 .expect("parent requires relationships are negated too");
6413 }
6414
6415 #[test]
6416 fn a_parent_argument_can_conflict_with_a_later_subcommand() {
6417 let spec: Spec = r#"name "ex"
6418bin "ex"
6419args_conflicts_with_subcommands #true
6420flag "--verbose"
6421cmd "run"
6422"#
6423 .parse()
6424 .unwrap();
6425
6426 parse(&spec, &input(&["ex", "run"]))
6427 .expect("the subcommand is valid without a parent argument");
6428 let err = parse(&spec, &input(&["ex", "--verbose", "run"])).unwrap_err();
6429 assert!(
6430 err.to_string().contains("cannot be used with arguments"),
6431 "{err}"
6432 );
6433 }
6434
6435 #[test]
6436 fn a_subcommand_can_take_precedence_over_a_variadic_flag() {
6437 let base = r#"name "ex"
6438bin "ex"
6439flag "--values <value>..."
6440cmd "run"
6441"#;
6442 let plain: Spec = base.parse().unwrap();
6443 let out = parse(&plain, &input(&["ex", "--values", "a", "run"])).unwrap();
6444 assert_eq!(out.cmd.name, "ex");
6445
6446 let precedence: Spec = base
6447 .replacen(
6448 "bin \"ex\"",
6449 "bin \"ex\"\nsubcommand_precedence_over_arg #true",
6450 1,
6451 )
6452 .parse()
6453 .unwrap();
6454 let out = parse(&precedence, &input(&["ex", "--values", "a", "run"])).unwrap();
6455 assert_eq!(out.cmd.name, "run");
6456 }
6457
6458 #[test]
6459 fn a_required_positional_can_follow_an_unfilled_optional_one() {
6460 let base = r#"name "ex"
6461bin "ex"
6462arg "[optional]"
6463arg "<required>"
6464"#;
6465 let plain: Spec = base.parse().unwrap();
6466 let err = parse(&plain, &input(&["ex", "value"])).unwrap_err();
6467 assert!(err.to_string().contains("required"), "{err}");
6468
6469 let enabled: Spec = base
6470 .replacen(
6471 "bin \"ex\"",
6472 "bin \"ex\"\nallow_missing_positional #true",
6473 1,
6474 )
6475 .parse()
6476 .unwrap();
6477 let out = parse(&enabled, &input(&["ex", "value"])).unwrap();
6478 assert!(!out.args.keys().any(|arg| arg.name == "optional"));
6479 let value = &out
6480 .args
6481 .iter()
6482 .find(|(arg, _)| arg.name == "required")
6483 .unwrap()
6484 .1;
6485 assert!(matches!(value, ParseValue::String(value) if value == "value"));
6486 }
6487
6488 #[test]
6489 fn unknown_flags_can_be_rejected_for_the_whole_cli() {
6490 let spec: Spec =
6491 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\nflag \"--force\"\nflag \"-0 --print0\"\narg \"[rest]...\" allow_negative_numbers=#true\n"
6492 .parse()
6493 .unwrap();
6494 let err = parse(&spec, &["ex".to_string(), "--wat".to_string()]).unwrap_err();
6495 assert!(
6496 err.to_string().contains("--wat"),
6497 "the message should name the token: {err}"
6498 );
6499
6500 let out = parse(&spec, &["ex".to_string(), "-1".to_string()]).unwrap();
6503 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6504 assert_eq!(out.args[rest].to_string(), "-1");
6505
6506 let out = parse(&spec, &["ex".to_string(), "-0".to_string()]).unwrap();
6507 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6508 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6509 }
6510
6511 #[test]
6512 fn a_declared_digit_short_does_not_stop_the_subcommand_scan() {
6513 let spec: Spec = r#"
6514name "ex"
6515bin "ex"
6516unknown_flags "error"
6517flag "-0 --print0" global=#true
6518cmd "run" {
6519 flag "--force"
6520}
6521"#
6522 .parse()
6523 .unwrap();
6524 let out = parse(&spec, &input(&["ex", "-0", "run", "--force"])).unwrap();
6525 assert_eq!(out.cmd.name, "run");
6526 let print0 = out.flags.keys().find(|flag| flag.name == "print0").unwrap();
6527 let force = out.flags.keys().find(|flag| flag.name == "force").unwrap();
6528 assert!(matches!(out.flags[print0], ParseValue::Bool(true)));
6529 assert!(matches!(out.flags[force], ParseValue::Bool(true)));
6530 }
6531
6532 #[test]
6533 fn a_command_may_override_the_cli_wide_setting() {
6534 let spec: Spec = r#"
6536name "ex"
6537bin "ex"
6538unknown_flags "error"
6539cmd "exec" unknown_flags="value" {
6540 arg "[rest]..."
6541}
6542cmd "build" {
6543 arg "[rest]..."
6544}
6545"#
6546 .parse()
6547 .unwrap();
6548
6549 let out = parse(
6550 &spec,
6551 &["ex".to_string(), "exec".to_string(), "--wat".to_string()],
6552 )
6553 .unwrap();
6554 let rest = out.args.keys().find(|a| a.name == "rest").unwrap();
6555 assert_eq!(out.args[rest].to_string(), "--wat");
6556
6557 assert!(
6558 parse(
6559 &spec,
6560 &["ex".to_string(), "build".to_string(), "--wat".to_string()]
6561 )
6562 .is_err(),
6563 "a command that says nothing inherits the CLI's choice"
6564 );
6565 }
6566
6567 #[test]
6568 fn the_setting_survives_a_round_trip() {
6569 let spec: Spec =
6570 "name \"ex\"\nbin \"ex\"\nunknown_flags \"error\"\ncmd \"x\" unknown_flags=\"value\"\n"
6571 .parse()
6572 .unwrap();
6573 let reparsed: Spec = spec.to_string().parse().unwrap();
6574 assert_eq!(reparsed.unknown_flags, Some(UnknownFlags::Error));
6575 assert_eq!(
6576 reparsed.cmd.subcommands["x"].unknown_flags,
6577 Some(UnknownFlags::Value)
6578 );
6579 }
6580
6581 #[test]
6582 fn test_default_subcommand() {
6583 let run_cmd = SpecCommand::builder()
6585 .name("run")
6586 .arg(SpecArg::builder().name("task").build())
6587 .build();
6588 let mut cmd = SpecCommand::builder().name("test").build();
6589 cmd.subcommands.insert("run".to_string(), run_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(), "mytask".to_string()];
6601 let parsed = parse(&spec, &input).unwrap();
6602
6603 assert_eq!(parsed.cmds.len(), 2);
6605 assert_eq!(parsed.cmds[1].name, "run");
6606
6607 assert_eq!(parsed.args.len(), 1);
6609 let arg = parsed.args.keys().next().unwrap();
6610 assert_eq!(arg.name, "task");
6611 let value = parsed.args.values().next().unwrap();
6612 assert_eq!(value.to_string(), "mytask");
6613 }
6614
6615 #[test]
6616 fn test_default_subcommand_explicit_still_works() {
6617 let run_cmd = SpecCommand::builder()
6619 .name("run")
6620 .arg(SpecArg::builder().name("task").build())
6621 .build();
6622 let other_cmd = SpecCommand::builder()
6623 .name("other")
6624 .arg(SpecArg::builder().name("other_arg").build())
6625 .build();
6626 let mut cmd = SpecCommand::builder().name("test").build();
6627 cmd.subcommands.insert("run".to_string(), run_cmd);
6628 cmd.subcommands.insert("other".to_string(), other_cmd);
6629
6630 let spec = Spec {
6631 name: "test".to_string(),
6632 bin: "test".to_string(),
6633 cmd,
6634 default_subcommand: Some("run".to_string()),
6635 ..Default::default()
6636 };
6637
6638 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
6640 let parsed = parse(&spec, &input).unwrap();
6641
6642 assert_eq!(parsed.cmds.len(), 2);
6644 assert_eq!(parsed.cmds[1].name, "other");
6645 }
6646
6647 #[test]
6648 fn test_default_subcommand_applies_only_at_the_root() {
6649 let mut config_ls = SpecCommand::builder().name("ls").build();
6655 config_ls.args.push(SpecArg::builder().name("what").build());
6656 let mut config_cmd = SpecCommand::builder().name("config").build();
6657 config_cmd.subcommands.insert("ls".to_string(), config_ls);
6658
6659 let mut root_ls = SpecCommand::builder().name("ls").build();
6662 root_ls.args.push(SpecArg::builder().name("what").build());
6663 let mut cmd = SpecCommand::builder().name("ex").build();
6664 cmd.subcommands.insert("ls".to_string(), root_ls);
6665 cmd.subcommands.insert("config".to_string(), config_cmd);
6666
6667 let spec = Spec {
6668 name: "ex".to_string(),
6669 bin: "ex".to_string(),
6670 cmd,
6671 default_subcommand: Some("ls".to_string()),
6672 ..Default::default()
6673 };
6674
6675 let input = vec!["ex".to_string(), "config".to_string(), "zzz".to_string()];
6679 assert!(
6680 parse(&spec, &input).is_err(),
6681 "`config` has no default subcommand and no argument, so `zzz` cannot bind"
6682 );
6683
6684 let input = vec!["ex".to_string(), "zzz".to_string()];
6686 let parsed = parse(&spec, &input).expect("the root's default applies");
6687 assert_eq!(
6688 parsed
6689 .cmds
6690 .iter()
6691 .map(|c| c.name.as_str())
6692 .collect::<Vec<_>>(),
6693 ["ex", "ls"]
6694 );
6695 assert_eq!(
6696 parsed.args.values().next().map(|v| v.to_string()),
6697 Some("zzz".to_string()),
6698 "and the word binds inside the command it reached"
6699 );
6700 }
6701
6702 #[test]
6703 fn test_default_subcommand_with_nested_subcommands() {
6704 let say_cmd = SpecCommand::builder()
6708 .name("say")
6709 .arg(SpecArg::builder().name("name").build())
6710 .build();
6711 let mut run_cmd = SpecCommand::builder().name("run").build();
6712 run_cmd.subcommands.insert("say".to_string(), say_cmd);
6713
6714 let mut cmd = SpecCommand::builder().name("test").build();
6715 cmd.subcommands.insert("run".to_string(), run_cmd);
6716
6717 let spec = Spec {
6718 name: "test".to_string(),
6719 bin: "test".to_string(),
6720 cmd,
6721 default_subcommand: Some("run".to_string()),
6722 ..Default::default()
6723 };
6724
6725 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
6727 let parsed = parse(&spec, &input).unwrap();
6728
6729 assert_eq!(parsed.cmds.len(), 3);
6731 assert_eq!(parsed.cmds[0].name, "test");
6732 assert_eq!(parsed.cmds[1].name, "run");
6733 assert_eq!(parsed.cmds[2].name, "say");
6734
6735 assert_eq!(parsed.args.len(), 1);
6737 let arg = parsed.args.keys().next().unwrap();
6738 assert_eq!(arg.name, "name");
6739 let value = parsed.args.values().next().unwrap();
6740 assert_eq!(value.to_string(), "hello");
6741 }
6742
6743 fn mounted_global_flag_spec() -> Spec {
6751 let task_cmd = SpecCommand::builder()
6752 .name("sample:run")
6753 .arg(
6754 SpecArg::builder()
6755 .name("profile")
6756 .choices(["alpha", "beta", "gamma"])
6757 .build(),
6758 )
6759 .build();
6760 let mut run_cmd = SpecCommand::builder()
6762 .name("run")
6763 .flag(
6764 SpecFlag::builder()
6765 .name("cd")
6766 .short('C')
6767 .long("cd")
6768 .arg(SpecArg::builder().name("dir").build())
6769 .global(false)
6770 .build(),
6771 )
6772 .build();
6773 run_cmd
6774 .subcommands
6775 .insert("sample:run".to_string(), task_cmd);
6776
6777 let mut cmd = SpecCommand::builder()
6778 .name("test")
6779 .flag(
6780 SpecFlag::builder()
6781 .name("cd")
6782 .short('C')
6783 .long("cd")
6784 .arg(SpecArg::builder().name("dir").build())
6785 .global(true)
6786 .build(),
6787 )
6788 .build();
6789 cmd.subcommands.insert("run".to_string(), run_cmd);
6790
6791 Spec {
6792 name: "test".to_string(),
6793 bin: "test".to_string(),
6794 cmd,
6795 ..Default::default()
6796 }
6797 }
6798
6799 #[test]
6800 fn test_prefix_global_flag_does_not_pollute_choices() {
6801 let spec = mounted_global_flag_spec();
6808
6809 for words in [
6812 &["test", "-C", "/tmp", "run", "sample:run"][..],
6813 &["test", "--cd=/tmp", "run", "sample:run"][..],
6815 ] {
6816 let parsed = parse_partial(&spec, &input(words)).unwrap();
6817 assert_eq!(
6818 parsed
6819 .cmds
6820 .iter()
6821 .map(|c| c.name.as_str())
6822 .collect::<Vec<_>>(),
6823 vec!["test", "run", "sample:run"],
6824 );
6825 assert!(
6827 parsed.args.is_empty(),
6828 "args should be empty, got {:?}",
6829 parsed.args
6830 );
6831
6832 let cd = parsed
6835 .available_flags
6836 .get("--cd")
6837 .expect("--cd should remain available after descending into the subcommand");
6838 assert!(cd.global, "--cd must stay global after descent");
6839 assert!(
6840 parsed.available_flags.get("-C").is_some_and(|f| f.global),
6841 "-C must stay global after descent",
6842 );
6843
6844 assert_eq!(
6848 parsed.as_env().get("usage_cd").map(String::as_str),
6849 Some("/tmp"),
6850 "global flag value must survive in as_env(), got {:?}",
6851 parsed.as_env(),
6852 );
6853 }
6854
6855 let parsed = parse_partial(
6857 &spec,
6858 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
6859 )
6860 .unwrap();
6861 assert_eq!(parsed.args.len(), 1);
6862 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6863
6864 assert_parse_err(
6866 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
6867 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
6868 );
6869 }
6870
6871 fn mounted_orphan_short_spec() -> Spec {
6876 let task_cmd = SpecCommand::builder()
6877 .name("sample:run")
6878 .arg(
6879 SpecArg::builder()
6880 .name("profile")
6881 .choices(["alpha", "beta", "gamma"])
6882 .build(),
6883 )
6884 .build();
6885 let mut run_cmd = SpecCommand::builder()
6888 .name("run")
6889 .flag(
6890 SpecFlag::builder()
6891 .name("raw")
6892 .short('r')
6893 .long("raw")
6894 .global(false)
6895 .build(),
6896 )
6897 .flag(
6898 SpecFlag::builder()
6899 .name("force")
6900 .short('f')
6901 .long("force")
6902 .global(false)
6903 .build(),
6904 )
6905 .build();
6906 run_cmd
6907 .subcommands
6908 .insert("sample:run".to_string(), task_cmd);
6909
6910 let mut cmd = SpecCommand::builder()
6912 .name("test")
6913 .flag(
6914 SpecFlag::builder()
6915 .name("raw")
6916 .long("raw")
6917 .global(true)
6918 .build(),
6919 )
6920 .build();
6921 cmd.subcommands.insert("run".to_string(), run_cmd);
6922
6923 Spec {
6924 name: "test".to_string(),
6925 bin: "test".to_string(),
6926 cmd,
6927 ..Default::default()
6928 }
6929 }
6930
6931 #[test]
6932 fn test_orphan_short_alias_survives_merge() {
6933 let spec = mounted_orphan_short_spec();
6939
6940 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
6941 assert_eq!(
6942 parsed
6943 .cmds
6944 .iter()
6945 .map(|c| c.name.as_str())
6946 .collect::<Vec<_>>(),
6947 vec!["test", "run", "sample:run"],
6948 );
6949
6950 assert!(
6953 parsed.available_flags.get("-r").is_some_and(|f| f.global),
6954 "-r must be merged onto the inherited global flag and stay global after descent",
6955 );
6956 assert!(
6957 parsed
6958 .available_flags
6959 .get("--raw")
6960 .is_some_and(|f| f.global),
6961 "--raw must stay global after descent",
6962 );
6963
6964 assert!(
6966 parsed.args.is_empty(),
6967 "args should be empty, got {:?}",
6968 parsed.args
6969 );
6970
6971 assert_eq!(
6973 parsed.as_env().get("usage_raw").map(String::as_str),
6974 Some("true"),
6975 "merged short's value must survive in as_env(), got {:?}",
6976 parsed.as_env(),
6977 );
6978
6979 assert!(
6982 !parsed.available_flags.contains_key("-f"),
6983 "purely-local -f must not be promoted onto a global",
6984 );
6985 assert!(
6986 !parsed.available_flags.contains_key("--force"),
6987 "purely-local --force must not be promoted onto a global",
6988 );
6989
6990 let parsed =
6992 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
6993 assert_eq!(parsed.args.len(), 1);
6994 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
6995
6996 assert_parse_err(
6998 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
6999 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
7000 );
7001 }
7002
7003 #[test]
7004 fn test_orphan_short_does_not_clobber_unrelated_global() {
7005 let run_cmd = SpecCommand::builder()
7010 .name("run")
7011 .flag(
7012 SpecFlag::builder()
7013 .name("raw")
7014 .short('r')
7015 .long("raw")
7016 .global(false)
7017 .build(),
7018 )
7019 .build();
7020 let mut cmd = SpecCommand::builder()
7021 .name("test")
7022 .flag(
7023 SpecFlag::builder()
7024 .name("raw")
7025 .long("raw")
7026 .global(true)
7027 .build(),
7028 )
7029 .flag(
7030 SpecFlag::builder()
7031 .name("restrict")
7032 .short('r')
7033 .long("restrict")
7034 .global(true)
7035 .build(),
7036 )
7037 .build();
7038 cmd.subcommands.insert("run".to_string(), run_cmd);
7039 let spec = Spec {
7040 name: "test".to_string(),
7041 bin: "test".to_string(),
7042 cmd,
7043 ..Default::default()
7044 };
7045
7046 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7047 assert_eq!(
7049 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
7050 Some("restrict"),
7051 "-r must remain owned by the unrelated global it already belonged to",
7052 );
7053 assert!(parsed
7055 .available_flags
7056 .get("--raw")
7057 .is_some_and(|f| f.global));
7058 assert!(parsed
7059 .available_flags
7060 .get("--restrict")
7061 .is_some_and(|f| f.global));
7062 }
7063
7064 #[test]
7065 fn test_redeclared_global_aliases_share_one_flag() {
7066 let spec = r#"
7073flag "-y --yes" global=#true effect="write"
7074cmd "run" {
7075 flag "-y --yes --assume-yes"
7076}
7077"#
7078 .parse::<Spec>()
7079 .unwrap();
7080
7081 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7082
7083 for key in ["-y", "--yes", "--assume-yes"] {
7084 let flag = parsed
7085 .available_flags
7086 .get(key)
7087 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7088 assert!(flag.global, "{key} must stay global after the descent");
7089 assert_eq!(
7090 flag.long,
7091 vec!["yes".to_string(), "assume-yes".to_string()],
7092 "{key} must resolve to the flag carrying every alias",
7093 );
7094 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
7095 }
7096
7097 assert_eq!(
7099 unique_flags(parsed.available_flags.values()).count(),
7100 1,
7101 "all aliases must point at one flag object, got {:?}",
7102 parsed.available_flags,
7103 );
7104
7105 assert_eq!(
7107 parsed.available_flags["-y"].effect,
7108 Some(crate::SpecCommandEffect::Write),
7109 );
7110 }
7111
7112 #[test]
7113 fn test_redeclared_global_keeps_hidden_alias_metadata() {
7114 let spec = r#"
7115flag "--yes" global=#true {
7116 alias "-q" "--quietly" hide=#true
7117}
7118cmd "run" {
7119 flag "--yes --assume-yes" {
7120 alias "-s" "--secret" hide=#true
7121 }
7122}
7123"#
7124 .parse::<Spec>()
7125 .unwrap();
7126
7127 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7128 let merged = &parsed.available_flags["--yes"];
7129 assert_eq!(merged.hidden_short_aliases, ['q', 's']);
7130 assert_eq!(merged.hidden_aliases, ["quietly", "secret"]);
7131 for key in ["-q", "-s", "--quietly", "--secret"] {
7132 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7133 }
7134 }
7135
7136 #[test]
7137 fn test_redeclared_global_can_promote_hidden_aliases() {
7138 let spec = r#"
7139flag "--yes" global=#true {
7140 alias "-q" "--quietly" hide=#true
7141}
7142cmd "run" {
7143 flag "-q --yes --quietly"
7144}
7145"#
7146 .parse::<Spec>()
7147 .unwrap();
7148
7149 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7150 let merged = &parsed.available_flags["--yes"];
7151 assert!(merged.hidden_short_aliases.is_empty());
7152 assert!(merged.hidden_aliases.is_empty());
7153 for key in ["-q", "--quietly"] {
7154 assert!(Arc::ptr_eq(&parsed.available_flags[key], merged), "{key}");
7155 }
7156 }
7157
7158 #[test]
7159 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
7160 let spec = r#"
7166flag "-y --yes --confirm" global=#true
7167cmd "run" {
7168 flag "--yes --assume-yes"
7169}
7170"#
7171 .parse::<Spec>()
7172 .unwrap();
7173
7174 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7175
7176 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
7177 let flag = parsed
7178 .available_flags
7179 .get(key)
7180 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
7181 assert!(flag.global, "{key} must stay global after the descent");
7182 assert_eq!(
7183 flag.long,
7184 vec![
7185 "yes".to_string(),
7186 "confirm".to_string(),
7187 "assume-yes".to_string()
7188 ],
7189 "{key} must resolve to the flag carrying every alias",
7190 );
7191 }
7192
7193 assert_eq!(
7194 unique_flags(parsed.available_flags.values()).count(),
7195 1,
7196 "all aliases must point at one flag object, got {:?}",
7197 parsed.available_flags,
7198 );
7199 }
7200
7201 fn mounted_task_flag_spec() -> Spec {
7208 let mut task_cmd = SpecCommand::builder()
7209 .name("mytask")
7210 .flag(
7211 SpecFlag::builder()
7212 .name("env")
7213 .long("env")
7214 .arg(
7215 SpecArg::builder()
7216 .name("name")
7217 .choices(["dev", "stage", "prod"])
7218 .build(),
7219 )
7220 .global(false)
7221 .build(),
7222 )
7223 .flag(
7224 SpecFlag::builder()
7225 .name("bump")
7226 .long("bump")
7227 .arg(
7228 SpecArg::builder()
7229 .name("type")
7230 .choices(["auto", "major"])
7231 .build(),
7232 )
7233 .global(false)
7234 .build(),
7235 )
7236 .build();
7237 task_cmd.mounted = true;
7238
7239 let mut run_cmd = SpecCommand::builder()
7240 .name("run")
7241 .flag(
7242 SpecFlag::builder()
7243 .name("force")
7244 .short('f')
7245 .long("force")
7246 .global(false)
7247 .build(),
7248 )
7249 .build();
7250 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
7251
7252 let mut cmd = SpecCommand::builder()
7253 .name("test")
7254 .flag(
7255 SpecFlag::builder()
7256 .name("env")
7257 .short('E')
7258 .long("env")
7259 .arg(SpecArg::builder().name("ENV").build())
7260 .global(true)
7261 .build(),
7262 )
7263 .flag(
7264 SpecFlag::builder()
7265 .name("silent")
7266 .long("silent")
7267 .global(true)
7268 .build(),
7269 )
7270 .build();
7271 cmd.subcommands.insert("run".to_string(), run_cmd);
7272
7273 Spec {
7274 name: "test".to_string(),
7275 bin: "test".to_string(),
7276 cmd,
7277 ..Default::default()
7278 }
7279 }
7280
7281 #[test]
7282 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
7283 let deep = SpecCommand::builder().name("deep").build();
7289 let mut sub = SpecCommand::builder()
7290 .name("sub")
7291 .flag(
7293 SpecFlag::builder()
7294 .name("cd")
7295 .short('C')
7296 .long("cd")
7297 .arg(SpecArg::builder().name("dir").build())
7298 .global(false)
7299 .build(),
7300 )
7301 .build();
7302 sub.subcommands.insert("deep".to_string(), deep);
7303 let mut task = SpecCommand::builder()
7304 .name("task")
7305 .flag(
7306 SpecFlag::builder()
7307 .name("cd")
7308 .short('C')
7309 .long("cd")
7310 .arg(SpecArg::builder().name("dir").build())
7311 .global(true)
7312 .build(),
7313 )
7314 .build();
7315 task.subcommands.insert("sub".to_string(), sub);
7316 task.mark_mounted();
7317
7318 let mut run_cmd = SpecCommand::builder().name("run").build();
7319 run_cmd.subcommands.insert("task".to_string(), task);
7320 let mut cmd = SpecCommand::builder().name("test").build();
7321 cmd.subcommands.insert("run".to_string(), run_cmd);
7322 let spec = Spec {
7323 name: "test".to_string(),
7324 bin: "test".to_string(),
7325 cmd,
7326 ..Default::default()
7327 };
7328
7329 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
7330 assert!(
7331 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
7332 "the mounted program's own global must survive descents inside the mounted tree",
7333 );
7334 assert!(
7335 parsed.completion_flags().contains_key("--cd"),
7336 "and must still be offered there: it belongs to the mounted program",
7337 );
7338 assert!(
7339 parsed.completion_flags().contains_key("-C"),
7340 "including the short the nested command re-declared",
7341 );
7342 }
7343
7344 #[test]
7345 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
7346 let mut task = SpecCommand::builder()
7351 .name("task")
7352 .flag(
7353 SpecFlag::builder()
7354 .name("bump")
7355 .long("bump")
7356 .global(false)
7357 .build(),
7358 )
7359 .build();
7360 task.mark_mounted();
7361
7362 let mut run_cmd = SpecCommand::builder().name("run").build();
7363 run_cmd.subcommands.insert("task".to_string(), task);
7364 run_cmd.flags = vec![
7366 SpecFlag::builder()
7367 .name("tglobal")
7368 .long("tglobal")
7369 .global(true)
7370 .build(),
7371 SpecFlag::builder()
7372 .name("tlocal")
7373 .long("tlocal")
7374 .global(false)
7375 .build(),
7376 ];
7377 run_cmd.flags_from_mount = true;
7378
7379 let mut cmd = SpecCommand::builder()
7380 .name("test")
7381 .flag(
7382 SpecFlag::builder()
7383 .name("silent")
7384 .long("silent")
7385 .global(true)
7386 .build(),
7387 )
7388 .build();
7389 cmd.subcommands.insert("run".to_string(), run_cmd);
7390 let spec = Spec {
7391 name: "test".to_string(),
7392 bin: "test".to_string(),
7393 cmd,
7394 ..Default::default()
7395 };
7396
7397 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
7398 assert_eq!(
7399 parsed.completion_flags().keys().collect::<Vec<_>>(),
7400 vec!["--bump", "--tglobal"],
7401 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
7402 `--silent` does not, and the mount's non-global root flag is not inherited",
7403 );
7404 }
7405
7406 #[test]
7407 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
7408 let spec = mounted_task_flag_spec();
7413 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
7414
7415 assert!(parsed.available_flags.contains_key("--silent"));
7417 assert!(parsed.available_flags.contains_key("-E"));
7418 assert_eq!(
7420 parsed.completion_flags().keys().collect::<Vec<_>>(),
7421 vec!["--bump", "--env"],
7422 "only the mounted command's own flags may be offered",
7423 );
7424
7425 assert!(!parsed.available_flags.contains_key("--force"));
7427 }
7428
7429 #[test]
7430 fn test_mounted_cmd_flag_wins_over_inherited_global() {
7431 let spec = mounted_task_flag_spec();
7434 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
7435
7436 let awaiting = parsed
7437 .flag_awaiting_value
7438 .first()
7439 .expect("--env should await a value");
7440 assert_eq!(
7441 awaiting
7442 .arg
7443 .as_ref()
7444 .and_then(|a| a.choices.as_ref())
7445 .map(|c| c.choices.clone()),
7446 Some(vec![
7447 "dev".to_string(),
7448 "stage".to_string(),
7449 "prod".to_string()
7450 ]),
7451 "the mounted command's own --env must win over the inherited global",
7452 );
7453
7454 let parsed =
7457 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
7458 assert!(
7459 parsed.args.is_empty(),
7460 "prefix global tokens must not be consumed as positionals, got {:?}",
7461 parsed.args
7462 );
7463 assert_eq!(
7464 parsed.as_env().get("usage_env").map(String::as_str),
7465 Some("anything"),
7466 );
7467 }
7468
7469 #[test]
7470 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
7471 let spec = mounted_task_flag_spec();
7476 let parsed = parse_partial(
7477 &spec,
7478 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
7479 )
7480 .unwrap();
7481 assert!(
7482 parsed.errors.is_empty(),
7483 "prefix global value must not be validated against the mounted flag: {:?}",
7484 parsed
7485 .errors
7486 .iter()
7487 .map(|e| e.to_string())
7488 .collect::<Vec<_>>(),
7489 );
7490 assert_eq!(
7491 parsed.as_env().get("usage_env").map(String::as_str),
7492 Some("not-a-task-choice"),
7493 );
7494
7495 let parsed = parse_partial(
7497 &spec,
7498 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
7499 )
7500 .unwrap();
7501 assert!(parsed.errors.is_empty());
7502 assert_eq!(
7503 parsed.as_env().get("usage_env").map(String::as_str),
7504 Some("not-a-task-choice"),
7505 );
7506
7507 let parsed = parse_partial(
7510 &spec,
7511 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
7512 )
7513 .unwrap();
7514 let awaiting = parsed
7515 .flag_awaiting_value
7516 .first()
7517 .expect("--env should await a value");
7518 assert_eq!(
7519 awaiting
7520 .arg
7521 .as_ref()
7522 .and_then(|a| a.choices.as_ref())
7523 .map(|c| c.choices.clone()),
7524 Some(vec![
7525 "dev".to_string(),
7526 "stage".to_string(),
7527 "prod".to_string()
7528 ]),
7529 "the mounted command's --env must own the name after the mounted command",
7530 );
7531 }
7532
7533 #[test]
7534 fn test_non_global_flag_does_not_hide_subcommand() {
7535 let spec = mounted_task_flag_spec();
7539
7540 for words in [
7541 &["test", "run", "--force", "mytask"][..],
7543 &["test", "run", "-f", "mytask"][..],
7544 &["test", "-E", "prod", "run", "--force", "mytask"][..],
7546 ] {
7547 let parsed = parse_partial(&spec, &input(words)).unwrap();
7548 assert_eq!(
7549 parsed
7550 .cmds
7551 .iter()
7552 .map(|c| c.name.as_str())
7553 .collect::<Vec<_>>(),
7554 vec!["test", "run", "mytask"],
7555 "{words:?} should descend into the mounted command",
7556 );
7557 assert!(
7558 parsed.args.is_empty(),
7559 "{words:?} should not consume a positional, got {:?}",
7560 parsed.args,
7561 );
7562 assert_eq!(
7563 parsed.as_env().get("usage_force").map(String::as_str),
7564 Some("true"),
7565 "the non-global flag must still be recorded for {words:?}",
7566 );
7567 }
7568
7569 let mut run_cmd = SpecCommand::builder()
7572 .name("run")
7573 .flag(
7574 SpecFlag::builder()
7575 .name("output")
7576 .short('o')
7577 .long("output")
7578 .arg(SpecArg::builder().name("mode").build())
7579 .global(false)
7580 .build(),
7581 )
7582 .build();
7583 run_cmd.subcommands.insert(
7584 "task".to_string(),
7585 SpecCommand::builder().name("task").build(),
7586 );
7587 let mut cmd = SpecCommand::builder().name("test").build();
7588 cmd.subcommands.insert("run".to_string(), run_cmd);
7589 let spec = Spec {
7590 name: "test".to_string(),
7591 bin: "test".to_string(),
7592 cmd,
7593 ..Default::default()
7594 };
7595
7596 let parsed =
7597 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
7598 assert_eq!(
7599 parsed
7600 .cmds
7601 .iter()
7602 .map(|c| c.name.as_str())
7603 .collect::<Vec<_>>(),
7604 vec!["test", "run", "task"],
7605 );
7606 assert_eq!(
7607 parsed.as_env().get("usage_output").map(String::as_str),
7608 Some("quiet"),
7609 );
7610
7611 assert_parse_err(
7614 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
7615 "unexpected word: --nope",
7616 );
7617 }
7618
7619 #[test]
7620 fn test_non_mounted_subcommand_offers_inherited_globals() {
7621 let mut run_cmd = SpecCommand::builder().name("run").build();
7624 run_cmd.subcommands.insert(
7625 "nested".to_string(),
7626 SpecCommand::builder().name("nested").build(),
7627 );
7628 let mut cmd = SpecCommand::builder()
7629 .name("test")
7630 .flag(
7631 SpecFlag::builder()
7632 .name("silent")
7633 .long("silent")
7634 .global(true)
7635 .build(),
7636 )
7637 .build();
7638 cmd.subcommands.insert("run".to_string(), run_cmd);
7639 let spec = Spec {
7640 name: "test".to_string(),
7641 bin: "test".to_string(),
7642 cmd,
7643 ..Default::default()
7644 };
7645
7646 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
7647 assert_eq!(
7648 parsed.completion_flags().keys().collect::<Vec<_>>(),
7649 parsed.available_flags.keys().collect::<Vec<_>>(),
7650 );
7651 assert!(parsed.completion_flags().contains_key("--silent"));
7652 }
7653
7654 #[test]
7655 fn test_subcommand_alias_collision_keeps_last_owner() {
7656 let run_cmd = SpecCommand::builder()
7660 .name("run")
7661 .flag(
7662 SpecFlag::builder()
7663 .name("alpha")
7664 .short('x')
7665 .long("alpha")
7666 .global(false)
7667 .build(),
7668 )
7669 .flag(
7670 SpecFlag::builder()
7671 .name("beta")
7672 .short('x')
7673 .long("beta")
7674 .global(false)
7675 .build(),
7676 )
7677 .build();
7678 let mut cmd = SpecCommand::builder().name("test").build();
7679 cmd.subcommands.insert("run".to_string(), run_cmd);
7680 let spec = Spec {
7681 name: "test".to_string(),
7682 bin: "test".to_string(),
7683 cmd,
7684 ..Default::default()
7685 };
7686
7687 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
7688 assert_eq!(
7690 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
7691 Some("beta"),
7692 "the last-declared flag must keep a shared short alias",
7693 );
7694 assert_eq!(
7696 parsed
7697 .available_flags
7698 .get("--alpha")
7699 .map(|f| f.name.as_str()),
7700 Some("alpha"),
7701 );
7702 assert_eq!(
7703 parsed
7704 .available_flags
7705 .get("--beta")
7706 .map(|f| f.name.as_str()),
7707 Some("beta"),
7708 );
7709 }
7710
7711 #[test]
7712 fn test_default_subcommand_same_name_child() {
7713 let run_task = SpecCommand::builder()
7717 .name("run")
7718 .arg(SpecArg::builder().name("args").build())
7719 .build();
7720 let mut run_cmd = SpecCommand::builder().name("run").build();
7721 run_cmd.subcommands.insert("run".to_string(), run_task);
7722
7723 let mut cmd = SpecCommand::builder().name("test").build();
7724 cmd.subcommands.insert("run".to_string(), run_cmd);
7725
7726 let spec = Spec {
7727 name: "test".to_string(),
7728 bin: "test".to_string(),
7729 cmd,
7730 default_subcommand: Some("run".to_string()),
7731 ..Default::default()
7732 };
7733
7734 let input = vec!["test".to_string(), "run".to_string()];
7736 let parsed = parse(&spec, &input).unwrap();
7737
7738 assert_eq!(parsed.cmds.len(), 2);
7740 assert_eq!(parsed.cmds[0].name, "test");
7741 assert_eq!(parsed.cmds[1].name, "run");
7742
7743 let input = vec![
7745 "test".to_string(),
7746 "run".to_string(),
7747 "run".to_string(),
7748 "hello".to_string(),
7749 ];
7750 let parsed = parse(&spec, &input).unwrap();
7751
7752 assert_eq!(parsed.cmds.len(), 3);
7753 assert_eq!(parsed.cmds[0].name, "test");
7754 assert_eq!(parsed.cmds[1].name, "run");
7755 assert_eq!(parsed.cmds[2].name, "run");
7756 assert_eq!(parsed.args.len(), 1);
7757 let value = parsed.args.values().next().unwrap();
7758 assert_eq!(value.to_string(), "hello");
7759
7760 let mut run_cmd = SpecCommand::builder()
7764 .name("run")
7765 .arg(SpecArg::builder().name("task").build())
7766 .build();
7767 let run_task = SpecCommand::builder().name("run").build();
7768 run_cmd.subcommands.insert("run".to_string(), run_task);
7769
7770 let mut cmd = SpecCommand::builder().name("test").build();
7771 cmd.subcommands.insert("run".to_string(), run_cmd);
7772
7773 let spec = Spec {
7774 name: "test".to_string(),
7775 bin: "test".to_string(),
7776 cmd,
7777 default_subcommand: Some("run".to_string()),
7778 ..Default::default()
7779 };
7780
7781 let input = vec!["test".to_string(), "other".to_string()];
7782 let parsed = parse(&spec, &input).unwrap();
7783
7784 assert_eq!(parsed.cmds.len(), 2);
7787 assert_eq!(parsed.cmds[0].name, "test");
7788 assert_eq!(parsed.cmds[1].name, "run");
7789
7790 assert_eq!(parsed.args.len(), 1);
7792 let value = parsed.args.values().next().unwrap();
7793 assert_eq!(value.to_string(), "other");
7794 }
7795
7796 #[test]
7797 fn test_restart_token() {
7798 let run_cmd = SpecCommand::builder()
7800 .name("run")
7801 .arg(SpecArg::builder().name("task").build())
7802 .restart_token(":::".to_string())
7803 .build();
7804 let mut cmd = SpecCommand::builder().name("test").build();
7805 cmd.subcommands.insert("run".to_string(), run_cmd);
7806
7807 let spec = Spec {
7808 name: "test".to_string(),
7809 bin: "test".to_string(),
7810 cmd,
7811 ..Default::default()
7812 };
7813
7814 let input = vec![
7816 "test".to_string(),
7817 "run".to_string(),
7818 "task1".to_string(),
7819 ":::".to_string(),
7820 "task2".to_string(),
7821 ];
7822 let parsed = parse(&spec, &input).unwrap();
7823
7824 assert_eq!(parsed.args.len(), 1);
7826 let value = parsed.args.values().next().unwrap();
7827 assert_eq!(value.to_string(), "task2");
7828 }
7829
7830 #[test]
7831 fn test_restart_token_multiple() {
7832 let run_cmd = SpecCommand::builder()
7834 .name("run")
7835 .arg(SpecArg::builder().name("task").build())
7836 .restart_token(":::".to_string())
7837 .build();
7838 let mut cmd = SpecCommand::builder().name("test").build();
7839 cmd.subcommands.insert("run".to_string(), run_cmd);
7840
7841 let spec = Spec {
7842 name: "test".to_string(),
7843 bin: "test".to_string(),
7844 cmd,
7845 ..Default::default()
7846 };
7847
7848 let input = vec![
7850 "test".to_string(),
7851 "run".to_string(),
7852 "task1".to_string(),
7853 ":::".to_string(),
7854 "task2".to_string(),
7855 ":::".to_string(),
7856 "task3".to_string(),
7857 ];
7858 let parsed = parse(&spec, &input).unwrap();
7859
7860 assert_eq!(parsed.args.len(), 1);
7862 let value = parsed.args.values().next().unwrap();
7863 assert_eq!(value.to_string(), "task3");
7864 }
7865
7866 #[test]
7867 fn test_restart_token_clears_flag_awaiting_value() {
7868 let run_cmd = SpecCommand::builder()
7870 .name("run")
7871 .arg(SpecArg::builder().name("task").build())
7872 .flag(
7873 SpecFlag::builder()
7874 .name("jobs")
7875 .long("jobs")
7876 .arg(SpecArg::builder().name("count").build())
7877 .build(),
7878 )
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 "--jobs".to_string(),
7897 ":::".to_string(),
7898 "task2".to_string(),
7899 ];
7900 let parsed = parse(&spec, &input).unwrap();
7901
7902 assert_eq!(parsed.args.len(), 1);
7904 let value = parsed.args.values().next().unwrap();
7905 assert_eq!(value.to_string(), "task2");
7906 assert!(parsed.flag_awaiting_value.is_empty());
7908 }
7909
7910 #[test]
7911 fn test_restart_token_resets_double_dash() {
7912 let run_cmd = SpecCommand::builder()
7914 .name("run")
7915 .arg(SpecArg::builder().name("task").build())
7916 .arg(SpecArg::builder().name("extra_args").var(true).build())
7917 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
7918 .restart_token(":::".to_string())
7919 .build();
7920 let mut cmd = SpecCommand::builder().name("test").build();
7921 cmd.subcommands.insert("run".to_string(), run_cmd);
7922
7923 let spec = Spec {
7924 name: "test".to_string(),
7925 bin: "test".to_string(),
7926 cmd,
7927 ..Default::default()
7928 };
7929
7930 let input = vec![
7932 "test".to_string(),
7933 "run".to_string(),
7934 "task1".to_string(),
7935 "--".to_string(),
7936 "extra".to_string(),
7937 ":::".to_string(),
7938 "--verbose".to_string(),
7939 "task2".to_string(),
7940 ];
7941 let parsed = parse(&spec, &input).unwrap();
7942
7943 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
7945 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
7947 let value = parsed.args.get(task_arg).unwrap();
7948 assert_eq!(value.to_string(), "task2");
7949 }
7950
7951 #[test]
7952 fn test_double_dashes_without_preserve() {
7953 let run_cmd = SpecCommand::builder()
7958 .name("run")
7959 .arg(SpecArg::builder().name("args").var(true).build())
7960 .build();
7961 let mut cmd = SpecCommand::builder().name("test").build();
7962 cmd.subcommands.insert("run".to_string(), run_cmd);
7963
7964 let spec = Spec {
7965 name: "test".to_string(),
7966 bin: "test".to_string(),
7967 cmd,
7968 ..Default::default()
7969 };
7970
7971 let input = vec![
7973 "test".to_string(),
7974 "run".to_string(),
7975 "arg1".to_string(),
7976 "--".to_string(),
7977 "arg2".to_string(),
7978 "--".to_string(),
7979 "arg3".to_string(),
7980 ];
7981 let parsed = parse(&spec, &input).unwrap();
7982
7983 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
7984 let value = parsed.args.get(args_arg).unwrap();
7985 assert_eq!(value.to_string(), "arg1 arg2 -- arg3");
7986 }
7987
7988 #[test]
7989 fn test_double_dashes_with_preserve() {
7990 let run_cmd = SpecCommand::builder()
7992 .name("run")
7993 .arg(
7994 SpecArg::builder()
7995 .name("args")
7996 .var(true)
7997 .double_dash(SpecDoubleDashChoices::Preserve)
7998 .build(),
7999 )
8000 .build();
8001 let mut cmd = SpecCommand::builder().name("test").build();
8002 cmd.subcommands.insert("run".to_string(), run_cmd);
8003
8004 let spec = Spec {
8005 name: "test".to_string(),
8006 bin: "test".to_string(),
8007 cmd,
8008 ..Default::default()
8009 };
8010
8011 let input = vec![
8013 "test".to_string(),
8014 "run".to_string(),
8015 "arg1".to_string(),
8016 "--".to_string(),
8017 "arg2".to_string(),
8018 "--".to_string(),
8019 "arg3".to_string(),
8020 ];
8021 let parsed = parse(&spec, &input).unwrap();
8022
8023 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8024 let value = parsed.args.get(args_arg).unwrap();
8025 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
8026 }
8027
8028 #[test]
8029 fn test_double_dashes_with_preserve_only_dashes() {
8030 let run_cmd = SpecCommand::builder()
8033 .name("run")
8034 .arg(
8035 SpecArg::builder()
8036 .name("args")
8037 .var(true)
8038 .double_dash(SpecDoubleDashChoices::Preserve)
8039 .build(),
8040 )
8041 .build();
8042 let mut cmd = SpecCommand::builder().name("test").build();
8043 cmd.subcommands.insert("run".to_string(), run_cmd);
8044
8045 let spec = Spec {
8046 name: "test".to_string(),
8047 bin: "test".to_string(),
8048 cmd,
8049 ..Default::default()
8050 };
8051
8052 let input = vec![
8054 "test".to_string(),
8055 "run".to_string(),
8056 "--".to_string(),
8057 "--".to_string(),
8058 ];
8059 let parsed = parse(&spec, &input).unwrap();
8060
8061 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
8062 let value = parsed.args.get(args_arg).unwrap();
8063 assert_eq!(value.to_string(), "-- --");
8064 }
8065
8066 #[test]
8067 fn test_double_dashes_with_preserve_multiple_args() {
8068 let run_cmd = SpecCommand::builder()
8070 .name("run")
8071 .arg(SpecArg::builder().name("task").build())
8072 .arg(
8073 SpecArg::builder()
8074 .name("extra_args")
8075 .var(true)
8076 .double_dash(SpecDoubleDashChoices::Preserve)
8077 .build(),
8078 )
8079 .build();
8080 let mut cmd = SpecCommand::builder().name("test").build();
8081 cmd.subcommands.insert("run".to_string(), run_cmd);
8082
8083 let spec = Spec {
8084 name: "test".to_string(),
8085 bin: "test".to_string(),
8086 cmd,
8087 ..Default::default()
8088 };
8089
8090 let input = vec![
8093 "test".to_string(),
8094 "run".to_string(),
8095 "task1".to_string(),
8096 "--".to_string(),
8097 "arg1".to_string(),
8098 "--".to_string(),
8099 "--foo".to_string(),
8100 ];
8101 let parsed = parse(&spec, &input).unwrap();
8102
8103 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
8104 let task_value = parsed.args.get(task_arg).unwrap();
8105 assert_eq!(task_value.to_string(), "task1");
8106
8107 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
8108 let extra_value = parsed.args.get(extra_arg).unwrap();
8109 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
8110 }
8111
8112 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
8113 let cmd = SpecCommand::builder().name("test").args(args).build();
8114 Spec {
8115 name: "test".to_string(),
8116 bin: "test".to_string(),
8117 cmd,
8118 ..Default::default()
8119 }
8120 }
8121
8122 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
8123 let arg = parsed
8124 .args
8125 .keys()
8126 .find(|a| a.name == name)
8127 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
8128 parsed.args.get(arg).unwrap().to_string()
8129 }
8130
8131 fn required_arg(name: &str) -> SpecArg {
8132 SpecArg::builder()
8133 .name(name)
8134 .var(true)
8135 .required(false)
8136 .double_dash(SpecDoubleDashChoices::Required)
8137 .build()
8138 }
8139
8140 #[test]
8141 fn test_double_dash_required_reports_error_once_for_variadic() {
8142 let spec = spec_with_args([required_arg("files")]);
8144
8145 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
8146
8147 assert!(parsed.args.is_empty());
8148 assert_eq!(parsed.errors.len(), 1);
8149 assert!(
8150 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
8151 );
8152 }
8153
8154 #[test]
8155 fn test_double_dash_required_suppresses_missing_arg() {
8156 let spec = spec_with_args([SpecArg::builder()
8158 .name("file")
8159 .required(true)
8160 .double_dash(SpecDoubleDashChoices::Required)
8161 .build()]);
8162
8163 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
8164
8165 assert_eq!(parsed.errors.len(), 1);
8166 assert!(matches!(
8167 &parsed.errors[0],
8168 UsageErr::ArgRequiresDoubleDash(_)
8169 ));
8170 assert_eq!(
8172 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
8173 Some("file")
8174 );
8175 assert!(!parsed.double_dash_seen);
8176 }
8177
8178 #[test]
8179 fn test_double_dash_routes_to_required_arg() {
8180 let spec = spec_with_args([
8183 SpecArg::builder()
8184 .name("tool")
8185 .var(true)
8186 .required(false)
8187 .build(),
8188 required_arg("command"),
8189 ]);
8190
8191 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
8192
8193 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8194 assert_eq!(arg_value(&parsed, "command"), "node app.js");
8195 assert!(parsed.double_dash_seen);
8196 }
8197
8198 #[test]
8199 fn test_double_dash_routes_with_gap_reports_missing_arg() {
8200 let spec = spec_with_args([
8203 SpecArg::builder()
8204 .name("tool")
8205 .var(true)
8206 .required(true)
8207 .build(),
8208 required_arg("command"),
8209 ]);
8210
8211 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
8212
8213 assert_eq!(arg_value(&parsed, "command"), "ls");
8214 assert!(parsed.args.keys().all(|a| a.name != "tool"));
8215 assert!(parsed
8216 .errors
8217 .iter()
8218 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
8219 }
8220
8221 #[test]
8222 fn test_double_dash_gap_applies_defaults() {
8223 let spec = spec_with_args([
8225 SpecArg::builder()
8226 .name("tool")
8227 .var(true)
8228 .required(false)
8229 .default_value("node@20")
8230 .build(),
8231 required_arg("command"),
8232 ]);
8233
8234 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
8235
8236 assert_eq!(arg_value(&parsed, "command"), "ls");
8237 assert_eq!(arg_value(&parsed, "tool"), "node@20");
8238 }
8239
8240 fn spec_with_restart_token_and_required_arg() -> Spec {
8241 let run_cmd = SpecCommand::builder()
8242 .name("run")
8243 .arg(SpecArg::builder().name("task").build())
8244 .arg(required_arg("run_args"))
8245 .restart_token(":::".to_string())
8246 .build();
8247 let mut cmd = SpecCommand::builder().name("test").build();
8248 cmd.subcommands.insert("run".to_string(), run_cmd);
8249 Spec {
8250 name: "test".to_string(),
8251 bin: "test".to_string(),
8252 cmd,
8253 ..Default::default()
8254 }
8255 }
8256
8257 #[test]
8258 fn test_double_dash_required_restart_token_resets_separator() {
8259 let spec = spec_with_restart_token_and_required_arg();
8261
8262 let parsed = parse_partial(
8263 &spec,
8264 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
8265 )
8266 .unwrap();
8267
8268 assert_eq!(arg_value(&parsed, "task"), "task2");
8269 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
8270 assert_eq!(
8272 parsed
8273 .errors
8274 .iter()
8275 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
8276 .count(),
8277 1
8278 );
8279 }
8280
8281 #[test]
8282 fn test_double_dash_required_restart_token_accepts_new_separator() {
8283 let spec = spec_with_restart_token_and_required_arg();
8284
8285 let parsed = parse(
8286 &spec,
8287 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
8288 )
8289 .unwrap();
8290
8291 assert_eq!(arg_value(&parsed, "task"), "task2");
8292 assert_eq!(arg_value(&parsed, "run_args"), "c");
8293 }
8294
8295 #[test]
8296 fn test_double_dash_preserve_is_not_a_separator() {
8297 let spec = spec_with_args([
8300 SpecArg::builder()
8301 .name("kept")
8302 .var(true)
8303 .var_max(1)
8304 .required(false)
8305 .double_dash(SpecDoubleDashChoices::Preserve)
8306 .build(),
8307 required_arg("rest"),
8308 ]);
8309
8310 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
8311
8312 assert_eq!(arg_value(&parsed, "kept"), "--");
8313 assert!(parsed.args.keys().all(|a| a.name != "rest"));
8314 assert!(!parsed.double_dash_seen);
8315 assert_eq!(parsed.errors.len(), 1);
8316 }
8317
8318 #[test]
8319 fn test_double_dash_required_does_not_bail_in_parse_partial() {
8320 let spec = spec_with_args([required_arg("file")]);
8322
8323 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
8324 assert!(parse(&spec, &input(&["test", "x"])).is_err());
8325 }
8326
8327 #[test]
8328 fn test_double_dash_without_required_arg_does_not_move_cursor() {
8329 let spec = spec_with_args([
8331 SpecArg::builder().name("first").required(false).build(),
8332 SpecArg::builder().name("second").required(false).build(),
8333 ]);
8334
8335 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
8336
8337 assert_eq!(arg_value(&parsed, "first"), "a");
8338 assert_eq!(arg_value(&parsed, "second"), "b");
8339 assert!(parsed.next_arg.is_none());
8340 }
8341
8342 #[test]
8343 fn test_parser_with_custom_env_for_required_arg() {
8344 let spec = spec_with_arg(
8345 SpecArg::builder()
8346 .name("name")
8347 .env("NAME")
8348 .required(true)
8349 .build(),
8350 );
8351 std::env::remove_var("NAME");
8352
8353 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
8354 .expect("parse should succeed with custom env");
8355 assert_eq!(parsed.args.len(), 1);
8356 assert_eq!(first_string_value(&parsed), "john");
8357 }
8358
8359 #[test]
8360 fn test_parser_with_custom_env_for_required_flag() {
8361 let spec = spec_with_flag(
8362 SpecFlag::builder()
8363 .long("name")
8364 .env("NAME")
8365 .required(true)
8366 .arg(SpecArg::builder().name("name").build())
8367 .build(),
8368 );
8369 std::env::remove_var("NAME");
8370
8371 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
8372 .expect("parse should succeed with custom env");
8373 assert_eq!(parsed.flags.len(), 1);
8374 assert_eq!(first_string_value(&parsed), "jane");
8375 }
8376
8377 #[test]
8378 fn test_flag_environment_fallbacks_preserve_declaration_order() {
8379 let spec = spec_with_flag(
8380 SpecFlag::builder()
8381 .long("name")
8382 .env("NAME")
8383 .env_fallback("OLD_NAME")
8384 .env_fallback("OLDER_NAME")
8385 .deprecated_env("DEPRECATED_NAME")
8386 .arg(SpecArg::builder().name("name").build())
8387 .build(),
8388 );
8389
8390 let parsed = parse_with_env(
8391 &spec,
8392 &["test"],
8393 &[
8394 ("NAME", "canonical"),
8395 ("OLD_NAME", "fallback"),
8396 ("DEPRECATED_NAME", "deprecated"),
8397 ],
8398 )
8399 .unwrap();
8400 assert_eq!(first_string_value(&parsed), "canonical");
8401
8402 let parsed = parse_with_env(
8403 &spec,
8404 &["test"],
8405 &[("OLDER_NAME", "older"), ("OLD_NAME", "old")],
8406 )
8407 .unwrap();
8408 assert_eq!(first_string_value(&parsed), "old");
8409
8410 let parsed =
8411 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8412 assert_eq!(first_string_value(&parsed), "deprecated");
8413 }
8414
8415 #[test]
8416 fn a_value_from_a_deprecated_alias_says_which_name_to_use() {
8417 let spec = spec_with_flag(
8418 SpecFlag::builder()
8419 .long("name")
8420 .env("NAME")
8421 .deprecated_env("DEPRECATED_NAME")
8422 .arg(SpecArg::builder().name("name").build())
8423 .build(),
8424 );
8425
8426 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "canonical")]).unwrap();
8428 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8429
8430 let parsed =
8431 parse_with_env(&spec, &["test"], &[("DEPRECATED_NAME", "deprecated")]).unwrap();
8432 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8433 assert_eq!(
8434 parsed.warnings[0].kind,
8435 crate::warn::WarningKind::DeprecatedEnv
8436 );
8437 assert_eq!(parsed.warnings[0].name, "DEPRECATED_NAME");
8438 assert_eq!(parsed.warnings[0].replacement.as_deref(), Some("NAME"));
8439 assert_eq!(first_string_value(&parsed), "deprecated");
8441 }
8442
8443 #[test]
8444 fn a_deprecated_flag_reports_only_when_it_was_used() {
8445 let spec = spec_with_flag(
8446 SpecFlag::builder()
8447 .long("output")
8448 .deprecated("use --out")
8449 .deprecated_remove_at("3.0.0")
8450 .arg(SpecArg::builder().name("output").build())
8451 .build(),
8452 );
8453
8454 let parsed = parse_with_env(&spec, &["test"], &[]).unwrap();
8455 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8456
8457 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8458 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8459 assert_eq!(
8460 parsed.warnings[0].kind,
8461 crate::warn::WarningKind::DeprecatedFlag
8462 );
8463 assert_eq!(parsed.warnings[0].name, "--output");
8465 assert_eq!(parsed.warnings[0].remove_at.as_deref(), Some("3.0.0"));
8466 assert_eq!(
8467 parsed.warnings[0].render(),
8468 "warning: --output is deprecated, removed at 3.0.0: use --out\n",
8469 );
8470 }
8471
8472 #[test]
8473 fn a_milestone_the_spec_has_not_reached_stays_quiet() {
8474 let flag = SpecFlag::builder()
8475 .long("output")
8476 .deprecated("use --out")
8477 .deprecated_warn_at("9.0.0")
8478 .arg(SpecArg::builder().name("output").build())
8479 .build();
8480 let mut spec = spec_with_flag(flag);
8481 spec.version = Some("2.0.0".to_string());
8482
8483 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8484 assert!(parsed.warnings.is_empty(), "{:?}", parsed.warnings);
8485
8486 spec.version = Some("9.0.0".to_string());
8488 let parsed = parse_with_env(&spec, &["test", "--output", "a.txt"], &[]).unwrap();
8489 assert_eq!(parsed.warnings.len(), 1, "{:?}", parsed.warnings);
8490 }
8491
8492 #[test]
8493 fn test_parser_with_custom_env_still_fails_when_missing() {
8494 let spec = spec_with_arg(
8495 SpecArg::builder()
8496 .name("name")
8497 .env("NAME")
8498 .required(true)
8499 .build(),
8500 );
8501 std::env::remove_var("NAME");
8502 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
8503 }
8504
8505 #[test]
8506 fn test_parser_does_not_treat_env_choice_value_as_help() {
8507 let spec = spec_with_arg(
8508 SpecArg::builder()
8509 .name("env")
8510 .env("CURRENT_ENV")
8511 .choices(["dev", "staging"])
8512 .required(false)
8513 .build(),
8514 );
8515
8516 assert_parse_err(
8517 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
8518 "Invalid choice for arg env: --help, expected one of dev, staging",
8519 );
8520 }
8521
8522 #[test]
8523 fn test_parser_does_not_treat_default_choice_value_as_help() {
8524 let spec = spec_with_flag(
8525 SpecFlag::builder()
8526 .long("env")
8527 .arg(
8528 SpecArg::builder()
8529 .name("env")
8530 .choices(["dev", "staging"])
8531 .build(),
8532 )
8533 .default_value("--help")
8534 .build(),
8535 );
8536
8537 assert_parse_err(
8538 parse_with_env(&spec, &["test"], &[]),
8539 "Invalid choice for option env: --help, expected one of dev, staging",
8540 );
8541 }
8542
8543 fn words(of: &[&str]) -> Vec<String> {
8545 of.iter().map(|s| s.to_string()).collect()
8546 }
8547
8548 #[test]
8549 fn a_command_that_needs_a_subcommand_says_so() {
8550 let spec: Spec = r#"
8554name "ex"
8555bin "ex"
8556cmd "gen" subcommand_required=#true {
8557 cmd "two" {}
8558 cmd "one" {}
8559 cmd "secret" hide=#true {}
8560 alias "g"
8561}
8562cmd "open" {
8563 cmd "sub" {}
8564}
8565"#
8566 .parse()
8567 .unwrap();
8568
8569 let err = parse(&spec, &words(&["ex", "gen"])).unwrap_err();
8570 assert_eq!(err.to_string(), "`gen` needs a subcommand: one of one, two");
8574
8575 let err = parse(&spec, &words(&["ex", "g"])).unwrap_err();
8577 assert!(err.to_string().starts_with("`gen` needs a subcommand"));
8578
8579 parse(&spec, &words(&["ex", "gen", "one"])).unwrap();
8581
8582 parse(&spec, &words(&["ex", "open"])).unwrap();
8585 parse(&spec, &words(&["ex", "open", "sub"])).unwrap();
8586 }
8587
8588 #[test]
8589 fn arg_required_else_help_observes_the_selected_commands_argv() {
8590 let spec: Spec = r#"
8591name "ex"
8592bin "ex"
8593flag "--verbose" global=#true
8594cmd "run" arg_required_else_help=#true {
8595 flag "--all"
8596}
8597"#
8598 .parse()
8599 .unwrap();
8600 let words = |items: &[&str]| items.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
8601
8602 let err = parse(&spec, &words(&["ex", "run"])).unwrap_err();
8603 assert!(err.to_string().contains("Usage: ex run"), "{err}");
8604
8605 let err = parse(&spec, &words(&["ex", "--verbose", "run"])).unwrap_err();
8608 assert!(err.to_string().contains("Usage: ex run"), "{err}");
8609
8610 parse(&spec, &words(&["ex", "run", "--all"])).expect("run received an argv token");
8611 }
8612
8613 #[test]
8614 fn an_unmatched_word_is_forwarded_when_external_subcommand_is_set() {
8615 let spec: Spec = r#"
8616name "ex"
8617bin "ex"
8618unknown_flags "error"
8619external_subcommand #true
8620cmd "install"
8621flag "-v --verbose" global=#true
8622"#
8623 .parse()
8624 .unwrap();
8625
8626 let parsed = parse(&spec, &input(&["ex", "foo", "--help", "bar"])).unwrap();
8627 assert_eq!(
8628 parsed.external,
8629 Some(vec!["foo".into(), "--help".into(), "bar".into()])
8630 );
8631 assert!(parsed.flags.is_empty());
8632
8633 let parsed = parse(&spec, &input(&["ex", "install"])).unwrap();
8635 assert_eq!(parsed.cmd.name, "install");
8636 assert!(parsed.external.is_none());
8637
8638 let parsed = parse(&spec, &input(&["ex", "-v", "foo", "--verbose"])).unwrap();
8640 assert_eq!(
8641 parsed.external,
8642 Some(vec!["foo".into(), "--verbose".into()])
8643 );
8644 assert!(parsed.flags.keys().any(|flag| flag.name == "verbose"));
8645
8646 assert!(parse(&spec, &input(&["ex", "--wat"])).is_err());
8648
8649 let parsed = parse(&spec, &input(&["ex", "-1", "rest"])).unwrap();
8653 assert_eq!(parsed.external, Some(vec!["-1".into(), "rest".into()]));
8654 }
8655
8656 #[test]
8657 fn an_external_subcommand_satisfies_subcommand_required() {
8658 let mut spec: Spec = r#"
8659name "ex"
8660bin "ex"
8661external_subcommand #true
8662cmd "install"
8663"#
8664 .parse()
8665 .unwrap();
8666 spec.cmd.subcommand_required = true;
8667
8668 parse(&spec, &input(&["ex", "foo", "--help"])).unwrap();
8669 assert!(parse(&spec, &input(&["ex"])).is_err());
8670 }
8671
8672 #[test]
8673 fn a_default_subcommand_outranks_an_external_one() {
8674 let spec: Spec = r#"
8675name "ex"
8676bin "ex"
8677default_subcommand "run"
8678external_subcommand #true
8679cmd "run" {
8680 arg "[task]"
8681}
8682"#
8683 .parse()
8684 .unwrap();
8685
8686 let parsed = parse(&spec, &input(&["ex", "build"])).unwrap();
8687 assert_eq!(parsed.cmd.name, "run");
8688 assert!(parsed.external.is_none());
8689 assert_eq!(first_string_value(&parsed), "build");
8690 }
8691
8692 #[test]
8693 fn multicall_basename_strips_a_path_and_exe() {
8694 assert_eq!(multicall_basename("/usr/bin/ls"), "ls");
8695 assert_eq!(multicall_basename(r"C:\busybox\ls.exe"), "ls");
8696 assert_eq!(multicall_basename("LS.EXE"), "LS");
8697 assert_eq!(multicall_basename("busybox"), "busybox");
8698 }
8699
8700 #[test]
8701 fn a_multicall_applet_is_the_first_word() {
8702 let spec: Spec = r#"
8703name "busybox"
8704bin "busybox"
8705multicall #true
8706cmd "ls" {
8707 arg "[ARGS]" var=#true
8708}
8709cmd "cat"
8710"#
8711 .parse()
8712 .unwrap();
8713
8714 let parsed = parse(&spec, &input(&["/usr/bin/ls", "-l"])).unwrap();
8716 assert_eq!(parsed.cmd.name, "ls");
8717 match parsed.args.values().next() {
8718 Some(ParseValue::MultiString(values)) => assert_eq!(values, &["-l".to_string()]),
8719 other => panic!("expected ARGS to collect -l, got {other:?}"),
8720 }
8721
8722 let parsed = parse(&spec, &input(&["/usr/bin/busybox", "ls", "-l"])).unwrap();
8724 assert_eq!(parsed.cmd.name, "ls");
8725
8726 let mut configured = spec.clone();
8728 configured.name = "BusyBox".to_string();
8729 configured.bin = "/opt/bin/busybox.exe".to_string();
8730 let parsed = parse(&configured, &input(&["/usr/bin/busybox.exe", "ls", "-l"])).unwrap();
8731 assert_eq!(parsed.cmd.name, "ls");
8732
8733 let parsed = parse(&spec, &input(&["ls.exe"])).unwrap();
8735 assert_eq!(parsed.cmd.name, "ls");
8736
8737 let mut plain = spec.clone();
8739 plain.multicall = false;
8740 let parsed = parse(&plain, &input(&["/usr/bin/ls", "ls"])).unwrap();
8741 assert_eq!(parsed.cmd.name, "ls");
8742 }
8743
8744 #[test]
8745 fn a_multicall_unknown_applet_can_be_external() {
8746 let spec: Spec = r#"
8747name "busybox"
8748bin "busybox"
8749multicall #true
8750unknown_flags "error"
8751external_subcommand #true
8752cmd "ls"
8753"#
8754 .parse()
8755 .unwrap();
8756
8757 let parsed = parse(&spec, &input(&["/usr/bin/git", "--help"])).unwrap();
8758 assert_eq!(parsed.external, Some(vec!["git".into(), "--help".into()]));
8759
8760 let mut closed = spec.clone();
8761 closed.cmd.external_subcommand = false;
8762 assert!(parse(&closed, &input(&["wat"])).is_err());
8763 }
8764
8765 #[cfg(feature = "unstable_choices_env")]
8766 #[test]
8767 fn test_parser_arg_choices_from_custom_env() {
8768 let spec = spec_arg_choices_env("DEPLOY_ENVS");
8769
8770 let parsed =
8771 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
8772 assert_eq!(first_string_value(&parsed), "bar");
8773
8774 assert_parse_err(
8775 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
8776 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
8777 );
8778 assert_parse_err(
8779 parse_with_env(&spec, &["test", "prod"], &[]),
8780 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
8781 );
8782 }
8783
8784 #[cfg(feature = "unstable_choices_env")]
8785 #[test]
8786 fn test_parser_validates_flag_choices_from_custom_env() {
8787 let spec = spec_flag_choices_env("DEPLOY_ENVS");
8788 let parsed = parse_with_env(
8789 &spec,
8790 &["test", "--env", "baz"],
8791 &[("DEPLOY_ENVS", "foo,bar baz")],
8792 )
8793 .unwrap();
8794 assert_eq!(first_string_value(&parsed), "baz");
8795 }
8796
8797 #[cfg(feature = "unstable_choices_env")]
8798 #[test]
8799 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
8800 let arg_env_spec = spec_with_arg(
8801 SpecArg::builder()
8802 .name("env")
8803 .env("CURRENT_ENV")
8804 .choices_env("DEPLOY_ENVS")
8805 .build(),
8806 );
8807 assert_parse_err(
8808 parse_with_env(
8809 &arg_env_spec,
8810 &["test"],
8811 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
8812 ),
8813 "Invalid choice for arg env: prod, expected one of dev, staging",
8814 );
8815
8816 let flag_default_spec = spec_with_flag(
8817 SpecFlag::builder()
8818 .long("env")
8819 .arg(
8820 SpecArg::builder()
8821 .name("env")
8822 .choices_env("DEPLOY_ENVS")
8823 .build(),
8824 )
8825 .default_value("prod")
8826 .build(),
8827 );
8828 assert_parse_err(
8829 parse_with_env(
8830 &flag_default_spec,
8831 &["test"],
8832 &[("DEPLOY_ENVS", "dev,staging")],
8833 ),
8834 "Invalid choice for option env: prod, expected one of dev, staging",
8835 );
8836 }
8837
8838 #[test]
8839 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
8840 let spec: Spec = r#"
8841 flag "-v --verbose" var=#true
8842 arg "[database]" default="myapp_dev"
8843 arg "[args...]"
8844 "#
8845 .parse()
8846 .unwrap();
8847 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8848 .into_iter()
8849 .map(String::from)
8850 .collect();
8851 let parsed = parse(&spec, &input).unwrap();
8852 let env = parsed.as_env();
8853 assert_eq!(env.get("usage_database").unwrap(), "mydb");
8854 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
8855 }
8856
8857 #[test]
8858 fn test_variadic_arg_captures_unknown_flags() {
8859 let cmd = SpecCommand::builder()
8860 .name("test")
8861 .flag(SpecFlag::builder().short('v').long("verbose").build())
8862 .arg(SpecArg::builder().name("database").required(false).build())
8863 .arg(
8864 SpecArg::builder()
8865 .name("args")
8866 .required(false)
8867 .var(true)
8868 .build(),
8869 )
8870 .build();
8871 let spec = Spec {
8872 name: "test".to_string(),
8873 bin: "test".to_string(),
8874 cmd,
8875 ..Default::default()
8876 };
8877
8878 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
8880 .into_iter()
8881 .map(String::from)
8882 .collect();
8883 let parsed = parse(&spec, &input).unwrap();
8884 assert_eq!(parsed.args.len(), 2);
8885 let args_val = parsed
8886 .args
8887 .iter()
8888 .find(|(a, _)| a.name == "args")
8889 .unwrap()
8890 .1;
8891 match args_val {
8892 ParseValue::MultiString(v) => {
8893 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8894 }
8895 _ => panic!("Expected MultiString, got {:?}", args_val),
8896 }
8897 }
8898
8899 #[test]
8900 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
8901 let cmd = SpecCommand::builder()
8902 .name("test")
8903 .flag(SpecFlag::builder().short('v').long("verbose").build())
8904 .arg(SpecArg::builder().name("database").required(false).build())
8905 .arg(
8906 SpecArg::builder()
8907 .name("args")
8908 .required(false)
8909 .var(true)
8910 .build(),
8911 )
8912 .build();
8913 let spec = Spec {
8914 name: "test".to_string(),
8915 bin: "test".to_string(),
8916 cmd,
8917 ..Default::default()
8918 };
8919
8920 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
8922 .into_iter()
8923 .map(String::from)
8924 .collect();
8925 let parsed = parse(&spec, &input).unwrap();
8926 assert_eq!(parsed.args.len(), 2);
8927 let args_val = parsed
8928 .args
8929 .iter()
8930 .find(|(a, _)| a.name == "args")
8931 .unwrap()
8932 .1;
8933 match args_val {
8934 ParseValue::MultiString(v) => {
8935 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
8936 }
8937 _ => panic!("Expected MultiString, got {:?}", args_val),
8938 }
8939 }
8940
8941 #[test]
8942 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
8943 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
8946
8947 let input: Vec<String> = vec!["test", "--option=3"]
8950 .into_iter()
8951 .map(String::from)
8952 .collect();
8953 let parsed = parse(&spec, &input).unwrap();
8954 let env = parsed.as_env();
8955 assert_eq!(
8956 env.get("usage_other_args").map(String::as_str),
8957 Some("'--option=3'"),
8958 "expected a single --option=3 token, got {:?}",
8959 env.get("usage_other_args"),
8960 );
8961
8962 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
8964 .into_iter()
8965 .map(String::from)
8966 .collect();
8967 let parsed2 = parse(&spec, &input2).unwrap();
8968 let env2 = parsed2.as_env();
8969 assert_eq!(
8970 env2.get("usage_other_args").map(String::as_str),
8971 Some("'--foo=bar' '--baz=qux'"),
8972 "expected two intact tokens, got {:?}",
8973 env2.get("usage_other_args"),
8974 );
8975
8976 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
8978 .into_iter()
8979 .map(String::from)
8980 .collect();
8981 let parsed3 = parse(&spec, &input3).unwrap();
8982 let env3 = parsed3.as_env();
8983 assert_eq!(
8984 env3.get("usage_other_args").map(String::as_str),
8985 Some("positional1 '--option=3' positional2"),
8986 "expected positional args and intact flag token, got {:?}",
8987 env3.get("usage_other_args"),
8988 );
8989 }
8990
8991 #[test]
8992 fn test_allow_hyphen_values_consumes_short_flag_collision() {
8993 let spec = r#"
8994flag "-d --working-dir <DIR>"
8995flag "-a --args <ARGS>" allow_hyphen_values=#true
8996"#
8997 .parse::<Spec>()
8998 .unwrap();
8999
9000 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
9001
9002 assert_eq!(parsed.flags.len(), 1);
9003 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9004 }
9005
9006 #[test]
9007 fn test_allow_hyphen_values_consumes_embedded_long_value() {
9008 let spec = r#"
9009flag "-d --working-dir <DIR>"
9010flag "-a --args <ARGS>" allow_hyphen_values=#true
9011"#
9012 .parse::<Spec>()
9013 .unwrap();
9014
9015 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
9016
9017 assert_eq!(parsed.flags.len(), 1);
9018 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
9019 }
9020
9021 #[test]
9022 fn test_allow_hyphen_values_takes_the_separator_as_its_value() {
9023 let spec = r#"
9028flag "-a --args <ARGS>" allow_hyphen_values=#true
9029arg "[rest]..."
9030"#
9031 .parse::<Spec>()
9032 .unwrap();
9033
9034 let parsed = parse(&spec, &input(&["test", "-a", "--", "-x"])).unwrap();
9035
9036 assert_eq!(flag_string_value(&parsed, "args"), "--");
9037 let rest = parsed
9038 .args
9039 .values()
9040 .next()
9041 .expect("expected the word after the separator to reach the argument");
9042 assert_eq!(rest.to_string(), "-x");
9043 }
9044
9045 #[test]
9046 fn test_variadic_allow_hyphen_values_collects_after_a_hyphenated_first_value() {
9047 let spec = r#"
9052flag "-a --args <ARGS>..." allow_hyphen_values=#true
9053"#
9054 .parse::<Spec>()
9055 .unwrap();
9056
9057 let parsed = parse(&spec, &input(&["test", "-a", "-x", "b", "c"])).unwrap();
9058
9059 let flag = parsed
9060 .flags
9061 .keys()
9062 .find(|flag| flag.name == "args")
9063 .expect("expected args flag");
9064 match parsed.flags.get(flag).expect("expected args value") {
9065 ParseValue::MultiString(values) => assert_eq!(values, &["-x", "b", "c"]),
9066 other => panic!("expected a list of values, got {other:?}"),
9067 }
9068 }
9069
9070 #[test]
9071 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
9072 let spec = r#"
9073flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
9074"#
9075 .parse::<Spec>()
9076 .unwrap();
9077
9078 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
9079
9080 let flag = parsed
9081 .flags
9082 .keys()
9083 .find(|flag| flag.name == "args")
9084 .expect("expected args flag");
9085 let value = parsed.flags.get(flag).expect("expected args value");
9086 match value {
9087 ParseValue::MultiString(values) => {
9088 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
9089 }
9090 _ => panic!("expected MultiString, got {value:?}"),
9091 }
9092 }
9093
9094 #[test]
9095 fn test_require_equals_accepts_attached_and_refuses_detached() {
9096 let spec = r#"
9097flag "--inspect <PORT>" require_equals=#true
9098"#
9099 .parse::<Spec>()
9100 .unwrap();
9101
9102 let parsed = parse(&spec, &input(&["test", "--inspect=9229"])).unwrap();
9103 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9104
9105 let err = parse(&spec, &input(&["test", "--inspect", "9229"])).unwrap_err();
9106 let msg = format!("{err}");
9107 assert!(
9108 msg.contains("requires an argument") || msg.contains("inspect"),
9109 "detached value must be refused: {msg}"
9110 );
9111 }
9112
9113 #[test]
9114 fn boolean_flags_can_accept_attached_values_when_enabled() {
9115 let spec: Spec = r#"
9116name "ex"
9117bin "ex"
9118flag "--color" negate="--no-color" bool_value=#true
9119arg "[rest]"
9120"#
9121 .parse()
9122 .unwrap();
9123
9124 for (token, expected) in [
9125 ("--color", true),
9126 ("--color=true", true),
9127 ("--color=false", false),
9128 ("--no-color", false),
9129 ("--no-color=false", true),
9130 ] {
9131 let parsed = parse(&spec, &input(&["ex", token])).unwrap();
9132 assert!(
9133 matches!(
9134 parsed.flags.get(&spec.cmd.flags[0]),
9135 Some(ParseValue::Bool(value)) if *value == expected
9136 ),
9137 "{token}"
9138 );
9139 }
9140
9141 let parsed = parse(&spec, &input(&["ex", "--color=false", "word"])).unwrap();
9142 assert!(matches!(
9143 parsed.args.get(&spec.cmd.args[0]),
9144 Some(ParseValue::String(value)) if value == "word"
9145 ));
9146 let err = parse(&spec, &input(&["ex", "--color=maybe"])).unwrap_err();
9147 assert!(err.to_string().contains("expected `true` or `false`"));
9148
9149 let strict: Spec = r#"
9150name "ex"
9151bin "ex"
9152args_override_self #false
9153flag "--color" negate="--no-color" bool_value=#true
9154"#
9155 .parse()
9156 .unwrap();
9157 assert!(parse(&strict, &input(&["ex", "--color=false", "--color=true"])).is_err());
9158 let parsed = parse(
9159 &strict,
9160 &input(&["ex", "--color=false", "--no-color=false"]),
9161 )
9162 .unwrap();
9163 assert!(matches!(
9164 parsed.flags.get(&strict.cmd.flags[0]),
9165 Some(ParseValue::Bool(true))
9166 ));
9167 }
9168
9169 #[test]
9170 fn test_require_equals_refuses_a_detached_value_after_a_short_bundle() {
9171 let spec = r#"
9172flag "-a --all"
9173flag "-i --inspect <PORT>" require_equals=#true
9174"#
9175 .parse::<Spec>()
9176 .unwrap();
9177
9178 let err = parse(&spec, &input(&["test", "-ai", "9229"])).unwrap_err();
9179 let msg = format!("{err}");
9180 assert!(
9181 msg.contains("requires an argument") || msg.contains("inspect"),
9182 "bundled short must refuse the following word: {msg}"
9183 );
9184 }
9185
9186 #[test]
9187 fn test_default_missing_binds_when_the_value_is_left_off() {
9188 let spec = r#"
9189flag "-c --color <WHEN>" default_missing="always"
9190flag "-v --verbose"
9191"#
9192 .parse::<Spec>()
9193 .unwrap();
9194
9195 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9196 assert_eq!(flag_string_value(&parsed, "color"), "always");
9197
9198 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9199 assert_eq!(flag_string_value(&parsed, "color"), "never");
9200
9201 let parsed = parse(&spec, &input(&["test", "--color", "never"])).unwrap();
9202 assert_eq!(flag_string_value(&parsed, "color"), "never");
9203
9204 let parsed = parse(&spec, &input(&["test", "--color", "--verbose"])).unwrap();
9205 assert_eq!(flag_string_value(&parsed, "color"), "always");
9206 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9207
9208 let parsed = parse(&spec, &input(&["test", "--color="])).unwrap();
9209 assert_eq!(flag_string_value(&parsed, "color"), "");
9210
9211 let parsed = parse(&spec, &input(&["test", "-cnever"])).unwrap();
9212 assert_eq!(flag_string_value(&parsed, "color"), "never");
9213
9214 let parsed = parse(&spec, &input(&["test", "-c", "-v"])).unwrap();
9215 assert_eq!(flag_string_value(&parsed, "color"), "always");
9216 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
9217 }
9218
9219 #[test]
9220 fn test_default_missing_requires_opt_in_for_detached_negative_flag_values() {
9221 let spec = r#"
9222flag "--apps <N>"
9223flag "--jobs <N>" default_missing="default missing"
9224flag "--kids <N>" default_missing="default missing" allow_negative_numbers=#true
9225"#
9226 .parse::<Spec>()
9227 .unwrap();
9228
9229 let parsed = parse(&spec, &input(&["test", "--apps", "-1"])).unwrap();
9230 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9231
9232 let err = parse(&spec, &input(&["test", "--jobs", "-1"])).unwrap_err();
9233 assert!(
9234 err.to_string().contains("unexpected word: -1"),
9235 "default_missing must keep an unopted negative value separate: {err}"
9236 );
9237
9238 let parsed = parse(&spec, &input(&["test", "--kids", "-1"])).unwrap();
9239 assert_eq!(flag_string_value(&parsed, "kids"), "-1");
9240
9241 let external_spec = r#"
9242external_subcommand #true
9243flag "--apps <N>"
9244"#
9245 .parse::<Spec>()
9246 .unwrap();
9247 let parsed = parse(&external_spec, &input(&["test", "--apps", "-1"])).unwrap();
9248 assert_eq!(flag_string_value(&parsed, "apps"), "-1");
9249 assert!(parsed.external.is_none());
9250
9251 for words in [
9252 &["test", "--apps", "--jobs", "1"][..],
9253 &["test", "--apps", "--kids", "-1"][..],
9254 ] {
9255 let err = parse(&spec, &input(words)).unwrap_err();
9256 let message = err.to_string();
9257 assert!(
9258 message.contains("--apps") && message.contains("requires an argument"),
9259 "the earlier flag must report its missing value for {words:?}: {message}"
9260 );
9261 }
9262 }
9263
9264 #[test]
9265 fn test_optional_flag_value_preserves_bare_and_explicit_empty_forms() {
9266 let spec = r#"
9267flag "--bump [LEVEL]" value_optional=#true
9268flag "--verbose"
9269arg "[FILE]"
9270"#
9271 .parse::<Spec>()
9272 .unwrap();
9273
9274 let absent = parse(&spec, &input(&["test"])).unwrap();
9275 assert!(!absent.flags.keys().any(|flag| flag.name == "bump"));
9276
9277 let bare = parse(&spec, &input(&["test", "--bump", "--verbose", "file.txt"])).unwrap();
9278 let bump = bare
9279 .flags
9280 .iter()
9281 .find(|(flag, _)| flag.name == "bump")
9282 .map(|(_, value)| value)
9283 .unwrap();
9284 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9285 assert!(bare.flags.keys().any(|flag| flag.name == "verbose"));
9286 assert_eq!(arg_value(&bare, "FILE"), "file.txt");
9287
9288 let explicit = parse(&spec, &input(&["test", "--bump=", "file.txt"])).unwrap();
9289 assert_eq!(flag_string_value(&explicit, "bump"), "");
9290
9291 let corrected = parse(
9292 &spec,
9293 &input(&["test", "--bump=2", "--bump", "--verbose", "file.txt"]),
9294 )
9295 .unwrap();
9296 let bump = corrected
9297 .flags
9298 .iter()
9299 .find(|(flag, _)| flag.name == "bump")
9300 .map(|(_, value)| value)
9301 .unwrap();
9302 assert!(matches!(bump, ParseValue::MultiString(values) if values.is_empty()));
9303
9304 let collecting = r#"
9305flag "--tag [TAG]..." value_optional=#true
9306flag "--verbose"
9307"#
9308 .parse::<Spec>()
9309 .unwrap();
9310 let valued = parse(
9311 &collecting,
9312 &input(&["test", "--tag", "one", "two", "--verbose"]),
9313 )
9314 .unwrap();
9315 let tag = valued
9316 .flags
9317 .iter()
9318 .find(|(flag, _)| flag.name == "tag")
9319 .map(|(_, value)| value)
9320 .unwrap();
9321 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]));
9322 }
9323
9324 #[test]
9325 fn test_repeatable_bare_optional_values_count_each_occurrence() {
9326 let spec = r#"
9327flag "--tag [TAG]" var=#true var_min=2 var_max=2 value_optional=#true
9328"#
9329 .parse::<Spec>()
9330 .unwrap();
9331
9332 let parsed = parse(&spec, &input(&["test", "--tag", "--tag"])).unwrap();
9333 let tag = parsed
9334 .flags
9335 .iter()
9336 .find(|(flag, _)| flag.name == "tag")
9337 .map(|(_, value)| value)
9338 .unwrap();
9339 assert!(matches!(tag, ParseValue::MultiString(values) if values == &["", ""]));
9340
9341 assert!(parse(&spec, &input(&["test", "--tag"])).is_err());
9342 assert!(parse(&spec, &input(&["test", "--tag", "--tag", "--tag"])).is_err());
9343 }
9344
9345 #[test]
9346 fn test_repeatable_variadic_optional_values_do_not_gain_bare_occurrences() {
9347 let spec = r#"
9348flag "--tag [TAG]..." var=#true value_optional=#true
9349flag "--verbose"
9350"#
9351 .parse::<Spec>()
9352 .unwrap();
9353
9354 for argv in [
9355 &["test", "--tag", "one", "two"][..],
9356 &["test", "--tag", "one", "two", "--verbose"][..],
9357 &["test", "--tag", "one", "--tag", "two"][..],
9358 ] {
9359 let parsed = parse(&spec, &input(argv)).unwrap();
9360 let tag = parsed
9361 .flags
9362 .iter()
9363 .find(|(flag, _)| flag.name == "tag")
9364 .map(|(_, value)| value)
9365 .unwrap();
9366 assert!(
9367 matches!(tag, ParseValue::MultiString(values) if values == &["one", "two"]),
9368 "argv={argv:?}: {tag:?}"
9369 );
9370 }
9371
9372 let bare = parse(&spec, &input(&["test", "--tag", "--verbose"])).unwrap();
9373 let tag = bare
9374 .flags
9375 .iter()
9376 .find(|(flag, _)| flag.name == "tag")
9377 .map(|(_, value)| value)
9378 .unwrap();
9379 assert!(matches!(tag, ParseValue::MultiString(values) if values == &[""]));
9380 }
9381
9382 #[test]
9383 fn test_default_missing_with_require_equals_refuses_the_following_word() {
9384 let spec = r#"
9385flag "--inspect <PORT>" require_equals=#true default_missing="9229"
9386arg "[rest]"
9387"#
9388 .parse::<Spec>()
9389 .unwrap();
9390
9391 let parsed = parse(&spec, &input(&["test", "--inspect"])).unwrap();
9392 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9393
9394 let parsed = parse(&spec, &input(&["test", "--inspect=1234"])).unwrap();
9395 assert_eq!(flag_string_value(&parsed, "inspect"), "1234");
9396
9397 let parsed = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap();
9399 assert_eq!(flag_string_value(&parsed, "inspect"), "9229");
9400 assert_eq!(
9401 parsed
9402 .args
9403 .values()
9404 .next()
9405 .map(|v| v.to_string())
9406 .as_deref(),
9407 Some("80")
9408 );
9409
9410 let parsed = parse(&spec, &input(&["test", "--inspect="])).unwrap();
9411 assert_eq!(flag_string_value(&parsed, "inspect"), "");
9412 }
9413
9414 #[test]
9415 fn test_default_missing_must_be_a_choice() {
9416 let spec = r#"
9417flag "--color <WHEN>" default_missing="always" {
9418 choices "auto" "always" "never"
9419}
9420"#
9421 .parse::<Spec>()
9422 .unwrap();
9423
9424 let parsed = parse(&spec, &input(&["test", "--color"])).unwrap();
9425 assert_eq!(flag_string_value(&parsed, "color"), "always");
9426
9427 let parsed = parse(&spec, &input(&["test", "--color=never"])).unwrap();
9428 assert_eq!(flag_string_value(&parsed, "color"), "never");
9429
9430 let spec = r#"
9431flag "--color <WHEN>" default_missing="wat" {
9432 choices "auto" "always" "never"
9433}
9434"#
9435 .parse::<Spec>()
9436 .unwrap();
9437
9438 let err = parse(&spec, &input(&["test", "--color"])).unwrap_err();
9439 let msg = format!("{err}");
9440 assert!(
9441 msg.contains("Invalid choice for option color: wat"),
9442 "missing default has to pass choices the same way a typed value does: {msg}"
9443 );
9444
9445 let err = parse(&spec, &input(&["test", "--color=wat"])).unwrap_err();
9446 let msg = format!("{err}");
9447 assert!(
9448 msg.contains("Invalid choice for option color: wat"),
9449 "an attached value that is not a choice is still refused: {msg}"
9450 );
9451
9452 let spec = r#"
9453flag "--inspect <PORT>" require_equals=#true default_missing="wat" {
9454 choices "9229" "80"
9455}
9456arg "[rest]"
9457"#
9458 .parse::<Spec>()
9459 .unwrap();
9460
9461 let err = parse(&spec, &input(&["test", "--inspect", "80"])).unwrap_err();
9462 let msg = format!("{err}");
9463 assert!(
9464 msg.contains("Invalid choice for option inspect: wat"),
9465 "require_equals still binds the missing string, so the error is the choice: {msg}"
9466 );
9467 }
9468
9469 #[test]
9470 fn test_hyphen_values_still_start_short_flag_parsing() {
9471 let spec = r#"
9472flag "-d --working-dir <DIR>"
9473flag "-a --args <ARGS>"
9474"#
9475 .parse::<Spec>()
9476 .unwrap();
9477
9478 let err = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap_err();
9479 let message = err.to_string();
9480 assert!(
9481 message.contains("--args") && message.contains("requires an argument"),
9482 "the recognized -d must leave the earlier -a missing: {message}"
9483 );
9484 }
9485
9486 mod available_flags {
9489 use super::*;
9490
9491 fn spec() -> Spec {
9492 r#"
9493bin "test"
9494flag "-v --verbose" global=#true
9495flag "--raw" global=#true effect="write"
9496flag "--local-only"
9497cmd "run" {
9498 flag "-r --raw"
9499 flag "-w --watch"
9500 cmd "once"
9501}
9502"#
9503 .parse::<Spec>()
9504 .unwrap()
9505 }
9506
9507 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
9508 let mut chain = vec![&spec.cmd];
9509 for segment in path {
9510 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
9511 }
9512 chain
9513 }
9514
9515 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
9516 let mut names: Vec<_> = available_flags(&chain(spec, path))
9517 .iter()
9518 .map(|f| f.name.clone())
9519 .collect();
9520 names.sort();
9521 names
9522 }
9523
9524 #[test]
9525 fn an_empty_chain_yields_nothing() {
9526 assert!(available_flags(&[]).is_empty());
9527 }
9528
9529 #[test]
9530 fn the_root_gets_its_own_flags() {
9531 let spec = spec();
9532 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
9533 }
9534
9535 #[test]
9536 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
9537 let spec = spec();
9538 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
9539 }
9540
9541 #[test]
9542 fn a_re_declared_global_is_listed_once() {
9543 let spec = r#"
9546bin "test"
9547flag "-y --yes" global=#true effect="write"
9548cmd "rm" {
9549 flag "-y --yes"
9550}
9551"#
9552 .parse::<Spec>()
9553 .unwrap();
9554 let flags = available_flags(&chain(&spec, &["rm"]));
9555 assert_eq!(flags.len(), 1, "{flags:?}");
9556 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
9557 }
9558
9559 #[test]
9560 fn a_re_declared_global_keeps_the_globals_declaration() {
9561 let spec = spec();
9565 let flags = available_flags(&chain(&spec, &["run"]));
9566 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
9567 assert!(raw.global);
9568 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
9569 assert_eq!(raw.short, ['r']);
9570 }
9571
9572 #[test]
9573 fn it_matches_what_a_parse_accepts() {
9574 let spec = spec();
9577 for path in [vec![], vec!["run"], vec!["run", "once"]] {
9578 let argv = std::iter::once("test".to_string())
9579 .chain(path.iter().map(|s| s.to_string()))
9580 .collect::<Vec<_>>();
9581 let parsed = parse_partial(&spec, &argv).unwrap();
9582
9583 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
9584 .map(|f| f.name.clone())
9585 .collect();
9586 from_parse.sort();
9587 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
9588 }
9589 }
9590 }
9591
9592 fn roles(parsed: &ParseOutput, index: usize) -> Vec<String> {
9597 parsed
9598 .tokens
9599 .iter()
9600 .find(|token| token.index == index)
9601 .unwrap_or_else(|| panic!("no token at {index}"))
9602 .roles
9603 .iter()
9604 .map(render_role)
9605 .collect()
9606 }
9607
9608 fn origins(parsed: &ParseOutput, flag: &str) -> Vec<ValueOrigin> {
9609 parsed
9610 .flag_origins
9611 .iter()
9612 .find(|(f, _)| f.name == flag)
9613 .map(|(_, origins)| origins.clone())
9614 .unwrap_or_default()
9615 }
9616
9617 fn explain_with_env(spec: &Spec, words: &[&str], env: &[(&str, &str)]) -> ParseOutput {
9618 let env = env
9619 .iter()
9620 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
9621 .collect();
9622 Parser::new(spec)
9623 .with_env(env)
9624 .explain(&input(words))
9625 .unwrap()
9626 }
9627
9628 fn explain(spec: &Spec, words: &[&str]) -> ParseOutput {
9629 explain_with_env(spec, words, &[])
9630 }
9631
9632 #[test]
9633 fn an_attached_long_value_is_recorded_on_the_flag_token() {
9634 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9635 .parse()
9636 .unwrap();
9637
9638 let parsed = explain(&spec, &["ex", "--env=prod"]);
9639
9640 assert_eq!(roles(&parsed, 0), ["program"]);
9641 assert_eq!(
9642 roles(&parsed, 1),
9643 ["flag env as --env", "value of env = [\"prod\"], attached"]
9644 );
9645 assert!(origins(&parsed, "env").is_empty(), "typed, so no fallback");
9648 }
9649
9650 #[test]
9651 fn a_detached_long_value_is_recorded_on_its_own_token() {
9652 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\n"
9653 .parse()
9654 .unwrap();
9655
9656 let parsed = explain(&spec, &["ex", "--env", "prod"]);
9657
9658 assert_eq!(roles(&parsed, 1), ["flag env as --env"]);
9659 assert_eq!(roles(&parsed, 2), ["value of env = [\"prod\"]"]);
9660 }
9661
9662 #[test]
9663 fn a_short_bundle_is_attributed_to_the_bundle_token() {
9664 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"-a\"\nflag \"-b\"\nflag \"-j <n>\"\n"
9665 .parse()
9666 .unwrap();
9667
9668 let parsed = explain(&spec, &["ex", "-abj8"]);
9669
9670 assert_eq!(
9673 roles(&parsed, 1),
9674 [
9675 "flag a as -a",
9676 "flag b as -b",
9677 "flag j as -j",
9678 "value of j = [\"8\"], attached",
9679 ]
9680 );
9681 assert_eq!(parsed.tokens.len(), 2);
9682 }
9683
9684 #[test]
9685 fn a_delimiter_splits_one_token_into_several_values() {
9686 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--tags <tags>...\" delimiter=\",\"\n"
9687 .parse()
9688 .unwrap();
9689
9690 let parsed = explain(&spec, &["ex", "--tags", "a,b,c"]);
9691
9692 assert_eq!(
9693 roles(&parsed, 2),
9694 ["value of tags = [\"a\", \"b\", \"c\"]"],
9695 "the values meant, not the word typed"
9696 );
9697 }
9698
9699 #[test]
9700 fn a_separator_and_the_words_after_it_are_distinguished() {
9701 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"<src>\"\narg \"[raw]...\"\n"
9702 .parse()
9703 .unwrap();
9704
9705 let parsed = explain(&spec, &["ex", "a", "--", "-x"]);
9706
9707 assert_eq!(roles(&parsed, 1), ["arg src = [\"a\"]"]);
9708 assert_eq!(roles(&parsed, 2), ["separator"]);
9709 assert_eq!(roles(&parsed, 3), ["arg raw = [\"-x\"]"]);
9711 }
9712
9713 #[test]
9714 fn a_second_separator_is_data() {
9715 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[raw]...\"\n"
9716 .parse()
9717 .unwrap();
9718
9719 let parsed = explain(&spec, &["ex", "--", "a", "--", "b"]);
9720
9721 assert_eq!(roles(&parsed, 1), ["separator"]);
9722 assert_eq!(roles(&parsed, 3), ["arg raw = [\"--\"]"]);
9723 }
9724
9725 #[test]
9726 fn an_unknown_flag_says_what_took_it() {
9727 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9728 .parse()
9729 .unwrap();
9730
9731 let parsed = explain(&spec, &["ex", "--wat"]);
9732
9733 assert_eq!(roles(&parsed, 1), ["unknown flag, bound as rest"]);
9736 }
9737
9738 #[test]
9739 fn a_subcommand_word_is_not_a_positional() {
9740 let spec: Spec = "name \"ex\"\nbin \"ex\"\ncmd \"build\" {\n arg \"<target>\"\n}\n"
9741 .parse()
9742 .unwrap();
9743
9744 let parsed = explain(&spec, &["ex", "build", "a"]);
9745
9746 assert_eq!(roles(&parsed, 1), ["subcommand build"]);
9747 assert_eq!(roles(&parsed, 2), ["arg target = [\"a\"]"]);
9748 }
9749
9750 #[test]
9751 fn a_multicall_applet_is_read_at_argv0() {
9752 let spec: Spec =
9753 "name \"box\"\nbin \"box\"\nmulticall #true\ncmd \"ls\" {\n flag \"-l\"\n}\n"
9754 .parse()
9755 .unwrap();
9756
9757 let parsed = explain(&spec, &["/usr/bin/ls", "-l"]);
9758
9759 assert_eq!(roles(&parsed, 0), ["program", "subcommand ls"]);
9762 assert!(parsed.tokens[0].synthesized);
9763 assert_eq!(parsed.tokens[0].word, "/usr/bin/ls");
9764 }
9765
9766 #[test]
9767 fn words_the_parse_never_reached_say_so() {
9768 let spec: Spec = "name \"ex\"\nbin \"ex\"\narg \"[rest]...\"\n"
9769 .parse()
9770 .unwrap();
9771
9772 let parsed = Parser::new(&spec)
9773 .explain(&input(&["ex", "--help", "a"]))
9774 .unwrap();
9775
9776 assert_eq!(roles(&parsed, 2), ["unread"]);
9777 }
9778
9779 #[test]
9780 fn an_env_origin_names_the_variable_that_fired() {
9781 let spec: Spec =
9782 "name \"ex\"\nbin \"ex\"\nflag \"--token <t>\" env=\"EX_TOKEN\" env_fallback=\"EX_TOKEN_OLD\"\n"
9783 .parse()
9784 .unwrap();
9785
9786 let primary = explain_with_env(&spec, &["ex"], &[("EX_TOKEN", "a")]);
9787 assert_eq!(
9788 origins(&primary, "token"),
9789 [ValueOrigin::Env("EX_TOKEN".to_string())]
9790 );
9791
9792 let fallback = explain_with_env(&spec, &["ex"], &[("EX_TOKEN_OLD", "b")]);
9795 assert_eq!(
9796 origins(&fallback, "token"),
9797 [ValueOrigin::Env("EX_TOKEN_OLD".to_string())]
9798 );
9799 }
9800
9801 #[test]
9802 fn a_default_origin_is_recorded_for_flags_and_args() {
9803 let spec: Spec =
9804 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default=\"auto\"\narg \"[src]\" default=\".\"\n"
9805 .parse()
9806 .unwrap();
9807
9808 let parsed = explain(&spec, &["ex"]);
9809
9810 assert_eq!(origins(&parsed, "color"), [ValueOrigin::Default]);
9811 let (arg, origins) = parsed.arg_origins.iter().next().unwrap();
9812 assert_eq!(arg.name, "src");
9813 assert_eq!(origins, &[ValueOrigin::Default]);
9814 }
9815
9816 #[test]
9817 fn a_default_if_origin_carries_the_condition_that_fired() {
9818 let spec: Spec = r#"
9819name "ex"
9820bin "ex"
9821flag "--profile <p>"
9822flag "--strict" {
9823 default_if "--profile" "prod" "true"
9824}
9825 "#
9826 .parse()
9827 .unwrap();
9828
9829 let parsed = explain(&spec, &["ex", "--profile", "prod"]);
9830
9831 assert_eq!(
9834 origins(&parsed, "strict"),
9835 [ValueOrigin::DefaultIf {
9836 selector: "--profile".to_string(),
9837 when: Some("prod".to_string()),
9838 }]
9839 );
9840 }
9841
9842 #[test]
9843 fn a_bare_optional_value_flag_records_default_missing() {
9844 let spec: Spec =
9845 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" default_missing=\"always\"\nflag \"-v\"\n"
9846 .parse()
9847 .unwrap();
9848
9849 let parsed = explain(&spec, &["ex", "--color", "-v"]);
9850
9851 assert_eq!(roles(&parsed, 1), ["flag color as --color"]);
9854 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9855 assert_eq!(roles(&parsed, 2), ["flag v as -v"]);
9856 }
9857
9858 #[test]
9859 fn a_var_flag_can_take_one_value_from_argv_and_one_from_default_missing() {
9860 let spec: Spec =
9861 "name \"ex\"\nbin \"ex\"\nflag \"--color <when>\" var=#true default_missing=\"always\"\n"
9862 .parse()
9863 .unwrap();
9864
9865 let parsed = explain(&spec, &["ex", "--color=red", "--color"]);
9866
9867 assert_eq!(
9869 roles(&parsed, 1),
9870 [
9871 "flag color as --color",
9872 "value of color = [\"red\"], attached"
9873 ]
9874 );
9875 assert_eq!(origins(&parsed, "color"), [ValueOrigin::DefaultMissing]);
9876 }
9877
9878 #[test]
9879 fn an_override_names_the_flag_that_did_it() {
9880 let spec: Spec =
9881 "name \"ex\"\nbin \"ex\"\nflag \"--quiet\" default=\"true\"\nflag \"--loud\" overrides=\"--quiet\"\n"
9882 .parse()
9883 .unwrap();
9884
9885 let parsed = explain(&spec, &["ex", "--loud"]);
9886
9887 assert_eq!(parsed.overridden_flags.get("quiet").unwrap(), "loud");
9890 assert!(origins(&parsed, "quiet").is_empty());
9891 }
9892
9893 #[test]
9894 fn a_restart_token_leaves_the_tokens_and_clears_the_arg_origins() {
9895 let spec: Spec = r#"
9896name "ex"
9897bin "ex"
9898cmd "run" restart_token=":::" {
9899 arg "<task>" default="build"
9900}
9901 "#
9902 .parse()
9903 .unwrap();
9904
9905 let parsed = explain(&spec, &["ex", "run", "lint", ":::", "test"]);
9906
9907 assert_eq!(roles(&parsed, 2), ["arg task = [\"lint\"]"]);
9911 assert_eq!(roles(&parsed, 3), ["restart"]);
9914 assert_eq!(roles(&parsed, 4), ["arg task = [\"test\"]"]);
9915 assert!(parsed.arg_origins.is_empty());
9916 }
9917
9918 #[test]
9919 fn a_value_terminator_says_which_run_it_ended() {
9920 let spec: Spec = r#"
9921name "ex"
9922bin "ex"
9923flag "--exec <cmd>..." value_terminator=";"
9924arg "<src>"
9925 "#
9926 .parse()
9927 .unwrap();
9928
9929 let parsed = explain(&spec, &["ex", "--exec", "rm", "tmp", ";", "a"]);
9930
9931 assert_eq!(roles(&parsed, 3), ["value of exec = [\"tmp\"]"]);
9932 assert_eq!(roles(&parsed, 4), ["value terminator, ends exec"]);
9935 assert_eq!(roles(&parsed, 5), ["arg src = [\"a\"]"]);
9936 }
9937
9938 #[test]
9939 fn an_args_value_terminator_says_which_run_it_ended() {
9940 let spec: Spec = r#"
9941name "ex"
9942bin "ex"
9943arg "<files>..." value_terminator=";"
9944arg "[dest]"
9945 "#
9946 .parse()
9947 .unwrap();
9948
9949 let parsed = explain(&spec, &["ex", "a", "b", ";", "out"]);
9950
9951 assert_eq!(roles(&parsed, 2), ["arg files = [\"b\"]"]);
9952 assert_eq!(roles(&parsed, 3), ["value terminator, ends files"]);
9953 assert_eq!(roles(&parsed, 4), ["arg dest = [\"out\"]"]);
9954 }
9955
9956 #[test]
9957 fn explain_keeps_the_bindings_of_a_command_line_that_fails() {
9958 let spec: Spec = "name \"ex\"\nbin \"ex\"\nflag \"--env <env>\"\narg \"<src>\"\n"
9959 .parse()
9960 .unwrap();
9961
9962 let parsed = Parser::new(&spec)
9963 .explain(&input(&["ex", "--env=prod"]))
9964 .unwrap();
9965
9966 assert!(Parser::new(&spec)
9969 .parse(&input(&["ex", "--env=prod"]))
9970 .is_err());
9971 assert_eq!(
9972 roles(&parsed, 1),
9973 ["flag env as --env", "value of env = [\"prod\"], attached"]
9974 );
9975 assert!(
9976 parsed.errors.iter().any(|e| e.to_string().contains("src")),
9977 "{:?}",
9978 parsed.errors
9979 );
9980 }
9981
9982 #[test]
9983 fn an_external_subcommand_forwards_whole_tokens() {
9984 let spec: Spec = "name \"ex\"\nbin \"ex\"\nexternal_subcommand #true\ncmd \"build\"\n"
9985 .parse()
9986 .unwrap();
9987
9988 let parsed = explain(&spec, &["ex", "deploy", "--now"]);
9989
9990 assert_eq!(roles(&parsed, 1), ["external"]);
9991 assert_eq!(roles(&parsed, 2), ["external"]);
9992 }
9993
9994 #[test]
9995 fn a_view_keeps_the_callers_argv_positions() {
9996 let spec: Spec = r#"
9997bin "ex"
9998view "runner" root="run"
9999cmd "run" {
10000 flag "--token <token>"
10001}
10002 "#
10003 .parse()
10004 .unwrap();
10005
10006 let parsed = explain(&spec, &["runner", "--token", "secret"]);
10007
10008 assert_eq!(roles(&parsed, 0), ["program"]);
10011 assert_eq!(roles(&parsed, 2), ["value of token = [\"secret\"]"]);
10012 }
10013}