Skip to main content

bpaf/
error.rs

1use std::ops::Range;
2
3use crate::{
4    args::{Arg, State},
5    buffer::{Block, Color, Doc, Style, Token},
6    item::{Item, ShortLong},
7    meta_help::Metavar,
8    meta_youmean::{Suggestion, Variant},
9    Meta,
10};
11
12/// Unsuccessful command line parsing outcome, internal representation
13#[derive(Debug)]
14pub struct Error(pub(crate) Message);
15
16impl Error {
17    pub(crate) fn combine_with(self, other: Self) -> Self {
18        Error(self.0.combine_with(other.0))
19    }
20}
21
22#[derive(Debug)]
23pub(crate) enum Message {
24    // those can be caught ---------------------------------------------------------------
25    /// Tried to consume an env variable with no fallback, variable was not set
26    NoEnv(&'static str),
27
28    /// User specified an error message on some
29    ParseSome(&'static str),
30
31    /// User asked for parser to fail explicitly
32    ParseFail(&'static str),
33
34    /// pure_with failed to parse a value
35    PureFailed(String),
36
37    /// Expected one of those values
38    ///
39    /// Used internally to generate better error messages
40    Missing(Vec<MissingItem>),
41
42    // those cannot be caught-------------------------------------------------------------
43    /// Parsing failed and this is the final output
44    ParseFailure(ParseFailure),
45
46    /// Tried to consume a strict positional argument, value was present but was not strictly
47    /// positional
48    StrictPos(usize, Metavar),
49
50    /// Tried to consume a non-strict positional argument, but the value was strict
51    NonStrictPos(usize, Metavar),
52
53    /// Parser provided by user failed to parse a value
54    ParseFailed(Option<usize>, String),
55
56    /// Parser provided by user failed to validate a value
57    GuardFailed(Option<usize>, &'static str),
58
59    /// Argument requres a value but something else was passed,
60    /// required: --foo <BAR>
61    /// given: --foo --bar
62    ///        --foo -- bar
63    ///        --foo
64    NoArgument(usize, Metavar),
65
66    /// Parser is expected to consume all the things from the command line
67    /// this item will contain an index of the unconsumed value
68    Unconsumed(/* TODO - unused? */ usize),
69
70    /// argument is ambigoups - parser can accept it as both a set of flags and a short flag with no =
71    Ambiguity(usize, String),
72
73    /// Suggested fixes for typos or missing input
74    Suggestion(usize, Suggestion),
75
76    /// Two arguments are mutually exclusive
77    /// --release --dev
78    Conflict(/* winner */ usize, usize),
79
80    /// Expected one or more items in the scope, got someting else if any
81    Expected(Vec<Item>, Option<usize>),
82
83    /// Parameter is accepted but only once
84    OnlyOnce(/* winner */ usize, usize),
85}
86
87impl Message {
88    pub(crate) fn can_catch(&self) -> bool {
89        match self {
90            Message::NoEnv(_)
91            | Message::ParseSome(_)
92            | Message::ParseFail(_)
93            | Message::Missing(_)
94            | Message::PureFailed(_)
95            | Message::NonStrictPos(_, _) => true,
96            Message::StrictPos(_, _)
97            | Message::ParseFailed(_, _)
98            | Message::GuardFailed(_, _)
99            | Message::Unconsumed(_)
100            | Message::Ambiguity(_, _)
101            | Message::Suggestion(_, _)
102            | Message::Conflict(_, _)
103            | Message::ParseFailure(_)
104            | Message::Expected(_, _)
105            | Message::OnlyOnce(_, _)
106            | Message::NoArgument(_, _) => false,
107        }
108    }
109
110    /// A list of errors that `catch` can't catch
111    pub(crate) fn wrong_input(&self) -> bool {
112        matches!(
113            self,
114            Message::StrictPos(_, _)
115                | Message::NonStrictPos(_, _)
116                | Message::NoArgument(_, _)
117                | Message::Unconsumed(_)
118                | Message::Ambiguity(_, _)
119        )
120    }
121}
122
123/// Missing item in a context
124#[derive(Debug, Clone)]
125pub struct MissingItem {
126    /// Item that is missing
127    pub(crate) item: Item,
128    /// Position it is missing from - exact for positionals, earliest possible for flags
129    pub(crate) position: usize,
130    /// Range where search was performed, important for combinators that narrow the search scope
131    /// such as adjacent
132    pub(crate) scope: Range<usize>,
133}
134
135impl Message {
136    #[must_use]
137    pub(crate) fn combine_with(self, other: Self) -> Self {
138        #[allow(clippy::match_same_arms)]
139        match (self, other) {
140            // help output takes priority
141            (a @ Message::ParseFailure(_), _) => a,
142            (_, b @ Message::ParseFailure(_)) => b,
143
144            // combine missing elements
145            (Message::Missing(mut a), Message::Missing(mut b)) => {
146                a.append(&mut b);
147                Message::Missing(a)
148            }
149
150            // otherwise earliest wins
151            (a, b) => {
152                if a.can_catch() {
153                    b
154                } else {
155                    a
156                }
157            }
158        }
159    }
160}
161
162/// Unsuccessful command line parsing outcome, use it for unit tests
163///
164/// When [`OptionParser::run_inner`](crate::OptionParser::run_inner) produces `Err(ParseFailure)`
165/// it means that the parser couldn't produce the value it supposed to produce and the program
166/// should terminate.
167///
168/// If you are handling variants manually - `Stdout` contains formatted output and you can use any
169/// logging framework to produce the output, `Completion` should be printed to stdout unchanged -
170/// shell completion mechanism relies on that. In both cases application should exit with error
171/// code of 0. `Stderr` variant indicates a genuinly parsing error which should be printed to
172/// stderr or a logging framework of your choice as an error and the app should exit with error
173/// code of 1. [`ParseFailure::exit_code`] is a helper method that performs printing and produces
174/// the exit code to use.
175///
176/// For purposes of for unit testing for user parsers, you can consume it with
177/// [`ParseFailure::unwrap_stdout`] and [`ParseFailure::unwrap_stdout`] - both of which produce a
178/// an unformatted `String` that parser might produce if failure type is correct or panics
179/// otherwise.
180#[derive(Clone, Debug)]
181pub enum ParseFailure {
182    /// Print this to stdout and exit with success code
183    Stdout(Doc, bool),
184    /// This also goes to stdout with exit code of 0,
185    /// this cannot be Doc because completion needs more control about rendering
186    Completion(String),
187    /// Print this to stderr and exit with failure code
188    Stderr(Doc),
189}
190
191impl ParseFailure {
192    /// Returns the contained `stderr` values - for unit tests
193    ///
194    /// # Panics
195    ///
196    /// Panics if failure contains `stdout`
197    #[allow(clippy::must_use_candidate)]
198    #[track_caller]
199    pub fn unwrap_stderr(self) -> String {
200        match self {
201            Self::Stderr(err) => err.monochrome(true),
202            Self::Completion(..) | Self::Stdout(..) => panic!("not an stderr: {:?}", self),
203        }
204    }
205
206    /// Returns the contained `stdout` values - for unit tests
207    ///
208    /// # Panics
209    ///
210    /// Panics if failure contains `stderr`
211    #[allow(clippy::must_use_candidate)]
212    #[track_caller]
213    pub fn unwrap_stdout(self) -> String {
214        match self {
215            Self::Stdout(err, full) => err.monochrome(full),
216            Self::Completion(s) => s,
217            Self::Stderr(..) => panic!("not an stdout: {:?}", self),
218        }
219    }
220
221    /// Returns the exit code for the failure
222    #[allow(clippy::must_use_candidate)]
223    pub fn exit_code(self) -> i32 {
224        match self {
225            Self::Stdout(..) | Self::Completion(..) => 0,
226            Self::Stderr(..) => 1,
227        }
228    }
229
230    #[doc(hidden)]
231    #[deprecated = "Please use ParseFailure::print_message, with two s"]
232    pub fn print_mesage(&self, max_width: usize) {
233        self.print_message(max_width)
234    }
235
236    /// Prints a message to `stdout` or `stderr` appropriate to the failure.
237    pub fn print_message(&self, max_width: usize) {
238        let color = Color::default();
239        match self {
240            ParseFailure::Stdout(msg, full) => {
241                println!("{}", msg.render_console(*full, color, max_width));
242            }
243            ParseFailure::Completion(s) => {
244                print!("{}", s);
245            }
246            ParseFailure::Stderr(msg) => {
247                #[allow(unused_mut)]
248                let mut error;
249                #[cfg(not(feature = "color"))]
250                {
251                    error = "Error: ";
252                }
253
254                #[cfg(feature = "color")]
255                {
256                    error = String::new();
257                    color.push_str(Style::Invalid, &mut error, "Error: ");
258                }
259
260                eprintln!("{}{}", error, msg.render_console(true, color, max_width));
261            }
262        }
263    }
264}
265
266fn check_conflicts(args: &State) -> Option<Message> {
267    let (loser, winner) = args.conflict()?;
268    Some(Message::Conflict(winner, loser))
269}
270
271fn textual_part(args: &State, ix: Option<usize>) -> Option<std::borrow::Cow<'_, str>> {
272    match args.items.get(ix?)? {
273        Arg::Short(_, _, _) | Arg::Long(_, _, _) => None,
274        Arg::ArgWord(s) | Arg::Word(s) | Arg::PosWord(s) => Some(s.to_string_lossy()),
275    }
276}
277
278fn only_once(args: &State, cur: usize) -> Option<usize> {
279    if cur == 0 {
280        return None;
281    }
282    let mut iter = args.items[..cur].iter().rev();
283    let offset = match args.items.get(cur)? {
284        Arg::Short(s, _, _) => iter.position(|a| a.match_short(*s)),
285        Arg::Long(l, _, _) => iter.position(|a| a.match_long(l)),
286        Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_) => None,
287    };
288    Some(cur - offset? - 1)
289}
290
291impl Message {
292    #[allow(clippy::too_many_lines)] // it's a huge match with lots of simple cases
293    pub(crate) fn render(mut self, args: &State, meta: &Meta) -> ParseFailure {
294        // try to come up with a better error message for a few cases
295        match self {
296            Message::Unconsumed(ix) => {
297                if let Some(conflict) = check_conflicts(args) {
298                    self = conflict;
299                } else if let Some(prev_ix) = only_once(args, ix) {
300                    self = Message::OnlyOnce(prev_ix, ix);
301                } else if let Some((ix, suggestion)) = crate::meta_youmean::suggest(args, meta) {
302                    self = Message::Suggestion(ix, suggestion);
303                }
304            }
305            Message::Missing(xs) => {
306                self = summarize_missing(&xs, meta, args);
307            }
308            _ => {}
309        }
310
311        let mut doc = Doc::default();
312        match self {
313            // already rendered
314            Message::ParseFailure(f) => return f,
315
316            // this case is handled above
317            Message::Missing(_) => {
318                // this one is unreachable
319            }
320
321            // Error: --foo is not expected in this context
322            Message::Unconsumed(ix) => {
323                let item = &args.items[ix];
324                doc.token(Token::BlockStart(Block::TermRef));
325                doc.write(item, Style::Invalid);
326                doc.token(Token::BlockEnd(Block::TermRef));
327                doc.text(" is not expected in this context");
328            }
329
330            // Error: environment variable FOO is not set
331            Message::NoEnv(name) => {
332                doc.text("environment variable ");
333                doc.token(Token::BlockStart(Block::TermRef));
334                doc.invalid(name);
335                doc.token(Token::BlockEnd(Block::TermRef));
336                doc.text(" is not set");
337            }
338
339            // Error: FOO expected to be  in the right side of --
340            Message::StrictPos(_ix, metavar) => {
341                doc.text("expected ");
342                doc.token(Token::BlockStart(Block::TermRef));
343                doc.metavar(metavar);
344                doc.token(Token::BlockEnd(Block::TermRef));
345                doc.text(" to be on the right side of ");
346                doc.token(Token::BlockStart(Block::TermRef));
347                doc.literal("--");
348                doc.token(Token::BlockEnd(Block::TermRef));
349            }
350
351            // Error: FOO expected to be on the left side of --
352            Message::NonStrictPos(_ix, metavar) => {
353                doc.text("expected ");
354                doc.token(Token::BlockStart(Block::TermRef));
355                doc.metavar(metavar);
356                doc.token(Token::BlockEnd(Block::TermRef));
357                doc.text(" to be on the left side of ");
358                doc.token(Token::BlockStart(Block::TermRef));
359                doc.literal("--");
360                doc.token(Token::BlockEnd(Block::TermRef));
361            }
362
363            // Error: <message from some or fail>
364            Message::ParseSome(s) | Message::ParseFail(s) => {
365                doc.text(s);
366            }
367
368            // Error: couldn't parse FIELD: <FromStr message>
369            Message::ParseFailed(mix, s) => {
370                doc.text("couldn't parse");
371                if let Some(field) = textual_part(args, mix) {
372                    doc.text(" ");
373                    doc.token(Token::BlockStart(Block::TermRef));
374                    if field.is_empty() {
375                        let mut s = String::new();
376                        if let Some(ix) = mix {
377                            if ix > 0 {
378                                if let Some(arg) = args.items.get(ix - 1) {
379                                    use std::fmt::Write as _;
380                                    match arg {
381                                        Arg::Short(n, true, _) => _ = write!(&mut s, "-{n}="),
382                                        Arg::Short(n, false, _) => _ = write!(&mut s, "-{n} "),
383                                        Arg::Long(l, false, _) => _ = write!(&mut s, "--{l} "),
384                                        Arg::Long(l, true, _) => _ = write!(&mut s, "--{l}="),
385                                        _ => {}
386                                    }
387                                }
388                            }
389                        }
390                        s.push_str(r#""""#);
391                        doc.invalid(&s);
392                    } else {
393                        doc.invalid(&field);
394                    }
395                    doc.token(Token::BlockEnd(Block::TermRef));
396                }
397                doc.text(": ");
398                doc.text(&s);
399            }
400
401            // Error: ( FIELD:  | check failed: ) <message from guard>
402            Message::GuardFailed(mix, s) => {
403                if let Some(field) = textual_part(args, mix) {
404                    doc.token(Token::BlockStart(Block::TermRef));
405                    doc.invalid(&field);
406                    doc.token(Token::BlockEnd(Block::TermRef));
407                    doc.text(": ");
408                } else {
409                    doc.text("check failed: ");
410                }
411                doc.text(s);
412            }
413
414            // Error: --foo requires an argument FOO, got a flag --bar, try --foo=-bar to use it as an argument
415            // Error: --foo requires an argument FOO
416            Message::NoArgument(x, mv) => match args.get(x + 1) {
417                Some(Arg::Short(_, _, os) | Arg::Long(_, _, os)) => {
418                    let arg = &args.items[x];
419                    let os = &os.to_string_lossy();
420
421                    doc.token(Token::BlockStart(Block::TermRef));
422                    doc.write(arg, Style::Literal);
423                    doc.token(Token::BlockEnd(Block::TermRef));
424                    doc.text(" requires an argument ");
425                    doc.token(Token::BlockStart(Block::TermRef));
426                    doc.metavar(mv);
427                    doc.token(Token::BlockEnd(Block::TermRef));
428                    doc.text(", got a flag ");
429                    doc.token(Token::BlockStart(Block::TermRef));
430                    doc.write(os, Style::Invalid);
431                    doc.token(Token::BlockEnd(Block::TermRef));
432                    doc.text(", try ");
433                    doc.token(Token::BlockStart(Block::TermRef));
434                    doc.write(arg, Style::Literal);
435                    doc.literal("=");
436                    doc.write(os, Style::Literal);
437                    doc.token(Token::BlockEnd(Block::TermRef));
438                    doc.text(" to use it as an argument");
439                }
440                // "Some" part of this branch is actually unreachable
441                Some(Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_)) | None => {
442                    let arg = &args.items[x];
443                    doc.token(Token::BlockStart(Block::TermRef));
444                    doc.write(arg, Style::Literal);
445                    doc.token(Token::BlockEnd(Block::TermRef));
446                    doc.text(" requires an argument ");
447                    doc.token(Token::BlockStart(Block::TermRef));
448                    doc.metavar(mv);
449                    doc.token(Token::BlockEnd(Block::TermRef));
450                }
451            },
452            // Error: <message from pure_with>
453            Message::PureFailed(s) => {
454                doc.text(&s);
455            }
456            // Error: app supports -f as both an option and an option-argument, try to split -foo
457            // into invididual options (-f -o ..) or use -f=oo syntax to disambiguate
458            Message::Ambiguity(ix, name) => {
459                let mut chars = name.chars();
460                let first = chars.next().unwrap();
461                let rest = chars.as_str();
462                let second = chars.next().unwrap();
463                let s = args.items[ix].os_str().to_str().unwrap();
464
465                if let Some(name) = args.path.first() {
466                    doc.literal(name);
467                    doc.text(" supports ");
468                } else {
469                    doc.text("app supports ");
470                }
471
472                doc.token(Token::BlockStart(Block::TermRef));
473                doc.literal("-");
474                doc.write_char(first, Style::Literal);
475                doc.token(Token::BlockEnd(Block::TermRef));
476                doc.text(" as both an option and an option-argument, try to split ");
477                doc.token(Token::BlockStart(Block::TermRef));
478                doc.write(s, Style::Literal);
479                doc.token(Token::BlockEnd(Block::TermRef));
480                doc.text(" into individual options (");
481                doc.literal("-");
482                doc.write_char(first, Style::Literal);
483                doc.literal(" -");
484                doc.write_char(second, Style::Literal);
485                doc.literal(" ..");
486                doc.text(") or use ");
487                doc.token(Token::BlockStart(Block::TermRef));
488                doc.literal("-");
489                doc.write_char(first, Style::Literal);
490                doc.literal("=");
491                doc.literal(rest);
492                doc.token(Token::BlockEnd(Block::TermRef));
493                doc.text(" syntax to disambiguate");
494            }
495            // Error: No such (flag|argument|command), did you mean  ...
496            Message::Suggestion(ix, suggestion) => {
497                let actual = &args.items[ix].to_string();
498                match suggestion {
499                    Suggestion::Variant(v) => {
500                        let ty = match &args.items[ix] {
501                            _ if actual.starts_with('-') => "flag",
502                            Arg::Short(_, _, _) | Arg::Long(_, _, _) => "flag",
503                            Arg::ArgWord(_) => "argument value",
504                            Arg::Word(_) | Arg::PosWord(_) => "command or positional",
505                        };
506
507                        doc.text("no such ");
508                        doc.text(ty);
509                        doc.text(": ");
510                        doc.token(Token::BlockStart(Block::TermRef));
511                        doc.invalid(actual);
512                        doc.token(Token::BlockEnd(Block::TermRef));
513                        doc.text(", did you mean ");
514                        doc.token(Token::BlockStart(Block::TermRef));
515
516                        match v {
517                            Variant::CommandLong(name) => doc.literal(name),
518                            Variant::Flag(ShortLong::Long(l) | ShortLong::Both(_, l)) => {
519                                doc.literal("--");
520                                doc.literal(l);
521                            }
522                            Variant::Flag(ShortLong::Short(s)) => {
523                                doc.literal("-");
524                                doc.write_char(s, Style::Literal);
525                            }
526                        };
527
528                        doc.token(Token::BlockEnd(Block::TermRef));
529                        doc.text("?");
530                    }
531                    Suggestion::MissingDash(name) => {
532                        doc.text("no such flag: ");
533                        doc.token(Token::BlockStart(Block::TermRef));
534                        doc.literal("-");
535                        doc.literal(name);
536                        doc.token(Token::BlockEnd(Block::TermRef));
537                        doc.text(" (with one dash), did you mean ");
538                        doc.token(Token::BlockStart(Block::TermRef));
539                        doc.literal("--");
540                        doc.literal(name);
541                        doc.token(Token::BlockEnd(Block::TermRef));
542                        doc.text("?");
543                    }
544                    Suggestion::ExtraDash(name) => {
545                        doc.text("no such flag: ");
546                        doc.token(Token::BlockStart(Block::TermRef));
547                        doc.literal("--");
548                        doc.write_char(name, Style::Literal);
549                        doc.token(Token::BlockEnd(Block::TermRef));
550                        doc.text(" (with two dashes), did you mean ");
551                        doc.token(Token::BlockStart(Block::TermRef));
552                        doc.literal("-");
553                        doc.write_char(name, Style::Literal);
554                        doc.token(Token::BlockEnd(Block::TermRef));
555                        doc.text("?");
556                    }
557                    Suggestion::Nested(x, v) => {
558                        let ty = match v {
559                            Variant::CommandLong(_) => "subcommand",
560                            Variant::Flag(_) => "flag",
561                        };
562                        doc.text(ty);
563                        doc.text(" ");
564                        doc.token(Token::BlockStart(Block::TermRef));
565                        doc.literal(actual);
566                        doc.token(Token::BlockEnd(Block::TermRef));
567                        doc.text(
568                            " is not valid in this context, did you mean to pass it to command ",
569                        );
570                        doc.token(Token::BlockStart(Block::TermRef));
571                        doc.literal(&x);
572                        doc.token(Token::BlockEnd(Block::TermRef));
573                        doc.text("?");
574                    }
575                }
576            }
577            // Error: Expected (no arguments|--foo), got ..., pass --help
578            Message::Expected(exp, actual) => {
579                doc.text("expected ");
580                match exp.len() {
581                    0 => {
582                        doc.text("no arguments");
583                    }
584                    1 => {
585                        doc.token(Token::BlockStart(Block::TermRef));
586                        doc.write_item(&exp[0]);
587                        doc.token(Token::BlockEnd(Block::TermRef));
588                    }
589                    2 => {
590                        doc.token(Token::BlockStart(Block::TermRef));
591                        doc.write_item(&exp[0]);
592                        doc.token(Token::BlockEnd(Block::TermRef));
593                        doc.text(" or ");
594                        doc.token(Token::BlockStart(Block::TermRef));
595                        doc.write_item(&exp[1]);
596                        doc.token(Token::BlockEnd(Block::TermRef));
597                    }
598                    _ => {
599                        doc.token(Token::BlockStart(Block::TermRef));
600                        doc.write_item(&exp[0]);
601                        doc.token(Token::BlockEnd(Block::TermRef));
602                        doc.text(", ");
603                        doc.token(Token::BlockStart(Block::TermRef));
604                        doc.write_item(&exp[1]);
605                        doc.token(Token::BlockEnd(Block::TermRef));
606                        doc.text(", or more");
607                    }
608                }
609                match actual {
610                    Some(actual) => {
611                        doc.text(", got ");
612                        doc.token(Token::BlockStart(Block::TermRef));
613                        doc.write(&args.items[actual], Style::Invalid);
614                        doc.token(Token::BlockEnd(Block::TermRef));
615                        doc.text(". Pass ");
616                    }
617                    None => {
618                        doc.text(", pass ");
619                    }
620                }
621                doc.token(Token::BlockStart(Block::TermRef));
622                doc.literal("--help");
623                doc.token(Token::BlockEnd(Block::TermRef));
624                doc.text(" for usage information");
625            }
626
627            // Error: --intel cannot be used at the same time as --att
628            Message::Conflict(winner, loser) => {
629                doc.token(Token::BlockStart(Block::TermRef));
630                doc.write(&args.items[loser], Style::Literal);
631                doc.token(Token::BlockEnd(Block::TermRef));
632                doc.text(" cannot be used at the same time as ");
633                doc.token(Token::BlockStart(Block::TermRef));
634                doc.write(&args.items[winner], Style::Literal);
635                doc.token(Token::BlockEnd(Block::TermRef));
636            }
637
638            // Error: argument FOO cannot be used multiple times in this context
639            Message::OnlyOnce(_winner, loser) => {
640                doc.text("argument ");
641                doc.token(Token::BlockStart(Block::TermRef));
642                doc.write(&args.items[loser], Style::Literal);
643                doc.token(Token::BlockEnd(Block::TermRef));
644                doc.text(" cannot be used multiple times in this context");
645            }
646        };
647
648        ParseFailure::Stderr(doc)
649    }
650}
651
652/// go over all the missing items, pick the left most scope
653pub(crate) fn summarize_missing(items: &[MissingItem], inner: &Meta, args: &State) -> Message {
654    // missing items can belong to different scopes, pick the best scope to work with
655    let best_item = match items
656        .iter()
657        .max_by_key(|item| (item.position, item.scope.start))
658    {
659        Some(x) => x,
660        None => return Message::ParseSome("parser requires an extra flag, argument or parameter, but its name is hidden by the author"),
661    };
662
663    let mut best_scope = best_item.scope.clone();
664
665    let mut saw_command = false;
666    let expected = items
667        .iter()
668        .filter_map(|i| {
669            let cmd = matches!(i.item, Item::Command { .. });
670            if i.scope == best_scope && !(saw_command && cmd) {
671                saw_command |= cmd;
672                Some(i.item.clone())
673            } else {
674                None
675            }
676        })
677        .collect::<Vec<_>>();
678
679    best_scope.start = best_scope.start.max(best_item.position);
680    let mut args = args.clone();
681    args.set_scope(best_scope);
682    if let Some((ix, _arg)) = args.items_iter().next() {
683        if let Some((ix, sugg)) = crate::meta_youmean::suggest(&args, inner) {
684            Message::Suggestion(ix, sugg)
685        } else {
686            Message::Expected(expected, Some(ix))
687        }
688    } else {
689        Message::Expected(expected, None)
690    }
691}
692
693/*
694#[inline(never)]
695/// the idea is to post some context for the error
696fn snip(buffer: &mut Buffer, args: &State, items: &[usize]) {
697    for ix in args.scope() {
698        buffer.write(ix, Style::Text);
699    }
700}
701*/