Skip to main content

winnow_args/
error.rs

1//! What went wrong, and where.
2//!
3//! [`Error`] is the error type of every parser in this crate. It implements
4//! winnow's [`ParserError`] and [`ModalError`] itself rather than living inside
5//! an `ErrMode`: `alt` asks [`ParserError::is_backtrack`], so one flag is all
6//! the modality a command line needs, and the common case — a branch of an
7//! `alt` that did not match — builds a 24-byte value without allocating.
8//!
9//! An error that reaches the user is printed by [`Error::render`]: the
10//! message, then a tip when a flag, a subcommand or a choice close to what
11//! was typed is accepted there ([`Error::similar`]). Only a failed parse
12//! looks for one.
13
14use std::fmt;
15
16use winnow::error::{FromExternalError, ModalError, ParserError};
17
18use crate::help::Style;
19use crate::stream::Argv;
20
21mod suggest;
22
23/// Boxed cause of an [`ErrorKind::InvalidValue`].
24pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
25
26/// The class of failure.
27#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum ErrorKind {
30    /// A parser did not match here; another one may.
31    Backtrack,
32    /// A flag-like word matched no flag.
33    UnknownFlag,
34    /// A word nothing could hold.
35    UnexpectedArg,
36    /// A flag needing a value did not get one.
37    MissingValue,
38    /// A flag that takes no value was given one: `--verbose=yes`.
39    UnexpectedValue,
40    /// A required flag never appeared.
41    MissingRequired,
42    /// A required positional argument was never filled.
43    MissingArgument,
44    /// A required subcommand was not given.
45    MissingSubcommand,
46    /// Two arguments that exclude each other were both given.
47    Conflict,
48    /// A required group had none of its members.
49    MissingOneOf,
50    /// A word reached an argument that only takes words after `--`.
51    RequiresDoubleDash,
52    /// `-h`/`--help`, or a bare invocation of an `arg_required_else_help`
53    /// command: show [`Error::render_help`] rather than report a failure.
54    HelpRequested,
55    /// `-V`/`--version`: show [`Error::version_text`].
56    VersionRequested,
57    /// A value that its type rejected.
58    InvalidValue,
59    /// A value outside a fixed set.
60    InvalidChoice,
61}
62
63/// What `--help` or `--version` asked for, and where.
64#[derive(Debug)]
65struct Request {
66    command: &'static crate::help::Command,
67    long: bool,
68    /// A bare invocation rather than a flag: help belongs on stderr.
69    bare: bool,
70    /// Subcommand names from the root down to `command`.
71    path: Vec<&'static str>,
72    /// The command the names start from, once [`Error::suggested`] says:
73    /// its page lists the global flags of the commands above it.
74    root: Option<&'static crate::help::Command>,
75}
76
77#[derive(Debug, Default)]
78struct Detail {
79    request: Option<Request>,
80    /// The flag as spelled on the command line (or its display name), or the word.
81    token: String,
82    value: Option<String>,
83    cause: Option<BoxError>,
84    /// Subcommand names from the root down to where it failed.
85    path: Vec<&'static str>,
86    /// What was probably meant, and what it is: "flag".
87    similar: Option<(&'static str, String)>,
88}
89
90/// A parse failure.
91#[derive(Debug)]
92pub struct Error {
93    kind: ErrorKind,
94    cut: bool,
95    offset: usize,
96    detail: Option<Box<Detail>>,
97}
98
99impl Error {
100    fn with_token(kind: ErrorKind, offset: usize, token: String) -> Self {
101        Self {
102            kind,
103            cut: true,
104            offset,
105            detail: Some(Box::new(Detail {
106                token,
107                ..Detail::default()
108            })),
109        }
110    }
111
112    /// A flag-like word, at `offset`, that matched nothing.
113    pub fn unknown_flag(offset: usize, token: impl Into<String>) -> Self {
114        Self::with_token(ErrorKind::UnknownFlag, offset, token.into())
115    }
116
117    /// A word, at `offset`, that nothing could hold.
118    pub fn unexpected_arg(offset: usize, token: impl Into<String>) -> Self {
119        Self::with_token(ErrorKind::UnexpectedArg, offset, token.into())
120    }
121
122    /// `flag`, at `offset`, needed a value and did not get one.
123    pub fn missing_value(offset: usize, flag: impl Into<String>) -> Self {
124        Self::with_token(ErrorKind::MissingValue, offset, flag.into())
125    }
126
127    /// `flag`, at `offset`, takes no value but was given `value`.
128    pub fn unexpected_value(offset: usize, flag: impl Into<String>, value: &[u8]) -> Self {
129        let mut error = Self::with_token(ErrorKind::UnexpectedValue, offset, flag.into());
130        error.detail_mut().value = Some(String::from_utf8_lossy(value).into_owned());
131        error
132    }
133
134    /// The required `flag` never appeared. The offset is the end of the line.
135    pub fn missing_required(offset: usize, flag: impl Into<String>) -> Self {
136        Self::with_token(ErrorKind::MissingRequired, offset, flag.into())
137    }
138
139    /// The required positional `name` was never filled. The offset is the end of the line.
140    pub fn missing_argument(offset: usize, name: impl Into<String>) -> Self {
141        Self::with_token(ErrorKind::MissingArgument, offset, name.into())
142    }
143
144    /// `name` requires `target`, which has no value. The offset is the end of the line.
145    pub fn required_by(offset: usize, target: impl Into<String>, name: &str) -> Self {
146        let mut error = Self::with_token(ErrorKind::MissingRequired, offset, target.into());
147        error.detail_mut().value = Some(name.to_owned());
148        error
149    }
150
151    /// `name` and `other` exclude each other and were both given.
152    pub fn conflict(offset: usize, name: impl Into<String>, other: &str) -> Self {
153        let mut error = Self::with_token(ErrorKind::Conflict, offset, name.into());
154        error.detail_mut().value = Some(other.to_owned());
155        error
156    }
157
158    /// The required group `group` had none of `members`.
159    pub fn missing_one_of(offset: usize, group: impl Into<String>, members: &[&str]) -> Self {
160        let mut error = Self::with_token(ErrorKind::MissingOneOf, offset, group.into());
161        error.detail_mut().value = Some(members.join(", "));
162        error
163    }
164
165    /// The word at `offset` reached `name`, which only takes words after `--`.
166    pub fn requires_double_dash(offset: usize, name: impl Into<String>) -> Self {
167        Self::with_token(ErrorKind::RequiresDoubleDash, offset, name.into())
168    }
169
170    fn request(
171        kind: ErrorKind,
172        command: &'static crate::help::Command,
173        long: bool,
174        bare: bool,
175    ) -> Self {
176        Self {
177            kind,
178            cut: true,
179            offset: 0,
180            detail: Some(Box::new(Detail {
181                request: Some(Request {
182                    command,
183                    long,
184                    bare,
185                    path: Vec::new(),
186                    root: None,
187                }),
188                ..Detail::default()
189            })),
190        }
191    }
192
193    /// `-h` (`long == false`) or `--help` for `command`.
194    pub fn help(command: &'static crate::help::Command, long: bool) -> Self {
195        Self::request(ErrorKind::HelpRequested, command, long, false)
196    }
197
198    /// A bare invocation of an `arg_required_else_help` command.
199    pub fn bare_help(command: &'static crate::help::Command) -> Self {
200        Self::request(ErrorKind::HelpRequested, command, false, true)
201    }
202
203    /// `-V`/`--version`.
204    pub fn version(command: &'static crate::help::Command) -> Self {
205        Self::request(ErrorKind::VersionRequested, command, false, false)
206    }
207
208    /// Record that this came from within subcommand `name`, for the usage line
209    /// and for what [`Error::suggested`] looks among.
210    #[cold]
211    #[inline(never)]
212    pub fn within(mut self, name: &'static str) -> Self {
213        if let Some(detail) = self.detail.as_deref_mut() {
214            match detail.request.as_mut() {
215                Some(request) => request.path.insert(0, name),
216                None => detail.path.insert(0, name),
217            }
218        }
219        self
220    }
221
222    /// Look among what `root` and the subcommand it failed in take for what
223    /// an unknown flag or word was probably meant to be: [`Error::similar`]
224    /// then has it. A request for a subcommand's help learns the global
225    /// flags of the commands above it, which its page then lists.
226    /// [`Args::parse_words`](crate::Args::parse_words) and what
227    /// is built on it do this; a parse through
228    /// [`Args::parse_argv`](crate::Args::parse_argv) does not.
229    #[cold]
230    #[inline(never)]
231    pub fn suggested(mut self, root: &'static crate::help::Command) -> Self {
232        let Some(detail) = self.detail.as_deref_mut() else {
233            return self;
234        };
235        if let Some(request) = detail.request.as_mut() {
236            request.root = Some(root);
237            return self;
238        }
239        let found = match self.kind {
240            ErrorKind::UnknownFlag => {
241                suggest::flag(root, &detail.path, &detail.token).map(|flag| ("flag", flag))
242            }
243            ErrorKind::UnexpectedArg => suggest::subcommand(root, &detail.path, &detail.token)
244                .map(|name| ("subcommand", name.to_owned())),
245            _ => None,
246        };
247        if let Some(found) = found {
248            detail.similar = Some(found);
249        }
250        self
251    }
252
253    /// What the flag, subcommand or value at fault was probably meant to be,
254    /// when something close to it is accepted there.
255    pub fn similar(&self) -> Option<&str> {
256        let (_, similar) = self.detail.as_deref()?.similar.as_ref()?;
257        Some(similar)
258    }
259
260    /// The help to show for a [`ErrorKind::HelpRequested`], `program` leading
261    /// the usage line; plain, see [`Error::render_help_styled`] for color.
262    pub fn render_help(&self, program: &str) -> Option<String> {
263        self.render_help_styled(program, Style::PLAIN)
264    }
265
266    /// `program version` for a [`ErrorKind::VersionRequested`].
267    pub fn version_text(&self, program: &str) -> Option<String> {
268        let request = self.detail.as_deref()?.request.as_ref()?;
269        let version = request.command.version?;
270        (self.kind == ErrorKind::VersionRequested).then(|| format!("{program} {version}"))
271    }
272
273    /// Help for a bare invocation, which is an error to report rather than an answer.
274    pub fn is_bare_help(&self) -> bool {
275        self.detail
276            .as_deref()
277            .and_then(|d| d.request.as_ref())
278            .is_some_and(|r| r.bare)
279    }
280
281    /// A required subcommand was not given. The offset is the end of the line.
282    pub fn missing_subcommand(offset: usize) -> Self {
283        Self {
284            kind: ErrorKind::MissingSubcommand,
285            cut: true,
286            offset,
287            detail: None,
288        }
289    }
290
291    /// `value` given to `flag` at `offset` was rejected by its type.
292    pub fn invalid_value(
293        offset: usize,
294        flag: impl Into<String>,
295        value: &[u8],
296        cause: impl Into<BoxError>,
297    ) -> Self {
298        let cause = cause.into();
299        let choices = cause.downcast_ref::<crate::value::ChoiceError>();
300        let kind = match choices {
301            Some(_) => ErrorKind::InvalidChoice,
302            None => ErrorKind::InvalidValue,
303        };
304        let value = String::from_utf8_lossy(value).into_owned();
305        let similar = choices
306            .and_then(|error| suggest::nearest(&value, error.choices.iter().copied()))
307            .map(|choice| ("value", choice.to_owned()));
308        let mut error = Self::with_token(kind, offset, flag.into());
309        let detail = error.detail_mut();
310        detail.value = Some(value);
311        detail.cause = Some(cause);
312        detail.similar = similar;
313        error
314    }
315
316    fn detail_mut(&mut self) -> &mut Detail {
317        self.detail.get_or_insert_with(Default::default)
318    }
319
320    /// The class of failure.
321    pub fn kind(&self) -> ErrorKind {
322        self.kind
323    }
324
325    /// Byte offset into the flattened command line.
326    pub fn offset(&self) -> usize {
327        self.offset
328    }
329
330    /// The flag or word the failure is about, if there is one.
331    pub fn token(&self) -> Option<&str> {
332        self.detail.as_deref().map(|d| d.token.as_str())
333    }
334
335    /// The offending value, for [`ErrorKind::UnexpectedValue`] and [`ErrorKind::InvalidValue`].
336    pub fn value(&self) -> Option<&str> {
337        self.detail.as_deref()?.value.as_deref()
338    }
339
340    /// Whether backtracking past this error is forbidden.
341    pub fn is_cut(&self) -> bool {
342        self.cut
343    }
344}
345
346impl PartialEq for Error {
347    /// Equal when they report the same thing at the same place; causes are not compared.
348    fn eq(&self, other: &Self) -> bool {
349        self.kind == other.kind
350            && self.offset == other.offset
351            && self.token() == other.token()
352            && self.value() == other.value()
353    }
354}
355
356impl fmt::Display for Error {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        f.write_str(&self.message(Style::PLAIN))
359    }
360}
361
362impl Error {
363    /// What went wrong, as [`Display`](fmt::Display) says it, with what was
364    /// typed painted in the palette's `invalid` and what is missing in `valid`.
365    pub fn message(&self, style: Style) -> String {
366        let bad = |text: &str| Style::paint(style.invalid(), text);
367        let good = |text: &str| Style::paint(style.valid(), text);
368        let token = self.token().unwrap_or_default();
369        let value = self.value().unwrap_or_default();
370        match self.kind {
371            ErrorKind::Backtrack => "invalid command line".to_owned(),
372            ErrorKind::UnknownFlag => format!("unknown flag `{}`", bad(token)),
373            ErrorKind::UnexpectedArg => format!("unexpected argument `{}`", bad(token)),
374            ErrorKind::MissingValue => format!("`{}` needs a value", good(token)),
375            ErrorKind::UnexpectedValue => format!(
376                "`{}` does not take a value, got `{}`",
377                bad(token),
378                bad(value)
379            ),
380            ErrorKind::MissingRequired => match self.value() {
381                Some(by) => format!("`{}` is required by `{}`", good(token), bad(by)),
382                None => format!("`{}` is required", good(token)),
383            },
384            ErrorKind::Conflict => format!("`{}` cannot be used with `{}`", bad(token), bad(value)),
385            ErrorKind::HelpRequested => "help requested".to_owned(),
386            ErrorKind::VersionRequested => "version requested".to_owned(),
387            ErrorKind::RequiresDoubleDash => {
388                format!("`{}` can only be set after a `--` separator", bad(token))
389            }
390            ErrorKind::MissingOneOf => format!("one of {} is required ({token})", good(value)),
391            ErrorKind::MissingArgument => format!("missing argument `{}`", good(token)),
392            ErrorKind::MissingSubcommand => "a subcommand is required".to_owned(),
393            ErrorKind::InvalidValue | ErrorKind::InvalidChoice => {
394                let mut text = format!("invalid value `{}` for `{token}`", bad(value));
395                if let Some(cause) = self.detail.as_deref().and_then(|d| d.cause.as_ref()) {
396                    text.push_str(": ");
397                    text.push_str(&cause.to_string());
398                }
399                text
400            }
401        }
402    }
403
404    /// `tip: a similar flag exists: …` for an error that has [`Error::similar`],
405    /// painted as what is missing is.
406    pub fn tip(&self, style: Style) -> Option<String> {
407        let (what, similar) = self.detail.as_deref()?.similar.as_ref()?;
408        Some(format!(
409            "{} a similar {what} exists: `{}`",
410            Style::paint(style.valid(), "tip:"),
411            Style::paint(style.valid(), similar),
412        ))
413    }
414
415    /// The whole report for an error that is not help or version:
416    /// `error: …`, a tip when something close was accepted, and a pointer to
417    /// `--help`.
418    pub fn render(&self, style: Style) -> String {
419        let tip = self.tip(style).map(|tip| format!("\n\n  {tip}"));
420        format!(
421            "{} {}{}\n\nFor more information, try '{}'.",
422            Style::paint(style.error(), "error:"),
423            self.message(style),
424            tip.unwrap_or_default(),
425            Style::paint(style.flag(), "--help"),
426        )
427    }
428
429    /// [`Error::render_help`], painted with `style`.
430    pub fn render_help_styled(&self, program: &str, style: Style) -> Option<String> {
431        let request = self.detail.as_deref()?.request.as_ref()?;
432        if self.kind != ErrorKind::HelpRequested {
433            return None;
434        }
435        let mut path = vec![program];
436        path.extend(&request.path);
437        let width = crate::help::width();
438        let inherited = request
439            .root
440            .map(|root| suggest::inherited(root, &request.path, request.command))
441            .unwrap_or_default();
442        Some(crate::help::render_page(
443            request.command,
444            &path,
445            request.long,
446            width,
447            style,
448            &inherited,
449        ))
450    }
451}
452
453impl std::error::Error for Error {
454    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
455        let cause = self.detail.as_deref()?.cause.as_ref()?;
456        Some(&**cause as &(dyn std::error::Error + 'static))
457    }
458}
459
460impl<'i> ParserError<Argv<'i>> for Error {
461    type Inner = Self;
462
463    #[inline]
464    fn from_input(input: &Argv<'i>) -> Self {
465        Self {
466            kind: ErrorKind::Backtrack,
467            cut: false,
468            offset: input.offset(),
469            detail: None,
470        }
471    }
472
473    /// Keep whichever of the two says more: a specific failure beats a bare
474    /// "did not match", and otherwise the later branch wins, as in winnow.
475    #[inline]
476    fn or(self, other: Self) -> Self {
477        if other.kind == ErrorKind::Backtrack && self.kind != ErrorKind::Backtrack {
478            self
479        } else {
480            other
481        }
482    }
483
484    #[inline(always)]
485    fn is_backtrack(&self) -> bool {
486        !self.cut
487    }
488
489    #[inline(always)]
490    fn into_inner(self) -> Result<Self::Inner, Self> {
491        Ok(self)
492    }
493}
494
495impl ModalError for Error {
496    #[inline(always)]
497    fn cut(mut self) -> Self {
498        self.cut = true;
499        self
500    }
501
502    #[inline(always)]
503    fn backtrack(mut self) -> Self {
504        self.cut = false;
505        self
506    }
507}
508
509/// Lets `try_map` report a conversion failure. It backtracks, like any winnow
510/// error; the flag parsers in [`crate::combinator`] convert values themselves
511/// and commit, so their errors can name the flag.
512impl<'i, E> FromExternalError<Argv<'i>, E> for Error
513where
514    E: std::error::Error + Send + Sync + 'static,
515{
516    fn from_external_error(input: &Argv<'i>, e: E) -> Self {
517        let mut error = Self::from_input(input);
518        error.kind = ErrorKind::InvalidValue;
519        error.detail_mut().cause = Some(Box::new(e));
520        error
521    }
522}