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, 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 mut out = parse_partial_with_env(self.spec, input, self.env.as_ref())?;
94 trace!("{out:?}");
95
96 let get_env = |key: &str| -> Option<String> {
97 if let Some(ref env_map) = self.env {
98 env_map.get(key).cloned()
99 } else {
100 std::env::var(key).ok()
101 }
102 };
103
104 for arg in out.cmd.args.iter().skip(out.args.len()) {
106 if let Some(env_var) = arg.env.as_ref() {
107 if let Some(env_value) = get_env(env_var) {
108 out.args
109 .insert(Arc::new(arg.clone()), ParseValue::String(env_value));
110 continue;
111 }
112 }
113 if !arg.default.is_empty() {
114 if arg.var {
116 out.args.insert(
118 Arc::new(arg.clone()),
119 ParseValue::MultiString(arg.default.clone()),
120 );
121 } else {
122 out.args.insert(
124 Arc::new(arg.clone()),
125 ParseValue::String(arg.default[0].clone()),
126 );
127 }
128 }
129 }
130
131 for flag in out.available_flags.values() {
133 if out.flags.contains_key(flag) {
134 continue;
135 }
136 if let Some(env_var) = flag.env.as_ref() {
137 if let Some(env_value) = get_env(env_var) {
138 if flag.arg.is_some() {
139 out.flags
140 .insert(Arc::clone(flag), ParseValue::String(env_value));
141 } else {
142 let is_true = matches!(env_value.as_str(), "1" | "true" | "True" | "TRUE");
144 out.flags
145 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
146 }
147 continue;
148 }
149 }
150 if !flag.default.is_empty() {
152 if flag.var {
154 if flag.arg.is_some() {
156 out.flags.insert(
157 Arc::clone(flag),
158 ParseValue::MultiString(flag.default.clone()),
159 );
160 } else {
161 let bools: Vec<bool> = flag
163 .default
164 .iter()
165 .map(|s| matches!(s.as_str(), "1" | "true" | "True" | "TRUE"))
166 .collect();
167 out.flags
168 .insert(Arc::clone(flag), ParseValue::MultiBool(bools));
169 }
170 } else {
171 if flag.arg.is_some() {
173 out.flags.insert(
174 Arc::clone(flag),
175 ParseValue::String(flag.default[0].clone()),
176 );
177 } else {
178 let is_true =
180 matches!(flag.default[0].as_str(), "1" | "true" | "True" | "TRUE");
181 out.flags
182 .insert(Arc::clone(flag), ParseValue::Bool(is_true));
183 }
184 }
185 }
186 if let Some(arg) = flag.arg.as_ref() {
188 if !out.flags.contains_key(flag) && !arg.default.is_empty() {
189 if flag.var {
190 out.flags.insert(
191 Arc::clone(flag),
192 ParseValue::MultiString(arg.default.clone()),
193 );
194 } else {
195 out.flags
196 .insert(Arc::clone(flag), ParseValue::String(arg.default[0].clone()));
197 }
198 }
199 }
200 }
201 if let Some(err) = out.errors.iter().find(|e| matches!(e, UsageErr::Help(_))) {
202 bail!("{err}");
203 }
204 if !out.errors.is_empty() {
205 bail!("{}", out.errors.iter().map(|e| e.to_string()).join("\n"));
206 }
207 Ok(out)
208 }
209}
210
211#[must_use = "parsing result should be used"]
218pub fn parse(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
219 Parser::new(spec).parse(input)
220}
221
222#[must_use = "parsing result should be used"]
226pub fn parse_partial(spec: &Spec, input: &[String]) -> Result<ParseOutput, miette::Error> {
227 parse_partial_with_env(spec, input, None)
228}
229
230fn parse_partial_with_env(
232 spec: &Spec,
233 input: &[String],
234 custom_env: Option<&HashMap<String, String>>,
235) -> Result<ParseOutput, miette::Error> {
236 trace!("parse_partial: {input:?}");
237 let mut input = input.iter().cloned().collect::<VecDeque<_>>();
238 input.pop_front();
239
240 let gather_flags = |cmd: &SpecCommand| {
241 cmd.flags
242 .iter()
243 .flat_map(|f| {
244 let f = Arc::new(f.clone()); let mut flags = f
246 .long
247 .iter()
248 .map(|l| (format!("--{l}"), Arc::clone(&f)))
249 .chain(f.short.iter().map(|s| (format!("-{s}"), Arc::clone(&f))))
250 .collect::<Vec<_>>();
251 if let Some(negate) = &f.negate {
252 flags.push((negate.clone(), Arc::clone(&f)));
253 }
254 flags
255 })
256 .collect()
257 };
258
259 let mut out = ParseOutput {
260 cmd: spec.cmd.clone(),
261 cmds: vec![spec.cmd.clone()],
262 args: IndexMap::new(),
263 flags: IndexMap::new(),
264 available_flags: gather_flags(&spec.cmd),
265 flag_awaiting_value: vec![],
266 errors: vec![],
267 };
268
269 let mut prefix_words: Vec<String> = vec![];
282 let mut idx = 0;
283
284 while idx < input.len() {
285 if let Some(subcommand) = out.cmd.find_subcommand(&input[idx]) {
286 let mut subcommand = subcommand.clone();
287 subcommand.mount(&prefix_words)?;
289 out.available_flags.retain(|_, f| f.global);
290 out.available_flags.extend(gather_flags(&subcommand));
291 input.remove(idx);
293 out.cmds.push(subcommand.clone());
294 out.cmd = subcommand.clone();
295 prefix_words.clear();
296 } else if input[idx].starts_with('-') {
299 let word = &input[idx];
301 let flag_key = get_flag_key(word);
302
303 if let Some(f) = out.available_flags.get(flag_key) {
304 if f.global {
306 prefix_words.push(input[idx].clone());
307 idx += 1;
308
309 if f.arg.is_some()
312 && !word.contains('=')
313 && idx < input.len()
314 && !input[idx].starts_with('-')
315 {
316 prefix_words.push(input[idx].clone());
317 idx += 1;
318 }
319 } else {
320 break;
324 }
325 } else {
326 break;
329 }
330 } else {
331 if let Some(default_name) = &spec.default_subcommand {
334 if let Some(subcommand) = out.cmd.find_subcommand(default_name) {
335 let mut subcommand = subcommand.clone();
336 subcommand.mount(&prefix_words)?;
338 out.available_flags.retain(|_, f| f.global);
339 out.available_flags.extend(gather_flags(&subcommand));
340 out.cmds.push(subcommand.clone());
341 out.cmd = subcommand.clone();
342 prefix_words.clear();
343 break;
346 }
347 }
348 break;
350 }
351 }
352
353 let mut next_arg = out.cmd.args.first();
358 let mut enable_flags = true;
359 let mut grouped_flag = false;
360
361 while !input.is_empty() {
362 let mut w = input.pop_front().unwrap();
363
364 if let Some(ref restart_token) = out.cmd.restart_token {
367 if w == *restart_token {
368 out.args.clear();
370 next_arg = out.cmd.args.first();
371 out.flag_awaiting_value.clear(); enable_flags = true; continue;
375 }
376 }
377
378 if w == "--" {
379 enable_flags = false;
381
382 let should_preserve = next_arg
385 .map(|arg| arg.var && arg.double_dash == SpecDoubleDashChoices::Preserve)
386 .unwrap_or(false);
387
388 if should_preserve {
389 } else {
391 continue;
393 }
394 }
395
396 if enable_flags && w.starts_with("--") {
398 grouped_flag = false;
399 let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
400 if !val.is_empty() {
401 input.push_front(val.to_string());
402 }
403 if let Some(f) = out.available_flags.get(word) {
404 if f.arg.is_some() {
405 out.flag_awaiting_value.push(Arc::clone(f));
406 } else if f.count {
407 let arr = out
408 .flags
409 .entry(Arc::clone(f))
410 .or_insert_with(|| ParseValue::MultiBool(vec![]))
411 .try_as_multi_bool_mut()
412 .unwrap();
413 arr.push(true);
414 } else {
415 let negate = f.negate.clone().unwrap_or_default();
416 out.flags
417 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
418 }
419 continue;
420 }
421 if is_help_arg(spec, &w) {
422 out.errors
423 .push(render_help_err(spec, &out.cmd, w.len() > 2));
424 return Ok(out);
425 }
426 }
427
428 if enable_flags && w.starts_with('-') && w.len() > 1 {
430 let short = w.chars().nth(1).unwrap();
431 if let Some(f) = out.available_flags.get(&format!("-{short}")) {
432 if w.len() > 2 {
433 input.push_front(format!("-{}", &w[2..]));
434 grouped_flag = true;
435 }
436 if f.arg.is_some() {
437 out.flag_awaiting_value.push(Arc::clone(f));
438 } else if f.count {
439 let arr = out
440 .flags
441 .entry(Arc::clone(f))
442 .or_insert_with(|| ParseValue::MultiBool(vec![]))
443 .try_as_multi_bool_mut()
444 .unwrap();
445 arr.push(true);
446 } else {
447 let negate = f.negate.clone().unwrap_or_default();
448 out.flags
449 .insert(Arc::clone(f), ParseValue::Bool(w != negate));
450 }
451 continue;
452 }
453 if is_help_arg(spec, &w) {
454 out.errors
455 .push(render_help_err(spec, &out.cmd, w.len() > 2));
456 return Ok(out);
457 }
458 if grouped_flag {
459 grouped_flag = false;
460 w.remove(0);
461 }
462 }
463
464 if !out.flag_awaiting_value.is_empty() {
465 while let Some(flag) = out.flag_awaiting_value.pop() {
466 let arg = flag.arg.as_ref().unwrap();
467 if flag.var {
468 let arr = out
469 .flags
470 .entry(flag)
471 .or_insert_with(|| ParseValue::MultiString(vec![]))
472 .try_as_multi_string_mut()
473 .unwrap();
474 arr.push(w);
475 } else {
476 if let Some(choices) = &arg.choices {
477 if !choices.choices.contains(&w) {
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 bail!(
484 "Invalid choice for option {}: {w}, expected one of {}",
485 flag.name,
486 choices.choices.join(", ")
487 );
488 }
489 }
490 out.flags.insert(flag, ParseValue::String(w));
491 }
492 w = "".to_string();
493 }
494 continue;
495 }
496
497 if let Some(arg) = next_arg {
498 if arg.var {
499 let arr = out
500 .args
501 .entry(Arc::new(arg.clone()))
502 .or_insert_with(|| ParseValue::MultiString(vec![]))
503 .try_as_multi_string_mut()
504 .unwrap();
505 arr.push(w);
506 if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
507 next_arg = out.cmd.args.get(out.args.len());
508 }
509 } else {
510 if let Some(choices) = &arg.choices {
511 if !choices.choices.contains(&w) {
512 if is_help_arg(spec, &w) {
513 out.errors
514 .push(render_help_err(spec, &out.cmd, w.len() > 2));
515 return Ok(out);
516 }
517 bail!(
518 "Invalid choice for arg {}: {w}, expected one of {}",
519 arg.name,
520 choices.choices.join(", ")
521 );
522 }
523 }
524 out.args
525 .insert(Arc::new(arg.clone()), ParseValue::String(w));
526 next_arg = out.cmd.args.get(out.args.len());
527 }
528 continue;
529 }
530 if is_help_arg(spec, &w) {
531 out.errors
532 .push(render_help_err(spec, &out.cmd, w.len() > 2));
533 return Ok(out);
534 }
535 bail!("unexpected word: {w}");
536 }
537
538 for arg in out.cmd.args.iter().skip(out.args.len()) {
539 if arg.required && arg.default.is_empty() {
540 let has_env = arg.env.as_ref().is_some_and(|e| {
542 custom_env.map(|env| env.contains_key(e)).unwrap_or(false)
543 || std::env::var(e).is_ok()
544 });
545 if !has_env {
546 out.errors.push(UsageErr::MissingArg(arg.name.clone()));
547 }
548 }
549 }
550
551 for flag in out.available_flags.values() {
552 if out.flags.contains_key(flag) {
553 continue;
554 }
555 let has_default =
556 !flag.default.is_empty() || flag.arg.iter().any(|a| !a.default.is_empty());
557 let has_env = flag.env.as_ref().is_some_and(|e| {
559 custom_env.map(|env| env.contains_key(e)).unwrap_or(false) || std::env::var(e).is_ok()
560 });
561 if flag.required && !has_default && !has_env {
562 out.errors.push(UsageErr::MissingFlag(flag.name.clone()));
563 }
564 }
565
566 for (arg, value) in &out.args {
568 if arg.var {
569 if let ParseValue::MultiString(values) = value {
570 if let Some(min) = arg.var_min {
571 if values.len() < min {
572 out.errors.push(UsageErr::VarArgTooFew {
573 name: arg.name.clone(),
574 min,
575 got: values.len(),
576 });
577 }
578 }
579 if let Some(max) = arg.var_max {
580 if values.len() > max {
581 out.errors.push(UsageErr::VarArgTooMany {
582 name: arg.name.clone(),
583 max,
584 got: values.len(),
585 });
586 }
587 }
588 }
589 }
590 }
591
592 for (flag, value) in &out.flags {
594 if flag.var {
595 let count = match value {
596 ParseValue::MultiString(values) => values.len(),
597 ParseValue::MultiBool(values) => values.len(),
598 _ => continue,
599 };
600 if let Some(min) = flag.var_min {
601 if count < min {
602 out.errors.push(UsageErr::VarFlagTooFew {
603 name: flag.name.clone(),
604 min,
605 got: count,
606 });
607 }
608 }
609 if let Some(max) = flag.var_max {
610 if count > max {
611 out.errors.push(UsageErr::VarFlagTooMany {
612 name: flag.name.clone(),
613 max,
614 got: count,
615 });
616 }
617 }
618 }
619 }
620
621 Ok(out)
622}
623
624#[cfg(feature = "docs")]
625fn render_help_err(spec: &Spec, cmd: &SpecCommand, long: bool) -> UsageErr {
626 UsageErr::Help(docs::cli::render_help(spec, cmd, long))
627}
628
629#[cfg(not(feature = "docs"))]
630fn render_help_err(_spec: &Spec, _cmd: &SpecCommand, _long: bool) -> UsageErr {
631 UsageErr::Help("help".to_string())
632}
633
634fn is_help_arg(spec: &Spec, w: &str) -> bool {
635 spec.disable_help != Some(true)
636 && (w == "--help"
637 || w == "-h"
638 || w == "-?"
639 || (spec.cmd.subcommands.is_empty() && w == "help"))
640}
641
642impl ParseOutput {
643 pub fn as_env(&self) -> BTreeMap<String, String> {
644 let mut env = BTreeMap::new();
645 for (flag, val) in &self.flags {
646 let key = format!("usage_{}", flag.name.to_snake_case());
647 let val = match val {
648 ParseValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
649 ParseValue::String(s) => s.clone(),
650 ParseValue::MultiBool(b) => b.iter().filter(|b| **b).count().to_string(),
651 ParseValue::MultiString(s) => shell_words::join(s),
652 };
653 env.insert(key, val);
654 }
655 for (arg, val) in &self.args {
656 let key = format!("usage_{}", arg.name.to_snake_case());
657 env.insert(key, val.to_string());
658 }
659 env
660 }
661}
662
663impl Display for ParseValue {
664 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
665 match self {
666 ParseValue::Bool(b) => write!(f, "{b}"),
667 ParseValue::String(s) => write!(f, "{s}"),
668 ParseValue::MultiBool(b) => write!(f, "{}", b.iter().join(" ")),
669 ParseValue::MultiString(s) => write!(f, "{}", shell_words::join(s)),
670 }
671 }
672}
673
674impl Debug for ParseOutput {
675 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
676 f.debug_struct("ParseOutput")
677 .field("cmds", &self.cmds.iter().map(|c| &c.name).join(" ").trim())
678 .field(
679 "args",
680 &self
681 .args
682 .iter()
683 .map(|(a, w)| format!("{}: {w}", &a.name))
684 .collect_vec(),
685 )
686 .field(
687 "available_flags",
688 &self
689 .available_flags
690 .iter()
691 .map(|(f, w)| format!("{f}: {w}"))
692 .collect_vec(),
693 )
694 .field(
695 "flags",
696 &self
697 .flags
698 .iter()
699 .map(|(f, w)| format!("{}: {w}", &f.name))
700 .collect_vec(),
701 )
702 .field("flag_awaiting_value", &self.flag_awaiting_value)
703 .field("errors", &self.errors)
704 .finish()
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[test]
713 fn test_parse() {
714 let cmd = SpecCommand::builder()
715 .name("test")
716 .arg(SpecArg::builder().name("arg").build())
717 .flag(SpecFlag::builder().long("flag").build())
718 .build();
719 let spec = Spec {
720 name: "test".to_string(),
721 bin: "test".to_string(),
722 cmd,
723 ..Default::default()
724 };
725 let input = vec!["test".to_string(), "arg1".to_string(), "--flag".to_string()];
726 let parsed = parse(&spec, &input).unwrap();
727 assert_eq!(parsed.cmds.len(), 1);
728 assert_eq!(parsed.cmds[0].name, "test");
729 assert_eq!(parsed.args.len(), 1);
730 assert_eq!(parsed.flags.len(), 1);
731 assert_eq!(parsed.available_flags.len(), 1);
732 }
733
734 #[test]
735 fn test_as_env() {
736 let cmd = SpecCommand::builder()
737 .name("test")
738 .arg(SpecArg::builder().name("arg").build())
739 .flag(SpecFlag::builder().long("flag").build())
740 .flag(
741 SpecFlag::builder()
742 .long("force")
743 .negate("--no-force")
744 .build(),
745 )
746 .build();
747 let spec = Spec {
748 name: "test".to_string(),
749 bin: "test".to_string(),
750 cmd,
751 ..Default::default()
752 };
753 let input = vec![
754 "test".to_string(),
755 "--flag".to_string(),
756 "--no-force".to_string(),
757 ];
758 let parsed = parse(&spec, &input).unwrap();
759 let env = parsed.as_env();
760 assert_eq!(env.len(), 2);
761 assert_eq!(env.get("usage_flag"), Some(&"true".to_string()));
762 assert_eq!(env.get("usage_force"), Some(&"false".to_string()));
763 }
764
765 #[test]
766 fn test_arg_env_var() {
767 let cmd = SpecCommand::builder()
768 .name("test")
769 .arg(
770 SpecArg::builder()
771 .name("input")
772 .env("TEST_ARG_INPUT")
773 .required(true)
774 .build(),
775 )
776 .build();
777 let spec = Spec {
778 name: "test".to_string(),
779 bin: "test".to_string(),
780 cmd,
781 ..Default::default()
782 };
783
784 std::env::set_var("TEST_ARG_INPUT", "test_file.txt");
786
787 let input = vec!["test".to_string()];
788 let parsed = parse(&spec, &input).unwrap();
789
790 assert_eq!(parsed.args.len(), 1);
791 let arg = parsed.args.keys().next().unwrap();
792 assert_eq!(arg.name, "input");
793 let value = parsed.args.values().next().unwrap();
794 assert_eq!(value.to_string(), "test_file.txt");
795
796 std::env::remove_var("TEST_ARG_INPUT");
798 }
799
800 #[test]
801 fn test_flag_env_var_with_arg() {
802 let cmd = SpecCommand::builder()
803 .name("test")
804 .flag(
805 SpecFlag::builder()
806 .long("output")
807 .env("TEST_FLAG_OUTPUT")
808 .arg(SpecArg::builder().name("file").build())
809 .build(),
810 )
811 .build();
812 let spec = Spec {
813 name: "test".to_string(),
814 bin: "test".to_string(),
815 cmd,
816 ..Default::default()
817 };
818
819 std::env::set_var("TEST_FLAG_OUTPUT", "output.txt");
821
822 let input = vec!["test".to_string()];
823 let parsed = parse(&spec, &input).unwrap();
824
825 assert_eq!(parsed.flags.len(), 1);
826 let flag = parsed.flags.keys().next().unwrap();
827 assert_eq!(flag.name, "output");
828 let value = parsed.flags.values().next().unwrap();
829 assert_eq!(value.to_string(), "output.txt");
830
831 std::env::remove_var("TEST_FLAG_OUTPUT");
833 }
834
835 #[test]
836 fn test_flag_env_var_boolean() {
837 let cmd = SpecCommand::builder()
838 .name("test")
839 .flag(
840 SpecFlag::builder()
841 .long("verbose")
842 .env("TEST_FLAG_VERBOSE")
843 .build(),
844 )
845 .build();
846 let spec = Spec {
847 name: "test".to_string(),
848 bin: "test".to_string(),
849 cmd,
850 ..Default::default()
851 };
852
853 std::env::set_var("TEST_FLAG_VERBOSE", "true");
855
856 let input = vec!["test".to_string()];
857 let parsed = parse(&spec, &input).unwrap();
858
859 assert_eq!(parsed.flags.len(), 1);
860 let flag = parsed.flags.keys().next().unwrap();
861 assert_eq!(flag.name, "verbose");
862 let value = parsed.flags.values().next().unwrap();
863 assert_eq!(value.to_string(), "true");
864
865 std::env::remove_var("TEST_FLAG_VERBOSE");
867 }
868
869 #[test]
870 fn test_env_var_precedence() {
871 let cmd = SpecCommand::builder()
873 .name("test")
874 .arg(
875 SpecArg::builder()
876 .name("input")
877 .env("TEST_PRECEDENCE_INPUT")
878 .required(true)
879 .build(),
880 )
881 .build();
882 let spec = Spec {
883 name: "test".to_string(),
884 bin: "test".to_string(),
885 cmd,
886 ..Default::default()
887 };
888
889 std::env::set_var("TEST_PRECEDENCE_INPUT", "env_file.txt");
891
892 let input = vec!["test".to_string(), "cli_file.txt".to_string()];
893 let parsed = parse(&spec, &input).unwrap();
894
895 assert_eq!(parsed.args.len(), 1);
896 let value = parsed.args.values().next().unwrap();
897 assert_eq!(value.to_string(), "cli_file.txt");
899
900 std::env::remove_var("TEST_PRECEDENCE_INPUT");
902 }
903
904 #[test]
905 fn test_flag_var_true_with_single_default() {
906 let cmd = SpecCommand::builder()
908 .name("test")
909 .flag(
910 SpecFlag::builder()
911 .long("foo")
912 .var(true)
913 .arg(SpecArg::builder().name("foo").build())
914 .default_value("bar")
915 .build(),
916 )
917 .build();
918 let spec = Spec {
919 name: "test".to_string(),
920 bin: "test".to_string(),
921 cmd,
922 ..Default::default()
923 };
924
925 let input = vec!["test".to_string()];
927 let parsed = parse(&spec, &input).unwrap();
928
929 assert_eq!(parsed.flags.len(), 1);
930 let flag = parsed.flags.keys().next().unwrap();
931 assert_eq!(flag.name, "foo");
932 let value = parsed.flags.values().next().unwrap();
933 match value {
935 ParseValue::MultiString(v) => {
936 assert_eq!(v.len(), 1);
937 assert_eq!(v[0], "bar");
938 }
939 _ => panic!("Expected MultiString, got {:?}", value),
940 }
941 }
942
943 #[test]
944 fn test_flag_var_true_with_multiple_defaults() {
945 let cmd = SpecCommand::builder()
947 .name("test")
948 .flag(
949 SpecFlag::builder()
950 .long("foo")
951 .var(true)
952 .arg(SpecArg::builder().name("foo").build())
953 .default_values(["xyz", "bar"])
954 .build(),
955 )
956 .build();
957 let spec = Spec {
958 name: "test".to_string(),
959 bin: "test".to_string(),
960 cmd,
961 ..Default::default()
962 };
963
964 let input = vec!["test".to_string()];
966 let parsed = parse(&spec, &input).unwrap();
967
968 assert_eq!(parsed.flags.len(), 1);
969 let value = parsed.flags.values().next().unwrap();
970 match value {
972 ParseValue::MultiString(v) => {
973 assert_eq!(v.len(), 2);
974 assert_eq!(v[0], "xyz");
975 assert_eq!(v[1], "bar");
976 }
977 _ => panic!("Expected MultiString, got {:?}", value),
978 }
979 }
980
981 #[test]
982 fn test_flag_var_false_with_default_remains_string() {
983 let cmd = SpecCommand::builder()
985 .name("test")
986 .flag(
987 SpecFlag::builder()
988 .long("foo")
989 .var(false) .arg(SpecArg::builder().name("foo").build())
991 .default_value("bar")
992 .build(),
993 )
994 .build();
995 let spec = Spec {
996 name: "test".to_string(),
997 bin: "test".to_string(),
998 cmd,
999 ..Default::default()
1000 };
1001
1002 let input = vec!["test".to_string()];
1004 let parsed = parse(&spec, &input).unwrap();
1005
1006 assert_eq!(parsed.flags.len(), 1);
1007 let value = parsed.flags.values().next().unwrap();
1008 match value {
1010 ParseValue::String(s) => {
1011 assert_eq!(s, "bar");
1012 }
1013 _ => panic!("Expected String, got {:?}", value),
1014 }
1015 }
1016
1017 #[test]
1018 fn test_arg_var_true_with_single_default() {
1019 let cmd = SpecCommand::builder()
1021 .name("test")
1022 .arg(
1023 SpecArg::builder()
1024 .name("files")
1025 .var(true)
1026 .default_value("default.txt")
1027 .required(false)
1028 .build(),
1029 )
1030 .build();
1031 let spec = Spec {
1032 name: "test".to_string(),
1033 bin: "test".to_string(),
1034 cmd,
1035 ..Default::default()
1036 };
1037
1038 let input = vec!["test".to_string()];
1040 let parsed = parse(&spec, &input).unwrap();
1041
1042 assert_eq!(parsed.args.len(), 1);
1043 let value = parsed.args.values().next().unwrap();
1044 match value {
1046 ParseValue::MultiString(v) => {
1047 assert_eq!(v.len(), 1);
1048 assert_eq!(v[0], "default.txt");
1049 }
1050 _ => panic!("Expected MultiString, got {:?}", value),
1051 }
1052 }
1053
1054 #[test]
1055 fn test_arg_var_true_with_multiple_defaults() {
1056 let cmd = SpecCommand::builder()
1058 .name("test")
1059 .arg(
1060 SpecArg::builder()
1061 .name("files")
1062 .var(true)
1063 .default_values(["file1.txt", "file2.txt"])
1064 .required(false)
1065 .build(),
1066 )
1067 .build();
1068 let spec = Spec {
1069 name: "test".to_string(),
1070 bin: "test".to_string(),
1071 cmd,
1072 ..Default::default()
1073 };
1074
1075 let input = vec!["test".to_string()];
1077 let parsed = parse(&spec, &input).unwrap();
1078
1079 assert_eq!(parsed.args.len(), 1);
1080 let value = parsed.args.values().next().unwrap();
1081 match value {
1083 ParseValue::MultiString(v) => {
1084 assert_eq!(v.len(), 2);
1085 assert_eq!(v[0], "file1.txt");
1086 assert_eq!(v[1], "file2.txt");
1087 }
1088 _ => panic!("Expected MultiString, got {:?}", value),
1089 }
1090 }
1091
1092 #[test]
1093 fn test_arg_var_false_with_default_remains_string() {
1094 let cmd = SpecCommand::builder()
1096 .name("test")
1097 .arg(
1098 SpecArg::builder()
1099 .name("file")
1100 .var(false)
1101 .default_value("default.txt")
1102 .required(false)
1103 .build(),
1104 )
1105 .build();
1106 let spec = Spec {
1107 name: "test".to_string(),
1108 bin: "test".to_string(),
1109 cmd,
1110 ..Default::default()
1111 };
1112
1113 let input = vec!["test".to_string()];
1115 let parsed = parse(&spec, &input).unwrap();
1116
1117 assert_eq!(parsed.args.len(), 1);
1118 let value = parsed.args.values().next().unwrap();
1119 match value {
1121 ParseValue::String(s) => {
1122 assert_eq!(s, "default.txt");
1123 }
1124 _ => panic!("Expected String, got {:?}", value),
1125 }
1126 }
1127
1128 #[test]
1129 fn test_default_subcommand() {
1130 let run_cmd = SpecCommand::builder()
1132 .name("run")
1133 .arg(SpecArg::builder().name("task").build())
1134 .build();
1135 let mut cmd = SpecCommand::builder().name("test").build();
1136 cmd.subcommands.insert("run".to_string(), run_cmd);
1137
1138 let spec = Spec {
1139 name: "test".to_string(),
1140 bin: "test".to_string(),
1141 cmd,
1142 default_subcommand: Some("run".to_string()),
1143 ..Default::default()
1144 };
1145
1146 let input = vec!["test".to_string(), "mytask".to_string()];
1148 let parsed = parse(&spec, &input).unwrap();
1149
1150 assert_eq!(parsed.cmds.len(), 2);
1152 assert_eq!(parsed.cmds[1].name, "run");
1153
1154 assert_eq!(parsed.args.len(), 1);
1156 let arg = parsed.args.keys().next().unwrap();
1157 assert_eq!(arg.name, "task");
1158 let value = parsed.args.values().next().unwrap();
1159 assert_eq!(value.to_string(), "mytask");
1160 }
1161
1162 #[test]
1163 fn test_default_subcommand_explicit_still_works() {
1164 let run_cmd = SpecCommand::builder()
1166 .name("run")
1167 .arg(SpecArg::builder().name("task").build())
1168 .build();
1169 let other_cmd = SpecCommand::builder()
1170 .name("other")
1171 .arg(SpecArg::builder().name("other_arg").build())
1172 .build();
1173 let mut cmd = SpecCommand::builder().name("test").build();
1174 cmd.subcommands.insert("run".to_string(), run_cmd);
1175 cmd.subcommands.insert("other".to_string(), other_cmd);
1176
1177 let spec = Spec {
1178 name: "test".to_string(),
1179 bin: "test".to_string(),
1180 cmd,
1181 default_subcommand: Some("run".to_string()),
1182 ..Default::default()
1183 };
1184
1185 let input = vec!["test".to_string(), "other".to_string(), "foo".to_string()];
1187 let parsed = parse(&spec, &input).unwrap();
1188
1189 assert_eq!(parsed.cmds.len(), 2);
1191 assert_eq!(parsed.cmds[1].name, "other");
1192 }
1193
1194 #[test]
1195 fn test_restart_token() {
1196 let run_cmd = SpecCommand::builder()
1198 .name("run")
1199 .arg(SpecArg::builder().name("task").build())
1200 .restart_token(":::".to_string())
1201 .build();
1202 let mut cmd = SpecCommand::builder().name("test").build();
1203 cmd.subcommands.insert("run".to_string(), run_cmd);
1204
1205 let spec = Spec {
1206 name: "test".to_string(),
1207 bin: "test".to_string(),
1208 cmd,
1209 ..Default::default()
1210 };
1211
1212 let input = vec![
1214 "test".to_string(),
1215 "run".to_string(),
1216 "task1".to_string(),
1217 ":::".to_string(),
1218 "task2".to_string(),
1219 ];
1220 let parsed = parse(&spec, &input).unwrap();
1221
1222 assert_eq!(parsed.args.len(), 1);
1224 let value = parsed.args.values().next().unwrap();
1225 assert_eq!(value.to_string(), "task2");
1226 }
1227
1228 #[test]
1229 fn test_restart_token_multiple() {
1230 let run_cmd = SpecCommand::builder()
1232 .name("run")
1233 .arg(SpecArg::builder().name("task").build())
1234 .restart_token(":::".to_string())
1235 .build();
1236 let mut cmd = SpecCommand::builder().name("test").build();
1237 cmd.subcommands.insert("run".to_string(), run_cmd);
1238
1239 let spec = Spec {
1240 name: "test".to_string(),
1241 bin: "test".to_string(),
1242 cmd,
1243 ..Default::default()
1244 };
1245
1246 let input = vec![
1248 "test".to_string(),
1249 "run".to_string(),
1250 "task1".to_string(),
1251 ":::".to_string(),
1252 "task2".to_string(),
1253 ":::".to_string(),
1254 "task3".to_string(),
1255 ];
1256 let parsed = parse(&spec, &input).unwrap();
1257
1258 assert_eq!(parsed.args.len(), 1);
1260 let value = parsed.args.values().next().unwrap();
1261 assert_eq!(value.to_string(), "task3");
1262 }
1263
1264 #[test]
1265 fn test_restart_token_clears_flag_awaiting_value() {
1266 let run_cmd = SpecCommand::builder()
1268 .name("run")
1269 .arg(SpecArg::builder().name("task").build())
1270 .flag(
1271 SpecFlag::builder()
1272 .name("jobs")
1273 .long("jobs")
1274 .arg(SpecArg::builder().name("count").build())
1275 .build(),
1276 )
1277 .restart_token(":::".to_string())
1278 .build();
1279 let mut cmd = SpecCommand::builder().name("test").build();
1280 cmd.subcommands.insert("run".to_string(), run_cmd);
1281
1282 let spec = Spec {
1283 name: "test".to_string(),
1284 bin: "test".to_string(),
1285 cmd,
1286 ..Default::default()
1287 };
1288
1289 let input = vec![
1291 "test".to_string(),
1292 "run".to_string(),
1293 "task1".to_string(),
1294 "--jobs".to_string(),
1295 ":::".to_string(),
1296 "task2".to_string(),
1297 ];
1298 let parsed = parse(&spec, &input).unwrap();
1299
1300 assert_eq!(parsed.args.len(), 1);
1302 let value = parsed.args.values().next().unwrap();
1303 assert_eq!(value.to_string(), "task2");
1304 assert!(parsed.flag_awaiting_value.is_empty());
1306 }
1307
1308 #[test]
1309 fn test_restart_token_resets_double_dash() {
1310 let run_cmd = SpecCommand::builder()
1312 .name("run")
1313 .arg(SpecArg::builder().name("task").build())
1314 .arg(SpecArg::builder().name("extra_args").var(true).build())
1315 .flag(SpecFlag::builder().name("verbose").long("verbose").build())
1316 .restart_token(":::".to_string())
1317 .build();
1318 let mut cmd = SpecCommand::builder().name("test").build();
1319 cmd.subcommands.insert("run".to_string(), run_cmd);
1320
1321 let spec = Spec {
1322 name: "test".to_string(),
1323 bin: "test".to_string(),
1324 cmd,
1325 ..Default::default()
1326 };
1327
1328 let input = vec![
1330 "test".to_string(),
1331 "run".to_string(),
1332 "task1".to_string(),
1333 "--".to_string(),
1334 "extra".to_string(),
1335 ":::".to_string(),
1336 "--verbose".to_string(),
1337 "task2".to_string(),
1338 ];
1339 let parsed = parse(&spec, &input).unwrap();
1340
1341 assert!(parsed.flags.keys().any(|f| f.name == "verbose"));
1343 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1345 let value = parsed.args.get(task_arg).unwrap();
1346 assert_eq!(value.to_string(), "task2");
1347 }
1348
1349 #[test]
1350 fn test_double_dashes_without_preserve() {
1351 let run_cmd = SpecCommand::builder()
1353 .name("run")
1354 .arg(SpecArg::builder().name("args").var(true).build())
1355 .build();
1356 let mut cmd = SpecCommand::builder().name("test").build();
1357 cmd.subcommands.insert("run".to_string(), run_cmd);
1358
1359 let spec = Spec {
1360 name: "test".to_string(),
1361 bin: "test".to_string(),
1362 cmd,
1363 ..Default::default()
1364 };
1365
1366 let input = vec![
1368 "test".to_string(),
1369 "run".to_string(),
1370 "arg1".to_string(),
1371 "--".to_string(),
1372 "arg2".to_string(),
1373 "--".to_string(),
1374 "arg3".to_string(),
1375 ];
1376 let parsed = parse(&spec, &input).unwrap();
1377
1378 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1379 let value = parsed.args.get(args_arg).unwrap();
1380 assert_eq!(value.to_string(), "arg1 arg2 arg3");
1381 }
1382
1383 #[test]
1384 fn test_double_dashes_with_preserve() {
1385 let run_cmd = SpecCommand::builder()
1387 .name("run")
1388 .arg(
1389 SpecArg::builder()
1390 .name("args")
1391 .var(true)
1392 .double_dash(SpecDoubleDashChoices::Preserve)
1393 .build(),
1394 )
1395 .build();
1396 let mut cmd = SpecCommand::builder().name("test").build();
1397 cmd.subcommands.insert("run".to_string(), run_cmd);
1398
1399 let spec = Spec {
1400 name: "test".to_string(),
1401 bin: "test".to_string(),
1402 cmd,
1403 ..Default::default()
1404 };
1405
1406 let input = vec![
1408 "test".to_string(),
1409 "run".to_string(),
1410 "arg1".to_string(),
1411 "--".to_string(),
1412 "arg2".to_string(),
1413 "--".to_string(),
1414 "arg3".to_string(),
1415 ];
1416 let parsed = parse(&spec, &input).unwrap();
1417
1418 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1419 let value = parsed.args.get(args_arg).unwrap();
1420 assert_eq!(value.to_string(), "arg1 -- arg2 -- arg3");
1421 }
1422
1423 #[test]
1424 fn test_double_dashes_with_preserve_only_dashes() {
1425 let run_cmd = SpecCommand::builder()
1428 .name("run")
1429 .arg(
1430 SpecArg::builder()
1431 .name("args")
1432 .var(true)
1433 .double_dash(SpecDoubleDashChoices::Preserve)
1434 .build(),
1435 )
1436 .build();
1437 let mut cmd = SpecCommand::builder().name("test").build();
1438 cmd.subcommands.insert("run".to_string(), run_cmd);
1439
1440 let spec = Spec {
1441 name: "test".to_string(),
1442 bin: "test".to_string(),
1443 cmd,
1444 ..Default::default()
1445 };
1446
1447 let input = vec![
1449 "test".to_string(),
1450 "run".to_string(),
1451 "--".to_string(),
1452 "--".to_string(),
1453 ];
1454 let parsed = parse(&spec, &input).unwrap();
1455
1456 let args_arg = parsed.args.keys().find(|a| a.name == "args").unwrap();
1457 let value = parsed.args.get(args_arg).unwrap();
1458 assert_eq!(value.to_string(), "-- --");
1459 }
1460
1461 #[test]
1462 fn test_double_dashes_with_preserve_multiple_args() {
1463 let run_cmd = SpecCommand::builder()
1465 .name("run")
1466 .arg(SpecArg::builder().name("task").build())
1467 .arg(
1468 SpecArg::builder()
1469 .name("extra_args")
1470 .var(true)
1471 .double_dash(SpecDoubleDashChoices::Preserve)
1472 .build(),
1473 )
1474 .build();
1475 let mut cmd = SpecCommand::builder().name("test").build();
1476 cmd.subcommands.insert("run".to_string(), run_cmd);
1477
1478 let spec = Spec {
1479 name: "test".to_string(),
1480 bin: "test".to_string(),
1481 cmd,
1482 ..Default::default()
1483 };
1484
1485 let input = vec![
1488 "test".to_string(),
1489 "run".to_string(),
1490 "task1".to_string(),
1491 "--".to_string(),
1492 "arg1".to_string(),
1493 "--".to_string(),
1494 "--foo".to_string(),
1495 ];
1496 let parsed = parse(&spec, &input).unwrap();
1497
1498 let task_arg = parsed.args.keys().find(|a| a.name == "task").unwrap();
1499 let task_value = parsed.args.get(task_arg).unwrap();
1500 assert_eq!(task_value.to_string(), "task1");
1501
1502 let extra_arg = parsed.args.keys().find(|a| a.name == "extra_args").unwrap();
1503 let extra_value = parsed.args.get(extra_arg).unwrap();
1504 assert_eq!(extra_value.to_string(), "-- arg1 -- --foo");
1505 }
1506
1507 #[test]
1508 fn test_parser_with_custom_env_for_required_arg() {
1509 let cmd = SpecCommand::builder()
1512 .name("test")
1513 .arg(
1514 SpecArg::builder()
1515 .name("name")
1516 .env("NAME")
1517 .required(true)
1518 .build(),
1519 )
1520 .build();
1521 let spec = Spec {
1522 name: "test".to_string(),
1523 bin: "test".to_string(),
1524 cmd,
1525 ..Default::default()
1526 };
1527
1528 std::env::remove_var("NAME");
1530
1531 let mut env = HashMap::new();
1533 env.insert("NAME".to_string(), "john".to_string());
1534
1535 let input = vec!["test".to_string()];
1536 let result = Parser::new(&spec).with_env(env).parse(&input);
1537
1538 let parsed = result.expect("parse should succeed with custom env");
1540 assert_eq!(parsed.args.len(), 1);
1541 let value = parsed.args.values().next().unwrap();
1542 assert_eq!(value.to_string(), "john");
1543 }
1544
1545 #[test]
1546 fn test_parser_with_custom_env_for_required_flag() {
1547 let cmd = SpecCommand::builder()
1549 .name("test")
1550 .flag(
1551 SpecFlag::builder()
1552 .long("name")
1553 .env("NAME")
1554 .required(true)
1555 .arg(SpecArg::builder().name("name").build())
1556 .build(),
1557 )
1558 .build();
1559 let spec = Spec {
1560 name: "test".to_string(),
1561 bin: "test".to_string(),
1562 cmd,
1563 ..Default::default()
1564 };
1565
1566 std::env::remove_var("NAME");
1568
1569 let mut env = HashMap::new();
1571 env.insert("NAME".to_string(), "jane".to_string());
1572
1573 let input = vec!["test".to_string()];
1574 let result = Parser::new(&spec).with_env(env).parse(&input);
1575
1576 let parsed = result.expect("parse should succeed with custom env");
1578 assert_eq!(parsed.flags.len(), 1);
1579 let value = parsed.flags.values().next().unwrap();
1580 assert_eq!(value.to_string(), "jane");
1581 }
1582
1583 #[test]
1584 fn test_parser_with_custom_env_still_fails_when_missing() {
1585 let cmd = SpecCommand::builder()
1587 .name("test")
1588 .arg(
1589 SpecArg::builder()
1590 .name("name")
1591 .env("NAME")
1592 .required(true)
1593 .build(),
1594 )
1595 .build();
1596 let spec = Spec {
1597 name: "test".to_string(),
1598 bin: "test".to_string(),
1599 cmd,
1600 ..Default::default()
1601 };
1602
1603 std::env::remove_var("NAME");
1605
1606 let env = HashMap::new();
1608
1609 let input = vec!["test".to_string()];
1610 let result = Parser::new(&spec).with_env(env).parse(&input);
1611
1612 assert!(result.is_err());
1614 }
1615}