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