larpa/
error.rs

1use std::{
2    ffi::OsString,
3    fmt,
4    io::{self, IsTerminal, Write, sink, stderr, stdout},
5    process,
6    str::Utf8Error,
7};
8
9use crate::{
10    Context,
11    desc::{ArgumentDesc, ArgumentName},
12    types::Color,
13    writer::{BOLD, DEFAULT_LINE_WIDTH, ITALIC, RED, RESET, Writer, YELLOW},
14};
15
16/// Indicates that a [`Command`] could not be parsed.
17///
18/// This error is returned from the methods in [`Command`] when the implementing type could not be
19/// parsed from the command-line arguments.
20/// This can happen when the user passes `--help` or `--version` (requesting a help or version
21/// output instead of normal program operation), or when the provided arguments are invalid.
22///
23/// [`Command`]: crate::Command
24#[derive(Debug)]
25pub struct Error {
26    pub(crate) kind: ErrorKind,
27    pub(crate) context: Context,
28}
29
30impl Error {
31    /// Reports the error to stdout or stderr, and exits the process with an appropriate status.
32    pub fn report_and_exit(&self) -> ! {
33        let mut reporter = self.reporter();
34        if self.is_fatal() {
35            reporter = reporter.output(stderr());
36        } else {
37            reporter = reporter.output(stdout());
38        };
39        reporter.report_and_exit()
40    }
41
42    /// Returns a [`Reporter`] that can write this error to a terminal and allows customization.
43    pub fn reporter(&self) -> Reporter<'_> {
44        Reporter {
45            error: self,
46            color: self.context.color(),
47            wrap_width: DEFAULT_LINE_WIDTH,
48            writer: Writer::io(sink()),
49        }
50    }
51
52    fn report_impl(&self, f: &mut Writer) -> io::Result<()> {
53        match &self.kind {
54            ErrorKind::VersionRequested => {
55                let fmt = self.context.root_desc.0.0.version_formatter;
56                let version = fmt(&self.context);
57                write!(f, "{version}")?;
58                if !version.ends_with('\n') {
59                    writeln!(f)?;
60                }
61            }
62            ErrorKind::HelpRequested => {
63                self.context
64                    .desc
65                    .help()
66                    .with_invocation(&self.context.command_chain)
67                    .write_to(f)?;
68            }
69            _ => {
70                write!(f, "{RED}error{RESET}: ")?;
71                f.set_indentation("error: ".len());
72                self.write_error(f)?;
73                write!(f, "\n\n")?;
74                let mut usage = self
75                    .context
76                    .desc
77                    .usage()
78                    .with_invocation(&self.context.command_chain)
79                    .with_prefix(format!("{BOLD}usage{RESET}: "));
80                if let Some(arg) = self.kind.arg_index() {
81                    usage = usage.highlight_arg(arg);
82                }
83                if self.kind.related_to_subcommand() {
84                    usage = usage.highlight_subcommand();
85                }
86                usage.write_to(f)?;
87                writeln!(f)?;
88            }
89        }
90
91        f.flush()
92    }
93
94    fn arg(&self, index: usize) -> &ArgumentDesc {
95        &self.context.desc.args()[index]
96    }
97
98    fn argname(&self, index: usize) -> impl fmt::Display {
99        struct Disp<'a>(&'a ArgumentName);
100        impl fmt::Display for Disp<'_> {
101            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102                write!(f, "`{YELLOW}{ITALIC}{}{RESET}`", self.0)
103            }
104        }
105        Disp(self.arg(index).name())
106    }
107
108    fn write_error(&self, f: &mut Writer) -> io::Result<()> {
109        match &self.kind {
110            ErrorKind::HelpRequested => write!(f, "help requested"),
111            ErrorKind::VersionRequested => write!(f, "version requested"),
112
113            ErrorKind::Utf8Error(utf8_error) => write!(f, "{utf8_error}"),
114            ErrorKind::ValueParseError { error, arg } => write!(
115                f,
116                "invalid value for argument {}: {error}",
117                self.argname(*arg)
118            ),
119            ErrorKind::UnexpectedArgValue(arg) => {
120                write!(f, "flag {} does not take a value", self.argname(*arg))
121            }
122            ErrorKind::UnexpectedArg { arg } => {
123                write!(f, "unexpected {arg}")?;
124                Ok(())
125            }
126            ErrorKind::DuplicateArg(arg) => {
127                write!(f, "duplicate argument {}", self.argname(*arg))
128            }
129            ErrorKind::MissingArg(arg) => {
130                write!(f, "missing argument {}", self.argname(*arg))
131            }
132            ErrorKind::MissingArgValue(arg) => {
133                write!(f, "argument {} requires a value", self.argname(*arg))
134            }
135            ErrorKind::MissingSubcommand => write!(f, "a subcommand is required"),
136            ErrorKind::UnknownSubcommand(cmd) => {
137                write!(f, "unknown subcommand `{}`", cmd.display())
138            }
139        }
140    }
141
142    fn is_fatal(&self) -> bool {
143        match &self.kind {
144            ErrorKind::HelpRequested | ErrorKind::VersionRequested => false,
145            _ => true,
146        }
147    }
148}
149
150/// This is the internal error type used throughout this library and by the macro-generated code.
151///
152/// It is only converted to a user-facing [`Error`] at the boundary, where [`Error`] can be enhanced
153/// with command-specific context.
154#[derive(Debug)]
155pub enum ErrorKind {
156    HelpRequested,
157    VersionRequested,
158
159    Utf8Error(Utf8Error),
160    /// Failed to parse an argument value.
161    ///
162    /// The boxed error can be [`Utf8Error`], when the type expects a valid UTF-8 input, or any
163    /// type-specific parsing error, such as [`ParseIntError`][std::num::ParseIntError].
164    ///
165    /// The [`ErrorKind::Utf8Error`] variant indicates that invalid UTF-8 was provided as part of
166    /// the *name* of an argument instead its value.
167    ValueParseError {
168        arg: usize,
169        error: Box<dyn std::error::Error>,
170    },
171    /// Argument doesn't take a value, but `--arg=...` was passed.
172    UnexpectedArgValue(usize),
173    UnexpectedArg {
174        arg: String,
175    },
176    /// A non-repeating argument was provided multiple times. The [`usize`] is the argument index in
177    /// the `CommandDesc`.
178    DuplicateArg(usize),
179    /// A required argument was not provided. The [`usize`] is the argument index in the
180    /// `CommandDesc`.
181    MissingArg(usize),
182    MissingArgValue(usize),
183    /// A subcommand is required, but none was provided.
184    MissingSubcommand,
185    /// A subcommand was provided, and a subcommand is accepted by this command, but the name
186    /// doesn't match any of the supported subcommands.
187    UnknownSubcommand(OsString),
188}
189
190impl ErrorKind {
191    fn arg_index(&self) -> Option<usize> {
192        match self {
193            ErrorKind::MissingArg(arg)
194            | ErrorKind::MissingArgValue(arg)
195            | ErrorKind::DuplicateArg(arg)
196            | ErrorKind::UnexpectedArgValue(arg)
197            | ErrorKind::ValueParseError { arg, .. } => Some(*arg),
198
199            ErrorKind::HelpRequested
200            | ErrorKind::VersionRequested
201            | ErrorKind::Utf8Error(..)
202            | ErrorKind::UnexpectedArg { .. }
203            | ErrorKind::MissingSubcommand
204            | ErrorKind::UnknownSubcommand(..) => None,
205        }
206    }
207
208    fn related_to_subcommand(&self) -> bool {
209        match self {
210            ErrorKind::MissingSubcommand | ErrorKind::UnknownSubcommand(..) => true,
211
212            ErrorKind::MissingArg(..)
213            | ErrorKind::MissingArgValue(..)
214            | ErrorKind::DuplicateArg(..)
215            | ErrorKind::HelpRequested
216            | ErrorKind::VersionRequested
217            | ErrorKind::Utf8Error(..)
218            | ErrorKind::ValueParseError { .. }
219            | ErrorKind::UnexpectedArgValue { .. }
220            | ErrorKind::UnexpectedArg { .. } => false,
221        }
222    }
223}
224
225impl fmt::Display for Error {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        self.report_impl(&mut Writer::display(f))
228            .map_err(|_| fmt::Error)
229    }
230}
231impl std::error::Error for Error {}
232
233/// Success status code.
234const EX_OK: i32 = 0;
235
236/// This is the exit code we return with when parsing the command line fails.
237///
238/// It is taken from the Linux/BSD/OS X `sysexits.h` and described as
239///
240/// > The command was used incorrectly, e.g., with the wrong number of arguments,
241/// > a bad flag, bad syntax in a parameter, or whatever.
242const EX_USAGE: i32 = 64;
243
244/// Writes an [`Error`] to a destination.
245///
246/// Returned by [`Error::reporter`].
247pub struct Reporter<'a> {
248    error: &'a Error,
249    color: Color,
250    wrap_width: usize,
251    writer: Writer<'a>,
252}
253
254impl<'a> Reporter<'a> {
255    /// Configures whether to use ANSI colors.
256    ///
257    /// By default (if this function isn't called), [`Reporter`] will respect any
258    /// a `--color=<always|auto|never>` argument that was encountered on the command line
259    /// (this mechanism requires the [`Command`][crate::Command] implementor to have an argument
260    /// of type [`Color`]).
261    ///
262    /// If no `--color` argument was passed, and this method *isn't* called, the default is
263    /// [`Color::Auto`].
264    pub fn color(self, color: Color) -> Self {
265        Self { color, ..self }
266    }
267
268    /// Sets the maximum line width.
269    ///
270    /// Lines will be soft-wrapped if they exceed this width.
271    ///
272    /// By default, lines will get wrapped after an unspecified, but typically reasonable width.
273    pub fn wrap_width(self, width: usize) -> Self {
274        Self {
275            wrap_width: width,
276            ..self
277        }
278    }
279
280    /// Writes output to the given destination.
281    ///
282    /// The provided type has to implement [`IsTerminal`] for automatic colorization to work.
283    /// To output to something that does not implement [`IsTerminal`], use [`Reporter::raw_output`].
284    pub fn output<W: io::Write + IsTerminal + 'a>(self, w: W) -> Self {
285        Self {
286            writer: Writer::fd(w),
287            ..self
288        }
289    }
290
291    /// Writes the error to an [`io::Write`] implementor.
292    ///
293    /// This will not use ANSI colors unless colors are forcibly enabled by passing `--color=always`
294    /// or calling [`Reporter::color`] with [`Color::Always`].
295    pub fn raw_output<W: io::Write + 'a>(self, w: W) -> Self {
296        Self {
297            writer: Writer::io(w),
298            ..self
299        }
300    }
301
302    /// Reports the error to the configured destination.
303    pub fn report(mut self) -> io::Result<()> {
304        match self.color {
305            Color::Never => self.writer.force_color(false),
306            Color::Always => self.writer.force_color(true),
307            _ => {}
308        }
309        self.writer.set_max_line_width(self.wrap_width);
310        self.error.report_impl(&mut self.writer)
311    }
312
313    /// Reports the error to the configured destination, and exits the process with an appropriate
314    /// status.
315    ///
316    /// If the [`Error`] is fatal (ie. the result of an incorrect command invocation), this will
317    /// exit with code `EX_USAGE` (64).
318    /// Otherwise, the [`Error`] indicates that the user requested `--help` or `--version` output,
319    /// and this method will exit the process with code 0.
320    pub fn report_and_exit(self) -> ! {
321        let code = if self.error.is_fatal() {
322            EX_USAGE
323        } else {
324            EX_OK
325        };
326        self.report().ok();
327        process::exit(code);
328    }
329}