Skip to content

Commit

Permalink
Merge pull request #5075 from epage/err
Browse files Browse the repository at this point in the history
feat(builder): Allow injecting known unknowns
  • Loading branch information
epage committed Aug 17, 2023
2 parents 063b153 + 9f65eb0 commit b87ca2f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 3 deletions.
1 change: 1 addition & 0 deletions clap_builder/src/builder/mod.rs
Expand Up @@ -59,6 +59,7 @@ pub use value_parser::RangedU64ValueParser;
pub use value_parser::StringValueParser;
pub use value_parser::TryMapValueParser;
pub use value_parser::TypedValueParser;
pub use value_parser::UnknownArgumentValueParser;
pub use value_parser::ValueParser;
pub use value_parser::ValueParserFactory;
pub use value_parser::_AnonymousValueParser;
Expand Down
101 changes: 101 additions & 0 deletions clap_builder/src/builder/value_parser.rs
@@ -1,6 +1,8 @@
use std::convert::TryInto;
use std::ops::RangeBounds;

use crate::builder::Str;
use crate::builder::StyledStr;
use crate::util::AnyValue;
use crate::util::AnyValueId;

Expand Down Expand Up @@ -2086,6 +2088,105 @@ where
}
}

/// When encountered, report [ErrorKind::UnknownArgument][crate::error::ErrorKind::UnknownArgument]
///
/// Useful to help users migrate, either from old versions or similar tools.
///
/// # Examples
///
/// ```rust
/// # use clap_builder as clap;
/// # use clap::Command;
/// # use clap::Arg;
/// let cmd = Command::new("mycmd")
/// .args([
/// Arg::new("current-dir")
/// .short('C'),
/// Arg::new("current-dir-unknown")
/// .long("cwd")
/// .aliases(["current-dir", "directory", "working-directory", "root"])
/// .value_parser(clap::builder::UnknownArgumentValueParser::suggest_arg("-C"))
/// .hide(true),
/// ]);
///
/// // Use a supported version of the argument
/// let matches = cmd.clone().try_get_matches_from(["mycmd", "-C", ".."]).unwrap();
/// assert!(matches.contains_id("current-dir"));
/// assert_eq!(
/// matches.get_many::<String>("current-dir").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),
/// vec![".."]
/// );
///
/// // Use one of the invalid versions
/// let err = cmd.try_get_matches_from(["mycmd", "--cwd", ".."]).unwrap_err();
/// assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
/// ```
#[derive(Clone, Debug)]
pub struct UnknownArgumentValueParser {
arg: Option<Str>,
suggestions: Vec<StyledStr>,
}

impl UnknownArgumentValueParser {
/// Suggest an alternative argument
pub fn suggest_arg(arg: impl Into<Str>) -> Self {
Self {
arg: Some(arg.into()),
suggestions: Default::default(),
}
}

/// Provide a general suggestion
pub fn suggest(text: impl Into<StyledStr>) -> Self {
Self {
arg: Default::default(),
suggestions: vec![text.into()],
}
}

/// Extend the suggestions
pub fn and_suggest(mut self, text: impl Into<StyledStr>) -> Self {
self.suggestions.push(text.into());
self
}
}

impl TypedValueParser for UnknownArgumentValueParser {
type Value = String;

fn parse_ref(
&self,
cmd: &crate::Command,
arg: Option<&crate::Arg>,
_value: &std::ffi::OsStr,
) -> Result<Self::Value, crate::Error> {
let arg = match arg {
Some(arg) => arg.to_string(),
None => "..".to_owned(),
};
let err = crate::Error::unknown_argument(
cmd,
arg,
self.arg.as_ref().map(|s| (s.as_str().to_owned(), None)),
false,
crate::output::Usage::new(cmd).create_usage_with_title(&[]),
);
#[cfg(feature = "error-context")]
let err = {
debug_assert_eq!(
err.get(crate::error::ContextKind::Suggested),
None,
"Assuming `Error::unknown_argument` doesn't apply any `Suggested` so we can without caution"
);
err.insert_context_unchecked(
crate::error::ContextKind::Suggested,
crate::error::ContextValue::StyledStrs(self.suggestions.clone()),
)
};
Err(err)
}
}

/// Register a type with [value_parser!][crate::value_parser!]
///
/// # Example
Expand Down
4 changes: 2 additions & 2 deletions clap_builder/src/error/mod.rs
Expand Up @@ -718,7 +718,7 @@ impl<F: ErrorFormatter> Error<F> {
let mut styled_suggestion = StyledStr::new();
let _ = write!(
styled_suggestion,
"'{}{sub} --{flag}{}' exists",
"'{}{sub} {flag}{}' exists",
valid.render(),
valid.render_reset()
);
Expand All @@ -727,7 +727,7 @@ impl<F: ErrorFormatter> Error<F> {
Some((flag, None)) => {
err = err.insert_context_unchecked(
ContextKind::SuggestedArg,
ContextValue::String(format!("--{flag}")),
ContextValue::String(flag),
);
}
None => {}
Expand Down
1 change: 1 addition & 0 deletions clap_builder/src/parser/parser.rs
Expand Up @@ -1521,6 +1521,7 @@ impl<'cmd> Parser<'cmd> {
self.start_custom_arg(matcher, arg, ValueSource::CommandLine);
}
}
let did_you_mean = did_you_mean.map(|(arg, cmd)| (format!("--{arg}"), cmd));

let required = self.cmd.required_graph();
let used: Vec<Id> = matcher
Expand Down
66 changes: 65 additions & 1 deletion tests/builder/error.rs
@@ -1,6 +1,6 @@
use super::utils;

use clap::{arg, error::Error, error::ErrorKind, value_parser, Arg, Command};
use clap::{arg, builder::ArgAction, error::Error, error::ErrorKind, value_parser, Arg, Command};

#[track_caller]
fn assert_error<F: clap::error::ErrorFormatter>(
Expand Down Expand Up @@ -209,3 +209,67 @@ For more information, try '--help'.
";
assert_error(err, expected_kind, MESSAGE, true);
}

#[test]
#[cfg(feature = "error-context")]
#[cfg(feature = "suggestions")]
fn unknown_argument_option() {
let cmd = Command::new("test").args([
Arg::new("current-dir").short('C'),
Arg::new("current-dir-unknown")
.long("cwd")
.aliases(["current-dir", "directory", "working-directory", "root"])
.value_parser(
clap::builder::UnknownArgumentValueParser::suggest_arg("-C")
.and_suggest("not much else to say"),
)
.hide(true),
]);
let res = cmd.try_get_matches_from(["test", "--cwd", ".."]);
assert!(res.is_err());
let err = res.unwrap_err();
let expected_kind = ErrorKind::UnknownArgument;
static MESSAGE: &str = "\
error: unexpected argument '--cwd <current-dir-unknown>' found
tip: a similar argument exists: '-C'
tip: not much else to say
Usage: test [OPTIONS]
For more information, try '--help'.
";
assert_error(err, expected_kind, MESSAGE, true);
}

#[test]
#[cfg(feature = "error-context")]
#[cfg(feature = "suggestions")]
fn unknown_argument_flag() {
let cmd = Command::new("test").args([
Arg::new("ignore-rust-version").long("ignore-rust-version"),
Arg::new("libtest-ignore")
.long("ignored")
.action(ArgAction::SetTrue)
.value_parser(
clap::builder::UnknownArgumentValueParser::suggest_arg("-- --ignored")
.and_suggest("not much else to say"),
)
.hide(true),
]);
let res = cmd.try_get_matches_from(["test", "--ignored"]);
assert!(res.is_err());
let err = res.unwrap_err();
let expected_kind = ErrorKind::UnknownArgument;
static MESSAGE: &str = "\
error: unexpected argument '--ignored' found
tip: a similar argument exists: '-- --ignored'
tip: not much else to say
Usage: test [OPTIONS]
For more information, try '--help'.
";
assert_error(err, expected_kind, MESSAGE, true);
}

0 comments on commit b87ca2f

Please sign in to comment.