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
This commit is contained in:
Sebastian Thiel 2015-05-05 11:47:39 +02:00 committed by Kevin K
parent d17dcb2920
commit 06e869b518
4 changed files with 44 additions and 0 deletions

View file

@ -13,6 +13,9 @@ keywords = ["argument", "command", "arg", "parser", "parse"]
license = "MIT"
[dependencies]
strsim = "*"
[features]
default=[]

View file

@ -35,6 +35,11 @@ SUBCOMMANDS:
help Prints this message
subcmd tests subcommands'''
_sc_dym_usage = '''Subcommand "subcm" is unknown. Did you mean "subcmd" ?
USAGE:
\tclaptests [POSITIONAL] [FLAGS] [OPTIONS] [SUBCOMMANDS]
For more information try --help'''
_excluded = '''The argument '--flag' cannot be used with '-F'
USAGE:
\tclaptests [positional2] -F --long-option-2 <option2>
@ -220,6 +225,7 @@ cmds = {'help short: ': ['{} -h'.format(_bin), _help],
'F(s),O(s),P: ': ['{} value -f -o some'.format(_bin), _fop],
'F(l),O(l),P: ': ['{} value --flag --option some'.format(_bin), _fop],
'F(l),O(l=),P: ': ['{} value --flag --option=some'.format(_bin), _fop],
'sc dym: ': ['{} subcm'.format(_bin), _sc_dym_usage],
'sc help short: ': ['{} subcmd -h'.format(_bin), _schelp],
'sc help long: ': ['{} subcmd --help'.format(_bin), _schelp],
'scF(l),O(l),P: ': ['{} subcmd value --flag --option some'.format(_bin), _scfop],

View file

@ -11,6 +11,30 @@ use args::{ ArgMatches, Arg, SubCommand, MatchedArg};
use args::{ FlagBuilder, OptBuilder, PosBuilder};
use args::ArgGroup;
use strsim;
/// Produces a string from a given list of possible values which is similar to
/// the passed in value `v` with a certain confidence.
/// Thus in a list of possible values like ["foo", "bar"], the value "fop" will yield
/// `Some("foo")`, whereas "blark" would yield `None`.
fn did_you_mean<'a, I, T>(v: &str, possible_values: I) -> Option<&'a str>
where T: AsRef<str> + 'a,
I: IntoIterator<Item=&'a T>{
let mut candidate: Option<(f64, &str)> = None;
for pv in possible_values.into_iter() {
let confidence = strsim::jaro_winkler(v, pv.as_ref());
if confidence > 0.8 && (candidate.is_none() ||
(candidate.as_ref().unwrap().0 < confidence)) {
candidate = Some((confidence, pv.as_ref()));
}
}
match candidate {
None => None,
Some((_, candidate)) => Some(candidate),
}
}
/// Used to create a representation of a command line program and all possible command line
/// arguments for parsing at runtime.
///
@ -1296,6 +1320,16 @@ impl<'a, 'v, 'ab, 'u, 'h, 'ar> App<'a, 'v, 'ab, 'u, 'h, 'ar>{
break;
}
if let Some(candidate_subcommand) = did_you_mean(&arg, self.subcommands.keys()) {
self.report_error(
format!("Subcommand \"{}\" is unknown. Did you mean \"{}\" ?",
arg,
candidate_subcommand),
true,
true,
None);
}
if self.positionals_idx.is_empty() {
self.report_error(
format!("Found argument \"{}\", but {} wasn't expecting any",

View file

@ -348,6 +348,7 @@
//! - `Arg::new()` -> `Arg::with_name()`
//! - `Arg::mutually_excludes()` -> `Arg::conflicts_with()`
//! - `Arg::mutually_excludes_all()` -> `Arg::conflicts_with_all()`
extern crate strsim;
pub use args::{Arg, SubCommand, ArgMatches, ArgGroup};
pub use app::App;