Skip to content

Commit b87ca2f

Browse files
authored
Merge pull request #5075 from epage/err
feat(builder): Allow injecting known unknowns
2 parents 063b153 + 9f65eb0 commit b87ca2f

File tree

5 files changed

+170
-3
lines changed

5 files changed

+170
-3
lines changed

clap_builder/src/builder/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub use value_parser::RangedU64ValueParser;
5959
pub use value_parser::StringValueParser;
6060
pub use value_parser::TryMapValueParser;
6161
pub use value_parser::TypedValueParser;
62+
pub use value_parser::UnknownArgumentValueParser;
6263
pub use value_parser::ValueParser;
6364
pub use value_parser::ValueParserFactory;
6465
pub use value_parser::_AnonymousValueParser;

clap_builder/src/builder/value_parser.rs

+101
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::convert::TryInto;
22
use std::ops::RangeBounds;
33

4+
use crate::builder::Str;
5+
use crate::builder::StyledStr;
46
use crate::util::AnyValue;
57
use crate::util::AnyValueId;
68

@@ -2086,6 +2088,105 @@ where
20862088
}
20872089
}
20882090

2091+
/// When encountered, report [ErrorKind::UnknownArgument][crate::error::ErrorKind::UnknownArgument]
2092+
///
2093+
/// Useful to help users migrate, either from old versions or similar tools.
2094+
///
2095+
/// # Examples
2096+
///
2097+
/// ```rust
2098+
/// # use clap_builder as clap;
2099+
/// # use clap::Command;
2100+
/// # use clap::Arg;
2101+
/// let cmd = Command::new("mycmd")
2102+
/// .args([
2103+
/// Arg::new("current-dir")
2104+
/// .short('C'),
2105+
/// Arg::new("current-dir-unknown")
2106+
/// .long("cwd")
2107+
/// .aliases(["current-dir", "directory", "working-directory", "root"])
2108+
/// .value_parser(clap::builder::UnknownArgumentValueParser::suggest_arg("-C"))
2109+
/// .hide(true),
2110+
/// ]);
2111+
///
2112+
/// // Use a supported version of the argument
2113+
/// let matches = cmd.clone().try_get_matches_from(["mycmd", "-C", ".."]).unwrap();
2114+
/// assert!(matches.contains_id("current-dir"));
2115+
/// assert_eq!(
2116+
/// matches.get_many::<String>("current-dir").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),
2117+
/// vec![".."]
2118+
/// );
2119+
///
2120+
/// // Use one of the invalid versions
2121+
/// let err = cmd.try_get_matches_from(["mycmd", "--cwd", ".."]).unwrap_err();
2122+
/// assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
2123+
/// ```
2124+
#[derive(Clone, Debug)]
2125+
pub struct UnknownArgumentValueParser {
2126+
arg: Option<Str>,
2127+
suggestions: Vec<StyledStr>,
2128+
}
2129+
2130+
impl UnknownArgumentValueParser {
2131+
/// Suggest an alternative argument
2132+
pub fn suggest_arg(arg: impl Into<Str>) -> Self {
2133+
Self {
2134+
arg: Some(arg.into()),
2135+
suggestions: Default::default(),
2136+
}
2137+
}
2138+
2139+
/// Provide a general suggestion
2140+
pub fn suggest(text: impl Into<StyledStr>) -> Self {
2141+
Self {
2142+
arg: Default::default(),
2143+
suggestions: vec![text.into()],
2144+
}
2145+
}
2146+
2147+
/// Extend the suggestions
2148+
pub fn and_suggest(mut self, text: impl Into<StyledStr>) -> Self {
2149+
self.suggestions.push(text.into());
2150+
self
2151+
}
2152+
}
2153+
2154+
impl TypedValueParser for UnknownArgumentValueParser {
2155+
type Value = String;
2156+
2157+
fn parse_ref(
2158+
&self,
2159+
cmd: &crate::Command,
2160+
arg: Option<&crate::Arg>,
2161+
_value: &std::ffi::OsStr,
2162+
) -> Result<Self::Value, crate::Error> {
2163+
let arg = match arg {
2164+
Some(arg) => arg.to_string(),
2165+
None => "..".to_owned(),
2166+
};
2167+
let err = crate::Error::unknown_argument(
2168+
cmd,
2169+
arg,
2170+
self.arg.as_ref().map(|s| (s.as_str().to_owned(), None)),
2171+
false,
2172+
crate::output::Usage::new(cmd).create_usage_with_title(&[]),
2173+
);
2174+
#[cfg(feature = "error-context")]
2175+
let err = {
2176+
debug_assert_eq!(
2177+
err.get(crate::error::ContextKind::Suggested),
2178+
None,
2179+
"Assuming `Error::unknown_argument` doesn't apply any `Suggested` so we can without caution"
2180+
);
2181+
err.insert_context_unchecked(
2182+
crate::error::ContextKind::Suggested,
2183+
crate::error::ContextValue::StyledStrs(self.suggestions.clone()),
2184+
)
2185+
};
2186+
Err(err)
2187+
}
2188+
}
2189+
20892190
/// Register a type with [value_parser!][crate::value_parser!]
20902191
///
20912192
/// # Example

clap_builder/src/error/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ impl<F: ErrorFormatter> Error<F> {
718718
let mut styled_suggestion = StyledStr::new();
719719
let _ = write!(
720720
styled_suggestion,
721-
"'{}{sub} --{flag}{}' exists",
721+
"'{}{sub} {flag}{}' exists",
722722
valid.render(),
723723
valid.render_reset()
724724
);
@@ -727,7 +727,7 @@ impl<F: ErrorFormatter> Error<F> {
727727
Some((flag, None)) => {
728728
err = err.insert_context_unchecked(
729729
ContextKind::SuggestedArg,
730-
ContextValue::String(format!("--{flag}")),
730+
ContextValue::String(flag),
731731
);
732732
}
733733
None => {}

clap_builder/src/parser/parser.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,7 @@ impl<'cmd> Parser<'cmd> {
15211521
self.start_custom_arg(matcher, arg, ValueSource::CommandLine);
15221522
}
15231523
}
1524+
let did_you_mean = did_you_mean.map(|(arg, cmd)| (format!("--{arg}"), cmd));
15241525

15251526
let required = self.cmd.required_graph();
15261527
let used: Vec<Id> = matcher

tests/builder/error.rs

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::utils;
22

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

55
#[track_caller]
66
fn assert_error<F: clap::error::ErrorFormatter>(
@@ -209,3 +209,67 @@ For more information, try '--help'.
209209
";
210210
assert_error(err, expected_kind, MESSAGE, true);
211211
}
212+
213+
#[test]
214+
#[cfg(feature = "error-context")]
215+
#[cfg(feature = "suggestions")]
216+
fn unknown_argument_option() {
217+
let cmd = Command::new("test").args([
218+
Arg::new("current-dir").short('C'),
219+
Arg::new("current-dir-unknown")
220+
.long("cwd")
221+
.aliases(["current-dir", "directory", "working-directory", "root"])
222+
.value_parser(
223+
clap::builder::UnknownArgumentValueParser::suggest_arg("-C")
224+
.and_suggest("not much else to say"),
225+
)
226+
.hide(true),
227+
]);
228+
let res = cmd.try_get_matches_from(["test", "--cwd", ".."]);
229+
assert!(res.is_err());
230+
let err = res.unwrap_err();
231+
let expected_kind = ErrorKind::UnknownArgument;
232+
static MESSAGE: &str = "\
233+
error: unexpected argument '--cwd <current-dir-unknown>' found
234+
235+
tip: a similar argument exists: '-C'
236+
tip: not much else to say
237+
238+
Usage: test [OPTIONS]
239+
240+
For more information, try '--help'.
241+
";
242+
assert_error(err, expected_kind, MESSAGE, true);
243+
}
244+
245+
#[test]
246+
#[cfg(feature = "error-context")]
247+
#[cfg(feature = "suggestions")]
248+
fn unknown_argument_flag() {
249+
let cmd = Command::new("test").args([
250+
Arg::new("ignore-rust-version").long("ignore-rust-version"),
251+
Arg::new("libtest-ignore")
252+
.long("ignored")
253+
.action(ArgAction::SetTrue)
254+
.value_parser(
255+
clap::builder::UnknownArgumentValueParser::suggest_arg("-- --ignored")
256+
.and_suggest("not much else to say"),
257+
)
258+
.hide(true),
259+
]);
260+
let res = cmd.try_get_matches_from(["test", "--ignored"]);
261+
assert!(res.is_err());
262+
let err = res.unwrap_err();
263+
let expected_kind = ErrorKind::UnknownArgument;
264+
static MESSAGE: &str = "\
265+
error: unexpected argument '--ignored' found
266+
267+
tip: a similar argument exists: '-- --ignored'
268+
tip: not much else to say
269+
270+
Usage: test [OPTIONS]
271+
272+
For more information, try '--help'.
273+
";
274+
assert_error(err, expected_kind, MESSAGE, true);
275+
}

0 commit comments

Comments
 (0)