1use heck::ToSnakeCase;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use log::trace;
5use miette::bail;
6use std::collections::{BTreeMap, HashMap, 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(
28 available: &mut BTreeMap<String, Arc<SpecFlag>>,
29 new_flags: BTreeMap<String, Arc<SpecFlag>>,
30) {
31 available.retain(|_, f| f.global);
33 for (key, flag) in new_flags {
34 if !flag.global && available.contains_key(&key) {
38 continue;
39 }
40 available.insert(key, flag);
41 }
42}
43
44fn get_flag_key(word: &str) -> &str {
47 if word.starts_with("--") {
48 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
50 } else if word.len() >= 2 {
51 &word[0..2]
53 } else {
54 word
55 }
56}
57
58pub struct ParseOutput {
59 pub cmd: SpecCommand,
60 pub cmds: Vec<SpecCommand>,
61 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
62 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
63 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
64 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
65 pub errors: Vec<UsageErr>,
66}
67
68#[derive(Debug, EnumTryAs, Clone)]
69pub enum ParseValue {
70 Bool(bool),
71 String(String),
72 MultiBool(Vec<bool>),
73 MultiString(Vec<String>),
74}
75
76#[non_exhaustive]
96pub struct Parser<'a> {
97 spec: &'a Spec,
98 env: Option<HashMap<String, String>>,
99}
100
101impl<'a> Parser<'a> {
102 pub fn new(spec: &'a Spec) -> Self {
104 Self { spec, env: None }
105 }
106
107 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
112 self.env = Some(env);
113 self
114 }
115
116 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
120 let custom_env = self.env.as_ref();
121 let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
122 trace!("{out:?}");
123
124 let get_env = |key: &str| -> Option<String> {
125 if let Some(env_map) = custom_env {
126 env_map.get(key).cloned()
127 } else {
128 std::env::var(key).ok()
129 }
130 };
131
132 for arg in out.cmd.args.iter().skip(out.args.len()) {
134 if let Some(env_var) = arg.env.as_ref() {
135 if let Some(env_value) = get_env(env_var) {
136 validate_choice_value(
137 ChoiceTarget::arg(arg),
138 &env_value,
139 arg.choices.as_ref(),
140 custom_env,
141 )?;
142 out.args
143 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
144 continue;
145 }
146 }
147 if !arg.default.is_empty() {
148 if arg.var {
150 validate_choice_values(
151 ChoiceTarget::arg(arg),
152 &arg.default,
153 arg.choices.as_ref(),
154 custom_env,
155 )?;
156 out.args.insert(
158 Arc::new(arg.clone()),
159 ParseValue::MultiString(arg.default.clone()),
160 );
161 } else {
162 validate_choice_value(
163 ChoiceTarget::arg(arg),
164 &arg.default[0],
165 arg.choices.as_ref(),
166 custom_env,
167 )?;
168 out.args.insert(
170 Arc::new(arg.clone()),
171 ParseValue::String(arg.default[0].clone()),
172 );
173 }
174 }
175 }
176
177 for flag in out.available_flags.values() {
179 if out.flags.contains_key(flag) {
180 continue;
181 }
182 if let Some(env_var) = flag.env.as_ref() {
183 if let Some(env_value) = get_env(env_var) {
184 if let Some(arg) = flag.arg.as_ref() {
185 validate_choice_value(
186 ChoiceTarget::option(flag),
187 &env_value,
188 arg.choices.as_ref(),
189 custom_env,
190 )?;
191 out.flags
192 .insert(Arc::clone(flag), ParseValue::String(env_value));
193 } else {
194 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
196 out.flags
197 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
198 }
199 continue;
200 }
201 }
202 if !flag.default.is_empty() {
204 if flag.var {
206 if let Some(arg) = flag.arg.as_ref() {
208 validate_choice_values(
209 ChoiceTarget::option(flag),
210 &flag.default,
211 arg.choices.as_ref(),
212 custom_env,
213 )?;
214 out.flags.insert(
215 Arc::clone(flag),
216 ParseValue::MultiString(flag.default.clone()),
217 );
218 } else {
219 let bools: Vec<bool> = flag
221 .default
222 .iter()
223 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
224 .collect();
225 out.flags
226 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
227 }
228 } else {
229 if let Some(arg) = flag.arg.as_ref() {
231 validate_choice_value(
232 ChoiceTarget::option(flag),
233 &flag.default[0],
234 arg.choices.as_ref(),
235 custom_env,
236 )?;
237 out.flags.insert(
238 Arc::clone(flag),
239 ParseValue::String(flag.default[0].clone()),
240 );
241 } else {
242 let is_true =
244 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
245 out.flags
246 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
247 }
248 }
249 }
250 if let Some(arg) = flag.arg.as_ref() {
252 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
253 if flag.var {
254 validate_choice_values(
255 ChoiceTarget::option(flag),
256 &arg.default,
257 arg.choices.as_ref(),
258 custom_env,
259 )?;
260 out.flags.insert(
261 Arc::clone(flag),
262 ParseValue::MultiString(arg.default.clone()),
263 );
264 } else {
265 validate_choice_value(
266 ChoiceTarget::option(flag),
267 &arg.default[0],
268 arg.choices.as_ref(),
269 custom_env,
270 )?;
271 out.flags
272 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
273 }
274 }
275 }
276 }
277 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
278 bail!("{err}");
279 }
280 if !out.errors.is_empty() {
281 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
282 }
283 Ok(out)
284 }
285}
286
287#[must_use = "parsing result should be used"]
294pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
295 Parser::new(spec).parse(input)
296}
297
298#[must_use = "parsing result should be used"]
302pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
303 parse_partial_with_env(spec, input, None)
304}
305
306fn parse_partial_with_env(
308 spec: &Spec,
309 input: &[String],
310 custom_env: Option<&HashMap<String, String>>,
311) -> Result<ParseOutput, miette::Error> {
312 trace!("parse_partial: {input:?}");
313 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
314 input.pop_front();
315
316 let gather_flags = |cmd: &SpecCommand| {
317 cmd.flags
318 .iter()
319 .flat_map(|f| {
320 let f = Arc::new(f.clone()); let mut flags = f
322 .long
323 .iter()
324 .map(|l| (format!("--{l}"), Arc::clone(&f)))
325 .chain(f.short.iter().map(|s| (format!("-{s}"), Arc::clone(&f))))
326 .collect::<Vec<_>>();
327 if let Some(negate) = &f.negate {
328 flags.push((negate.clone(), Arc::clone(&f)));
329 }
330 flags
331 })
332 .collect()
333 };
334
335 let mut out = ParseOutput {
336 cmd: spec.cmd.clone(),
337 cmds: vec![spec.cmd.clone()],
338 args: IndexMap::new(),
339 flags: IndexMap::new(),
340 available_flags: gather_flags(&spec.cmd),
341 flag_awaiting_value: vec![],
342 errors: vec![],
343 };
344
345 let mut prefix_words: Vec<String> = vec![];
358 let mut idx = 0;
359 let mut used_default_subcommand = false;
362
363 while idx < input.len() {
364 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
365 let mut subcommand = subcommand.clone();
366 subcommand.mount(&prefix_words)?;
368 merge_subcommand_flags(&mut out.available_flags, gather_flags(&subcommand));
369 input.remove(idx);
371 out.cmds.push(subcommand.clone());
372 out.cmd = subcommand.clone();
373 prefix_words.clear();
374 } else if input[idx].starts_with('-') {
377 let word = &input[idx];
379 let flag_key = get_flag_key(word);
380
381 if let Some(f) = out.available_flags.get(flag_key) {
382 if f.global {
391 prefix_words.push(input[idx].clone());
392 idx += 1;
393
394 if f.arg.is_some()
397 && !word.contains('=')
398 && idx < input.len()
399 && !input[idx].starts_with('-')
400 {
401 prefix_words.push(input[idx].clone());
402 idx += 1;
403 }
404 } else {
405 break;
409 }
410 } else {
411 break;
414 }
415 } else {
416 if !used_default_subcommand {
419 if let Some(default_name) = &spec.default_subcommand {
420 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
421 let mut subcommand = subcommand.clone();
422 subcommand.mount(&prefix_words)?;
424 merge_subcommand_flags(&mut out.available_flags, gather_flags(&subcommand));
425 out.cmds.push(subcommand.clone());
426 out.cmd = subcommand.clone();
427 prefix_words.clear();
428 used_default_subcommand = true;
429 continue;
434 }
435 }
436 }
437 break;
439 }
440 }
441
442 let mut next_arg = out.cmd.args.first();
447 let mut enable_flags = true;
448 let mut grouped_flag = false;
449
450 while !input.is_empty() {
451 let mut w = input.pop_front().unwrap();
452
453 if let Some(ref restart_token) = out.cmd.restart_token {
456 if w == *restart_token {
457 out.args.clear();
459 next_arg = out.cmd.args.first();
460 out.flag_awaiting_value.clear(); enable_flags = true; continue;
464 }
465 }
466
467 if w == "--" {
468 enable_flags = false;
470
471 let should_preserve = next_arg
474 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
475 .unwrap_or(false);
476
477 if should_preserve {
478 } else {
480 continue;
482 }
483 }
484
485 if enable_flags && w.starts_with("--") {
487 grouped_flag = false;
488 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
489 if !val.is_empty() {
490 input.push_front(val.to_string());
491 }
492 if let Some(f) = out.available_flags.get(word) {
493 if f.arg.is_some() {
494 out.flag_awaiting_value.push(Arc::clone(f));
495 } else if f.count {
496 let arr = out
497 .flags
498 .entry(Arc::clone(f))
499 .or_insert_with(|| ParseValue::MultiBool(vec![]))
500 .try_as_multi_bool_mut()
501 .unwrap();
502 arr.push(true);
503 } else {
504 let negate = f.negate.clone().unwrap_or_default();
505 out.flags
506 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
507 }
508 continue;
509 }
510 if is_help_arg(spec, &w) {
511 out.errors
512 .push(render_help_err(spec, &out.cmd, w.len() > 2));
513 return Ok(out);
514 }
515 }
516
517 if enable_flags && w.starts_with('-') && w.len() > 1 {
519 let short = w.chars().nth(1).unwrap();
520 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
521 if w.len() > 2 {
522 input.push_front(format!("-{}", &w[2..]));
523 grouped_flag = true;
524 }
525 if f.arg.is_some() {
526 out.flag_awaiting_value.push(Arc::clone(f));
527 } else if f.count {
528 let arr = out
529 .flags
530 .entry(Arc::clone(f))
531 .or_insert_with(|| ParseValue::MultiBool(vec![]))
532 .try_as_multi_bool_mut()
533 .unwrap();
534 arr.push(true);
535 } else {
536 let negate = f.negate.clone().unwrap_or_default();
537 out.flags
538 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
539 }
540 continue;
541 }
542 if is_help_arg(spec, &w) {
543 out.errors
544 .push(render_help_err(spec, &out.cmd, w.len() > 2));
545 return Ok(out);
546 }
547 if grouped_flag {
548 grouped_flag = false;
549 w.remove(0);
550 }
551 }
552
553 if !out.flag_awaiting_value.is_empty() {
554 while let Some(flag) = out.flag_awaiting_value.pop() {
555 let arg = flag.arg.as_ref().unwrap();
556 if validate_choices(
557 spec,
558 &out.cmd,
559 &mut out.errors,
560 ChoiceTarget::option(&flag),
561 &w,
562 arg.choices.as_ref(),
563 custom_env,
564 )? {
565 return Ok(out);
566 }
567 if flag.var {
568 let arr = out
569 .flags
570 .entry(flag)
571 .or_insert_with(|| ParseValue::MultiString(vec![]))
572 .try_as_multi_string_mut()
573 .unwrap();
574 arr.push(w);
575 } else {
576 out.flags.insert(flag, ParseValue::String(w));
577 }
578 w = "".to_string();
579 }
580 continue;
581 }
582
583 if let Some(arg) = next_arg {
584 if validate_choices(
585 spec,
586 &out.cmd,
587 &mut out.errors,
588 ChoiceTarget::arg(arg),
589 &w,
590 arg.choices.as_ref(),
591 custom_env,
592 )? {
593 return Ok(out);
594 }
595 if arg.var {
596 let arr = out
597 .args
598 .entry(Arc::new(arg.clone()))
599 .or_insert_with(|| ParseValue::MultiString(vec![]))
600 .try_as_multi_string_mut()
601 .unwrap();
602 arr.push(w);
603 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
604 next_arg = out.cmd.args.get(out.args.len());
605 }
606 } else {
607 out.args
608 .insert(Arc::new(arg.clone()), ParseValue::String(w));
609 next_arg = out.cmd.args.get(out.args.len());
610 }
611 continue;
612 }
613 if is_help_arg(spec, &w) {
614 out.errors
615 .push(render_help_err(spec, &out.cmd, w.len() > 2));
616 return Ok(out);
617 }
618 bail!("unexpected word: {w}");
619 }
620
621 for arg in out.cmd.args.iter().skip(out.args.len()) {
622 if arg.required && arg.default.is_empty() {
623 let has_env = arg.env.as_ref().is_some_and(|e| {
625 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
626 || std::env::var(e).is_ok()
627 });
628 if !has_env {
629 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
630 }
631 }
632 }
633
634 for flag in out.available_flags.values() {
635 if out.flags.contains_key(flag) {
636 continue;
637 }
638 let has_default =
639 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
640 let has_env = flag.env.as_ref().is_some_and(|e| {
642 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
643 });
644 if flag.required && !has_default && !has_env {
645 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
646 }
647 }
648
649 for (arg, value) in &out.args {
651 if arg.var {
652 if let ParseValue::MultiString(values) = value {
653 if let Some(min) = arg.var_min {
654 if values.len() < min {
655 out.errors.push(UsageErr::VarArgTooFew {
656 name: arg.name.clone(),
657 min,
658 got: values.len(),
659 });
660 }
661 }
662 if let Some(max) = arg.var_max {
663 if values.len() > max {
664 out.errors.push(UsageErr::VarArgTooMany {
665 name: arg.name.clone(),
666 max,
667 got: values.len(),
668 });
669 }
670 }
671 }
672 }
673 }
674
675 for (flag, value) in &out.flags {
677 if flag.var {
678 let count = match value {
679 ParseValue::MultiString(values) => values.len(),
680 ParseValue::MultiBool(values) => values.len(),
681 _ => continue,
682 };
683 if let Some(min) = flag.var_min {
684 if count < min {
685 out.errors.push(UsageErr::VarFlagTooFew {
686 name: flag.name.clone(),
687 min,
688 got: count,
689 });
690 }
691 }
692 if let Some(max) = flag.var_max {
693 if count > max {
694 out.errors.push(UsageErr::VarFlagTooMany {
695 name: flag.name.clone(),
696 max,
697 got: count,
698 });
699 }
700 }
701 }
702 }
703
704 Ok(out)
705}
706
707#[cfg(feature = "docs")]
708fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
709 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
710}
711
712#[cfg(not(feature = "docs"))]
713fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
714 UsageErr::Help("help".to_string())
715}
716
717#[derive(Copy, Clone)]
718struct ChoiceTarget<'a> {
719 kind: &'a str,
720 name: &'a str,
721}
722
723impl<'a> ChoiceTarget<'a> {
724 fn arg(arg: &'a SpecArg) -> Self {
725 Self {
726 kind: "arg",
727 name: &arg.name,
728 }
729 }
730
731 fn option(flag: &'a SpecFlag) -> Self {
732 Self {
733 kind: "option",
734 name: &flag.name,
735 }
736 }
737}
738
739fn choice_error(
740 target: ChoiceTarget<'_>,
741 value: &str,
742 choices: Option<&SpecChoices>,
743 custom_env: Option<&HashMap<String, String>>,
744) -> Option<String> {
745 let choices = choices?;
746 let values = choices.values_with_env(custom_env);
747 if values.iter().any(|choice| choice == value) {
748 return None;
749 }
750 if let Some(env) = choices.env() {
751 if values.is_empty() {
752 return Some(format!(
753 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
754 target.kind, target.name,
755 ));
756 }
757 }
758 Some(format!(
759 "Invalid choice for {} {}: {value}, expected one of {}",
760 target.kind,
761 target.name,
762 values.join(", ")
763 ))
764}
765
766fn validate_choices(
767 spec: &Spec,
768 cmd: &SpecCommand,
769 errors: &mut Vec<UsageErr>,
770 target: ChoiceTarget<'_>,
771 value: &str,
772 choices: Option<&SpecChoices>,
773 custom_env: Option<&HashMap<String, String>>,
774) -> miette::Result<bool> {
775 if is_help_arg(spec, value)
776 && choices.is_some_and(|choices| {
777 !choices
778 .values_with_env(custom_env)
779 .iter()
780 .any(|choice| choice == value)
781 })
782 {
783 errors.push(render_help_err(spec, cmd, value.len() > 2));
784 return Ok(true);
785 }
786
787 if let Some(err) = choice_error(target, value, choices, custom_env) {
788 bail!("{err}");
789 }
790 Ok(false)
791}
792
793fn validate_choice_value(
794 target: ChoiceTarget<'_>,
795 value: &str,
796 choices: Option<&SpecChoices>,
797 custom_env: Option<&HashMap<String, String>>,
798) -> miette::Result<()> {
799 if let Some(err) = choice_error(target, value, choices, custom_env) {
800 bail!("{err}");
801 }
802 Ok(())
803}
804
805fn validate_choice_values(
806 target: ChoiceTarget<'_>,
807 values: &[String],
808 choices: Option<&SpecChoices>,
809 custom_env: Option<&HashMap<String, String>>,
810) -> miette::Result<()> {
811 for value in values {
812 validate_choice_value(target, value, choices, custom_env)?;
813 }
814 Ok(())
815}
816
817fn is_help_arg(spec: &Spec, w: &str) -> bool {
818 spec.disable_help != Some(true)
819 && (w == "--help"
820 || w == "-h"
821 || w == "-?"
822 || (spec.cmd.subcommands.is_empty() && w == "help"))
823}
824
825impl ParseOutput {
826 pub fn as_env(&self) -> BTreeMap<String, String> {
827 let mut env = BTreeMap::new();
828 for (flag, val) in &self.flags {
829 let key = format!("usage_{}", flag.name.to_snake_case());
830 let val = match val {
831 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
832 ParseValue::String(s) => s.clone(),
833 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
834 ParseValue::MultiString(s) => shell_words::join(s),
835 };
836 env.insert(key, val);
837 }
838 for (arg, val) in &self.args {
839 let key = format!("usage_{}", arg.name.to_snake_case());
840 env.insert(key, val.to_string());
841 }
842 env
843 }
844}
845
846impl Display for ParseValue {
847 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
848 match self {
849 ParseValue::Bool(b) => write!(f, "{b}"),
850 ParseValue::String(s) => write!(f, "{s}"),
851 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
852 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
853 }
854 }
855}
856
857impl Debug for ParseOutput {
858 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
859 f.debug_struct("ParseOutput")
860 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
861 .field(
862 "args",
863 &self
864 .args
865 .iter()
866 .map(|(a, w)| format!("{}: {w}", &a.name))
867 .collect_vec(),
868 )
869 .field(
870 "available_flags",
871 &self
872 .available_flags
873 .iter()
874 .map(|(f, w)| format!("{f}: {w}"))
875 .collect_vec(),
876 )
877 .field(
878 "flags",
879 &self
880 .flags
881 .iter()
882 .map(|(f, w)| format!("{}: {w}", &f.name))
883 .collect_vec(),
884 )
885 .field("flag_awaiting_value", &self.flag_awaiting_value)
886 .field("errors", &self.errors)
887 .finish()
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894
895 fn input(words: &[&str]) -> Vec<String> {
896 words.iter().map(|word| (*word).to_string()).collect()
897 }
898
899 fn spec_with_arg(arg: SpecArg) -> Spec {
900 let cmd = SpecCommand::builder().name("test").arg(arg).build();
901 Spec {
902 name: "test".to_string(),
903 bin: "test".to_string(),
904 cmd,
905 ..Default::default()
906 }
907 }
908
909 fn spec_with_flag(flag: SpecFlag) -> Spec {
910 let cmd = SpecCommand::builder().name("test").flag(flag).build();
911 Spec {
912 name: "test".to_string(),
913 bin: "test".to_string(),
914 cmd,
915 ..Default::default()
916 }
917 }
918
919 fn parse_with_env(
920 spec: &Spec,
921 words: &[&str],
922 env: &[(&str, &str)],
923 ) -> Result<ParseOutput, miette::Error> {
924 let env = env
925 .iter()
926 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
927 .collect();
928 Parser::new(spec).with_env(env).parse(&input(words))
929 }
930
931 fn first_string_value(parsed: &ParseOutput) -> &str {
932 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
933 return value;
934 }
935 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
936 return value;
937 }
938 panic!("expected first parsed value to be ParseValue::String");
939 }
940
941 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
942 let err = result.expect_err("expected parser error");
943 assert_eq!(format!("{err}"), expected);
944 }
945
946 #[cfg(feature = "unstable_choices_env")]
947 fn spec_arg_choices_env(key: &str) -> Spec {
948 spec_with_arg(
949 SpecArg::builder()
950 .name("env")
951 .choices_env(key)
952 .required(false)
953 .build(),
954 )
955 }
956
957 #[cfg(feature = "unstable_choices_env")]
958 fn spec_flag_choices_env(key: &str) -> Spec {
959 spec_with_flag(
960 SpecFlag::builder()
961 .long("env")
962 .arg(SpecArg::builder().name("env").choices_env(key).build())
963 .build(),
964 )
965 }
966
967 #[test]
968 fn test_parse() {
969 let cmd = SpecCommand::builder()
970 .name("test")
971 .arg(SpecArg::builder().name("arg").build())
972 .flag(SpecFlag::builder().long("flag").build())
973 .build();
974 let spec = Spec {
975 name: "test".to_string(),
976 bin: "test".to_string(),
977 cmd,
978 ..Default::default()
979 };
980 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
981 let parsed = parse(&spec, &input).unwrap();
982 assert_eq!(parsed.cmds.len(), 1);
983 assert_eq!(parsed.cmds[0].name, "test");
984 assert_eq!(parsed.args.len(), 1);
985 assert_eq!(parsed.flags.len(), 1);
986 assert_eq!(parsed.available_flags.len(), 1);
987 }
988
989 #[test]
990 fn test_as_env() {
991 let cmd = SpecCommand::builder()
992 .name("test")
993 .arg(SpecArg::builder().name("arg").build())
994 .flag(SpecFlag::builder().long("flag").build())
995 .flag(
996 SpecFlag::builder()
997 .long("force")
998 .negate("--no-force")
999 .build(),
1000 )
1001 .build();
1002 let spec = Spec {
1003 name: "test".to_string(),
1004 bin: "test".to_string(),
1005 cmd,
1006 ..Default::default()
1007 };
1008 let input = vec![
1009 "test".to_string(),
1010 "--flag".to_string(),
1011 "--no-force".to_string(),
1012 ];
1013 let parsed = parse(&spec, &input).unwrap();
1014 let env = parsed.as_env();
1015 assert_eq!(env.len(), 2);
1016 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
1017 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
1018 }
1019
1020 #[test]
1021 fn test_arg_env_var() {
1022 let cmd = SpecCommand::builder()
1023 .name("test")
1024 .arg(
1025 SpecArg::builder()
1026 .name("input")
1027 .env("TEST_ARG_INPUT")
1028 .required(true)
1029 .build(),
1030 )
1031 .build();
1032 let spec = Spec {
1033 name: "test".to_string(),
1034 bin: "test".to_string(),
1035 cmd,
1036 ..Default::default()
1037 };
1038
1039 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1041
1042 let input = vec!["test".to_string()];
1043 let parsed = parse(&spec, &input).unwrap();
1044
1045 assert_eq!(parsed.args.len(), 1);
1046 let arg = parsed.args.keys().next().unwrap();
1047 assert_eq!(arg.name, "input");
1048 let value = parsed.args.values().next().unwrap();
1049 assert_eq!(value.to_string(), "test_file.txt");
1050
1051 std::env::remove_var("TEST_ARG_INPUT");
1053 }
1054
1055 #[test]
1056 fn test_flag_env_var_with_arg() {
1057 let cmd = SpecCommand::builder()
1058 .name("test")
1059 .flag(
1060 SpecFlag::builder()
1061 .long("output")
1062 .env("TEST_FLAG_OUTPUT")
1063 .arg(SpecArg::builder().name("file").build())
1064 .build(),
1065 )
1066 .build();
1067 let spec = Spec {
1068 name: "test".to_string(),
1069 bin: "test".to_string(),
1070 cmd,
1071 ..Default::default()
1072 };
1073
1074 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1076
1077 let input = vec!["test".to_string()];
1078 let parsed = parse(&spec, &input).unwrap();
1079
1080 assert_eq!(parsed.flags.len(), 1);
1081 let flag = parsed.flags.keys().next().unwrap();
1082 assert_eq!(flag.name, "output");
1083 let value = parsed.flags.values().next().unwrap();
1084 assert_eq!(value.to_string(), "output.txt");
1085
1086 std::env::remove_var("TEST_FLAG_OUTPUT");
1088 }
1089
1090 #[test]
1091 fn test_flag_env_var_boolean() {
1092 let cmd = SpecCommand::builder()
1093 .name("test")
1094 .flag(
1095 SpecFlag::builder()
1096 .long("verbose")
1097 .env("TEST_FLAG_VERBOSE")
1098 .build(),
1099 )
1100 .build();
1101 let spec = Spec {
1102 name: "test".to_string(),
1103 bin: "test".to_string(),
1104 cmd,
1105 ..Default::default()
1106 };
1107
1108 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1110
1111 let input = vec!["test".to_string()];
1112 let parsed = parse(&spec, &input).unwrap();
1113
1114 assert_eq!(parsed.flags.len(), 1);
1115 let flag = parsed.flags.keys().next().unwrap();
1116 assert_eq!(flag.name, "verbose");
1117 let value = parsed.flags.values().next().unwrap();
1118 assert_eq!(value.to_string(), "true");
1119
1120 std::env::remove_var("TEST_FLAG_VERBOSE");
1122 }
1123
1124 #[test]
1125 fn test_env_var_precedence() {
1126 let cmd = SpecCommand::builder()
1128 .name("test")
1129 .arg(
1130 SpecArg::builder()
1131 .name("input")
1132 .env("TEST_PRECEDENCE_INPUT")
1133 .required(true)
1134 .build(),
1135 )
1136 .build();
1137 let spec = Spec {
1138 name: "test".to_string(),
1139 bin: "test".to_string(),
1140 cmd,
1141 ..Default::default()
1142 };
1143
1144 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1146
1147 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1148 let parsed = parse(&spec, &input).unwrap();
1149
1150 assert_eq!(parsed.args.len(), 1);
1151 let value = parsed.args.values().next().unwrap();
1152 assert_eq!(value.to_string(), "cli_file.txt");
1154
1155 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1157 }
1158
1159 #[test]
1160 fn test_flag_var_true_with_single_default() {
1161 let cmd = SpecCommand::builder()
1163 .name("test")
1164 .flag(
1165 SpecFlag::builder()
1166 .long("foo")
1167 .var(true)
1168 .arg(SpecArg::builder().name("foo").build())
1169 .default_value("bar")
1170 .build(),
1171 )
1172 .build();
1173 let spec = Spec {
1174 name: "test".to_string(),
1175 bin: "test".to_string(),
1176 cmd,
1177 ..Default::default()
1178 };
1179
1180 let input = vec!["test".to_string()];
1182 let parsed = parse(&spec, &input).unwrap();
1183
1184 assert_eq!(parsed.flags.len(), 1);
1185 let flag = parsed.flags.keys().next().unwrap();
1186 assert_eq!(flag.name, "foo");
1187 let value = parsed.flags.values().next().unwrap();
1188 match value {
1190 ParseValue::MultiString(v) => {
1191 assert_eq!(v.len(), 1);
1192 assert_eq!(v[0], "bar");
1193 }
1194 _ => panic!("Expected MultiString, got {:?}", value),
1195 }
1196 }
1197
1198 #[test]
1199 fn test_flag_var_true_with_multiple_defaults() {
1200 let cmd = SpecCommand::builder()
1202 .name("test")
1203 .flag(
1204 SpecFlag::builder()
1205 .long("foo")
1206 .var(true)
1207 .arg(SpecArg::builder().name("foo").build())
1208 .default_values(["xyz", "bar"])
1209 .build(),
1210 )
1211 .build();
1212 let spec = Spec {
1213 name: "test".to_string(),
1214 bin: "test".to_string(),
1215 cmd,
1216 ..Default::default()
1217 };
1218
1219 let input = vec!["test".to_string()];
1221 let parsed = parse(&spec, &input).unwrap();
1222
1223 assert_eq!(parsed.flags.len(), 1);
1224 let value = parsed.flags.values().next().unwrap();
1225 match value {
1227 ParseValue::MultiString(v) => {
1228 assert_eq!(v.len(), 2);
1229 assert_eq!(v[0], "xyz");
1230 assert_eq!(v[1], "bar");
1231 }
1232 _ => panic!("Expected MultiString, got {:?}", value),
1233 }
1234 }
1235
1236 #[test]
1237 fn test_flag_var_false_with_default_remains_string() {
1238 let cmd = SpecCommand::builder()
1240 .name("test")
1241 .flag(
1242 SpecFlag::builder()
1243 .long("foo")
1244 .var(false) .arg(SpecArg::builder().name("foo").build())
1246 .default_value("bar")
1247 .build(),
1248 )
1249 .build();
1250 let spec = Spec {
1251 name: "test".to_string(),
1252 bin: "test".to_string(),
1253 cmd,
1254 ..Default::default()
1255 };
1256
1257 let input = vec!["test".to_string()];
1259 let parsed = parse(&spec, &input).unwrap();
1260
1261 assert_eq!(parsed.flags.len(), 1);
1262 let value = parsed.flags.values().next().unwrap();
1263 match value {
1265 ParseValue::String(s) => {
1266 assert_eq!(s, "bar");
1267 }
1268 _ => panic!("Expected String, got {:?}", value),
1269 }
1270 }
1271
1272 #[test]
1273 fn test_arg_var_true_with_single_default() {
1274 let cmd = SpecCommand::builder()
1276 .name("test")
1277 .arg(
1278 SpecArg::builder()
1279 .name("files")
1280 .var(true)
1281 .default_value("default.txt")
1282 .required(false)
1283 .build(),
1284 )
1285 .build();
1286 let spec = Spec {
1287 name: "test".to_string(),
1288 bin: "test".to_string(),
1289 cmd,
1290 ..Default::default()
1291 };
1292
1293 let input = vec!["test".to_string()];
1295 let parsed = parse(&spec, &input).unwrap();
1296
1297 assert_eq!(parsed.args.len(), 1);
1298 let value = parsed.args.values().next().unwrap();
1299 match value {
1301 ParseValue::MultiString(v) => {
1302 assert_eq!(v.len(), 1);
1303 assert_eq!(v[0], "default.txt");
1304 }
1305 _ => panic!("Expected MultiString, got {:?}", value),
1306 }
1307 }
1308
1309 #[test]
1310 fn test_arg_var_true_with_multiple_defaults() {
1311 let cmd = SpecCommand::builder()
1313 .name("test")
1314 .arg(
1315 SpecArg::builder()
1316 .name("files")
1317 .var(true)
1318 .default_values(["file1.txt", "file2.txt"])
1319 .required(false)
1320 .build(),
1321 )
1322 .build();
1323 let spec = Spec {
1324 name: "test".to_string(),
1325 bin: "test".to_string(),
1326 cmd,
1327 ..Default::default()
1328 };
1329
1330 let input = vec!["test".to_string()];
1332 let parsed = parse(&spec, &input).unwrap();
1333
1334 assert_eq!(parsed.args.len(), 1);
1335 let value = parsed.args.values().next().unwrap();
1336 match value {
1338 ParseValue::MultiString(v) => {
1339 assert_eq!(v.len(), 2);
1340 assert_eq!(v[0], "file1.txt");
1341 assert_eq!(v[1], "file2.txt");
1342 }
1343 _ => panic!("Expected MultiString, got {:?}", value),
1344 }
1345 }
1346
1347 #[test]
1348 fn test_arg_var_false_with_default_remains_string() {
1349 let cmd = SpecCommand::builder()
1351 .name("test")
1352 .arg(
1353 SpecArg::builder()
1354 .name("file")
1355 .var(false)
1356 .default_value("default.txt")
1357 .required(false)
1358 .build(),
1359 )
1360 .build();
1361 let spec = Spec {
1362 name: "test".to_string(),
1363 bin: "test".to_string(),
1364 cmd,
1365 ..Default::default()
1366 };
1367
1368 let input = vec!["test".to_string()];
1370 let parsed = parse(&spec, &input).unwrap();
1371
1372 assert_eq!(parsed.args.len(), 1);
1373 let value = parsed.args.values().next().unwrap();
1374 match value {
1376 ParseValue::String(s) => {
1377 assert_eq!(s, "default.txt");
1378 }
1379 _ => panic!("Expected String, got {:?}", value),
1380 }
1381 }
1382
1383 #[test]
1384 fn test_scalar_defaults_validate_only_first_default_choice() {
1385 let specs = [
1386 spec_with_arg(
1387 SpecArg::builder()
1388 .name("env")
1389 .var(false)
1390 .default_values(["dev", "prod"])
1391 .choices(["dev"])
1392 .required(false)
1393 .build(),
1394 ),
1395 spec_with_flag(
1396 SpecFlag::builder()
1397 .long("env")
1398 .arg(
1399 SpecArg::builder()
1400 .name("env")
1401 .default_values(["dev", "prod"])
1402 .choices(["dev"])
1403 .build(),
1404 )
1405 .build(),
1406 ),
1407 ];
1408
1409 for spec in specs {
1410 let parsed = parse(&spec, &input(&["test"])).unwrap();
1411 assert_eq!(first_string_value(&parsed), "dev");
1412 }
1413 }
1414
1415 #[test]
1416 fn test_default_subcommand() {
1417 let run_cmd = SpecCommand::builder()
1419 .name("run")
1420 .arg(SpecArg::builder().name("task").build())
1421 .build();
1422 let mut cmd = SpecCommand::builder().name("test").build();
1423 cmd.subcommands.insert("run".to_string(), run_cmd);
1424
1425 let spec = Spec {
1426 name: "test".to_string(),
1427 bin: "test".to_string(),
1428 cmd,
1429 default_subcommand: Some("run".to_string()),
1430 ..Default::default()
1431 };
1432
1433 let input = vec!["test".to_string(), "mytask".to_string()];
1435 let parsed = parse(&spec, &input).unwrap();
1436
1437 assert_eq!(parsed.cmds.len(), 2);
1439 assert_eq!(parsed.cmds[1].name, "run");
1440
1441 assert_eq!(parsed.args.len(), 1);
1443 let arg = parsed.args.keys().next().unwrap();
1444 assert_eq!(arg.name, "task");
1445 let value = parsed.args.values().next().unwrap();
1446 assert_eq!(value.to_string(), "mytask");
1447 }
1448
1449 #[test]
1450 fn test_default_subcommand_explicit_still_works() {
1451 let run_cmd = SpecCommand::builder()
1453 .name("run")
1454 .arg(SpecArg::builder().name("task").build())
1455 .build();
1456 let other_cmd = SpecCommand::builder()
1457 .name("other")
1458 .arg(SpecArg::builder().name("other_arg").build())
1459 .build();
1460 let mut cmd = SpecCommand::builder().name("test").build();
1461 cmd.subcommands.insert("run".to_string(), run_cmd);
1462 cmd.subcommands.insert("other".to_string(), other_cmd);
1463
1464 let spec = Spec {
1465 name: "test".to_string(),
1466 bin: "test".to_string(),
1467 cmd,
1468 default_subcommand: Some("run".to_string()),
1469 ..Default::default()
1470 };
1471
1472 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1474 let parsed = parse(&spec, &input).unwrap();
1475
1476 assert_eq!(parsed.cmds.len(), 2);
1478 assert_eq!(parsed.cmds[1].name, "other");
1479 }
1480
1481 #[test]
1482 fn test_default_subcommand_with_nested_subcommands() {
1483 let say_cmd = SpecCommand::builder()
1487 .name("say")
1488 .arg(SpecArg::builder().name("name").build())
1489 .build();
1490 let mut run_cmd = SpecCommand::builder().name("run").build();
1491 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1492
1493 let mut cmd = SpecCommand::builder().name("test").build();
1494 cmd.subcommands.insert("run".to_string(), run_cmd);
1495
1496 let spec = Spec {
1497 name: "test".to_string(),
1498 bin: "test".to_string(),
1499 cmd,
1500 default_subcommand: Some("run".to_string()),
1501 ..Default::default()
1502 };
1503
1504 let input = vec!["test".to_string(), "say".to_string(), "hello".to_string()];
1506 let parsed = parse(&spec, &input).unwrap();
1507
1508 assert_eq!(parsed.cmds.len(), 3);
1510 assert_eq!(parsed.cmds[0].name, "test");
1511 assert_eq!(parsed.cmds[1].name, "run");
1512 assert_eq!(parsed.cmds[2].name, "say");
1513
1514 assert_eq!(parsed.args.len(), 1);
1516 let arg = parsed.args.keys().next().unwrap();
1517 assert_eq!(arg.name, "name");
1518 let value = parsed.args.values().next().unwrap();
1519 assert_eq!(value.to_string(), "hello");
1520 }
1521
1522 fn mounted_global_flag_spec() -> Spec {
1530 let task_cmd = SpecCommand::builder()
1531 .name("sample:run")
1532 .arg(
1533 SpecArg::builder()
1534 .name("profile")
1535 .choices(["alpha", "beta", "gamma"])
1536 .build(),
1537 )
1538 .build();
1539 let mut run_cmd = SpecCommand::builder()
1541 .name("run")
1542 .flag(
1543 SpecFlag::builder()
1544 .name("cd")
1545 .short('C')
1546 .long("cd")
1547 .arg(SpecArg::builder().name("dir").build())
1548 .global(false)
1549 .build(),
1550 )
1551 .build();
1552 run_cmd
1553 .subcommands
1554 .insert("sample:run".to_string(), task_cmd);
1555
1556 let mut cmd = SpecCommand::builder()
1557 .name("test")
1558 .flag(
1559 SpecFlag::builder()
1560 .name("cd")
1561 .short('C')
1562 .long("cd")
1563 .arg(SpecArg::builder().name("dir").build())
1564 .global(true)
1565 .build(),
1566 )
1567 .build();
1568 cmd.subcommands.insert("run".to_string(), run_cmd);
1569
1570 Spec {
1571 name: "test".to_string(),
1572 bin: "test".to_string(),
1573 cmd,
1574 ..Default::default()
1575 }
1576 }
1577
1578 #[test]
1579 fn test_prefix_global_flag_does_not_pollute_choices() {
1580 let spec = mounted_global_flag_spec();
1587
1588 for words in [
1591 &["test", "-C", "/tmp", "run", "sample:run"][..],
1592 &["test", "--cd=/tmp", "run", "sample:run"][..],
1594 ] {
1595 let parsed = parse_partial(&spec, &input(words)).unwrap();
1596 assert_eq!(
1597 parsed
1598 .cmds
1599 .iter()
1600 .map(|c| c.name.as_str())
1601 .collect::<Vec<_>>(),
1602 vec!["test", "run", "sample:run"],
1603 );
1604 assert!(
1606 parsed.args.is_empty(),
1607 "args should be empty, got {:?}",
1608 parsed.args
1609 );
1610
1611 let cd = parsed
1614 .available_flags
1615 .get("--cd")
1616 .expect("--cd should remain available after descending into the subcommand");
1617 assert!(cd.global, "--cd must stay global after descent");
1618 assert!(
1619 parsed.available_flags.get("-C").is_some_and(|f| f.global),
1620 "-C must stay global after descent",
1621 );
1622
1623 assert_eq!(
1627 parsed.as_env().get("usage_cd").map(String::as_str),
1628 Some("/tmp"),
1629 "global flag value must survive in as_env(), got {:?}",
1630 parsed.as_env(),
1631 );
1632 }
1633
1634 let parsed = parse_partial(
1636 &spec,
1637 &input(&["test", "-C", "/tmp", "run", "sample:run", "alpha"]),
1638 )
1639 .unwrap();
1640 assert_eq!(parsed.args.len(), 1);
1641 assert_eq!(parsed.args.values().next().unwrap().to_string(), "alpha");
1642
1643 assert_parse_err(
1645 parse_partial(&spec, &input(&["test", "run", "sample:run", "wrong"])),
1646 "Invalid choice for arg profile: wrong, expected one of alpha, beta, gamma",
1647 );
1648 }
1649
1650 #[test]
1651 fn test_default_subcommand_same_name_child() {
1652 let run_task = SpecCommand::builder()
1656 .name("run")
1657 .arg(SpecArg::builder().name("args").build())
1658 .build();
1659 let mut run_cmd = SpecCommand::builder().name("run").build();
1660 run_cmd.subcommands.insert("run".to_string(), run_task);
1661
1662 let mut cmd = SpecCommand::builder().name("test").build();
1663 cmd.subcommands.insert("run".to_string(), run_cmd);
1664
1665 let spec = Spec {
1666 name: "test".to_string(),
1667 bin: "test".to_string(),
1668 cmd,
1669 default_subcommand: Some("run".to_string()),
1670 ..Default::default()
1671 };
1672
1673 let input = vec!["test".to_string(), "run".to_string()];
1675 let parsed = parse(&spec, &input).unwrap();
1676
1677 assert_eq!(parsed.cmds.len(), 2);
1679 assert_eq!(parsed.cmds[0].name, "test");
1680 assert_eq!(parsed.cmds[1].name, "run");
1681
1682 let input = vec![
1684 "test".to_string(),
1685 "run".to_string(),
1686 "run".to_string(),
1687 "hello".to_string(),
1688 ];
1689 let parsed = parse(&spec, &input).unwrap();
1690
1691 assert_eq!(parsed.cmds.len(), 3);
1692 assert_eq!(parsed.cmds[0].name, "test");
1693 assert_eq!(parsed.cmds[1].name, "run");
1694 assert_eq!(parsed.cmds[2].name, "run");
1695 assert_eq!(parsed.args.len(), 1);
1696 let value = parsed.args.values().next().unwrap();
1697 assert_eq!(value.to_string(), "hello");
1698
1699 let mut run_cmd = SpecCommand::builder()
1703 .name("run")
1704 .arg(SpecArg::builder().name("task").build())
1705 .build();
1706 let run_task = SpecCommand::builder().name("run").build();
1707 run_cmd.subcommands.insert("run".to_string(), run_task);
1708
1709 let mut cmd = SpecCommand::builder().name("test").build();
1710 cmd.subcommands.insert("run".to_string(), run_cmd);
1711
1712 let spec = Spec {
1713 name: "test".to_string(),
1714 bin: "test".to_string(),
1715 cmd,
1716 default_subcommand: Some("run".to_string()),
1717 ..Default::default()
1718 };
1719
1720 let input = vec!["test".to_string(), "other".to_string()];
1721 let parsed = parse(&spec, &input).unwrap();
1722
1723 assert_eq!(parsed.cmds.len(), 2);
1726 assert_eq!(parsed.cmds[0].name, "test");
1727 assert_eq!(parsed.cmds[1].name, "run");
1728
1729 assert_eq!(parsed.args.len(), 1);
1731 let value = parsed.args.values().next().unwrap();
1732 assert_eq!(value.to_string(), "other");
1733 }
1734
1735 #[test]
1736 fn test_restart_token() {
1737 let run_cmd = SpecCommand::builder()
1739 .name("run")
1740 .arg(SpecArg::builder().name("task").build())
1741 .restart_token(":::".to_string())
1742 .build();
1743 let mut cmd = SpecCommand::builder().name("test").build();
1744 cmd.subcommands.insert("run".to_string(), run_cmd);
1745
1746 let spec = Spec {
1747 name: "test".to_string(),
1748 bin: "test".to_string(),
1749 cmd,
1750 ..Default::default()
1751 };
1752
1753 let input = vec![
1755 "test".to_string(),
1756 "run".to_string(),
1757 "task1".to_string(),
1758 ":::".to_string(),
1759 "task2".to_string(),
1760 ];
1761 let parsed = parse(&spec, &input).unwrap();
1762
1763 assert_eq!(parsed.args.len(), 1);
1765 let value = parsed.args.values().next().unwrap();
1766 assert_eq!(value.to_string(), "task2");
1767 }
1768
1769 #[test]
1770 fn test_restart_token_multiple() {
1771 let run_cmd = SpecCommand::builder()
1773 .name("run")
1774 .arg(SpecArg::builder().name("task").build())
1775 .restart_token(":::".to_string())
1776 .build();
1777 let mut cmd = SpecCommand::builder().name("test").build();
1778 cmd.subcommands.insert("run".to_string(), run_cmd);
1779
1780 let spec = Spec {
1781 name: "test".to_string(),
1782 bin: "test".to_string(),
1783 cmd,
1784 ..Default::default()
1785 };
1786
1787 let input = vec![
1789 "test".to_string(),
1790 "run".to_string(),
1791 "task1".to_string(),
1792 ":::".to_string(),
1793 "task2".to_string(),
1794 ":::".to_string(),
1795 "task3".to_string(),
1796 ];
1797 let parsed = parse(&spec, &input).unwrap();
1798
1799 assert_eq!(parsed.args.len(), 1);
1801 let value = parsed.args.values().next().unwrap();
1802 assert_eq!(value.to_string(), "task3");
1803 }
1804
1805 #[test]
1806 fn test_restart_token_clears_flag_awaiting_value() {
1807 let run_cmd = SpecCommand::builder()
1809 .name("run")
1810 .arg(SpecArg::builder().name("task").build())
1811 .flag(
1812 SpecFlag::builder()
1813 .name("jobs")
1814 .long("jobs")
1815 .arg(SpecArg::builder().name("count").build())
1816 .build(),
1817 )
1818 .restart_token(":::".to_string())
1819 .build();
1820 let mut cmd = SpecCommand::builder().name("test").build();
1821 cmd.subcommands.insert("run".to_string(), run_cmd);
1822
1823 let spec = Spec {
1824 name: "test".to_string(),
1825 bin: "test".to_string(),
1826 cmd,
1827 ..Default::default()
1828 };
1829
1830 let input = vec![
1832 "test".to_string(),
1833 "run".to_string(),
1834 "task1".to_string(),
1835 "--jobs".to_string(),
1836 ":::".to_string(),
1837 "task2".to_string(),
1838 ];
1839 let parsed = parse(&spec, &input).unwrap();
1840
1841 assert_eq!(parsed.args.len(), 1);
1843 let value = parsed.args.values().next().unwrap();
1844 assert_eq!(value.to_string(), "task2");
1845 assert!(parsed.flag_awaiting_value.is_empty());
1847 }
1848
1849 #[test]
1850 fn test_restart_token_resets_double_dash() {
1851 let run_cmd = SpecCommand::builder()
1853 .name("run")
1854 .arg(SpecArg::builder().name("task").build())
1855 .arg(SpecArg::builder().name("extra_args").var(true).build())
1856 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1857 .restart_token(":::".to_string())
1858 .build();
1859 let mut cmd = SpecCommand::builder().name("test").build();
1860 cmd.subcommands.insert("run".to_string(), run_cmd);
1861
1862 let spec = Spec {
1863 name: "test".to_string(),
1864 bin: "test".to_string(),
1865 cmd,
1866 ..Default::default()
1867 };
1868
1869 let input = vec![
1871 "test".to_string(),
1872 "run".to_string(),
1873 "task1".to_string(),
1874 "--".to_string(),
1875 "extra".to_string(),
1876 ":::".to_string(),
1877 "--verbose".to_string(),
1878 "task2".to_string(),
1879 ];
1880 let parsed = parse(&spec, &input).unwrap();
1881
1882 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1884 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1886 let value = parsed.args.get(task_arg).unwrap();
1887 assert_eq!(value.to_string(), "task2");
1888 }
1889
1890 #[test]
1891 fn test_double_dashes_without_preserve() {
1892 let run_cmd = SpecCommand::builder()
1894 .name("run")
1895 .arg(SpecArg::builder().name("args").var(true).build())
1896 .build();
1897 let mut cmd = SpecCommand::builder().name("test").build();
1898 cmd.subcommands.insert("run".to_string(), run_cmd);
1899
1900 let spec = Spec {
1901 name: "test".to_string(),
1902 bin: "test".to_string(),
1903 cmd,
1904 ..Default::default()
1905 };
1906
1907 let input = vec![
1909 "test".to_string(),
1910 "run".to_string(),
1911 "arg1".to_string(),
1912 "--".to_string(),
1913 "arg2".to_string(),
1914 "--".to_string(),
1915 "arg3".to_string(),
1916 ];
1917 let parsed = parse(&spec, &input).unwrap();
1918
1919 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1920 let value = parsed.args.get(args_arg).unwrap();
1921 assert_eq!(value.to_string(), "arg1 arg2 arg3");
1922 }
1923
1924 #[test]
1925 fn test_double_dashes_with_preserve() {
1926 let run_cmd = SpecCommand::builder()
1928 .name("run")
1929 .arg(
1930 SpecArg::builder()
1931 .name("args")
1932 .var(true)
1933 .double_dash(SpecDoubleDashChoices::Preserve)
1934 .build(),
1935 )
1936 .build();
1937 let mut cmd = SpecCommand::builder().name("test").build();
1938 cmd.subcommands.insert("run".to_string(), run_cmd);
1939
1940 let spec = Spec {
1941 name: "test".to_string(),
1942 bin: "test".to_string(),
1943 cmd,
1944 ..Default::default()
1945 };
1946
1947 let input = vec![
1949 "test".to_string(),
1950 "run".to_string(),
1951 "arg1".to_string(),
1952 "--".to_string(),
1953 "arg2".to_string(),
1954 "--".to_string(),
1955 "arg3".to_string(),
1956 ];
1957 let parsed = parse(&spec, &input).unwrap();
1958
1959 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1960 let value = parsed.args.get(args_arg).unwrap();
1961 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1962 }
1963
1964 #[test]
1965 fn test_double_dashes_with_preserve_only_dashes() {
1966 let run_cmd = SpecCommand::builder()
1969 .name("run")
1970 .arg(
1971 SpecArg::builder()
1972 .name("args")
1973 .var(true)
1974 .double_dash(SpecDoubleDashChoices::Preserve)
1975 .build(),
1976 )
1977 .build();
1978 let mut cmd = SpecCommand::builder().name("test").build();
1979 cmd.subcommands.insert("run".to_string(), run_cmd);
1980
1981 let spec = Spec {
1982 name: "test".to_string(),
1983 bin: "test".to_string(),
1984 cmd,
1985 ..Default::default()
1986 };
1987
1988 let input = vec![
1990 "test".to_string(),
1991 "run".to_string(),
1992 "--".to_string(),
1993 "--".to_string(),
1994 ];
1995 let parsed = parse(&spec, &input).unwrap();
1996
1997 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1998 let value = parsed.args.get(args_arg).unwrap();
1999 assert_eq!(value.to_string(), "-- --");
2000 }
2001
2002 #[test]
2003 fn test_double_dashes_with_preserve_multiple_args() {
2004 let run_cmd = SpecCommand::builder()
2006 .name("run")
2007 .arg(SpecArg::builder().name("task").build())
2008 .arg(
2009 SpecArg::builder()
2010 .name("extra_args")
2011 .var(true)
2012 .double_dash(SpecDoubleDashChoices::Preserve)
2013 .build(),
2014 )
2015 .build();
2016 let mut cmd = SpecCommand::builder().name("test").build();
2017 cmd.subcommands.insert("run".to_string(), run_cmd);
2018
2019 let spec = Spec {
2020 name: "test".to_string(),
2021 bin: "test".to_string(),
2022 cmd,
2023 ..Default::default()
2024 };
2025
2026 let input = vec![
2029 "test".to_string(),
2030 "run".to_string(),
2031 "task1".to_string(),
2032 "--".to_string(),
2033 "arg1".to_string(),
2034 "--".to_string(),
2035 "--foo".to_string(),
2036 ];
2037 let parsed = parse(&spec, &input).unwrap();
2038
2039 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
2040 let task_value = parsed.args.get(task_arg).unwrap();
2041 assert_eq!(task_value.to_string(), "task1");
2042
2043 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
2044 let extra_value = parsed.args.get(extra_arg).unwrap();
2045 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
2046 }
2047
2048 #[test]
2049 fn test_parser_with_custom_env_for_required_arg() {
2050 let spec = spec_with_arg(
2051 SpecArg::builder()
2052 .name("name")
2053 .env("NAME")
2054 .required(true)
2055 .build(),
2056 );
2057 std::env::remove_var("NAME");
2058
2059 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
2060 .expect("parse should succeed with custom env");
2061 assert_eq!(parsed.args.len(), 1);
2062 assert_eq!(first_string_value(&parsed), "john");
2063 }
2064
2065 #[test]
2066 fn test_parser_with_custom_env_for_required_flag() {
2067 let spec = spec_with_flag(
2068 SpecFlag::builder()
2069 .long("name")
2070 .env("NAME")
2071 .required(true)
2072 .arg(SpecArg::builder().name("name").build())
2073 .build(),
2074 );
2075 std::env::remove_var("NAME");
2076
2077 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
2078 .expect("parse should succeed with custom env");
2079 assert_eq!(parsed.flags.len(), 1);
2080 assert_eq!(first_string_value(&parsed), "jane");
2081 }
2082
2083 #[test]
2084 fn test_parser_with_custom_env_still_fails_when_missing() {
2085 let spec = spec_with_arg(
2086 SpecArg::builder()
2087 .name("name")
2088 .env("NAME")
2089 .required(true)
2090 .build(),
2091 );
2092 std::env::remove_var("NAME");
2093 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
2094 }
2095
2096 #[test]
2097 fn test_parser_does_not_treat_env_choice_value_as_help() {
2098 let spec = spec_with_arg(
2099 SpecArg::builder()
2100 .name("env")
2101 .env("CURRENT_ENV")
2102 .choices(["dev", "staging"])
2103 .required(false)
2104 .build(),
2105 );
2106
2107 assert_parse_err(
2108 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
2109 "Invalid choice for arg env: --help, expected one of dev, staging",
2110 );
2111 }
2112
2113 #[test]
2114 fn test_parser_does_not_treat_default_choice_value_as_help() {
2115 let spec = spec_with_flag(
2116 SpecFlag::builder()
2117 .long("env")
2118 .arg(
2119 SpecArg::builder()
2120 .name("env")
2121 .choices(["dev", "staging"])
2122 .build(),
2123 )
2124 .default_value("--help")
2125 .build(),
2126 );
2127
2128 assert_parse_err(
2129 parse_with_env(&spec, &["test"], &[]),
2130 "Invalid choice for option env: --help, expected one of dev, staging",
2131 );
2132 }
2133
2134 #[cfg(feature = "unstable_choices_env")]
2135 #[test]
2136 fn test_parser_arg_choices_from_custom_env() {
2137 let spec = spec_arg_choices_env("DEPLOY_ENVS");
2138
2139 let parsed =
2140 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
2141 assert_eq!(first_string_value(&parsed), "bar");
2142
2143 assert_parse_err(
2144 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
2145 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
2146 );
2147 assert_parse_err(
2148 parse_with_env(&spec, &["test", "prod"], &[]),
2149 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
2150 );
2151 }
2152
2153 #[cfg(feature = "unstable_choices_env")]
2154 #[test]
2155 fn test_parser_validates_flag_choices_from_custom_env() {
2156 let spec = spec_flag_choices_env("DEPLOY_ENVS");
2157 let parsed = parse_with_env(
2158 &spec,
2159 &["test", "--env", "baz"],
2160 &[("DEPLOY_ENVS", "foo,bar baz")],
2161 )
2162 .unwrap();
2163 assert_eq!(first_string_value(&parsed), "baz");
2164 }
2165
2166 #[cfg(feature = "unstable_choices_env")]
2167 #[test]
2168 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
2169 let arg_env_spec = spec_with_arg(
2170 SpecArg::builder()
2171 .name("env")
2172 .env("CURRENT_ENV")
2173 .choices_env("DEPLOY_ENVS")
2174 .build(),
2175 );
2176 assert_parse_err(
2177 parse_with_env(
2178 &arg_env_spec,
2179 &["test"],
2180 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
2181 ),
2182 "Invalid choice for arg env: prod, expected one of dev, staging",
2183 );
2184
2185 let flag_default_spec = spec_with_flag(
2186 SpecFlag::builder()
2187 .long("env")
2188 .arg(
2189 SpecArg::builder()
2190 .name("env")
2191 .choices_env("DEPLOY_ENVS")
2192 .build(),
2193 )
2194 .default_value("prod")
2195 .build(),
2196 );
2197 assert_parse_err(
2198 parse_with_env(
2199 &flag_default_spec,
2200 &["test"],
2201 &[("DEPLOY_ENVS", "dev,staging")],
2202 ),
2203 "Invalid choice for option env: prod, expected one of dev, staging",
2204 );
2205 }
2206
2207 #[test]
2208 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
2209 let spec: Spec = r#"
2210 flag "-v --verbose" var=#true
2211 arg "[database]" default="myapp_dev"
2212 arg "[args...]"
2213 "#
2214 .parse()
2215 .unwrap();
2216 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2217 .into_iter()
2218 .map(String::from)
2219 .collect();
2220 let parsed = parse(&spec, &input).unwrap();
2221 let env = parsed.as_env();
2222 assert_eq!(env.get("usage_database").unwrap(), "mydb");
2223 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
2224 }
2225
2226 #[test]
2227 fn test_variadic_arg_captures_unknown_flags() {
2228 let cmd = SpecCommand::builder()
2229 .name("test")
2230 .flag(SpecFlag::builder().short('v').long("verbose").build())
2231 .arg(SpecArg::builder().name("database").required(false).build())
2232 .arg(
2233 SpecArg::builder()
2234 .name("args")
2235 .required(false)
2236 .var(true)
2237 .build(),
2238 )
2239 .build();
2240 let spec = Spec {
2241 name: "test".to_string(),
2242 bin: "test".to_string(),
2243 cmd,
2244 ..Default::default()
2245 };
2246
2247 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2249 .into_iter()
2250 .map(String::from)
2251 .collect();
2252 let parsed = parse(&spec, &input).unwrap();
2253 assert_eq!(parsed.args.len(), 2);
2254 let args_val = parsed
2255 .args
2256 .iter()
2257 .find(|(a, _)| a.name == "args")
2258 .unwrap()
2259 .1;
2260 match args_val {
2261 ParseValue::MultiString(v) => {
2262 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2263 }
2264 _ => panic!("Expected MultiString, got {:?}", args_val),
2265 }
2266 }
2267
2268 #[test]
2269 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
2270 let cmd = SpecCommand::builder()
2271 .name("test")
2272 .flag(SpecFlag::builder().short('v').long("verbose").build())
2273 .arg(SpecArg::builder().name("database").required(false).build())
2274 .arg(
2275 SpecArg::builder()
2276 .name("args")
2277 .required(false)
2278 .var(true)
2279 .build(),
2280 )
2281 .build();
2282 let spec = Spec {
2283 name: "test".to_string(),
2284 bin: "test".to_string(),
2285 cmd,
2286 ..Default::default()
2287 };
2288
2289 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
2291 .into_iter()
2292 .map(String::from)
2293 .collect();
2294 let parsed = parse(&spec, &input).unwrap();
2295 assert_eq!(parsed.args.len(), 2);
2296 let args_val = parsed
2297 .args
2298 .iter()
2299 .find(|(a, _)| a.name == "args")
2300 .unwrap()
2301 .1;
2302 match args_val {
2303 ParseValue::MultiString(v) => {
2304 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2305 }
2306 _ => panic!("Expected MultiString, got {:?}", args_val),
2307 }
2308 }
2309}