shi/
parser.rs

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
7/// A parser that parses input lines into `Command` invocations.
8pub struct Parser {
9    tokenizer: DefaultTokenizer,
10}
11
12#[derive(Debug, PartialEq)]
13/// CommandType represents part of a parse result. A parse attempt for a command will result in a
14/// decision on whether a given input line represents a `Builtin` command, a `Custom` command, or
15/// `Unknown`, in the case of an unsuccessful or incomplete parse.
16pub enum CommandType {
17    Builtin,
18    Custom,
19    Unknown,
20}
21
22#[derive(Debug, PartialEq)]
23/// Outcome is the final result of a parse attempt. It includes various useful bits of information
24/// from the parse:
25///
26/// * `cmd_path` - The components of the command invocation. In particular, it shows the chain of
27/// ancestry in terms of `Parent` commands and the eventual `Leaf` command.
28/// * `remaining` - The remaining components of the string. In the case of a successful parse, this
29/// represents the arguments passed to the command. In the case of unsuccessful or incomplete
30/// parses, this represents the part of the input string that was not able to be parsed.
31/// * `cmd_type` - The type of the command. See `CommandType`.
32/// * `possibilities` - Includes the potential candidates that the parser is expecting to see
33/// following the input line.
34/// * `complete` - A flag denoting whether we had a successful and complete parse.
35pub 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    /// Prints an error message for the `Outcome`. Of course, if the `Outcome` was complete, the
59    /// error message is empty.
60    pub fn error_msg(&self) -> String {
61        // TODO: We should split apart this function.
62
63        // If we parsed successfully, we obviously shouldn't produce an error message.
64        if self.complete {
65            return String::from("");
66        }
67
68        // This will be our String buffer.
69        let mut msg = String::new();
70
71        if self.cmd_path.is_empty() && self.remaining.is_empty() {
72            // In this case, we must have found an empty string, which is obviously not parseable
73            // as a command.
74            msg += "Empty string could not be parsed as a command.";
75        } else if self.cmd_path.is_empty() {
76            // If the `cmd_path` is empty, this implies we immediately failed the parse, and it was
77            // not at least partially complete. This then implies that the first element of the
78            // remaining components must be the thing we failed to parse as a recognized command.
79            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                // This should not be possible. If the remaining tokens were empty, then the prior
86                // if case should have caught it.
87                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    /// Constructs a new Parser.
139    pub fn new() -> Parser {
140        Parser {
141            tokenizer: DefaultTokenizer::new(vec!['\'', '"']),
142        }
143    }
144
145    /// Parses a given Vector of tokens into a parse `Outcome`.
146    ///
147    /// # Arguments
148    /// `tokens` - The tokens produced from an input line.
149    /// `cmd_type` - The type of command contained in `set`. See `CommandType`.
150    /// `set` - The available commands to parse into.
151    ///
152    /// # Returns
153    /// `Outcome` - The parse outcome, given the arguments.
154    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            // Try looking up the token in our set.
164            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                        // NOTE Since i < len, .get(i..) will never panic.
173                        remaining: tokenization.tokens.get(i..).unwrap().to_vec(),
174                        cmd_type: if i == 0 {
175                            // If this is the first lookup, then obviously we have no idea what the
176                            // type is.
177                            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            // At this point, we have successfully found the token in the set.
189            // Now, if this command has children, we want to go deeper into the set.
190            // If it is a leaf command, and has we're actually done and can return the current
191            // cmd_path and remaining tokens and complete.
192            match &**looked_up_cmd {
193                Command::Leaf(cmd) => {
194                    // This is a leaf command, so we are actually almost done.
195                    // Leaf commands themselves, can, given their arguments, attempt a local
196                    // autocompletion. Let's give that a shot and then finish.
197                    return Outcome {
198                        cmd_path,
199                        // NOTE Since i < len, .get(i+1..) will never panic.
200                        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        // We will basically only arrive here if the number of tokens is zero.
217        Outcome {
218            cmd_path,
219            remaining: Vec::new(), // If we get here, we are out of tokens anyways.
220            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    /// Parses a given Vector of tokens into a parse `Outcome`.
232    ///
233    /// # Arguments
234    /// `tokens` - The tokens produced from an input line.
235    /// `cmds` - The available custom commands to parse into.
236    /// `builtins` - The available builtins to parse into.
237    ///
238    /// # Returns
239    /// `Outcome` - The parse outcome, given the arguments.
240    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    /// Parses the given information into a parse `Outcome`.
261    ///
262    /// # Arguments
263    /// `line` - The input line.
264    /// `cmds` - The available custom commands to parse into.
265    /// `builtins` - The available builtins to parse into.
266    ///
267    /// # Returns
268    /// `Outcome` - The parse outcome, given the arguments.
269    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    // TODO: We should not need to stutter and call it 'ParseTestCommand'. Just call it
292    // 'TestCommand'. It's obviously for parser tests.
293    #[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 we don't have any autocompletions set, then just short-circuit out.
335            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                        // If the last argument is in our autocompletions, then we're good, nothing
343                        // more to complete.
344                        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                            // If nothing matched, then we have no completions.
355                            return Completion::Nothing;
356                        }
357                        // If not, then perhaps it is a prefix of an autocompletion. Let's give
358                        // back some partial arg completions if so!
359                        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                // Although these match other command names, since they come after grault, we
574                // expect them to be treated as basic arguments.
575                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        // We are testing that custom commands have a higher precedence. Although this command
667        // exists identically in the builtin set, the custom variant is chosen.
668        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}