1use crate::command::{Command, Completion};
2use crate::command_set::CommandSet;
3use crate::error::ShiError;
4use crate::shell::Shell;
5use crate::tokenizer::{DefaultTokenizer, Tokenization, Tokenizer};
6
7pub struct Parser {
9 tokenizer: DefaultTokenizer,
10}
11
12#[derive(Debug, PartialEq)]
13pub enum CommandType {
17 Builtin,
18 Custom,
19 Unknown,
20}
21
22#[derive(Debug, PartialEq)]
23pub struct Outcome<'a> {
36 pub cmd_path: Vec<&'a str>,
37 pub remaining: Vec<&'a str>,
38 pub cmd_type: CommandType,
39 pub possibilities: Vec<String>,
40 pub leaf_completion: Option<Completion>,
41 pub complete: bool,
42}
43
44impl<'a> Outcome<'a> {
45 pub fn error(&self) -> Option<ShiError> {
46 if !self.complete {
47 Some(ShiError::ParseError {
48 msg: self.error_msg(),
49 cmd_path: self.cmd_path.iter().map(|s| s.to_string()).collect(),
50 remaining: self.remaining.iter().map(|s| s.to_string()).collect(),
51 possibilities: self.possibilities.clone(),
52 })
53 } else {
54 None
55 }
56 }
57
58 pub fn error_msg(&self) -> String {
61 if self.complete {
65 return String::from("");
66 }
67
68 let mut msg = String::new();
70
71 if self.cmd_path.is_empty() && self.remaining.is_empty() {
72 msg += "Empty string could not be parsed as a command.";
75 } else if self.cmd_path.is_empty() {
76 if let Some(first_remaining_word) = self.remaining.get(0) {
80 msg.push_str(&format!(
81 "'{}' is not a recognized command.",
82 first_remaining_word
83 ));
84 } else {
85 unreachable!("remaining unparsed tokens cannot be empty at this point")
88 }
89 } else {
90 msg += "Failed to parse fully:\n";
91 msg += "\n";
92
93 let valid_prefix = self.cmd_path.join(" ");
94 let invalid_suffix = self.remaining.join(" ");
95 msg += "\t (spaces trimmed)\n";
96 if self.remaining.is_empty() {
97 msg += &format!("\t => '{} '\n", valid_prefix);
98 } else {
99 msg += &format!("\t => '{} {}'\n", valid_prefix, invalid_suffix);
100 }
101 msg += &format!("\t {}^\n", " ".repeat(valid_prefix.len() + 1));
102
103 msg += "expected a valid subcommand\n";
104 msg += "instead, got: ";
105 if let Some(first_remaining_word) = self.remaining.get(0) {
106 msg += &format!("'{}';\n", first_remaining_word);
107 } else {
108 msg += "nothing;\n"
109 }
110
111 msg += "\n";
112 msg.push_str(&format!(
113 "Run '{} help' for more info on the command.",
114 valid_prefix
115 ));
116 }
117
118 if !self.possibilities.is_empty() {
119 msg += "\n\n";
120 msg.push_str(&format!(
121 "\t => expected one of {}.\n",
122 self.possibilities
123 .iter()
124 .map(|s| format!("'{}'", s))
125 .collect::<Vec<String>>()
126 .join(" or ")
127 ))
128 }
129
130 msg += "\n";
131 msg += "Run 'helptree' for more info on the entire command tree.\n";
132
133 msg
134 }
135}
136
137impl Parser {
138 pub fn new() -> Parser {
140 Parser {
141 tokenizer: DefaultTokenizer::new(vec!['\'', '"']),
142 }
143 }
144
145 fn parse_tokens_with_set<'a, T>(
155 &self,
156 tokenization: &Tokenization<'a>,
157 cmd_type: CommandType,
158 set: &CommandSet<T>,
159 ) -> Outcome<'a> {
160 let mut cmd_path: Vec<&str> = Vec::new();
161 let mut current_set = set;
162 for (i, token) in tokenization.tokens.iter().enumerate() {
163 let looked_up_cmd = match current_set.get(token) {
165 Some(cmd) => {
166 cmd_path.push(token);
167 cmd
168 }
169 None => {
170 return Outcome {
171 cmd_path,
172 remaining: tokenization.tokens.get(i..).unwrap().to_vec(),
174 cmd_type: if i == 0 {
175 CommandType::Unknown
178 } else {
179 cmd_type
180 },
181 possibilities: current_set.names(),
182 leaf_completion: None,
183 complete: false,
184 };
185 }
186 };
187
188 match &**looked_up_cmd {
193 Command::Leaf(cmd) => {
194 return Outcome {
198 cmd_path,
199 remaining: tokenization.tokens.get(i + 1..).unwrap().to_vec(),
201 cmd_type,
202 possibilities: Vec::new(),
203 leaf_completion: Some(cmd.autocomplete(
204 tokenization.tokens.get(i + 1..).unwrap().to_vec(),
205 tokenization.trailing_space,
206 )),
207 complete: true,
208 };
209 }
210 Command::Parent(cmd) => {
211 current_set = cmd.sub_commands();
212 }
213 }
214 }
215
216 Outcome {
218 cmd_path,
219 remaining: Vec::new(), cmd_type: if tokenization.tokens.is_empty() {
221 CommandType::Unknown
222 } else {
223 cmd_type
224 },
225 possibilities: current_set.names(),
226 leaf_completion: None,
227 complete: false,
228 }
229 }
230
231 fn parse_tokens<'a, S>(
241 &self,
242 tokenization: &Tokenization<'a>,
243 cmds: &CommandSet<S>,
244 builtins: &CommandSet<Shell<S>>,
245 ) -> Outcome<'a> {
246 let cmd_outcome = self.parse_tokens_with_set(tokenization, CommandType::Custom, cmds);
247 if cmd_outcome.complete {
248 return cmd_outcome;
249 }
250
251 let builtin_outcome =
252 self.parse_tokens_with_set(tokenization, CommandType::Builtin, builtins);
253 if builtin_outcome.complete {
254 return builtin_outcome;
255 }
256
257 cmd_outcome
258 }
259
260 pub fn parse<'a, S>(
270 &self,
271 line: &'a str,
272 cmds: &CommandSet<S>,
273 builtins: &CommandSet<Shell<S>>,
274 ) -> Outcome<'a> {
275 let tokenization = self.tokenizer.tokenize(line);
276 self.parse_tokens(&tokenization, cmds, builtins)
277 }
278}
279
280#[cfg(test)]
281pub mod test {
282 use super::*;
283
284 use crate::command::BaseCommand;
285 use crate::Result;
286
287 use pretty_assertions::assert_eq;
288
289 use std::marker::PhantomData;
290
291 #[derive(Debug)]
294 struct ParseTestCommand<'a, S> {
295 name: &'a str,
296 autocompletions: Vec<&'a str>,
297 phantom: PhantomData<S>,
298 }
299
300 impl<'a, S> ParseTestCommand<'a, S> {
301 fn new(name: &str) -> ParseTestCommand<S> {
302 ParseTestCommand {
303 name,
304 autocompletions: Vec::new(),
305 phantom: PhantomData,
306 }
307 }
308
309 fn new_with_completions(
310 name: &'a str,
311 completions: Vec<&'a str>,
312 ) -> ParseTestCommand<'a, S> {
313 ParseTestCommand {
314 name,
315 autocompletions: completions,
316 phantom: PhantomData,
317 }
318 }
319 }
320
321 impl<'a, S> BaseCommand for ParseTestCommand<'a, S> {
322 type State = S;
323
324 fn name(&self) -> &str {
325 self.name
326 }
327
328 #[cfg(not(tarpaulin_include))]
329 fn validate_args(&self, _: &[String]) -> Result<()> {
330 Ok(())
331 }
332
333 fn autocomplete(&self, args: Vec<&str>, _: bool) -> Completion {
334 if self.autocompletions.is_empty() {
336 return Completion::Nothing;
337 }
338
339 match args.last() {
340 Some(last) => {
341 if self.autocompletions.iter().filter(|s| s == &last).count() > 0 {
342 Completion::Nothing
345 } else {
346 let prefix_matches: Vec<String> = self
347 .autocompletions
348 .iter()
349 .filter(|s| s.starts_with(last))
350 .map(|s| s.to_string())
351 .collect();
352
353 if prefix_matches.is_empty() {
354 return Completion::Nothing;
356 }
357 Completion::PartialArgCompletion(prefix_matches)
360 }
361 }
362 None => Completion::Possibilities(
363 self.autocompletions.iter().map(|s| s.to_string()).collect(),
364 ),
365 }
366 }
367
368 #[cfg(not(tarpaulin_include))]
369 fn execute(&self, _: &mut S, _: &[String]) -> Result<String> {
370 Ok(String::from(""))
371 }
372 }
373
374 pub fn make_parser_cmds<'a>() -> (CommandSet<'a, ()>, CommandSet<'a, Shell<'a, ()>>) {
375 (
376 CommandSet::new_from_vec(vec![
377 Command::new_parent(
378 "foo-c",
379 vec![
380 Command::new_leaf(ParseTestCommand::new("bar-c")),
381 Command::new_leaf(ParseTestCommand::new("baz-c")),
382 Command::new_parent(
383 "qux-c",
384 vec![
385 Command::new_leaf(ParseTestCommand::new("quux-c")),
386 Command::new_leaf(ParseTestCommand::new("corge-c")),
387 ],
388 ),
389 ],
390 ),
391 Command::new_leaf(ParseTestCommand::new("grault-c")),
392 Command::new_leaf(ParseTestCommand::new("conflict-tie")),
393 Command::new_leaf(ParseTestCommand::new(
394 "conflict-builtin-longer-match-but-still-loses",
395 )),
396 Command::new_parent(
397 "conflict-custom-wins",
398 vec![Command::new_leaf(ParseTestCommand::new("child"))],
399 ),
400 ]),
401 CommandSet::new_from_vec(vec![
402 Command::new_parent(
403 "foo-b",
404 vec![Command::new_leaf(ParseTestCommand::new_with_completions(
405 "bar-b",
406 vec!["ho", "he", "bum"],
407 ))],
408 ),
409 Command::new_leaf(ParseTestCommand::new("conflict-tie")),
410 Command::new_leaf(ParseTestCommand::new("conflict-custom-wins")),
411 Command::new_parent(
412 "conflict-builtin-longer-match-but-still-loses",
413 vec![Command::new_leaf(ParseTestCommand::new("child"))],
414 ),
415 ]),
416 )
417 }
418
419 #[test]
420 fn nesting() {
421 let cmds = make_parser_cmds();
422
423 assert_eq!(
424 Parser::new().parse("foo-c bar-c he", &cmds.0, &cmds.1),
425 Outcome {
426 cmd_path: vec!["foo-c", "bar-c"],
427 remaining: vec!["he"],
428 cmd_type: CommandType::Custom,
429 possibilities: Vec::new(),
430 leaf_completion: Some(Completion::Nothing),
431 complete: true,
432 }
433 );
434 }
435
436 #[test]
437 fn no_nesting_no_args() {
438 let cmds = make_parser_cmds();
439
440 assert_eq!(
441 Parser::new().parse("foo-c bar-c", &cmds.0, &cmds.1),
442 Outcome {
443 cmd_path: vec!["foo-c", "bar-c"],
444 remaining: vec![],
445 cmd_type: CommandType::Custom,
446 possibilities: Vec::new(),
447 leaf_completion: Some(Completion::Nothing),
448 complete: true,
449 }
450 );
451 }
452
453 #[test]
454 fn end_with_no_args_but_is_parent() {
455 let cmds = make_parser_cmds();
456
457 assert_eq!(
458 Parser::new().parse("foo-c qux-c", &cmds.0, &cmds.1),
459 Outcome {
460 cmd_path: vec!["foo-c", "qux-c"],
461 remaining: vec![],
462 cmd_type: CommandType::Custom,
463 possibilities: vec![String::from("quux-c"), String::from("corge-c")],
464 leaf_completion: None,
465 complete: false,
466 }
467 );
468 }
469
470 #[test]
471 fn builtin_nesting() {
472 let cmds = make_parser_cmds();
473
474 assert_eq!(
475 Parser::new().parse("foo-b bar-b he", &cmds.0, &cmds.1),
476 Outcome {
477 cmd_path: vec!["foo-b", "bar-b"],
478 remaining: vec!["he"],
479 cmd_type: CommandType::Builtin,
480 possibilities: Vec::new(),
481 leaf_completion: Some(Completion::Nothing),
482 complete: true,
483 }
484 );
485 }
486
487 #[test]
488 fn empty() {
489 let cmds = make_parser_cmds();
490
491 assert_eq!(
492 Parser::new().parse("", &cmds.0, &cmds.1),
493 Outcome {
494 cmd_path: vec![],
495 remaining: vec![],
496 cmd_type: CommandType::Unknown,
497 possibilities: vec![
498 String::from("foo-c"),
499 String::from("grault-c"),
500 String::from("conflict-tie"),
501 String::from("conflict-builtin-longer-match-but-still-loses"),
502 String::from("conflict-custom-wins"),
503 ],
504 leaf_completion: None,
505 complete: false,
506 }
507 );
508 }
509
510 #[test]
511 fn invalid_subcmd() {
512 let cmds = make_parser_cmds();
513
514 assert_eq!(
515 Parser::new().parse("foo-c he", &cmds.0, &cmds.1),
516 Outcome {
517 cmd_path: vec!["foo-c"],
518 remaining: vec!["he"],
519 cmd_type: CommandType::Custom,
520 possibilities: vec![
521 String::from("bar-c"),
522 String::from("baz-c"),
523 String::from("qux-c"),
524 ],
525 leaf_completion: None,
526 complete: false,
527 }
528 );
529 }
530
531 #[test]
532 fn no_nesting() {
533 let cmds = make_parser_cmds();
534
535 assert_eq!(
536 Parser::new().parse("grault-c la la", &cmds.0, &cmds.1),
537 Outcome {
538 cmd_path: vec!["grault-c"],
539 remaining: vec!["la", "la"],
540 cmd_type: CommandType::Custom,
541 possibilities: Vec::new(),
542 leaf_completion: Some(Completion::Nothing),
543 complete: true,
544 }
545 );
546 }
547
548 #[test]
549 fn no_args_no_nesting() {
550 let cmds = make_parser_cmds();
551
552 assert_eq!(
553 Parser::new().parse("grault-c", &cmds.0, &cmds.1),
554 Outcome {
555 cmd_path: vec!["grault-c"],
556 remaining: vec![],
557 cmd_type: CommandType::Custom,
558 possibilities: Vec::new(),
559 leaf_completion: Some(Completion::Nothing),
560 complete: true,
561 }
562 );
563 }
564
565 #[test]
566 fn cmd_has_args_that_match_other_cmds() {
567 let cmds = make_parser_cmds();
568
569 assert_eq!(
570 Parser::new().parse("grault-c foo-c bar-c", &cmds.0, &cmds.1),
571 Outcome {
572 cmd_path: vec!["grault-c"],
573 remaining: vec!["foo-c", "bar-c"],
576 cmd_type: CommandType::Custom,
577 possibilities: Vec::new(),
578 leaf_completion: Some(Completion::Nothing),
579 complete: true,
580 }
581 );
582 }
583
584 #[test]
585 fn nonexistent_cmd() {
586 let cmds = make_parser_cmds();
587
588 assert_eq!(
589 Parser::new().parse("notacmd", &cmds.0, &cmds.1),
590 Outcome {
591 cmd_path: vec![],
592 remaining: vec!["notacmd"],
593 cmd_type: CommandType::Unknown,
594 possibilities: vec![
595 String::from("foo-c"),
596 String::from("grault-c"),
597 String::from("conflict-tie"),
598 String::from("conflict-builtin-longer-match-but-still-loses"),
599 String::from("conflict-custom-wins"),
600 ],
601 leaf_completion: None,
602 complete: false,
603 }
604 );
605 }
606
607 #[test]
608 fn args_with_nonexistent_cmd() {
609 let cmds = make_parser_cmds();
610
611 assert_eq!(
612 Parser::new().parse("notacmd la la", &cmds.0, &cmds.1),
613 Outcome {
614 cmd_path: vec![],
615 remaining: vec!["notacmd", "la", "la"],
616 cmd_type: CommandType::Unknown,
617 possibilities: vec![
618 String::from("foo-c"),
619 String::from("grault-c"),
620 String::from("conflict-tie"),
621 String::from("conflict-builtin-longer-match-but-still-loses"),
622 String::from("conflict-custom-wins"),
623 ],
624 leaf_completion: None,
625 complete: false,
626 }
627 );
628 }
629
630 #[test]
631 fn three_levels_deep() {
632 let cmds = make_parser_cmds();
633
634 assert_eq!(
635 Parser::new().parse("foo-c qux-c quux-c la la", &cmds.0, &cmds.1),
636 Outcome {
637 cmd_path: vec!["foo-c", "qux-c", "quux-c"],
638 remaining: vec!["la", "la"],
639 cmd_type: CommandType::Custom,
640 possibilities: Vec::new(),
641 leaf_completion: Some(Completion::Nothing),
642 complete: true,
643 }
644 );
645 }
646
647 #[test]
648 fn perfect_tie_custom_wins_tie_breaker() {
649 let cmds = make_parser_cmds();
650
651 assert_eq!(
652 Parser::new().parse("conflict-tie ha ha", &cmds.0, &cmds.1),
653 Outcome {
654 cmd_path: vec!["conflict-tie"],
655 remaining: vec!["ha", "ha"],
656 cmd_type: CommandType::Custom,
657 possibilities: Vec::new(),
658 leaf_completion: Some(Completion::Nothing),
659 complete: true,
660 }
661 );
662 }
663
664 #[test]
665 fn conflict_but_builtin_has_longer_match() {
666 let cmds = make_parser_cmds();
669
670 assert_eq!(
671 Parser::new().parse(
672 "conflict-builtin-longer-match-but-still-loses child ha",
673 &cmds.0,
674 &cmds.1
675 ),
676 Outcome {
677 cmd_path: vec!["conflict-builtin-longer-match-but-still-loses"],
678 remaining: vec!["child", "ha"],
679 cmd_type: CommandType::Custom,
680 possibilities: Vec::new(),
681 leaf_completion: Some(Completion::Nothing),
682 complete: true,
683 }
684 );
685 }
686
687 #[test]
688 fn conflict_but_custom_has_longer_match() {
689 let cmds = make_parser_cmds();
690
691 assert_eq!(
692 Parser::new().parse("conflict-custom-wins child ha", &cmds.0, &cmds.1),
693 Outcome {
694 cmd_path: vec!["conflict-custom-wins", "child"],
695 remaining: vec!["ha"],
696 cmd_type: CommandType::Custom,
697 possibilities: Vec::new(),
698 leaf_completion: Some(Completion::Nothing),
699 complete: true,
700 }
701 );
702 }
703
704 #[test]
705 fn cmd_level_partial_autocompletion_multiple_choices() {
706 let cmds = make_parser_cmds();
707
708 assert_eq!(
709 Parser::new().parse("foo-b bar-b h", &cmds.0, &cmds.1),
710 Outcome {
711 cmd_path: vec!["foo-b", "bar-b"],
712 remaining: vec!["h"],
713 cmd_type: CommandType::Builtin,
714 possibilities: Vec::new(),
715 leaf_completion: Some(Completion::PartialArgCompletion(vec![
716 String::from("ho"),
717 String::from("he")
718 ])),
719 complete: true,
720 }
721 );
722 }
723
724 #[test]
725 fn cmd_level_partial_autocompletion_single_choice() {
726 let cmds = make_parser_cmds();
727
728 assert_eq!(
729 Parser::new().parse("foo-b bar-b b", &cmds.0, &cmds.1),
730 Outcome {
731 cmd_path: vec!["foo-b", "bar-b"],
732 remaining: vec!["b"],
733 cmd_type: CommandType::Builtin,
734 possibilities: Vec::new(),
735 leaf_completion: Some(Completion::PartialArgCompletion(vec![String::from("bum"),])),
736 complete: true,
737 }
738 );
739 }
740
741 #[test]
742 fn cmd_level_completion_all_options() {
743 let cmds = make_parser_cmds();
744
745 assert_eq!(
746 Parser::new().parse("foo-b bar-b", &cmds.0, &cmds.1),
747 Outcome {
748 cmd_path: vec!["foo-b", "bar-b"],
749 remaining: vec![],
750 cmd_type: CommandType::Builtin,
751 possibilities: Vec::new(),
752 leaf_completion: Some(Completion::Possibilities(vec![
753 String::from("ho"),
754 String::from("he"),
755 String::from("bum"),
756 ])),
757 complete: true,
758 }
759 );
760 }
761
762 #[test]
763 fn cmd_level_completion_no_matches() {
764 let cmds = make_parser_cmds();
765
766 assert_eq!(
767 Parser::new().parse("foo-b bar-b z", &cmds.0, &cmds.1),
768 Outcome {
769 cmd_path: vec!["foo-b", "bar-b"],
770 remaining: vec!["z"],
771 cmd_type: CommandType::Builtin,
772 possibilities: Vec::new(),
773 leaf_completion: Some(Completion::Nothing),
774 complete: true,
775 }
776 );
777 }
778
779 #[test]
780 fn cmd_level_completion_already_complete() {
781 let cmds = make_parser_cmds();
782
783 assert_eq!(
784 Parser::new().parse("foo-b bar-b bum", &cmds.0, &cmds.1),
785 Outcome {
786 cmd_path: vec!["foo-b", "bar-b"],
787 remaining: vec!["bum"],
788 cmd_type: CommandType::Builtin,
789 possibilities: Vec::new(),
790 leaf_completion: Some(Completion::Nothing),
791 complete: true,
792 }
793 );
794 }
795
796 mod outcome {
797 use super::{CommandType, Completion, Outcome};
798
799 use pretty_assertions::assert_eq;
800
801 #[test]
802 fn outcome_error_msg() {
803 let outcome = Outcome {
804 cmd_path: vec!["foo", "bar"],
805 remaining: vec!["la", "la"],
806 cmd_type: CommandType::Custom,
807 possibilities: Vec::new(),
808 leaf_completion: None,
809 complete: false,
810 };
811
812 assert_eq!(
813 outcome.error_msg(),
814 vec![
815 "Failed to parse fully:\n",
816 "\n",
817 "\t (spaces trimmed)\n",
818 "\t => 'foo bar la la'\n",
819 "\t ^\n",
820 "expected a valid subcommand\n",
821 "instead, got: 'la';\n",
822 "\n",
823 "Run 'foo bar help' for more info on the command.\n",
824 "Run 'helptree' for more info on the entire command tree.\n",
825 ]
826 .join(""),
827 );
828 }
829
830 #[test]
831 fn empty_remaining_in_outcome() {
832 let outcome = Outcome {
833 cmd_path: vec!["foo", "bar"],
834 remaining: vec![],
835 cmd_type: CommandType::Custom,
836 possibilities: Vec::new(),
837 leaf_completion: None,
838 complete: false,
839 };
840
841 assert_eq!(
842 outcome.error_msg(),
843 vec![
844 "Failed to parse fully:\n",
845 "\n",
846 "\t (spaces trimmed)\n",
847 "\t => 'foo bar '\n",
848 "\t ^\n",
849 "expected a valid subcommand\n",
850 "instead, got: nothing;\n",
851 "\n",
852 "Run 'foo bar help' for more info on the command.\n",
853 "Run 'helptree' for more info on the entire command tree.\n",
854 ]
855 .join(""),
856 );
857 }
858
859 #[test]
860 fn empty() {
861 let outcome = Outcome {
862 cmd_path: vec![],
863 remaining: vec![],
864 cmd_type: CommandType::Custom,
865 possibilities: vec![
866 String::from("conflict-tie"),
867 String::from("conflict-builtin-longer-match-but-still-loses"),
868 String::from("conflict-custom-wins"),
869 String::from("foo-c"),
870 String::from("grault-c"),
871 ],
872 leaf_completion: None,
873 complete: false,
874 };
875
876 assert_eq!(
877 outcome.error_msg(),
878 vec![
879 "Empty string could not be parsed as a command.\n",
880 "\n",
881 "\t => expected one of 'conflict-tie' or 'conflict-builtin-longer-match-but-still-loses' or 'conflict-custom-wins' or 'foo-c' or 'grault-c'.",
882 "\n",
883 "\n",
884 "Run 'helptree' for more info on the entire command tree.\n",
885 ]
886 .join(""),
887 );
888 }
889
890 #[test]
891 fn unrecognized_first_cmd() {
892 let outcome = Outcome {
893 cmd_path: vec![],
894 remaining: vec!["notfound", "la"],
895 cmd_type: CommandType::Custom,
896 possibilities: Vec::new(),
897 leaf_completion: None,
898 complete: false,
899 };
900
901 assert_eq!(
902 outcome.error_msg(),
903 vec![
904 "'notfound' is not a recognized command.\n",
905 "Run 'helptree' for more info on the entire command tree.\n",
906 ]
907 .join(""),
908 );
909 }
910
911 #[test]
912 fn error_msg_is_blank_for_complete_parse() {
913 let outcome = Outcome {
914 cmd_path: vec![],
915 remaining: vec![],
916 cmd_type: CommandType::Custom,
917 possibilities: Vec::new(),
918 leaf_completion: Some(Completion::Nothing),
919 complete: true,
920 };
921
922 assert_eq!(outcome.error_msg(), String::from(""));
923 }
924 }
925}