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