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
57 for (key, flag) in new_flags {
62 if flag.global {
63 available.insert(key, flag);
65 continue;
66 }
67
68 let inherited_global = flag.long.iter().find_map(|l| {
77 available
78 .get(&format!("--{l}"))
79 .filter(|f| f.global)
80 .cloned()
81 });
82 if let Some(global_flag) = inherited_global {
83 if available
88 .get(&key)
89 .is_some_and(|existing| existing.global && !Arc::ptr_eq(existing, &global_flag))
90 {
91 continue;
92 }
93 let merged = merged_cache
94 .entry(Arc::as_ptr(&flag) as usize)
95 .or_insert_with(|| {
96 let mut merged = (*global_flag).clone();
97 for s in &flag.short {
98 if !merged.short.contains(s) {
99 merged.short.push(*s);
100 }
101 }
102 for l in &flag.long {
103 if !merged.long.contains(l) {
104 merged.long.push(l.clone());
105 }
106 }
107 Arc::new(merged)
108 })
109 .clone();
110 available.insert(key, merged);
111 continue;
112 }
113
114 if available.contains_key(&key) {
118 continue;
119 }
120 available.insert(key, flag);
121 }
122}
123
124fn flag_keys(flag: &SpecFlag) -> Vec<String> {
127 let mut keys: Vec<String> = flag
128 .long
129 .iter()
130 .map(|l| format!("--{l}"))
131 .chain(flag.short.iter().map(|s| format!("-{s}")))
132 .collect();
133 if let Some(negate) = &flag.negate {
134 keys.push(negate.clone());
135 }
136 keys
137}
138
139fn gather_flags(cmd: &SpecCommand) -> BTreeMap<String, Arc<SpecFlag>> {
141 cmd.flags
142 .iter()
143 .flat_map(|f| {
144 let f = Arc::new(f.clone()); flag_keys(&f)
146 .into_iter()
147 .map(|key| (key, Arc::clone(&f)))
148 .collect::<Vec<_>>()
149 })
150 .collect()
151}
152
153fn unique_flags<'a>(
154 flags: impl IntoIterator<Item = &'a Arc<SpecFlag>>,
155) -> impl Iterator<Item = &'a Arc<SpecFlag>> {
156 let mut seen = HashSet::new();
157 flags
158 .into_iter()
159 .filter(move |flag| seen.insert(Arc::as_ptr(flag) as usize))
160}
161
162fn get_flag_key(word: &str) -> &str {
165 if word.starts_with("--") {
166 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
168 } else if word.len() >= 2 {
169 &word[0..2]
171 } else {
172 word
173 }
174}
175
176pub struct ParseOutput {
177 pub cmd: SpecCommand,
178 pub cmds: Vec<SpecCommand>,
179 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
180 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
181 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
188 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
189 pub errors: Vec<UsageErr>,
190}
191
192impl ParseOutput {
193 pub fn completion_flags(&self) -> BTreeMap<String, Arc<SpecFlag>> {
202 let Some(boundary) = self.cmds.iter().position(|cmd| cmd.mounted) else {
203 return self.available_flags.clone();
204 };
205 let start = match boundary.checked_sub(1) {
209 Some(prev) if self.cmds[prev].flags_from_mount => prev,
210 _ => boundary,
211 };
212 let mut offered = gather_flags(&self.cmds[start]);
216 for cmd in &self.cmds[start + 1..] {
217 merge_subcommand_flags(&mut offered, gather_flags(cmd), false);
218 }
219 offered
220 }
221}
222
223#[derive(Debug, EnumTryAs, Clone)]
224pub enum ParseValue {
225 Bool(bool),
226 String(String),
227 MultiBool(Vec<bool>),
228 MultiString(Vec<String>),
229}
230
231#[non_exhaustive]
251pub struct Parser<'a> {
252 spec: &'a Spec,
253 env: Option<HashMap<String, String>>,
254}
255
256impl<'a> Parser<'a> {
257 pub fn new(spec: &'a Spec) -> Self {
259 Self { spec, env: None }
260 }
261
262 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
267 self.env = Some(env);
268 self
269 }
270
271 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
275 let custom_env = self.env.as_ref();
276 let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
277 trace!("{out:?}");
278
279 let get_env = |key: &str| -> Option<String> {
280 if let Some(env_map) = custom_env {
281 env_map.get(key).cloned()
282 } else {
283 std::env::var(key).ok()
284 }
285 };
286
287 for arg in out.cmd.args.iter().skip(out.args.len()) {
289 if let Some(env_var) = arg.env.as_ref() {
290 if let Some(env_value) = get_env(env_var) {
291 validate_choice_value(
292 ChoiceTarget::arg(arg),
293 &env_value,
294 arg.choices.as_ref(),
295 custom_env,
296 )?;
297 out.args
298 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
299 continue;
300 }
301 }
302 if !arg.default.is_empty() {
303 if arg.var {
305 validate_choice_values(
306 ChoiceTarget::arg(arg),
307 &arg.default,
308 arg.choices.as_ref(),
309 custom_env,
310 )?;
311 out.args.insert(
313 Arc::new(arg.clone()),
314 ParseValue::MultiString(arg.default.clone()),
315 );
316 } else {
317 validate_choice_value(
318 ChoiceTarget::arg(arg),
319 &arg.default[0],
320 arg.choices.as_ref(),
321 custom_env,
322 )?;
323 out.args.insert(
325 Arc::new(arg.clone()),
326 ParseValue::String(arg.default[0].clone()),
327 );
328 }
329 }
330 }
331
332 for flag in out.available_flags.values() {
334 if out.flags.contains_key(flag) {
335 continue;
336 }
337 if let Some(env_var) = flag.env.as_ref() {
338 if let Some(env_value) = get_env(env_var) {
339 if let Some(arg) = flag.arg.as_ref() {
340 validate_choice_value(
341 ChoiceTarget::option(flag),
342 &env_value,
343 arg.choices.as_ref(),
344 custom_env,
345 )?;
346 out.flags
347 .insert(Arc::clone(flag), ParseValue::String(env_value));
348 } else {
349 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
351 out.flags
352 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
353 }
354 continue;
355 }
356 }
357 if !flag.default.is_empty() {
359 if flag.var {
361 if let Some(arg) = flag.arg.as_ref() {
363 validate_choice_values(
364 ChoiceTarget::option(flag),
365 &flag.default,
366 arg.choices.as_ref(),
367 custom_env,
368 )?;
369 out.flags.insert(
370 Arc::clone(flag),
371 ParseValue::MultiString(flag.default.clone()),
372 );
373 } else {
374 let bools: Vec<bool> = flag
376 .default
377 .iter()
378 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
379 .collect();
380 out.flags
381 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
382 }
383 } else {
384 if let Some(arg) = flag.arg.as_ref() {
386 validate_choice_value(
387 ChoiceTarget::option(flag),
388 &flag.default[0],
389 arg.choices.as_ref(),
390 custom_env,
391 )?;
392 out.flags.insert(
393 Arc::clone(flag),
394 ParseValue::String(flag.default[0].clone()),
395 );
396 } else {
397 let is_true =
399 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
400 out.flags
401 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
402 }
403 }
404 }
405 if let Some(arg) = flag.arg.as_ref() {
407 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
408 if flag.var {
409 validate_choice_values(
410 ChoiceTarget::option(flag),
411 &arg.default,
412 arg.choices.as_ref(),
413 custom_env,
414 )?;
415 out.flags.insert(
416 Arc::clone(flag),
417 ParseValue::MultiString(arg.default.clone()),
418 );
419 } else {
420 validate_choice_value(
421 ChoiceTarget::option(flag),
422 &arg.default[0],
423 arg.choices.as_ref(),
424 custom_env,
425 )?;
426 out.flags
427 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
428 }
429 }
430 }
431 }
432 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
433 bail!("{err}");
434 }
435 if !out.errors.is_empty() {
436 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
437 }
438 Ok(out)
439 }
440}
441
442#[must_use = "parsing result should be used"]
449pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
450 Parser::new(spec).parse(input)
451}
452
453#[must_use = "parsing result should be used"]
457pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
458 parse_partial_with_env(spec, input, None)
459}
460
461fn parse_partial_with_env(
463 spec: &Spec,
464 input: &[String],
465 custom_env: Option<&HashMap<String, String>>,
466) -> Result<ParseOutput, miette::Error> {
467 trace!("parse_partial: {input:?}");
468 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
469 input.pop_front();
470
471 let mut out = ParseOutput {
472 cmd: spec.cmd.clone(),
473 cmds: vec![spec.cmd.clone()],
474 args: IndexMap::new(),
475 flags: IndexMap::new(),
476 available_flags: gather_flags(&spec.cmd),
477 flag_awaiting_value: vec![],
478 errors: vec![],
479 };
480
481 let mut prefix_words: Vec<String> = vec![];
494 let mut prefix_bindings: VecDeque<Option<Arc<SpecFlag>>> = VecDeque::new();
502 let mut idx = 0;
503 let mut used_default_subcommand = false;
506
507 while idx < input.len() {
508 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
509 let mut subcommand = subcommand.clone();
510 subcommand.mount(&prefix_words)?;
512 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
515 merge_subcommand_flags(
516 &mut out.available_flags,
517 gather_flags(&subcommand),
518 crossing_mount,
519 );
520 input.remove(idx);
522 out.cmds.push(subcommand.clone());
523 out.cmd = subcommand.clone();
524 prefix_words.clear();
525 } else if input[idx].starts_with('-') {
528 let word = input[idx].clone();
530 let flag_key = get_flag_key(&word);
531
532 if let Some(f) = out.available_flags.get(flag_key).cloned() {
533 prefix_bindings.push_back(Some(Arc::clone(&f)));
541 if f.global {
542 prefix_words.push(word.clone());
543 }
544 idx += 1;
545
546 if f.arg.is_some()
549 && !word.contains('=')
550 && idx < input.len()
551 && !input[idx].starts_with('-')
552 {
553 if f.global {
554 prefix_words.push(input[idx].clone());
555 }
556 prefix_bindings.push_back(None);
557 idx += 1;
558 }
559 } else {
560 break;
563 }
564 } else {
565 if !used_default_subcommand {
568 if let Some(default_name) = &spec.default_subcommand {
569 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
570 let mut subcommand = subcommand.clone();
571 subcommand.mount(&prefix_words)?;
573 let crossing_mount = subcommand.mounted && !out.cmd.mounted;
574 merge_subcommand_flags(
575 &mut out.available_flags,
576 gather_flags(&subcommand),
577 crossing_mount,
578 );
579 out.cmds.push(subcommand.clone());
580 out.cmd = subcommand.clone();
581 prefix_words.clear();
582 used_default_subcommand = true;
583 continue;
588 }
589 }
590 }
591 break;
593 }
594 }
595
596 let mut next_arg = out.cmd.args.first();
601 let mut enable_flags = true;
602 let mut grouped_flag = false;
603
604 while !input.is_empty() {
605 let mut w = input.pop_front().unwrap();
606 let binding = prefix_bindings.pop_front().flatten();
609
610 if let Some(ref restart_token) = out.cmd.restart_token {
613 if w == *restart_token {
614 out.args.clear();
616 next_arg = out.cmd.args.first();
617 out.flag_awaiting_value.clear(); enable_flags = true; continue;
621 }
622 }
623
624 if w == "--" {
625 enable_flags = false;
627
628 let should_preserve = next_arg
631 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
632 .unwrap_or(false);
633
634 if should_preserve {
635 } else {
637 continue;
639 }
640 }
641
642 if w.starts_with('-')
643 && out
644 .flag_awaiting_value
645 .last()
646 .is_some_and(|flag| flag.allow_hyphen_values())
647 {
648 let should_return = drain_pending_flag_values(
649 spec,
650 &out.cmd,
651 &mut out.errors,
652 &mut out.flags,
653 &mut out.flag_awaiting_value,
654 &mut w,
655 custom_env,
656 )?;
657 if should_return {
658 return Ok(out);
659 }
660 continue;
661 }
662
663 if enable_flags && w.starts_with("--") {
665 grouped_flag = false;
666 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
667 if let Some(f) = binding.as_ref().or_else(|| out.available_flags.get(word)) {
668 if !val.is_empty() {
672 input.push_front(val.to_string());
673 prefix_bindings.push_front(None);
674 }
675 if f.arg.is_some() {
676 out.flag_awaiting_value.push(Arc::clone(f));
677 } else if f.count {
678 let arr = out
679 .flags
680 .entry(Arc::clone(f))
681 .or_insert_with(|| ParseValue::MultiBool(vec![]))
682 .try_as_multi_bool_mut()
683 .unwrap();
684 arr.push(true);
685 } else {
686 let negate = f.negate.clone().unwrap_or_default();
687 out.flags
688 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
689 }
690 continue;
691 }
692 if is_help_arg(spec, &w) {
693 out.errors
694 .push(render_help_err(spec, &out.cmd, w.len() > 2));
695 return Ok(out);
696 }
697 }
698
699 if enable_flags && w.starts_with('-') && w.len() > 1 {
701 let short = w.chars().nth(1).unwrap();
702 if let Some(f) = binding
703 .as_ref()
704 .or_else(|| out.available_flags.get(&format!("-{short}")))
705 {
706 if w.len() > 2 {
707 input.push_front(format!("-{}", &w[2..]));
708 prefix_bindings.push_front(None);
709 grouped_flag = true;
710 }
711 if f.arg.is_some() {
712 out.flag_awaiting_value.push(Arc::clone(f));
713 } else if f.count {
714 let arr = out
715 .flags
716 .entry(Arc::clone(f))
717 .or_insert_with(|| ParseValue::MultiBool(vec![]))
718 .try_as_multi_bool_mut()
719 .unwrap();
720 arr.push(true);
721 } else {
722 let negate = f.negate.clone().unwrap_or_default();
723 out.flags
724 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
725 }
726 continue;
727 }
728 if is_help_arg(spec, &w) {
729 out.errors
730 .push(render_help_err(spec, &out.cmd, w.len() > 2));
731 return Ok(out);
732 }
733 if grouped_flag {
734 grouped_flag = false;
735 w.remove(0);
736 }
737 }
738
739 if !out.flag_awaiting_value.is_empty() {
740 let should_return = drain_pending_flag_values(
741 spec,
742 &out.cmd,
743 &mut out.errors,
744 &mut out.flags,
745 &mut out.flag_awaiting_value,
746 &mut w,
747 custom_env,
748 )?;
749 if should_return {
750 return Ok(out);
751 }
752 continue;
753 }
754
755 if let Some(arg) = next_arg {
756 if validate_choices(
757 spec,
758 &out.cmd,
759 &mut out.errors,
760 ChoiceTarget::arg(arg),
761 &w,
762 arg.choices.as_ref(),
763 custom_env,
764 )? {
765 return Ok(out);
766 }
767 if arg.var {
768 let arr = out
769 .args
770 .entry(Arc::new(arg.clone()))
771 .or_insert_with(|| ParseValue::MultiString(vec![]))
772 .try_as_multi_string_mut()
773 .unwrap();
774 arr.push(w);
775 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
776 next_arg = out.cmd.args.get(out.args.len());
777 }
778 } else {
779 out.args
780 .insert(Arc::new(arg.clone()), ParseValue::String(w));
781 next_arg = out.cmd.args.get(out.args.len());
782 }
783 continue;
784 }
785 if is_help_arg(spec, &w) {
786 out.errors
787 .push(render_help_err(spec, &out.cmd, w.len() > 2));
788 return Ok(out);
789 }
790 bail!("unexpected word: {w}");
791 }
792
793 for arg in out.cmd.args.iter().skip(out.args.len()) {
794 if arg.required && arg.default.is_empty() {
795 let has_env = arg.env.as_ref().is_some_and(|e| {
797 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
798 || std::env::var(e).is_ok()
799 });
800 if !has_env {
801 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
802 }
803 }
804 }
805
806 for flag in unique_flags(out.available_flags.values()) {
807 if out.flags.contains_key(flag) {
808 continue;
809 }
810 let has_default =
811 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
812 let has_env = flag.env.as_ref().is_some_and(|e| {
814 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
815 });
816 if flag.required && !has_default && !has_env {
817 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
818 }
819 }
820
821 for (arg, value) in &out.args {
823 if arg.var {
824 if let ParseValue::MultiString(values) = value {
825 if let Some(min) = arg.var_min {
826 if values.len() < min {
827 out.errors.push(UsageErr::VarArgTooFew {
828 name: arg.name.clone(),
829 min,
830 got: values.len(),
831 });
832 }
833 }
834 if let Some(max) = arg.var_max {
835 if values.len() > max {
836 out.errors.push(UsageErr::VarArgTooMany {
837 name: arg.name.clone(),
838 max,
839 got: values.len(),
840 });
841 }
842 }
843 }
844 }
845 }
846
847 for (flag, value) in &out.flags {
849 if flag.var {
850 let count = match value {
851 ParseValue::MultiString(values) => values.len(),
852 ParseValue::MultiBool(values) => values.len(),
853 _ => continue,
854 };
855 if let Some(min) = flag.var_min {
856 if count < min {
857 out.errors.push(UsageErr::VarFlagTooFew {
858 name: flag.name.clone(),
859 min,
860 got: count,
861 });
862 }
863 }
864 if let Some(max) = flag.var_max {
865 if count > max {
866 out.errors.push(UsageErr::VarFlagTooMany {
867 name: flag.name.clone(),
868 max,
869 got: count,
870 });
871 }
872 }
873 }
874 }
875
876 Ok(out)
877}
878
879#[cfg(feature = "docs")]
880fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
881 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
882}
883
884#[cfg(not(feature = "docs"))]
885fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
886 UsageErr::Help("help".to_string())
887}
888
889#[derive(Copy, Clone)]
890struct ChoiceTarget<'a> {
891 kind: &'a str,
892 name: &'a str,
893}
894
895impl<'a> ChoiceTarget<'a> {
896 fn arg(arg: &'a SpecArg) -> Self {
897 Self {
898 kind: "arg",
899 name: &arg.name,
900 }
901 }
902
903 fn option(flag: &'a SpecFlag) -> Self {
904 Self {
905 kind: "option",
906 name: &flag.name,
907 }
908 }
909}
910
911fn drain_pending_flag_values(
912 spec: &Spec,
913 cmd: &SpecCommand,
914 errors: &mut Vec<UsageErr>,
915 flags: &mut IndexMap<Arc<SpecFlag>, ParseValue>,
916 flag_awaiting_value: &mut Vec<Arc<SpecFlag>>,
917 word: &mut String,
918 custom_env: Option<&HashMap<String, String>>,
919) -> miette::Result<bool> {
920 while let Some(flag) = flag_awaiting_value.pop() {
921 let arg = flag.arg.as_ref().unwrap();
922 if validate_choices(
923 spec,
924 cmd,
925 errors,
926 ChoiceTarget::option(&flag),
927 word,
928 arg.choices.as_ref(),
929 custom_env,
930 )? {
931 return Ok(true);
932 }
933 let value = std::mem::take(word);
934 if flag.var {
935 let arr = flags
936 .entry(flag)
937 .or_insert_with(|| ParseValue::MultiString(vec![]))
938 .try_as_multi_string_mut()
939 .unwrap();
940 arr.push(value);
941 } else {
942 flags.insert(flag, ParseValue::String(value));
943 }
944 }
945 Ok(false)
946}
947
948fn choice_error(
949 target: ChoiceTarget<'_>,
950 value: &str,
951 choices: Option<&SpecChoices>,
952 custom_env: Option<&HashMap<String, String>>,
953) -> Option<String> {
954 let choices = choices?;
955 let values = choices.values_with_env(custom_env);
956 if values.iter().any(|choice| choice == value) {
957 return None;
958 }
959 if let Some(env) = choices.env() {
960 if values.is_empty() {
961 return Some(format!(
962 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
963 target.kind, target.name,
964 ));
965 }
966 }
967 Some(format!(
968 "Invalid choice for {} {}: {value}, expected one of {}",
969 target.kind,
970 target.name,
971 values.join(", ")
972 ))
973}
974
975fn validate_choices(
976 spec: &Spec,
977 cmd: &SpecCommand,
978 errors: &mut Vec<UsageErr>,
979 target: ChoiceTarget<'_>,
980 value: &str,
981 choices: Option<&SpecChoices>,
982 custom_env: Option<&HashMap<String, String>>,
983) -> miette::Result<bool> {
984 if is_help_arg(spec, value)
985 && choices.is_some_and(|choices| {
986 !choices
987 .values_with_env(custom_env)
988 .iter()
989 .any(|choice| choice == value)
990 })
991 {
992 errors.push(render_help_err(spec, cmd, value.len() > 2));
993 return Ok(true);
994 }
995
996 if let Some(err) = choice_error(target, value, choices, custom_env) {
997 bail!("{err}");
998 }
999 Ok(false)
1000}
1001
1002fn validate_choice_value(
1003 target: ChoiceTarget<'_>,
1004 value: &str,
1005 choices: Option<&SpecChoices>,
1006 custom_env: Option<&HashMap<String, String>>,
1007) -> miette::Result<()> {
1008 if let Some(err) = choice_error(target, value, choices, custom_env) {
1009 bail!("{err}");
1010 }
1011 Ok(())
1012}
1013
1014fn validate_choice_values(
1015 target: ChoiceTarget<'_>,
1016 values: &[String],
1017 choices: Option<&SpecChoices>,
1018 custom_env: Option<&HashMap<String, String>>,
1019) -> miette::Result<()> {
1020 for value in values {
1021 validate_choice_value(target, value, choices, custom_env)?;
1022 }
1023 Ok(())
1024}
1025
1026fn is_help_arg(spec: &Spec, w: &str) -> bool {
1027 spec.disable_help != Some(true)
1028 && (w == "--help"
1029 || w == "-h"
1030 || w == "-?"
1031 || (spec.cmd.subcommands.is_empty() && w == "help"))
1032}
1033
1034impl ParseOutput {
1035 pub fn as_env(&self) -> BTreeMap<String, String> {
1036 let mut env = BTreeMap::new();
1037 for (flag, val) in &self.flags {
1038 let key = format!("usage_{}", flag.name.to_snake_case());
1039 let val = match val {
1040 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1041 ParseValue::String(s) => s.clone(),
1042 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
1043 ParseValue::MultiString(s) => shell_words::join(s),
1044 };
1045 env.insert(key, val);
1046 }
1047 for (arg, val) in &self.args {
1048 let key = format!("usage_{}", arg.name.to_snake_case());
1049 env.insert(key, val.to_string());
1050 }
1051 env
1052 }
1053}
1054
1055impl Display for ParseValue {
1056 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1057 match self {
1058 ParseValue::Bool(b) => write!(f, "{b}"),
1059 ParseValue::String(s) => write!(f, "{s}"),
1060 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
1061 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
1062 }
1063 }
1064}
1065
1066impl Debug for ParseOutput {
1067 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1068 f.debug_struct("ParseOutput")
1069 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
1070 .field(
1071 "args",
1072 &self
1073 .args
1074 .iter()
1075 .map(|(a, w)| format!("{}: {w}", a.name))
1076 .collect_vec(),
1077 )
1078 .field(
1079 "available_flags",
1080 &self
1081 .available_flags
1082 .iter()
1083 .map(|(f, w)| format!("{f}: {w}"))
1084 .collect_vec(),
1085 )
1086 .field(
1087 "flags",
1088 &self
1089 .flags
1090 .iter()
1091 .map(|(f, w)| format!("{}: {w}", f.name))
1092 .collect_vec(),
1093 )
1094 .field("flag_awaiting_value", &self.flag_awaiting_value)
1095 .field("errors", &self.errors)
1096 .finish()
1097 }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 fn input(words: &[&str]) -> Vec<String> {
1105 words.iter().map(|word| (*word).to_string()).collect()
1106 }
1107
1108 fn spec_with_arg(arg: SpecArg) -> Spec {
1109 let cmd = SpecCommand::builder().name("test").arg(arg).build();
1110 Spec {
1111 name: "test".to_string(),
1112 bin: "test".to_string(),
1113 cmd,
1114 ..Default::default()
1115 }
1116 }
1117
1118 fn spec_with_flag(flag: SpecFlag) -> Spec {
1119 let cmd = SpecCommand::builder().name("test").flag(flag).build();
1120 Spec {
1121 name: "test".to_string(),
1122 bin: "test".to_string(),
1123 cmd,
1124 ..Default::default()
1125 }
1126 }
1127
1128 fn parse_with_env(
1129 spec: &Spec,
1130 words: &[&str],
1131 env: &[(&str, &str)],
1132 ) -> Result<ParseOutput, miette::Error> {
1133 let env = env
1134 .iter()
1135 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1136 .collect();
1137 Parser::new(spec).with_env(env).parse(&input(words))
1138 }
1139
1140 fn first_string_value(parsed: &ParseOutput) -> &str {
1141 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
1142 return value;
1143 }
1144 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
1145 return value;
1146 }
1147 panic!("expected first parsed value to be ParseValue::String");
1148 }
1149
1150 fn flag_string_value<'a>(parsed: &'a ParseOutput, name: &str) -> &'a str {
1151 let flag = parsed
1152 .flags
1153 .keys()
1154 .find(|flag| flag.name == name)
1155 .unwrap_or_else(|| panic!("expected flag {name}"));
1156 let value = parsed
1157 .flags
1158 .get(flag)
1159 .unwrap_or_else(|| panic!("expected value for flag {name}"));
1160 match value {
1161 ParseValue::String(value) => value,
1162 _ => panic!("expected flag {name} to be ParseValue::String"),
1163 }
1164 }
1165
1166 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
1167 let err = result.expect_err("expected parser error");
1168 assert_eq!(format!("{err}"), expected);
1169 }
1170
1171 #[cfg(feature = "unstable_choices_env")]
1172 fn spec_arg_choices_env(key: &str) -> Spec {
1173 spec_with_arg(
1174 SpecArg::builder()
1175 .name("env")
1176 .choices_env(key)
1177 .required(false)
1178 .build(),
1179 )
1180 }
1181
1182 #[cfg(feature = "unstable_choices_env")]
1183 fn spec_flag_choices_env(key: &str) -> Spec {
1184 spec_with_flag(
1185 SpecFlag::builder()
1186 .long("env")
1187 .arg(SpecArg::builder().name("env").choices_env(key).build())
1188 .build(),
1189 )
1190 }
1191
1192 #[test]
1193 fn test_parse() {
1194 let cmd = SpecCommand::builder()
1195 .name("test")
1196 .arg(SpecArg::builder().name("arg").build())
1197 .flag(SpecFlag::builder().long("flag").build())
1198 .build();
1199 let spec = Spec {
1200 name: "test".to_string(),
1201 bin: "test".to_string(),
1202 cmd,
1203 ..Default::default()
1204 };
1205 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
1206 let parsed = parse(&spec, &input).unwrap();
1207 assert_eq!(parsed.cmds.len(), 1);
1208 assert_eq!(parsed.cmds[0].name, "test");
1209 assert_eq!(parsed.args.len(), 1);
1210 assert_eq!(parsed.flags.len(), 1);
1211 assert_eq!(parsed.available_flags.len(), 1);
1212 }
1213
1214 #[test]
1215 fn test_as_env() {
1216 let cmd = SpecCommand::builder()
1217 .name("test")
1218 .arg(SpecArg::builder().name("arg").build())
1219 .flag(SpecFlag::builder().long("flag").build())
1220 .flag(
1221 SpecFlag::builder()
1222 .long("force")
1223 .negate("--no-force")
1224 .build(),
1225 )
1226 .build();
1227 let spec = Spec {
1228 name: "test".to_string(),
1229 bin: "test".to_string(),
1230 cmd,
1231 ..Default::default()
1232 };
1233 let input = vec![
1234 "test".to_string(),
1235 "--flag".to_string(),
1236 "--no-force".to_string(),
1237 ];
1238 let parsed = parse(&spec, &input).unwrap();
1239 let env = parsed.as_env();
1240 assert_eq!(env.len(), 2);
1241 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1242 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1243 }
1244
1245 #[test]
1246 fn test_arg_env_var() {
1247 let cmd = SpecCommand::builder()
1248 .name("test")
1249 .arg(
1250 SpecArg::builder()
1251 .name("input")
1252 .env("TEST_ARG_INPUT")
1253 .required(true)
1254 .build(),
1255 )
1256 .build();
1257 let spec = Spec {
1258 name: "test".to_string(),
1259 bin: "test".to_string(),
1260 cmd,
1261 ..Default::default()
1262 };
1263
1264 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1266
1267 let input = vec!["test".to_string()];
1268 let parsed = parse(&spec, &input).unwrap();
1269
1270 assert_eq!(parsed.args.len(), 1);
1271 let arg = parsed.args.keys().next().unwrap();
1272 assert_eq!(arg.name, "input");
1273 let value = parsed.args.values().next().unwrap();
1274 assert_eq!(value.to_string(), "test_file.txt");
1275
1276 std::env::remove_var("TEST_ARG_INPUT");
1278 }
1279
1280 #[test]
1281 fn test_flag_env_var_with_arg() {
1282 let cmd = SpecCommand::builder()
1283 .name("test")
1284 .flag(
1285 SpecFlag::builder()
1286 .long("output")
1287 .env("TEST_FLAG_OUTPUT")
1288 .arg(SpecArg::builder().name("file").build())
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
1299 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1301
1302 let input = vec!["test".to_string()];
1303 let parsed = parse(&spec, &input).unwrap();
1304
1305 assert_eq!(parsed.flags.len(), 1);
1306 let flag = parsed.flags.keys().next().unwrap();
1307 assert_eq!(flag.name, "output");
1308 let value = parsed.flags.values().next().unwrap();
1309 assert_eq!(value.to_string(), "output.txt");
1310
1311 std::env::remove_var("TEST_FLAG_OUTPUT");
1313 }
1314
1315 #[test]
1316 fn test_flag_env_var_boolean() {
1317 let cmd = SpecCommand::builder()
1318 .name("test")
1319 .flag(
1320 SpecFlag::builder()
1321 .long("verbose")
1322 .env("TEST_FLAG_VERBOSE")
1323 .build(),
1324 )
1325 .build();
1326 let spec = Spec {
1327 name: "test".to_string(),
1328 bin: "test".to_string(),
1329 cmd,
1330 ..Default::default()
1331 };
1332
1333 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1335
1336 let input = vec!["test".to_string()];
1337 let parsed = parse(&spec, &input).unwrap();
1338
1339 assert_eq!(parsed.flags.len(), 1);
1340 let flag = parsed.flags.keys().next().unwrap();
1341 assert_eq!(flag.name, "verbose");
1342 let value = parsed.flags.values().next().unwrap();
1343 assert_eq!(value.to_string(), "true");
1344
1345 std::env::remove_var("TEST_FLAG_VERBOSE");
1347 }
1348
1349 #[test]
1350 fn test_env_var_precedence() {
1351 let cmd = SpecCommand::builder()
1353 .name("test")
1354 .arg(
1355 SpecArg::builder()
1356 .name("input")
1357 .env("TEST_PRECEDENCE_INPUT")
1358 .required(true)
1359 .build(),
1360 )
1361 .build();
1362 let spec = Spec {
1363 name: "test".to_string(),
1364 bin: "test".to_string(),
1365 cmd,
1366 ..Default::default()
1367 };
1368
1369 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1371
1372 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1373 let parsed = parse(&spec, &input).unwrap();
1374
1375 assert_eq!(parsed.args.len(), 1);
1376 let value = parsed.args.values().next().unwrap();
1377 assert_eq!(value.to_string(), "cli_file.txt");
1379
1380 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1382 }
1383
1384 #[test]
1385 fn test_flag_var_true_with_single_default() {
1386 let cmd = SpecCommand::builder()
1388 .name("test")
1389 .flag(
1390 SpecFlag::builder()
1391 .long("foo")
1392 .var(true)
1393 .arg(SpecArg::builder().name("foo").build())
1394 .default_value("bar")
1395 .build(),
1396 )
1397 .build();
1398 let spec = Spec {
1399 name: "test".to_string(),
1400 bin: "test".to_string(),
1401 cmd,
1402 ..Default::default()
1403 };
1404
1405 let input = vec!["test".to_string()];
1407 let parsed = parse(&spec, &input).unwrap();
1408
1409 assert_eq!(parsed.flags.len(), 1);
1410 let flag = parsed.flags.keys().next().unwrap();
1411 assert_eq!(flag.name, "foo");
1412 let value = parsed.flags.values().next().unwrap();
1413 match value {
1415 ParseValue::MultiString(v) => {
1416 assert_eq!(v.len(), 1);
1417 assert_eq!(v[0], "bar");
1418 }
1419 _ => panic!("Expected MultiString, got {:?}", value),
1420 }
1421 }
1422
1423 #[test]
1424 fn test_flag_var_true_with_multiple_defaults() {
1425 let cmd = SpecCommand::builder()
1427 .name("test")
1428 .flag(
1429 SpecFlag::builder()
1430 .long("foo")
1431 .var(true)
1432 .arg(SpecArg::builder().name("foo").build())
1433 .default_values(["xyz", "bar"])
1434 .build(),
1435 )
1436 .build();
1437 let spec = Spec {
1438 name: "test".to_string(),
1439 bin: "test".to_string(),
1440 cmd,
1441 ..Default::default()
1442 };
1443
1444 let input = vec!["test".to_string()];
1446 let parsed = parse(&spec, &input).unwrap();
1447
1448 assert_eq!(parsed.flags.len(), 1);
1449 let value = parsed.flags.values().next().unwrap();
1450 match value {
1452 ParseValue::MultiString(v) => {
1453 assert_eq!(v.len(), 2);
1454 assert_eq!(v[0], "xyz");
1455 assert_eq!(v[1], "bar");
1456 }
1457 _ => panic!("Expected MultiString, got {:?}", value),
1458 }
1459 }
1460
1461 #[test]
1462 fn test_flag_var_false_with_default_remains_string() {
1463 let cmd = SpecCommand::builder()
1465 .name("test")
1466 .flag(
1467 SpecFlag::builder()
1468 .long("foo")
1469 .var(false) .arg(SpecArg::builder().name("foo").build())
1471 .default_value("bar")
1472 .build(),
1473 )
1474 .build();
1475 let spec = Spec {
1476 name: "test".to_string(),
1477 bin: "test".to_string(),
1478 cmd,
1479 ..Default::default()
1480 };
1481
1482 let input = vec!["test".to_string()];
1484 let parsed = parse(&spec, &input).unwrap();
1485
1486 assert_eq!(parsed.flags.len(), 1);
1487 let value = parsed.flags.values().next().unwrap();
1488 match value {
1490 ParseValue::String(s) => {
1491 assert_eq!(s, "bar");
1492 }
1493 _ => panic!("Expected String, got {:?}", value),
1494 }
1495 }
1496
1497 #[test]
1498 fn test_arg_var_true_with_single_default() {
1499 let cmd = SpecCommand::builder()
1501 .name("test")
1502 .arg(
1503 SpecArg::builder()
1504 .name("files")
1505 .var(true)
1506 .default_value("default.txt")
1507 .required(false)
1508 .build(),
1509 )
1510 .build();
1511 let spec = Spec {
1512 name: "test".to_string(),
1513 bin: "test".to_string(),
1514 cmd,
1515 ..Default::default()
1516 };
1517
1518 let input = vec!["test".to_string()];
1520 let parsed = parse(&spec, &input).unwrap();
1521
1522 assert_eq!(parsed.args.len(), 1);
1523 let value = parsed.args.values().next().unwrap();
1524 match value {
1526 ParseValue::MultiString(v) => {
1527 assert_eq!(v.len(), 1);
1528 assert_eq!(v[0], "default.txt");
1529 }
1530 _ => panic!("Expected MultiString, got {:?}", value),
1531 }
1532 }
1533
1534 #[test]
1535 fn test_arg_var_true_with_multiple_defaults() {
1536 let cmd = SpecCommand::builder()
1538 .name("test")
1539 .arg(
1540 SpecArg::builder()
1541 .name("files")
1542 .var(true)
1543 .default_values(["file1.txt", "file2.txt"])
1544 .required(false)
1545 .build(),
1546 )
1547 .build();
1548 let spec = Spec {
1549 name: "test".to_string(),
1550 bin: "test".to_string(),
1551 cmd,
1552 ..Default::default()
1553 };
1554
1555 let input = vec!["test".to_string()];
1557 let parsed = parse(&spec, &input).unwrap();
1558
1559 assert_eq!(parsed.args.len(), 1);
1560 let value = parsed.args.values().next().unwrap();
1561 match value {
1563 ParseValue::MultiString(v) => {
1564 assert_eq!(v.len(), 2);
1565 assert_eq!(v[0], "file1.txt");
1566 assert_eq!(v[1], "file2.txt");
1567 }
1568 _ => panic!("Expected MultiString, got {:?}", value),
1569 }
1570 }
1571
1572 #[test]
1573 fn test_arg_var_false_with_default_remains_string() {
1574 let cmd = SpecCommand::builder()
1576 .name("test")
1577 .arg(
1578 SpecArg::builder()
1579 .name("file")
1580 .var(false)
1581 .default_value("default.txt")
1582 .required(false)
1583 .build(),
1584 )
1585 .build();
1586 let spec = Spec {
1587 name: "test".to_string(),
1588 bin: "test".to_string(),
1589 cmd,
1590 ..Default::default()
1591 };
1592
1593 let input = vec!["test".to_string()];
1595 let parsed = parse(&spec, &input).unwrap();
1596
1597 assert_eq!(parsed.args.len(), 1);
1598 let value = parsed.args.values().next().unwrap();
1599 match value {
1601 ParseValue::String(s) => {
1602 assert_eq!(s, "default.txt");
1603 }
1604 _ => panic!("Expected String, got {:?}", value),
1605 }
1606 }
1607
1608 #[test]
1609 fn test_scalar_defaults_validate_only_first_default_choice() {
1610 let specs = [
1611 spec_with_arg(
1612 SpecArg::builder()
1613 .name("env")
1614 .var(false)
1615 .default_values(["dev", "prod"])
1616 .choices(["dev"])
1617 .required(false)
1618 .build(),
1619 ),
1620 spec_with_flag(
1621 SpecFlag::builder()
1622 .long("env")
1623 .arg(
1624 SpecArg::builder()
1625 .name("env")
1626 .default_values(["dev", "prod"])
1627 .choices(["dev"])
1628 .build(),
1629 )
1630 .build(),
1631 ),
1632 ];
1633
1634 for spec in specs {
1635 let parsed = parse(&spec, &input(&["test"])).unwrap();
1636 assert_eq!(first_string_value(&parsed), "dev");
1637 }
1638 }
1639
1640 #[test]
1641 fn test_default_subcommand() {
1642 let run_cmd = SpecCommand::builder()
1644 .name("run")
1645 .arg(SpecArg::builder().name("task").build())
1646 .build();
1647 let mut cmd = SpecCommand::builder().name("test").build();
1648 cmd.subcommands.insert("run".to_string(), run_cmd);
1649
1650 let spec = Spec {
1651 name: "test".to_string(),
1652 bin: "test".to_string(),
1653 cmd,
1654 default_subcommand: Some("run".to_string()),
1655 ..Default::default()
1656 };
1657
1658 let input = vec!["test".to_string(), "mytask".to_string()];
1660 let parsed = parse(&spec, &input).unwrap();
1661
1662 assert_eq!(parsed.cmds.len(), 2);
1664 assert_eq!(parsed.cmds[1].name, "run");
1665
1666 assert_eq!(parsed.args.len(), 1);
1668 let arg = parsed.args.keys().next().unwrap();
1669 assert_eq!(arg.name, "task");
1670 let value = parsed.args.values().next().unwrap();
1671 assert_eq!(value.to_string(), "mytask");
1672 }
1673
1674 #[test]
1675 fn test_default_subcommand_explicit_still_works() {
1676 let run_cmd = SpecCommand::builder()
1678 .name("run")
1679 .arg(SpecArg::builder().name("task").build())
1680 .build();
1681 let other_cmd = SpecCommand::builder()
1682 .name("other")
1683 .arg(SpecArg::builder().name("other_arg").build())
1684 .build();
1685 let mut cmd = SpecCommand::builder().name("test").build();
1686 cmd.subcommands.insert("run".to_string(), run_cmd);
1687 cmd.subcommands.insert("other".to_string(), other_cmd);
1688
1689 let spec = Spec {
1690 name: "test".to_string(),
1691 bin: "test".to_string(),
1692 cmd,
1693 default_subcommand: Some("run".to_string()),
1694 ..Default::default()
1695 };
1696
1697 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1699 let parsed = parse(&spec, &input).unwrap();
1700
1701 assert_eq!(parsed.cmds.len(), 2);
1703 assert_eq!(parsed.cmds[1].name, "other");
1704 }
1705
1706 #[test]
1707 fn test_default_subcommand_with_nested_subcommands() {
1708 let say_cmd = SpecCommand::builder()
1712 .name("say")
1713 .arg(SpecArg::builder().name("name").build())
1714 .build();
1715 let mut run_cmd = SpecCommand::builder().name("run").build();
1716 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1717
1718 let mut cmd = SpecCommand::builder().name("test").build();
1719 cmd.subcommands.insert("run".to_string(), run_cmd);
1720
1721 let spec = Spec {
1722 name: "test".to_string(),
1723 bin: "test".to_string(),
1724 cmd,
1725 default_subcommand: Some("run".to_string()),
1726 ..Default::default()
1727 };
1728
1729 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1731 let parsed = parse(&spec, &input).unwrap();
1732
1733 assert_eq!(parsed.cmds.len(), 3);
1735 assert_eq!(parsed.cmds[0].name, "test");
1736 assert_eq!(parsed.cmds[1].name, "run");
1737 assert_eq!(parsed.cmds[2].name, "say");
1738
1739 assert_eq!(parsed.args.len(), 1);
1741 let arg = parsed.args.keys().next().unwrap();
1742 assert_eq!(arg.name, "name");
1743 let value = parsed.args.values().next().unwrap();
1744 assert_eq!(value.to_string(), "hello");
1745 }
1746
1747 fn mounted_global_flag_spec() -> Spec {
1755 let task_cmd = SpecCommand::builder()
1756 .name("sample:run")
1757 .arg(
1758 SpecArg::builder()
1759 .name("profile")
1760 .choices(["alpha", "beta", "gamma"])
1761 .build(),
1762 )
1763 .build();
1764 let mut run_cmd = SpecCommand::builder()
1766 .name("run")
1767 .flag(
1768 SpecFlag::builder()
1769 .name("cd")
1770 .short('C')
1771 .long("cd")
1772 .arg(SpecArg::builder().name("dir").build())
1773 .global(false)
1774 .build(),
1775 )
1776 .build();
1777 run_cmd
1778 .subcommands
1779 .insert("sample:run".to_string(), task_cmd);
1780
1781 let mut cmd = SpecCommand::builder()
1782 .name("test")
1783 .flag(
1784 SpecFlag::builder()
1785 .name("cd")
1786 .short('C')
1787 .long("cd")
1788 .arg(SpecArg::builder().name("dir").build())
1789 .global(true)
1790 .build(),
1791 )
1792 .build();
1793 cmd.subcommands.insert("run".to_string(), run_cmd);
1794
1795 Spec {
1796 name: "test".to_string(),
1797 bin: "test".to_string(),
1798 cmd,
1799 ..Default::default()
1800 }
1801 }
1802
1803 #[test]
1804 fn test_prefix_global_flag_does_not_pollute_choices() {
1805 let spec = mounted_global_flag_spec();
1812
1813 for words in [
1816 &["test", "-C", "/tmp", "run", "sample:run"][..],
1817 &["test", "--cd=/tmp", "run", "sample:run"][..],
1819 ] {
1820 let parsed = parse_partial(&spec, &input(words)).unwrap();
1821 assert_eq!(
1822 parsed
1823 .cmds
1824 .iter()
1825 .map(|c| c.name.as_str())
1826 .collect::<Vec<_>>(),
1827 vec!["test", "run", "sample:run"],
1828 );
1829 assert!(
1831 parsed.args.is_empty(),
1832 "args should be empty, got {:?}",
1833 parsed.args
1834 );
1835
1836 let cd = parsed
1839 .available_flags
1840 .get("--cd")
1841 .expect("--cd should remain available after descending into the subcommand");
1842 assert!(cd.global, "--cd must stay global after descent");
1843 assert!(
1844 parsed.available_flags.get("-C").is_some_and(|f| f.global),
1845 "-C must stay global after descent",
1846 );
1847
1848 assert_eq!(
1852 parsed.as_env().get("usage_cd").map(String::as_str),
1853 Some("/tmp"),
1854 "global flag value must survive in as_env(), got {:?}",
1855 parsed.as_env(),
1856 );
1857 }
1858
1859 let parsed = parse_partial(
1861 &spec,
1862 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
1863 )
1864 .unwrap();
1865 assert_eq!(parsed.args.len(), 1);
1866 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1867
1868 assert_parse_err(
1870 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
1871 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1872 );
1873 }
1874
1875 fn mounted_orphan_short_spec() -> Spec {
1880 let task_cmd = SpecCommand::builder()
1881 .name("sample:run")
1882 .arg(
1883 SpecArg::builder()
1884 .name("profile")
1885 .choices(["alpha", "beta", "gamma"])
1886 .build(),
1887 )
1888 .build();
1889 let mut run_cmd = SpecCommand::builder()
1892 .name("run")
1893 .flag(
1894 SpecFlag::builder()
1895 .name("raw")
1896 .short('r')
1897 .long("raw")
1898 .global(false)
1899 .build(),
1900 )
1901 .flag(
1902 SpecFlag::builder()
1903 .name("force")
1904 .short('f')
1905 .long("force")
1906 .global(false)
1907 .build(),
1908 )
1909 .build();
1910 run_cmd
1911 .subcommands
1912 .insert("sample:run".to_string(), task_cmd);
1913
1914 let mut cmd = SpecCommand::builder()
1916 .name("test")
1917 .flag(
1918 SpecFlag::builder()
1919 .name("raw")
1920 .long("raw")
1921 .global(true)
1922 .build(),
1923 )
1924 .build();
1925 cmd.subcommands.insert("run".to_string(), run_cmd);
1926
1927 Spec {
1928 name: "test".to_string(),
1929 bin: "test".to_string(),
1930 cmd,
1931 ..Default::default()
1932 }
1933 }
1934
1935 #[test]
1936 fn test_orphan_short_alias_survives_merge() {
1937 let spec = mounted_orphan_short_spec();
1943
1944 let parsed = parse_partial(&spec, &input(&["test", "run", "-r", "sample:run"])).unwrap();
1945 assert_eq!(
1946 parsed
1947 .cmds
1948 .iter()
1949 .map(|c| c.name.as_str())
1950 .collect::<Vec<_>>(),
1951 vec!["test", "run", "sample:run"],
1952 );
1953
1954 assert!(
1957 parsed.available_flags.get("-r").is_some_and(|f| f.global),
1958 "-r must be merged onto the inherited global flag and stay global after descent",
1959 );
1960 assert!(
1961 parsed
1962 .available_flags
1963 .get("--raw")
1964 .is_some_and(|f| f.global),
1965 "--raw must stay global after descent",
1966 );
1967
1968 assert!(
1970 parsed.args.is_empty(),
1971 "args should be empty, got {:?}",
1972 parsed.args
1973 );
1974
1975 assert_eq!(
1977 parsed.as_env().get("usage_raw").map(String::as_str),
1978 Some("true"),
1979 "merged short's value must survive in as_env(), got {:?}",
1980 parsed.as_env(),
1981 );
1982
1983 assert!(
1986 parsed.available_flags.get("-f").is_none(),
1987 "purely-local -f must not be promoted onto a global",
1988 );
1989 assert!(
1990 parsed.available_flags.get("--force").is_none(),
1991 "purely-local --force must not be promoted onto a global",
1992 );
1993
1994 let parsed =
1996 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "alpha"])).unwrap();
1997 assert_eq!(parsed.args.len(), 1);
1998 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1999
2000 assert_parse_err(
2002 parse_partial(&spec, &input(&["test", "run", "-r", "sample:run", "wrong"])),
2003 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
2004 );
2005 }
2006
2007 #[test]
2008 fn test_orphan_short_does_not_clobber_unrelated_global() {
2009 let run_cmd = SpecCommand::builder()
2014 .name("run")
2015 .flag(
2016 SpecFlag::builder()
2017 .name("raw")
2018 .short('r')
2019 .long("raw")
2020 .global(false)
2021 .build(),
2022 )
2023 .build();
2024 let mut cmd = SpecCommand::builder()
2025 .name("test")
2026 .flag(
2027 SpecFlag::builder()
2028 .name("raw")
2029 .long("raw")
2030 .global(true)
2031 .build(),
2032 )
2033 .flag(
2034 SpecFlag::builder()
2035 .name("restrict")
2036 .short('r')
2037 .long("restrict")
2038 .global(true)
2039 .build(),
2040 )
2041 .build();
2042 cmd.subcommands.insert("run".to_string(), run_cmd);
2043 let spec = Spec {
2044 name: "test".to_string(),
2045 bin: "test".to_string(),
2046 cmd,
2047 ..Default::default()
2048 };
2049
2050 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2051 assert_eq!(
2053 parsed.available_flags.get("-r").map(|f| f.name.as_str()),
2054 Some("restrict"),
2055 "-r must remain owned by the unrelated global it already belonged to",
2056 );
2057 assert!(parsed
2059 .available_flags
2060 .get("--raw")
2061 .is_some_and(|f| f.global));
2062 assert!(parsed
2063 .available_flags
2064 .get("--restrict")
2065 .is_some_and(|f| f.global));
2066 }
2067
2068 fn mounted_task_flag_spec() -> Spec {
2075 let mut task_cmd = SpecCommand::builder()
2076 .name("mytask")
2077 .flag(
2078 SpecFlag::builder()
2079 .name("env")
2080 .long("env")
2081 .arg(
2082 SpecArg::builder()
2083 .name("name")
2084 .choices(["dev", "stage", "prod"])
2085 .build(),
2086 )
2087 .global(false)
2088 .build(),
2089 )
2090 .flag(
2091 SpecFlag::builder()
2092 .name("bump")
2093 .long("bump")
2094 .arg(
2095 SpecArg::builder()
2096 .name("type")
2097 .choices(["auto", "major"])
2098 .build(),
2099 )
2100 .global(false)
2101 .build(),
2102 )
2103 .build();
2104 task_cmd.mounted = true;
2105
2106 let mut run_cmd = SpecCommand::builder()
2107 .name("run")
2108 .flag(
2109 SpecFlag::builder()
2110 .name("force")
2111 .short('f')
2112 .long("force")
2113 .global(false)
2114 .build(),
2115 )
2116 .build();
2117 run_cmd.subcommands.insert("mytask".to_string(), task_cmd);
2118
2119 let mut cmd = SpecCommand::builder()
2120 .name("test")
2121 .flag(
2122 SpecFlag::builder()
2123 .name("env")
2124 .short('E')
2125 .long("env")
2126 .arg(SpecArg::builder().name("ENV").build())
2127 .global(true)
2128 .build(),
2129 )
2130 .flag(
2131 SpecFlag::builder()
2132 .name("silent")
2133 .long("silent")
2134 .global(true)
2135 .build(),
2136 )
2137 .build();
2138 cmd.subcommands.insert("run".to_string(), run_cmd);
2139
2140 Spec {
2141 name: "test".to_string(),
2142 bin: "test".to_string(),
2143 cmd,
2144 ..Default::default()
2145 }
2146 }
2147
2148 #[test]
2149 fn test_mount_boundary_does_not_apply_inside_the_mounted_tree() {
2150 let deep = SpecCommand::builder().name("deep").build();
2156 let mut sub = SpecCommand::builder()
2157 .name("sub")
2158 .flag(
2160 SpecFlag::builder()
2161 .name("cd")
2162 .short('C')
2163 .long("cd")
2164 .arg(SpecArg::builder().name("dir").build())
2165 .global(false)
2166 .build(),
2167 )
2168 .build();
2169 sub.subcommands.insert("deep".to_string(), deep);
2170 let mut task = SpecCommand::builder()
2171 .name("task")
2172 .flag(
2173 SpecFlag::builder()
2174 .name("cd")
2175 .short('C')
2176 .long("cd")
2177 .arg(SpecArg::builder().name("dir").build())
2178 .global(true)
2179 .build(),
2180 )
2181 .build();
2182 task.subcommands.insert("sub".to_string(), sub);
2183 task.mark_mounted();
2184
2185 let mut run_cmd = SpecCommand::builder().name("run").build();
2186 run_cmd.subcommands.insert("task".to_string(), task);
2187 let mut cmd = SpecCommand::builder().name("test").build();
2188 cmd.subcommands.insert("run".to_string(), run_cmd);
2189 let spec = Spec {
2190 name: "test".to_string(),
2191 bin: "test".to_string(),
2192 cmd,
2193 ..Default::default()
2194 };
2195
2196 let parsed = parse_partial(&spec, &input(&["test", "run", "task", "sub", "deep"])).unwrap();
2197 assert!(
2198 parsed.available_flags.get("--cd").is_some_and(|f| f.global),
2199 "the mounted program's own global must survive descents inside the mounted tree",
2200 );
2201 assert!(
2202 parsed.completion_flags().contains_key("--cd"),
2203 "and must still be offered there: it belongs to the mounted program",
2204 );
2205 assert!(
2206 parsed.completion_flags().contains_key("-C"),
2207 "including the short the nested command re-declared",
2208 );
2209 }
2210
2211 #[test]
2212 fn test_mount_flags_merged_into_the_mounting_cmd_are_offered() {
2213 let mut task = SpecCommand::builder()
2218 .name("task")
2219 .flag(
2220 SpecFlag::builder()
2221 .name("bump")
2222 .long("bump")
2223 .global(false)
2224 .build(),
2225 )
2226 .build();
2227 task.mark_mounted();
2228
2229 let mut run_cmd = SpecCommand::builder().name("run").build();
2230 run_cmd.subcommands.insert("task".to_string(), task);
2231 run_cmd.flags = vec![
2233 SpecFlag::builder()
2234 .name("tglobal")
2235 .long("tglobal")
2236 .global(true)
2237 .build(),
2238 SpecFlag::builder()
2239 .name("tlocal")
2240 .long("tlocal")
2241 .global(false)
2242 .build(),
2243 ];
2244 run_cmd.flags_from_mount = true;
2245
2246 let mut cmd = SpecCommand::builder()
2247 .name("test")
2248 .flag(
2249 SpecFlag::builder()
2250 .name("silent")
2251 .long("silent")
2252 .global(true)
2253 .build(),
2254 )
2255 .build();
2256 cmd.subcommands.insert("run".to_string(), run_cmd);
2257 let spec = Spec {
2258 name: "test".to_string(),
2259 bin: "test".to_string(),
2260 cmd,
2261 ..Default::default()
2262 };
2263
2264 let parsed = parse_partial(&spec, &input(&["test", "run", "task"])).unwrap();
2265 assert_eq!(
2266 parsed.completion_flags().keys().collect::<Vec<_>>(),
2267 vec!["--bump", "--tglobal"],
2268 "the mounted spec's root global belongs to the mounted program; the mounting CLI's \
2269 `--silent` does not, and the mount's non-global root flag is not inherited",
2270 );
2271 }
2272
2273 #[test]
2274 fn test_mounted_cmd_does_not_offer_mounting_cli_globals() {
2275 let spec = mounted_task_flag_spec();
2280 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask"])).unwrap();
2281
2282 assert!(parsed.available_flags.contains_key("--silent"));
2284 assert!(parsed.available_flags.contains_key("-E"));
2285 assert_eq!(
2287 parsed.completion_flags().keys().collect::<Vec<_>>(),
2288 vec!["--bump", "--env"],
2289 "only the mounted command's own flags may be offered",
2290 );
2291
2292 assert!(!parsed.available_flags.contains_key("--force"));
2294 }
2295
2296 #[test]
2297 fn test_mounted_cmd_flag_wins_over_inherited_global() {
2298 let spec = mounted_task_flag_spec();
2301 let parsed = parse_partial(&spec, &input(&["test", "run", "mytask", "--env"])).unwrap();
2302
2303 let awaiting = parsed
2304 .flag_awaiting_value
2305 .first()
2306 .expect("--env should await a value");
2307 assert_eq!(
2308 awaiting
2309 .arg
2310 .as_ref()
2311 .and_then(|a| a.choices.as_ref())
2312 .map(|c| c.choices.clone()),
2313 Some(vec![
2314 "dev".to_string(),
2315 "stage".to_string(),
2316 "prod".to_string()
2317 ]),
2318 "the mounted command's own --env must win over the inherited global",
2319 );
2320
2321 let parsed =
2324 parse_partial(&spec, &input(&["test", "-E", "anything", "run", "mytask"])).unwrap();
2325 assert!(
2326 parsed.args.is_empty(),
2327 "prefix global tokens must not be consumed as positionals, got {:?}",
2328 parsed.args
2329 );
2330 assert_eq!(
2331 parsed.as_env().get("usage_env").map(String::as_str),
2332 Some("anything"),
2333 );
2334 }
2335
2336 #[test]
2337 fn test_prefix_flag_keeps_the_flag_it_was_read_as() {
2338 let spec = mounted_task_flag_spec();
2343 let parsed = parse_partial(
2344 &spec,
2345 &input(&["test", "--env", "not-a-task-choice", "run", "mytask"]),
2346 )
2347 .unwrap();
2348 assert!(
2349 parsed.errors.is_empty(),
2350 "prefix global value must not be validated against the mounted flag: {:?}",
2351 parsed
2352 .errors
2353 .iter()
2354 .map(|e| e.to_string())
2355 .collect::<Vec<_>>(),
2356 );
2357 assert_eq!(
2358 parsed.as_env().get("usage_env").map(String::as_str),
2359 Some("not-a-task-choice"),
2360 );
2361
2362 let parsed = parse_partial(
2364 &spec,
2365 &input(&["test", "--env=not-a-task-choice", "run", "mytask"]),
2366 )
2367 .unwrap();
2368 assert!(parsed.errors.is_empty());
2369 assert_eq!(
2370 parsed.as_env().get("usage_env").map(String::as_str),
2371 Some("not-a-task-choice"),
2372 );
2373
2374 let parsed = parse_partial(
2377 &spec,
2378 &input(&["test", "--env", "prod", "run", "mytask", "--env"]),
2379 )
2380 .unwrap();
2381 let awaiting = parsed
2382 .flag_awaiting_value
2383 .first()
2384 .expect("--env should await a value");
2385 assert_eq!(
2386 awaiting
2387 .arg
2388 .as_ref()
2389 .and_then(|a| a.choices.as_ref())
2390 .map(|c| c.choices.clone()),
2391 Some(vec![
2392 "dev".to_string(),
2393 "stage".to_string(),
2394 "prod".to_string()
2395 ]),
2396 "the mounted command's --env must own the name after the mounted command",
2397 );
2398 }
2399
2400 #[test]
2401 fn test_non_global_flag_does_not_hide_subcommand() {
2402 let spec = mounted_task_flag_spec();
2406
2407 for words in [
2408 &["test", "run", "--force", "mytask"][..],
2410 &["test", "run", "-f", "mytask"][..],
2411 &["test", "-E", "prod", "run", "--force", "mytask"][..],
2413 ] {
2414 let parsed = parse_partial(&spec, &input(words)).unwrap();
2415 assert_eq!(
2416 parsed
2417 .cmds
2418 .iter()
2419 .map(|c| c.name.as_str())
2420 .collect::<Vec<_>>(),
2421 vec!["test", "run", "mytask"],
2422 "{words:?} should descend into the mounted command",
2423 );
2424 assert!(
2425 parsed.args.is_empty(),
2426 "{words:?} should not consume a positional, got {:?}",
2427 parsed.args,
2428 );
2429 assert_eq!(
2430 parsed.as_env().get("usage_force").map(String::as_str),
2431 Some("true"),
2432 "the non-global flag must still be recorded for {words:?}",
2433 );
2434 }
2435
2436 let mut run_cmd = SpecCommand::builder()
2439 .name("run")
2440 .flag(
2441 SpecFlag::builder()
2442 .name("output")
2443 .short('o')
2444 .long("output")
2445 .arg(SpecArg::builder().name("mode").build())
2446 .global(false)
2447 .build(),
2448 )
2449 .build();
2450 run_cmd.subcommands.insert(
2451 "task".to_string(),
2452 SpecCommand::builder().name("task").build(),
2453 );
2454 let mut cmd = SpecCommand::builder().name("test").build();
2455 cmd.subcommands.insert("run".to_string(), run_cmd);
2456 let spec = Spec {
2457 name: "test".to_string(),
2458 bin: "test".to_string(),
2459 cmd,
2460 ..Default::default()
2461 };
2462
2463 let parsed =
2464 parse_partial(&spec, &input(&["test", "run", "--output", "quiet", "task"])).unwrap();
2465 assert_eq!(
2466 parsed
2467 .cmds
2468 .iter()
2469 .map(|c| c.name.as_str())
2470 .collect::<Vec<_>>(),
2471 vec!["test", "run", "task"],
2472 );
2473 assert_eq!(
2474 parsed.as_env().get("usage_output").map(String::as_str),
2475 Some("quiet"),
2476 );
2477
2478 assert_parse_err(
2481 parse_partial(&spec, &input(&["test", "run", "--nope", "task"])),
2482 "unexpected word: --nope",
2483 );
2484 }
2485
2486 #[test]
2487 fn test_non_mounted_subcommand_offers_inherited_globals() {
2488 let mut run_cmd = SpecCommand::builder().name("run").build();
2491 run_cmd.subcommands.insert(
2492 "nested".to_string(),
2493 SpecCommand::builder().name("nested").build(),
2494 );
2495 let mut cmd = SpecCommand::builder()
2496 .name("test")
2497 .flag(
2498 SpecFlag::builder()
2499 .name("silent")
2500 .long("silent")
2501 .global(true)
2502 .build(),
2503 )
2504 .build();
2505 cmd.subcommands.insert("run".to_string(), run_cmd);
2506 let spec = Spec {
2507 name: "test".to_string(),
2508 bin: "test".to_string(),
2509 cmd,
2510 ..Default::default()
2511 };
2512
2513 let parsed = parse_partial(&spec, &input(&["test", "run", "nested"])).unwrap();
2514 assert_eq!(
2515 parsed.completion_flags().keys().collect::<Vec<_>>(),
2516 parsed.available_flags.keys().collect::<Vec<_>>(),
2517 );
2518 assert!(parsed.completion_flags().contains_key("--silent"));
2519 }
2520
2521 #[test]
2522 fn test_subcommand_alias_collision_keeps_last_owner() {
2523 let run_cmd = SpecCommand::builder()
2527 .name("run")
2528 .flag(
2529 SpecFlag::builder()
2530 .name("alpha")
2531 .short('x')
2532 .long("alpha")
2533 .global(false)
2534 .build(),
2535 )
2536 .flag(
2537 SpecFlag::builder()
2538 .name("beta")
2539 .short('x')
2540 .long("beta")
2541 .global(false)
2542 .build(),
2543 )
2544 .build();
2545 let mut cmd = SpecCommand::builder().name("test").build();
2546 cmd.subcommands.insert("run".to_string(), run_cmd);
2547 let spec = Spec {
2548 name: "test".to_string(),
2549 bin: "test".to_string(),
2550 cmd,
2551 ..Default::default()
2552 };
2553
2554 let parsed = parse_partial(&spec, &input(&["test", "run"])).unwrap();
2555 assert_eq!(
2557 parsed.available_flags.get("-x").map(|f| f.name.as_str()),
2558 Some("beta"),
2559 "the last-declared flag must keep a shared short alias",
2560 );
2561 assert_eq!(
2563 parsed
2564 .available_flags
2565 .get("--alpha")
2566 .map(|f| f.name.as_str()),
2567 Some("alpha"),
2568 );
2569 assert_eq!(
2570 parsed
2571 .available_flags
2572 .get("--beta")
2573 .map(|f| f.name.as_str()),
2574 Some("beta"),
2575 );
2576 }
2577
2578 #[test]
2579 fn test_default_subcommand_same_name_child() {
2580 let run_task = SpecCommand::builder()
2584 .name("run")
2585 .arg(SpecArg::builder().name("args").build())
2586 .build();
2587 let mut run_cmd = SpecCommand::builder().name("run").build();
2588 run_cmd.subcommands.insert("run".to_string(), run_task);
2589
2590 let mut cmd = SpecCommand::builder().name("test").build();
2591 cmd.subcommands.insert("run".to_string(), run_cmd);
2592
2593 let spec = Spec {
2594 name: "test".to_string(),
2595 bin: "test".to_string(),
2596 cmd,
2597 default_subcommand: Some("run".to_string()),
2598 ..Default::default()
2599 };
2600
2601 let input = vec!["test".to_string(), "run".to_string()];
2603 let parsed = parse(&spec, &input).unwrap();
2604
2605 assert_eq!(parsed.cmds.len(), 2);
2607 assert_eq!(parsed.cmds[0].name, "test");
2608 assert_eq!(parsed.cmds[1].name, "run");
2609
2610 let input = vec![
2612 "test".to_string(),
2613 "run".to_string(),
2614 "run".to_string(),
2615 "hello".to_string(),
2616 ];
2617 let parsed = parse(&spec, &input).unwrap();
2618
2619 assert_eq!(parsed.cmds.len(), 3);
2620 assert_eq!(parsed.cmds[0].name, "test");
2621 assert_eq!(parsed.cmds[1].name, "run");
2622 assert_eq!(parsed.cmds[2].name, "run");
2623 assert_eq!(parsed.args.len(), 1);
2624 let value = parsed.args.values().next().unwrap();
2625 assert_eq!(value.to_string(), "hello");
2626
2627 let mut run_cmd = SpecCommand::builder()
2631 .name("run")
2632 .arg(SpecArg::builder().name("task").build())
2633 .build();
2634 let run_task = SpecCommand::builder().name("run").build();
2635 run_cmd.subcommands.insert("run".to_string(), run_task);
2636
2637 let mut cmd = SpecCommand::builder().name("test").build();
2638 cmd.subcommands.insert("run".to_string(), run_cmd);
2639
2640 let spec = Spec {
2641 name: "test".to_string(),
2642 bin: "test".to_string(),
2643 cmd,
2644 default_subcommand: Some("run".to_string()),
2645 ..Default::default()
2646 };
2647
2648 let input = vec!["test".to_string(), "other".to_string()];
2649 let parsed = parse(&spec, &input).unwrap();
2650
2651 assert_eq!(parsed.cmds.len(), 2);
2654 assert_eq!(parsed.cmds[0].name, "test");
2655 assert_eq!(parsed.cmds[1].name, "run");
2656
2657 assert_eq!(parsed.args.len(), 1);
2659 let value = parsed.args.values().next().unwrap();
2660 assert_eq!(value.to_string(), "other");
2661 }
2662
2663 #[test]
2664 fn test_restart_token() {
2665 let run_cmd = SpecCommand::builder()
2667 .name("run")
2668 .arg(SpecArg::builder().name("task").build())
2669 .restart_token(":::".to_string())
2670 .build();
2671 let mut cmd = SpecCommand::builder().name("test").build();
2672 cmd.subcommands.insert("run".to_string(), run_cmd);
2673
2674 let spec = Spec {
2675 name: "test".to_string(),
2676 bin: "test".to_string(),
2677 cmd,
2678 ..Default::default()
2679 };
2680
2681 let input = vec![
2683 "test".to_string(),
2684 "run".to_string(),
2685 "task1".to_string(),
2686 ":::".to_string(),
2687 "task2".to_string(),
2688 ];
2689 let parsed = parse(&spec, &input).unwrap();
2690
2691 assert_eq!(parsed.args.len(), 1);
2693 let value = parsed.args.values().next().unwrap();
2694 assert_eq!(value.to_string(), "task2");
2695 }
2696
2697 #[test]
2698 fn test_restart_token_multiple() {
2699 let run_cmd = SpecCommand::builder()
2701 .name("run")
2702 .arg(SpecArg::builder().name("task").build())
2703 .restart_token(":::".to_string())
2704 .build();
2705 let mut cmd = SpecCommand::builder().name("test").build();
2706 cmd.subcommands.insert("run".to_string(), run_cmd);
2707
2708 let spec = Spec {
2709 name: "test".to_string(),
2710 bin: "test".to_string(),
2711 cmd,
2712 ..Default::default()
2713 };
2714
2715 let input = vec![
2717 "test".to_string(),
2718 "run".to_string(),
2719 "task1".to_string(),
2720 ":::".to_string(),
2721 "task2".to_string(),
2722 ":::".to_string(),
2723 "task3".to_string(),
2724 ];
2725 let parsed = parse(&spec, &input).unwrap();
2726
2727 assert_eq!(parsed.args.len(), 1);
2729 let value = parsed.args.values().next().unwrap();
2730 assert_eq!(value.to_string(), "task3");
2731 }
2732
2733 #[test]
2734 fn test_restart_token_clears_flag_awaiting_value() {
2735 let run_cmd = SpecCommand::builder()
2737 .name("run")
2738 .arg(SpecArg::builder().name("task").build())
2739 .flag(
2740 SpecFlag::builder()
2741 .name("jobs")
2742 .long("jobs")
2743 .arg(SpecArg::builder().name("count").build())
2744 .build(),
2745 )
2746 .restart_token(":::".to_string())
2747 .build();
2748 let mut cmd = SpecCommand::builder().name("test").build();
2749 cmd.subcommands.insert("run".to_string(), run_cmd);
2750
2751 let spec = Spec {
2752 name: "test".to_string(),
2753 bin: "test".to_string(),
2754 cmd,
2755 ..Default::default()
2756 };
2757
2758 let input = vec![
2760 "test".to_string(),
2761 "run".to_string(),
2762 "task1".to_string(),
2763 "--jobs".to_string(),
2764 ":::".to_string(),
2765 "task2".to_string(),
2766 ];
2767 let parsed = parse(&spec, &input).unwrap();
2768
2769 assert_eq!(parsed.args.len(), 1);
2771 let value = parsed.args.values().next().unwrap();
2772 assert_eq!(value.to_string(), "task2");
2773 assert!(parsed.flag_awaiting_value.is_empty());
2775 }
2776
2777 #[test]
2778 fn test_restart_token_resets_double_dash() {
2779 let run_cmd = SpecCommand::builder()
2781 .name("run")
2782 .arg(SpecArg::builder().name("task").build())
2783 .arg(SpecArg::builder().name("extra_args").var(true).build())
2784 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
2785 .restart_token(":::".to_string())
2786 .build();
2787 let mut cmd = SpecCommand::builder().name("test").build();
2788 cmd.subcommands.insert("run".to_string(), run_cmd);
2789
2790 let spec = Spec {
2791 name: "test".to_string(),
2792 bin: "test".to_string(),
2793 cmd,
2794 ..Default::default()
2795 };
2796
2797 let input = vec![
2799 "test".to_string(),
2800 "run".to_string(),
2801 "task1".to_string(),
2802 "--".to_string(),
2803 "extra".to_string(),
2804 ":::".to_string(),
2805 "--verbose".to_string(),
2806 "task2".to_string(),
2807 ];
2808 let parsed = parse(&spec, &input).unwrap();
2809
2810 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
2812 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2814 let value = parsed.args.get(task_arg).unwrap();
2815 assert_eq!(value.to_string(), "task2");
2816 }
2817
2818 #[test]
2819 fn test_double_dashes_without_preserve() {
2820 let run_cmd = SpecCommand::builder()
2822 .name("run")
2823 .arg(SpecArg::builder().name("args").var(true).build())
2824 .build();
2825 let mut cmd = SpecCommand::builder().name("test").build();
2826 cmd.subcommands.insert("run".to_string(), run_cmd);
2827
2828 let spec = Spec {
2829 name: "test".to_string(),
2830 bin: "test".to_string(),
2831 cmd,
2832 ..Default::default()
2833 };
2834
2835 let input = vec![
2837 "test".to_string(),
2838 "run".to_string(),
2839 "arg1".to_string(),
2840 "--".to_string(),
2841 "arg2".to_string(),
2842 "--".to_string(),
2843 "arg3".to_string(),
2844 ];
2845 let parsed = parse(&spec, &input).unwrap();
2846
2847 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2848 let value = parsed.args.get(args_arg).unwrap();
2849 assert_eq!(value.to_string(), "arg1 arg2 arg3");
2850 }
2851
2852 #[test]
2853 fn test_double_dashes_with_preserve() {
2854 let run_cmd = SpecCommand::builder()
2856 .name("run")
2857 .arg(
2858 SpecArg::builder()
2859 .name("args")
2860 .var(true)
2861 .double_dash(SpecDoubleDashChoices::Preserve)
2862 .build(),
2863 )
2864 .build();
2865 let mut cmd = SpecCommand::builder().name("test").build();
2866 cmd.subcommands.insert("run".to_string(), run_cmd);
2867
2868 let spec = Spec {
2869 name: "test".to_string(),
2870 bin: "test".to_string(),
2871 cmd,
2872 ..Default::default()
2873 };
2874
2875 let input = vec![
2877 "test".to_string(),
2878 "run".to_string(),
2879 "arg1".to_string(),
2880 "--".to_string(),
2881 "arg2".to_string(),
2882 "--".to_string(),
2883 "arg3".to_string(),
2884 ];
2885 let parsed = parse(&spec, &input).unwrap();
2886
2887 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2888 let value = parsed.args.get(args_arg).unwrap();
2889 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
2890 }
2891
2892 #[test]
2893 fn test_double_dashes_with_preserve_only_dashes() {
2894 let run_cmd = SpecCommand::builder()
2897 .name("run")
2898 .arg(
2899 SpecArg::builder()
2900 .name("args")
2901 .var(true)
2902 .double_dash(SpecDoubleDashChoices::Preserve)
2903 .build(),
2904 )
2905 .build();
2906 let mut cmd = SpecCommand::builder().name("test").build();
2907 cmd.subcommands.insert("run".to_string(), run_cmd);
2908
2909 let spec = Spec {
2910 name: "test".to_string(),
2911 bin: "test".to_string(),
2912 cmd,
2913 ..Default::default()
2914 };
2915
2916 let input = vec![
2918 "test".to_string(),
2919 "run".to_string(),
2920 "--".to_string(),
2921 "--".to_string(),
2922 ];
2923 let parsed = parse(&spec, &input).unwrap();
2924
2925 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
2926 let value = parsed.args.get(args_arg).unwrap();
2927 assert_eq!(value.to_string(), "-- --");
2928 }
2929
2930 #[test]
2931 fn test_double_dashes_with_preserve_multiple_args() {
2932 let run_cmd = SpecCommand::builder()
2934 .name("run")
2935 .arg(SpecArg::builder().name("task").build())
2936 .arg(
2937 SpecArg::builder()
2938 .name("extra_args")
2939 .var(true)
2940 .double_dash(SpecDoubleDashChoices::Preserve)
2941 .build(),
2942 )
2943 .build();
2944 let mut cmd = SpecCommand::builder().name("test").build();
2945 cmd.subcommands.insert("run".to_string(), run_cmd);
2946
2947 let spec = Spec {
2948 name: "test".to_string(),
2949 bin: "test".to_string(),
2950 cmd,
2951 ..Default::default()
2952 };
2953
2954 let input = vec![
2957 "test".to_string(),
2958 "run".to_string(),
2959 "task1".to_string(),
2960 "--".to_string(),
2961 "arg1".to_string(),
2962 "--".to_string(),
2963 "--foo".to_string(),
2964 ];
2965 let parsed = parse(&spec, &input).unwrap();
2966
2967 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2968 let task_value = parsed.args.get(task_arg).unwrap();
2969 assert_eq!(task_value.to_string(), "task1");
2970
2971 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
2972 let extra_value = parsed.args.get(extra_arg).unwrap();
2973 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
2974 }
2975
2976 #[test]
2977 fn test_parser_with_custom_env_for_required_arg() {
2978 let spec = spec_with_arg(
2979 SpecArg::builder()
2980 .name("name")
2981 .env("NAME")
2982 .required(true)
2983 .build(),
2984 );
2985 std::env::remove_var("NAME");
2986
2987 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
2988 .expect("parse should succeed with custom env");
2989 assert_eq!(parsed.args.len(), 1);
2990 assert_eq!(first_string_value(&parsed), "john");
2991 }
2992
2993 #[test]
2994 fn test_parser_with_custom_env_for_required_flag() {
2995 let spec = spec_with_flag(
2996 SpecFlag::builder()
2997 .long("name")
2998 .env("NAME")
2999 .required(true)
3000 .arg(SpecArg::builder().name("name").build())
3001 .build(),
3002 );
3003 std::env::remove_var("NAME");
3004
3005 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
3006 .expect("parse should succeed with custom env");
3007 assert_eq!(parsed.flags.len(), 1);
3008 assert_eq!(first_string_value(&parsed), "jane");
3009 }
3010
3011 #[test]
3012 fn test_parser_with_custom_env_still_fails_when_missing() {
3013 let spec = spec_with_arg(
3014 SpecArg::builder()
3015 .name("name")
3016 .env("NAME")
3017 .required(true)
3018 .build(),
3019 );
3020 std::env::remove_var("NAME");
3021 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
3022 }
3023
3024 #[test]
3025 fn test_parser_does_not_treat_env_choice_value_as_help() {
3026 let spec = spec_with_arg(
3027 SpecArg::builder()
3028 .name("env")
3029 .env("CURRENT_ENV")
3030 .choices(["dev", "staging"])
3031 .required(false)
3032 .build(),
3033 );
3034
3035 assert_parse_err(
3036 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
3037 "Invalid choice for arg env: --help, expected one of dev, staging",
3038 );
3039 }
3040
3041 #[test]
3042 fn test_parser_does_not_treat_default_choice_value_as_help() {
3043 let spec = spec_with_flag(
3044 SpecFlag::builder()
3045 .long("env")
3046 .arg(
3047 SpecArg::builder()
3048 .name("env")
3049 .choices(["dev", "staging"])
3050 .build(),
3051 )
3052 .default_value("--help")
3053 .build(),
3054 );
3055
3056 assert_parse_err(
3057 parse_with_env(&spec, &["test"], &[]),
3058 "Invalid choice for option env: --help, expected one of dev, staging",
3059 );
3060 }
3061
3062 #[cfg(feature = "unstable_choices_env")]
3063 #[test]
3064 fn test_parser_arg_choices_from_custom_env() {
3065 let spec = spec_arg_choices_env("DEPLOY_ENVS");
3066
3067 let parsed =
3068 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
3069 assert_eq!(first_string_value(&parsed), "bar");
3070
3071 assert_parse_err(
3072 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
3073 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
3074 );
3075 assert_parse_err(
3076 parse_with_env(&spec, &["test", "prod"], &[]),
3077 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
3078 );
3079 }
3080
3081 #[cfg(feature = "unstable_choices_env")]
3082 #[test]
3083 fn test_parser_validates_flag_choices_from_custom_env() {
3084 let spec = spec_flag_choices_env("DEPLOY_ENVS");
3085 let parsed = parse_with_env(
3086 &spec,
3087 &["test", "--env", "baz"],
3088 &[("DEPLOY_ENVS", "foo,bar baz")],
3089 )
3090 .unwrap();
3091 assert_eq!(first_string_value(&parsed), "baz");
3092 }
3093
3094 #[cfg(feature = "unstable_choices_env")]
3095 #[test]
3096 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
3097 let arg_env_spec = spec_with_arg(
3098 SpecArg::builder()
3099 .name("env")
3100 .env("CURRENT_ENV")
3101 .choices_env("DEPLOY_ENVS")
3102 .build(),
3103 );
3104 assert_parse_err(
3105 parse_with_env(
3106 &arg_env_spec,
3107 &["test"],
3108 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
3109 ),
3110 "Invalid choice for arg env: prod, expected one of dev, staging",
3111 );
3112
3113 let flag_default_spec = spec_with_flag(
3114 SpecFlag::builder()
3115 .long("env")
3116 .arg(
3117 SpecArg::builder()
3118 .name("env")
3119 .choices_env("DEPLOY_ENVS")
3120 .build(),
3121 )
3122 .default_value("prod")
3123 .build(),
3124 );
3125 assert_parse_err(
3126 parse_with_env(
3127 &flag_default_spec,
3128 &["test"],
3129 &[("DEPLOY_ENVS", "dev,staging")],
3130 ),
3131 "Invalid choice for option env: prod, expected one of dev, staging",
3132 );
3133 }
3134
3135 #[test]
3136 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
3137 let spec: Spec = r#"
3138 flag "-v --verbose" var=#true
3139 arg "[database]" default="myapp_dev"
3140 arg "[args...]"
3141 "#
3142 .parse()
3143 .unwrap();
3144 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3145 .into_iter()
3146 .map(String::from)
3147 .collect();
3148 let parsed = parse(&spec, &input).unwrap();
3149 let env = parsed.as_env();
3150 assert_eq!(env.get("usage_database").unwrap(), "mydb");
3151 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
3152 }
3153
3154 #[test]
3155 fn test_variadic_arg_captures_unknown_flags() {
3156 let cmd = SpecCommand::builder()
3157 .name("test")
3158 .flag(SpecFlag::builder().short('v').long("verbose").build())
3159 .arg(SpecArg::builder().name("database").required(false).build())
3160 .arg(
3161 SpecArg::builder()
3162 .name("args")
3163 .required(false)
3164 .var(true)
3165 .build(),
3166 )
3167 .build();
3168 let spec = Spec {
3169 name: "test".to_string(),
3170 bin: "test".to_string(),
3171 cmd,
3172 ..Default::default()
3173 };
3174
3175 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
3177 .into_iter()
3178 .map(String::from)
3179 .collect();
3180 let parsed = parse(&spec, &input).unwrap();
3181 assert_eq!(parsed.args.len(), 2);
3182 let args_val = parsed
3183 .args
3184 .iter()
3185 .find(|(a, _)| a.name == "args")
3186 .unwrap()
3187 .1;
3188 match args_val {
3189 ParseValue::MultiString(v) => {
3190 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3191 }
3192 _ => panic!("Expected MultiString, got {:?}", args_val),
3193 }
3194 }
3195
3196 #[test]
3197 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
3198 let cmd = SpecCommand::builder()
3199 .name("test")
3200 .flag(SpecFlag::builder().short('v').long("verbose").build())
3201 .arg(SpecArg::builder().name("database").required(false).build())
3202 .arg(
3203 SpecArg::builder()
3204 .name("args")
3205 .required(false)
3206 .var(true)
3207 .build(),
3208 )
3209 .build();
3210 let spec = Spec {
3211 name: "test".to_string(),
3212 bin: "test".to_string(),
3213 cmd,
3214 ..Default::default()
3215 };
3216
3217 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
3219 .into_iter()
3220 .map(String::from)
3221 .collect();
3222 let parsed = parse(&spec, &input).unwrap();
3223 assert_eq!(parsed.args.len(), 2);
3224 let args_val = parsed
3225 .args
3226 .iter()
3227 .find(|(a, _)| a.name == "args")
3228 .unwrap()
3229 .1;
3230 match args_val {
3231 ParseValue::MultiString(v) => {
3232 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
3233 }
3234 _ => panic!("Expected MultiString, got {:?}", args_val),
3235 }
3236 }
3237
3238 #[test]
3239 fn test_variadic_arg_unknown_flag_equals_value_not_split() {
3240 let spec: Spec = r#"arg "[other_args]" var=#true"#.parse().unwrap();
3243
3244 let input: Vec<String> = vec!["test", "--option=3"]
3247 .into_iter()
3248 .map(String::from)
3249 .collect();
3250 let parsed = parse(&spec, &input).unwrap();
3251 let env = parsed.as_env();
3252 assert_eq!(
3253 env.get("usage_other_args").map(String::as_str),
3254 Some("'--option=3'"),
3255 "expected a single --option=3 token, got {:?}",
3256 env.get("usage_other_args"),
3257 );
3258
3259 let input2: Vec<String> = vec!["test", "--foo=bar", "--baz=qux"]
3261 .into_iter()
3262 .map(String::from)
3263 .collect();
3264 let parsed2 = parse(&spec, &input2).unwrap();
3265 let env2 = parsed2.as_env();
3266 assert_eq!(
3267 env2.get("usage_other_args").map(String::as_str),
3268 Some("'--foo=bar' '--baz=qux'"),
3269 "expected two intact tokens, got {:?}",
3270 env2.get("usage_other_args"),
3271 );
3272
3273 let input3: Vec<String> = vec!["test", "positional1", "--option=3", "positional2"]
3275 .into_iter()
3276 .map(String::from)
3277 .collect();
3278 let parsed3 = parse(&spec, &input3).unwrap();
3279 let env3 = parsed3.as_env();
3280 assert_eq!(
3281 env3.get("usage_other_args").map(String::as_str),
3282 Some("positional1 '--option=3' positional2"),
3283 "expected positional args and intact flag token, got {:?}",
3284 env3.get("usage_other_args"),
3285 );
3286 }
3287
3288 #[test]
3289 fn test_allow_hyphen_values_consumes_short_flag_collision() {
3290 let spec = r#"
3291flag "-d --working-dir <DIR>"
3292flag "-a --args <ARGS>" allow_hyphen_values=#true
3293"#
3294 .parse::<Spec>()
3295 .unwrap();
3296
3297 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3298
3299 assert_eq!(parsed.flags.len(), 1);
3300 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3301 }
3302
3303 #[test]
3304 fn test_allow_hyphen_values_consumes_embedded_long_value() {
3305 let spec = r#"
3306flag "-d --working-dir <DIR>"
3307flag "-a --args <ARGS>" allow_hyphen_values=#true
3308"#
3309 .parse::<Spec>()
3310 .unwrap();
3311
3312 let parsed = parse(&spec, &input(&["test", "--args=-destroy"])).unwrap();
3313
3314 assert_eq!(parsed.flags.len(), 1);
3315 assert_eq!(flag_string_value(&parsed, "args"), "-destroy");
3316 }
3317
3318 #[test]
3319 fn test_variadic_allow_hyphen_values_consumes_repeated_flag_values() {
3320 let spec = r#"
3321flag "-a --args <ARGS>" var=#true allow_hyphen_values=#true
3322"#
3323 .parse::<Spec>()
3324 .unwrap();
3325
3326 let parsed = parse(&spec, &input(&["test", "-a", "-val1", "-a", "-val2"])).unwrap();
3327
3328 let flag = parsed
3329 .flags
3330 .keys()
3331 .find(|flag| flag.name == "args")
3332 .expect("expected args flag");
3333 let value = parsed.flags.get(flag).expect("expected args value");
3334 match value {
3335 ParseValue::MultiString(values) => {
3336 assert_eq!(values, &vec!["-val1".to_string(), "-val2".to_string()]);
3337 }
3338 _ => panic!("expected MultiString, got {value:?}"),
3339 }
3340 }
3341
3342 #[test]
3343 fn test_hyphen_values_still_default_to_short_flag_parsing() {
3344 let spec = r#"
3345flag "-d --working-dir <DIR>"
3346flag "-a --args <ARGS>"
3347"#
3348 .parse::<Spec>()
3349 .unwrap();
3350
3351 let parsed = parse(&spec, &input(&["test", "-a", "-destroy"])).unwrap();
3352
3353 assert_eq!(flag_string_value(&parsed, "working-dir"), "estroy");
3354 }
3355}