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