Skip to main content

getargs_derive/
error.rs

1/// An option as it was written on the command line, without its leading dashes.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3pub enum Opt<'a> {
4    /// A short option, like `-z` (stored as `'z'`).
5    Short(char),
6    /// A long option, like `--bogus` (stored as `"bogus"`).
7    Long(&'a str),
8}
9
10impl core::fmt::Display for Opt<'_> {
11    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
12        match self {
13            Opt::Short(short) => write!(f, "-{short}"),
14            Opt::Long(long) => write!(f, "--{long}"),
15        }
16    }
17}
18
19/// Errors that can occur when parsing CLI arguments with [`GetArgs`](crate::GetArgs).
20///
21/// The lifetime `'a` is the lifetime of the parsed arguments: variants describing what the
22/// user typed borrow it from the command line instead of copying it, so no allocation is needed.
23///
24/// Names of the struct's own arguments (`&'static str`) are written as in the generated help:
25/// `<INPUT>` for a positional argument, `--output <OUTPUT>` for an option taking a value
26/// (`-o <OUTPUT>` when it has no long name).
27///
28/// The enum is `#[non_exhaustive]`: new variants can be added in a minor release, so a `match` on
29/// it needs a wildcard arm.
30#[must_use]
31#[non_exhaustive]
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Error<'a> {
34    /// A required positional argument or named option was not provided on the command line.
35    ///
36    /// The string is the argument name, e.g. `"<INPUT>"` or `"--output <OUTPUT>"`.
37    MissingArgument(&'static str),
38
39    /// An option that the struct does not define was given, e.g. `--bogus` or `-z`.
40    UnknownOption {
41        /// The unknown option, as written by the user.
42        option: Opt<'a>,
43        /// The closest long option name (without `--`) when an unknown long option looks like a
44        /// typo of a known one, e.g. `Some("verbose")` for `--verbos`.
45        suggestion: Option<&'static str>,
46    },
47
48    /// A positional argument was given but the struct expects no more of them.
49    ///
50    /// The string is the extra argument, as written by the user.
51    UnexpectedArgument(&'a str),
52
53    /// A value was attached to an option that does not take one, e.g. `--verbose=yes` for a flag.
54    ///
55    /// The option is the one written by the user.
56    UnexpectedValue(Opt<'a>),
57
58    /// An option taking a value was given without one, e.g. `--output` at the end of the
59    /// command line.
60    ///
61    /// The string is the option name, e.g. `"--output <OUTPUT>"`.
62    MissingOptionValue(&'static str),
63
64    /// Two arguments that cannot be used together were both given, because of `conflicts_with`
65    /// or because they belong to the same group.
66    ArgumentConflict {
67        /// The argument declaring the conflict (or the first member of the group), e.g.
68        /// `"--quiet"`.
69        name: &'static str,
70        /// The other argument, e.g. `"--verbose"`.
71        other: &'static str,
72    },
73
74    /// An argument was given without another argument it `requires`.
75    MissingRequiredArgument {
76        /// The missing argument, e.g. `"--output <OUTPUT>"`.
77        name: &'static str,
78        /// The given argument that requires it, e.g. `"--compress"`.
79        required_by: &'static str,
80    },
81
82    /// None of a set of arguments was given although one of them is required, because of
83    /// `required_unless_present` or a `required` group.
84    ///
85    /// The slice lists the arguments, e.g. `["--config <CONFIG>", "--stdin"]`.
86    MissingOneOf(&'static [&'static str]),
87
88    /// A required subcommand was not given.
89    ///
90    /// The slice lists the visible subcommands, e.g. `["add", "remove"]`. It is only returned for
91    /// a `#[command(subcommand)]` field that is not an `Option`.
92    MissingSubcommand(&'static [&'static str]),
93
94    /// A positional argument was given where a subcommand was expected, and it is not the name
95    /// or an alias of any subcommand.
96    UnknownSubcommand {
97        /// The unknown subcommand, as written by the user.
98        name: &'a str,
99        /// The closest visible subcommand name or alias when the unknown one looks like a typo of
100        /// it, e.g. `Some("remove")` for `remov`.
101        suggestion: Option<&'static str>,
102    },
103
104    /// The underlying `getargs` parser reported an error that this crate does not recognise.
105    ///
106    /// `getargs` may add error kinds in a compatible release; they are reported as this variant
107    /// instead of panicking. It is never returned with the `getargs` 0.5 releases known to this
108    /// crate.
109    InvalidArguments,
110
111    /// A value could not be parsed into the target field type, e.g. `abc` for a `u32` field.
112    ///
113    /// This is also returned when a `default_value` string does not parse.
114    InvalidValue {
115        /// The argument name, e.g. `"--port <PORT>"` or `"<COUNT>"`.
116        name: &'static str,
117        /// The value that failed to parse.
118        value: &'a str,
119    },
120
121    /// A value is not one of the argument's `possible_values`, e.g. `turbo` for
122    /// `possible_values = ["fast", "slow"]`.
123    ///
124    /// This is also returned when a `default_value` string is not one of them.
125    InvalidPossibleValue {
126        /// The argument name, e.g. `"--mode <MODE>"`.
127        name: &'static str,
128        /// The value that was given.
129        value: &'a str,
130        /// The accepted values, in declaration order, e.g. `["fast", "slow"]`.
131        possible_values: &'static [&'static str],
132    },
133
134    /// A value parsed into the field type but is outside the argument's `value_parser` range,
135    /// e.g. `0` for `value_parser = 1..=64`.
136    ///
137    /// This is also returned when a `default_value` string is outside the range.
138    ValueOutOfRange {
139        /// The argument name, e.g. `"--jobs <JOBS>"`.
140        name: &'static str,
141        /// The value that was given.
142        value: &'a str,
143        /// The range as written in the attribute, e.g. `"1..=64"`.
144        range: &'static str,
145    },
146
147    /// More values were given to a repeated argument than its collection can hold, or than its
148    /// `num_args` range accepts, e.g. a third `-I` for a `Values<&str, 2>` field.
149    ///
150    /// This is also returned when an environment variable or a default splits into too many
151    /// values.
152    TooManyValues {
153        /// The argument name, e.g. `"--include <INCLUDE>"` or `"<FILES>..."`.
154        name: &'static str,
155        /// The first value that did not fit.
156        value: &'a str,
157        /// The maximum number of values.
158        max: usize,
159    },
160
161    /// A positional argument collecting several values received fewer than the minimum of its
162    /// `num_args` range, but at least one. [`Error::MissingArgument`] is returned when it received
163    /// none.
164    TooFewValues {
165        /// The argument name, e.g. `"<FILES>..."`.
166        name: &'static str,
167        /// The minimum number of values.
168        min: usize,
169        /// The number of values given.
170        actual: usize,
171    },
172
173    /// An argument that is not valid UTF-8 was given where text is required, when parsing raw
174    /// bytes with `from_bytes`, `parse_bytes` or `from_args_os`.
175    ///
176    /// It is returned for a value of a `&str` field or of a field parsed from a string, and for an
177    /// argument that matches no field, such as an option name, a subcommand name or an extra
178    /// positional argument, since those are reported as text by the other variants.
179    InvalidUtf8 {
180        /// The argument name, e.g. `"--name <NAME>"`, or `None` for an argument that matches no
181        /// field.
182        name: Option<&'static str>,
183        /// The raw bytes of the argument, displayed with [`Lossy`](crate::Lossy).
184        value: &'a [u8],
185    },
186
187    /// Help was requested with `-h` or `--help`.
188    ///
189    /// This is not a parse failure: the string is the generated help text, the struct's `HELP`
190    /// constant for `-h` and its `LONG_HELP` constant for `--help`. After a subcommand, it is the
191    /// help of that subcommand instead. Applications should print it to standard output and exit
192    /// successfully.
193    /// It is never returned when help is disabled with `#[command(disable_help_flag)]`.
194    Help(&'static str),
195
196    /// The version was requested with `-V` or `--version`.
197    ///
198    /// This is not a parse failure: the string is the version line, the command name followed by
199    /// the version (the struct's `VERSION_TEXT` constant). Applications should print it to standard
200    /// output and exit successfully. It is only returned when `#[command(version)]` is set.
201    Version(&'static str),
202}
203
204impl Error<'_> {
205    /// Returns `true` if this is [`Error::Help`], i.e. the user asked for help rather than
206    /// passing invalid arguments.
207    #[must_use]
208    pub fn is_help(&self) -> bool {
209        matches!(self, Error::Help(_))
210    }
211
212    /// Returns `true` if this is [`Error::Version`], i.e. the user asked for the version rather
213    /// than passing invalid arguments.
214    #[must_use]
215    pub fn is_version(&self) -> bool {
216        matches!(self, Error::Version(_))
217    }
218}
219
220impl core::fmt::Display for Error<'_> {
221    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
222        match self {
223            Error::MissingArgument(name) => {
224                write!(
225                    f,
226                    "the following required argument was not provided: {name}"
227                )
228            }
229            Error::UnknownOption { option, suggestion } => {
230                write!(f, "unexpected argument '{option}' found")?;
231                match suggestion {
232                    Some(suggestion) => {
233                        write!(f, "\n\n  tip: a similar argument exists: '--{suggestion}'")
234                    }
235                    None => Ok(()),
236                }
237            }
238            Error::UnexpectedArgument(argument) => {
239                write!(f, "unexpected argument '{argument}' found")
240            }
241            Error::UnexpectedValue(option) => {
242                write!(
243                    f,
244                    "unexpected value for '{option}': it does not take a value"
245                )
246            }
247            Error::ArgumentConflict { name, other } => {
248                write!(f, "the argument '{name}' cannot be used with '{other}'")
249            }
250            Error::MissingRequiredArgument { name, required_by } => {
251                write!(
252                    f,
253                    "the following required argument was not provided: {name} (required by '{required_by}')"
254                )
255            }
256            Error::MissingOneOf(names) => {
257                f.write_str("the following required arguments were not provided: <")?;
258                for (index, name) in names.iter().enumerate() {
259                    if index > 0 {
260                        f.write_str("|")?;
261                    }
262                    f.write_str(name)?;
263                }
264                f.write_str(">")
265            }
266            Error::MissingSubcommand(names) => {
267                f.write_str("a subcommand is required but one was not provided\n  [subcommands: ")?;
268                for (index, name) in names.iter().enumerate() {
269                    if index > 0 {
270                        f.write_str(", ")?;
271                    }
272                    f.write_str(name)?;
273                }
274                f.write_str("]")
275            }
276            Error::UnknownSubcommand { name, suggestion } => {
277                write!(f, "unrecognized subcommand '{name}'")?;
278                match suggestion {
279                    Some(suggestion) => {
280                        write!(f, "\n\n  tip: a similar subcommand exists: '{suggestion}'")
281                    }
282                    None => Ok(()),
283                }
284            }
285            Error::InvalidArguments => f.write_str("the arguments could not be parsed"),
286            Error::MissingOptionValue(name) => {
287                write!(f, "a value is required for '{name}' but none was supplied")
288            }
289            Error::InvalidValue { name, value } => {
290                write!(f, "invalid value '{value}' for '{name}'")
291            }
292            Error::InvalidPossibleValue {
293                name,
294                value,
295                possible_values,
296            } => {
297                write!(
298                    f,
299                    "invalid value '{value}' for '{name}'\n  [possible values: "
300                )?;
301                for (index, possible) in possible_values.iter().enumerate() {
302                    if index > 0 {
303                        f.write_str(", ")?;
304                    }
305                    if possible.is_empty() || possible.contains(char::is_whitespace) {
306                        write!(f, "{possible:?}")?;
307                    } else {
308                        f.write_str(possible)?;
309                    }
310                }
311                f.write_str("]")
312            }
313            Error::ValueOutOfRange { name, value, range } => {
314                write!(
315                    f,
316                    "invalid value '{value}' for '{name}': {value} is not in {range}"
317                )
318            }
319            Error::TooManyValues { name, value, .. } => {
320                write!(
321                    f,
322                    "unexpected value '{value}' for '{name}' found; no more were expected"
323                )
324            }
325            Error::TooFewValues { name, min, actual } => {
326                let verb = if *actual == 1 { "was" } else { "were" };
327                write!(
328                    f,
329                    "{min} values required by '{name}'; only {actual} {verb} provided"
330                )
331            }
332            Error::InvalidUtf8 {
333                name: Some(name),
334                value,
335            } => {
336                write!(
337                    f,
338                    "invalid UTF-8 in value '{}' for '{name}'",
339                    crate::Lossy(value)
340                )
341            }
342            Error::InvalidUtf8 { name: None, value } => {
343                write!(f, "invalid UTF-8 in argument '{}'", crate::Lossy(value))
344            }
345            Error::Help(help) => f.write_str(help),
346            Error::Version(version) => f.write_str(version),
347        }
348    }
349}
350
351impl core::error::Error for Error<'_> {}