argp/
error.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// SPDX-FileCopyrightText: 2023 Jakub Jirutka <jakub@jirutka.cz>
3
4use std::ffi::OsString;
5use std::fmt::{self, Write as _};
6
7/// The error type for the argp parser.
8#[derive(Debug, PartialEq)]
9pub enum Error {
10    /// Duplicate value for a non-repeating option. The contained `String` is
11    /// the option name (e.g. `--foo`).
12    DuplicateOption(String),
13
14    /// No value provided for the specified option.
15    MissingArgValue(String),
16
17    /// Missing required positional argument(s), option(s) or subcommand(s).
18    MissingRequirements(MissingRequirements),
19
20    /// Trailing options after the `help` subcommand.
21    OptionsAfterHelp,
22
23    /// Error parsing the given value for the positional argument or option.
24    ParseArgument {
25        /// The positional argument or option.
26        arg: String,
27        /// The given value that failed to be parsed.
28        value: OsString,
29        /// The error message from the value parser.
30        msg: String,
31    },
32
33    /// Unknown argument.
34    UnknownArgument(OsString),
35
36    /// Any other error.
37    Other(String),
38}
39
40impl Error {
41    /// A convenient method for creating [`Error::Other`].
42    #[inline]
43    pub fn other<S: ToString>(msg: S) -> Self {
44        Self::Other(msg.to_string())
45    }
46}
47
48impl std::error::Error for Error {}
49
50impl fmt::Display for Error {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        use Error::*;
53
54        match &self {
55            DuplicateOption(arg) => write!(f, "Option '{}' can only be used once.", arg),
56            MissingArgValue(arg) => write!(f, "Option '{}' requires a value.", arg),
57            MissingRequirements(req) => req.fmt(f),
58            OptionsAfterHelp => {
59                write!(f, "Trailing options are not allowed after 'help' subcommand.")
60            }
61            ParseArgument { arg, value, msg } => {
62                let subj = if arg.starts_with('-') {
63                    "option"
64                } else {
65                    "argument"
66                };
67                write!(f, "Error parsing {} '{}' with value '{:?}': {}.", subj, arg, value, msg)
68            }
69            UnknownArgument(arg) => write!(f, "Unrecognized argument: {}", arg.to_string_lossy()),
70            Other(msg) => msg.fmt(f),
71        }
72    }
73}
74
75/// An error string builder to report missing required options and subcommands.
76#[doc(hidden)]
77#[derive(Debug, Default, PartialEq)]
78pub struct MissingRequirements {
79    options: Vec<&'static str>,
80    subcommands: Option<Vec<&'static str>>,
81    positional_args: Vec<&'static str>,
82}
83
84impl MissingRequirements {
85    /// Adds a missing required option.
86    #[doc(hidden)]
87    pub fn missing_option(&mut self, name: &'static str) {
88        self.options.push(name)
89    }
90
91    /// Adds a missing required subcommand.
92    #[doc(hidden)]
93    pub fn missing_subcommands(&mut self, commands: impl Iterator<Item = &'static str>) {
94        self.subcommands = Some(commands.collect());
95    }
96
97    /// Adds a missing positional argument.
98    #[doc(hidden)]
99    pub fn missing_positional_arg(&mut self, name: &'static str) {
100        self.positional_args.push(name)
101    }
102
103    /// If any missing options or subcommands were provided, returns an error
104    /// string describing the missing args.
105    #[doc(hidden)]
106    pub fn err_on_any(self) -> Result<(), Error> {
107        if self.options.is_empty() && self.subcommands.is_none() && self.positional_args.is_empty()
108        {
109            Ok(())
110        } else {
111            Err(Error::MissingRequirements(self))
112        }
113    }
114}
115
116const NEWLINE_INDENT: &str = "\n    ";
117
118impl fmt::Display for MissingRequirements {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        if !self.positional_args.is_empty() {
121            f.write_str("Required positional arguments not provided:")?;
122            for arg in &self.positional_args {
123                f.write_str(NEWLINE_INDENT)?;
124                f.write_str(arg)?;
125            }
126        }
127
128        if !self.options.is_empty() {
129            if !self.positional_args.is_empty() {
130                f.write_char('\n')?;
131            }
132            f.write_str("Required options not provided:")?;
133            for option in &self.options {
134                f.write_str(NEWLINE_INDENT)?;
135                f.write_str(option)?;
136            }
137        }
138
139        if let Some(missing_subcommands) = &self.subcommands {
140            if !self.options.is_empty() {
141                f.write_char('\n')?;
142            }
143            f.write_str("One of the following subcommands must be present:")?;
144            f.write_str(NEWLINE_INDENT)?;
145            f.write_str("help")?;
146            for subcommand in missing_subcommands {
147                f.write_str(NEWLINE_INDENT)?;
148                f.write_str(subcommand)?;
149            }
150        }
151
152        f.write_char('\n')
153    }
154}