Skip to content

Commit 06e869b

Browse files
Byronkbknapp
authored andcommitted
feat(did-you-mean): for subcommands
If an argument is not understood as subcommand, but has a high-confidence match in the list of all known subcommands, we will use this one to print a customized error message. Previously, it would say that a positional argument wasn't understood, now it will say that a subcommand was unknown, and if the user meant `high-confidence-candidate`. If the argument doesn't sufficiently match any subcommand, the default handling will take over and try to treat it as positional argument. * added dependency to `strsym` crate * new `did_you_mean` function uses `strsim::jaro_winkler(...)` to look for good candidates. Related to #103
1 parent d17dcb2 commit 06e869b

File tree

4 files changed

+44
-0
lines changed

4 files changed

+44
-0
lines changed

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ keywords = ["argument", "command", "arg", "parser", "parse"]
1313

1414
license = "MIT"
1515

16+
[dependencies]
17+
strsim = "*"
18+
1619
[features]
1720
default=[]
1821

clap-tests/run_tests.py

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
help Prints this message
3636
subcmd tests subcommands'''
3737

38+
_sc_dym_usage = '''Subcommand "subcm" is unknown. Did you mean "subcmd" ?
39+
USAGE:
40+
\tclaptests [POSITIONAL] [FLAGS] [OPTIONS] [SUBCOMMANDS]
41+
For more information try --help'''
42+
3843
_excluded = '''The argument '--flag' cannot be used with '-F'
3944
USAGE:
4045
\tclaptests [positional2] -F --long-option-2 <option2>
@@ -220,6 +225,7 @@
220225
'F(s),O(s),P: ': ['{} value -f -o some'.format(_bin), _fop],
221226
'F(l),O(l),P: ': ['{} value --flag --option some'.format(_bin), _fop],
222227
'F(l),O(l=),P: ': ['{} value --flag --option=some'.format(_bin), _fop],
228+
'sc dym: ': ['{} subcm'.format(_bin), _sc_dym_usage],
223229
'sc help short: ': ['{} subcmd -h'.format(_bin), _schelp],
224230
'sc help long: ': ['{} subcmd --help'.format(_bin), _schelp],
225231
'scF(l),O(l),P: ': ['{} subcmd value --flag --option some'.format(_bin), _scfop],

src/app.rs

+34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,30 @@ use args::{ ArgMatches, Arg, SubCommand, MatchedArg};
1111
use args::{ FlagBuilder, OptBuilder, PosBuilder};
1212
use args::ArgGroup;
1313

14+
use strsim;
15+
16+
/// Produces a string from a given list of possible values which is similar to
17+
/// the passed in value `v` with a certain confidence.
18+
/// Thus in a list of possible values like ["foo", "bar"], the value "fop" will yield
19+
/// `Some("foo")`, whereas "blark" would yield `None`.
20+
fn did_you_mean<'a, I, T>(v: &str, possible_values: I) -> Option<&'a str>
21+
where T: AsRef<str> + 'a,
22+
I: IntoIterator<Item=&'a T>{
23+
24+
let mut candidate: Option<(f64, &str)> = None;
25+
for pv in possible_values.into_iter() {
26+
let confidence = strsim::jaro_winkler(v, pv.as_ref());
27+
if confidence > 0.8 && (candidate.is_none() ||
28+
(candidate.as_ref().unwrap().0 < confidence)) {
29+
candidate = Some((confidence, pv.as_ref()));
30+
}
31+
}
32+
match candidate {
33+
None => None,
34+
Some((_, candidate)) => Some(candidate),
35+
}
36+
}
37+
1438
/// Used to create a representation of a command line program and all possible command line
1539
/// arguments for parsing at runtime.
1640
///
@@ -1296,6 +1320,16 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
12961320
break;
12971321
}
12981322

1323+
if let Some(candidate_subcommand) = did_you_mean(&arg, self.subcommands.keys()) {
1324+
self.report_error(
1325+
format!("Subcommand \"{}\" is unknown. Did you mean \"{}\" ?",
1326+
arg,
1327+
candidate_subcommand),
1328+
true,
1329+
true,
1330+
None);
1331+
}
1332+
12991333
if self.positionals_idx.is_empty() {
13001334
self.report_error(
13011335
format!("Found argument \"{}\", but {} wasn't expecting any",

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@
348348
//! - `Arg::new()` -> `Arg::with_name()`
349349
//! - `Arg::mutually_excludes()` -> `Arg::conflicts_with()`
350350
//! - `Arg::mutually_excludes_all()` -> `Arg::conflicts_with_all()`
351+
extern crate strsim;
351352

352353
pub use args::{Arg, SubCommand, ArgMatches, ArgGroup};
353354
pub use app::App;

0 commit comments

Comments
 (0)