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 get_flag_key(word: &str) -> &str {
20 if word.starts_with("--") {
21 word.split_once('=').map(|(k, _)| k).unwrap_or(word)
23 } else if word.len() >= 2 {
24 &word[0..2]
26 } else {
27 word
28 }
29}
30
31pub struct ParseOutput {
32 pub cmd: SpecCommand,
33 pub cmds: Vec<SpecCommand>,
34 pub args: IndexMap<Arc<SpecArg>, ParseValue>,
35 pub flags: IndexMap<Arc<SpecFlag>, ParseValue>,
36 pub available_flags: BTreeMap<String, Arc<SpecFlag>>,
37 pub flag_awaiting_value: Vec<Arc<SpecFlag>>,
38 pub errors: Vec<UsageErr>,
39}
40
41#[derive(Debug, EnumTryAs, Clone)]
42pub enum ParseValue {
43 Bool(bool),
44 String(String),
45 MultiBool(Vec<bool>),
46 MultiString(Vec<String>),
47}
48
49#[non_exhaustive]
69pub struct Parser<'a> {
70 spec: &'a Spec,
71 env: Option<HashMap<String, String>>,
72}
73
74impl<'a> Parser<'a> {
75 pub fn new(spec: &'a Spec) -> Self {
77 Self { spec, env: None }
78 }
79
80 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
85 self.env = Some(env);
86 self
87 }
88
89 pub fn parse(self, input: &[String]) -> Result<ParseOutput, miette::Error> {
93 let custom_env = self.env.as_ref();
94 let mut out = parse_partial_with_env(self.spec, input, custom_env)?;
95 trace!("{out:?}");
96
97 let get_env = |key: &str| -> Option<String> {
98 if let Some(env_map) = custom_env {
99 env_map.get(key).cloned()
100 } else {
101 std::env::var(key).ok()
102 }
103 };
104
105 for arg in out.cmd.args.iter().skip(out.args.len()) {
107 if let Some(env_var) = arg.env.as_ref() {
108 if let Some(env_value) = get_env(env_var) {
109 validate_choice_value(
110 ChoiceTarget::arg(arg),
111 &env_value,
112 arg.choices.as_ref(),
113 custom_env,
114 )?;
115 out.args
116 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
117 continue;
118 }
119 }
120 if !arg.default.is_empty() {
121 if arg.var {
123 validate_choice_values(
124 ChoiceTarget::arg(arg),
125 &arg.default,
126 arg.choices.as_ref(),
127 custom_env,
128 )?;
129 out.args.insert(
131 Arc::new(arg.clone()),
132 ParseValue::MultiString(arg.default.clone()),
133 );
134 } else {
135 validate_choice_value(
136 ChoiceTarget::arg(arg),
137 &arg.default[0],
138 arg.choices.as_ref(),
139 custom_env,
140 )?;
141 out.args.insert(
143 Arc::new(arg.clone()),
144 ParseValue::String(arg.default[0].clone()),
145 );
146 }
147 }
148 }
149
150 for flag in out.available_flags.values() {
152 if out.flags.contains_key(flag) {
153 continue;
154 }
155 if let Some(env_var) = flag.env.as_ref() {
156 if let Some(env_value) = get_env(env_var) {
157 if let Some(arg) = flag.arg.as_ref() {
158 validate_choice_value(
159 ChoiceTarget::option(flag),
160 &env_value,
161 arg.choices.as_ref(),
162 custom_env,
163 )?;
164 out.flags
165 .insert(Arc::clone(flag), ParseValue::String(env_value));
166 } else {
167 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
169 out.flags
170 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
171 }
172 continue;
173 }
174 }
175 if !flag.default.is_empty() {
177 if flag.var {
179 if let Some(arg) = flag.arg.as_ref() {
181 validate_choice_values(
182 ChoiceTarget::option(flag),
183 &flag.default,
184 arg.choices.as_ref(),
185 custom_env,
186 )?;
187 out.flags.insert(
188 Arc::clone(flag),
189 ParseValue::MultiString(flag.default.clone()),
190 );
191 } else {
192 let bools: Vec<bool> = flag
194 .default
195 .iter()
196 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
197 .collect();
198 out.flags
199 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
200 }
201 } else {
202 if let Some(arg) = flag.arg.as_ref() {
204 validate_choice_value(
205 ChoiceTarget::option(flag),
206 &flag.default[0],
207 arg.choices.as_ref(),
208 custom_env,
209 )?;
210 out.flags.insert(
211 Arc::clone(flag),
212 ParseValue::String(flag.default[0].clone()),
213 );
214 } else {
215 let is_true =
217 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
218 out.flags
219 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
220 }
221 }
222 }
223 if let Some(arg) = flag.arg.as_ref() {
225 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
226 if flag.var {
227 validate_choice_values(
228 ChoiceTarget::option(flag),
229 &arg.default,
230 arg.choices.as_ref(),
231 custom_env,
232 )?;
233 out.flags.insert(
234 Arc::clone(flag),
235 ParseValue::MultiString(arg.default.clone()),
236 );
237 } else {
238 validate_choice_value(
239 ChoiceTarget::option(flag),
240 &arg.default[0],
241 arg.choices.as_ref(),
242 custom_env,
243 )?;
244 out.flags
245 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
246 }
247 }
248 }
249 }
250 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
251 bail!("{err}");
252 }
253 if !out.errors.is_empty() {
254 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
255 }
256 Ok(out)
257 }
258}
259
260#[must_use = "parsing result should be used"]
267pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
268 Parser::new(spec).parse(input)
269}
270
271#[must_use = "parsing result should be used"]
275pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
276 parse_partial_with_env(spec, input, None)
277}
278
279fn parse_partial_with_env(
281 spec: &Spec,
282 input: &[String],
283 custom_env: Option<&HashMap<String, String>>,
284) -> Result<ParseOutput, miette::Error> {
285 trace!("parse_partial: {input:?}");
286 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
287 input.pop_front();
288
289 let gather_flags = |cmd: &SpecCommand| {
290 cmd.flags
291 .iter()
292 .flat_map(|f| {
293 let f = Arc::new(f.clone()); let mut flags = f
295 .long
296 .iter()
297 .map(|l| (format!("--{l}"), Arc::clone(&f)))
298 .chain(f.short.iter().map(|s| (format!("-{s}"), Arc::clone(&f))))
299 .collect::<Vec<_>>();
300 if let Some(negate) = &f.negate {
301 flags.push((negate.clone(), Arc::clone(&f)));
302 }
303 flags
304 })
305 .collect()
306 };
307
308 let mut out = ParseOutput {
309 cmd: spec.cmd.clone(),
310 cmds: vec![spec.cmd.clone()],
311 args: IndexMap::new(),
312 flags: IndexMap::new(),
313 available_flags: gather_flags(&spec.cmd),
314 flag_awaiting_value: vec![],
315 errors: vec![],
316 };
317
318 let mut prefix_words: Vec<String> = vec![];
331 let mut idx = 0;
332 let mut used_default_subcommand = false;
335
336 while idx < input.len() {
337 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
338 let mut subcommand = subcommand.clone();
339 subcommand.mount(&prefix_words)?;
341 out.available_flags.retain(|_, f| f.global);
342 out.available_flags.extend(gather_flags(&subcommand));
343 input.remove(idx);
345 out.cmds.push(subcommand.clone());
346 out.cmd = subcommand.clone();
347 prefix_words.clear();
348 } else if input[idx].starts_with('-') {
351 let word = &input[idx];
353 let flag_key = get_flag_key(word);
354
355 if let Some(f) = out.available_flags.get(flag_key) {
356 if f.global {
358 prefix_words.push(input[idx].clone());
359 idx += 1;
360
361 if f.arg.is_some()
364 && !word.contains('=')
365 && idx < input.len()
366 && !input[idx].starts_with('-')
367 {
368 prefix_words.push(input[idx].clone());
369 idx += 1;
370 }
371 } else {
372 break;
376 }
377 } else {
378 break;
381 }
382 } else {
383 if !used_default_subcommand {
386 if let Some(default_name) = &spec.default_subcommand {
387 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
388 let mut subcommand = subcommand.clone();
389 subcommand.mount(&prefix_words)?;
391 out.available_flags.retain(|_, f| f.global);
392 out.available_flags.extend(gather_flags(&subcommand));
393 out.cmds.push(subcommand.clone());
394 out.cmd = subcommand.clone();
395 prefix_words.clear();
396 used_default_subcommand = true;
397 continue;
402 }
403 }
404 }
405 break;
407 }
408 }
409
410 let mut next_arg = out.cmd.args.first();
415 let mut enable_flags = true;
416 let mut grouped_flag = false;
417
418 while !input.is_empty() {
419 let mut w = input.pop_front().unwrap();
420
421 if let Some(ref restart_token) = out.cmd.restart_token {
424 if w == *restart_token {
425 out.args.clear();
427 next_arg = out.cmd.args.first();
428 out.flag_awaiting_value.clear(); enable_flags = true; continue;
432 }
433 }
434
435 if w == "--" {
436 enable_flags = false;
438
439 let should_preserve = next_arg
442 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
443 .unwrap_or(false);
444
445 if should_preserve {
446 } else {
448 continue;
450 }
451 }
452
453 if enable_flags && w.starts_with("--") {
455 grouped_flag = false;
456 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
457 if !val.is_empty() {
458 input.push_front(val.to_string());
459 }
460 if let Some(f) = out.available_flags.get(word) {
461 if f.arg.is_some() {
462 out.flag_awaiting_value.push(Arc::clone(f));
463 } else if f.count {
464 let arr = out
465 .flags
466 .entry(Arc::clone(f))
467 .or_insert_with(|| ParseValue::MultiBool(vec![]))
468 .try_as_multi_bool_mut()
469 .unwrap();
470 arr.push(true);
471 } else {
472 let negate = f.negate.clone().unwrap_or_default();
473 out.flags
474 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
475 }
476 continue;
477 }
478 if is_help_arg(spec, &w) {
479 out.errors
480 .push(render_help_err(spec, &out.cmd, w.len() > 2));
481 return Ok(out);
482 }
483 }
484
485 if enable_flags && w.starts_with('-') && w.len() > 1 {
487 let short = w.chars().nth(1).unwrap();
488 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
489 if w.len() > 2 {
490 input.push_front(format!("-{}", &w[2..]));
491 grouped_flag = true;
492 }
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 if grouped_flag {
516 grouped_flag = false;
517 w.remove(0);
518 }
519 }
520
521 if !out.flag_awaiting_value.is_empty() {
522 while let Some(flag) = out.flag_awaiting_value.pop() {
523 let arg = flag.arg.as_ref().unwrap();
524 if validate_choices(
525 spec,
526 &out.cmd,
527 &mut out.errors,
528 ChoiceTarget::option(&flag),
529 &w,
530 arg.choices.as_ref(),
531 custom_env,
532 )? {
533 return Ok(out);
534 }
535 if flag.var {
536 let arr = out
537 .flags
538 .entry(flag)
539 .or_insert_with(|| ParseValue::MultiString(vec![]))
540 .try_as_multi_string_mut()
541 .unwrap();
542 arr.push(w);
543 } else {
544 out.flags.insert(flag, ParseValue::String(w));
545 }
546 w = "".to_string();
547 }
548 continue;
549 }
550
551 if let Some(arg) = next_arg {
552 if validate_choices(
553 spec,
554 &out.cmd,
555 &mut out.errors,
556 ChoiceTarget::arg(arg),
557 &w,
558 arg.choices.as_ref(),
559 custom_env,
560 )? {
561 return Ok(out);
562 }
563 if arg.var {
564 let arr = out
565 .args
566 .entry(Arc::new(arg.clone()))
567 .or_insert_with(|| ParseValue::MultiString(vec![]))
568 .try_as_multi_string_mut()
569 .unwrap();
570 arr.push(w);
571 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
572 next_arg = out.cmd.args.get(out.args.len());
573 }
574 } else {
575 out.args
576 .insert(Arc::new(arg.clone()), ParseValue::String(w));
577 next_arg = out.cmd.args.get(out.args.len());
578 }
579 continue;
580 }
581 if is_help_arg(spec, &w) {
582 out.errors
583 .push(render_help_err(spec, &out.cmd, w.len() > 2));
584 return Ok(out);
585 }
586 bail!("unexpected word: {w}");
587 }
588
589 for arg in out.cmd.args.iter().skip(out.args.len()) {
590 if arg.required && arg.default.is_empty() {
591 let has_env = arg.env.as_ref().is_some_and(|e| {
593 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
594 || std::env::var(e).is_ok()
595 });
596 if !has_env {
597 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
598 }
599 }
600 }
601
602 for flag in out.available_flags.values() {
603 if out.flags.contains_key(flag) {
604 continue;
605 }
606 let has_default =
607 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
608 let has_env = flag.env.as_ref().is_some_and(|e| {
610 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
611 });
612 if flag.required && !has_default && !has_env {
613 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
614 }
615 }
616
617 for (arg, value) in &out.args {
619 if arg.var {
620 if let ParseValue::MultiString(values) = value {
621 if let Some(min) = arg.var_min {
622 if values.len() < min {
623 out.errors.push(UsageErr::VarArgTooFew {
624 name: arg.name.clone(),
625 min,
626 got: values.len(),
627 });
628 }
629 }
630 if let Some(max) = arg.var_max {
631 if values.len() > max {
632 out.errors.push(UsageErr::VarArgTooMany {
633 name: arg.name.clone(),
634 max,
635 got: values.len(),
636 });
637 }
638 }
639 }
640 }
641 }
642
643 for (flag, value) in &out.flags {
645 if flag.var {
646 let count = match value {
647 ParseValue::MultiString(values) => values.len(),
648 ParseValue::MultiBool(values) => values.len(),
649 _ => continue,
650 };
651 if let Some(min) = flag.var_min {
652 if count < min {
653 out.errors.push(UsageErr::VarFlagTooFew {
654 name: flag.name.clone(),
655 min,
656 got: count,
657 });
658 }
659 }
660 if let Some(max) = flag.var_max {
661 if count > max {
662 out.errors.push(UsageErr::VarFlagTooMany {
663 name: flag.name.clone(),
664 max,
665 got: count,
666 });
667 }
668 }
669 }
670 }
671
672 Ok(out)
673}
674
675#[cfg(feature = "docs")]
676fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
677 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
678}
679
680#[cfg(not(feature = "docs"))]
681fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
682 UsageErr::Help("help".to_string())
683}
684
685#[derive(Copy, Clone)]
686struct ChoiceTarget<'a> {
687 kind: &'a str,
688 name: &'a str,
689}
690
691impl<'a> ChoiceTarget<'a> {
692 fn arg(arg: &'a SpecArg) -> Self {
693 Self {
694 kind: "arg",
695 name: &arg.name,
696 }
697 }
698
699 fn option(flag: &'a SpecFlag) -> Self {
700 Self {
701 kind: "option",
702 name: &flag.name,
703 }
704 }
705}
706
707fn choice_error(
708 target: ChoiceTarget<'_>,
709 value: &str,
710 choices: Option<&SpecChoices>,
711 custom_env: Option<&HashMap<String, String>>,
712) -> Option<String> {
713 let choices = choices?;
714 let values = choices.values_with_env(custom_env);
715 if values.iter().any(|choice| choice == value) {
716 return None;
717 }
718 if let Some(env) = choices.env() {
719 if values.is_empty() {
720 return Some(format!(
721 "Invalid choice for {} {}: {value}, no choices resolved from env {env}",
722 target.kind, target.name,
723 ));
724 }
725 }
726 Some(format!(
727 "Invalid choice for {} {}: {value}, expected one of {}",
728 target.kind,
729 target.name,
730 values.join(", ")
731 ))
732}
733
734fn validate_choices(
735 spec: &Spec,
736 cmd: &SpecCommand,
737 errors: &mut Vec<UsageErr>,
738 target: ChoiceTarget<'_>,
739 value: &str,
740 choices: Option<&SpecChoices>,
741 custom_env: Option<&HashMap<String, String>>,
742) -> miette::Result<bool> {
743 if is_help_arg(spec, value)
744 && choices.is_some_and(|choices| {
745 !choices
746 .values_with_env(custom_env)
747 .iter()
748 .any(|choice| choice == value)
749 })
750 {
751 errors.push(render_help_err(spec, cmd, value.len() > 2));
752 return Ok(true);
753 }
754
755 if let Some(err) = choice_error(target, value, choices, custom_env) {
756 bail!("{err}");
757 }
758 Ok(false)
759}
760
761fn validate_choice_value(
762 target: ChoiceTarget<'_>,
763 value: &str,
764 choices: Option<&SpecChoices>,
765 custom_env: Option<&HashMap<String, String>>,
766) -> miette::Result<()> {
767 if let Some(err) = choice_error(target, value, choices, custom_env) {
768 bail!("{err}");
769 }
770 Ok(())
771}
772
773fn validate_choice_values(
774 target: ChoiceTarget<'_>,
775 values: &[String],
776 choices: Option<&SpecChoices>,
777 custom_env: Option<&HashMap<String, String>>,
778) -> miette::Result<()> {
779 for value in values {
780 validate_choice_value(target, value, choices, custom_env)?;
781 }
782 Ok(())
783}
784
785fn is_help_arg(spec: &Spec, w: &str) -> bool {
786 spec.disable_help != Some(true)
787 && (w == "--help"
788 || w == "-h"
789 || w == "-?"
790 || (spec.cmd.subcommands.is_empty() && w == "help"))
791}
792
793impl ParseOutput {
794 pub fn as_env(&self) -> BTreeMap<String, String> {
795 let mut env = BTreeMap::new();
796 for (flag, val) in &self.flags {
797 let key = format!("usage_{}", flag.name.to_snake_case());
798 let val = match val {
799 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
800 ParseValue::String(s) => s.clone(),
801 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
802 ParseValue::MultiString(s) => shell_words::join(s),
803 };
804 env.insert(key, val);
805 }
806 for (arg, val) in &self.args {
807 let key = format!("usage_{}", arg.name.to_snake_case());
808 env.insert(key, val.to_string());
809 }
810 env
811 }
812}
813
814impl Display for ParseValue {
815 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
816 match self {
817 ParseValue::Bool(b) => write!(f, "{b}"),
818 ParseValue::String(s) => write!(f, "{s}"),
819 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
820 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
821 }
822 }
823}
824
825impl Debug for ParseOutput {
826 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
827 f.debug_struct("ParseOutput")
828 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
829 .field(
830 "args",
831 &self
832 .args
833 .iter()
834 .map(|(a, w)| format!("{}: {w}", &a.name))
835 .collect_vec(),
836 )
837 .field(
838 "available_flags",
839 &self
840 .available_flags
841 .iter()
842 .map(|(f, w)| format!("{f}: {w}"))
843 .collect_vec(),
844 )
845 .field(
846 "flags",
847 &self
848 .flags
849 .iter()
850 .map(|(f, w)| format!("{}: {w}", &f.name))
851 .collect_vec(),
852 )
853 .field("flag_awaiting_value", &self.flag_awaiting_value)
854 .field("errors", &self.errors)
855 .finish()
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862
863 fn input(words: &[&str]) -> Vec<String> {
864 words.iter().map(|word| (*word).to_string()).collect()
865 }
866
867 fn spec_with_arg(arg: SpecArg) -> Spec {
868 let cmd = SpecCommand::builder().name("test").arg(arg).build();
869 Spec {
870 name: "test".to_string(),
871 bin: "test".to_string(),
872 cmd,
873 ..Default::default()
874 }
875 }
876
877 fn spec_with_flag(flag: SpecFlag) -> Spec {
878 let cmd = SpecCommand::builder().name("test").flag(flag).build();
879 Spec {
880 name: "test".to_string(),
881 bin: "test".to_string(),
882 cmd,
883 ..Default::default()
884 }
885 }
886
887 fn parse_with_env(
888 spec: &Spec,
889 words: &[&str],
890 env: &[(&str, &str)],
891 ) -> Result<ParseOutput, miette::Error> {
892 let env = env
893 .iter()
894 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
895 .collect();
896 Parser::new(spec).with_env(env).parse(&input(words))
897 }
898
899 fn first_string_value(parsed: &ParseOutput) -> &str {
900 if let Some(ParseValue::String(value)) = parsed.args.values().next() {
901 return value;
902 }
903 if let Some(ParseValue::String(value)) = parsed.flags.values().next() {
904 return value;
905 }
906 panic!("expected first parsed value to be ParseValue::String");
907 }
908
909 fn assert_parse_err(result: Result<ParseOutput, miette::Error>, expected: &str) {
910 let err = result.expect_err("expected parser error");
911 assert_eq!(format!("{err}"), expected);
912 }
913
914 #[cfg(feature = "unstable_choices_env")]
915 fn spec_arg_choices_env(key: &str) -> Spec {
916 spec_with_arg(
917 SpecArg::builder()
918 .name("env")
919 .choices_env(key)
920 .required(false)
921 .build(),
922 )
923 }
924
925 #[cfg(feature = "unstable_choices_env")]
926 fn spec_flag_choices_env(key: &str) -> Spec {
927 spec_with_flag(
928 SpecFlag::builder()
929 .long("env")
930 .arg(SpecArg::builder().name("env").choices_env(key).build())
931 .build(),
932 )
933 }
934
935 #[test]
936 fn test_parse() {
937 let cmd = SpecCommand::builder()
938 .name("test")
939 .arg(SpecArg::builder().name("arg").build())
940 .flag(SpecFlag::builder().long("flag").build())
941 .build();
942 let spec = Spec {
943 name: "test".to_string(),
944 bin: "test".to_string(),
945 cmd,
946 ..Default::default()
947 };
948 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
949 let parsed = parse(&spec, &input).unwrap();
950 assert_eq!(parsed.cmds.len(), 1);
951 assert_eq!(parsed.cmds[0].name, "test");
952 assert_eq!(parsed.args.len(), 1);
953 assert_eq!(parsed.flags.len(), 1);
954 assert_eq!(parsed.available_flags.len(), 1);
955 }
956
957 #[test]
958 fn test_as_env() {
959 let cmd = SpecCommand::builder()
960 .name("test")
961 .arg(SpecArg::builder().name("arg").build())
962 .flag(SpecFlag::builder().long("flag").build())
963 .flag(
964 SpecFlag::builder()
965 .long("force")
966 .negate("--no-force")
967 .build(),
968 )
969 .build();
970 let spec = Spec {
971 name: "test".to_string(),
972 bin: "test".to_string(),
973 cmd,
974 ..Default::default()
975 };
976 let input = vec![
977 "test".to_string(),
978 "--flag".to_string(),
979 "--no-force".to_string(),
980 ];
981 let parsed = parse(&spec, &input).unwrap();
982 let env = parsed.as_env();
983 assert_eq!(env.len(), 2);
984 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
985 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
986 }
987
988 #[test]
989 fn test_arg_env_var() {
990 let cmd = SpecCommand::builder()
991 .name("test")
992 .arg(
993 SpecArg::builder()
994 .name("input")
995 .env("TEST_ARG_INPUT")
996 .required(true)
997 .build(),
998 )
999 .build();
1000 let spec = Spec {
1001 name: "test".to_string(),
1002 bin: "test".to_string(),
1003 cmd,
1004 ..Default::default()
1005 };
1006
1007 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
1009
1010 let input = vec!["test".to_string()];
1011 let parsed = parse(&spec, &input).unwrap();
1012
1013 assert_eq!(parsed.args.len(), 1);
1014 let arg = parsed.args.keys().next().unwrap();
1015 assert_eq!(arg.name, "input");
1016 let value = parsed.args.values().next().unwrap();
1017 assert_eq!(value.to_string(), "test_file.txt");
1018
1019 std::env::remove_var("TEST_ARG_INPUT");
1021 }
1022
1023 #[test]
1024 fn test_flag_env_var_with_arg() {
1025 let cmd = SpecCommand::builder()
1026 .name("test")
1027 .flag(
1028 SpecFlag::builder()
1029 .long("output")
1030 .env("TEST_FLAG_OUTPUT")
1031 .arg(SpecArg::builder().name("file").build())
1032 .build(),
1033 )
1034 .build();
1035 let spec = Spec {
1036 name: "test".to_string(),
1037 bin: "test".to_string(),
1038 cmd,
1039 ..Default::default()
1040 };
1041
1042 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
1044
1045 let input = vec!["test".to_string()];
1046 let parsed = parse(&spec, &input).unwrap();
1047
1048 assert_eq!(parsed.flags.len(), 1);
1049 let flag = parsed.flags.keys().next().unwrap();
1050 assert_eq!(flag.name, "output");
1051 let value = parsed.flags.values().next().unwrap();
1052 assert_eq!(value.to_string(), "output.txt");
1053
1054 std::env::remove_var("TEST_FLAG_OUTPUT");
1056 }
1057
1058 #[test]
1059 fn test_flag_env_var_boolean() {
1060 let cmd = SpecCommand::builder()
1061 .name("test")
1062 .flag(
1063 SpecFlag::builder()
1064 .long("verbose")
1065 .env("TEST_FLAG_VERBOSE")
1066 .build(),
1067 )
1068 .build();
1069 let spec = Spec {
1070 name: "test".to_string(),
1071 bin: "test".to_string(),
1072 cmd,
1073 ..Default::default()
1074 };
1075
1076 std::env::set_var("TEST_FLAG_VERBOSE", "true");
1078
1079 let input = vec!["test".to_string()];
1080 let parsed = parse(&spec, &input).unwrap();
1081
1082 assert_eq!(parsed.flags.len(), 1);
1083 let flag = parsed.flags.keys().next().unwrap();
1084 assert_eq!(flag.name, "verbose");
1085 let value = parsed.flags.values().next().unwrap();
1086 assert_eq!(value.to_string(), "true");
1087
1088 std::env::remove_var("TEST_FLAG_VERBOSE");
1090 }
1091
1092 #[test]
1093 fn test_env_var_precedence() {
1094 let cmd = SpecCommand::builder()
1096 .name("test")
1097 .arg(
1098 SpecArg::builder()
1099 .name("input")
1100 .env("TEST_PRECEDENCE_INPUT")
1101 .required(true)
1102 .build(),
1103 )
1104 .build();
1105 let spec = Spec {
1106 name: "test".to_string(),
1107 bin: "test".to_string(),
1108 cmd,
1109 ..Default::default()
1110 };
1111
1112 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
1114
1115 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
1116 let parsed = parse(&spec, &input).unwrap();
1117
1118 assert_eq!(parsed.args.len(), 1);
1119 let value = parsed.args.values().next().unwrap();
1120 assert_eq!(value.to_string(), "cli_file.txt");
1122
1123 std::env::remove_var("TEST_PRECEDENCE_INPUT");
1125 }
1126
1127 #[test]
1128 fn test_flag_var_true_with_single_default() {
1129 let cmd = SpecCommand::builder()
1131 .name("test")
1132 .flag(
1133 SpecFlag::builder()
1134 .long("foo")
1135 .var(true)
1136 .arg(SpecArg::builder().name("foo").build())
1137 .default_value("bar")
1138 .build(),
1139 )
1140 .build();
1141 let spec = Spec {
1142 name: "test".to_string(),
1143 bin: "test".to_string(),
1144 cmd,
1145 ..Default::default()
1146 };
1147
1148 let input = vec!["test".to_string()];
1150 let parsed = parse(&spec, &input).unwrap();
1151
1152 assert_eq!(parsed.flags.len(), 1);
1153 let flag = parsed.flags.keys().next().unwrap();
1154 assert_eq!(flag.name, "foo");
1155 let value = parsed.flags.values().next().unwrap();
1156 match value {
1158 ParseValue::MultiString(v) => {
1159 assert_eq!(v.len(), 1);
1160 assert_eq!(v[0], "bar");
1161 }
1162 _ => panic!("Expected MultiString, got {:?}", value),
1163 }
1164 }
1165
1166 #[test]
1167 fn test_flag_var_true_with_multiple_defaults() {
1168 let cmd = SpecCommand::builder()
1170 .name("test")
1171 .flag(
1172 SpecFlag::builder()
1173 .long("foo")
1174 .var(true)
1175 .arg(SpecArg::builder().name("foo").build())
1176 .default_values(["xyz", "bar"])
1177 .build(),
1178 )
1179 .build();
1180 let spec = Spec {
1181 name: "test".to_string(),
1182 bin: "test".to_string(),
1183 cmd,
1184 ..Default::default()
1185 };
1186
1187 let input = vec!["test".to_string()];
1189 let parsed = parse(&spec, &input).unwrap();
1190
1191 assert_eq!(parsed.flags.len(), 1);
1192 let value = parsed.flags.values().next().unwrap();
1193 match value {
1195 ParseValue::MultiString(v) => {
1196 assert_eq!(v.len(), 2);
1197 assert_eq!(v[0], "xyz");
1198 assert_eq!(v[1], "bar");
1199 }
1200 _ => panic!("Expected MultiString, got {:?}", value),
1201 }
1202 }
1203
1204 #[test]
1205 fn test_flag_var_false_with_default_remains_string() {
1206 let cmd = SpecCommand::builder()
1208 .name("test")
1209 .flag(
1210 SpecFlag::builder()
1211 .long("foo")
1212 .var(false) .arg(SpecArg::builder().name("foo").build())
1214 .default_value("bar")
1215 .build(),
1216 )
1217 .build();
1218 let spec = Spec {
1219 name: "test".to_string(),
1220 bin: "test".to_string(),
1221 cmd,
1222 ..Default::default()
1223 };
1224
1225 let input = vec!["test".to_string()];
1227 let parsed = parse(&spec, &input).unwrap();
1228
1229 assert_eq!(parsed.flags.len(), 1);
1230 let value = parsed.flags.values().next().unwrap();
1231 match value {
1233 ParseValue::String(s) => {
1234 assert_eq!(s, "bar");
1235 }
1236 _ => panic!("Expected String, got {:?}", value),
1237 }
1238 }
1239
1240 #[test]
1241 fn test_arg_var_true_with_single_default() {
1242 let cmd = SpecCommand::builder()
1244 .name("test")
1245 .arg(
1246 SpecArg::builder()
1247 .name("files")
1248 .var(true)
1249 .default_value("default.txt")
1250 .required(false)
1251 .build(),
1252 )
1253 .build();
1254 let spec = Spec {
1255 name: "test".to_string(),
1256 bin: "test".to_string(),
1257 cmd,
1258 ..Default::default()
1259 };
1260
1261 let input = vec!["test".to_string()];
1263 let parsed = parse(&spec, &input).unwrap();
1264
1265 assert_eq!(parsed.args.len(), 1);
1266 let value = parsed.args.values().next().unwrap();
1267 match value {
1269 ParseValue::MultiString(v) => {
1270 assert_eq!(v.len(), 1);
1271 assert_eq!(v[0], "default.txt");
1272 }
1273 _ => panic!("Expected MultiString, got {:?}", value),
1274 }
1275 }
1276
1277 #[test]
1278 fn test_arg_var_true_with_multiple_defaults() {
1279 let cmd = SpecCommand::builder()
1281 .name("test")
1282 .arg(
1283 SpecArg::builder()
1284 .name("files")
1285 .var(true)
1286 .default_values(["file1.txt", "file2.txt"])
1287 .required(false)
1288 .build(),
1289 )
1290 .build();
1291 let spec = Spec {
1292 name: "test".to_string(),
1293 bin: "test".to_string(),
1294 cmd,
1295 ..Default::default()
1296 };
1297
1298 let input = vec!["test".to_string()];
1300 let parsed = parse(&spec, &input).unwrap();
1301
1302 assert_eq!(parsed.args.len(), 1);
1303 let value = parsed.args.values().next().unwrap();
1304 match value {
1306 ParseValue::MultiString(v) => {
1307 assert_eq!(v.len(), 2);
1308 assert_eq!(v[0], "file1.txt");
1309 assert_eq!(v[1], "file2.txt");
1310 }
1311 _ => panic!("Expected MultiString, got {:?}", value),
1312 }
1313 }
1314
1315 #[test]
1316 fn test_arg_var_false_with_default_remains_string() {
1317 let cmd = SpecCommand::builder()
1319 .name("test")
1320 .arg(
1321 SpecArg::builder()
1322 .name("file")
1323 .var(false)
1324 .default_value("default.txt")
1325 .required(false)
1326 .build(),
1327 )
1328 .build();
1329 let spec = Spec {
1330 name: "test".to_string(),
1331 bin: "test".to_string(),
1332 cmd,
1333 ..Default::default()
1334 };
1335
1336 let input = vec!["test".to_string()];
1338 let parsed = parse(&spec, &input).unwrap();
1339
1340 assert_eq!(parsed.args.len(), 1);
1341 let value = parsed.args.values().next().unwrap();
1342 match value {
1344 ParseValue::String(s) => {
1345 assert_eq!(s, "default.txt");
1346 }
1347 _ => panic!("Expected String, got {:?}", value),
1348 }
1349 }
1350
1351 #[test]
1352 fn test_scalar_defaults_validate_only_first_default_choice() {
1353 let specs = [
1354 spec_with_arg(
1355 SpecArg::builder()
1356 .name("env")
1357 .var(false)
1358 .default_values(["dev", "prod"])
1359 .choices(["dev"])
1360 .required(false)
1361 .build(),
1362 ),
1363 spec_with_flag(
1364 SpecFlag::builder()
1365 .long("env")
1366 .arg(
1367 SpecArg::builder()
1368 .name("env")
1369 .default_values(["dev", "prod"])
1370 .choices(["dev"])
1371 .build(),
1372 )
1373 .build(),
1374 ),
1375 ];
1376
1377 for spec in specs {
1378 let parsed = parse(&spec, &input(&["test"])).unwrap();
1379 assert_eq!(first_string_value(&parsed), "dev");
1380 }
1381 }
1382
1383 #[test]
1384 fn test_default_subcommand() {
1385 let run_cmd = SpecCommand::builder()
1387 .name("run")
1388 .arg(SpecArg::builder().name("task").build())
1389 .build();
1390 let mut cmd = SpecCommand::builder().name("test").build();
1391 cmd.subcommands.insert("run".to_string(), run_cmd);
1392
1393 let spec = Spec {
1394 name: "test".to_string(),
1395 bin: "test".to_string(),
1396 cmd,
1397 default_subcommand: Some("run".to_string()),
1398 ..Default::default()
1399 };
1400
1401 let input = vec!["test".to_string(), "mytask".to_string()];
1403 let parsed = parse(&spec, &input).unwrap();
1404
1405 assert_eq!(parsed.cmds.len(), 2);
1407 assert_eq!(parsed.cmds[1].name, "run");
1408
1409 assert_eq!(parsed.args.len(), 1);
1411 let arg = parsed.args.keys().next().unwrap();
1412 assert_eq!(arg.name, "task");
1413 let value = parsed.args.values().next().unwrap();
1414 assert_eq!(value.to_string(), "mytask");
1415 }
1416
1417 #[test]
1418 fn test_default_subcommand_explicit_still_works() {
1419 let run_cmd = SpecCommand::builder()
1421 .name("run")
1422 .arg(SpecArg::builder().name("task").build())
1423 .build();
1424 let other_cmd = SpecCommand::builder()
1425 .name("other")
1426 .arg(SpecArg::builder().name("other_arg").build())
1427 .build();
1428 let mut cmd = SpecCommand::builder().name("test").build();
1429 cmd.subcommands.insert("run".to_string(), run_cmd);
1430 cmd.subcommands.insert("other".to_string(), other_cmd);
1431
1432 let spec = Spec {
1433 name: "test".to_string(),
1434 bin: "test".to_string(),
1435 cmd,
1436 default_subcommand: Some("run".to_string()),
1437 ..Default::default()
1438 };
1439
1440 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1442 let parsed = parse(&spec, &input).unwrap();
1443
1444 assert_eq!(parsed.cmds.len(), 2);
1446 assert_eq!(parsed.cmds[1].name, "other");
1447 }
1448
1449 #[test]
1450 fn test_default_subcommand_with_nested_subcommands() {
1451 let say_cmd = SpecCommand::builder()
1455 .name("say")
1456 .arg(SpecArg::builder().name("name").build())
1457 .build();
1458 let mut run_cmd = SpecCommand::builder().name("run").build();
1459 run_cmd.subcommands.insert("say".to_string(), say_cmd);
1460
1461 let mut cmd = SpecCommand::builder().name("test").build();
1462 cmd.subcommands.insert("run".to_string(), run_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(), "say".to_string(), "hello".to_string()];
1474 let parsed = parse(&spec, &input).unwrap();
1475
1476 assert_eq!(parsed.cmds.len(), 3);
1478 assert_eq!(parsed.cmds[0].name, "test");
1479 assert_eq!(parsed.cmds[1].name, "run");
1480 assert_eq!(parsed.cmds[2].name, "say");
1481
1482 assert_eq!(parsed.args.len(), 1);
1484 let arg = parsed.args.keys().next().unwrap();
1485 assert_eq!(arg.name, "name");
1486 let value = parsed.args.values().next().unwrap();
1487 assert_eq!(value.to_string(), "hello");
1488 }
1489
1490 #[test]
1491 fn test_default_subcommand_same_name_child() {
1492 let run_task = SpecCommand::builder()
1496 .name("run")
1497 .arg(SpecArg::builder().name("args").build())
1498 .build();
1499 let mut run_cmd = SpecCommand::builder().name("run").build();
1500 run_cmd.subcommands.insert("run".to_string(), run_task);
1501
1502 let mut cmd = SpecCommand::builder().name("test").build();
1503 cmd.subcommands.insert("run".to_string(), run_cmd);
1504
1505 let spec = Spec {
1506 name: "test".to_string(),
1507 bin: "test".to_string(),
1508 cmd,
1509 default_subcommand: Some("run".to_string()),
1510 ..Default::default()
1511 };
1512
1513 let input = vec!["test".to_string(), "run".to_string()];
1515 let parsed = parse(&spec, &input).unwrap();
1516
1517 assert_eq!(parsed.cmds.len(), 2);
1519 assert_eq!(parsed.cmds[0].name, "test");
1520 assert_eq!(parsed.cmds[1].name, "run");
1521
1522 let input = vec![
1524 "test".to_string(),
1525 "run".to_string(),
1526 "run".to_string(),
1527 "hello".to_string(),
1528 ];
1529 let parsed = parse(&spec, &input).unwrap();
1530
1531 assert_eq!(parsed.cmds.len(), 3);
1532 assert_eq!(parsed.cmds[0].name, "test");
1533 assert_eq!(parsed.cmds[1].name, "run");
1534 assert_eq!(parsed.cmds[2].name, "run");
1535 assert_eq!(parsed.args.len(), 1);
1536 let value = parsed.args.values().next().unwrap();
1537 assert_eq!(value.to_string(), "hello");
1538
1539 let mut run_cmd = SpecCommand::builder()
1543 .name("run")
1544 .arg(SpecArg::builder().name("task").build())
1545 .build();
1546 let run_task = SpecCommand::builder().name("run").build();
1547 run_cmd.subcommands.insert("run".to_string(), run_task);
1548
1549 let mut cmd = SpecCommand::builder().name("test").build();
1550 cmd.subcommands.insert("run".to_string(), run_cmd);
1551
1552 let spec = Spec {
1553 name: "test".to_string(),
1554 bin: "test".to_string(),
1555 cmd,
1556 default_subcommand: Some("run".to_string()),
1557 ..Default::default()
1558 };
1559
1560 let input = vec!["test".to_string(), "other".to_string()];
1561 let parsed = parse(&spec, &input).unwrap();
1562
1563 assert_eq!(parsed.cmds.len(), 2);
1566 assert_eq!(parsed.cmds[0].name, "test");
1567 assert_eq!(parsed.cmds[1].name, "run");
1568
1569 assert_eq!(parsed.args.len(), 1);
1571 let value = parsed.args.values().next().unwrap();
1572 assert_eq!(value.to_string(), "other");
1573 }
1574
1575 #[test]
1576 fn test_restart_token() {
1577 let run_cmd = SpecCommand::builder()
1579 .name("run")
1580 .arg(SpecArg::builder().name("task").build())
1581 .restart_token(":::".to_string())
1582 .build();
1583 let mut cmd = SpecCommand::builder().name("test").build();
1584 cmd.subcommands.insert("run".to_string(), run_cmd);
1585
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![
1595 "test".to_string(),
1596 "run".to_string(),
1597 "task1".to_string(),
1598 ":::".to_string(),
1599 "task2".to_string(),
1600 ];
1601 let parsed = parse(&spec, &input).unwrap();
1602
1603 assert_eq!(parsed.args.len(), 1);
1605 let value = parsed.args.values().next().unwrap();
1606 assert_eq!(value.to_string(), "task2");
1607 }
1608
1609 #[test]
1610 fn test_restart_token_multiple() {
1611 let run_cmd = SpecCommand::builder()
1613 .name("run")
1614 .arg(SpecArg::builder().name("task").build())
1615 .restart_token(":::".to_string())
1616 .build();
1617 let mut cmd = SpecCommand::builder().name("test").build();
1618 cmd.subcommands.insert("run".to_string(), run_cmd);
1619
1620 let spec = Spec {
1621 name: "test".to_string(),
1622 bin: "test".to_string(),
1623 cmd,
1624 ..Default::default()
1625 };
1626
1627 let input = vec![
1629 "test".to_string(),
1630 "run".to_string(),
1631 "task1".to_string(),
1632 ":::".to_string(),
1633 "task2".to_string(),
1634 ":::".to_string(),
1635 "task3".to_string(),
1636 ];
1637 let parsed = parse(&spec, &input).unwrap();
1638
1639 assert_eq!(parsed.args.len(), 1);
1641 let value = parsed.args.values().next().unwrap();
1642 assert_eq!(value.to_string(), "task3");
1643 }
1644
1645 #[test]
1646 fn test_restart_token_clears_flag_awaiting_value() {
1647 let run_cmd = SpecCommand::builder()
1649 .name("run")
1650 .arg(SpecArg::builder().name("task").build())
1651 .flag(
1652 SpecFlag::builder()
1653 .name("jobs")
1654 .long("jobs")
1655 .arg(SpecArg::builder().name("count").build())
1656 .build(),
1657 )
1658 .restart_token(":::".to_string())
1659 .build();
1660 let mut cmd = SpecCommand::builder().name("test").build();
1661 cmd.subcommands.insert("run".to_string(), run_cmd);
1662
1663 let spec = Spec {
1664 name: "test".to_string(),
1665 bin: "test".to_string(),
1666 cmd,
1667 ..Default::default()
1668 };
1669
1670 let input = vec![
1672 "test".to_string(),
1673 "run".to_string(),
1674 "task1".to_string(),
1675 "--jobs".to_string(),
1676 ":::".to_string(),
1677 "task2".to_string(),
1678 ];
1679 let parsed = parse(&spec, &input).unwrap();
1680
1681 assert_eq!(parsed.args.len(), 1);
1683 let value = parsed.args.values().next().unwrap();
1684 assert_eq!(value.to_string(), "task2");
1685 assert!(parsed.flag_awaiting_value.is_empty());
1687 }
1688
1689 #[test]
1690 fn test_restart_token_resets_double_dash() {
1691 let run_cmd = SpecCommand::builder()
1693 .name("run")
1694 .arg(SpecArg::builder().name("task").build())
1695 .arg(SpecArg::builder().name("extra_args").var(true).build())
1696 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1697 .restart_token(":::".to_string())
1698 .build();
1699 let mut cmd = SpecCommand::builder().name("test").build();
1700 cmd.subcommands.insert("run".to_string(), run_cmd);
1701
1702 let spec = Spec {
1703 name: "test".to_string(),
1704 bin: "test".to_string(),
1705 cmd,
1706 ..Default::default()
1707 };
1708
1709 let input = vec![
1711 "test".to_string(),
1712 "run".to_string(),
1713 "task1".to_string(),
1714 "--".to_string(),
1715 "extra".to_string(),
1716 ":::".to_string(),
1717 "--verbose".to_string(),
1718 "task2".to_string(),
1719 ];
1720 let parsed = parse(&spec, &input).unwrap();
1721
1722 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1724 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1726 let value = parsed.args.get(task_arg).unwrap();
1727 assert_eq!(value.to_string(), "task2");
1728 }
1729
1730 #[test]
1731 fn test_double_dashes_without_preserve() {
1732 let run_cmd = SpecCommand::builder()
1734 .name("run")
1735 .arg(SpecArg::builder().name("args").var(true).build())
1736 .build();
1737 let mut cmd = SpecCommand::builder().name("test").build();
1738 cmd.subcommands.insert("run".to_string(), run_cmd);
1739
1740 let spec = Spec {
1741 name: "test".to_string(),
1742 bin: "test".to_string(),
1743 cmd,
1744 ..Default::default()
1745 };
1746
1747 let input = vec![
1749 "test".to_string(),
1750 "run".to_string(),
1751 "arg1".to_string(),
1752 "--".to_string(),
1753 "arg2".to_string(),
1754 "--".to_string(),
1755 "arg3".to_string(),
1756 ];
1757 let parsed = parse(&spec, &input).unwrap();
1758
1759 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1760 let value = parsed.args.get(args_arg).unwrap();
1761 assert_eq!(value.to_string(), "arg1 arg2 arg3");
1762 }
1763
1764 #[test]
1765 fn test_double_dashes_with_preserve() {
1766 let run_cmd = SpecCommand::builder()
1768 .name("run")
1769 .arg(
1770 SpecArg::builder()
1771 .name("args")
1772 .var(true)
1773 .double_dash(SpecDoubleDashChoices::Preserve)
1774 .build(),
1775 )
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 "arg1".to_string(),
1792 "--".to_string(),
1793 "arg2".to_string(),
1794 "--".to_string(),
1795 "arg3".to_string(),
1796 ];
1797 let parsed = parse(&spec, &input).unwrap();
1798
1799 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1800 let value = parsed.args.get(args_arg).unwrap();
1801 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1802 }
1803
1804 #[test]
1805 fn test_double_dashes_with_preserve_only_dashes() {
1806 let run_cmd = SpecCommand::builder()
1809 .name("run")
1810 .arg(
1811 SpecArg::builder()
1812 .name("args")
1813 .var(true)
1814 .double_dash(SpecDoubleDashChoices::Preserve)
1815 .build(),
1816 )
1817 .build();
1818 let mut cmd = SpecCommand::builder().name("test").build();
1819 cmd.subcommands.insert("run".to_string(), run_cmd);
1820
1821 let spec = Spec {
1822 name: "test".to_string(),
1823 bin: "test".to_string(),
1824 cmd,
1825 ..Default::default()
1826 };
1827
1828 let input = vec![
1830 "test".to_string(),
1831 "run".to_string(),
1832 "--".to_string(),
1833 "--".to_string(),
1834 ];
1835 let parsed = parse(&spec, &input).unwrap();
1836
1837 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1838 let value = parsed.args.get(args_arg).unwrap();
1839 assert_eq!(value.to_string(), "-- --");
1840 }
1841
1842 #[test]
1843 fn test_double_dashes_with_preserve_multiple_args() {
1844 let run_cmd = SpecCommand::builder()
1846 .name("run")
1847 .arg(SpecArg::builder().name("task").build())
1848 .arg(
1849 SpecArg::builder()
1850 .name("extra_args")
1851 .var(true)
1852 .double_dash(SpecDoubleDashChoices::Preserve)
1853 .build(),
1854 )
1855 .build();
1856 let mut cmd = SpecCommand::builder().name("test").build();
1857 cmd.subcommands.insert("run".to_string(), run_cmd);
1858
1859 let spec = Spec {
1860 name: "test".to_string(),
1861 bin: "test".to_string(),
1862 cmd,
1863 ..Default::default()
1864 };
1865
1866 let input = vec![
1869 "test".to_string(),
1870 "run".to_string(),
1871 "task1".to_string(),
1872 "--".to_string(),
1873 "arg1".to_string(),
1874 "--".to_string(),
1875 "--foo".to_string(),
1876 ];
1877 let parsed = parse(&spec, &input).unwrap();
1878
1879 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1880 let task_value = parsed.args.get(task_arg).unwrap();
1881 assert_eq!(task_value.to_string(), "task1");
1882
1883 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
1884 let extra_value = parsed.args.get(extra_arg).unwrap();
1885 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
1886 }
1887
1888 #[test]
1889 fn test_parser_with_custom_env_for_required_arg() {
1890 let spec = spec_with_arg(
1891 SpecArg::builder()
1892 .name("name")
1893 .env("NAME")
1894 .required(true)
1895 .build(),
1896 );
1897 std::env::remove_var("NAME");
1898
1899 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "john")])
1900 .expect("parse should succeed with custom env");
1901 assert_eq!(parsed.args.len(), 1);
1902 assert_eq!(first_string_value(&parsed), "john");
1903 }
1904
1905 #[test]
1906 fn test_parser_with_custom_env_for_required_flag() {
1907 let spec = spec_with_flag(
1908 SpecFlag::builder()
1909 .long("name")
1910 .env("NAME")
1911 .required(true)
1912 .arg(SpecArg::builder().name("name").build())
1913 .build(),
1914 );
1915 std::env::remove_var("NAME");
1916
1917 let parsed = parse_with_env(&spec, &["test"], &[("NAME", "jane")])
1918 .expect("parse should succeed with custom env");
1919 assert_eq!(parsed.flags.len(), 1);
1920 assert_eq!(first_string_value(&parsed), "jane");
1921 }
1922
1923 #[test]
1924 fn test_parser_with_custom_env_still_fails_when_missing() {
1925 let spec = spec_with_arg(
1926 SpecArg::builder()
1927 .name("name")
1928 .env("NAME")
1929 .required(true)
1930 .build(),
1931 );
1932 std::env::remove_var("NAME");
1933 assert!(parse_with_env(&spec, &["test"], &[]).is_err());
1934 }
1935
1936 #[test]
1937 fn test_parser_does_not_treat_env_choice_value_as_help() {
1938 let spec = spec_with_arg(
1939 SpecArg::builder()
1940 .name("env")
1941 .env("CURRENT_ENV")
1942 .choices(["dev", "staging"])
1943 .required(false)
1944 .build(),
1945 );
1946
1947 assert_parse_err(
1948 parse_with_env(&spec, &["test"], &[("CURRENT_ENV", "--help")]),
1949 "Invalid choice for arg env: --help, expected one of dev, staging",
1950 );
1951 }
1952
1953 #[test]
1954 fn test_parser_does_not_treat_default_choice_value_as_help() {
1955 let spec = spec_with_flag(
1956 SpecFlag::builder()
1957 .long("env")
1958 .arg(
1959 SpecArg::builder()
1960 .name("env")
1961 .choices(["dev", "staging"])
1962 .build(),
1963 )
1964 .default_value("--help")
1965 .build(),
1966 );
1967
1968 assert_parse_err(
1969 parse_with_env(&spec, &["test"], &[]),
1970 "Invalid choice for option env: --help, expected one of dev, staging",
1971 );
1972 }
1973
1974 #[cfg(feature = "unstable_choices_env")]
1975 #[test]
1976 fn test_parser_arg_choices_from_custom_env() {
1977 let spec = spec_arg_choices_env("DEPLOY_ENVS");
1978
1979 let parsed =
1980 parse_with_env(&spec, &["test", "bar"], &[("DEPLOY_ENVS", "foo,bar baz")]).unwrap();
1981 assert_eq!(first_string_value(&parsed), "bar");
1982
1983 assert_parse_err(
1984 parse_with_env(&spec, &["test", "prod"], &[("DEPLOY_ENVS", "foo,bar baz")]),
1985 "Invalid choice for arg env: prod, expected one of foo, bar, baz",
1986 );
1987 assert_parse_err(
1988 parse_with_env(&spec, &["test", "prod"], &[]),
1989 "Invalid choice for arg env: prod, no choices resolved from env DEPLOY_ENVS",
1990 );
1991 }
1992
1993 #[cfg(feature = "unstable_choices_env")]
1994 #[test]
1995 fn test_parser_validates_flag_choices_from_custom_env() {
1996 let spec = spec_flag_choices_env("DEPLOY_ENVS");
1997 let parsed = parse_with_env(
1998 &spec,
1999 &["test", "--env", "baz"],
2000 &[("DEPLOY_ENVS", "foo,bar baz")],
2001 )
2002 .unwrap();
2003 assert_eq!(first_string_value(&parsed), "baz");
2004 }
2005
2006 #[cfg(feature = "unstable_choices_env")]
2007 #[test]
2008 fn test_parser_revalidates_env_and_default_values_against_choices_env() {
2009 let arg_env_spec = spec_with_arg(
2010 SpecArg::builder()
2011 .name("env")
2012 .env("CURRENT_ENV")
2013 .choices_env("DEPLOY_ENVS")
2014 .build(),
2015 );
2016 assert_parse_err(
2017 parse_with_env(
2018 &arg_env_spec,
2019 &["test"],
2020 &[("CURRENT_ENV", "prod"), ("DEPLOY_ENVS", "dev,staging")],
2021 ),
2022 "Invalid choice for arg env: prod, expected one of dev, staging",
2023 );
2024
2025 let flag_default_spec = spec_with_flag(
2026 SpecFlag::builder()
2027 .long("env")
2028 .arg(
2029 SpecArg::builder()
2030 .name("env")
2031 .choices_env("DEPLOY_ENVS")
2032 .build(),
2033 )
2034 .default_value("prod")
2035 .build(),
2036 );
2037 assert_parse_err(
2038 parse_with_env(
2039 &flag_default_spec,
2040 &["test"],
2041 &[("DEPLOY_ENVS", "dev,staging")],
2042 ),
2043 "Invalid choice for option env: prod, expected one of dev, staging",
2044 );
2045 }
2046
2047 #[test]
2048 fn test_variadic_arg_captures_unknown_flags_from_spec_string() {
2049 let spec: Spec = r#"
2050 flag "-v --verbose" var=#true
2051 arg "[database]" default="myapp_dev"
2052 arg "[args...]"
2053 "#
2054 .parse()
2055 .unwrap();
2056 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2057 .into_iter()
2058 .map(String::from)
2059 .collect();
2060 let parsed = parse(&spec, &input).unwrap();
2061 let env = parsed.as_env();
2062 assert_eq!(env.get("usage_database").unwrap(), "mydb");
2063 assert_eq!(env.get("usage_args").unwrap(), "--host localhost");
2064 }
2065
2066 #[test]
2067 fn test_variadic_arg_captures_unknown_flags() {
2068 let cmd = SpecCommand::builder()
2069 .name("test")
2070 .flag(SpecFlag::builder().short('v').long("verbose").build())
2071 .arg(SpecArg::builder().name("database").required(false).build())
2072 .arg(
2073 SpecArg::builder()
2074 .name("args")
2075 .required(false)
2076 .var(true)
2077 .build(),
2078 )
2079 .build();
2080 let spec = Spec {
2081 name: "test".to_string(),
2082 bin: "test".to_string(),
2083 cmd,
2084 ..Default::default()
2085 };
2086
2087 let input: Vec<String> = vec!["test", "mydb", "--host", "localhost"]
2089 .into_iter()
2090 .map(String::from)
2091 .collect();
2092 let parsed = parse(&spec, &input).unwrap();
2093 assert_eq!(parsed.args.len(), 2);
2094 let args_val = parsed
2095 .args
2096 .iter()
2097 .find(|(a, _)| a.name == "args")
2098 .unwrap()
2099 .1;
2100 match args_val {
2101 ParseValue::MultiString(v) => {
2102 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2103 }
2104 _ => panic!("Expected MultiString, got {:?}", args_val),
2105 }
2106 }
2107
2108 #[test]
2109 fn test_variadic_arg_captures_unknown_flags_with_double_dash() {
2110 let cmd = SpecCommand::builder()
2111 .name("test")
2112 .flag(SpecFlag::builder().short('v').long("verbose").build())
2113 .arg(SpecArg::builder().name("database").required(false).build())
2114 .arg(
2115 SpecArg::builder()
2116 .name("args")
2117 .required(false)
2118 .var(true)
2119 .build(),
2120 )
2121 .build();
2122 let spec = Spec {
2123 name: "test".to_string(),
2124 bin: "test".to_string(),
2125 cmd,
2126 ..Default::default()
2127 };
2128
2129 let input: Vec<String> = vec!["test", "--", "mydb", "--host", "localhost"]
2131 .into_iter()
2132 .map(String::from)
2133 .collect();
2134 let parsed = parse(&spec, &input).unwrap();
2135 assert_eq!(parsed.args.len(), 2);
2136 let args_val = parsed
2137 .args
2138 .iter()
2139 .find(|(a, _)| a.name == "args")
2140 .unwrap()
2141 .1;
2142 match args_val {
2143 ParseValue::MultiString(v) => {
2144 assert_eq!(v, &vec!["--host".to_string(), "localhost".to_string()]);
2145 }
2146 _ => panic!("Expected MultiString, got {:?}", args_val),
2147 }
2148 }
2149}