winnow-args 0.2.1

Command line argument parsing built from winnow parsers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
//! What went wrong, and where.
//!
//! [`Error`] is the error type of every parser in this crate. It implements
//! winnow's [`ParserError`] and [`ModalError`] itself rather than living inside
//! an `ErrMode`: `alt` asks [`ParserError::is_backtrack`], so one flag is all
//! the modality a command line needs, and the common case — a branch of an
//! `alt` that did not match — builds a 24-byte value without allocating.
//!
//! An error that reaches the user is printed by [`Error::render`]: the
//! message, then a tip when a flag, a subcommand or a choice close to what
//! was typed is accepted there ([`Error::similar`]). Only a failed parse
//! looks for one.

use std::fmt;

use winnow::error::{FromExternalError, ModalError, ParserError};

use crate::help::Style;
use crate::stream::Argv;

mod suggest;

/// Boxed cause of an [`ErrorKind::InvalidValue`].
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;

/// The class of failure.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
    /// A parser did not match here; another one may.
    Backtrack,
    /// A flag-like word matched no flag.
    UnknownFlag,
    /// A word nothing could hold.
    UnexpectedArg,
    /// A flag needing a value did not get one.
    MissingValue,
    /// A flag that takes no value was given one: `--verbose=yes`.
    UnexpectedValue,
    /// A required flag never appeared.
    MissingRequired,
    /// A required positional argument was never filled.
    MissingArgument,
    /// A required subcommand was not given.
    MissingSubcommand,
    /// Two arguments that exclude each other were both given.
    Conflict,
    /// A required group had none of its members.
    MissingOneOf,
    /// A word reached an argument that only takes words after `--`.
    RequiresDoubleDash,
    /// `-h`/`--help`, or a bare invocation of an `arg_required_else_help`
    /// command: show [`Error::render_help`] rather than report a failure.
    HelpRequested,
    /// `-V`/`--version`: show [`Error::version_text`].
    VersionRequested,
    /// A value that its type rejected.
    InvalidValue,
    /// A value outside a fixed set.
    InvalidChoice,
}

/// What `--help` or `--version` asked for, and where.
#[derive(Debug)]
struct Request {
    command: &'static crate::help::Command,
    long: bool,
    /// A bare invocation rather than a flag: help belongs on stderr.
    bare: bool,
    /// Subcommand names from the root down to `command`.
    path: Vec<&'static str>,
}

#[derive(Debug, Default)]
struct Detail {
    request: Option<Request>,
    /// The flag as spelled on the command line (or its display name), or the word.
    token: String,
    value: Option<String>,
    cause: Option<BoxError>,
    /// Subcommand names from the root down to where it failed.
    path: Vec<&'static str>,
    /// What was probably meant, and what it is: "flag".
    similar: Option<(&'static str, String)>,
}

/// A parse failure.
#[derive(Debug)]
pub struct Error {
    kind: ErrorKind,
    cut: bool,
    offset: usize,
    detail: Option<Box<Detail>>,
}

impl Error {
    fn with_token(kind: ErrorKind, offset: usize, token: String) -> Self {
        Self {
            kind,
            cut: true,
            offset,
            detail: Some(Box::new(Detail {
                token,
                ..Detail::default()
            })),
        }
    }

    /// A flag-like word, at `offset`, that matched nothing.
    pub fn unknown_flag(offset: usize, token: impl Into<String>) -> Self {
        Self::with_token(ErrorKind::UnknownFlag, offset, token.into())
    }

    /// A word, at `offset`, that nothing could hold.
    pub fn unexpected_arg(offset: usize, token: impl Into<String>) -> Self {
        Self::with_token(ErrorKind::UnexpectedArg, offset, token.into())
    }

    /// `flag`, at `offset`, needed a value and did not get one.
    pub fn missing_value(offset: usize, flag: impl Into<String>) -> Self {
        Self::with_token(ErrorKind::MissingValue, offset, flag.into())
    }

    /// `flag`, at `offset`, takes no value but was given `value`.
    pub fn unexpected_value(offset: usize, flag: impl Into<String>, value: &[u8]) -> Self {
        let mut error = Self::with_token(ErrorKind::UnexpectedValue, offset, flag.into());
        error.detail_mut().value = Some(String::from_utf8_lossy(value).into_owned());
        error
    }

    /// The required `flag` never appeared. The offset is the end of the line.
    pub fn missing_required(offset: usize, flag: impl Into<String>) -> Self {
        Self::with_token(ErrorKind::MissingRequired, offset, flag.into())
    }

    /// The required positional `name` was never filled. The offset is the end of the line.
    pub fn missing_argument(offset: usize, name: impl Into<String>) -> Self {
        Self::with_token(ErrorKind::MissingArgument, offset, name.into())
    }

    /// `name` requires `target`, which has no value. The offset is the end of the line.
    pub fn required_by(offset: usize, target: impl Into<String>, name: &str) -> Self {
        let mut error = Self::with_token(ErrorKind::MissingRequired, offset, target.into());
        error.detail_mut().value = Some(name.to_owned());
        error
    }

    /// `name` and `other` exclude each other and were both given.
    pub fn conflict(offset: usize, name: impl Into<String>, other: &str) -> Self {
        let mut error = Self::with_token(ErrorKind::Conflict, offset, name.into());
        error.detail_mut().value = Some(other.to_owned());
        error
    }

    /// The required group `group` had none of `members`.
    pub fn missing_one_of(offset: usize, group: impl Into<String>, members: &[&str]) -> Self {
        let mut error = Self::with_token(ErrorKind::MissingOneOf, offset, group.into());
        error.detail_mut().value = Some(members.join(", "));
        error
    }

    /// The word at `offset` reached `name`, which only takes words after `--`.
    pub fn requires_double_dash(offset: usize, name: impl Into<String>) -> Self {
        Self::with_token(ErrorKind::RequiresDoubleDash, offset, name.into())
    }

    fn request(
        kind: ErrorKind,
        command: &'static crate::help::Command,
        long: bool,
        bare: bool,
    ) -> Self {
        Self {
            kind,
            cut: true,
            offset: 0,
            detail: Some(Box::new(Detail {
                request: Some(Request {
                    command,
                    long,
                    bare,
                    path: Vec::new(),
                }),
                ..Detail::default()
            })),
        }
    }

    /// `-h` (`long == false`) or `--help` for `command`.
    pub fn help(command: &'static crate::help::Command, long: bool) -> Self {
        Self::request(ErrorKind::HelpRequested, command, long, false)
    }

    /// A bare invocation of an `arg_required_else_help` command.
    pub fn bare_help(command: &'static crate::help::Command) -> Self {
        Self::request(ErrorKind::HelpRequested, command, false, true)
    }

    /// `-V`/`--version`.
    pub fn version(command: &'static crate::help::Command) -> Self {
        Self::request(ErrorKind::VersionRequested, command, false, false)
    }

    /// Record that this came from within subcommand `name`, for the usage line
    /// and for what [`Error::suggested`] looks among.
    #[cold]
    #[inline(never)]
    pub fn within(mut self, name: &'static str) -> Self {
        if let Some(detail) = self.detail.as_deref_mut() {
            match detail.request.as_mut() {
                Some(request) => request.path.insert(0, name),
                None => detail.path.insert(0, name),
            }
        }
        self
    }

    /// Look among what `root` and the subcommand it failed in take for what
    /// an unknown flag or word was probably meant to be: [`Error::similar`]
    /// then has it. [`Args::parse_words`](crate::Args::parse_words) and what
    /// is built on it do this; a parse through
    /// [`Args::parse_argv`](crate::Args::parse_argv) does not.
    #[cold]
    #[inline(never)]
    pub fn suggested(mut self, root: &'static crate::help::Command) -> Self {
        let Some(detail) = self.detail.as_deref_mut() else {
            return self;
        };
        let found = match self.kind {
            ErrorKind::UnknownFlag => {
                suggest::flag(root, &detail.path, &detail.token).map(|flag| ("flag", flag))
            }
            ErrorKind::UnexpectedArg => suggest::subcommand(root, &detail.path, &detail.token)
                .map(|name| ("subcommand", name.to_owned())),
            _ => None,
        };
        if let Some(found) = found {
            detail.similar = Some(found);
        }
        self
    }

    /// What the flag, subcommand or value at fault was probably meant to be,
    /// when something close to it is accepted there.
    pub fn similar(&self) -> Option<&str> {
        let (_, similar) = self.detail.as_deref()?.similar.as_ref()?;
        Some(similar)
    }

    /// The help to show for a [`ErrorKind::HelpRequested`], `program` leading
    /// the usage line; plain, see [`Error::render_help_styled`] for color.
    pub fn render_help(&self, program: &str) -> Option<String> {
        self.render_help_styled(program, Style::PLAIN)
    }

    /// `program version` for a [`ErrorKind::VersionRequested`].
    pub fn version_text(&self, program: &str) -> Option<String> {
        let request = self.detail.as_deref()?.request.as_ref()?;
        let version = request.command.version?;
        (self.kind == ErrorKind::VersionRequested).then(|| format!("{program} {version}"))
    }

    /// Help for a bare invocation, which is an error to report rather than an answer.
    pub fn is_bare_help(&self) -> bool {
        self.detail
            .as_deref()
            .and_then(|d| d.request.as_ref())
            .is_some_and(|r| r.bare)
    }

    /// A required subcommand was not given. The offset is the end of the line.
    pub fn missing_subcommand(offset: usize) -> Self {
        Self {
            kind: ErrorKind::MissingSubcommand,
            cut: true,
            offset,
            detail: None,
        }
    }

    /// `value` given to `flag` at `offset` was rejected by its type.
    pub fn invalid_value(
        offset: usize,
        flag: impl Into<String>,
        value: &[u8],
        cause: impl Into<BoxError>,
    ) -> Self {
        let cause = cause.into();
        let choices = cause.downcast_ref::<crate::value::ChoiceError>();
        let kind = match choices {
            Some(_) => ErrorKind::InvalidChoice,
            None => ErrorKind::InvalidValue,
        };
        let value = String::from_utf8_lossy(value).into_owned();
        let similar = choices
            .and_then(|error| suggest::nearest(&value, error.choices.iter().copied()))
            .map(|choice| ("value", choice.to_owned()));
        let mut error = Self::with_token(kind, offset, flag.into());
        let detail = error.detail_mut();
        detail.value = Some(value);
        detail.cause = Some(cause);
        detail.similar = similar;
        error
    }

    fn detail_mut(&mut self) -> &mut Detail {
        self.detail.get_or_insert_with(Default::default)
    }

    /// The class of failure.
    pub fn kind(&self) -> ErrorKind {
        self.kind
    }

    /// Byte offset into the flattened command line.
    pub fn offset(&self) -> usize {
        self.offset
    }

    /// The flag or word the failure is about, if there is one.
    pub fn token(&self) -> Option<&str> {
        self.detail.as_deref().map(|d| d.token.as_str())
    }

    /// The offending value, for [`ErrorKind::UnexpectedValue`] and [`ErrorKind::InvalidValue`].
    pub fn value(&self) -> Option<&str> {
        self.detail.as_deref()?.value.as_deref()
    }

    /// Whether backtracking past this error is forbidden.
    pub fn is_cut(&self) -> bool {
        self.cut
    }
}

impl PartialEq for Error {
    /// Equal when they report the same thing at the same place; causes are not compared.
    fn eq(&self, other: &Self) -> bool {
        self.kind == other.kind
            && self.offset == other.offset
            && self.token() == other.token()
            && self.value() == other.value()
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message(Style::PLAIN))
    }
}

impl Error {
    /// What went wrong, as [`Display`](fmt::Display) says it, with what was
    /// typed painted in the palette's `invalid` and what is missing in `valid`.
    pub fn message(&self, style: Style) -> String {
        let bad = |text: &str| Style::paint(style.invalid(), text);
        let good = |text: &str| Style::paint(style.valid(), text);
        let token = self.token().unwrap_or_default();
        let value = self.value().unwrap_or_default();
        match self.kind {
            ErrorKind::Backtrack => "invalid command line".to_owned(),
            ErrorKind::UnknownFlag => format!("unknown flag `{}`", bad(token)),
            ErrorKind::UnexpectedArg => format!("unexpected argument `{}`", bad(token)),
            ErrorKind::MissingValue => format!("`{}` needs a value", good(token)),
            ErrorKind::UnexpectedValue => format!(
                "`{}` does not take a value, got `{}`",
                bad(token),
                bad(value)
            ),
            ErrorKind::MissingRequired => match self.value() {
                Some(by) => format!("`{}` is required by `{}`", good(token), bad(by)),
                None => format!("`{}` is required", good(token)),
            },
            ErrorKind::Conflict => format!("`{}` cannot be used with `{}`", bad(token), bad(value)),
            ErrorKind::HelpRequested => "help requested".to_owned(),
            ErrorKind::VersionRequested => "version requested".to_owned(),
            ErrorKind::RequiresDoubleDash => {
                format!("`{}` can only be set after a `--` separator", bad(token))
            }
            ErrorKind::MissingOneOf => format!("one of {} is required ({token})", good(value)),
            ErrorKind::MissingArgument => format!("missing argument `{}`", good(token)),
            ErrorKind::MissingSubcommand => "a subcommand is required".to_owned(),
            ErrorKind::InvalidValue | ErrorKind::InvalidChoice => {
                let mut text = format!("invalid value `{}` for `{token}`", bad(value));
                if let Some(cause) = self.detail.as_deref().and_then(|d| d.cause.as_ref()) {
                    text.push_str(": ");
                    text.push_str(&cause.to_string());
                }
                text
            }
        }
    }

    /// `tip: a similar flag exists: …` for an error that has [`Error::similar`],
    /// painted as what is missing is.
    pub fn tip(&self, style: Style) -> Option<String> {
        let (what, similar) = self.detail.as_deref()?.similar.as_ref()?;
        Some(format!(
            "{} a similar {what} exists: `{}`",
            Style::paint(style.valid(), "tip:"),
            Style::paint(style.valid(), similar),
        ))
    }

    /// The whole report for an error that is not help or version:
    /// `error: …`, a tip when something close was accepted, and a pointer to
    /// `--help`.
    pub fn render(&self, style: Style) -> String {
        let tip = self.tip(style).map(|tip| format!("\n\n  {tip}"));
        format!(
            "{} {}{}\n\nFor more information, try '{}'.",
            Style::paint(style.error(), "error:"),
            self.message(style),
            tip.unwrap_or_default(),
            Style::paint(style.flag(), "--help"),
        )
    }

    /// [`Error::render_help`], painted with `style`.
    pub fn render_help_styled(&self, program: &str, style: Style) -> Option<String> {
        let request = self.detail.as_deref()?.request.as_ref()?;
        if self.kind != ErrorKind::HelpRequested {
            return None;
        }
        let mut path = vec![program];
        path.extend(&request.path);
        let width = crate::help::width();
        Some(crate::help::render_styled(
            request.command,
            &path,
            request.long,
            width,
            style,
        ))
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        let cause = self.detail.as_deref()?.cause.as_ref()?;
        Some(&**cause as &(dyn std::error::Error + 'static))
    }
}

impl<'i> ParserError<Argv<'i>> for Error {
    type Inner = Self;

    #[inline]
    fn from_input(input: &Argv<'i>) -> Self {
        Self {
            kind: ErrorKind::Backtrack,
            cut: false,
            offset: input.offset(),
            detail: None,
        }
    }

    /// Keep whichever of the two says more: a specific failure beats a bare
    /// "did not match", and otherwise the later branch wins, as in winnow.
    #[inline]
    fn or(self, other: Self) -> Self {
        if other.kind == ErrorKind::Backtrack && self.kind != ErrorKind::Backtrack {
            self
        } else {
            other
        }
    }

    #[inline(always)]
    fn is_backtrack(&self) -> bool {
        !self.cut
    }

    #[inline(always)]
    fn into_inner(self) -> Result<Self::Inner, Self> {
        Ok(self)
    }
}

impl ModalError for Error {
    #[inline(always)]
    fn cut(mut self) -> Self {
        self.cut = true;
        self
    }

    #[inline(always)]
    fn backtrack(mut self) -> Self {
        self.cut = false;
        self
    }
}

/// Lets `try_map` report a conversion failure. It backtracks, like any winnow
/// error; the flag parsers in [`crate::combinator`] convert values themselves
/// and commit, so their errors can name the flag.
impl<'i, E> FromExternalError<Argv<'i>, E> for Error
where
    E: std::error::Error + Send + Sync + 'static,
{
    fn from_external_error(input: &Argv<'i>, e: E) -> Self {
        let mut error = Self::from_input(input);
        error.kind = ErrorKind::InvalidValue;
        error.detail_mut().cause = Some(Box::new(e));
        error
    }
}