1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
7use std::fmt::{Debug, Display, Formatter};
8use std::sync::Arc;
9use strum::EnumTryAs;
10
11#[cfg(feature = "docs")]
12use crate::docs;
13use crate::error::UsageErr;
14use crate::spec::arg::SpecDoubleDashChoices;
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 {
112 if !merged.short.contains(s) {
113 merged.short.push(*s);
114 }
115 }
116 for l in &flag.long {
117 if !merged.long.contains(l) {
118 merged.long.push(l.clone());
119 }
120 }
121 let merged = Arc::new(merged);
122 merged_cache.insert(Arc::as_ptr(&flag) as usize, Arc::clone(&merged));
123 merged_origin.insert(Arc::as_ptr(&merged) as usize, global_origin);
124 for existing in available.values_mut() {
130 if origin_of(&merged_origin, existing) == global_origin {
131 *existing = Arc::clone(&merged);
132 }
133 }
134 merged
135 }
136 };
137 available.insert(key, merged);
138 continue;
139 }
140
141 if available.contains_key(&key) {
145 continue;
146 }
147 available.insert(key, flag);
148 }
149}
150
151fn flag_keys(flag: &SpecFlag) -> Vec<String> {
154 let mut keys: Vec<String> = flag
155 .long
156 .iter()
157 .map(|l| format!("--{l}"))
158 .chain(flag.short.iter().map(|s| format!("-{s}")))
159 .collect();
160 if let Some(negate) = &flag.negate {
161 keys.push(negate.clone());
162 }
163 keys
164}
165
166fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
168 cmd.flags
169 .iter()
170 .flat_map(|f| {
171 let f = Arc::new(f.clone()); flag_keys(&f)
173 .into_iter()
174 .map(|key| (key, Arc::clone(&f)))
175 .collect::<Vec<_>>()
176 })
177 .collect()
178}
179
180fn unique_flags<'a>(
181 flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
182) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
183 let mut seen = HashSet::new();
184 flags
185 .into_iter()
186 .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
187}
188
189pub fn available_flags(chain: &[&SpecCommand]) -> Vec<Arc<SpecFlag>> {
202 let Some((root, rest)) = chain.split_first() else {
203 return vec![];
204 };
205 let mut available = gather_flags(root);
206 for cmd in rest {
207 merge_subcommand_flags(&mut available, gather_flags(cmd), false);
208 }
209
210 let mut seen_names = HashSet::new();
221 unique_flags(available.values())
222 .filter(|f| seen_names.insert(f.name.clone()))
223 .cloned()
224 .collect()
225}
226
227fn get_flag_key(word: &str) -> &str {
230 if word.starts_with("--") {
231 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
233 } else if word.len() >= 2 {
234 &word[0..2]
236 } else {
237 word
238 }
239}
240
241pub struct ParseOutput {
242 pub cmd: SpecCommand,
243 pub cmds: Vec<SpecCommand>,
244 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
245 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
246 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
253 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
254 pub errors: Vec<UsageErr>,
255 pub next_arg: Option<Arc<SpecArg>>,
262 pub double_dash_seen: bool,
268}
269
270impl ParseOutput {
271 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
280 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
281 return self.available_flags.clone();
282 };
283 let start = match boundary.checked_sub(1) {
287 Some(prev) if self.cmds[prev].flags_from_mount => prev,
288 _ => boundary,
289 };
290 let mut offered = gather_flags(&self.cmds[start]);
294 for cmd in &self.cmds[start + 1..] {
295 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
296 }
297 offered
298 }
299}
300
301#[derive(Debug, EnumTryAs, Clone)]
302pub enum ParseValue {
303 Bool(bool),
304 String(String),
305 MultiBool(Vec<bool>),
306 MultiString(Vec<String>),
307}
308
309#[non_exhaustive]
329pub struct Parser<'a> {
330 spec: &'a Spec,
331 env: Option<HashMap<String, String>>,
332}
333
334impl<'a> Parser<'a> {
335 pub fn new(spec: &'a Spec) -> Self {
337 Self { spec, env: None }
338 }
339
340 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
345 self.env = Some(env);
346 self
347 }
348
349 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
353 let custom_env = self.env.as_ref();
354 let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
355 trace!("{out:?}");
356
357 let get_env = |key: &str| -> Option<String> {
358 if let Some(env_map) = custom_env {
359 env_map.get(key).cloned()
360 } else {
361 std::env::var(key).ok()
362 }
363 };
364
365 for arg in out.cmd.args.iter() {
370 if out.args.contains_key(arg) {
371 continue;
372 }
373 if let Some(env_var) = arg.env.as_ref() {
374 if let Some(env_value) = get_env(env_var) {
375 validate_choice_value(
376 ChoiceTarget::arg(arg),
377 &env_value,
378 arg.choices.as_ref(),
379 custom_env,
380 )?;
381 out.args
382 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
383 continue;
384 }
385 }
386 if !arg.default.is_empty() {
387 if arg.var {
389 validate_choice_values(
390 ChoiceTarget::arg(arg),
391 &arg.default,
392 arg.choices.as_ref(),
393 custom_env,
394 )?;
395 out.args.insert(
397 Arc::new(arg.clone()),
398 ParseValue::MultiString(arg.default.clone()),
399 );
400 } else {
401 validate_choice_value(
402 ChoiceTarget::arg(arg),
403 &arg.default[0],
404 arg.choices.as_ref(),
405 custom_env,
406 )?;
407 out.args.insert(
409 Arc::new(arg.clone()),
410 ParseValue::String(arg.default[0].clone()),
411 );
412 }
413 }
414 }
415
416 for flag in out.available_flags.values() {
418 if out.flags.contains_key(flag) {
419 continue;
420 }
421 if let Some(env_var) = flag.env.as_ref() {
422 if let Some(env_value) = get_env(env_var) {
423 if let Some(arg) = flag.arg.as_ref() {
424 validate_choice_value(
425 ChoiceTarget::option(flag),
426 &env_value,
427 arg.choices.as_ref(),
428 custom_env,
429 )?;
430 out.flags
431 .insert(Arc::clone(flag), ParseValue::String(env_value));
432 } else {
433 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
435 out.flags
436 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
437 }
438 continue;
439 }
440 }
441 if !flag.default.is_empty() {
443 if flag.var {
445 if let Some(arg) = flag.arg.as_ref() {
447 validate_choice_values(
448 ChoiceTarget::option(flag),
449 &flag.default,
450 arg.choices.as_ref(),
451 custom_env,
452 )?;
453 out.flags.insert(
454 Arc::clone(flag),
455 ParseValue::MultiString(flag.default.clone()),
456 );
457 } else {
458 let bools: Vec<bool> = flag
460 .default
461 .iter()
462 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
463 .collect();
464 out.flags
465 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
466 }
467 } else {
468 if let Some(arg) = flag.arg.as_ref() {
470 validate_choice_value(
471 ChoiceTarget::option(flag),
472 &flag.default[0],
473 arg.choices.as_ref(),
474 custom_env,
475 )?;
476 out.flags.insert(
477 Arc::clone(flag),
478 ParseValue::String(flag.default[0].clone()),
479 );
480 } else {
481 let is_true =
483 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
484 out.flags
485 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
486 }
487 }
488 }
489 if let Some(arg) = flag.arg.as_ref() {
491 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
492 if flag.var {
493 validate_choice_values(
494 ChoiceTarget::option(flag),
495 &arg.default,
496 arg.choices.as_ref(),
497 custom_env,
498 )?;
499 out.flags.insert(
500 Arc::clone(flag),
501 ParseValue::MultiString(arg.default.clone()),
502 );
503 } else {
504 validate_choice_value(
505 ChoiceTarget::option(flag),
506 &arg.default[0],
507 arg.choices.as_ref(),
508 custom_env,
509 )?;
510 out.flags
511 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
512 }
513 }
514 }
515 }
516 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
517 bail!("{err}");
518 }
519 if !out.errors.is_empty() {
520 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
521 }
522 Ok(out)
523 }
524}
525
526#[must_use = "parsing result should be used"]
533pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
534 Parser::new(spec).parse(input)
535}
536
537#[must_use = "parsing result should be used"]
541pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
542 parse_partial_with_env(spec, input, None)
543}
544
545fn parse_partial_with_env(
547 spec: &Spec,
548 input: &[String],
549 custom_env: Option<&HashMap<String, String>>,
550) -> Result<ParseOutput, miette::Error> {
551 trace!("parse_partial: {input:?}");
552 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
553 input.pop_front();
554
555 let mut out = ParseOutput {
556 cmd: spec.cmd.clone(),
557 cmds: vec![spec.cmd.clone()],
558 args: IndexMap::new(),
559 flags: IndexMap::new(),
560 available_flags: gather_flags(&spec.cmd),
561 flag_awaiting_value: vec![],
562 errors: vec![],
563 next_arg: None,
564 double_dash_seen: false,
565 };
566
567 let mut prefix_words: Vec<String> = vec![];
580 let mut prefix_bindings: VecDeque<Option<Arc<SpecFlag>>> = VecDeque::new();
588 let mut idx = 0;
589 let mut used_default_subcommand = false;
592
593 while idx < input.len() {
594 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
595 let mut subcommand = subcommand.clone();
596 subcommand.mount(&prefix_words)?;
598 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
601 merge_subcommand_flags(
602 &mut out.available_flags,
603 gather_flags(&subcommand),
604 crossing_mount,
605 );
606 input.remove(idx);
608 out.cmds.push(subcommand.clone());
609 out.cmd = subcommand.clone();
610 prefix_words.clear();
611 } else if input[idx].starts_with('-') {
614 let word = input[idx].clone();
616 let flag_key = get_flag_key(&word);
617
618 if let Some(f) = out.available_flags.get(flag_key).cloned() {
619 prefix_bindings.push_back(Some(Arc::clone(&f)));
627 if f.global {
628 prefix_words.push(word.clone());
629 }
630 idx += 1;
631
632 if f.arg.is_some()
635 && !word.contains('=')
636 && idx < input.len()
637 && !input[idx].starts_with('-')
638 {
639 if f.global {
640 prefix_words.push(input[idx].clone());
641 }
642 prefix_bindings.push_back(None);
643 idx += 1;
644 }
645 } else {
646 break;
649 }
650 } else {
651 if !used_default_subcommand {
654 if let Some(default_name) = &spec.default_subcommand {
655 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
656 let mut subcommand = subcommand.clone();
657 subcommand.mount(&prefix_words)?;
659 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
660 merge_subcommand_flags(
661 &mut out.available_flags,
662 gather_flags(&subcommand),
663 crossing_mount,
664 );
665 out.cmds.push(subcommand.clone());
666 out.cmd = subcommand.clone();
667 prefix_words.clear();
668 used_default_subcommand = true;
669 continue;
674 }
675 }
676 }
677 break;
679 }
680 }
681
682 let mut next_arg_idx: usize = 0;
692 let mut enable_flags = true;
693 let mut grouped_flag = false;
694 let mut seen_double_dash = false;
698 let mut double_dash_violations: HashSet<String> = HashSet::new();
701
702 while !input.is_empty() {
703 let mut w = input.pop_front().unwrap();
704 let binding = prefix_bindings.pop_front().flatten();
707
708 if let Some(ref restart_token) = out.cmd.restart_token {
711 if w == *restart_token {
712 out.args.clear();
717 next_arg_idx = 0;
718 out.flag_awaiting_value.clear(); enable_flags = true; seen_double_dash = false; continue;
722 }
723 }
724
725 if w == "--" {
726 enable_flags = false;
728
729 let should_preserve = out
732 .cmd
733 .args
734 .get(next_arg_idx)
735 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
736 .unwrap_or(false);
737
738 if should_preserve {
739 } else {
742 seen_double_dash = true;
743
744 let target = out.cmd.args.iter().position(|arg| {
750 arg.double_dash == SpecDoubleDashChoices::Required
751 && !out.args.contains_key(arg)
752 });
753 if let Some(target) = target {
754 if target > next_arg_idx {
761 next_arg_idx = target;
762 }
763 }
764 continue;
765 }
766 }
767
768 if w.starts_with('-')
769 && out
770 .flag_awaiting_value
771 .last()
772 .is_some_and(|flag| flag.allow_hyphen_values())
773 {
774 let should_return = drain_pending_flag_values(
775 spec,
776 &out.cmd,
777 &mut out.errors,
778 &mut out.flags,
779 &mut out.flag_awaiting_value,
780 &mut w,
781 custom_env,
782 )?;
783 if should_return {
784 record_cursor(&mut out, next_arg_idx, seen_double_dash);
785 return Ok(out);
786 }
787 continue;
788 }
789
790 if enable_flags && w.starts_with("--") {
792 grouped_flag = false;
793 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
794 if let Some(f) = binding.as_ref().or_else(|| out.available_flags.get(word)) {
795 if !val.is_empty() {
799 input.push_front(val.to_string());
800 prefix_bindings.push_front(None);
801 }
802 if f.arg.is_some() {
803 out.flag_awaiting_value.push(Arc::clone(f));
804 } else if f.count {
805 let arr = out
806 .flags
807 .entry(Arc::clone(f))
808 .or_insert_with(|| ParseValue::MultiBool(vec![]))
809 .try_as_multi_bool_mut()
810 .unwrap();
811 arr.push(true);
812 } else {
813 let negate = f.negate.clone().unwrap_or_default();
814 out.flags
815 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
816 }
817 continue;
818 }
819 if is_help_arg(spec, &w) {
820 out.errors
821 .push(render_help_err(spec, &out.cmd, w.len() > 2));
822 record_cursor(&mut out, next_arg_idx, seen_double_dash);
823 return Ok(out);
824 }
825 }
826
827 if enable_flags && w.starts_with('-') && w.len() > 1 {
829 let short = w.chars().nth(1).unwrap();
830 if let Some(f) = binding
831 .as_ref()
832 .or_else(|| out.available_flags.get(&format!("-{short}")))
833 {
834 if w.len() > 2 {
835 input.push_front(format!("-{}", &w[2..]));
836 prefix_bindings.push_front(None);
837 grouped_flag = true;
838 }
839 if f.arg.is_some() {
840 out.flag_awaiting_value.push(Arc::clone(f));
841 } else if f.count {
842 let arr = out
843 .flags
844 .entry(Arc::clone(f))
845 .or_insert_with(|| ParseValue::MultiBool(vec![]))
846 .try_as_multi_bool_mut()
847 .unwrap();
848 arr.push(true);
849 } else {
850 let negate = f.negate.clone().unwrap_or_default();
851 out.flags
852 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
853 }
854 continue;
855 }
856 if is_help_arg(spec, &w) {
857 out.errors
858 .push(render_help_err(spec, &out.cmd, w.len() > 2));
859 record_cursor(&mut out, next_arg_idx, seen_double_dash);
860 return Ok(out);
861 }
862 if grouped_flag {
863 grouped_flag = false;
864 w.remove(0);
865 }
866 }
867
868 if !out.flag_awaiting_value.is_empty() {
869 let should_return = drain_pending_flag_values(
870 spec,
871 &out.cmd,
872 &mut out.errors,
873 &mut out.flags,
874 &mut out.flag_awaiting_value,
875 &mut w,
876 custom_env,
877 )?;
878 if should_return {
879 record_cursor(&mut out, next_arg_idx, seen_double_dash);
880 return Ok(out);
881 }
882 continue;
883 }
884
885 if let Some(arg) = out.cmd.args.get(next_arg_idx) {
886 if arg.double_dash == SpecDoubleDashChoices::Required && !seen_double_dash {
890 report_double_dash_violation(arg, &mut out.errors, &mut double_dash_violations);
891 continue;
895 }
896 if validate_choices(
897 spec,
898 &out.cmd,
899 &mut out.errors,
900 ChoiceTarget::arg(arg),
901 &w,
902 arg.choices.as_ref(),
903 custom_env,
904 )? {
905 record_cursor(&mut out, next_arg_idx, seen_double_dash);
906 return Ok(out);
907 }
908 if arg.var {
909 let arr = out
910 .args
911 .entry(Arc::new(arg.clone()))
912 .or_insert_with(|| ParseValue::MultiString(vec![]))
913 .try_as_multi_string_mut()
914 .unwrap();
915 arr.push(w);
916 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
917 next_arg_idx += 1;
918 }
919 } else {
920 out.args
921 .insert(Arc::new(arg.clone()), ParseValue::String(w));
922 next_arg_idx += 1;
923 }
924 continue;
925 }
926 if is_help_arg(spec, &w) {
927 out.errors
928 .push(render_help_err(spec, &out.cmd, w.len() > 2));
929 record_cursor(&mut out, next_arg_idx, seen_double_dash);
930 return Ok(out);
931 }
932 bail!("unexpected word: {w}");
933 }
934
935 record_cursor(&mut out, next_arg_idx, seen_double_dash);
936
937 for arg in out.cmd.args.iter() {
940 if out.args.contains_key(arg) {
941 continue;
942 }
943 if double_dash_violations.contains(&arg.name) {
945 continue;
946 }
947 if arg.required && arg.default.is_empty() {
948 let has_env = arg.env.as_ref().is_some_and(|e| {
950 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
951 || std::env::var(e).is_ok()
952 });
953 if !has_env {
954 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
955 }
956 }
957 }
958
959 for flag in unique_flags(out.available_flags.values()) {
960 if out.flags.contains_key(flag) {
961 continue;
962 }
963 let has_default =
964 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
965 let has_env = flag.env.as_ref().is_some_and(|e| {
967 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
968 });
969 if flag.required && !has_default && !has_env {
970 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
971 }
972 }
973
974 for (arg, value) in &out.args {
976 if arg.var {
977 if let ParseValue::MultiString(values) = value {
978 if let Some(min) = arg.var_min {
979 if values.len() < min {
980 out.errors.push(UsageErr::VarArgTooFew {
981 name: arg.name.clone(),
982 min,
983 got: values.len(),
984 });
985 }
986 }
987 if let Some(max) = arg.var_max {
988 if values.len() > max {
989 out.errors.push(UsageErr::VarArgTooMany {
990 name: arg.name.clone(),
991 max,
992 got: values.len(),
993 });
994 }
995 }
996 }
997 }
998 }
999
1000 for (flag, value) in &out.flags {
1002 if flag.var {
1003 let count = match value {
1004 ParseValue::MultiString(values) => values.len(),
1005 ParseValue::MultiBool(values) => values.len(),
1006 _ => continue,
1007 };
1008 if let Some(min) = flag.var_min {
1009 if count < min {
1010 out.errors.push(UsageErr::VarFlagTooFew {
1011 name: flag.name.clone(),
1012 min,
1013 got: count,
1014 });
1015 }
1016 }
1017 if let Some(max) = flag.var_max {
1018 if count > max {
1019 out.errors.push(UsageErr::VarFlagTooMany {
1020 name: flag.name.clone(),
1021 max,
1022 got: count,
1023 });
1024 }
1025 }
1026 }
1027 }
1028
1029 Ok(out)
1030}
1031
1032#[cfg(feature = "docs")]
1033fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
1034 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
1035}
1036
1037#[cfg(not(feature = "docs"))]
1038fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
1039 UsageErr::Help("help".to_string())
1040}
1041
1042#[derive(Copy, Clone)]
1043struct ChoiceTarget<'a> {
1044 kind: &'a str,
1045 name: &'a str,
1046}
1047
1048impl<'a> ChoiceTarget<'a> {
1049 fn arg(arg: &'a SpecArg) -> Self {
1050 Self {
1051 kind: "arg",
1052 name: &arg.name,
1053 }
1054 }
1055
1056 fn option(flag: &'a SpecFlag) -> Self {
1057 Self {
1058 kind: "option",
1059 name: &flag.name,
1060 }
1061 }
1062}
1063
1064fn drain_pending_flag_values(
1065 spec: &Spec,
1066 cmd: &SpecCommand,
1067 errors: &mut Vec<UsageErr>,
1068 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
1069 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
1070 word: &mut String,
1071 custom_env: Option<&HashMap<String, String>>,
1072) -> miette::Result<bool> {
1073 while let Some(flag) = flag_awaiting_value.pop() {
1074 let arg = flag.arg.as_ref().unwrap();
1075 if validate_choices(
1076 spec,
1077 cmd,
1078 errors,
1079 ChoiceTarget::option(&flag),
1080 word,
1081 arg.choices.as_ref(),
1082 custom_env,
1083 )? {
1084 return Ok(true);
1085 }
1086 let value = std::mem::take(word);
1087 if flag.var {
1088 let arr = flags
1089 .entry(flag)
1090 .or_insert_with(|| ParseValue::MultiString(vec![]))
1091 .try_as_multi_string_mut()
1092 .unwrap();
1093 arr.push(value);
1094 } else {
1095 flags.insert(flag, ParseValue::String(value));
1096 }
1097 }
1098 Ok(false)
1099}
1100
1101fn choice_error(
1102 target: ChoiceTarget<'_>,
1103 value: &str,
1104 choices: Option<&SpecChoices>,
1105 custom_env: Option<&HashMap<String, String>>,
1106) -> Option<String> {
1107 let choices = choices?;
1108 let values = choices.values_with_env(custom_env);
1109 if values.iter().any(|choice| choice == value) {
1110 return None;
1111 }
1112 if let Some(env) = choices.env() {
1113 if values.is_empty() {
1114 return Some(format!(
1115 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
1116 target.kind, target.name,
1117 ));
1118 }
1119 }
1120 Some(format!(
1121 "Invalid choice for {} {}: {value}, expected one of {}",
1122 target.kind,
1123 target.name,
1124 values.join(", ")
1125 ))
1126}
1127
1128fn validate_choices(
1129 spec: &Spec,
1130 cmd: &SpecCommand,
1131 errors: &mut Vec<UsageErr>,
1132 target: ChoiceTarget<'_>,
1133 value: &str,
1134 choices: Option<&SpecChoices>,
1135 custom_env: Option<&HashMap<String, String>>,
1136) -> miette::Result<bool> {
1137 if is_help_arg(spec, value)
1138 && choices.is_some_and(|choices| {
1139 !choices
1140 .values_with_env(custom_env)
1141 .iter()
1142 .any(|choice| choice == value)
1143 })
1144 {
1145 errors.push(render_help_err(spec, cmd, value.len() > 2));
1146 return Ok(true);
1147 }
1148
1149 if let Some(err) = choice_error(target, value, choices, custom_env) {
1150 bail!("{err}");
1151 }
1152 Ok(false)
1153}
1154
1155fn validate_choice_value(
1156 target: ChoiceTarget<'_>,
1157 value: &str,
1158 choices: Option<&SpecChoices>,
1159 custom_env: Option<&HashMap<String, String>>,
1160) -> miette::Result<()> {
1161 if let Some(err) = choice_error(target, value, choices, custom_env) {
1162 bail!("{err}");
1163 }
1164 Ok(())
1165}
1166
1167fn validate_choice_values(
1168 target: ChoiceTarget<'_>,
1169 values: &[String],
1170 choices: Option<&SpecChoices>,
1171 custom_env: Option<&HashMap<String, String>>,
1172) -> miette::Result<()> {
1173 for value in values {
1174 validate_choice_value(target, value, choices, custom_env)?;
1175 }
1176 Ok(())
1177}
1178
1179fn record_cursor(out: &mut ParseOutput, next_arg_idx: usize, seen_double_dash: bool) {
1183 out.next_arg = out.cmd.args.get(next_arg_idx).cloned().map(Arc::new);
1184 out.double_dash_seen = seen_double_dash;
1185}
1186
1187fn report_double_dash_violation(
1194 arg: &SpecArg,
1195 errors: &mut Vec<UsageErr>,
1196 violations: &mut HashSet<String>,
1197) {
1198 if violations.insert(arg.name.clone()) {
1199 errors.push(UsageErr::ArgRequiresDoubleDash(arg.name.clone()));
1200 }
1201}
1202
1203fn is_help_arg(spec: &Spec, w: &str) -> bool {
1204 spec.disable_help != Some(true)
1205 && (w == "--help"
1206 || w == "-h"
1207 || w == "-?"
1208 || (spec.cmd.subcommands.is_empty() && w == "help"))
1209}
1210
1211impl ParseOutput {
1212 pub fn as_env(&self) -> BTreeMap<String, String> {
1213 let mut env = BTreeMap::new();
1214 for (flag, val) in &self.flags {
1215 let key = format!("usage_{}", flag.name.to_snake_case());
1216 let val = match val {
1217 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1218 ParseValue::String(s) => s.clone(),
1219 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
1220 ParseValue::MultiString(s) => shell_words::join(s),
1221 };
1222 env.insert(key, val);
1223 }
1224 for (arg, val) in &self.args {
1225 let key = format!("usage_{}", arg.name.to_snake_case());
1226 env.insert(key, val.to_string());
1227 }
1228 env
1229 }
1230}
1231
1232impl Display for ParseValue {
1233 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1234 match self {
1235 ParseValue::Bool(b) => write!(f, "{b}"),
1236 ParseValue::String(s) => write!(f, "{s}"),
1237 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
1238 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
1239 }
1240 }
1241}
1242
1243impl Debug for ParseOutput {
1244 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1245 f.debug_struct("ParseOutput")
1246 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
1247 .field(
1248 "args",
1249 &self
1250 .args
1251 .iter()
1252 .map(|(a, w)| format!("{}: {w}", a.name))
1253 .collect_vec(),
1254 )
1255 .field(
1256 "available_flags",
1257 &self
1258 .available_flags
1259 .iter()
1260 .map(|(f, w)| format!("{f}: {w}"))
1261 .collect_vec(),
1262 )
1263 .field(
1264 "flags",
1265 &self
1266 .flags
1267 .iter()
1268 .map(|(f, w)| format!("{}: {w}", f.name))
1269 .collect_vec(),
1270 )
1271 .field("flag_awaiting_value", &self.flag_awaiting_value)
1272 .field("errors", &self.errors)
1273 .finish()
1274 }
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279 use super::*;
1280
1281 fn input(words: &[&str]) -> Vec<String> {
1282 words.iter().map(|word| (*word).to_string()).collect()
1283 }
1284
1285 fn spec_with_arg(arg: SpecArg) -> Spec {
1286 let cmd = SpecCommand::builder().name("test").arg(arg).build();
1287 Spec {
1288 name: "test".to_string(),
1289 bin: "test".to_string(),
1290 cmd,
1291 ..Default::default()
1292 }
1293 }
1294
1295 fn spec_with_flag(flag: SpecFlag) -> Spec {
1296 let cmd = SpecCommand::builder().name("test").flag(flag).build();
1297 Spec {
1298 name: "test".to_string(),
1299 bin: "test".to_string(),
1300 cmd,
1301 ..Default::default()
1302 }
1303 }
1304
1305 fn parse_with_env(
1306 spec: &Spec,
1307 words: &[&str],
1308 env: &[(&str, &str)],
1309 ) -> Result<ParseOutput, miette::Error> {
1310 let env = env
1311 .iter()
1312 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1313 .collect();
1314 Parser::new(spec).with_env(env).parse(&input(words))
1315 }
1316
1317 fn first_string_value(parsed: &ParseOutput) -> &str {
1318 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1319 return value;
1320 }
1321 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1322 return value;
1323 }
1324 panic!("expected first parsed value to be ParseValue::String");
1325 }
1326
1327 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
1328 let flag = parsed
1329 .flags
1330 .keys()
1331 .find(|flag| flag.name == name)
1332 .unwrap_or_else(|| panic!("expected flag {name}"));
1333 let value = parsed
1334 .flags
1335 .get(flag)
1336 .unwrap_or_else(|| panic!("expected value for flag {name}"));
1337 match value {
1338 ParseValue::String(value) => value,
1339 _ => panic!("expected flag {name} to be ParseValue::String"),
1340 }
1341 }
1342
1343 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1344 let err = result.expect_err("expected parser error");
1345 assert_eq!(format!("{err}"), expected);
1346 }
1347
1348 #[cfg(feature = "unstable_choices_env")]
1349 fn spec_arg_choices_env(key: &str) -> Spec {
1350 spec_with_arg(
1351 SpecArg::builder()
1352 .name("env")
1353 .choices_env(key)
1354 .required(false)
1355 .build(),
1356 )
1357 }
1358
1359 #[cfg(feature = "unstable_choices_env")]
1360 fn spec_flag_choices_env(key: &str) -> Spec {
1361 spec_with_flag(
1362 SpecFlag::builder()
1363 .long("env")
1364 .arg(SpecArg::builder().name("env").choices_env(key).build())
1365 .build(),
1366 )
1367 }
1368
1369 #[test]
1370 fn test_parse() {
1371 let cmd = SpecCommand::builder()
1372 .name("test")
1373 .arg(SpecArg::builder().name("arg").build())
1374 .flag(SpecFlag::builder().long("flag").build())
1375 .build();
1376 let spec = Spec {
1377 name: "test".to_string(),
1378 bin: "test".to_string(),
1379 cmd,
1380 ..Default::default()
1381 };
1382 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1383 let parsed = parse(&spec, &input).unwrap();
1384 assert_eq!(parsed.cmds.len(), 1);
1385 assert_eq!(parsed.cmds[0].name, "test");
1386 assert_eq!(parsed.args.len(), 1);
1387 assert_eq!(parsed.flags.len(), 1);
1388 assert_eq!(parsed.available_flags.len(), 1);
1389 }
1390
1391 #[test]
1392 fn test_as_env() {
1393 let cmd = SpecCommand::builder()
1394 .name("test")
1395 .arg(SpecArg::builder().name("arg").build())
1396 .flag(SpecFlag::builder().long("flag").build())
1397 .flag(
1398 SpecFlag::builder()
1399 .long("force")
1400 .negate("--no-force")
1401 .build(),
1402 )
1403 .build();
1404 let spec = Spec {
1405 name: "test".to_string(),
1406 bin: "test".to_string(),
1407 cmd,
1408 ..Default::default()
1409 };
1410 let input = vec![
1411 "test".to_string(),
1412 "--flag".to_string(),
1413 "--no-force".to_string(),
1414 ];
1415 let parsed = parse(&spec, &input).unwrap();
1416 let env = parsed.as_env();
1417 assert_eq!(env.len(), 2);
1418 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1419 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1420 }
1421
1422 #[test]
1423 fn test_arg_env_var() {
1424 let cmd = SpecCommand::builder()
1425 .name("test")
1426 .arg(
1427 SpecArg::builder()
1428 .name("input")
1429 .env("TEST_ARG_INPUT")
1430 .required(true)
1431 .build(),
1432 )
1433 .build();
1434 let spec = Spec {
1435 name: "test".to_string(),
1436 bin: "test".to_string(),
1437 cmd,
1438 ..Default::default()
1439 };
1440
1441 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1443
1444 let input = vec!["test".to_string()];
1445 let parsed = parse(&spec, &input).unwrap();
1446
1447 assert_eq!(parsed.args.len(), 1);
1448 let arg = parsed.args.keys().next().unwrap();
1449 assert_eq!(arg.name, "input");
1450 let value = parsed.args.values().next().unwrap();
1451 assert_eq!(value.to_string(), "test_file.txt");
1452
1453 std::env::remove_var("TEST_ARG_INPUT");
1455 }
1456
1457 #[test]
1458 fn test_flag_env_var_with_arg() {
1459 let cmd = SpecCommand::builder()
1460 .name("test")
1461 .flag(
1462 SpecFlag::builder()
1463 .long("output")
1464 .env("TEST_FLAG_OUTPUT")
1465 .arg(SpecArg::builder().name("file").build())
1466 .build(),
1467 )
1468 .build();
1469 let spec = Spec {
1470 name: "test".to_string(),
1471 bin: "test".to_string(),
1472 cmd,
1473 ..Default::default()
1474 };
1475
1476 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1478
1479 let input = vec!["test".to_string()];
1480 let parsed = parse(&spec, &input).unwrap();
1481
1482 assert_eq!(parsed.flags.len(), 1);
1483 let flag = parsed.flags.keys().next().unwrap();
1484 assert_eq!(flag.name, "output");
1485 let value = parsed.flags.values().next().unwrap();
1486 assert_eq!(value.to_string(), "output.txt");
1487
1488 std::env::remove_var("TEST_FLAG_OUTPUT");
1490 }
1491
1492 #[test]
1493 fn test_flag_env_var_boolean() {
1494 let cmd = SpecCommand::builder()
1495 .name("test")
1496 .flag(
1497 SpecFlag::builder()
1498 .long("verbose")
1499 .env("TEST_FLAG_VERBOSE")
1500 .build(),
1501 )
1502 .build();
1503 let spec = Spec {
1504 name: "test".to_string(),
1505 bin: "test".to_string(),
1506 cmd,
1507 ..Default::default()
1508 };
1509
1510 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1512
1513 let input = vec!["test".to_string()];
1514 let parsed = parse(&spec, &input).unwrap();
1515
1516 assert_eq!(parsed.flags.len(), 1);
1517 let flag = parsed.flags.keys().next().unwrap();
1518 assert_eq!(flag.name, "verbose");
1519 let value = parsed.flags.values().next().unwrap();
1520 assert_eq!(value.to_string(), "true");
1521
1522 std::env::remove_var("TEST_FLAG_VERBOSE");
1524 }
1525
1526 #[test]
1527 fn test_env_var_precedence() {
1528 let cmd = SpecCommand::builder()
1530 .name("test")
1531 .arg(
1532 SpecArg::builder()
1533 .name("input")
1534 .env("TEST_PRECEDENCE_INPUT")
1535 .required(true)
1536 .build(),
1537 )
1538 .build();
1539 let spec = Spec {
1540 name: "test".to_string(),
1541 bin: "test".to_string(),
1542 cmd,
1543 ..Default::default()
1544 };
1545
1546 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1548
1549 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1550 let parsed = parse(&spec, &input).unwrap();
1551
1552 assert_eq!(parsed.args.len(), 1);
1553 let value = parsed.args.values().next().unwrap();
1554 assert_eq!(value.to_string(), "cli_file.txt");
1556
1557 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1559 }
1560
1561 #[test]
1562 fn test_flag_var_true_with_single_default() {
1563 let cmd = SpecCommand::builder()
1565 .name("test")
1566 .flag(
1567 SpecFlag::builder()
1568 .long("foo")
1569 .var(true)
1570 .arg(SpecArg::builder().name("foo").build())
1571 .default_value("bar")
1572 .build(),
1573 )
1574 .build();
1575 let spec = Spec {
1576 name: "test".to_string(),
1577 bin: "test".to_string(),
1578 cmd,
1579 ..Default::default()
1580 };
1581
1582 let input = vec!["test".to_string()];
1584 let parsed = parse(&spec, &input).unwrap();
1585
1586 assert_eq!(parsed.flags.len(), 1);
1587 let flag = parsed.flags.keys().next().unwrap();
1588 assert_eq!(flag.name, "foo");
1589 let value = parsed.flags.values().next().unwrap();
1590 match value {
1592 ParseValue::MultiString(v) => {
1593 assert_eq!(v.len(), 1);
1594 assert_eq!(v[0], "bar");
1595 }
1596 _ => panic!("Expected MultiString, got {:?}", value),
1597 }
1598 }
1599
1600 #[test]
1601 fn test_flag_var_true_with_multiple_defaults() {
1602 let cmd = SpecCommand::builder()
1604 .name("test")
1605 .flag(
1606 SpecFlag::builder()
1607 .long("foo")
1608 .var(true)
1609 .arg(SpecArg::builder().name("foo").build())
1610 .default_values(["xyz", "bar"])
1611 .build(),
1612 )
1613 .build();
1614 let spec = Spec {
1615 name: "test".to_string(),
1616 bin: "test".to_string(),
1617 cmd,
1618 ..Default::default()
1619 };
1620
1621 let input = vec!["test".to_string()];
1623 let parsed = parse(&spec, &input).unwrap();
1624
1625 assert_eq!(parsed.flags.len(), 1);
1626 let value = parsed.flags.values().next().unwrap();
1627 match value {
1629 ParseValue::MultiString(v) => {
1630 assert_eq!(v.len(), 2);
1631 assert_eq!(v[0], "xyz");
1632 assert_eq!(v[1], "bar");
1633 }
1634 _ => panic!("Expected MultiString, got {:?}", value),
1635 }
1636 }
1637
1638 #[test]
1639 fn test_flag_var_false_with_default_remains_string() {
1640 let cmd = SpecCommand::builder()
1642 .name("test")
1643 .flag(
1644 SpecFlag::builder()
1645 .long("foo")
1646 .var(false) .arg(SpecArg::builder().name("foo").build())
1648 .default_value("bar")
1649 .build(),
1650 )
1651 .build();
1652 let spec = Spec {
1653 name: "test".to_string(),
1654 bin: "test".to_string(),
1655 cmd,
1656 ..Default::default()
1657 };
1658
1659 let input = vec!["test".to_string()];
1661 let parsed = parse(&spec, &input).unwrap();
1662
1663 assert_eq!(parsed.flags.len(), 1);
1664 let value = parsed.flags.values().next().unwrap();
1665 match value {
1667 ParseValue::String(s) => {
1668 assert_eq!(s, "bar");
1669 }
1670 _ => panic!("Expected String, got {:?}", value),
1671 }
1672 }
1673
1674 #[test]
1675 fn test_arg_var_true_with_single_default() {
1676 let cmd = SpecCommand::builder()
1678 .name("test")
1679 .arg(
1680 SpecArg::builder()
1681 .name("files")
1682 .var(true)
1683 .default_value("default.txt")
1684 .required(false)
1685 .build(),
1686 )
1687 .build();
1688 let spec = Spec {
1689 name: "test".to_string(),
1690 bin: "test".to_string(),
1691 cmd,
1692 ..Default::default()
1693 };
1694
1695 let input = vec!["test".to_string()];
1697 let parsed = parse(&spec, &input).unwrap();
1698
1699 assert_eq!(parsed.args.len(), 1);
1700 let value = parsed.args.values().next().unwrap();
1701 match value {
1703 ParseValue::MultiString(v) => {
1704 assert_eq!(v.len(), 1);
1705 assert_eq!(v[0], "default.txt");
1706 }
1707 _ => panic!("Expected MultiString, got {:?}", value),
1708 }
1709 }
1710
1711 #[test]
1712 fn test_arg_var_true_with_multiple_defaults() {
1713 let cmd = SpecCommand::builder()
1715 .name("test")
1716 .arg(
1717 SpecArg::builder()
1718 .name("files")
1719 .var(true)
1720 .default_values(["file1.txt", "file2.txt"])
1721 .required(false)
1722 .build(),
1723 )
1724 .build();
1725 let spec = Spec {
1726 name: "test".to_string(),
1727 bin: "test".to_string(),
1728 cmd,
1729 ..Default::default()
1730 };
1731
1732 let input = vec!["test".to_string()];
1734 let parsed = parse(&spec, &input).unwrap();
1735
1736 assert_eq!(parsed.args.len(), 1);
1737 let value = parsed.args.values().next().unwrap();
1738 match value {
1740 ParseValue::MultiString(v) => {
1741 assert_eq!(v.len(), 2);
1742 assert_eq!(v[0], "file1.txt");
1743 assert_eq!(v[1], "file2.txt");
1744 }
1745 _ => panic!("Expected MultiString, got {:?}", value),
1746 }
1747 }
1748
1749 #[test]
1750 fn test_arg_var_false_with_default_remains_string() {
1751 let cmd = SpecCommand::builder()
1753 .name("test")
1754 .arg(
1755 SpecArg::builder()
1756 .name("file")
1757 .var(false)
1758 .default_value("default.txt")
1759 .required(false)
1760 .build(),
1761 )
1762 .build();
1763 let spec = Spec {
1764 name: "test".to_string(),
1765 bin: "test".to_string(),
1766 cmd,
1767 ..Default::default()
1768 };
1769
1770 let input = vec!["test".to_string()];
1772 let parsed = parse(&spec, &input).unwrap();
1773
1774 assert_eq!(parsed.args.len(), 1);
1775 let value = parsed.args.values().next().unwrap();
1776 match value {
1778 ParseValue::String(s) => {
1779 assert_eq!(s, "default.txt");
1780 }
1781 _ => panic!("Expected String, got {:?}", value),
1782 }
1783 }
1784
1785 #[test]
1786 fn test_scalar_defaults_validate_only_first_default_choice() {
1787 let specs = [
1788 spec_with_arg(
1789 SpecArg::builder()
1790 .name("env")
1791 .var(false)
1792 .default_values(["dev", "prod"])
1793 .choices(["dev"])
1794 .required(false)
1795 .build(),
1796 ),
1797 spec_with_flag(
1798 SpecFlag::builder()
1799 .long("env")
1800 .arg(
1801 SpecArg::builder()
1802 .name("env")
1803 .default_values(["dev", "prod"])
1804 .choices(["dev"])
1805 .build(),
1806 )
1807 .build(),
1808 ),
1809 ];
1810
1811 for spec in specs {
1812 let parsed = parse(&spec, &input(&["test"])).unwrap();
1813 assert_eq!(first_string_value(&parsed), "dev");
1814 }
1815 }
1816
1817 #[test]
1818 fn test_default_subcommand() {
1819 let run_cmd = SpecCommand::builder()
1821 .name("run")
1822 .arg(SpecArg::builder().name("task").build())
1823 .build();
1824 let mut cmd = SpecCommand::builder().name("test").build();
1825 cmd.subcommands.insert("run".to_string(), run_cmd);
1826
1827 let spec = Spec {
1828 name: "test".to_string(),
1829 bin: "test".to_string(),
1830 cmd,
1831 default_subcommand: Some("run".to_string()),
1832 ..Default::default()
1833 };
1834
1835 let input = vec!["test".to_string(), "mytask".to_string()];
1837 let parsed = parse(&spec, &input).unwrap();
1838
1839 assert_eq!(parsed.cmds.len(), 2);
1841 assert_eq!(parsed.cmds[1].name, "run");
1842
1843 assert_eq!(parsed.args.len(), 1);
1845 let arg = parsed.args.keys().next().unwrap();
1846 assert_eq!(arg.name, "task");
1847 let value = parsed.args.values().next().unwrap();
1848 assert_eq!(value.to_string(), "mytask");
1849 }
1850
1851 #[test]
1852 fn test_default_subcommand_explicit_still_works() {
1853 let run_cmd = SpecCommand::builder()
1855 .name("run")
1856 .arg(SpecArg::builder().name("task").build())
1857 .build();
1858 let other_cmd = SpecCommand::builder()
1859 .name("other")
1860 .arg(SpecArg::builder().name("other_arg").build())
1861 .build();
1862 let mut cmd = SpecCommand::builder().name("test").build();
1863 cmd.subcommands.insert("run".to_string(), run_cmd);
1864 cmd.subcommands.insert("other".to_string(), other_cmd);
1865
1866 let spec = Spec {
1867 name: "test".to_string(),
1868 bin: "test".to_string(),
1869 cmd,
1870 default_subcommand: Some("run".to_string()),
1871 ..Default::default()
1872 };
1873
1874 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1876 let parsed = parse(&spec, &input).unwrap();
1877
1878 assert_eq!(parsed.cmds.len(), 2);
1880 assert_eq!(parsed.cmds[1].name, "other");
1881 }
1882
1883 #[test]
1884 fn test_default_subcommand_with_nested_subcommands() {
1885 let say_cmd = SpecCommand::builder()
1889 .name("say")
1890 .arg(SpecArg::builder().name("name").build())
1891 .build();
1892 let mut run_cmd = SpecCommand::builder().name("run").build();
1893 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1894
1895 let mut cmd = SpecCommand::builder().name("test").build();
1896 cmd.subcommands.insert("run".to_string(), run_cmd);
1897
1898 let spec = Spec {
1899 name: "test".to_string(),
1900 bin: "test".to_string(),
1901 cmd,
1902 default_subcommand: Some("run".to_string()),
1903 ..Default::default()
1904 };
1905
1906 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1908 let parsed = parse(&spec, &input).unwrap();
1909
1910 assert_eq!(parsed.cmds.len(), 3);
1912 assert_eq!(parsed.cmds[0].name, "test");
1913 assert_eq!(parsed.cmds[1].name, "run");
1914 assert_eq!(parsed.cmds[2].name, "say");
1915
1916 assert_eq!(parsed.args.len(), 1);
1918 let arg = parsed.args.keys().next().unwrap();
1919 assert_eq!(arg.name, "name");
1920 let value = parsed.args.values().next().unwrap();
1921 assert_eq!(value.to_string(), "hello");
1922 }
1923
1924 fn mounted_global_flag_spec() -> Spec {
1932 let task_cmd = SpecCommand::builder()
1933 .name("sample:run")
1934 .arg(
1935 SpecArg::builder()
1936 .name("profile")
1937 .choices(["alpha", "beta", "gamma"])
1938 .build(),
1939 )
1940 .build();
1941 let mut run_cmd = SpecCommand::builder()
1943 .name("run")
1944 .flag(
1945 SpecFlag::builder()
1946 .name("cd")
1947 .short('C')
1948 .long("cd")
1949 .arg(SpecArg::builder().name("dir").build())
1950 .global(false)
1951 .build(),
1952 )
1953 .build();
1954 run_cmd
1955 .subcommands
1956 .insert("sample:run".to_string(), task_cmd);
1957
1958 let mut cmd = SpecCommand::builder()
1959 .name("test")
1960 .flag(
1961 SpecFlag::builder()
1962 .name("cd")
1963 .short('C')
1964 .long("cd")
1965 .arg(SpecArg::builder().name("dir").build())
1966 .global(true)
1967 .build(),
1968 )
1969 .build();
1970 cmd.subcommands.insert("run".to_string(), run_cmd);
1971
1972 Spec {
1973 name: "test".to_string(),
1974 bin: "test".to_string(),
1975 cmd,
1976 ..Default::default()
1977 }
1978 }
1979
1980 #[test]
1981 fn test_prefix_global_flag_does_not_pollute_choices() {
1982 let spec = mounted_global_flag_spec();
1989
1990 for words in [
1993 &["test", "-C", "/tmp", "run", "sample:run"][..],
1994 &["test", "--cd=/tmp", "run", "sample:run"][..],
1996 ] {
1997 let parsed = parse_partial(&spec, &input(words)).unwrap();
1998 assert_eq!(
1999 parsed
2000 .cmds
2001 .iter()
2002 .map(|c| c.name.as_str())
2003 .collect::<Vec<_>>(),
2004 vec!["test", "run", "sample:run"],
2005 );
2006 assert!(
2008 parsed.args.is_empty(),
2009 "args should be empty, got {:?}",
2010 parsed.args
2011 );
2012
2013 let cd = parsed
2016 .available_flags
2017 .get("--cd")
2018 .expect("--cd should remain available after descending into the subcommand");
2019 assert!(cd.global, "--cd must stay global after descent");
2020 assert!(
2021 parsed.available_flags.get("-C").is_some_and(|f| f.global),
2022 "-C must stay global after descent",
2023 );
2024
2025 assert_eq!(
2029 parsed.as_env().get("usage_cd").map(String::as_str),
2030 Some("/tmp"),
2031 "global flag value must survive in as_env(), got {:?}",
2032 parsed.as_env(),
2033 );
2034 }
2035
2036 let parsed = parse_partial(
2038 &spec,
2039 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
2040 )
2041 .unwrap();
2042 assert_eq!(parsed.args.len(), 1);
2043 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
2044
2045 assert_parse_err(
2047 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
2048 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
2049 );
2050 }
2051
2052 fn mounted_orphan_short_spec() -> Spec {
2057 let task_cmd = SpecCommand::builder()
2058 .name("sample:run")
2059 .arg(
2060 SpecArg::builder()
2061 .name("profile")
2062 .choices(["alpha", "beta", "gamma"])
2063 .build(),
2064 )
2065 .build();
2066 let mut run_cmd = SpecCommand::builder()
2069 .name("run")
2070 .flag(
2071 SpecFlag::builder()
2072 .name("raw")
2073 .short('r')
2074 .long("raw")
2075 .global(false)
2076 .build(),
2077 )
2078 .flag(
2079 SpecFlag::builder()
2080 .name("force")
2081 .short('f')
2082 .long("force")
2083 .global(false)
2084 .build(),
2085 )
2086 .build();
2087 run_cmd
2088 .subcommands
2089 .insert("sample:run".to_string(), task_cmd);
2090
2091 let mut cmd = SpecCommand::builder()
2093 .name("test")
2094 .flag(
2095 SpecFlag::builder()
2096 .name("raw")
2097 .long("raw")
2098 .global(true)
2099 .build(),
2100 )
2101 .build();
2102 cmd.subcommands.insert("run".to_string(), run_cmd);
2103
2104 Spec {
2105 name: "test".to_string(),
2106 bin: "test".to_string(),
2107 cmd,
2108 ..Default::default()
2109 }
2110 }
2111
2112 #[test]
2113 fn test_orphan_short_alias_survives_merge() {
2114 let spec = mounted_orphan_short_spec();
2120
2121 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
2122 assert_eq!(
2123 parsed
2124 .cmds
2125 .iter()
2126 .map(|c| c.name.as_str())
2127 .collect::<Vec<_>>(),
2128 vec!["test", "run", "sample:run"],
2129 );
2130
2131 assert!(
2134 parsed.available_flags.get("-r").is_some_and(|f| f.global),
2135 "-r must be merged onto the inherited global flag and stay global after descent",
2136 );
2137 assert!(
2138 parsed
2139 .available_flags
2140 .get("--raw")
2141 .is_some_and(|f| f.global),
2142 "--raw must stay global after descent",
2143 );
2144
2145 assert!(
2147 parsed.args.is_empty(),
2148 "args should be empty, got {:?}",
2149 parsed.args
2150 );
2151
2152 assert_eq!(
2154 parsed.as_env().get("usage_raw").map(String::as_str),
2155 Some("true"),
2156 "merged short's value must survive in as_env(), got {:?}",
2157 parsed.as_env(),
2158 );
2159
2160 assert!(
2163 !parsed.available_flags.contains_key("-f"),
2164 "purely-local -f must not be promoted onto a global",
2165 );
2166 assert!(
2167 !parsed.available_flags.contains_key("--force"),
2168 "purely-local --force must not be promoted onto a global",
2169 );
2170
2171 let parsed =
2173 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
2174 assert_eq!(parsed.args.len(), 1);
2175 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
2176
2177 assert_parse_err(
2179 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
2180 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
2181 );
2182 }
2183
2184 #[test]
2185 fn test_orphan_short_does_not_clobber_unrelated_global() {
2186 let run_cmd = SpecCommand::builder()
2191 .name("run")
2192 .flag(
2193 SpecFlag::builder()
2194 .name("raw")
2195 .short('r')
2196 .long("raw")
2197 .global(false)
2198 .build(),
2199 )
2200 .build();
2201 let mut cmd = SpecCommand::builder()
2202 .name("test")
2203 .flag(
2204 SpecFlag::builder()
2205 .name("raw")
2206 .long("raw")
2207 .global(true)
2208 .build(),
2209 )
2210 .flag(
2211 SpecFlag::builder()
2212 .name("restrict")
2213 .short('r')
2214 .long("restrict")
2215 .global(true)
2216 .build(),
2217 )
2218 .build();
2219 cmd.subcommands.insert("run".to_string(), run_cmd);
2220 let spec = Spec {
2221 name: "test".to_string(),
2222 bin: "test".to_string(),
2223 cmd,
2224 ..Default::default()
2225 };
2226
2227 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2228 assert_eq!(
2230 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
2231 Some("restrict"),
2232 "-r must remain owned by the unrelated global it already belonged to",
2233 );
2234 assert!(parsed
2236 .available_flags
2237 .get("--raw")
2238 .is_some_and(|f| f.global));
2239 assert!(parsed
2240 .available_flags
2241 .get("--restrict")
2242 .is_some_and(|f| f.global));
2243 }
2244
2245 #[test]
2246 fn test_redeclared_global_aliases_share_one_flag() {
2247 let spec = r#"
2254flag "-y --yes" global=#true effect="write"
2255cmd "run" {
2256 flag "-y --yes --assume-yes"
2257}
2258"#
2259 .parse::<Spec>()
2260 .unwrap();
2261
2262 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2263
2264 for key in ["-y", "--yes", "--assume-yes"] {
2265 let flag = parsed
2266 .available_flags
2267 .get(key)
2268 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
2269 assert!(flag.global, "{key} must stay global after the descent");
2270 assert_eq!(
2271 flag.long,
2272 vec!["yes".to_string(), "assume-yes".to_string()],
2273 "{key} must resolve to the flag carrying every alias",
2274 );
2275 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
2276 }
2277
2278 assert_eq!(
2280 unique_flags(parsed.available_flags.values()).count(),
2281 1,
2282 "all aliases must point at one flag object, got {:?}",
2283 parsed.available_flags,
2284 );
2285
2286 assert_eq!(
2288 parsed.available_flags["-y"].effect,
2289 Some(crate::SpecCommandEffect::Write),
2290 );
2291 }
2292
2293 #[test]
2294 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
2295 let spec = r#"
2301flag "-y --yes --confirm" global=#true
2302cmd "run" {
2303 flag "--yes --assume-yes"
2304}
2305"#
2306 .parse::<Spec>()
2307 .unwrap();
2308
2309 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2310
2311 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
2312 let flag = parsed
2313 .available_flags
2314 .get(key)
2315 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
2316 assert!(flag.global, "{key} must stay global after the descent");
2317 assert_eq!(
2318 flag.long,
2319 vec![
2320 "yes".to_string(),
2321 "confirm".to_string(),
2322 "assume-yes".to_string()
2323 ],
2324 "{key} must resolve to the flag carrying every alias",
2325 );
2326 }
2327
2328 assert_eq!(
2329 unique_flags(parsed.available_flags.values()).count(),
2330 1,
2331 "all aliases must point at one flag object, got {:?}",
2332 parsed.available_flags,
2333 );
2334 }
2335
2336 fn mounted_task_flag_spec() -> Spec {
2343 let mut task_cmd = SpecCommand::builder()
2344 .name("mytask")
2345 .flag(
2346 SpecFlag::builder()
2347 .name("env")
2348 .long("env")
2349 .arg(
2350 SpecArg::builder()
2351 .name("name")
2352 .choices(["dev", "stage", "prod"])
2353 .build(),
2354 )
2355 .global(false)
2356 .build(),
2357 )
2358 .flag(
2359 SpecFlag::builder()
2360 .name("bump")
2361 .long("bump")
2362 .arg(
2363 SpecArg::builder()
2364 .name("type")
2365 .choices(["auto", "major"])
2366 .build(),
2367 )
2368 .global(false)
2369 .build(),
2370 )
2371 .build();
2372 task_cmd.mounted = true;
2373
2374 let mut run_cmd = SpecCommand::builder()
2375 .name("run")
2376 .flag(
2377 SpecFlag::builder()
2378 .name("force")
2379 .short('f')
2380 .long("force")
2381 .global(false)
2382 .build(),
2383 )
2384 .build();
2385 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
2386
2387 let mut cmd = SpecCommand::builder()
2388 .name("test")
2389 .flag(
2390 SpecFlag::builder()
2391 .name("env")
2392 .short('E')
2393 .long("env")
2394 .arg(SpecArg::builder().name("ENV").build())
2395 .global(true)
2396 .build(),
2397 )
2398 .flag(
2399 SpecFlag::builder()
2400 .name("silent")
2401 .long("silent")
2402 .global(true)
2403 .build(),
2404 )
2405 .build();
2406 cmd.subcommands.insert("run".to_string(), run_cmd);
2407
2408 Spec {
2409 name: "test".to_string(),
2410 bin: "test".to_string(),
2411 cmd,
2412 ..Default::default()
2413 }
2414 }
2415
2416 #[test]
2417 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
2418 let deep = SpecCommand::builder().name("deep").build();
2424 let mut sub = SpecCommand::builder()
2425 .name("sub")
2426 .flag(
2428 SpecFlag::builder()
2429 .name("cd")
2430 .short('C')
2431 .long("cd")
2432 .arg(SpecArg::builder().name("dir").build())
2433 .global(false)
2434 .build(),
2435 )
2436 .build();
2437 sub.subcommands.insert("deep".to_string(), deep);
2438 let mut task = SpecCommand::builder()
2439 .name("task")
2440 .flag(
2441 SpecFlag::builder()
2442 .name("cd")
2443 .short('C')
2444 .long("cd")
2445 .arg(SpecArg::builder().name("dir").build())
2446 .global(true)
2447 .build(),
2448 )
2449 .build();
2450 task.subcommands.insert("sub".to_string(), sub);
2451 task.mark_mounted();
2452
2453 let mut run_cmd = SpecCommand::builder().name("run").build();
2454 run_cmd.subcommands.insert("task".to_string(), task);
2455 let mut cmd = SpecCommand::builder().name("test").build();
2456 cmd.subcommands.insert("run".to_string(), run_cmd);
2457 let spec = Spec {
2458 name: "test".to_string(),
2459 bin: "test".to_string(),
2460 cmd,
2461 ..Default::default()
2462 };
2463
2464 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
2465 assert!(
2466 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
2467 "the mounted program's own global must survive descents inside the mounted tree",
2468 );
2469 assert!(
2470 parsed.completion_flags().contains_key("--cd"),
2471 "and must still be offered there: it belongs to the mounted program",
2472 );
2473 assert!(
2474 parsed.completion_flags().contains_key("-C"),
2475 "including the short the nested command re-declared",
2476 );
2477 }
2478
2479 #[test]
2480 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
2481 let mut task = SpecCommand::builder()
2486 .name("task")
2487 .flag(
2488 SpecFlag::builder()
2489 .name("bump")
2490 .long("bump")
2491 .global(false)
2492 .build(),
2493 )
2494 .build();
2495 task.mark_mounted();
2496
2497 let mut run_cmd = SpecCommand::builder().name("run").build();
2498 run_cmd.subcommands.insert("task".to_string(), task);
2499 run_cmd.flags = vec![
2501 SpecFlag::builder()
2502 .name("tglobal")
2503 .long("tglobal")
2504 .global(true)
2505 .build(),
2506 SpecFlag::builder()
2507 .name("tlocal")
2508 .long("tlocal")
2509 .global(false)
2510 .build(),
2511 ];
2512 run_cmd.flags_from_mount = true;
2513
2514 let mut cmd = SpecCommand::builder()
2515 .name("test")
2516 .flag(
2517 SpecFlag::builder()
2518 .name("silent")
2519 .long("silent")
2520 .global(true)
2521 .build(),
2522 )
2523 .build();
2524 cmd.subcommands.insert("run".to_string(), run_cmd);
2525 let spec = Spec {
2526 name: "test".to_string(),
2527 bin: "test".to_string(),
2528 cmd,
2529 ..Default::default()
2530 };
2531
2532 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
2533 assert_eq!(
2534 parsed.completion_flags().keys().collect::<Vec<_>>(),
2535 vec!["--bump", "--tglobal"],
2536 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
2537 `--silent` does not, and the mount's non-global root flag is not inherited",
2538 );
2539 }
2540
2541 #[test]
2542 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
2543 let spec = mounted_task_flag_spec();
2548 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
2549
2550 assert!(parsed.available_flags.contains_key("--silent"));
2552 assert!(parsed.available_flags.contains_key("-E"));
2553 assert_eq!(
2555 parsed.completion_flags().keys().collect::<Vec<_>>(),
2556 vec!["--bump", "--env"],
2557 "only the mounted command's own flags may be offered",
2558 );
2559
2560 assert!(!parsed.available_flags.contains_key("--force"));
2562 }
2563
2564 #[test]
2565 fn test_mounted_cmd_flag_wins_over_inherited_global() {
2566 let spec = mounted_task_flag_spec();
2569 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
2570
2571 let awaiting = parsed
2572 .flag_awaiting_value
2573 .first()
2574 .expect("--env should await a value");
2575 assert_eq!(
2576 awaiting
2577 .arg
2578 .as_ref()
2579 .and_then(|a| a.choices.as_ref())
2580 .map(|c| c.choices.clone()),
2581 Some(vec![
2582 "dev".to_string(),
2583 "stage".to_string(),
2584 "prod".to_string()
2585 ]),
2586 "the mounted command's own --env must win over the inherited global",
2587 );
2588
2589 let parsed =
2592 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
2593 assert!(
2594 parsed.args.is_empty(),
2595 "prefix global tokens must not be consumed as positionals, got {:?}",
2596 parsed.args
2597 );
2598 assert_eq!(
2599 parsed.as_env().get("usage_env").map(String::as_str),
2600 Some("anything"),
2601 );
2602 }
2603
2604 #[test]
2605 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
2606 let spec = mounted_task_flag_spec();
2611 let parsed = parse_partial(
2612 &spec,
2613 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
2614 )
2615 .unwrap();
2616 assert!(
2617 parsed.errors.is_empty(),
2618 "prefix global value must not be validated against the mounted flag: {:?}",
2619 parsed
2620 .errors
2621 .iter()
2622 .map(|e| e.to_string())
2623 .collect::<Vec<_>>(),
2624 );
2625 assert_eq!(
2626 parsed.as_env().get("usage_env").map(String::as_str),
2627 Some("not-a-task-choice"),
2628 );
2629
2630 let parsed = parse_partial(
2632 &spec,
2633 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
2634 )
2635 .unwrap();
2636 assert!(parsed.errors.is_empty());
2637 assert_eq!(
2638 parsed.as_env().get("usage_env").map(String::as_str),
2639 Some("not-a-task-choice"),
2640 );
2641
2642 let parsed = parse_partial(
2645 &spec,
2646 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
2647 )
2648 .unwrap();
2649 let awaiting = parsed
2650 .flag_awaiting_value
2651 .first()
2652 .expect("--env should await a value");
2653 assert_eq!(
2654 awaiting
2655 .arg
2656 .as_ref()
2657 .and_then(|a| a.choices.as_ref())
2658 .map(|c| c.choices.clone()),
2659 Some(vec![
2660 "dev".to_string(),
2661 "stage".to_string(),
2662 "prod".to_string()
2663 ]),
2664 "the mounted command's --env must own the name after the mounted command",
2665 );
2666 }
2667
2668 #[test]
2669 fn test_non_global_flag_does_not_hide_subcommand() {
2670 let spec = mounted_task_flag_spec();
2674
2675 for words in [
2676 &["test", "run", "--force", "mytask"][..],
2678 &["test", "run", "-f", "mytask"][..],
2679 &["test", "-E", "prod", "run", "--force", "mytask"][..],
2681 ] {
2682 let parsed = parse_partial(&spec, &input(words)).unwrap();
2683 assert_eq!(
2684 parsed
2685 .cmds
2686 .iter()
2687 .map(|c| c.name.as_str())
2688 .collect::<Vec<_>>(),
2689 vec!["test", "run", "mytask"],
2690 "{words:?} should descend into the mounted command",
2691 );
2692 assert!(
2693 parsed.args.is_empty(),
2694 "{words:?} should not consume a positional, got {:?}",
2695 parsed.args,
2696 );
2697 assert_eq!(
2698 parsed.as_env().get("usage_force").map(String::as_str),
2699 Some("true"),
2700 "the non-global flag must still be recorded for {words:?}",
2701 );
2702 }
2703
2704 let mut run_cmd = SpecCommand::builder()
2707 .name("run")
2708 .flag(
2709 SpecFlag::builder()
2710 .name("output")
2711 .short('o')
2712 .long("output")
2713 .arg(SpecArg::builder().name("mode").build())
2714 .global(false)
2715 .build(),
2716 )
2717 .build();
2718 run_cmd.subcommands.insert(
2719 "task".to_string(),
2720 SpecCommand::builder().name("task").build(),
2721 );
2722 let mut cmd = SpecCommand::builder().name("test").build();
2723 cmd.subcommands.insert("run".to_string(), run_cmd);
2724 let spec = Spec {
2725 name: "test".to_string(),
2726 bin: "test".to_string(),
2727 cmd,
2728 ..Default::default()
2729 };
2730
2731 let parsed =
2732 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
2733 assert_eq!(
2734 parsed
2735 .cmds
2736 .iter()
2737 .map(|c| c.name.as_str())
2738 .collect::<Vec<_>>(),
2739 vec!["test", "run", "task"],
2740 );
2741 assert_eq!(
2742 parsed.as_env().get("usage_output").map(String::as_str),
2743 Some("quiet"),
2744 );
2745
2746 assert_parse_err(
2749 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
2750 "unexpected word: --nope",
2751 );
2752 }
2753
2754 #[test]
2755 fn test_non_mounted_subcommand_offers_inherited_globals() {
2756 let mut run_cmd = SpecCommand::builder().name("run").build();
2759 run_cmd.subcommands.insert(
2760 "nested".to_string(),
2761 SpecCommand::builder().name("nested").build(),
2762 );
2763 let mut cmd = SpecCommand::builder()
2764 .name("test")
2765 .flag(
2766 SpecFlag::builder()
2767 .name("silent")
2768 .long("silent")
2769 .global(true)
2770 .build(),
2771 )
2772 .build();
2773 cmd.subcommands.insert("run".to_string(), run_cmd);
2774 let spec = Spec {
2775 name: "test".to_string(),
2776 bin: "test".to_string(),
2777 cmd,
2778 ..Default::default()
2779 };
2780
2781 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
2782 assert_eq!(
2783 parsed.completion_flags().keys().collect::<Vec<_>>(),
2784 parsed.available_flags.keys().collect::<Vec<_>>(),
2785 );
2786 assert!(parsed.completion_flags().contains_key("--silent"));
2787 }
2788
2789 #[test]
2790 fn test_subcommand_alias_collision_keeps_last_owner() {
2791 let run_cmd = SpecCommand::builder()
2795 .name("run")
2796 .flag(
2797 SpecFlag::builder()
2798 .name("alpha")
2799 .short('x')
2800 .long("alpha")
2801 .global(false)
2802 .build(),
2803 )
2804 .flag(
2805 SpecFlag::builder()
2806 .name("beta")
2807 .short('x')
2808 .long("beta")
2809 .global(false)
2810 .build(),
2811 )
2812 .build();
2813 let mut cmd = SpecCommand::builder().name("test").build();
2814 cmd.subcommands.insert("run".to_string(), run_cmd);
2815 let spec = Spec {
2816 name: "test".to_string(),
2817 bin: "test".to_string(),
2818 cmd,
2819 ..Default::default()
2820 };
2821
2822 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2823 assert_eq!(
2825 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
2826 Some("beta"),
2827 "the last-declared flag must keep a shared short alias",
2828 );
2829 assert_eq!(
2831 parsed
2832 .available_flags
2833 .get("--alpha")
2834 .map(|f| f.name.as_str()),
2835 Some("alpha"),
2836 );
2837 assert_eq!(
2838 parsed
2839 .available_flags
2840 .get("--beta")
2841 .map(|f| f.name.as_str()),
2842 Some("beta"),
2843 );
2844 }
2845
2846 #[test]
2847 fn test_default_subcommand_same_name_child() {
2848 let run_task = SpecCommand::builder()
2852 .name("run")
2853 .arg(SpecArg::builder().name("args").build())
2854 .build();
2855 let mut run_cmd = SpecCommand::builder().name("run").build();
2856 run_cmd.subcommands.insert("run".to_string(), run_task);
2857
2858 let mut cmd = SpecCommand::builder().name("test").build();
2859 cmd.subcommands.insert("run".to_string(), run_cmd);
2860
2861 let spec = Spec {
2862 name: "test".to_string(),
2863 bin: "test".to_string(),
2864 cmd,
2865 default_subcommand: Some("run".to_string()),
2866 ..Default::default()
2867 };
2868
2869 let input = vec!["test".to_string(), "run".to_string()];
2871 let parsed = parse(&spec, &input).unwrap();
2872
2873 assert_eq!(parsed.cmds.len(), 2);
2875 assert_eq!(parsed.cmds[0].name, "test");
2876 assert_eq!(parsed.cmds[1].name, "run");
2877
2878 let input = vec![
2880 "test".to_string(),
2881 "run".to_string(),
2882 "run".to_string(),
2883 "hello".to_string(),
2884 ];
2885 let parsed = parse(&spec, &input).unwrap();
2886
2887 assert_eq!(parsed.cmds.len(), 3);
2888 assert_eq!(parsed.cmds[0].name, "test");
2889 assert_eq!(parsed.cmds[1].name, "run");
2890 assert_eq!(parsed.cmds[2].name, "run");
2891 assert_eq!(parsed.args.len(), 1);
2892 let value = parsed.args.values().next().unwrap();
2893 assert_eq!(value.to_string(), "hello");
2894
2895 let mut run_cmd = SpecCommand::builder()
2899 .name("run")
2900 .arg(SpecArg::builder().name("task").build())
2901 .build();
2902 let run_task = SpecCommand::builder().name("run").build();
2903 run_cmd.subcommands.insert("run".to_string(), run_task);
2904
2905 let mut cmd = SpecCommand::builder().name("test").build();
2906 cmd.subcommands.insert("run".to_string(), run_cmd);
2907
2908 let spec = Spec {
2909 name: "test".to_string(),
2910 bin: "test".to_string(),
2911 cmd,
2912 default_subcommand: Some("run".to_string()),
2913 ..Default::default()
2914 };
2915
2916 let input = vec!["test".to_string(), "other".to_string()];
2917 let parsed = parse(&spec, &input).unwrap();
2918
2919 assert_eq!(parsed.cmds.len(), 2);
2922 assert_eq!(parsed.cmds[0].name, "test");
2923 assert_eq!(parsed.cmds[1].name, "run");
2924
2925 assert_eq!(parsed.args.len(), 1);
2927 let value = parsed.args.values().next().unwrap();
2928 assert_eq!(value.to_string(), "other");
2929 }
2930
2931 #[test]
2932 fn test_restart_token() {
2933 let run_cmd = SpecCommand::builder()
2935 .name("run")
2936 .arg(SpecArg::builder().name("task").build())
2937 .restart_token(":::".to_string())
2938 .build();
2939 let mut cmd = SpecCommand::builder().name("test").build();
2940 cmd.subcommands.insert("run".to_string(), run_cmd);
2941
2942 let spec = Spec {
2943 name: "test".to_string(),
2944 bin: "test".to_string(),
2945 cmd,
2946 ..Default::default()
2947 };
2948
2949 let input = vec![
2951 "test".to_string(),
2952 "run".to_string(),
2953 "task1".to_string(),
2954 ":::".to_string(),
2955 "task2".to_string(),
2956 ];
2957 let parsed = parse(&spec, &input).unwrap();
2958
2959 assert_eq!(parsed.args.len(), 1);
2961 let value = parsed.args.values().next().unwrap();
2962 assert_eq!(value.to_string(), "task2");
2963 }
2964
2965 #[test]
2966 fn test_restart_token_multiple() {
2967 let run_cmd = SpecCommand::builder()
2969 .name("run")
2970 .arg(SpecArg::builder().name("task").build())
2971 .restart_token(":::".to_string())
2972 .build();
2973 let mut cmd = SpecCommand::builder().name("test").build();
2974 cmd.subcommands.insert("run".to_string(), run_cmd);
2975
2976 let spec = Spec {
2977 name: "test".to_string(),
2978 bin: "test".to_string(),
2979 cmd,
2980 ..Default::default()
2981 };
2982
2983 let input = vec![
2985 "test".to_string(),
2986 "run".to_string(),
2987 "task1".to_string(),
2988 ":::".to_string(),
2989 "task2".to_string(),
2990 ":::".to_string(),
2991 "task3".to_string(),
2992 ];
2993 let parsed = parse(&spec, &input).unwrap();
2994
2995 assert_eq!(parsed.args.len(), 1);
2997 let value = parsed.args.values().next().unwrap();
2998 assert_eq!(value.to_string(), "task3");
2999 }
3000
3001 #[test]
3002 fn test_restart_token_clears_flag_awaiting_value() {
3003 let run_cmd = SpecCommand::builder()
3005 .name("run")
3006 .arg(SpecArg::builder().name("task").build())
3007 .flag(
3008 SpecFlag::builder()
3009 .name("jobs")
3010 .long("jobs")
3011 .arg(SpecArg::builder().name("count").build())
3012 .build(),
3013 )
3014 .restart_token(":::".to_string())
3015 .build();
3016 let mut cmd = SpecCommand::builder().name("test").build();
3017 cmd.subcommands.insert("run".to_string(), run_cmd);
3018
3019 let spec = Spec {
3020 name: "test".to_string(),
3021 bin: "test".to_string(),
3022 cmd,
3023 ..Default::default()
3024 };
3025
3026 let input = vec![
3028 "test".to_string(),
3029 "run".to_string(),
3030 "task1".to_string(),
3031 "--jobs".to_string(),
3032 ":::".to_string(),
3033 "task2".to_string(),
3034 ];
3035 let parsed = parse(&spec, &input).unwrap();
3036
3037 assert_eq!(parsed.args.len(), 1);
3039 let value = parsed.args.values().next().unwrap();
3040 assert_eq!(value.to_string(), "task2");
3041 assert!(parsed.flag_awaiting_value.is_empty());
3043 }
3044
3045 #[test]
3046 fn test_restart_token_resets_double_dash() {
3047 let run_cmd = SpecCommand::builder()
3049 .name("run")
3050 .arg(SpecArg::builder().name("task").build())
3051 .arg(SpecArg::builder().name("extra_args").var(true).build())
3052 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
3053 .restart_token(":::".to_string())
3054 .build();
3055 let mut cmd = SpecCommand::builder().name("test").build();
3056 cmd.subcommands.insert("run".to_string(), run_cmd);
3057
3058 let spec = Spec {
3059 name: "test".to_string(),
3060 bin: "test".to_string(),
3061 cmd,
3062 ..Default::default()
3063 };
3064
3065 let input = vec![
3067 "test".to_string(),
3068 "run".to_string(),
3069 "task1".to_string(),
3070 "--".to_string(),
3071 "extra".to_string(),
3072 ":::".to_string(),
3073 "--verbose".to_string(),
3074 "task2".to_string(),
3075 ];
3076 let parsed = parse(&spec, &input).unwrap();
3077
3078 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
3080 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
3082 let value = parsed.args.get(task_arg).unwrap();
3083 assert_eq!(value.to_string(), "task2");
3084 }
3085
3086 #[test]
3087 fn test_double_dashes_without_preserve() {
3088 let run_cmd = SpecCommand::builder()
3090 .name("run")
3091 .arg(SpecArg::builder().name("args").var(true).build())
3092 .build();
3093 let mut cmd = SpecCommand::builder().name("test").build();
3094 cmd.subcommands.insert("run".to_string(), run_cmd);
3095
3096 let spec = Spec {
3097 name: "test".to_string(),
3098 bin: "test".to_string(),
3099 cmd,
3100 ..Default::default()
3101 };
3102
3103 let input = vec![
3105 "test".to_string(),
3106 "run".to_string(),
3107 "arg1".to_string(),
3108 "--".to_string(),
3109 "arg2".to_string(),
3110 "--".to_string(),
3111 "arg3".to_string(),
3112 ];
3113 let parsed = parse(&spec, &input).unwrap();
3114
3115 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3116 let value = parsed.args.get(args_arg).unwrap();
3117 assert_eq!(value.to_string(), "arg1 arg2 arg3");
3118 }
3119
3120 #[test]
3121 fn test_double_dashes_with_preserve() {
3122 let run_cmd = SpecCommand::builder()
3124 .name("run")
3125 .arg(
3126 SpecArg::builder()
3127 .name("args")
3128 .var(true)
3129 .double_dash(SpecDoubleDashChoices::Preserve)
3130 .build(),
3131 )
3132 .build();
3133 let mut cmd = SpecCommand::builder().name("test").build();
3134 cmd.subcommands.insert("run".to_string(), run_cmd);
3135
3136 let spec = Spec {
3137 name: "test".to_string(),
3138 bin: "test".to_string(),
3139 cmd,
3140 ..Default::default()
3141 };
3142
3143 let input = vec![
3145 "test".to_string(),
3146 "run".to_string(),
3147 "arg1".to_string(),
3148 "--".to_string(),
3149 "arg2".to_string(),
3150 "--".to_string(),
3151 "arg3".to_string(),
3152 ];
3153 let parsed = parse(&spec, &input).unwrap();
3154
3155 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3156 let value = parsed.args.get(args_arg).unwrap();
3157 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
3158 }
3159
3160 #[test]
3161 fn test_double_dashes_with_preserve_only_dashes() {
3162 let run_cmd = SpecCommand::builder()
3165 .name("run")
3166 .arg(
3167 SpecArg::builder()
3168 .name("args")
3169 .var(true)
3170 .double_dash(SpecDoubleDashChoices::Preserve)
3171 .build(),
3172 )
3173 .build();
3174 let mut cmd = SpecCommand::builder().name("test").build();
3175 cmd.subcommands.insert("run".to_string(), run_cmd);
3176
3177 let spec = Spec {
3178 name: "test".to_string(),
3179 bin: "test".to_string(),
3180 cmd,
3181 ..Default::default()
3182 };
3183
3184 let input = vec![
3186 "test".to_string(),
3187 "run".to_string(),
3188 "--".to_string(),
3189 "--".to_string(),
3190 ];
3191 let parsed = parse(&spec, &input).unwrap();
3192
3193 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3194 let value = parsed.args.get(args_arg).unwrap();
3195 assert_eq!(value.to_string(), "-- --");
3196 }
3197
3198 #[test]
3199 fn test_double_dashes_with_preserve_multiple_args() {
3200 let run_cmd = SpecCommand::builder()
3202 .name("run")
3203 .arg(SpecArg::builder().name("task").build())
3204 .arg(
3205 SpecArg::builder()
3206 .name("extra_args")
3207 .var(true)
3208 .double_dash(SpecDoubleDashChoices::Preserve)
3209 .build(),
3210 )
3211 .build();
3212 let mut cmd = SpecCommand::builder().name("test").build();
3213 cmd.subcommands.insert("run".to_string(), run_cmd);
3214
3215 let spec = Spec {
3216 name: "test".to_string(),
3217 bin: "test".to_string(),
3218 cmd,
3219 ..Default::default()
3220 };
3221
3222 let input = vec![
3225 "test".to_string(),
3226 "run".to_string(),
3227 "task1".to_string(),
3228 "--".to_string(),
3229 "arg1".to_string(),
3230 "--".to_string(),
3231 "--foo".to_string(),
3232 ];
3233 let parsed = parse(&spec, &input).unwrap();
3234
3235 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
3236 let task_value = parsed.args.get(task_arg).unwrap();
3237 assert_eq!(task_value.to_string(), "task1");
3238
3239 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
3240 let extra_value = parsed.args.get(extra_arg).unwrap();
3241 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
3242 }
3243
3244 fn spec_with_args(args: impl IntoIterator<Item = SpecArg>) -> Spec {
3245 let cmd = SpecCommand::builder().name("test").args(args).build();
3246 Spec {
3247 name: "test".to_string(),
3248 bin: "test".to_string(),
3249 cmd,
3250 ..Default::default()
3251 }
3252 }
3253
3254 fn arg_value(parsed: &ParseOutput, name: &str) -> String {
3255 let arg = parsed
3256 .args
3257 .keys()
3258 .find(|a| a.name == name)
3259 .unwrap_or_else(|| panic!("expected arg {name} to be parsed"));
3260 parsed.args.get(arg).unwrap().to_string()
3261 }
3262
3263 fn required_arg(name: &str) -> SpecArg {
3264 SpecArg::builder()
3265 .name(name)
3266 .var(true)
3267 .required(false)
3268 .double_dash(SpecDoubleDashChoices::Required)
3269 .build()
3270 }
3271
3272 #[test]
3273 fn test_double_dash_required_reports_error_once_for_variadic() {
3274 let spec = spec_with_args([required_arg("files")]);
3276
3277 let parsed = parse_partial(&spec, &input(&["test", "a", "b", "c"])).unwrap();
3278
3279 assert!(parsed.args.is_empty());
3280 assert_eq!(parsed.errors.len(), 1);
3281 assert!(
3282 matches!(&parsed.errors[0], UsageErr::ArgRequiresDoubleDash(name) if name == "files")
3283 );
3284 }
3285
3286 #[test]
3287 fn test_double_dash_required_suppresses_missing_arg() {
3288 let spec = spec_with_args([SpecArg::builder()
3290 .name("file")
3291 .required(true)
3292 .double_dash(SpecDoubleDashChoices::Required)
3293 .build()]);
3294
3295 let parsed = parse_partial(&spec, &input(&["test", "x"])).unwrap();
3296
3297 assert_eq!(parsed.errors.len(), 1);
3298 assert!(matches!(
3299 &parsed.errors[0],
3300 UsageErr::ArgRequiresDoubleDash(_)
3301 ));
3302 assert_eq!(
3304 parsed.next_arg.as_ref().map(|a| a.name.as_str()),
3305 Some("file")
3306 );
3307 assert!(!parsed.double_dash_seen);
3308 }
3309
3310 #[test]
3311 fn test_double_dash_routes_to_required_arg() {
3312 let spec = spec_with_args([
3315 SpecArg::builder()
3316 .name("tool")
3317 .var(true)
3318 .required(false)
3319 .build(),
3320 required_arg("command"),
3321 ]);
3322
3323 let parsed = parse(&spec, &input(&["test", "node@20", "--", "node", "app.js"])).unwrap();
3324
3325 assert_eq!(arg_value(&parsed, "tool"), "node@20");
3326 assert_eq!(arg_value(&parsed, "command"), "node app.js");
3327 assert!(parsed.double_dash_seen);
3328 }
3329
3330 #[test]
3331 fn test_double_dash_routes_with_gap_reports_missing_arg() {
3332 let spec = spec_with_args([
3335 SpecArg::builder()
3336 .name("tool")
3337 .var(true)
3338 .required(true)
3339 .build(),
3340 required_arg("command"),
3341 ]);
3342
3343 let parsed = parse_partial(&spec, &input(&["test", "--", "ls"])).unwrap();
3344
3345 assert_eq!(arg_value(&parsed, "command"), "ls");
3346 assert!(parsed.args.keys().all(|a| a.name != "tool"));
3347 assert!(parsed
3348 .errors
3349 .iter()
3350 .any(|e| matches!(e, UsageErr::MissingArg(name) if name == "tool")));
3351 }
3352
3353 #[test]
3354 fn test_double_dash_gap_applies_defaults() {
3355 let spec = spec_with_args([
3357 SpecArg::builder()
3358 .name("tool")
3359 .var(true)
3360 .required(false)
3361 .default_value("node@20")
3362 .build(),
3363 required_arg("command"),
3364 ]);
3365
3366 let parsed = parse(&spec, &input(&["test", "--", "ls"])).unwrap();
3367
3368 assert_eq!(arg_value(&parsed, "command"), "ls");
3369 assert_eq!(arg_value(&parsed, "tool"), "node@20");
3370 }
3371
3372 fn spec_with_restart_token_and_required_arg() -> Spec {
3373 let run_cmd = SpecCommand::builder()
3374 .name("run")
3375 .arg(SpecArg::builder().name("task").build())
3376 .arg(required_arg("run_args"))
3377 .restart_token(":::".to_string())
3378 .build();
3379 let mut cmd = SpecCommand::builder().name("test").build();
3380 cmd.subcommands.insert("run".to_string(), run_cmd);
3381 Spec {
3382 name: "test".to_string(),
3383 bin: "test".to_string(),
3384 cmd,
3385 ..Default::default()
3386 }
3387 }
3388
3389 #[test]
3390 fn test_double_dash_required_restart_token_resets_separator() {
3391 let spec = spec_with_restart_token_and_required_arg();
3393
3394 let parsed = parse_partial(
3395 &spec,
3396 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "b"]),
3397 )
3398 .unwrap();
3399
3400 assert_eq!(arg_value(&parsed, "task"), "task2");
3401 assert!(parsed.args.keys().all(|a| a.name != "run_args"));
3402 assert_eq!(
3404 parsed
3405 .errors
3406 .iter()
3407 .filter(|e| matches!(e, UsageErr::ArgRequiresDoubleDash(_)))
3408 .count(),
3409 1
3410 );
3411 }
3412
3413 #[test]
3414 fn test_double_dash_required_restart_token_accepts_new_separator() {
3415 let spec = spec_with_restart_token_and_required_arg();
3416
3417 let parsed = parse(
3418 &spec,
3419 &input(&["test", "run", "task1", "--", "a", ":::", "task2", "--", "c"]),
3420 )
3421 .unwrap();
3422
3423 assert_eq!(arg_value(&parsed, "task"), "task2");
3424 assert_eq!(arg_value(&parsed, "run_args"), "c");
3425 }
3426
3427 #[test]
3428 fn test_double_dash_preserve_is_not_a_separator() {
3429 let spec = spec_with_args([
3432 SpecArg::builder()
3433 .name("kept")
3434 .var(true)
3435 .var_max(1)
3436 .required(false)
3437 .double_dash(SpecDoubleDashChoices::Preserve)
3438 .build(),
3439 required_arg("rest"),
3440 ]);
3441
3442 let parsed = parse_partial(&spec, &input(&["test", "--", "x"])).unwrap();
3443
3444 assert_eq!(arg_value(&parsed, "kept"), "--");
3445 assert!(parsed.args.keys().all(|a| a.name != "rest"));
3446 assert!(!parsed.double_dash_seen);
3447 assert_eq!(parsed.errors.len(), 1);
3448 }
3449
3450 #[test]
3451 fn test_double_dash_required_does_not_bail_in_parse_partial() {
3452 let spec = spec_with_args([required_arg("file")]);
3454
3455 assert!(parse_partial(&spec, &input(&["test", "x"])).is_ok());
3456 assert!(parse(&spec, &input(&["test", "x"])).is_err());
3457 }
3458
3459 #[test]
3460 fn test_double_dash_without_required_arg_does_not_move_cursor() {
3461 let spec = spec_with_args([
3463 SpecArg::builder().name("first").required(false).build(),
3464 SpecArg::builder().name("second").required(false).build(),
3465 ]);
3466
3467 let parsed = parse(&spec, &input(&["test", "--", "a", "b"])).unwrap();
3468
3469 assert_eq!(arg_value(&parsed, "first"), "a");
3470 assert_eq!(arg_value(&parsed, "second"), "b");
3471 assert!(parsed.next_arg.is_none());
3472 }
3473
3474 #[test]
3475 fn test_parser_with_custom_env_for_required_arg() {
3476 let spec = spec_with_arg(
3477 SpecArg::builder()
3478 .name("name")
3479 .env("NAME")
3480 .required(true)
3481 .build(),
3482 );
3483 std::env::remove_var("NAME");
3484
3485 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
3486 .expect("parse should succeed with custom env");
3487 assert_eq!(parsed.args.len(), 1);
3488 assert_eq!(first_string_value(&parsed), "john");
3489 }
3490
3491 #[test]
3492 fn test_parser_with_custom_env_for_required_flag() {
3493 let spec = spec_with_flag(
3494 SpecFlag::builder()
3495 .long("name")
3496 .env("NAME")
3497 .required(true)
3498 .arg(SpecArg::builder().name("name").build())
3499 .build(),
3500 );
3501 std::env::remove_var("NAME");
3502
3503 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
3504 .expect("parse should succeed with custom env");
3505 assert_eq!(parsed.flags.len(), 1);
3506 assert_eq!(first_string_value(&parsed), "jane");
3507 }
3508
3509 #[test]
3510 fn test_parser_with_custom_env_still_fails_when_missing() {
3511 let spec = spec_with_arg(
3512 SpecArg::builder()
3513 .name("name")
3514 .env("NAME")
3515 .required(true)
3516 .build(),
3517 );
3518 std::env::remove_var("NAME");
3519 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
3520 }
3521
3522 #[test]
3523 fn test_parser_does_not_treat_env_choice_value_as_help() {
3524 let spec = spec_with_arg(
3525 SpecArg::builder()
3526 .name("env")
3527 .env("CURRENT_ENV")
3528 .choices(["dev", "staging"])
3529 .required(false)
3530 .build(),
3531 );
3532
3533 assert_parse_err(
3534 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
3535 "Invalid choice for arg env: --help, expected one of dev, staging",
3536 );
3537 }
3538
3539 #[test]
3540 fn test_parser_does_not_treat_default_choice_value_as_help() {
3541 let spec = spec_with_flag(
3542 SpecFlag::builder()
3543 .long("env")
3544 .arg(
3545 SpecArg::builder()
3546 .name("env")
3547 .choices(["dev", "staging"])
3548 .build(),
3549 )
3550 .default_value("--help")
3551 .build(),
3552 );
3553
3554 assert_parse_err(
3555 parse_with_env(&spec, &["test"], &[]),
3556 "Invalid choice for option env: --help, expected one of dev, staging",
3557 );
3558 }
3559
3560 #[cfg(feature = "unstable_choices_env")]
3561 #[test]
3562 fn test_parser_arg_choices_from_custom_env() {
3563 let spec = spec_arg_choices_env("DEPLOY_ENVS");
3564
3565 let parsed =
3566 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
3567 assert_eq!(first_string_value(&parsed), "bar");
3568
3569 assert_parse_err(
3570 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
3571 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
3572 );
3573 assert_parse_err(
3574 parse_with_env(&spec, &["test", "prod"], &[]),
3575 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
3576 );
3577 }
3578
3579 #[cfg(feature = "unstable_choices_env")]
3580 #[test]
3581 fn test_parser_validates_flag_choices_from_custom_env() {
3582 let spec = spec_flag_choices_env("DEPLOY_ENVS");
3583 let parsed = parse_with_env(
3584 &spec,
3585 &["test", "--env", "baz"],
3586 &[("DEPLOY_ENVS", "foo,bar baz")],
3587 )
3588 .unwrap();
3589 assert_eq!(first_string_value(&parsed), "baz");
3590 }
3591
3592 #[cfg(feature = "unstable_choices_env")]
3593 #[test]
3594 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
3595 let arg_env_spec = spec_with_arg(
3596 SpecArg::builder()
3597 .name("env")
3598 .env("CURRENT_ENV")
3599 .choices_env("DEPLOY_ENVS")
3600 .build(),
3601 );
3602 assert_parse_err(
3603 parse_with_env(
3604 &arg_env_spec,
3605 &["test"],
3606 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
3607 ),
3608 "Invalid choice for arg env: prod, expected one of dev, staging",
3609 );
3610
3611 let flag_default_spec = spec_with_flag(
3612 SpecFlag::builder()
3613 .long("env")
3614 .arg(
3615 SpecArg::builder()
3616 .name("env")
3617 .choices_env("DEPLOY_ENVS")
3618 .build(),
3619 )
3620 .default_value("prod")
3621 .build(),
3622 );
3623 assert_parse_err(
3624 parse_with_env(
3625 &flag_default_spec,
3626 &["test"],
3627 &[("DEPLOY_ENVS", "dev,staging")],
3628 ),
3629 "Invalid choice for option env: prod, expected one of dev, staging",
3630 );
3631 }
3632
3633 #[test]
3634 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
3635 let spec: Spec = r#"
3636 flag "-v --verbose" var=#true
3637 arg "[database]" default="myapp_dev"
3638 arg "[args...]"
3639 "#
3640 .parse()
3641 .unwrap();
3642 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3643 .into_iter()
3644 .map(String::from)
3645 .collect();
3646 let parsed = parse(&spec, &input).unwrap();
3647 let env = parsed.as_env();
3648 assert_eq!(env.get("usage_database").unwrap(), "mydb");
3649 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
3650 }
3651
3652 #[test]
3653 fn test_variadic_arg_captures_unknown_flags() {
3654 let cmd = SpecCommand::builder()
3655 .name("test")
3656 .flag(SpecFlag::builder().short('v').long("verbose").build())
3657 .arg(SpecArg::builder().name("database").required(false).build())
3658 .arg(
3659 SpecArg::builder()
3660 .name("args")
3661 .required(false)
3662 .var(true)
3663 .build(),
3664 )
3665 .build();
3666 let spec = Spec {
3667 name: "test".to_string(),
3668 bin: "test".to_string(),
3669 cmd,
3670 ..Default::default()
3671 };
3672
3673 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3675 .into_iter()
3676 .map(String::from)
3677 .collect();
3678 let parsed = parse(&spec, &input).unwrap();
3679 assert_eq!(parsed.args.len(), 2);
3680 let args_val = parsed
3681 .args
3682 .iter()
3683 .find(|(a, _)| a.name == "args")
3684 .unwrap()
3685 .1;
3686 match args_val {
3687 ParseValue::MultiString(v) => {
3688 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3689 }
3690 _ => panic!("Expected MultiString, got {:?}", args_val),
3691 }
3692 }
3693
3694 #[test]
3695 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
3696 let cmd = SpecCommand::builder()
3697 .name("test")
3698 .flag(SpecFlag::builder().short('v').long("verbose").build())
3699 .arg(SpecArg::builder().name("database").required(false).build())
3700 .arg(
3701 SpecArg::builder()
3702 .name("args")
3703 .required(false)
3704 .var(true)
3705 .build(),
3706 )
3707 .build();
3708 let spec = Spec {
3709 name: "test".to_string(),
3710 bin: "test".to_string(),
3711 cmd,
3712 ..Default::default()
3713 };
3714
3715 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
3717 .into_iter()
3718 .map(String::from)
3719 .collect();
3720 let parsed = parse(&spec, &input).unwrap();
3721 assert_eq!(parsed.args.len(), 2);
3722 let args_val = parsed
3723 .args
3724 .iter()
3725 .find(|(a, _)| a.name == "args")
3726 .unwrap()
3727 .1;
3728 match args_val {
3729 ParseValue::MultiString(v) => {
3730 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3731 }
3732 _ => panic!("Expected MultiString, got {:?}", args_val),
3733 }
3734 }
3735
3736 #[test]
3737 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
3738 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
3741
3742 let input: Vec<String> = vec!["test", "--option=3"]
3745 .into_iter()
3746 .map(String::from)
3747 .collect();
3748 let parsed = parse(&spec, &input).unwrap();
3749 let env = parsed.as_env();
3750 assert_eq!(
3751 env.get("usage_other_args").map(String::as_str),
3752 Some("'--option=3'"),
3753 "expected a single --option=3 token, got {:?}",
3754 env.get("usage_other_args"),
3755 );
3756
3757 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
3759 .into_iter()
3760 .map(String::from)
3761 .collect();
3762 let parsed2 = parse(&spec, &input2).unwrap();
3763 let env2 = parsed2.as_env();
3764 assert_eq!(
3765 env2.get("usage_other_args").map(String::as_str),
3766 Some("'--foo=bar' '--baz=qux'"),
3767 "expected two intact tokens, got {:?}",
3768 env2.get("usage_other_args"),
3769 );
3770
3771 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
3773 .into_iter()
3774 .map(String::from)
3775 .collect();
3776 let parsed3 = parse(&spec, &input3).unwrap();
3777 let env3 = parsed3.as_env();
3778 assert_eq!(
3779 env3.get("usage_other_args").map(String::as_str),
3780 Some("positional1 '--option=3' positional2"),
3781 "expected positional args and intact flag token, got {:?}",
3782 env3.get("usage_other_args"),
3783 );
3784 }
3785
3786 #[test]
3787 fn test_allow_hyphen_values_consumes_short_flag_collision() {
3788 let spec = r#"
3789flag "-d --working-dir <DIR>"
3790flag "-a --args <ARGS>" allow_hyphen_values=#true
3791"#
3792 .parse::<Spec>()
3793 .unwrap();
3794
3795 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3796
3797 assert_eq!(parsed.flags.len(), 1);
3798 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3799 }
3800
3801 #[test]
3802 fn test_allow_hyphen_values_consumes_embedded_long_value() {
3803 let spec = r#"
3804flag "-d --working-dir <DIR>"
3805flag "-a --args <ARGS>" allow_hyphen_values=#true
3806"#
3807 .parse::<Spec>()
3808 .unwrap();
3809
3810 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
3811
3812 assert_eq!(parsed.flags.len(), 1);
3813 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3814 }
3815
3816 #[test]
3817 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
3818 let spec = r#"
3819flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
3820"#
3821 .parse::<Spec>()
3822 .unwrap();
3823
3824 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
3825
3826 let flag = parsed
3827 .flags
3828 .keys()
3829 .find(|flag| flag.name == "args")
3830 .expect("expected args flag");
3831 let value = parsed.flags.get(flag).expect("expected args value");
3832 match value {
3833 ParseValue::MultiString(values) => {
3834 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
3835 }
3836 _ => panic!("expected MultiString, got {value:?}"),
3837 }
3838 }
3839
3840 #[test]
3841 fn test_hyphen_values_still_default_to_short_flag_parsing() {
3842 let spec = r#"
3843flag "-d --working-dir <DIR>"
3844flag "-a --args <ARGS>"
3845"#
3846 .parse::<Spec>()
3847 .unwrap();
3848
3849 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3850
3851 assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
3852 }
3853
3854 mod available_flags {
3857 use super::*;
3858
3859 fn spec() -> Spec {
3860 r#"
3861bin "test"
3862flag "-v --verbose" global=#true
3863flag "--raw" global=#true effect="write"
3864flag "--local-only"
3865cmd "run" {
3866 flag "-r --raw"
3867 flag "-w --watch"
3868 cmd "once"
3869}
3870"#
3871 .parse::<Spec>()
3872 .unwrap()
3873 }
3874
3875 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
3876 let mut chain = vec![&spec.cmd];
3877 for segment in path {
3878 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
3879 }
3880 chain
3881 }
3882
3883 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
3884 let mut names: Vec<_> = available_flags(&chain(spec, path))
3885 .iter()
3886 .map(|f| f.name.clone())
3887 .collect();
3888 names.sort();
3889 names
3890 }
3891
3892 #[test]
3893 fn an_empty_chain_yields_nothing() {
3894 assert!(available_flags(&[]).is_empty());
3895 }
3896
3897 #[test]
3898 fn the_root_gets_its_own_flags() {
3899 let spec = spec();
3900 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
3901 }
3902
3903 #[test]
3904 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
3905 let spec = spec();
3906 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
3907 }
3908
3909 #[test]
3910 fn a_re_declared_global_is_listed_once() {
3911 let spec = r#"
3914bin "test"
3915flag "-y --yes" global=#true effect="write"
3916cmd "rm" {
3917 flag "-y --yes"
3918}
3919"#
3920 .parse::<Spec>()
3921 .unwrap();
3922 let flags = available_flags(&chain(&spec, &["rm"]));
3923 assert_eq!(flags.len(), 1, "{flags:?}");
3924 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
3925 }
3926
3927 #[test]
3928 fn a_re_declared_global_keeps_the_globals_declaration() {
3929 let spec = spec();
3933 let flags = available_flags(&chain(&spec, &["run"]));
3934 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
3935 assert!(raw.global);
3936 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
3937 assert_eq!(raw.short, ['r']);
3938 }
3939
3940 #[test]
3941 fn it_matches_what_a_parse_accepts() {
3942 let spec = spec();
3945 for path in [vec![], vec!["run"], vec!["run", "once"]] {
3946 let argv = std::iter::once("test".to_string())
3947 .chain(path.iter().map(|s| s.to_string()))
3948 .collect::<Vec<_>>();
3949 let parsed = parse_partial(&spec, &argv).unwrap();
3950
3951 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
3952 .map(|f| f.name.clone())
3953 .collect();
3954 from_parse.sort();
3955 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
3956 }
3957 }
3958 }
3959}