Skip to content

Commit 8413c15

Browse files
committed
feat(builder): Allow injecting known unknowns
Fixes #4706
1 parent 063b153 commit 8413c15

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
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

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

4+
use crate::builder::Str;
45
use crate::util::AnyValue;
56
use crate::util::AnyValueId;
67

@@ -2086,6 +2087,74 @@ where
20862087
}
20872088
}
20882089

2090+
/// When encountered, report [ErrorKind::UnknownArgument][crate::error::ErrorKind::UnknownArgument]
2091+
///
2092+
/// Useful to help users migrate, either from old versions or similar tools.
2093+
///
2094+
/// # Examples
2095+
///
2096+
/// ```rust
2097+
/// # use clap_builder as clap;
2098+
/// # use clap::Command;
2099+
/// # use clap::Arg;
2100+
/// let cmd = Command::new("mycmd")
2101+
/// .args([
2102+
/// Arg::new("current-dir")
2103+
/// .short('C'),
2104+
/// Arg::new("current-dir-unknown")
2105+
/// .long("cwd")
2106+
/// .aliases(["current-dir", "directory", "working-directory", "root"])
2107+
/// .value_parser(clap::builder::UnknownArgumentValueParser::suggest("-C"))
2108+
/// .hide(true),
2109+
/// ]);
2110+
///
2111+
/// // Use a supported version of the argument
2112+
/// let matches = cmd.clone().try_get_matches_from(["mycmd", "-C", ".."]).unwrap();
2113+
/// assert!(matches.contains_id("current-dir"));
2114+
/// assert_eq!(
2115+
/// matches.get_many::<String>("current-dir").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),
2116+
/// vec![".."]
2117+
/// );
2118+
///
2119+
/// // Use one of the invalid versions
2120+
/// let err = cmd.try_get_matches_from(["mycmd", "--cwd", ".."]).unwrap_err();
2121+
/// assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
2122+
/// ```
2123+
#[derive(Clone, Debug)]
2124+
pub struct UnknownArgumentValueParser {
2125+
arg: Str,
2126+
}
2127+
2128+
impl UnknownArgumentValueParser {
2129+
/// Suggest an alternative argument
2130+
pub fn suggest(arg: impl Into<Str>) -> Self {
2131+
Self { arg: arg.into() }
2132+
}
2133+
}
2134+
2135+
impl TypedValueParser for UnknownArgumentValueParser {
2136+
type Value = String;
2137+
2138+
fn parse_ref(
2139+
&self,
2140+
cmd: &crate::Command,
2141+
arg: Option<&crate::Arg>,
2142+
_value: &std::ffi::OsStr,
2143+
) -> Result<Self::Value, crate::Error> {
2144+
let arg = match arg {
2145+
Some(arg) => arg.to_string(),
2146+
None => "..".to_owned(),
2147+
};
2148+
Err(crate::Error::unknown_argument(
2149+
cmd,
2150+
arg,
2151+
Some((self.arg.as_str().to_owned(), None)),
2152+
false,
2153+
crate::output::Usage::new(cmd).create_usage_with_title(&[]),
2154+
))
2155+
}
2156+
}
2157+
20892158
/// Register a type with [value_parser!][crate::value_parser!]
20902159
///
20912160
/// # Example

tests/builder/error.rs

+59-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,61 @@ 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(clap::builder::UnknownArgumentValueParser::suggest("-C"))
223+
.hide(true),
224+
]);
225+
let res = cmd.try_get_matches_from(["test", "--cwd", ".."]);
226+
assert!(res.is_err());
227+
let err = res.unwrap_err();
228+
let expected_kind = ErrorKind::UnknownArgument;
229+
static MESSAGE: &str = "\
230+
error: unexpected argument '--cwd <current-dir-unknown>' found
231+
232+
tip: a similar argument exists: '---C'
233+
234+
Usage: test [OPTIONS]
235+
236+
For more information, try '--help'.
237+
";
238+
assert_error(err, expected_kind, MESSAGE, true);
239+
}
240+
241+
#[test]
242+
#[cfg(feature = "error-context")]
243+
#[cfg(feature = "suggestions")]
244+
fn unknown_argument_flag() {
245+
let cmd = Command::new("test").args([
246+
Arg::new("ignore-rust-version").long("ignore-rust-version"),
247+
Arg::new("libtest-ignore")
248+
.long("ignored")
249+
.action(ArgAction::SetTrue)
250+
.value_parser(clap::builder::UnknownArgumentValueParser::suggest(
251+
"-- --ignored",
252+
))
253+
.hide(true),
254+
]);
255+
let res = cmd.try_get_matches_from(["test", "--ignored"]);
256+
assert!(res.is_err());
257+
let err = res.unwrap_err();
258+
let expected_kind = ErrorKind::UnknownArgument;
259+
static MESSAGE: &str = "\
260+
error: unexpected argument '--ignored' found
261+
262+
tip: a similar argument exists: '---- --ignored'
263+
264+
Usage: test [OPTIONS]
265+
266+
For more information, try '--help'.
267+
";
268+
assert_error(err, expected_kind, MESSAGE, true);
269+
}

0 commit comments

Comments
 (0)