1mod tokenizer;
49
50use std::collections::{HashMap, VecDeque};
51
52use thiserror::Error;
53
54use crate::model::{Command, ParsedCommand};
55use crate::resolver::{ResolveError, Resolver};
56
57use tokenizer::{tokenize, Token};
58
59#[derive(Debug, Error, PartialEq)]
61pub enum ParseError {
62 #[error("no command provided")]
64 NoCommand,
65 #[error(transparent)]
70 Resolve(#[from] ResolveError),
71 #[error("missing required argument: {0}")]
75 MissingArgument(String),
76 #[error("unexpected argument: {0}")]
80 UnexpectedArgument(String),
81 #[error("missing required flag: --{0}")]
86 MissingFlag(String),
87 #[error("flag --{name} requires a value")]
89 FlagMissingValue {
90 name: String,
92 },
93 #[error("unknown flag: {0}")]
101 UnknownFlag(String),
102 #[error("unknown subcommand `{got}` for `{parent}`")]
108 UnknownSubcommand {
109 parent: String,
111 got: String,
113 },
114 #[error("invalid value `{value}` for `--{flag}`: expected one of {choices:?}")]
116 InvalidChoice {
117 flag: String,
119 value: String,
121 choices: Vec<String>,
123 },
124 #[error("flags {flags:?} are mutually exclusive — provide at most one")]
128 MutuallyExclusive {
129 flags: Vec<String>,
131 },
132}
133
134pub struct Parser<'a> {
150 commands: &'a [Command],
151}
152
153impl<'a> Parser<'a> {
154 pub fn new(commands: &'a [Command]) -> Self {
161 Self { commands }
162 }
163
164 pub fn parse(&self, argv: &[&str]) -> Result<ParsedCommand<'a>, ParseError> {
218 let tokens = tokenize(argv);
219 let mut pos = 0;
220
221 let cmd_name = match tokens.get(pos) {
223 Some(Token::Word(w)) => {
224 pos += 1;
225 w.clone()
226 }
227 _ => return Err(ParseError::NoCommand),
228 };
229
230 let resolver = Resolver::new(self.commands);
231 let mut cmd: &'a Command = resolver.resolve(&cmd_name)?;
232
233 loop {
235 if cmd.subcommands.is_empty() {
236 break;
237 }
238 match tokens.get(pos) {
239 Some(Token::Word(w)) => {
240 let sub_resolver = Resolver::new(&cmd.subcommands);
241 match sub_resolver.resolve(w) {
242 Ok(sub) => {
243 cmd = sub;
244 pos += 1;
245 }
246 Err(e) => match e {
247 ResolveError::Ambiguous { .. } => return Err(ParseError::Resolve(e)),
249 ResolveError::Unknown { .. } => {
253 if cmd.arguments.is_empty() {
254 return Err(ParseError::UnknownSubcommand {
255 parent: cmd.canonical.clone(),
256 got: w.clone(),
257 });
258 }
259 break;
260 }
261 },
262 }
263 }
264 _ => break,
265 }
266 }
267
268 let mut positionals: Vec<String> = Vec::new();
272 let mut flags: HashMap<String, String> = HashMap::new();
273
274 let mut queue: VecDeque<Token> = tokens[pos..].iter().cloned().collect();
275
276 while let Some(token) = queue.pop_front() {
277 match token {
278 Token::Separator => {
279 }
283 Token::Word(w) => {
284 positionals.push(w);
285 }
286 Token::LongFlag { name, value } => {
287 if let Some(base) = name.strip_prefix("no-") {
289 if let Some(flag_def) =
290 cmd.flags.iter().find(|f| f.name == base && !f.takes_value)
291 {
292 if value.is_some() {
293 return Err(ParseError::UnknownFlag(format!("--{}", name)));
294 }
295 flags.insert(flag_def.name.clone(), "false".to_string());
296 continue;
297 }
298 }
299
300 let flag_def = cmd
301 .flags
302 .iter()
303 .find(|f| f.name == name)
304 .ok_or_else(|| ParseError::UnknownFlag(format!("--{}", name)))?;
305
306 let val = if flag_def.takes_value {
307 if let Some(v) = value {
308 v
309 } else {
310 match queue.pop_front() {
311 Some(Token::Word(w)) => w,
312 _ => {
313 return Err(ParseError::FlagMissingValue {
314 name: flag_def.name.clone(),
315 })
316 }
317 }
318 }
319 } else {
320 "true".to_string()
321 };
322
323 if let Some(choices) = &flag_def.choices {
325 if !choices.contains(&val) {
326 return Err(ParseError::InvalidChoice {
327 flag: flag_def.name.clone(),
328 value: val,
329 choices: choices.clone(),
330 });
331 }
332 }
333
334 if flag_def.repeatable {
336 if flag_def.takes_value {
337 let new_val = match flags.get(&flag_def.name) {
339 None => serde_json::to_string(&[&val]).expect("serde_json serialization of &[&str] is infallible for simple string types"),
340 Some(existing) => {
341 let mut arr: Vec<String> = serde_json::from_str(existing)
342 .unwrap_or_else(|_| vec![existing.clone()]);
343 arr.push(val);
344 serde_json::to_string(&arr).expect("serde_json serialization of Vec<String> is infallible")
345 }
346 };
347 flags.insert(flag_def.name.clone(), new_val);
348 } else {
349 let count = flags
351 .get(&flag_def.name)
352 .and_then(|v| v.parse::<u64>().ok())
353 .unwrap_or(0);
354 flags.insert(flag_def.name.clone(), (count + 1).to_string());
355 }
356 } else {
357 flags.insert(flag_def.name.clone(), val);
358 }
359 }
360 Token::ShortFlag { name: c, value } => {
361 let flag_def = cmd
362 .flags
363 .iter()
364 .find(|f| f.short == Some(c))
365 .ok_or_else(|| ParseError::UnknownFlag(format!("-{}", c)))?;
366
367 if flag_def.takes_value {
368 let val = if let Some(v) = value {
369 v
370 } else {
371 match queue.pop_front() {
372 Some(Token::Word(w)) => w,
373 _ => {
374 return Err(ParseError::FlagMissingValue {
375 name: flag_def.name.clone(),
376 })
377 }
378 }
379 };
380
381 if let Some(choices) = &flag_def.choices {
383 if !choices.contains(&val) {
384 return Err(ParseError::InvalidChoice {
385 flag: flag_def.name.clone(),
386 value: val,
387 choices: choices.clone(),
388 });
389 }
390 }
391
392 if flag_def.repeatable {
394 let new_val = match flags.get(&flag_def.name) {
395 None => serde_json::to_string(&[&val]).expect("serde_json serialization of &[&str] is infallible for simple string types"),
396 Some(existing) => {
397 let mut arr: Vec<String> = serde_json::from_str(existing)
398 .unwrap_or_else(|_| vec![existing.clone()]);
399 arr.push(val);
400 serde_json::to_string(&arr).expect("serde_json serialization of Vec<String> is infallible")
401 }
402 };
403 flags.insert(flag_def.name.clone(), new_val);
404 } else {
405 flags.insert(flag_def.name.clone(), val);
406 }
407 } else {
408 if flag_def.repeatable {
410 let count = flags
411 .get(&flag_def.name)
412 .and_then(|v| v.parse::<u64>().ok())
413 .unwrap_or(0);
414 flags.insert(flag_def.name.clone(), (count + 1).to_string());
415 } else {
416 flags.insert(flag_def.name.clone(), "true".to_string());
417 }
418 if let Some(rest) = value {
419 if !rest.is_empty() {
420 let mut chars = rest.chars();
421 let next_c =
422 chars.next().expect("guarded by is_empty() check above");
423 let remainder: String = chars.collect();
424 queue.push_front(Token::ShortFlag {
425 name: next_c,
426 value: if remainder.is_empty() {
427 None
428 } else {
429 Some(remainder)
430 },
431 });
432 }
433 }
434 }
435 }
436 }
437 }
438
439 let mut args: HashMap<String, String> = HashMap::new();
441 for (i, arg_def) in cmd.arguments.iter().enumerate() {
442 if arg_def.variadic {
443 let values: Vec<&String> = positionals[i..].iter().collect();
445 if values.is_empty() && arg_def.required {
446 return Err(ParseError::MissingArgument(arg_def.name.clone()));
447 } else if !values.is_empty() {
448 let json_val = serde_json::to_string(
449 &values.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
450 )
451 .expect(
452 "serde_json serialization of &[&str] is infallible for simple string types",
453 );
454 args.insert(arg_def.name.clone(), json_val);
455 } else if let Some(default) = &arg_def.default {
456 args.insert(arg_def.name.clone(), default.clone());
457 }
458 break; }
460 if let Some(val) = positionals.get(i) {
461 args.insert(arg_def.name.clone(), val.clone());
462 } else if arg_def.required {
463 return Err(ParseError::MissingArgument(arg_def.name.clone()));
464 } else if let Some(default) = &arg_def.default {
465 args.insert(arg_def.name.clone(), default.clone());
466 }
467 }
468
469 let last_is_variadic = cmd.arguments.last().map(|a| a.variadic).unwrap_or(false);
471 if positionals.len() > cmd.arguments.len() && !last_is_variadic {
472 return Err(ParseError::UnexpectedArgument(
473 positionals[cmd.arguments.len()].clone(),
474 ));
475 }
476
477 for flag_def in &cmd.flags {
479 if !flags.contains_key(&flag_def.name) {
480 if let Some(ref var_name) = flag_def.env {
481 if let Ok(val) = std::env::var(var_name) {
482 if !val.is_empty() {
483 if let Some(ref choices) = flag_def.choices {
485 if !choices.contains(&val) {
486 return Err(ParseError::InvalidChoice {
487 flag: flag_def.name.clone(),
488 value: val,
489 choices: choices.clone(),
490 });
491 }
492 }
493 flags.insert(flag_def.name.clone(), val);
494 }
495 }
496 }
497 }
498 }
499
500 for group in &cmd.exclusive_groups {
502 let set: Vec<String> = group
503 .iter()
504 .filter(|name| flags.contains_key(*name))
505 .map(|name| format!("--{}", name))
506 .collect();
507 if set.len() > 1 {
508 return Err(ParseError::MutuallyExclusive { flags: set });
509 }
510 }
511
512 for flag_def in &cmd.flags {
514 if flag_def.required && !flags.contains_key(&flag_def.name) {
515 return Err(ParseError::MissingFlag(flag_def.name.clone()));
516 }
517 if !flags.contains_key(&flag_def.name) {
518 if let Some(default) = &flag_def.default {
519 flags.insert(flag_def.name.clone(), default.clone());
520 }
521 }
522 }
523
524 Ok(ParsedCommand {
525 command: cmd,
526 args,
527 flags,
528 })
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::model::{Argument, Command, Example, Flag};
536
537 fn build_commands() -> Vec<Command> {
538 let remote_add = Command::builder("add")
539 .summary("Add a remote")
540 .argument(
541 Argument::builder("name")
542 .description("remote name")
543 .required()
544 .build()
545 .unwrap(),
546 )
547 .argument(
548 Argument::builder("url")
549 .description("remote url")
550 .required()
551 .build()
552 .unwrap(),
553 )
554 .build()
555 .unwrap();
556
557 let remote_remove = Command::builder("remove")
558 .alias("rm")
559 .summary("Remove a remote")
560 .argument(
561 Argument::builder("name")
562 .description("remote name")
563 .required()
564 .build()
565 .unwrap(),
566 )
567 .build()
568 .unwrap();
569
570 let remote = Command::builder("remote")
571 .summary("Manage remotes")
572 .subcommand(remote_add)
573 .subcommand(remote_remove)
574 .build()
575 .unwrap();
576
577 let list = Command::builder("list")
578 .alias("ls")
579 .summary("List items")
580 .argument(
581 Argument::builder("filter")
582 .description("optional filter")
583 .build()
584 .unwrap(),
585 )
586 .flag(
587 Flag::builder("verbose")
588 .short('v')
589 .description("verbose output")
590 .build()
591 .unwrap(),
592 )
593 .flag(
594 Flag::builder("output")
595 .short('o')
596 .description("output format")
597 .takes_value()
598 .default_value("text")
599 .build()
600 .unwrap(),
601 )
602 .example(Example::new("list all", "myapp list"))
603 .build()
604 .unwrap();
605
606 let deploy = Command::builder("deploy")
607 .summary("Deploy")
608 .flag(
609 Flag::builder("env")
610 .description("target environment")
611 .takes_value()
612 .required()
613 .build()
614 .unwrap(),
615 )
616 .build()
617 .unwrap();
618
619 vec![list, remote, deploy]
620 }
621
622 struct TestCase {
623 name: &'static str,
624 argv: &'static [&'static str],
625 expect_err: bool,
626 expected_canonical: Option<&'static str>,
627 expected_args: Vec<(&'static str, &'static str)>,
628 expected_flags: Vec<(&'static str, &'static str)>,
629 }
630
631 #[test]
632 fn test_parse() {
633 let commands = build_commands();
634 let parser = Parser::new(&commands);
635
636 let cases = vec![
637 TestCase {
638 name: "flat command no args",
639 argv: &["list"],
640 expect_err: false,
641 expected_canonical: Some("list"),
642 expected_args: vec![],
643 expected_flags: vec![("output", "text")],
644 },
645 TestCase {
646 name: "flat command with positional",
647 argv: &["list", "foo"],
648 expect_err: false,
649 expected_canonical: Some("list"),
650 expected_args: vec![("filter", "foo")],
651 expected_flags: vec![("output", "text")],
652 },
653 TestCase {
654 name: "alias resolved",
655 argv: &["ls"],
656 expect_err: false,
657 expected_canonical: Some("list"),
658 expected_args: vec![],
659 expected_flags: vec![("output", "text")],
660 },
661 TestCase {
662 name: "boolean flag short",
663 argv: &["list", "-v"],
664 expect_err: false,
665 expected_canonical: Some("list"),
666 expected_args: vec![],
667 expected_flags: vec![("verbose", "true"), ("output", "text")],
668 },
669 TestCase {
670 name: "long flag equals",
671 argv: &["list", "--output=json"],
672 expect_err: false,
673 expected_canonical: Some("list"),
674 expected_args: vec![],
675 expected_flags: vec![("output", "json")],
676 },
677 TestCase {
678 name: "long flag space value",
679 argv: &["list", "--output", "json"],
680 expect_err: false,
681 expected_canonical: Some("list"),
682 expected_args: vec![],
683 expected_flags: vec![("output", "json")],
684 },
685 TestCase {
686 name: "short flag space value",
687 argv: &["list", "-o", "json"],
688 expect_err: false,
689 expected_canonical: Some("list"),
690 expected_args: vec![],
691 expected_flags: vec![("output", "json")],
692 },
693 TestCase {
694 name: "two-level subcommand",
695 argv: &["remote", "add", "origin", "https://example.com"],
696 expect_err: false,
697 expected_canonical: Some("add"),
698 expected_args: vec![("name", "origin"), ("url", "https://example.com")],
699 expected_flags: vec![],
700 },
701 TestCase {
702 name: "subcommand alias",
703 argv: &["remote", "rm", "origin"],
704 expect_err: false,
705 expected_canonical: Some("remove"),
706 expected_args: vec![("name", "origin")],
707 expected_flags: vec![],
708 },
709 TestCase {
710 name: "no command",
711 argv: &[],
712 expect_err: true,
713 expected_canonical: None,
714 expected_args: vec![],
715 expected_flags: vec![],
716 },
717 TestCase {
718 name: "unknown command",
719 argv: &["unknown"],
720 expect_err: true,
721 expected_canonical: None,
722 expected_args: vec![],
723 expected_flags: vec![],
724 },
725 TestCase {
726 name: "unknown flag",
727 argv: &["list", "--nope"],
728 expect_err: true,
729 expected_canonical: None,
730 expected_args: vec![],
731 expected_flags: vec![],
732 },
733 TestCase {
734 name: "missing required flag",
735 argv: &["deploy"],
736 expect_err: true,
737 expected_canonical: None,
738 expected_args: vec![],
739 expected_flags: vec![],
740 },
741 TestCase {
742 name: "unexpected positional",
743 argv: &["list", "one", "two"],
744 expect_err: true,
745 expected_canonical: None,
746 expected_args: vec![],
747 expected_flags: vec![],
748 },
749 ];
750
751 for tc in &cases {
752 let result = parser.parse(tc.argv);
753 if tc.expect_err {
754 assert!(result.is_err(), "case '{}': expected error", tc.name);
755 } else {
756 let parsed = result
757 .unwrap_or_else(|e| panic!("case '{}': unexpected error: {}", tc.name, e));
758 assert_eq!(
759 parsed.command.canonical,
760 tc.expected_canonical.unwrap(),
761 "case '{}'",
762 tc.name
763 );
764 for (k, v) in &tc.expected_args {
765 assert_eq!(
766 parsed.args.get(*k).map(String::as_str),
767 Some(*v),
768 "case '{}': arg {}",
769 tc.name,
770 k
771 );
772 }
773 for (k, v) in &tc.expected_flags {
774 assert_eq!(
775 parsed.flags.get(*k).map(String::as_str),
776 Some(*v),
777 "case '{}': flag {}",
778 tc.name,
779 k
780 );
781 }
782 }
783 }
784 }
785
786 #[test]
787 fn test_double_dash_separator() {
788 let cmds = vec![Command::builder("run")
789 .argument(
790 Argument::builder("script")
791 .description("script to run")
792 .required()
793 .build()
794 .unwrap(),
795 )
796 .build()
797 .unwrap()];
798 let parser = Parser::new(&cmds);
799 let result = parser.parse(&["run", "--", "myscript"]);
803 assert!(result.is_ok());
804 assert_eq!(result.unwrap().args["script"], "myscript");
805 }
806
807 #[test]
808 fn test_missing_required_argument() {
809 let cmds = vec![Command::builder("get")
810 .argument(
811 Argument::builder("id")
812 .description("item id")
813 .required()
814 .build()
815 .unwrap(),
816 )
817 .build()
818 .unwrap()];
819 let parser = Parser::new(&cmds);
820 assert!(matches!(
821 parser.parse(&["get"]),
822 Err(ParseError::MissingArgument(ref s)) if s == "id"
823 ));
824 }
825
826 #[test]
827 fn test_flag_missing_value() {
828 let cmds = vec![Command::builder("build")
829 .flag(Flag::builder("target").takes_value().build().unwrap())
830 .build()
831 .unwrap()];
832 let parser = Parser::new(&cmds);
833 assert!(matches!(
834 parser.parse(&["build", "--target"]),
835 Err(ParseError::FlagMissingValue { .. })
836 ));
837 }
838
839 #[test]
840 fn test_ambiguous_subcommand() {
841 let fetch = Command::builder("fetch").build().unwrap();
842 let force_push = Command::builder("force-push").build().unwrap();
843 let cmds = vec![Command::builder("git")
844 .subcommand(fetch)
845 .subcommand(force_push)
846 .build()
847 .unwrap()];
848 let parser = Parser::new(&cmds);
849 let result = parser.parse(&["git", "f"]);
850 assert!(
851 matches!(
852 result,
853 Err(ParseError::Resolve(ResolveError::Ambiguous { .. }))
854 ),
855 "expected Resolve(Ambiguous), got {:?}",
856 result
857 );
858 }
859
860 #[test]
861 fn test_unknown_subcommand_on_no_positionals() {
862 let cmds = vec![Command::builder("remote")
863 .subcommand(Command::builder("add").build().unwrap())
864 .build()
865 .unwrap()];
866 let parser = Parser::new(&cmds);
867 assert!(matches!(
868 parser.parse(&["remote", "xyz"]),
869 Err(ParseError::UnknownSubcommand { .. })
870 ));
871 }
872
873 #[test]
874 fn test_unknown_word_treated_as_positional_when_parent_has_args() {
875 let cmds = vec![Command::builder("deploy")
876 .subcommand(Command::builder("production").build().unwrap())
877 .argument(
878 Argument::builder("target")
879 .description("deployment target")
880 .required()
881 .build()
882 .unwrap(),
883 )
884 .build()
885 .unwrap()];
886 let parser = Parser::new(&cmds);
887 let result = parser.parse(&["deploy", "staging"]);
888 assert!(result.is_ok(), "expected Ok, got {:?}", result);
889 let parsed = result.unwrap();
890 assert_eq!(parsed.command.canonical, "deploy");
891 assert_eq!(
892 parsed.args.get("target").map(String::as_str),
893 Some("staging")
894 );
895 }
896
897 fn build_multi_flag_command() -> Vec<Command> {
902 vec![Command::builder("cmd")
903 .flag(Flag::builder("verbose").short('v').build().unwrap())
904 .flag(Flag::builder("no-wait").short('n').build().unwrap())
905 .flag(
906 Flag::builder("output")
907 .short('o')
908 .takes_value()
909 .default_value("text")
910 .build()
911 .unwrap(),
912 )
913 .build()
914 .unwrap()]
915 }
916
917 #[test]
918 fn test_adjacent_short_flags() {
919 let cmds = build_multi_flag_command();
920 let parser = Parser::new(&cmds);
921
922 let result = parser.parse(&["cmd", "-vo", "json"]);
924 assert!(result.is_ok(), "expected Ok, got {:?}", result);
925 let parsed = result.unwrap();
926 assert_eq!(
927 parsed.flags.get("verbose").map(String::as_str),
928 Some("true")
929 );
930 assert_eq!(parsed.flags.get("output").map(String::as_str), Some("json"));
931
932 let result2 = parser.parse(&["cmd", "-vn"]);
934 assert!(result2.is_ok(), "expected Ok, got {:?}", result2);
935 let parsed2 = result2.unwrap();
936 assert_eq!(
937 parsed2.flags.get("verbose").map(String::as_str),
938 Some("true")
939 );
940 assert_eq!(
941 parsed2.flags.get("no-wait").map(String::as_str),
942 Some("true")
943 );
944 }
945
946 #[test]
947 fn test_adjacent_short_flags_with_value() {
948 let cmds = build_multi_flag_command();
949 let parser = Parser::new(&cmds);
950
951 let result = parser.parse(&["cmd", "-ofile.txt"]);
953 assert!(result.is_ok(), "expected Ok, got {:?}", result);
954 let parsed = result.unwrap();
955 assert_eq!(
956 parsed.flags.get("output").map(String::as_str),
957 Some("file.txt")
958 );
959 }
960
961 #[test]
966 fn test_flag_negation() {
967 let cmds = vec![Command::builder("cmd")
968 .flag(Flag::builder("verbose").short('v').build().unwrap())
969 .build()
970 .unwrap()];
971 let parser = Parser::new(&cmds);
972
973 let result = parser.parse(&["cmd", "--no-verbose"]);
974 assert!(result.is_ok(), "expected Ok, got {:?}", result);
975 let parsed = result.unwrap();
976 assert_eq!(
977 parsed.flags.get("verbose").map(String::as_str),
978 Some("false")
979 );
980 }
981
982 #[test]
983 fn test_flag_negation_unknown() {
984 let cmds = vec![Command::builder("cmd")
985 .flag(Flag::builder("verbose").short('v').build().unwrap())
986 .build()
987 .unwrap()];
988 let parser = Parser::new(&cmds);
989
990 let result = parser.parse(&["cmd", "--no-nonexistent"]);
992 assert!(
993 matches!(result, Err(ParseError::UnknownFlag(_))),
994 "expected UnknownFlag, got {:?}",
995 result
996 );
997 }
998
999 #[test]
1004 fn test_variadic_argument() {
1005 let cmds = vec![Command::builder("cmd")
1006 .argument(Argument::builder("files").variadic().build().unwrap())
1007 .build()
1008 .unwrap()];
1009 let parser = Parser::new(&cmds);
1010
1011 let result = parser.parse(&["cmd", "a", "b", "c"]);
1012 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1013 let parsed = result.unwrap();
1014 let raw = parsed.args.get("files").expect("files key missing");
1015 let values: Vec<String> = serde_json::from_str(raw).expect("not valid JSON array");
1016 assert_eq!(values, vec!["a", "b", "c"]);
1017 }
1018
1019 #[test]
1020 fn test_variadic_argument_required_empty() {
1021 let cmds = vec![Command::builder("cmd")
1022 .argument(
1023 Argument::builder("files")
1024 .required()
1025 .variadic()
1026 .build()
1027 .unwrap(),
1028 )
1029 .build()
1030 .unwrap()];
1031 let parser = Parser::new(&cmds);
1032
1033 let result = parser.parse(&["cmd"]);
1034 assert!(
1035 matches!(result, Err(ParseError::MissingArgument(ref s)) if s == "files"),
1036 "expected MissingArgument(files), got {:?}",
1037 result
1038 );
1039 }
1040
1041 #[test]
1042 fn test_variadic_argument_default() {
1043 let cmds = vec![Command::builder("cmd")
1044 .argument(
1045 Argument::builder("files")
1046 .default_value("[]")
1047 .variadic()
1048 .build()
1049 .unwrap(),
1050 )
1051 .build()
1052 .unwrap()];
1053 let parser = Parser::new(&cmds);
1054
1055 let result = parser.parse(&["cmd"]);
1057 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1058 let parsed = result.unwrap();
1059 assert_eq!(parsed.args.get("files").map(String::as_str), Some("[]"));
1060 }
1061
1062 #[test]
1067 fn test_flag_choices_valid() {
1068 let cmds = vec![Command::builder("build")
1069 .flag(
1070 Flag::builder("format")
1071 .takes_value()
1072 .choices(["json", "yaml", "text"])
1073 .build()
1074 .unwrap(),
1075 )
1076 .build()
1077 .unwrap()];
1078 let parser = Parser::new(&cmds);
1079 let parsed = parser.parse(&["build", "--format=json"]).unwrap();
1080 assert_eq!(parsed.flags["format"], "json");
1081 }
1082
1083 #[test]
1084 fn test_flag_choices_invalid() {
1085 let cmds = vec![Command::builder("build")
1086 .flag(
1087 Flag::builder("format")
1088 .takes_value()
1089 .choices(["json", "yaml", "text"])
1090 .build()
1091 .unwrap(),
1092 )
1093 .build()
1094 .unwrap()];
1095 let parser = Parser::new(&cmds);
1096 let result = parser.parse(&["build", "--format=xml"]);
1097 assert!(
1098 matches!(result, Err(ParseError::InvalidChoice { ref value, .. }) if value == "xml")
1099 );
1100 }
1101
1102 #[test]
1103 fn test_repeatable_boolean_flag() {
1104 let cmds = vec![Command::builder("run")
1105 .flag(
1106 Flag::builder("verbose")
1107 .short('v')
1108 .repeatable()
1109 .build()
1110 .unwrap(),
1111 )
1112 .build()
1113 .unwrap()];
1114 let parser = Parser::new(&cmds);
1115 let parsed = parser.parse(&["run", "-v", "-v", "-v"]).unwrap();
1117 assert_eq!(parsed.flags["verbose"], "3");
1118 }
1119
1120 #[test]
1121 fn test_repeatable_value_flag() {
1122 let cmds = vec![Command::builder("run")
1123 .flag(
1124 Flag::builder("tag")
1125 .takes_value()
1126 .repeatable()
1127 .build()
1128 .unwrap(),
1129 )
1130 .build()
1131 .unwrap()];
1132 let parser = Parser::new(&cmds);
1133 let parsed = parser.parse(&["run", "--tag=alpha", "--tag=beta"]).unwrap();
1134 let tags: Vec<String> = serde_json::from_str(&parsed.flags["tag"]).unwrap();
1135 assert_eq!(tags, vec!["alpha", "beta"]);
1136 }
1137
1138 #[test]
1139 fn test_adjacent_short_repeatable() {
1140 let cmds = vec![Command::builder("run")
1142 .flag(
1143 Flag::builder("verbose")
1144 .short('v')
1145 .repeatable()
1146 .build()
1147 .unwrap(),
1148 )
1149 .build()
1150 .unwrap()];
1151 let parser = Parser::new(&cmds);
1152 let parsed = parser.parse(&["run", "-vvv"]).unwrap();
1153 assert_eq!(parsed.flags["verbose"], "3");
1154 }
1155
1156 #[test]
1157 fn test_empty_choices_build_error() {
1158 use crate::model::BuildError;
1159 let flag = Flag::builder("format")
1160 .takes_value()
1161 .choices(Vec::<String>::new())
1162 .build()
1163 .unwrap();
1164 let result = Command::builder("cmd").flag(flag).build();
1165 assert!(matches!(result, Err(BuildError::EmptyChoices(_))));
1166 }
1167
1168 #[test]
1169 fn test_env_var_fallback_basic() {
1170 let var = "ARGOT_TEST_ENVFLAG_BASIC_11111";
1171 std::env::remove_var(var);
1172 let cmds = vec![Command::builder("cmd")
1173 .flag(
1174 Flag::builder("token")
1175 .takes_value()
1176 .env(var)
1177 .build()
1178 .unwrap(),
1179 )
1180 .build()
1181 .unwrap()];
1182 let parser = Parser::new(&cmds);
1183
1184 assert!(!parser.parse(&["cmd"]).unwrap().flags.contains_key("token"));
1186
1187 std::env::set_var(var, "abc123");
1189 assert_eq!(parser.parse(&["cmd"]).unwrap().flags["token"], "abc123");
1190
1191 assert_eq!(
1193 parser.parse(&["cmd", "--token=override"]).unwrap().flags["token"],
1194 "override"
1195 );
1196 std::env::remove_var(var);
1197 }
1198
1199 #[test]
1200 fn test_env_var_fallback_with_default() {
1201 let var = "ARGOT_TEST_ENVFLAG_DEFAULT_22222";
1202 std::env::remove_var(var);
1203 let cmds = vec![Command::builder("cmd")
1204 .flag(
1205 Flag::builder("mode")
1206 .takes_value()
1207 .env(var)
1208 .default_value("dev")
1209 .build()
1210 .unwrap(),
1211 )
1212 .build()
1213 .unwrap()];
1214 let parser = Parser::new(&cmds);
1215
1216 assert_eq!(parser.parse(&["cmd"]).unwrap().flags["mode"], "dev");
1218
1219 std::env::set_var(var, "prod");
1221 assert_eq!(parser.parse(&["cmd"]).unwrap().flags["mode"], "prod");
1222 std::env::remove_var(var);
1223 }
1224
1225 #[test]
1226 fn test_env_var_satisfies_required_flag() {
1227 let var = "ARGOT_TEST_ENVFLAG_REQUIRED_33333";
1228 std::env::remove_var(var);
1229 let cmds = vec![Command::builder("cmd")
1230 .flag(
1231 Flag::builder("token")
1232 .takes_value()
1233 .required()
1234 .env(var)
1235 .build()
1236 .unwrap(),
1237 )
1238 .build()
1239 .unwrap()];
1240 let parser = Parser::new(&cmds);
1241
1242 std::env::set_var(var, "secret");
1244 assert_eq!(parser.parse(&["cmd"]).unwrap().flags["token"], "secret");
1245
1246 std::env::remove_var(var);
1248 assert!(matches!(
1249 parser.parse(&["cmd"]),
1250 Err(ParseError::MissingFlag(_))
1251 ));
1252 }
1253
1254 #[test]
1255 fn test_env_var_validates_choices() {
1256 let var = "ARGOT_TEST_ENVFLAG_CHOICES_44444";
1257 std::env::remove_var(var);
1258 let cmds = vec![Command::builder("cmd")
1259 .flag(
1260 Flag::builder("env_name")
1261 .takes_value()
1262 .choices(["prod", "staging"])
1263 .env(var)
1264 .build()
1265 .unwrap(),
1266 )
1267 .build()
1268 .unwrap()];
1269 let parser = Parser::new(&cmds);
1270
1271 std::env::set_var(var, "staging");
1272 assert_eq!(parser.parse(&["cmd"]).unwrap().flags["env_name"], "staging");
1273
1274 std::env::set_var(var, "local");
1275 assert!(matches!(
1276 parser.parse(&["cmd"]),
1277 Err(ParseError::InvalidChoice { .. })
1278 ));
1279
1280 std::env::remove_var(var);
1281 }
1282
1283 #[test]
1284 fn test_exclusive_flags_one_set_ok() {
1285 let cmd = Command::builder("export")
1286 .flag(Flag::builder("json").build().unwrap())
1287 .flag(Flag::builder("yaml").build().unwrap())
1288 .exclusive(["json", "yaml"])
1289 .build()
1290 .unwrap();
1291 let cmds = vec![cmd];
1292 let parser = Parser::new(&cmds);
1293 let parsed = parser.parse(&["export", "--json"]).unwrap();
1294 assert_eq!(parsed.flags["json"], "true");
1295 }
1296
1297 #[test]
1298 fn test_exclusive_flags_two_set_errors() {
1299 let cmd = Command::builder("export")
1300 .flag(Flag::builder("json").build().unwrap())
1301 .flag(Flag::builder("yaml").build().unwrap())
1302 .exclusive(["json", "yaml"])
1303 .build()
1304 .unwrap();
1305 let cmds = vec![cmd];
1306 let parser = Parser::new(&cmds);
1307 assert!(matches!(
1308 parser.parse(&["export", "--json", "--yaml"]),
1309 Err(ParseError::MutuallyExclusive { .. })
1310 ));
1311 }
1312
1313 #[test]
1314 fn test_exclusive_neither_set_ok() {
1315 let cmd = Command::builder("export")
1316 .flag(Flag::builder("json").build().unwrap())
1317 .flag(Flag::builder("yaml").build().unwrap())
1318 .exclusive(["json", "yaml"])
1319 .build()
1320 .unwrap();
1321 let cmds = vec![cmd];
1322 let parser = Parser::new(&cmds);
1323 assert!(parser.parse(&["export"]).is_ok());
1324 }
1325
1326 #[test]
1327 fn test_exclusive_group_unknown_flag_build_error() {
1328 use crate::model::BuildError;
1329 let result = Command::builder("cmd")
1330 .flag(Flag::builder("json").build().unwrap())
1331 .exclusive(["json", "nonexistent"])
1332 .build();
1333 assert!(matches!(
1334 result,
1335 Err(BuildError::ExclusiveGroupUnknownFlag(_))
1336 ));
1337 }
1338
1339 #[test]
1340 fn test_exclusive_group_too_small_build_error() {
1341 use crate::model::BuildError;
1342 let result = Command::builder("cmd")
1343 .flag(Flag::builder("json").build().unwrap())
1344 .exclusive(["json"])
1345 .build();
1346 assert!(matches!(result, Err(BuildError::ExclusiveGroupTooSmall)));
1347 }
1348
1349 #[test]
1354 fn test_subcommand_walk_breaks_on_flag_token() {
1355 let cmds = vec![Command::builder("git")
1358 .subcommand(Command::builder("status").build().unwrap())
1359 .flag(Flag::builder("verbose").build().unwrap())
1360 .build()
1361 .unwrap()];
1362 let parser = Parser::new(&cmds);
1363 let result = parser.parse(&["git", "--verbose"]);
1366 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1367 let parsed = result.unwrap();
1368 assert_eq!(parsed.command.canonical, "git");
1369 assert_eq!(
1370 parsed.flags.get("verbose").map(String::as_str),
1371 Some("true")
1372 );
1373 }
1374
1375 #[test]
1376 fn test_no_negation_with_value_is_unknown_flag() {
1377 let cmds = vec![Command::builder("cmd")
1379 .flag(Flag::builder("verbose").build().unwrap())
1380 .build()
1381 .unwrap()];
1382 let parser = Parser::new(&cmds);
1383 let result = parser.parse(&["cmd", "--no-verbose=true"]);
1384 assert!(
1385 matches!(result, Err(ParseError::UnknownFlag(_))),
1386 "expected UnknownFlag for --no-<name>=value, got {:?}",
1387 result
1388 );
1389 }
1390
1391 #[test]
1392 fn test_long_flag_missing_value_non_word_token() {
1393 let cmds = vec![Command::builder("cmd")
1395 .flag(Flag::builder("output").takes_value().build().unwrap())
1396 .flag(Flag::builder("verbose").build().unwrap())
1397 .build()
1398 .unwrap()];
1399 let parser = Parser::new(&cmds);
1400 let result = parser.parse(&["cmd", "--output", "--verbose"]);
1401 assert!(
1402 matches!(result, Err(ParseError::FlagMissingValue { .. })),
1403 "expected FlagMissingValue, got {:?}",
1404 result
1405 );
1406 }
1407
1408 #[test]
1409 fn test_short_flag_takes_value_missing_value() {
1410 let cmds = vec![Command::builder("cmd")
1412 .flag(
1413 Flag::builder("output")
1414 .short('o')
1415 .takes_value()
1416 .build()
1417 .unwrap(),
1418 )
1419 .build()
1420 .unwrap()];
1421 let parser = Parser::new(&cmds);
1422 let result = parser.parse(&["cmd", "-o"]);
1424 assert!(
1425 matches!(result, Err(ParseError::FlagMissingValue { .. })),
1426 "expected FlagMissingValue for short flag with no value, got {:?}",
1427 result
1428 );
1429 }
1430
1431 #[test]
1432 fn test_short_flag_takes_value_invalid_choice() {
1433 let cmds = vec![Command::builder("cmd")
1434 .flag(
1435 Flag::builder("format")
1436 .short('f')
1437 .takes_value()
1438 .choices(["json", "yaml"])
1439 .build()
1440 .unwrap(),
1441 )
1442 .build()
1443 .unwrap()];
1444 let parser = Parser::new(&cmds);
1445 let result = parser.parse(&["cmd", "-f", "xml"]);
1446 assert!(
1447 matches!(result, Err(ParseError::InvalidChoice { .. })),
1448 "expected InvalidChoice for invalid short flag value, got {:?}",
1449 result
1450 );
1451 }
1452
1453 #[test]
1454 fn test_short_flag_repeatable_value() {
1455 let cmds = vec![Command::builder("run")
1456 .flag(
1457 Flag::builder("tag")
1458 .short('t')
1459 .takes_value()
1460 .repeatable()
1461 .build()
1462 .unwrap(),
1463 )
1464 .build()
1465 .unwrap()];
1466 let parser = Parser::new(&cmds);
1467 let parsed = parser.parse(&["run", "-t", "alpha", "-t", "beta"]).unwrap();
1469 let tags: Vec<String> = serde_json::from_str(&parsed.flags["tag"]).unwrap();
1470 assert_eq!(tags, vec!["alpha", "beta"]);
1471 }
1472
1473 #[test]
1474 fn test_optional_arg_with_default_applied() {
1475 let cmds = vec![Command::builder("serve")
1477 .argument(
1478 Argument::builder("host")
1479 .default_value("localhost")
1480 .build()
1481 .unwrap(),
1482 )
1483 .build()
1484 .unwrap()];
1485 let parser = Parser::new(&cmds);
1486 let parsed = parser.parse(&["serve"]).unwrap();
1487 assert_eq!(
1488 parsed.args.get("host").map(String::as_str),
1489 Some("localhost")
1490 );
1491 }
1492
1493 #[test]
1494 fn test_long_flag_repeatable_boolean() {
1495 let cmds = vec![Command::builder("run")
1497 .flag(Flag::builder("verbose").repeatable().build().unwrap())
1498 .build()
1499 .unwrap()];
1500 let parser = Parser::new(&cmds);
1501 let parsed = parser
1502 .parse(&["run", "--verbose", "--verbose", "--verbose"])
1503 .unwrap();
1504 assert_eq!(parsed.flags["verbose"], "3");
1505 }
1506
1507 #[test]
1508 fn test_long_flag_repeatable_value() {
1509 let cmds = vec![Command::builder("run")
1511 .flag(
1512 Flag::builder("tag")
1513 .takes_value()
1514 .repeatable()
1515 .build()
1516 .unwrap(),
1517 )
1518 .build()
1519 .unwrap()];
1520 let parser = Parser::new(&cmds);
1521 let parsed = parser
1523 .parse(&["run", "--tag=first", "--tag=second"])
1524 .unwrap();
1525 let tags: Vec<String> = serde_json::from_str(&parsed.flags["tag"]).unwrap();
1526 assert_eq!(tags, vec!["first", "second"]);
1527 }
1528
1529 #[test]
1530 fn test_unknown_short_flag_error() {
1531 let cmds = vec![Command::builder("cmd").build().unwrap()];
1532 let parser = Parser::new(&cmds);
1533 let result = parser.parse(&["cmd", "-z"]);
1534 assert!(
1535 matches!(result, Err(ParseError::UnknownFlag(_))),
1536 "expected UnknownFlag for unknown short flag, got {:?}",
1537 result
1538 );
1539 }
1540}
1541
1542#[cfg(test)]
1543mod typed_getter_tests {
1544 use super::*;
1545 use crate::model::{Argument, Command, Flag};
1546
1547 #[test]
1548 fn test_parsed_command_typed_getters() {
1549 let cmd = Command::builder("run")
1550 .argument(Argument::builder("script").required().build().unwrap())
1551 .flag(Flag::builder("verbose").short('v').build().unwrap())
1552 .flag(
1553 Flag::builder("output")
1554 .takes_value()
1555 .default_value("text")
1556 .build()
1557 .unwrap(),
1558 )
1559 .build()
1560 .unwrap();
1561 let cmds = vec![cmd];
1562 let parser = Parser::new(&cmds);
1563 let parsed = parser.parse(&["run", "myscript", "-v"]).unwrap();
1564
1565 assert_eq!(parsed.arg("script"), Some("myscript"));
1566 assert_eq!(parsed.arg("missing"), None);
1567 assert_eq!(parsed.flag("verbose"), Some("true"));
1568 assert_eq!(parsed.flag("output"), Some("text")); assert!(parsed.flag_bool("verbose"));
1570 assert!(!parsed.flag_bool("output")); assert_eq!(parsed.flag_count("verbose"), 1);
1572 assert_eq!(parsed.flag_count("missing"), 0);
1573 assert_eq!(parsed.flag_values("output"), vec!["text"]);
1574 assert!(parsed.flag_values("missing").is_empty());
1575 }
1576}
1577
1578#[cfg(test)]
1579mod has_flag_tests {
1580 use super::*;
1581 use crate::model::{Command, Flag};
1582
1583 #[test]
1584 fn test_has_flag() {
1585 let cmd = Command::builder("run")
1586 .flag(Flag::builder("verbose").build().unwrap())
1587 .flag(
1588 Flag::builder("output")
1589 .takes_value()
1590 .default_value("text")
1591 .build()
1592 .unwrap(),
1593 )
1594 .build()
1595 .unwrap();
1596 let cmds = vec![cmd];
1597 let parser = Parser::new(&cmds);
1598 let parsed = parser.parse(&["run", "--verbose"]).unwrap();
1599 assert!(parsed.has_flag("verbose"));
1600 assert!(parsed.has_flag("output")); assert!(!parsed.has_flag("nonexistent"));
1602 }
1603}
1604
1605#[cfg(test)]
1606mod coercion_tests {
1607 use super::*;
1608 use crate::model::{Argument, Command, Flag};
1609
1610 #[test]
1611 fn test_arg_as_u32() {
1612 let cmd = Command::builder("resize")
1613 .argument(Argument::builder("width").required().build().unwrap())
1614 .build()
1615 .unwrap();
1616 let cmds = vec![cmd];
1617 let parsed = Parser::new(&cmds).parse(&["resize", "1920"]).unwrap();
1618 let w: u32 = parsed.arg_as("width").unwrap().unwrap();
1619 assert_eq!(w, 1920);
1620 }
1621
1622 #[test]
1623 fn test_arg_as_parse_error() {
1624 let cmd = Command::builder("cmd")
1625 .argument(Argument::builder("n").required().build().unwrap())
1626 .build()
1627 .unwrap();
1628 let cmds = vec![cmd];
1629 let parsed = Parser::new(&cmds).parse(&["cmd", "notanumber"]).unwrap();
1630 assert!(parsed.arg_as::<u32>("n").unwrap().is_err());
1631 }
1632
1633 #[test]
1634 fn test_arg_as_absent() {
1635 let cmd = Command::builder("cmd").build().unwrap();
1636 let cmds = vec![cmd];
1637 let parsed = Parser::new(&cmds).parse(&["cmd"]).unwrap();
1638 assert!(parsed.arg_as::<u32>("missing").is_none());
1639 }
1640
1641 #[test]
1642 fn test_flag_as_u16() {
1643 let cmd = Command::builder("serve")
1644 .flag(
1645 Flag::builder("port")
1646 .takes_value()
1647 .default_value("8080")
1648 .build()
1649 .unwrap(),
1650 )
1651 .build()
1652 .unwrap();
1653 let cmds = vec![cmd];
1654 let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
1655 let port: u16 = parsed.flag_as("port").unwrap().unwrap();
1656 assert_eq!(port, 8080);
1657 }
1658
1659 #[test]
1660 fn test_flag_as_bool() {
1661 let cmd = Command::builder("run")
1662 .flag(Flag::builder("verbose").build().unwrap())
1663 .build()
1664 .unwrap();
1665 let cmds = vec![cmd];
1666 let parsed = Parser::new(&cmds).parse(&["run", "--verbose"]).unwrap();
1667 let v: bool = parsed.flag_as("verbose").unwrap().unwrap();
1668 assert!(v);
1669 }
1670
1671 #[test]
1672 fn test_arg_as_or_default() {
1673 let cmd = Command::builder("run")
1674 .argument(Argument::builder("count").build().unwrap())
1675 .build()
1676 .unwrap();
1677 let cmds = vec![cmd];
1678 let parsed = Parser::new(&cmds).parse(&["run"]).unwrap();
1679 assert_eq!(parsed.arg_as_or("count", 42u32), 42u32);
1680 }
1681
1682 #[test]
1683 fn test_flag_as_or_default() {
1684 let cmd = Command::builder("serve")
1685 .flag(Flag::builder("workers").takes_value().build().unwrap())
1686 .build()
1687 .unwrap();
1688 let cmds = vec![cmd];
1689 let parsed = Parser::new(&cmds).parse(&["serve"]).unwrap();
1690 assert_eq!(parsed.flag_as_or("workers", 4u32), 4u32);
1691 }
1692}