Skip to content

Commit

Permalink
feat(did-you-mean): for subcommands
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Byron authored and kbknapp committed May 5, 2015
1 parent d17dcb2 commit 06e869b
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ keywords = ["argument", "command", "arg", "parser", "parse"]

license = "MIT"

[dependencies]
strsim = "*"

[features]
default=[]

Expand Down
6 changes: 6 additions & 0 deletions clap-tests/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
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>
Expand Down Expand Up @@ -220,6 +225,7 @@
'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],
Expand Down
34 changes: 34 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 06e869b

Please sign in to comment.