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}
256
257impl ParseOutput {
258 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
267 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
268 return self.available_flags.clone();
269 };
270 let start = match boundary.checked_sub(1) {
274 Some(prev) if self.cmds[prev].flags_from_mount => prev,
275 _ => boundary,
276 };
277 let mut offered = gather_flags(&self.cmds[start]);
281 for cmd in &self.cmds[start + 1..] {
282 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
283 }
284 offered
285 }
286}
287
288#[derive(Debug, EnumTryAs, Clone)]
289pub enum ParseValue {
290 Bool(bool),
291 String(String),
292 MultiBool(Vec<bool>),
293 MultiString(Vec<String>),
294}
295
296#[non_exhaustive]
316pub struct Parser<'a> {
317 spec: &'a Spec,
318 env: Option<HashMap<String, String>>,
319}
320
321impl<'a> Parser<'a> {
322 pub fn new(spec: &'a Spec) -> Self {
324 Self { spec, env: None }
325 }
326
327 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
332 self.env = Some(env);
333 self
334 }
335
336 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
340 let custom_env = self.env.as_ref();
341 let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
342 trace!("{out:?}");
343
344 let get_env = |key: &str| -> Option<String> {
345 if let Some(env_map) = custom_env {
346 env_map.get(key).cloned()
347 } else {
348 std::env::var(key).ok()
349 }
350 };
351
352 for arg in out.cmd.args.iter().skip(out.args.len()) {
354 if let Some(env_var) = arg.env.as_ref() {
355 if let Some(env_value) = get_env(env_var) {
356 validate_choice_value(
357 ChoiceTarget::arg(arg),
358 &env_value,
359 arg.choices.as_ref(),
360 custom_env,
361 )?;
362 out.args
363 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
364 continue;
365 }
366 }
367 if !arg.default.is_empty() {
368 if arg.var {
370 validate_choice_values(
371 ChoiceTarget::arg(arg),
372 &arg.default,
373 arg.choices.as_ref(),
374 custom_env,
375 )?;
376 out.args.insert(
378 Arc::new(arg.clone()),
379 ParseValue::MultiString(arg.default.clone()),
380 );
381 } else {
382 validate_choice_value(
383 ChoiceTarget::arg(arg),
384 &arg.default[0],
385 arg.choices.as_ref(),
386 custom_env,
387 )?;
388 out.args.insert(
390 Arc::new(arg.clone()),
391 ParseValue::String(arg.default[0].clone()),
392 );
393 }
394 }
395 }
396
397 for flag in out.available_flags.values() {
399 if out.flags.contains_key(flag) {
400 continue;
401 }
402 if let Some(env_var) = flag.env.as_ref() {
403 if let Some(env_value) = get_env(env_var) {
404 if let Some(arg) = flag.arg.as_ref() {
405 validate_choice_value(
406 ChoiceTarget::option(flag),
407 &env_value,
408 arg.choices.as_ref(),
409 custom_env,
410 )?;
411 out.flags
412 .insert(Arc::clone(flag), ParseValue::String(env_value));
413 } else {
414 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
416 out.flags
417 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
418 }
419 continue;
420 }
421 }
422 if !flag.default.is_empty() {
424 if flag.var {
426 if let Some(arg) = flag.arg.as_ref() {
428 validate_choice_values(
429 ChoiceTarget::option(flag),
430 &flag.default,
431 arg.choices.as_ref(),
432 custom_env,
433 )?;
434 out.flags.insert(
435 Arc::clone(flag),
436 ParseValue::MultiString(flag.default.clone()),
437 );
438 } else {
439 let bools: Vec<bool> = flag
441 .default
442 .iter()
443 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
444 .collect();
445 out.flags
446 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
447 }
448 } else {
449 if let Some(arg) = flag.arg.as_ref() {
451 validate_choice_value(
452 ChoiceTarget::option(flag),
453 &flag.default[0],
454 arg.choices.as_ref(),
455 custom_env,
456 )?;
457 out.flags.insert(
458 Arc::clone(flag),
459 ParseValue::String(flag.default[0].clone()),
460 );
461 } else {
462 let is_true =
464 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
465 out.flags
466 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
467 }
468 }
469 }
470 if let Some(arg) = flag.arg.as_ref() {
472 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
473 if flag.var {
474 validate_choice_values(
475 ChoiceTarget::option(flag),
476 &arg.default,
477 arg.choices.as_ref(),
478 custom_env,
479 )?;
480 out.flags.insert(
481 Arc::clone(flag),
482 ParseValue::MultiString(arg.default.clone()),
483 );
484 } else {
485 validate_choice_value(
486 ChoiceTarget::option(flag),
487 &arg.default[0],
488 arg.choices.as_ref(),
489 custom_env,
490 )?;
491 out.flags
492 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
493 }
494 }
495 }
496 }
497 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
498 bail!("{err}");
499 }
500 if !out.errors.is_empty() {
501 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
502 }
503 Ok(out)
504 }
505}
506
507#[must_use = "parsing result should be used"]
514pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
515 Parser::new(spec).parse(input)
516}
517
518#[must_use = "parsing result should be used"]
522pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
523 parse_partial_with_env(spec, input, None)
524}
525
526fn parse_partial_with_env(
528 spec: &Spec,
529 input: &[String],
530 custom_env: Option<&HashMap<String, String>>,
531) -> Result<ParseOutput, miette::Error> {
532 trace!("parse_partial: {input:?}");
533 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
534 input.pop_front();
535
536 let mut out = ParseOutput {
537 cmd: spec.cmd.clone(),
538 cmds: vec![spec.cmd.clone()],
539 args: IndexMap::new(),
540 flags: IndexMap::new(),
541 available_flags: gather_flags(&spec.cmd),
542 flag_awaiting_value: vec![],
543 errors: vec![],
544 };
545
546 let mut prefix_words: Vec<String> = vec![];
559 let mut prefix_bindings: VecDeque<Option<Arc<SpecFlag>>> = VecDeque::new();
567 let mut idx = 0;
568 let mut used_default_subcommand = false;
571
572 while idx < input.len() {
573 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
574 let mut subcommand = subcommand.clone();
575 subcommand.mount(&prefix_words)?;
577 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
580 merge_subcommand_flags(
581 &mut out.available_flags,
582 gather_flags(&subcommand),
583 crossing_mount,
584 );
585 input.remove(idx);
587 out.cmds.push(subcommand.clone());
588 out.cmd = subcommand.clone();
589 prefix_words.clear();
590 } else if input[idx].starts_with('-') {
593 let word = input[idx].clone();
595 let flag_key = get_flag_key(&word);
596
597 if let Some(f) = out.available_flags.get(flag_key).cloned() {
598 prefix_bindings.push_back(Some(Arc::clone(&f)));
606 if f.global {
607 prefix_words.push(word.clone());
608 }
609 idx += 1;
610
611 if f.arg.is_some()
614 && !word.contains('=')
615 && idx < input.len()
616 && !input[idx].starts_with('-')
617 {
618 if f.global {
619 prefix_words.push(input[idx].clone());
620 }
621 prefix_bindings.push_back(None);
622 idx += 1;
623 }
624 } else {
625 break;
628 }
629 } else {
630 if !used_default_subcommand {
633 if let Some(default_name) = &spec.default_subcommand {
634 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
635 let mut subcommand = subcommand.clone();
636 subcommand.mount(&prefix_words)?;
638 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
639 merge_subcommand_flags(
640 &mut out.available_flags,
641 gather_flags(&subcommand),
642 crossing_mount,
643 );
644 out.cmds.push(subcommand.clone());
645 out.cmd = subcommand.clone();
646 prefix_words.clear();
647 used_default_subcommand = true;
648 continue;
653 }
654 }
655 }
656 break;
658 }
659 }
660
661 let mut next_arg = out.cmd.args.first();
666 let mut enable_flags = true;
667 let mut grouped_flag = false;
668
669 while !input.is_empty() {
670 let mut w = input.pop_front().unwrap();
671 let binding = prefix_bindings.pop_front().flatten();
674
675 if let Some(ref restart_token) = out.cmd.restart_token {
678 if w == *restart_token {
679 out.args.clear();
681 next_arg = out.cmd.args.first();
682 out.flag_awaiting_value.clear(); enable_flags = true; continue;
686 }
687 }
688
689 if w == "--" {
690 enable_flags = false;
692
693 let should_preserve = next_arg
696 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
697 .unwrap_or(false);
698
699 if should_preserve {
700 } else {
702 continue;
704 }
705 }
706
707 if w.starts_with('-')
708 && out
709 .flag_awaiting_value
710 .last()
711 .is_some_and(|flag| flag.allow_hyphen_values())
712 {
713 let should_return = drain_pending_flag_values(
714 spec,
715 &out.cmd,
716 &mut out.errors,
717 &mut out.flags,
718 &mut out.flag_awaiting_value,
719 &mut w,
720 custom_env,
721 )?;
722 if should_return {
723 return Ok(out);
724 }
725 continue;
726 }
727
728 if enable_flags && w.starts_with("--") {
730 grouped_flag = false;
731 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
732 if let Some(f) = binding.as_ref().or_else(|| out.available_flags.get(word)) {
733 if !val.is_empty() {
737 input.push_front(val.to_string());
738 prefix_bindings.push_front(None);
739 }
740 if f.arg.is_some() {
741 out.flag_awaiting_value.push(Arc::clone(f));
742 } else if f.count {
743 let arr = out
744 .flags
745 .entry(Arc::clone(f))
746 .or_insert_with(|| ParseValue::MultiBool(vec![]))
747 .try_as_multi_bool_mut()
748 .unwrap();
749 arr.push(true);
750 } else {
751 let negate = f.negate.clone().unwrap_or_default();
752 out.flags
753 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
754 }
755 continue;
756 }
757 if is_help_arg(spec, &w) {
758 out.errors
759 .push(render_help_err(spec, &out.cmd, w.len() > 2));
760 return Ok(out);
761 }
762 }
763
764 if enable_flags && w.starts_with('-') && w.len() > 1 {
766 let short = w.chars().nth(1).unwrap();
767 if let Some(f) = binding
768 .as_ref()
769 .or_else(|| out.available_flags.get(&format!("-{short}")))
770 {
771 if w.len() > 2 {
772 input.push_front(format!("-{}", &w[2..]));
773 prefix_bindings.push_front(None);
774 grouped_flag = true;
775 }
776 if f.arg.is_some() {
777 out.flag_awaiting_value.push(Arc::clone(f));
778 } else if f.count {
779 let arr = out
780 .flags
781 .entry(Arc::clone(f))
782 .or_insert_with(|| ParseValue::MultiBool(vec![]))
783 .try_as_multi_bool_mut()
784 .unwrap();
785 arr.push(true);
786 } else {
787 let negate = f.negate.clone().unwrap_or_default();
788 out.flags
789 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
790 }
791 continue;
792 }
793 if is_help_arg(spec, &w) {
794 out.errors
795 .push(render_help_err(spec, &out.cmd, w.len() > 2));
796 return Ok(out);
797 }
798 if grouped_flag {
799 grouped_flag = false;
800 w.remove(0);
801 }
802 }
803
804 if !out.flag_awaiting_value.is_empty() {
805 let should_return = drain_pending_flag_values(
806 spec,
807 &out.cmd,
808 &mut out.errors,
809 &mut out.flags,
810 &mut out.flag_awaiting_value,
811 &mut w,
812 custom_env,
813 )?;
814 if should_return {
815 return Ok(out);
816 }
817 continue;
818 }
819
820 if let Some(arg) = next_arg {
821 if validate_choices(
822 spec,
823 &out.cmd,
824 &mut out.errors,
825 ChoiceTarget::arg(arg),
826 &w,
827 arg.choices.as_ref(),
828 custom_env,
829 )? {
830 return Ok(out);
831 }
832 if arg.var {
833 let arr = out
834 .args
835 .entry(Arc::new(arg.clone()))
836 .or_insert_with(|| ParseValue::MultiString(vec![]))
837 .try_as_multi_string_mut()
838 .unwrap();
839 arr.push(w);
840 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
841 next_arg = out.cmd.args.get(out.args.len());
842 }
843 } else {
844 out.args
845 .insert(Arc::new(arg.clone()), ParseValue::String(w));
846 next_arg = out.cmd.args.get(out.args.len());
847 }
848 continue;
849 }
850 if is_help_arg(spec, &w) {
851 out.errors
852 .push(render_help_err(spec, &out.cmd, w.len() > 2));
853 return Ok(out);
854 }
855 bail!("unexpected word: {w}");
856 }
857
858 for arg in out.cmd.args.iter().skip(out.args.len()) {
859 if arg.required && arg.default.is_empty() {
860 let has_env = arg.env.as_ref().is_some_and(|e| {
862 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
863 || std::env::var(e).is_ok()
864 });
865 if !has_env {
866 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
867 }
868 }
869 }
870
871 for flag in unique_flags(out.available_flags.values()) {
872 if out.flags.contains_key(flag) {
873 continue;
874 }
875 let has_default =
876 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
877 let has_env = flag.env.as_ref().is_some_and(|e| {
879 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
880 });
881 if flag.required && !has_default && !has_env {
882 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
883 }
884 }
885
886 for (arg, value) in &out.args {
888 if arg.var {
889 if let ParseValue::MultiString(values) = value {
890 if let Some(min) = arg.var_min {
891 if values.len() < min {
892 out.errors.push(UsageErr::VarArgTooFew {
893 name: arg.name.clone(),
894 min,
895 got: values.len(),
896 });
897 }
898 }
899 if let Some(max) = arg.var_max {
900 if values.len() > max {
901 out.errors.push(UsageErr::VarArgTooMany {
902 name: arg.name.clone(),
903 max,
904 got: values.len(),
905 });
906 }
907 }
908 }
909 }
910 }
911
912 for (flag, value) in &out.flags {
914 if flag.var {
915 let count = match value {
916 ParseValue::MultiString(values) => values.len(),
917 ParseValue::MultiBool(values) => values.len(),
918 _ => continue,
919 };
920 if let Some(min) = flag.var_min {
921 if count < min {
922 out.errors.push(UsageErr::VarFlagTooFew {
923 name: flag.name.clone(),
924 min,
925 got: count,
926 });
927 }
928 }
929 if let Some(max) = flag.var_max {
930 if count > max {
931 out.errors.push(UsageErr::VarFlagTooMany {
932 name: flag.name.clone(),
933 max,
934 got: count,
935 });
936 }
937 }
938 }
939 }
940
941 Ok(out)
942}
943
944#[cfg(feature = "docs")]
945fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
946 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
947}
948
949#[cfg(not(feature = "docs"))]
950fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
951 UsageErr::Help("help".to_string())
952}
953
954#[derive(Copy, Clone)]
955struct ChoiceTarget<'a> {
956 kind: &'a str,
957 name: &'a str,
958}
959
960impl<'a> ChoiceTarget<'a> {
961 fn arg(arg: &'a SpecArg) -> Self {
962 Self {
963 kind: "arg",
964 name: &arg.name,
965 }
966 }
967
968 fn option(flag: &'a SpecFlag) -> Self {
969 Self {
970 kind: "option",
971 name: &flag.name,
972 }
973 }
974}
975
976fn drain_pending_flag_values(
977 spec: &Spec,
978 cmd: &SpecCommand,
979 errors: &mut Vec<UsageErr>,
980 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
981 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
982 word: &mut String,
983 custom_env: Option<&HashMap<String, String>>,
984) -> miette::Result<bool> {
985 while let Some(flag) = flag_awaiting_value.pop() {
986 let arg = flag.arg.as_ref().unwrap();
987 if validate_choices(
988 spec,
989 cmd,
990 errors,
991 ChoiceTarget::option(&flag),
992 word,
993 arg.choices.as_ref(),
994 custom_env,
995 )? {
996 return Ok(true);
997 }
998 let value = std::mem::take(word);
999 if flag.var {
1000 let arr = flags
1001 .entry(flag)
1002 .or_insert_with(|| ParseValue::MultiString(vec![]))
1003 .try_as_multi_string_mut()
1004 .unwrap();
1005 arr.push(value);
1006 } else {
1007 flags.insert(flag, ParseValue::String(value));
1008 }
1009 }
1010 Ok(false)
1011}
1012
1013fn choice_error(
1014 target: ChoiceTarget<'_>,
1015 value: &str,
1016 choices: Option<&SpecChoices>,
1017 custom_env: Option<&HashMap<String, String>>,
1018) -> Option<String> {
1019 let choices = choices?;
1020 let values = choices.values_with_env(custom_env);
1021 if values.iter().any(|choice| choice == value) {
1022 return None;
1023 }
1024 if let Some(env) = choices.env() {
1025 if values.is_empty() {
1026 return Some(format!(
1027 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
1028 target.kind, target.name,
1029 ));
1030 }
1031 }
1032 Some(format!(
1033 "Invalid choice for {} {}: {value}, expected one of {}",
1034 target.kind,
1035 target.name,
1036 values.join(", ")
1037 ))
1038}
1039
1040fn validate_choices(
1041 spec: &Spec,
1042 cmd: &SpecCommand,
1043 errors: &mut Vec<UsageErr>,
1044 target: ChoiceTarget<'_>,
1045 value: &str,
1046 choices: Option<&SpecChoices>,
1047 custom_env: Option<&HashMap<String, String>>,
1048) -> miette::Result<bool> {
1049 if is_help_arg(spec, value)
1050 && choices.is_some_and(|choices| {
1051 !choices
1052 .values_with_env(custom_env)
1053 .iter()
1054 .any(|choice| choice == value)
1055 })
1056 {
1057 errors.push(render_help_err(spec, cmd, value.len() > 2));
1058 return Ok(true);
1059 }
1060
1061 if let Some(err) = choice_error(target, value, choices, custom_env) {
1062 bail!("{err}");
1063 }
1064 Ok(false)
1065}
1066
1067fn validate_choice_value(
1068 target: ChoiceTarget<'_>,
1069 value: &str,
1070 choices: Option<&SpecChoices>,
1071 custom_env: Option<&HashMap<String, String>>,
1072) -> miette::Result<()> {
1073 if let Some(err) = choice_error(target, value, choices, custom_env) {
1074 bail!("{err}");
1075 }
1076 Ok(())
1077}
1078
1079fn validate_choice_values(
1080 target: ChoiceTarget<'_>,
1081 values: &[String],
1082 choices: Option<&SpecChoices>,
1083 custom_env: Option<&HashMap<String, String>>,
1084) -> miette::Result<()> {
1085 for value in values {
1086 validate_choice_value(target, value, choices, custom_env)?;
1087 }
1088 Ok(())
1089}
1090
1091fn is_help_arg(spec: &Spec, w: &str) -> bool {
1092 spec.disable_help != Some(true)
1093 && (w == "--help"
1094 || w == "-h"
1095 || w == "-?"
1096 || (spec.cmd.subcommands.is_empty() && w == "help"))
1097}
1098
1099impl ParseOutput {
1100 pub fn as_env(&self) -> BTreeMap<String, String> {
1101 let mut env = BTreeMap::new();
1102 for (flag, val) in &self.flags {
1103 let key = format!("usage_{}", flag.name.to_snake_case());
1104 let val = match val {
1105 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1106 ParseValue::String(s) => s.clone(),
1107 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
1108 ParseValue::MultiString(s) => shell_words::join(s),
1109 };
1110 env.insert(key, val);
1111 }
1112 for (arg, val) in &self.args {
1113 let key = format!("usage_{}", arg.name.to_snake_case());
1114 env.insert(key, val.to_string());
1115 }
1116 env
1117 }
1118}
1119
1120impl Display for ParseValue {
1121 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1122 match self {
1123 ParseValue::Bool(b) => write!(f, "{b}"),
1124 ParseValue::String(s) => write!(f, "{s}"),
1125 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
1126 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
1127 }
1128 }
1129}
1130
1131impl Debug for ParseOutput {
1132 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1133 f.debug_struct("ParseOutput")
1134 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
1135 .field(
1136 "args",
1137 &self
1138 .args
1139 .iter()
1140 .map(|(a, w)| format!("{}: {w}", a.name))
1141 .collect_vec(),
1142 )
1143 .field(
1144 "available_flags",
1145 &self
1146 .available_flags
1147 .iter()
1148 .map(|(f, w)| format!("{f}: {w}"))
1149 .collect_vec(),
1150 )
1151 .field(
1152 "flags",
1153 &self
1154 .flags
1155 .iter()
1156 .map(|(f, w)| format!("{}: {w}", f.name))
1157 .collect_vec(),
1158 )
1159 .field("flag_awaiting_value", &self.flag_awaiting_value)
1160 .field("errors", &self.errors)
1161 .finish()
1162 }
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167 use super::*;
1168
1169 fn input(words: &[&str]) -> Vec<String> {
1170 words.iter().map(|word| (*word).to_string()).collect()
1171 }
1172
1173 fn spec_with_arg(arg: SpecArg) -> Spec {
1174 let cmd = SpecCommand::builder().name("test").arg(arg).build();
1175 Spec {
1176 name: "test".to_string(),
1177 bin: "test".to_string(),
1178 cmd,
1179 ..Default::default()
1180 }
1181 }
1182
1183 fn spec_with_flag(flag: SpecFlag) -> Spec {
1184 let cmd = SpecCommand::builder().name("test").flag(flag).build();
1185 Spec {
1186 name: "test".to_string(),
1187 bin: "test".to_string(),
1188 cmd,
1189 ..Default::default()
1190 }
1191 }
1192
1193 fn parse_with_env(
1194 spec: &Spec,
1195 words: &[&str],
1196 env: &[(&str, &str)],
1197 ) -> Result<ParseOutput, miette::Error> {
1198 let env = env
1199 .iter()
1200 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1201 .collect();
1202 Parser::new(spec).with_env(env).parse(&input(words))
1203 }
1204
1205 fn first_string_value(parsed: &ParseOutput) -> &str {
1206 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1207 return value;
1208 }
1209 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1210 return value;
1211 }
1212 panic!("expected first parsed value to be ParseValue::String");
1213 }
1214
1215 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
1216 let flag = parsed
1217 .flags
1218 .keys()
1219 .find(|flag| flag.name == name)
1220 .unwrap_or_else(|| panic!("expected flag {name}"));
1221 let value = parsed
1222 .flags
1223 .get(flag)
1224 .unwrap_or_else(|| panic!("expected value for flag {name}"));
1225 match value {
1226 ParseValue::String(value) => value,
1227 _ => panic!("expected flag {name} to be ParseValue::String"),
1228 }
1229 }
1230
1231 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1232 let err = result.expect_err("expected parser error");
1233 assert_eq!(format!("{err}"), expected);
1234 }
1235
1236 #[cfg(feature = "unstable_choices_env")]
1237 fn spec_arg_choices_env(key: &str) -> Spec {
1238 spec_with_arg(
1239 SpecArg::builder()
1240 .name("env")
1241 .choices_env(key)
1242 .required(false)
1243 .build(),
1244 )
1245 }
1246
1247 #[cfg(feature = "unstable_choices_env")]
1248 fn spec_flag_choices_env(key: &str) -> Spec {
1249 spec_with_flag(
1250 SpecFlag::builder()
1251 .long("env")
1252 .arg(SpecArg::builder().name("env").choices_env(key).build())
1253 .build(),
1254 )
1255 }
1256
1257 #[test]
1258 fn test_parse() {
1259 let cmd = SpecCommand::builder()
1260 .name("test")
1261 .arg(SpecArg::builder().name("arg").build())
1262 .flag(SpecFlag::builder().long("flag").build())
1263 .build();
1264 let spec = Spec {
1265 name: "test".to_string(),
1266 bin: "test".to_string(),
1267 cmd,
1268 ..Default::default()
1269 };
1270 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1271 let parsed = parse(&spec, &input).unwrap();
1272 assert_eq!(parsed.cmds.len(), 1);
1273 assert_eq!(parsed.cmds[0].name, "test");
1274 assert_eq!(parsed.args.len(), 1);
1275 assert_eq!(parsed.flags.len(), 1);
1276 assert_eq!(parsed.available_flags.len(), 1);
1277 }
1278
1279 #[test]
1280 fn test_as_env() {
1281 let cmd = SpecCommand::builder()
1282 .name("test")
1283 .arg(SpecArg::builder().name("arg").build())
1284 .flag(SpecFlag::builder().long("flag").build())
1285 .flag(
1286 SpecFlag::builder()
1287 .long("force")
1288 .negate("--no-force")
1289 .build(),
1290 )
1291 .build();
1292 let spec = Spec {
1293 name: "test".to_string(),
1294 bin: "test".to_string(),
1295 cmd,
1296 ..Default::default()
1297 };
1298 let input = vec![
1299 "test".to_string(),
1300 "--flag".to_string(),
1301 "--no-force".to_string(),
1302 ];
1303 let parsed = parse(&spec, &input).unwrap();
1304 let env = parsed.as_env();
1305 assert_eq!(env.len(), 2);
1306 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1307 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1308 }
1309
1310 #[test]
1311 fn test_arg_env_var() {
1312 let cmd = SpecCommand::builder()
1313 .name("test")
1314 .arg(
1315 SpecArg::builder()
1316 .name("input")
1317 .env("TEST_ARG_INPUT")
1318 .required(true)
1319 .build(),
1320 )
1321 .build();
1322 let spec = Spec {
1323 name: "test".to_string(),
1324 bin: "test".to_string(),
1325 cmd,
1326 ..Default::default()
1327 };
1328
1329 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1331
1332 let input = vec!["test".to_string()];
1333 let parsed = parse(&spec, &input).unwrap();
1334
1335 assert_eq!(parsed.args.len(), 1);
1336 let arg = parsed.args.keys().next().unwrap();
1337 assert_eq!(arg.name, "input");
1338 let value = parsed.args.values().next().unwrap();
1339 assert_eq!(value.to_string(), "test_file.txt");
1340
1341 std::env::remove_var("TEST_ARG_INPUT");
1343 }
1344
1345 #[test]
1346 fn test_flag_env_var_with_arg() {
1347 let cmd = SpecCommand::builder()
1348 .name("test")
1349 .flag(
1350 SpecFlag::builder()
1351 .long("output")
1352 .env("TEST_FLAG_OUTPUT")
1353 .arg(SpecArg::builder().name("file").build())
1354 .build(),
1355 )
1356 .build();
1357 let spec = Spec {
1358 name: "test".to_string(),
1359 bin: "test".to_string(),
1360 cmd,
1361 ..Default::default()
1362 };
1363
1364 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1366
1367 let input = vec!["test".to_string()];
1368 let parsed = parse(&spec, &input).unwrap();
1369
1370 assert_eq!(parsed.flags.len(), 1);
1371 let flag = parsed.flags.keys().next().unwrap();
1372 assert_eq!(flag.name, "output");
1373 let value = parsed.flags.values().next().unwrap();
1374 assert_eq!(value.to_string(), "output.txt");
1375
1376 std::env::remove_var("TEST_FLAG_OUTPUT");
1378 }
1379
1380 #[test]
1381 fn test_flag_env_var_boolean() {
1382 let cmd = SpecCommand::builder()
1383 .name("test")
1384 .flag(
1385 SpecFlag::builder()
1386 .long("verbose")
1387 .env("TEST_FLAG_VERBOSE")
1388 .build(),
1389 )
1390 .build();
1391 let spec = Spec {
1392 name: "test".to_string(),
1393 bin: "test".to_string(),
1394 cmd,
1395 ..Default::default()
1396 };
1397
1398 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1400
1401 let input = vec!["test".to_string()];
1402 let parsed = parse(&spec, &input).unwrap();
1403
1404 assert_eq!(parsed.flags.len(), 1);
1405 let flag = parsed.flags.keys().next().unwrap();
1406 assert_eq!(flag.name, "verbose");
1407 let value = parsed.flags.values().next().unwrap();
1408 assert_eq!(value.to_string(), "true");
1409
1410 std::env::remove_var("TEST_FLAG_VERBOSE");
1412 }
1413
1414 #[test]
1415 fn test_env_var_precedence() {
1416 let cmd = SpecCommand::builder()
1418 .name("test")
1419 .arg(
1420 SpecArg::builder()
1421 .name("input")
1422 .env("TEST_PRECEDENCE_INPUT")
1423 .required(true)
1424 .build(),
1425 )
1426 .build();
1427 let spec = Spec {
1428 name: "test".to_string(),
1429 bin: "test".to_string(),
1430 cmd,
1431 ..Default::default()
1432 };
1433
1434 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1436
1437 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1438 let parsed = parse(&spec, &input).unwrap();
1439
1440 assert_eq!(parsed.args.len(), 1);
1441 let value = parsed.args.values().next().unwrap();
1442 assert_eq!(value.to_string(), "cli_file.txt");
1444
1445 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1447 }
1448
1449 #[test]
1450 fn test_flag_var_true_with_single_default() {
1451 let cmd = SpecCommand::builder()
1453 .name("test")
1454 .flag(
1455 SpecFlag::builder()
1456 .long("foo")
1457 .var(true)
1458 .arg(SpecArg::builder().name("foo").build())
1459 .default_value("bar")
1460 .build(),
1461 )
1462 .build();
1463 let spec = Spec {
1464 name: "test".to_string(),
1465 bin: "test".to_string(),
1466 cmd,
1467 ..Default::default()
1468 };
1469
1470 let input = vec!["test".to_string()];
1472 let parsed = parse(&spec, &input).unwrap();
1473
1474 assert_eq!(parsed.flags.len(), 1);
1475 let flag = parsed.flags.keys().next().unwrap();
1476 assert_eq!(flag.name, "foo");
1477 let value = parsed.flags.values().next().unwrap();
1478 match value {
1480 ParseValue::MultiString(v) => {
1481 assert_eq!(v.len(), 1);
1482 assert_eq!(v[0], "bar");
1483 }
1484 _ => panic!("Expected MultiString, got {:?}", value),
1485 }
1486 }
1487
1488 #[test]
1489 fn test_flag_var_true_with_multiple_defaults() {
1490 let cmd = SpecCommand::builder()
1492 .name("test")
1493 .flag(
1494 SpecFlag::builder()
1495 .long("foo")
1496 .var(true)
1497 .arg(SpecArg::builder().name("foo").build())
1498 .default_values(["xyz", "bar"])
1499 .build(),
1500 )
1501 .build();
1502 let spec = Spec {
1503 name: "test".to_string(),
1504 bin: "test".to_string(),
1505 cmd,
1506 ..Default::default()
1507 };
1508
1509 let input = vec!["test".to_string()];
1511 let parsed = parse(&spec, &input).unwrap();
1512
1513 assert_eq!(parsed.flags.len(), 1);
1514 let value = parsed.flags.values().next().unwrap();
1515 match value {
1517 ParseValue::MultiString(v) => {
1518 assert_eq!(v.len(), 2);
1519 assert_eq!(v[0], "xyz");
1520 assert_eq!(v[1], "bar");
1521 }
1522 _ => panic!("Expected MultiString, got {:?}", value),
1523 }
1524 }
1525
1526 #[test]
1527 fn test_flag_var_false_with_default_remains_string() {
1528 let cmd = SpecCommand::builder()
1530 .name("test")
1531 .flag(
1532 SpecFlag::builder()
1533 .long("foo")
1534 .var(false) .arg(SpecArg::builder().name("foo").build())
1536 .default_value("bar")
1537 .build(),
1538 )
1539 .build();
1540 let spec = Spec {
1541 name: "test".to_string(),
1542 bin: "test".to_string(),
1543 cmd,
1544 ..Default::default()
1545 };
1546
1547 let input = vec!["test".to_string()];
1549 let parsed = parse(&spec, &input).unwrap();
1550
1551 assert_eq!(parsed.flags.len(), 1);
1552 let value = parsed.flags.values().next().unwrap();
1553 match value {
1555 ParseValue::String(s) => {
1556 assert_eq!(s, "bar");
1557 }
1558 _ => panic!("Expected String, got {:?}", value),
1559 }
1560 }
1561
1562 #[test]
1563 fn test_arg_var_true_with_single_default() {
1564 let cmd = SpecCommand::builder()
1566 .name("test")
1567 .arg(
1568 SpecArg::builder()
1569 .name("files")
1570 .var(true)
1571 .default_value("default.txt")
1572 .required(false)
1573 .build(),
1574 )
1575 .build();
1576 let spec = Spec {
1577 name: "test".to_string(),
1578 bin: "test".to_string(),
1579 cmd,
1580 ..Default::default()
1581 };
1582
1583 let input = vec!["test".to_string()];
1585 let parsed = parse(&spec, &input).unwrap();
1586
1587 assert_eq!(parsed.args.len(), 1);
1588 let value = parsed.args.values().next().unwrap();
1589 match value {
1591 ParseValue::MultiString(v) => {
1592 assert_eq!(v.len(), 1);
1593 assert_eq!(v[0], "default.txt");
1594 }
1595 _ => panic!("Expected MultiString, got {:?}", value),
1596 }
1597 }
1598
1599 #[test]
1600 fn test_arg_var_true_with_multiple_defaults() {
1601 let cmd = SpecCommand::builder()
1603 .name("test")
1604 .arg(
1605 SpecArg::builder()
1606 .name("files")
1607 .var(true)
1608 .default_values(["file1.txt", "file2.txt"])
1609 .required(false)
1610 .build(),
1611 )
1612 .build();
1613 let spec = Spec {
1614 name: "test".to_string(),
1615 bin: "test".to_string(),
1616 cmd,
1617 ..Default::default()
1618 };
1619
1620 let input = vec!["test".to_string()];
1622 let parsed = parse(&spec, &input).unwrap();
1623
1624 assert_eq!(parsed.args.len(), 1);
1625 let value = parsed.args.values().next().unwrap();
1626 match value {
1628 ParseValue::MultiString(v) => {
1629 assert_eq!(v.len(), 2);
1630 assert_eq!(v[0], "file1.txt");
1631 assert_eq!(v[1], "file2.txt");
1632 }
1633 _ => panic!("Expected MultiString, got {:?}", value),
1634 }
1635 }
1636
1637 #[test]
1638 fn test_arg_var_false_with_default_remains_string() {
1639 let cmd = SpecCommand::builder()
1641 .name("test")
1642 .arg(
1643 SpecArg::builder()
1644 .name("file")
1645 .var(false)
1646 .default_value("default.txt")
1647 .required(false)
1648 .build(),
1649 )
1650 .build();
1651 let spec = Spec {
1652 name: "test".to_string(),
1653 bin: "test".to_string(),
1654 cmd,
1655 ..Default::default()
1656 };
1657
1658 let input = vec!["test".to_string()];
1660 let parsed = parse(&spec, &input).unwrap();
1661
1662 assert_eq!(parsed.args.len(), 1);
1663 let value = parsed.args.values().next().unwrap();
1664 match value {
1666 ParseValue::String(s) => {
1667 assert_eq!(s, "default.txt");
1668 }
1669 _ => panic!("Expected String, got {:?}", value),
1670 }
1671 }
1672
1673 #[test]
1674 fn test_scalar_defaults_validate_only_first_default_choice() {
1675 let specs = [
1676 spec_with_arg(
1677 SpecArg::builder()
1678 .name("env")
1679 .var(false)
1680 .default_values(["dev", "prod"])
1681 .choices(["dev"])
1682 .required(false)
1683 .build(),
1684 ),
1685 spec_with_flag(
1686 SpecFlag::builder()
1687 .long("env")
1688 .arg(
1689 SpecArg::builder()
1690 .name("env")
1691 .default_values(["dev", "prod"])
1692 .choices(["dev"])
1693 .build(),
1694 )
1695 .build(),
1696 ),
1697 ];
1698
1699 for spec in specs {
1700 let parsed = parse(&spec, &input(&["test"])).unwrap();
1701 assert_eq!(first_string_value(&parsed), "dev");
1702 }
1703 }
1704
1705 #[test]
1706 fn test_default_subcommand() {
1707 let run_cmd = SpecCommand::builder()
1709 .name("run")
1710 .arg(SpecArg::builder().name("task").build())
1711 .build();
1712 let mut cmd = SpecCommand::builder().name("test").build();
1713 cmd.subcommands.insert("run".to_string(), run_cmd);
1714
1715 let spec = Spec {
1716 name: "test".to_string(),
1717 bin: "test".to_string(),
1718 cmd,
1719 default_subcommand: Some("run".to_string()),
1720 ..Default::default()
1721 };
1722
1723 let input = vec!["test".to_string(), "mytask".to_string()];
1725 let parsed = parse(&spec, &input).unwrap();
1726
1727 assert_eq!(parsed.cmds.len(), 2);
1729 assert_eq!(parsed.cmds[1].name, "run");
1730
1731 assert_eq!(parsed.args.len(), 1);
1733 let arg = parsed.args.keys().next().unwrap();
1734 assert_eq!(arg.name, "task");
1735 let value = parsed.args.values().next().unwrap();
1736 assert_eq!(value.to_string(), "mytask");
1737 }
1738
1739 #[test]
1740 fn test_default_subcommand_explicit_still_works() {
1741 let run_cmd = SpecCommand::builder()
1743 .name("run")
1744 .arg(SpecArg::builder().name("task").build())
1745 .build();
1746 let other_cmd = SpecCommand::builder()
1747 .name("other")
1748 .arg(SpecArg::builder().name("other_arg").build())
1749 .build();
1750 let mut cmd = SpecCommand::builder().name("test").build();
1751 cmd.subcommands.insert("run".to_string(), run_cmd);
1752 cmd.subcommands.insert("other".to_string(), other_cmd);
1753
1754 let spec = Spec {
1755 name: "test".to_string(),
1756 bin: "test".to_string(),
1757 cmd,
1758 default_subcommand: Some("run".to_string()),
1759 ..Default::default()
1760 };
1761
1762 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1764 let parsed = parse(&spec, &input).unwrap();
1765
1766 assert_eq!(parsed.cmds.len(), 2);
1768 assert_eq!(parsed.cmds[1].name, "other");
1769 }
1770
1771 #[test]
1772 fn test_default_subcommand_with_nested_subcommands() {
1773 let say_cmd = SpecCommand::builder()
1777 .name("say")
1778 .arg(SpecArg::builder().name("name").build())
1779 .build();
1780 let mut run_cmd = SpecCommand::builder().name("run").build();
1781 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1782
1783 let mut cmd = SpecCommand::builder().name("test").build();
1784 cmd.subcommands.insert("run".to_string(), run_cmd);
1785
1786 let spec = Spec {
1787 name: "test".to_string(),
1788 bin: "test".to_string(),
1789 cmd,
1790 default_subcommand: Some("run".to_string()),
1791 ..Default::default()
1792 };
1793
1794 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1796 let parsed = parse(&spec, &input).unwrap();
1797
1798 assert_eq!(parsed.cmds.len(), 3);
1800 assert_eq!(parsed.cmds[0].name, "test");
1801 assert_eq!(parsed.cmds[1].name, "run");
1802 assert_eq!(parsed.cmds[2].name, "say");
1803
1804 assert_eq!(parsed.args.len(), 1);
1806 let arg = parsed.args.keys().next().unwrap();
1807 assert_eq!(arg.name, "name");
1808 let value = parsed.args.values().next().unwrap();
1809 assert_eq!(value.to_string(), "hello");
1810 }
1811
1812 fn mounted_global_flag_spec() -> Spec {
1820 let task_cmd = SpecCommand::builder()
1821 .name("sample:run")
1822 .arg(
1823 SpecArg::builder()
1824 .name("profile")
1825 .choices(["alpha", "beta", "gamma"])
1826 .build(),
1827 )
1828 .build();
1829 let mut run_cmd = SpecCommand::builder()
1831 .name("run")
1832 .flag(
1833 SpecFlag::builder()
1834 .name("cd")
1835 .short('C')
1836 .long("cd")
1837 .arg(SpecArg::builder().name("dir").build())
1838 .global(false)
1839 .build(),
1840 )
1841 .build();
1842 run_cmd
1843 .subcommands
1844 .insert("sample:run".to_string(), task_cmd);
1845
1846 let mut cmd = SpecCommand::builder()
1847 .name("test")
1848 .flag(
1849 SpecFlag::builder()
1850 .name("cd")
1851 .short('C')
1852 .long("cd")
1853 .arg(SpecArg::builder().name("dir").build())
1854 .global(true)
1855 .build(),
1856 )
1857 .build();
1858 cmd.subcommands.insert("run".to_string(), run_cmd);
1859
1860 Spec {
1861 name: "test".to_string(),
1862 bin: "test".to_string(),
1863 cmd,
1864 ..Default::default()
1865 }
1866 }
1867
1868 #[test]
1869 fn test_prefix_global_flag_does_not_pollute_choices() {
1870 let spec = mounted_global_flag_spec();
1877
1878 for words in [
1881 &["test", "-C", "/tmp", "run", "sample:run"][..],
1882 &["test", "--cd=/tmp", "run", "sample:run"][..],
1884 ] {
1885 let parsed = parse_partial(&spec, &input(words)).unwrap();
1886 assert_eq!(
1887 parsed
1888 .cmds
1889 .iter()
1890 .map(|c| c.name.as_str())
1891 .collect::<Vec<_>>(),
1892 vec!["test", "run", "sample:run"],
1893 );
1894 assert!(
1896 parsed.args.is_empty(),
1897 "args should be empty, got {:?}",
1898 parsed.args
1899 );
1900
1901 let cd = parsed
1904 .available_flags
1905 .get("--cd")
1906 .expect("--cd should remain available after descending into the subcommand");
1907 assert!(cd.global, "--cd must stay global after descent");
1908 assert!(
1909 parsed.available_flags.get("-C").is_some_and(|f| f.global),
1910 "-C must stay global after descent",
1911 );
1912
1913 assert_eq!(
1917 parsed.as_env().get("usage_cd").map(String::as_str),
1918 Some("/tmp"),
1919 "global flag value must survive in as_env(), got {:?}",
1920 parsed.as_env(),
1921 );
1922 }
1923
1924 let parsed = parse_partial(
1926 &spec,
1927 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
1928 )
1929 .unwrap();
1930 assert_eq!(parsed.args.len(), 1);
1931 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1932
1933 assert_parse_err(
1935 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
1936 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1937 );
1938 }
1939
1940 fn mounted_orphan_short_spec() -> Spec {
1945 let task_cmd = SpecCommand::builder()
1946 .name("sample:run")
1947 .arg(
1948 SpecArg::builder()
1949 .name("profile")
1950 .choices(["alpha", "beta", "gamma"])
1951 .build(),
1952 )
1953 .build();
1954 let mut run_cmd = SpecCommand::builder()
1957 .name("run")
1958 .flag(
1959 SpecFlag::builder()
1960 .name("raw")
1961 .short('r')
1962 .long("raw")
1963 .global(false)
1964 .build(),
1965 )
1966 .flag(
1967 SpecFlag::builder()
1968 .name("force")
1969 .short('f')
1970 .long("force")
1971 .global(false)
1972 .build(),
1973 )
1974 .build();
1975 run_cmd
1976 .subcommands
1977 .insert("sample:run".to_string(), task_cmd);
1978
1979 let mut cmd = SpecCommand::builder()
1981 .name("test")
1982 .flag(
1983 SpecFlag::builder()
1984 .name("raw")
1985 .long("raw")
1986 .global(true)
1987 .build(),
1988 )
1989 .build();
1990 cmd.subcommands.insert("run".to_string(), run_cmd);
1991
1992 Spec {
1993 name: "test".to_string(),
1994 bin: "test".to_string(),
1995 cmd,
1996 ..Default::default()
1997 }
1998 }
1999
2000 #[test]
2001 fn test_orphan_short_alias_survives_merge() {
2002 let spec = mounted_orphan_short_spec();
2008
2009 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
2010 assert_eq!(
2011 parsed
2012 .cmds
2013 .iter()
2014 .map(|c| c.name.as_str())
2015 .collect::<Vec<_>>(),
2016 vec!["test", "run", "sample:run"],
2017 );
2018
2019 assert!(
2022 parsed.available_flags.get("-r").is_some_and(|f| f.global),
2023 "-r must be merged onto the inherited global flag and stay global after descent",
2024 );
2025 assert!(
2026 parsed
2027 .available_flags
2028 .get("--raw")
2029 .is_some_and(|f| f.global),
2030 "--raw must stay global after descent",
2031 );
2032
2033 assert!(
2035 parsed.args.is_empty(),
2036 "args should be empty, got {:?}",
2037 parsed.args
2038 );
2039
2040 assert_eq!(
2042 parsed.as_env().get("usage_raw").map(String::as_str),
2043 Some("true"),
2044 "merged short's value must survive in as_env(), got {:?}",
2045 parsed.as_env(),
2046 );
2047
2048 assert!(
2051 parsed.available_flags.get("-f").is_none(),
2052 "purely-local -f must not be promoted onto a global",
2053 );
2054 assert!(
2055 parsed.available_flags.get("--force").is_none(),
2056 "purely-local --force must not be promoted onto a global",
2057 );
2058
2059 let parsed =
2061 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
2062 assert_eq!(parsed.args.len(), 1);
2063 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
2064
2065 assert_parse_err(
2067 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
2068 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
2069 );
2070 }
2071
2072 #[test]
2073 fn test_orphan_short_does_not_clobber_unrelated_global() {
2074 let run_cmd = SpecCommand::builder()
2079 .name("run")
2080 .flag(
2081 SpecFlag::builder()
2082 .name("raw")
2083 .short('r')
2084 .long("raw")
2085 .global(false)
2086 .build(),
2087 )
2088 .build();
2089 let mut cmd = SpecCommand::builder()
2090 .name("test")
2091 .flag(
2092 SpecFlag::builder()
2093 .name("raw")
2094 .long("raw")
2095 .global(true)
2096 .build(),
2097 )
2098 .flag(
2099 SpecFlag::builder()
2100 .name("restrict")
2101 .short('r')
2102 .long("restrict")
2103 .global(true)
2104 .build(),
2105 )
2106 .build();
2107 cmd.subcommands.insert("run".to_string(), run_cmd);
2108 let spec = Spec {
2109 name: "test".to_string(),
2110 bin: "test".to_string(),
2111 cmd,
2112 ..Default::default()
2113 };
2114
2115 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2116 assert_eq!(
2118 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
2119 Some("restrict"),
2120 "-r must remain owned by the unrelated global it already belonged to",
2121 );
2122 assert!(parsed
2124 .available_flags
2125 .get("--raw")
2126 .is_some_and(|f| f.global));
2127 assert!(parsed
2128 .available_flags
2129 .get("--restrict")
2130 .is_some_and(|f| f.global));
2131 }
2132
2133 #[test]
2134 fn test_redeclared_global_aliases_share_one_flag() {
2135 let spec = r#"
2142flag "-y --yes" global=#true effect="write"
2143cmd "run" {
2144 flag "-y --yes --assume-yes"
2145}
2146"#
2147 .parse::<Spec>()
2148 .unwrap();
2149
2150 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2151
2152 for key in ["-y", "--yes", "--assume-yes"] {
2153 let flag = parsed
2154 .available_flags
2155 .get(key)
2156 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
2157 assert!(flag.global, "{key} must stay global after the descent");
2158 assert_eq!(
2159 flag.long,
2160 vec!["yes".to_string(), "assume-yes".to_string()],
2161 "{key} must resolve to the flag carrying every alias",
2162 );
2163 assert_eq!(flag.short, vec!['y'], "{key} must keep the global's short");
2164 }
2165
2166 assert_eq!(
2168 unique_flags(parsed.available_flags.values()).count(),
2169 1,
2170 "all aliases must point at one flag object, got {:?}",
2171 parsed.available_flags,
2172 );
2173
2174 assert_eq!(
2176 parsed.available_flags["-y"].effect,
2177 Some(crate::SpecCommandEffect::Write),
2178 );
2179 }
2180
2181 #[test]
2182 fn test_partially_redeclared_global_keeps_all_aliases_on_one_flag() {
2183 let spec = r#"
2189flag "-y --yes --confirm" global=#true
2190cmd "run" {
2191 flag "--yes --assume-yes"
2192}
2193"#
2194 .parse::<Spec>()
2195 .unwrap();
2196
2197 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2198
2199 for key in ["-y", "--yes", "--confirm", "--assume-yes"] {
2200 let flag = parsed
2201 .available_flags
2202 .get(key)
2203 .unwrap_or_else(|| panic!("{key} must be recognized after the descent"));
2204 assert!(flag.global, "{key} must stay global after the descent");
2205 assert_eq!(
2206 flag.long,
2207 vec![
2208 "yes".to_string(),
2209 "confirm".to_string(),
2210 "assume-yes".to_string()
2211 ],
2212 "{key} must resolve to the flag carrying every alias",
2213 );
2214 }
2215
2216 assert_eq!(
2217 unique_flags(parsed.available_flags.values()).count(),
2218 1,
2219 "all aliases must point at one flag object, got {:?}",
2220 parsed.available_flags,
2221 );
2222 }
2223
2224 fn mounted_task_flag_spec() -> Spec {
2231 let mut task_cmd = SpecCommand::builder()
2232 .name("mytask")
2233 .flag(
2234 SpecFlag::builder()
2235 .name("env")
2236 .long("env")
2237 .arg(
2238 SpecArg::builder()
2239 .name("name")
2240 .choices(["dev", "stage", "prod"])
2241 .build(),
2242 )
2243 .global(false)
2244 .build(),
2245 )
2246 .flag(
2247 SpecFlag::builder()
2248 .name("bump")
2249 .long("bump")
2250 .arg(
2251 SpecArg::builder()
2252 .name("type")
2253 .choices(["auto", "major"])
2254 .build(),
2255 )
2256 .global(false)
2257 .build(),
2258 )
2259 .build();
2260 task_cmd.mounted = true;
2261
2262 let mut run_cmd = SpecCommand::builder()
2263 .name("run")
2264 .flag(
2265 SpecFlag::builder()
2266 .name("force")
2267 .short('f')
2268 .long("force")
2269 .global(false)
2270 .build(),
2271 )
2272 .build();
2273 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
2274
2275 let mut cmd = SpecCommand::builder()
2276 .name("test")
2277 .flag(
2278 SpecFlag::builder()
2279 .name("env")
2280 .short('E')
2281 .long("env")
2282 .arg(SpecArg::builder().name("ENV").build())
2283 .global(true)
2284 .build(),
2285 )
2286 .flag(
2287 SpecFlag::builder()
2288 .name("silent")
2289 .long("silent")
2290 .global(true)
2291 .build(),
2292 )
2293 .build();
2294 cmd.subcommands.insert("run".to_string(), run_cmd);
2295
2296 Spec {
2297 name: "test".to_string(),
2298 bin: "test".to_string(),
2299 cmd,
2300 ..Default::default()
2301 }
2302 }
2303
2304 #[test]
2305 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
2306 let deep = SpecCommand::builder().name("deep").build();
2312 let mut sub = SpecCommand::builder()
2313 .name("sub")
2314 .flag(
2316 SpecFlag::builder()
2317 .name("cd")
2318 .short('C')
2319 .long("cd")
2320 .arg(SpecArg::builder().name("dir").build())
2321 .global(false)
2322 .build(),
2323 )
2324 .build();
2325 sub.subcommands.insert("deep".to_string(), deep);
2326 let mut task = SpecCommand::builder()
2327 .name("task")
2328 .flag(
2329 SpecFlag::builder()
2330 .name("cd")
2331 .short('C')
2332 .long("cd")
2333 .arg(SpecArg::builder().name("dir").build())
2334 .global(true)
2335 .build(),
2336 )
2337 .build();
2338 task.subcommands.insert("sub".to_string(), sub);
2339 task.mark_mounted();
2340
2341 let mut run_cmd = SpecCommand::builder().name("run").build();
2342 run_cmd.subcommands.insert("task".to_string(), task);
2343 let mut cmd = SpecCommand::builder().name("test").build();
2344 cmd.subcommands.insert("run".to_string(), run_cmd);
2345 let spec = Spec {
2346 name: "test".to_string(),
2347 bin: "test".to_string(),
2348 cmd,
2349 ..Default::default()
2350 };
2351
2352 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
2353 assert!(
2354 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
2355 "the mounted program's own global must survive descents inside the mounted tree",
2356 );
2357 assert!(
2358 parsed.completion_flags().contains_key("--cd"),
2359 "and must still be offered there: it belongs to the mounted program",
2360 );
2361 assert!(
2362 parsed.completion_flags().contains_key("-C"),
2363 "including the short the nested command re-declared",
2364 );
2365 }
2366
2367 #[test]
2368 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
2369 let mut task = SpecCommand::builder()
2374 .name("task")
2375 .flag(
2376 SpecFlag::builder()
2377 .name("bump")
2378 .long("bump")
2379 .global(false)
2380 .build(),
2381 )
2382 .build();
2383 task.mark_mounted();
2384
2385 let mut run_cmd = SpecCommand::builder().name("run").build();
2386 run_cmd.subcommands.insert("task".to_string(), task);
2387 run_cmd.flags = vec![
2389 SpecFlag::builder()
2390 .name("tglobal")
2391 .long("tglobal")
2392 .global(true)
2393 .build(),
2394 SpecFlag::builder()
2395 .name("tlocal")
2396 .long("tlocal")
2397 .global(false)
2398 .build(),
2399 ];
2400 run_cmd.flags_from_mount = true;
2401
2402 let mut cmd = SpecCommand::builder()
2403 .name("test")
2404 .flag(
2405 SpecFlag::builder()
2406 .name("silent")
2407 .long("silent")
2408 .global(true)
2409 .build(),
2410 )
2411 .build();
2412 cmd.subcommands.insert("run".to_string(), run_cmd);
2413 let spec = Spec {
2414 name: "test".to_string(),
2415 bin: "test".to_string(),
2416 cmd,
2417 ..Default::default()
2418 };
2419
2420 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
2421 assert_eq!(
2422 parsed.completion_flags().keys().collect::<Vec<_>>(),
2423 vec!["--bump", "--tglobal"],
2424 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
2425 `--silent` does not, and the mount's non-global root flag is not inherited",
2426 );
2427 }
2428
2429 #[test]
2430 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
2431 let spec = mounted_task_flag_spec();
2436 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
2437
2438 assert!(parsed.available_flags.contains_key("--silent"));
2440 assert!(parsed.available_flags.contains_key("-E"));
2441 assert_eq!(
2443 parsed.completion_flags().keys().collect::<Vec<_>>(),
2444 vec!["--bump", "--env"],
2445 "only the mounted command's own flags may be offered",
2446 );
2447
2448 assert!(!parsed.available_flags.contains_key("--force"));
2450 }
2451
2452 #[test]
2453 fn test_mounted_cmd_flag_wins_over_inherited_global() {
2454 let spec = mounted_task_flag_spec();
2457 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
2458
2459 let awaiting = parsed
2460 .flag_awaiting_value
2461 .first()
2462 .expect("--env should await a value");
2463 assert_eq!(
2464 awaiting
2465 .arg
2466 .as_ref()
2467 .and_then(|a| a.choices.as_ref())
2468 .map(|c| c.choices.clone()),
2469 Some(vec![
2470 "dev".to_string(),
2471 "stage".to_string(),
2472 "prod".to_string()
2473 ]),
2474 "the mounted command's own --env must win over the inherited global",
2475 );
2476
2477 let parsed =
2480 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
2481 assert!(
2482 parsed.args.is_empty(),
2483 "prefix global tokens must not be consumed as positionals, got {:?}",
2484 parsed.args
2485 );
2486 assert_eq!(
2487 parsed.as_env().get("usage_env").map(String::as_str),
2488 Some("anything"),
2489 );
2490 }
2491
2492 #[test]
2493 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
2494 let spec = mounted_task_flag_spec();
2499 let parsed = parse_partial(
2500 &spec,
2501 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
2502 )
2503 .unwrap();
2504 assert!(
2505 parsed.errors.is_empty(),
2506 "prefix global value must not be validated against the mounted flag: {:?}",
2507 parsed
2508 .errors
2509 .iter()
2510 .map(|e| e.to_string())
2511 .collect::<Vec<_>>(),
2512 );
2513 assert_eq!(
2514 parsed.as_env().get("usage_env").map(String::as_str),
2515 Some("not-a-task-choice"),
2516 );
2517
2518 let parsed = parse_partial(
2520 &spec,
2521 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
2522 )
2523 .unwrap();
2524 assert!(parsed.errors.is_empty());
2525 assert_eq!(
2526 parsed.as_env().get("usage_env").map(String::as_str),
2527 Some("not-a-task-choice"),
2528 );
2529
2530 let parsed = parse_partial(
2533 &spec,
2534 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
2535 )
2536 .unwrap();
2537 let awaiting = parsed
2538 .flag_awaiting_value
2539 .first()
2540 .expect("--env should await a value");
2541 assert_eq!(
2542 awaiting
2543 .arg
2544 .as_ref()
2545 .and_then(|a| a.choices.as_ref())
2546 .map(|c| c.choices.clone()),
2547 Some(vec![
2548 "dev".to_string(),
2549 "stage".to_string(),
2550 "prod".to_string()
2551 ]),
2552 "the mounted command's --env must own the name after the mounted command",
2553 );
2554 }
2555
2556 #[test]
2557 fn test_non_global_flag_does_not_hide_subcommand() {
2558 let spec = mounted_task_flag_spec();
2562
2563 for words in [
2564 &["test", "run", "--force", "mytask"][..],
2566 &["test", "run", "-f", "mytask"][..],
2567 &["test", "-E", "prod", "run", "--force", "mytask"][..],
2569 ] {
2570 let parsed = parse_partial(&spec, &input(words)).unwrap();
2571 assert_eq!(
2572 parsed
2573 .cmds
2574 .iter()
2575 .map(|c| c.name.as_str())
2576 .collect::<Vec<_>>(),
2577 vec!["test", "run", "mytask"],
2578 "{words:?} should descend into the mounted command",
2579 );
2580 assert!(
2581 parsed.args.is_empty(),
2582 "{words:?} should not consume a positional, got {:?}",
2583 parsed.args,
2584 );
2585 assert_eq!(
2586 parsed.as_env().get("usage_force").map(String::as_str),
2587 Some("true"),
2588 "the non-global flag must still be recorded for {words:?}",
2589 );
2590 }
2591
2592 let mut run_cmd = SpecCommand::builder()
2595 .name("run")
2596 .flag(
2597 SpecFlag::builder()
2598 .name("output")
2599 .short('o')
2600 .long("output")
2601 .arg(SpecArg::builder().name("mode").build())
2602 .global(false)
2603 .build(),
2604 )
2605 .build();
2606 run_cmd.subcommands.insert(
2607 "task".to_string(),
2608 SpecCommand::builder().name("task").build(),
2609 );
2610 let mut cmd = SpecCommand::builder().name("test").build();
2611 cmd.subcommands.insert("run".to_string(), run_cmd);
2612 let spec = Spec {
2613 name: "test".to_string(),
2614 bin: "test".to_string(),
2615 cmd,
2616 ..Default::default()
2617 };
2618
2619 let parsed =
2620 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
2621 assert_eq!(
2622 parsed
2623 .cmds
2624 .iter()
2625 .map(|c| c.name.as_str())
2626 .collect::<Vec<_>>(),
2627 vec!["test", "run", "task"],
2628 );
2629 assert_eq!(
2630 parsed.as_env().get("usage_output").map(String::as_str),
2631 Some("quiet"),
2632 );
2633
2634 assert_parse_err(
2637 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
2638 "unexpected word: --nope",
2639 );
2640 }
2641
2642 #[test]
2643 fn test_non_mounted_subcommand_offers_inherited_globals() {
2644 let mut run_cmd = SpecCommand::builder().name("run").build();
2647 run_cmd.subcommands.insert(
2648 "nested".to_string(),
2649 SpecCommand::builder().name("nested").build(),
2650 );
2651 let mut cmd = SpecCommand::builder()
2652 .name("test")
2653 .flag(
2654 SpecFlag::builder()
2655 .name("silent")
2656 .long("silent")
2657 .global(true)
2658 .build(),
2659 )
2660 .build();
2661 cmd.subcommands.insert("run".to_string(), run_cmd);
2662 let spec = Spec {
2663 name: "test".to_string(),
2664 bin: "test".to_string(),
2665 cmd,
2666 ..Default::default()
2667 };
2668
2669 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
2670 assert_eq!(
2671 parsed.completion_flags().keys().collect::<Vec<_>>(),
2672 parsed.available_flags.keys().collect::<Vec<_>>(),
2673 );
2674 assert!(parsed.completion_flags().contains_key("--silent"));
2675 }
2676
2677 #[test]
2678 fn test_subcommand_alias_collision_keeps_last_owner() {
2679 let run_cmd = SpecCommand::builder()
2683 .name("run")
2684 .flag(
2685 SpecFlag::builder()
2686 .name("alpha")
2687 .short('x')
2688 .long("alpha")
2689 .global(false)
2690 .build(),
2691 )
2692 .flag(
2693 SpecFlag::builder()
2694 .name("beta")
2695 .short('x')
2696 .long("beta")
2697 .global(false)
2698 .build(),
2699 )
2700 .build();
2701 let mut cmd = SpecCommand::builder().name("test").build();
2702 cmd.subcommands.insert("run".to_string(), run_cmd);
2703 let spec = Spec {
2704 name: "test".to_string(),
2705 bin: "test".to_string(),
2706 cmd,
2707 ..Default::default()
2708 };
2709
2710 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2711 assert_eq!(
2713 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
2714 Some("beta"),
2715 "the last-declared flag must keep a shared short alias",
2716 );
2717 assert_eq!(
2719 parsed
2720 .available_flags
2721 .get("--alpha")
2722 .map(|f| f.name.as_str()),
2723 Some("alpha"),
2724 );
2725 assert_eq!(
2726 parsed
2727 .available_flags
2728 .get("--beta")
2729 .map(|f| f.name.as_str()),
2730 Some("beta"),
2731 );
2732 }
2733
2734 #[test]
2735 fn test_default_subcommand_same_name_child() {
2736 let run_task = SpecCommand::builder()
2740 .name("run")
2741 .arg(SpecArg::builder().name("args").build())
2742 .build();
2743 let mut run_cmd = SpecCommand::builder().name("run").build();
2744 run_cmd.subcommands.insert("run".to_string(), run_task);
2745
2746 let mut cmd = SpecCommand::builder().name("test").build();
2747 cmd.subcommands.insert("run".to_string(), run_cmd);
2748
2749 let spec = Spec {
2750 name: "test".to_string(),
2751 bin: "test".to_string(),
2752 cmd,
2753 default_subcommand: Some("run".to_string()),
2754 ..Default::default()
2755 };
2756
2757 let input = vec!["test".to_string(), "run".to_string()];
2759 let parsed = parse(&spec, &input).unwrap();
2760
2761 assert_eq!(parsed.cmds.len(), 2);
2763 assert_eq!(parsed.cmds[0].name, "test");
2764 assert_eq!(parsed.cmds[1].name, "run");
2765
2766 let input = vec![
2768 "test".to_string(),
2769 "run".to_string(),
2770 "run".to_string(),
2771 "hello".to_string(),
2772 ];
2773 let parsed = parse(&spec, &input).unwrap();
2774
2775 assert_eq!(parsed.cmds.len(), 3);
2776 assert_eq!(parsed.cmds[0].name, "test");
2777 assert_eq!(parsed.cmds[1].name, "run");
2778 assert_eq!(parsed.cmds[2].name, "run");
2779 assert_eq!(parsed.args.len(), 1);
2780 let value = parsed.args.values().next().unwrap();
2781 assert_eq!(value.to_string(), "hello");
2782
2783 let mut run_cmd = SpecCommand::builder()
2787 .name("run")
2788 .arg(SpecArg::builder().name("task").build())
2789 .build();
2790 let run_task = SpecCommand::builder().name("run").build();
2791 run_cmd.subcommands.insert("run".to_string(), run_task);
2792
2793 let mut cmd = SpecCommand::builder().name("test").build();
2794 cmd.subcommands.insert("run".to_string(), run_cmd);
2795
2796 let spec = Spec {
2797 name: "test".to_string(),
2798 bin: "test".to_string(),
2799 cmd,
2800 default_subcommand: Some("run".to_string()),
2801 ..Default::default()
2802 };
2803
2804 let input = vec!["test".to_string(), "other".to_string()];
2805 let parsed = parse(&spec, &input).unwrap();
2806
2807 assert_eq!(parsed.cmds.len(), 2);
2810 assert_eq!(parsed.cmds[0].name, "test");
2811 assert_eq!(parsed.cmds[1].name, "run");
2812
2813 assert_eq!(parsed.args.len(), 1);
2815 let value = parsed.args.values().next().unwrap();
2816 assert_eq!(value.to_string(), "other");
2817 }
2818
2819 #[test]
2820 fn test_restart_token() {
2821 let run_cmd = SpecCommand::builder()
2823 .name("run")
2824 .arg(SpecArg::builder().name("task").build())
2825 .restart_token(":::".to_string())
2826 .build();
2827 let mut cmd = SpecCommand::builder().name("test").build();
2828 cmd.subcommands.insert("run".to_string(), run_cmd);
2829
2830 let spec = Spec {
2831 name: "test".to_string(),
2832 bin: "test".to_string(),
2833 cmd,
2834 ..Default::default()
2835 };
2836
2837 let input = vec![
2839 "test".to_string(),
2840 "run".to_string(),
2841 "task1".to_string(),
2842 ":::".to_string(),
2843 "task2".to_string(),
2844 ];
2845 let parsed = parse(&spec, &input).unwrap();
2846
2847 assert_eq!(parsed.args.len(), 1);
2849 let value = parsed.args.values().next().unwrap();
2850 assert_eq!(value.to_string(), "task2");
2851 }
2852
2853 #[test]
2854 fn test_restart_token_multiple() {
2855 let run_cmd = SpecCommand::builder()
2857 .name("run")
2858 .arg(SpecArg::builder().name("task").build())
2859 .restart_token(":::".to_string())
2860 .build();
2861 let mut cmd = SpecCommand::builder().name("test").build();
2862 cmd.subcommands.insert("run".to_string(), run_cmd);
2863
2864 let spec = Spec {
2865 name: "test".to_string(),
2866 bin: "test".to_string(),
2867 cmd,
2868 ..Default::default()
2869 };
2870
2871 let input = vec![
2873 "test".to_string(),
2874 "run".to_string(),
2875 "task1".to_string(),
2876 ":::".to_string(),
2877 "task2".to_string(),
2878 ":::".to_string(),
2879 "task3".to_string(),
2880 ];
2881 let parsed = parse(&spec, &input).unwrap();
2882
2883 assert_eq!(parsed.args.len(), 1);
2885 let value = parsed.args.values().next().unwrap();
2886 assert_eq!(value.to_string(), "task3");
2887 }
2888
2889 #[test]
2890 fn test_restart_token_clears_flag_awaiting_value() {
2891 let run_cmd = SpecCommand::builder()
2893 .name("run")
2894 .arg(SpecArg::builder().name("task").build())
2895 .flag(
2896 SpecFlag::builder()
2897 .name("jobs")
2898 .long("jobs")
2899 .arg(SpecArg::builder().name("count").build())
2900 .build(),
2901 )
2902 .restart_token(":::".to_string())
2903 .build();
2904 let mut cmd = SpecCommand::builder().name("test").build();
2905 cmd.subcommands.insert("run".to_string(), run_cmd);
2906
2907 let spec = Spec {
2908 name: "test".to_string(),
2909 bin: "test".to_string(),
2910 cmd,
2911 ..Default::default()
2912 };
2913
2914 let input = vec![
2916 "test".to_string(),
2917 "run".to_string(),
2918 "task1".to_string(),
2919 "--jobs".to_string(),
2920 ":::".to_string(),
2921 "task2".to_string(),
2922 ];
2923 let parsed = parse(&spec, &input).unwrap();
2924
2925 assert_eq!(parsed.args.len(), 1);
2927 let value = parsed.args.values().next().unwrap();
2928 assert_eq!(value.to_string(), "task2");
2929 assert!(parsed.flag_awaiting_value.is_empty());
2931 }
2932
2933 #[test]
2934 fn test_restart_token_resets_double_dash() {
2935 let run_cmd = SpecCommand::builder()
2937 .name("run")
2938 .arg(SpecArg::builder().name("task").build())
2939 .arg(SpecArg::builder().name("extra_args").var(true).build())
2940 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
2941 .restart_token(":::".to_string())
2942 .build();
2943 let mut cmd = SpecCommand::builder().name("test").build();
2944 cmd.subcommands.insert("run".to_string(), run_cmd);
2945
2946 let spec = Spec {
2947 name: "test".to_string(),
2948 bin: "test".to_string(),
2949 cmd,
2950 ..Default::default()
2951 };
2952
2953 let input = vec![
2955 "test".to_string(),
2956 "run".to_string(),
2957 "task1".to_string(),
2958 "--".to_string(),
2959 "extra".to_string(),
2960 ":::".to_string(),
2961 "--verbose".to_string(),
2962 "task2".to_string(),
2963 ];
2964 let parsed = parse(&spec, &input).unwrap();
2965
2966 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
2968 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2970 let value = parsed.args.get(task_arg).unwrap();
2971 assert_eq!(value.to_string(), "task2");
2972 }
2973
2974 #[test]
2975 fn test_double_dashes_without_preserve() {
2976 let run_cmd = SpecCommand::builder()
2978 .name("run")
2979 .arg(SpecArg::builder().name("args").var(true).build())
2980 .build();
2981 let mut cmd = SpecCommand::builder().name("test").build();
2982 cmd.subcommands.insert("run".to_string(), run_cmd);
2983
2984 let spec = Spec {
2985 name: "test".to_string(),
2986 bin: "test".to_string(),
2987 cmd,
2988 ..Default::default()
2989 };
2990
2991 let input = vec![
2993 "test".to_string(),
2994 "run".to_string(),
2995 "arg1".to_string(),
2996 "--".to_string(),
2997 "arg2".to_string(),
2998 "--".to_string(),
2999 "arg3".to_string(),
3000 ];
3001 let parsed = parse(&spec, &input).unwrap();
3002
3003 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3004 let value = parsed.args.get(args_arg).unwrap();
3005 assert_eq!(value.to_string(), "arg1 arg2 arg3");
3006 }
3007
3008 #[test]
3009 fn test_double_dashes_with_preserve() {
3010 let run_cmd = SpecCommand::builder()
3012 .name("run")
3013 .arg(
3014 SpecArg::builder()
3015 .name("args")
3016 .var(true)
3017 .double_dash(SpecDoubleDashChoices::Preserve)
3018 .build(),
3019 )
3020 .build();
3021 let mut cmd = SpecCommand::builder().name("test").build();
3022 cmd.subcommands.insert("run".to_string(), run_cmd);
3023
3024 let spec = Spec {
3025 name: "test".to_string(),
3026 bin: "test".to_string(),
3027 cmd,
3028 ..Default::default()
3029 };
3030
3031 let input = vec![
3033 "test".to_string(),
3034 "run".to_string(),
3035 "arg1".to_string(),
3036 "--".to_string(),
3037 "arg2".to_string(),
3038 "--".to_string(),
3039 "arg3".to_string(),
3040 ];
3041 let parsed = parse(&spec, &input).unwrap();
3042
3043 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3044 let value = parsed.args.get(args_arg).unwrap();
3045 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
3046 }
3047
3048 #[test]
3049 fn test_double_dashes_with_preserve_only_dashes() {
3050 let run_cmd = SpecCommand::builder()
3053 .name("run")
3054 .arg(
3055 SpecArg::builder()
3056 .name("args")
3057 .var(true)
3058 .double_dash(SpecDoubleDashChoices::Preserve)
3059 .build(),
3060 )
3061 .build();
3062 let mut cmd = SpecCommand::builder().name("test").build();
3063 cmd.subcommands.insert("run".to_string(), run_cmd);
3064
3065 let spec = Spec {
3066 name: "test".to_string(),
3067 bin: "test".to_string(),
3068 cmd,
3069 ..Default::default()
3070 };
3071
3072 let input = vec![
3074 "test".to_string(),
3075 "run".to_string(),
3076 "--".to_string(),
3077 "--".to_string(),
3078 ];
3079 let parsed = parse(&spec, &input).unwrap();
3080
3081 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
3082 let value = parsed.args.get(args_arg).unwrap();
3083 assert_eq!(value.to_string(), "-- --");
3084 }
3085
3086 #[test]
3087 fn test_double_dashes_with_preserve_multiple_args() {
3088 let run_cmd = SpecCommand::builder()
3090 .name("run")
3091 .arg(SpecArg::builder().name("task").build())
3092 .arg(
3093 SpecArg::builder()
3094 .name("extra_args")
3095 .var(true)
3096 .double_dash(SpecDoubleDashChoices::Preserve)
3097 .build(),
3098 )
3099 .build();
3100 let mut cmd = SpecCommand::builder().name("test").build();
3101 cmd.subcommands.insert("run".to_string(), run_cmd);
3102
3103 let spec = Spec {
3104 name: "test".to_string(),
3105 bin: "test".to_string(),
3106 cmd,
3107 ..Default::default()
3108 };
3109
3110 let input = vec![
3113 "test".to_string(),
3114 "run".to_string(),
3115 "task1".to_string(),
3116 "--".to_string(),
3117 "arg1".to_string(),
3118 "--".to_string(),
3119 "--foo".to_string(),
3120 ];
3121 let parsed = parse(&spec, &input).unwrap();
3122
3123 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
3124 let task_value = parsed.args.get(task_arg).unwrap();
3125 assert_eq!(task_value.to_string(), "task1");
3126
3127 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
3128 let extra_value = parsed.args.get(extra_arg).unwrap();
3129 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
3130 }
3131
3132 #[test]
3133 fn test_parser_with_custom_env_for_required_arg() {
3134 let spec = spec_with_arg(
3135 SpecArg::builder()
3136 .name("name")
3137 .env("NAME")
3138 .required(true)
3139 .build(),
3140 );
3141 std::env::remove_var("NAME");
3142
3143 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
3144 .expect("parse should succeed with custom env");
3145 assert_eq!(parsed.args.len(), 1);
3146 assert_eq!(first_string_value(&parsed), "john");
3147 }
3148
3149 #[test]
3150 fn test_parser_with_custom_env_for_required_flag() {
3151 let spec = spec_with_flag(
3152 SpecFlag::builder()
3153 .long("name")
3154 .env("NAME")
3155 .required(true)
3156 .arg(SpecArg::builder().name("name").build())
3157 .build(),
3158 );
3159 std::env::remove_var("NAME");
3160
3161 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
3162 .expect("parse should succeed with custom env");
3163 assert_eq!(parsed.flags.len(), 1);
3164 assert_eq!(first_string_value(&parsed), "jane");
3165 }
3166
3167 #[test]
3168 fn test_parser_with_custom_env_still_fails_when_missing() {
3169 let spec = spec_with_arg(
3170 SpecArg::builder()
3171 .name("name")
3172 .env("NAME")
3173 .required(true)
3174 .build(),
3175 );
3176 std::env::remove_var("NAME");
3177 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
3178 }
3179
3180 #[test]
3181 fn test_parser_does_not_treat_env_choice_value_as_help() {
3182 let spec = spec_with_arg(
3183 SpecArg::builder()
3184 .name("env")
3185 .env("CURRENT_ENV")
3186 .choices(["dev", "staging"])
3187 .required(false)
3188 .build(),
3189 );
3190
3191 assert_parse_err(
3192 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
3193 "Invalid choice for arg env: --help, expected one of dev, staging",
3194 );
3195 }
3196
3197 #[test]
3198 fn test_parser_does_not_treat_default_choice_value_as_help() {
3199 let spec = spec_with_flag(
3200 SpecFlag::builder()
3201 .long("env")
3202 .arg(
3203 SpecArg::builder()
3204 .name("env")
3205 .choices(["dev", "staging"])
3206 .build(),
3207 )
3208 .default_value("--help")
3209 .build(),
3210 );
3211
3212 assert_parse_err(
3213 parse_with_env(&spec, &["test"], &[]),
3214 "Invalid choice for option env: --help, expected one of dev, staging",
3215 );
3216 }
3217
3218 #[cfg(feature = "unstable_choices_env")]
3219 #[test]
3220 fn test_parser_arg_choices_from_custom_env() {
3221 let spec = spec_arg_choices_env("DEPLOY_ENVS");
3222
3223 let parsed =
3224 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
3225 assert_eq!(first_string_value(&parsed), "bar");
3226
3227 assert_parse_err(
3228 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
3229 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
3230 );
3231 assert_parse_err(
3232 parse_with_env(&spec, &["test", "prod"], &[]),
3233 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
3234 );
3235 }
3236
3237 #[cfg(feature = "unstable_choices_env")]
3238 #[test]
3239 fn test_parser_validates_flag_choices_from_custom_env() {
3240 let spec = spec_flag_choices_env("DEPLOY_ENVS");
3241 let parsed = parse_with_env(
3242 &spec,
3243 &["test", "--env", "baz"],
3244 &[("DEPLOY_ENVS", "foo,bar baz")],
3245 )
3246 .unwrap();
3247 assert_eq!(first_string_value(&parsed), "baz");
3248 }
3249
3250 #[cfg(feature = "unstable_choices_env")]
3251 #[test]
3252 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
3253 let arg_env_spec = spec_with_arg(
3254 SpecArg::builder()
3255 .name("env")
3256 .env("CURRENT_ENV")
3257 .choices_env("DEPLOY_ENVS")
3258 .build(),
3259 );
3260 assert_parse_err(
3261 parse_with_env(
3262 &arg_env_spec,
3263 &["test"],
3264 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
3265 ),
3266 "Invalid choice for arg env: prod, expected one of dev, staging",
3267 );
3268
3269 let flag_default_spec = spec_with_flag(
3270 SpecFlag::builder()
3271 .long("env")
3272 .arg(
3273 SpecArg::builder()
3274 .name("env")
3275 .choices_env("DEPLOY_ENVS")
3276 .build(),
3277 )
3278 .default_value("prod")
3279 .build(),
3280 );
3281 assert_parse_err(
3282 parse_with_env(
3283 &flag_default_spec,
3284 &["test"],
3285 &[("DEPLOY_ENVS", "dev,staging")],
3286 ),
3287 "Invalid choice for option env: prod, expected one of dev, staging",
3288 );
3289 }
3290
3291 #[test]
3292 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
3293 let spec: Spec = r#"
3294 flag "-v --verbose" var=#true
3295 arg "[database]" default="myapp_dev"
3296 arg "[args...]"
3297 "#
3298 .parse()
3299 .unwrap();
3300 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3301 .into_iter()
3302 .map(String::from)
3303 .collect();
3304 let parsed = parse(&spec, &input).unwrap();
3305 let env = parsed.as_env();
3306 assert_eq!(env.get("usage_database").unwrap(), "mydb");
3307 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
3308 }
3309
3310 #[test]
3311 fn test_variadic_arg_captures_unknown_flags() {
3312 let cmd = SpecCommand::builder()
3313 .name("test")
3314 .flag(SpecFlag::builder().short('v').long("verbose").build())
3315 .arg(SpecArg::builder().name("database").required(false).build())
3316 .arg(
3317 SpecArg::builder()
3318 .name("args")
3319 .required(false)
3320 .var(true)
3321 .build(),
3322 )
3323 .build();
3324 let spec = Spec {
3325 name: "test".to_string(),
3326 bin: "test".to_string(),
3327 cmd,
3328 ..Default::default()
3329 };
3330
3331 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3333 .into_iter()
3334 .map(String::from)
3335 .collect();
3336 let parsed = parse(&spec, &input).unwrap();
3337 assert_eq!(parsed.args.len(), 2);
3338 let args_val = parsed
3339 .args
3340 .iter()
3341 .find(|(a, _)| a.name == "args")
3342 .unwrap()
3343 .1;
3344 match args_val {
3345 ParseValue::MultiString(v) => {
3346 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3347 }
3348 _ => panic!("Expected MultiString, got {:?}", args_val),
3349 }
3350 }
3351
3352 #[test]
3353 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
3354 let cmd = SpecCommand::builder()
3355 .name("test")
3356 .flag(SpecFlag::builder().short('v').long("verbose").build())
3357 .arg(SpecArg::builder().name("database").required(false).build())
3358 .arg(
3359 SpecArg::builder()
3360 .name("args")
3361 .required(false)
3362 .var(true)
3363 .build(),
3364 )
3365 .build();
3366 let spec = Spec {
3367 name: "test".to_string(),
3368 bin: "test".to_string(),
3369 cmd,
3370 ..Default::default()
3371 };
3372
3373 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
3375 .into_iter()
3376 .map(String::from)
3377 .collect();
3378 let parsed = parse(&spec, &input).unwrap();
3379 assert_eq!(parsed.args.len(), 2);
3380 let args_val = parsed
3381 .args
3382 .iter()
3383 .find(|(a, _)| a.name == "args")
3384 .unwrap()
3385 .1;
3386 match args_val {
3387 ParseValue::MultiString(v) => {
3388 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3389 }
3390 _ => panic!("Expected MultiString, got {:?}", args_val),
3391 }
3392 }
3393
3394 #[test]
3395 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
3396 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
3399
3400 let input: Vec<String> = vec!["test", "--option=3"]
3403 .into_iter()
3404 .map(String::from)
3405 .collect();
3406 let parsed = parse(&spec, &input).unwrap();
3407 let env = parsed.as_env();
3408 assert_eq!(
3409 env.get("usage_other_args").map(String::as_str),
3410 Some("'--option=3'"),
3411 "expected a single --option=3 token, got {:?}",
3412 env.get("usage_other_args"),
3413 );
3414
3415 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
3417 .into_iter()
3418 .map(String::from)
3419 .collect();
3420 let parsed2 = parse(&spec, &input2).unwrap();
3421 let env2 = parsed2.as_env();
3422 assert_eq!(
3423 env2.get("usage_other_args").map(String::as_str),
3424 Some("'--foo=bar' '--baz=qux'"),
3425 "expected two intact tokens, got {:?}",
3426 env2.get("usage_other_args"),
3427 );
3428
3429 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
3431 .into_iter()
3432 .map(String::from)
3433 .collect();
3434 let parsed3 = parse(&spec, &input3).unwrap();
3435 let env3 = parsed3.as_env();
3436 assert_eq!(
3437 env3.get("usage_other_args").map(String::as_str),
3438 Some("positional1 '--option=3' positional2"),
3439 "expected positional args and intact flag token, got {:?}",
3440 env3.get("usage_other_args"),
3441 );
3442 }
3443
3444 #[test]
3445 fn test_allow_hyphen_values_consumes_short_flag_collision() {
3446 let spec = r#"
3447flag "-d --working-dir <DIR>"
3448flag "-a --args <ARGS>" allow_hyphen_values=#true
3449"#
3450 .parse::<Spec>()
3451 .unwrap();
3452
3453 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3454
3455 assert_eq!(parsed.flags.len(), 1);
3456 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3457 }
3458
3459 #[test]
3460 fn test_allow_hyphen_values_consumes_embedded_long_value() {
3461 let spec = r#"
3462flag "-d --working-dir <DIR>"
3463flag "-a --args <ARGS>" allow_hyphen_values=#true
3464"#
3465 .parse::<Spec>()
3466 .unwrap();
3467
3468 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
3469
3470 assert_eq!(parsed.flags.len(), 1);
3471 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3472 }
3473
3474 #[test]
3475 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
3476 let spec = r#"
3477flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
3478"#
3479 .parse::<Spec>()
3480 .unwrap();
3481
3482 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
3483
3484 let flag = parsed
3485 .flags
3486 .keys()
3487 .find(|flag| flag.name == "args")
3488 .expect("expected args flag");
3489 let value = parsed.flags.get(flag).expect("expected args value");
3490 match value {
3491 ParseValue::MultiString(values) => {
3492 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
3493 }
3494 _ => panic!("expected MultiString, got {value:?}"),
3495 }
3496 }
3497
3498 #[test]
3499 fn test_hyphen_values_still_default_to_short_flag_parsing() {
3500 let spec = r#"
3501flag "-d --working-dir <DIR>"
3502flag "-a --args <ARGS>"
3503"#
3504 .parse::<Spec>()
3505 .unwrap();
3506
3507 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3508
3509 assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
3510 }
3511
3512 mod available_flags {
3515 use super::*;
3516
3517 fn spec() -> Spec {
3518 r#"
3519bin "test"
3520flag "-v --verbose" global=#true
3521flag "--raw" global=#true effect="write"
3522flag "--local-only"
3523cmd "run" {
3524 flag "-r --raw"
3525 flag "-w --watch"
3526 cmd "once"
3527}
3528"#
3529 .parse::<Spec>()
3530 .unwrap()
3531 }
3532
3533 fn chain<'a>(spec: &'a Spec, path: &[&str]) -> Vec<&'a SpecCommand> {
3534 let mut chain = vec![&spec.cmd];
3535 for segment in path {
3536 chain.push(chain.last().unwrap().find_subcommand(segment).unwrap());
3537 }
3538 chain
3539 }
3540
3541 fn names(spec: &Spec, path: &[&str]) -> Vec<String> {
3542 let mut names: Vec<_> = available_flags(&chain(spec, path))
3543 .iter()
3544 .map(|f| f.name.clone())
3545 .collect();
3546 names.sort();
3547 names
3548 }
3549
3550 #[test]
3551 fn an_empty_chain_yields_nothing() {
3552 assert!(available_flags(&[]).is_empty());
3553 }
3554
3555 #[test]
3556 fn the_root_gets_its_own_flags() {
3557 let spec = spec();
3558 assert_eq!(names(&spec, &[]), ["local-only", "raw", "verbose"]);
3559 }
3560
3561 #[test]
3562 fn a_subcommand_keeps_globals_and_drops_local_only_ancestors() {
3563 let spec = spec();
3564 assert_eq!(names(&spec, &["run"]), ["raw", "verbose", "watch"]);
3565 }
3566
3567 #[test]
3568 fn a_re_declared_global_is_listed_once() {
3569 let spec = r#"
3572bin "test"
3573flag "-y --yes" global=#true effect="write"
3574cmd "rm" {
3575 flag "-y --yes"
3576}
3577"#
3578 .parse::<Spec>()
3579 .unwrap();
3580 let flags = available_flags(&chain(&spec, &["rm"]));
3581 assert_eq!(flags.len(), 1, "{flags:?}");
3582 assert_eq!(flags[0].effect.map(|e| e.as_str()), Some("write"));
3583 }
3584
3585 #[test]
3586 fn a_re_declared_global_keeps_the_globals_declaration() {
3587 let spec = spec();
3591 let flags = available_flags(&chain(&spec, &["run"]));
3592 let raw = flags.iter().find(|f| f.name == "raw").unwrap();
3593 assert!(raw.global);
3594 assert_eq!(raw.effect.map(|e| e.as_str()), Some("write"));
3595 assert_eq!(raw.short, ['r']);
3596 }
3597
3598 #[test]
3599 fn it_matches_what_a_parse_accepts() {
3600 let spec = spec();
3603 for path in [vec![], vec!["run"], vec!["run", "once"]] {
3604 let argv = std::iter::once("test".to_string())
3605 .chain(path.iter().map(|s| s.to_string()))
3606 .collect::<Vec<_>>();
3607 let parsed = parse_partial(&spec, &argv).unwrap();
3608
3609 let mut from_parse: Vec<_> = unique_flags(parsed.available_flags.values())
3610 .map(|f| f.name.clone())
3611 .collect();
3612 from_parse.sort();
3613 assert_eq!(names(&spec, &path), from_parse, "path {path:?}");
3614 }
3615 }
3616 }
3617}