Skip to content
This repository was archived by the owner on Sep 14, 2023. It is now read-only.

pattern suggestion for commands #227

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/dopt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use regex::{Captures, Regex};
use serde::de;
use serde::de::IntoDeserializer;

use parse::Parser;
use parse::{Parser, Matches};
use synonym::SynonymMap;

use self::Value::{Switch, Counted, Plain, List};
Expand Down Expand Up @@ -235,8 +235,9 @@ impl Docopt {
.map_err(|s| self.err_with_usage(Argv(s)))
.and_then(|argv|
match self.p.matches(&argv) {
Some(m) => Ok(ArgvMap { map: m }),
None => Err(self.err_with_usage(NoMatch)),
Matches::Some(m) => Ok(ArgvMap { map: m }),
Matches::None => Err(self.err_with_usage(NoMatch)),
Matches::Suggestion(s) => Err(self.err_with_usage(Argv(s))),
})?;
if self.help && vals.get_bool("--help") {
return Err(self.err_with_full_doc(Help));
Expand Down
73 changes: 65 additions & 8 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ pub struct Parser {
last_atom_added: Option<Atom>, // context for [default: ...]
}

pub enum Matches {
Some(SynonymMap<String, Value>),
Suggestion(String),
None,
}

impl From<Option<String>> for Matches {
fn from(o: Option<String>) -> Self {
match o {
Some(s) => Matches::Suggestion(s),
None => Matches::None,
}
}
}

impl Parser {
pub fn new(doc: &str) -> Result<Parser, String> {
let mut d = Parser {
Expand All @@ -83,14 +98,16 @@ impl Parser {
Ok(d)
}

pub fn matches(&self, argv: &Argv) -> Option<SynonymMap<String, Value>> {
pub fn matches(&self, argv: &Argv) -> Matches {
let mut suggestion = None;
for usage in &self.usages {
match Matcher::matches(argv, usage) {
None => continue,
Some(vals) => return Some(vals),
Matches::None => continue,
Matches::Some(vals) => return Matches::Some(vals),
Matches::Suggestion(s) => suggestion = Some(s),
}
}
None
suggestion.into()
}

pub fn parse_argv(&self, argv: Vec<String>, options_first: bool)
Expand Down Expand Up @@ -1176,16 +1193,15 @@ impl MState {
}

impl<'a, 'b> Matcher<'a, 'b> {
fn matches(argv: &'a Argv, pat: &Pattern)
-> Option<SynonymMap<String, Value>> {
fn matches(argv: &'a Argv, pat: &Pattern) -> Matches {
let m = Matcher { argv: argv };
let init = MState {
argvi: 0,
counts: argv.counts.clone(),
max_counts: HashMap::new(),
vals: HashMap::new(),
};
m.states(pat, &init)
let matches = m.states(pat, &init)
.into_iter()
.filter(|s| m.state_consumed_all_argv(s))
.filter(|s| m.state_has_valid_flags(s))
Expand All @@ -1209,7 +1225,12 @@ impl<'a, 'b> Matcher<'a, 'b> {
}
}
synmap
})
});

match matches {
Some(matches) => Matches::Some(matches),
None => m.pattern_suggestion(pat).into(),
}
}

fn token_from(&self, state: &MState) -> Option<&ArgvToken> {
Expand Down Expand Up @@ -1268,6 +1289,42 @@ impl<'a, 'b> Matcher<'a, 'b> {
}
}

fn pattern_suggestion(&self, pattern: &Pattern) -> Option<String> {
fn pattern_suggestion_for_arg(arg: &str, pattern: &Pattern) -> Option<String> {
match *pattern {
Pattern::PatAtom(ref atom) => match *atom {
Atom::Short(short) if &short.to_string() == arg => Some(format!("-{}", short)),
Atom::Long(ref long) if long == arg => Some(format!("--{}", long)),
_ => None,
},
Pattern::Optional(ref optional) => {
for pat in optional {
let suggestion = pattern_suggestion_for_arg(arg, pat);
if suggestion.is_some() {
return suggestion;
}
}
None
},
_ => None,
}
}

if let Pattern::Sequence(ref seq) = *pattern {
for (arg, pat) in self.argv.positional.iter().zip(seq.iter()) {
let arg_str = match arg.atom {
Atom::Positional(ref s) => s,
_ => unreachable!(),
};

if let Some(suggestion) = pattern_suggestion_for_arg(arg_str, pat) {
return Some(format!("Unknown command: '{}'. Did you mean '{}'?", arg_str, suggestion));
}
}
}
None
}

fn state_consumed_all_argv(&self, state: &MState) -> bool {
self.argv.positional.len() == state.argvi
}
Expand Down
21 changes: 21 additions & 0 deletions src/test/suggestions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,24 @@ test_suggest!(test_suggest_5,
",
&["--import", "--complte"], "Unknown flag: '--complte'. Did you mean '--complete'?");

test_suggest!(test_suggest_6,
"Usage: cargo owner [options]

Options:
-h, --help Print this message
-a, --add LOGIN Name of a user or team to add as an owner
-r, --remove LOGIN Name of a user or team to remove as an owner
-l, --list List owners of a crate
",
&["owner", "add"], "Unknown command: 'add'. Did you mean '--add'?");

test_suggest!(test_suggest_7,
"Usage: cargo owner [options]

Options:
-h, --help Print this message
-a, --add LOGIN Name of a user or team to add as an owner
-r, --remove LOGIN Name of a user or team to remove as an owner
-l, --list List owners of a crate
",
&["owner", "list"], "Unknown command: 'list'. Did you mean '--list'?");