From 4f9c90a662b7d97a94ea9c94b88e57f958aa1cc9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 15 Oct 2021 16:06:21 -0500 Subject: [PATCH] feat: Expose clap-style errors to users This gives users the basic error template for quick and dirty messages. In addition to the lack of customization, they are not given anything to help them with coloring or for programmayic use (info, source). This is something I've wanted many times for one-off validation that can't be expressed with clap's validation or it just wasn't worth the hoops. The more pressing need is for #2255, I need `clap_derive` to be able to create error messages and `Error::with_description` seemed too disjoint from the rest of the clap experience that it seemed like users would immediately create issues about it showing up. With this available, I've gone ahead and deprecated `Error::with_description` (added in 58512f2fc), assuming this will be sufficient for users needs (or they can use IO Errors as a back door). I did so according to the pattern in #2718 despite us not being fully resolved on that approach yet. --- src/build/app/mod.rs | 17 ++++++++++++- src/parse/errors.rs | 48 +++++++++++++++++++++++++---------- tests/error.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ tests/utils.rs | 2 +- 4 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 tests/error.rs diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index b12f8c8fbf81..5a531ae7c99c 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -29,7 +29,7 @@ use crate::{ output::{fmt::Colorizer, Help, HelpWriter, Usage}, parse::{ArgMatcher, ArgMatches, Input, Parser}, util::{color::ColorChoice, safe_exit, Id, Key, USAGE_CODE}, - Result as ClapResult, INTERNAL_ERROR_MSG, + Error, ErrorKind, Result as ClapResult, INTERNAL_ERROR_MSG, }; /// Represents a command line interface which is made up of all possible @@ -1764,6 +1764,21 @@ impl<'help> App<'help> { self } + /// Custom error message for post-parsing validation + /// + /// # Examples + /// + /// ```rust + /// # use clap::{App, ErrorKind}; + /// let mut app = App::new("myprog"); + /// let err = app.error(ErrorKind::InvalidValue, "Some failure case"); + /// ``` + pub fn error(&mut self, kind: ErrorKind, message: impl std::fmt::Display) -> Error { + self._build(); + let usage = self.render_usage(); + Error::user_error(self, usage, kind, message) + } + /// Prints the full help message to [`io::stdout()`] using a [`BufWriter`] using the same /// method as if someone ran `-h` to request the help message. /// diff --git a/src/parse/errors.rs b/src/parse/errors.rs index 50e8ca595094..11e0a48c5dd8 100644 --- a/src/parse/errors.rs +++ b/src/parse/errors.rs @@ -423,6 +423,10 @@ pub enum ErrorKind { } /// Command Line Argument Parser Error +/// +/// See [`App::error`] to create an error. +/// +/// [`App::error`]: crate::App::error #[derive(Debug)] pub struct Error { /// Formatted error message, enhancing the cause message with extra information @@ -540,6 +544,27 @@ impl Error { } } + pub(crate) fn user_error( + app: &App, + usage: String, + kind: ErrorKind, + message: impl std::fmt::Display, + ) -> Self { + let mut c = Colorizer::new(true, app.get_color()); + + start_error(&mut c, message.to_string()); + put_usage(&mut c, usage); + try_help(app, &mut c); + + Self { + message: c, + kind, + info: vec![], + source: None, + backtrace: Backtrace::new(), + } + } + pub(crate) fn argument_conflict( app: &App, arg: &Arg, @@ -1077,34 +1102,31 @@ impl Error { } } - /// Create an error with a custom description. + /// Deprecated, see [`App::error`] /// - /// This can be used in combination with `Error::exit` to exit your program - /// with a custom error message. + /// [`App::error`]: crate::App::error + #[deprecated(since = "3.0.0", note = "Replaced with `App::error`")] pub fn with_description(description: String, kind: ErrorKind) -> Self { let mut c = Colorizer::new(true, ColorChoice::Auto); start_error(&mut c, description); - - Error { - message: c, - kind, - info: vec![], - source: None, - backtrace: Backtrace::new(), - } + Error::new(c, kind) } } impl From for Error { fn from(e: io::Error) -> Self { - Error::with_description(e.to_string(), ErrorKind::Io) + let mut c = Colorizer::new(true, ColorChoice::Auto); + start_error(&mut c, e.to_string()); + Error::new(c, ErrorKind::Io) } } impl From for Error { fn from(e: fmt::Error) -> Self { - Error::with_description(e.to_string(), ErrorKind::Format) + let mut c = Colorizer::new(true, ColorChoice::Auto); + start_error(&mut c, e.to_string()); + Error::new(c, ErrorKind::Format) } } diff --git a/tests/error.rs b/tests/error.rs new file mode 100644 index 000000000000..73ee067e98a1 --- /dev/null +++ b/tests/error.rs @@ -0,0 +1,59 @@ +mod utils; + +use clap::{App, Arg, ColorChoice, Error, ErrorKind}; + +fn compare_error( + err: Error, + expected_kind: ErrorKind, + expected_output: &str, + stderr: bool, +) -> bool { + let actual_output = err.to_string(); + assert_eq!( + stderr, + err.use_stderr(), + "Should Use STDERR failed. Should be {} but is {}", + stderr, + err.use_stderr() + ); + assert_eq!(expected_kind, err.kind); + utils::compare(expected_output, actual_output) +} + +#[test] +fn app_error() { + static MESSAGE: &str = "error: Failed for mysterious reasons + +USAGE: + test [OPTIONS] --all + +For more information try --help +"; + let mut app = App::new("test") + .color(ColorChoice::Never) + .arg( + Arg::new("all") + .short('a') + .long("all") + .required(true) + .about("Also do versioning for private crates (will not be published)"), + ) + .arg( + Arg::new("exact") + .long("exact") + .about("Specify inter dependency version numbers exactly with `=`"), + ) + .arg( + Arg::new("no_git_commit") + .long("no-git-commit") + .about("Do not commit version changes"), + ) + .arg( + Arg::new("no_git_push") + .long("no-git-push") + .about("Do not push generated commit and tags to git remote"), + ); + let expected_kind = ErrorKind::InvalidValue; + let err = app.error(expected_kind, "Failed for mysterious reasons"); + assert!(compare_error(err, expected_kind, MESSAGE, true)); +} diff --git a/tests/utils.rs b/tests/utils.rs index a339ffb0ea9e..af2e1d46b536 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -7,7 +7,7 @@ use regex::Regex; use clap::{App, Arg, ArgGroup}; -fn compare(l: S, r: S2) -> bool +pub fn compare(l: S, r: S2) -> bool where S: AsRef, S2: AsRef,