Suggest alternatives to uknown recipes (#91)

Kind of silly, but why not. Will only suggest an alternative if edit
distance is less than 3. This could probably increase if the names are
longer.
This commit is contained in:
Casey Rodarmor 2016-11-12 12:36:12 -08:00 committed by GitHub
parent 26910a9fdc
commit 3d8d901968
6 changed files with 95 additions and 8 deletions

7
Cargo.lock generated
View File

@ -6,6 +6,7 @@ dependencies = [
"atty 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
"edit-distance 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
@ -65,6 +66,11 @@ dependencies = [
"vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "edit-distance"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "either"
version = "1.0.1"
@ -212,6 +218,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
"checksum brev 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "79571b60a8aa293f43b46370d8ba96fed28a5bee1303ea0e015d175ed0c63b40"
"checksum clap 2.17.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27dac76762fb56019b04aed3ccb43a770a18f80f9c2eb62ee1a18d9fb4ea2430"
"checksum edit-distance 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cd50a61206c09132fdf9cbaccc64a82cfccb6be528453903e03d4eb4ec80a61d"
"checksum either 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8aa2c82b7e1abd89a8a59fd89c4a51576ea76a894edf5d5b28944dd46edfed8d"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum itertools 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ef81b0a15a9e1808cfd3ebe6a87277d29ee88b34ac1197cc7547f1dd6d9f5424"

View File

@ -11,6 +11,7 @@ ansi_term = "^0.9.0"
atty = "^0.2.1"
brev = "^0.1.6"
clap = "^2.0.0"
edit-distance = "^1.0.0"
itertools = "^0.5.5"
lazy_static = "^0.2.1"
regex = "^0.1.77"

View File

@ -211,7 +211,13 @@ pub fn app() {
println!("{}", recipe);
process::exit(0);
}
None => die!("justfile contains no recipe \"{}\"", name)
None => {
warn!("Justfile does not contain recipe `{}`.", name);
if let Some(suggestion) = justfile.suggest(name) {
warn!("Did you mean `{}`?", suggestion);
}
process::exit(-1)
}
}
}

View File

@ -914,7 +914,7 @@ fn unknown_recipe() {
"hello:",
255,
"",
"error: Justfile does not contain recipe `foo`\n",
"error: Justfile does not contain recipe `foo`.\n",
);
}
@ -925,7 +925,7 @@ fn unknown_recipes() {
"hello:",
255,
"",
"error: Justfile does not contain recipes `foo` or `bar`\n",
"error: Justfile does not contain recipes `foo` or `bar`.\n",
);
}
@ -1093,3 +1093,51 @@ a Z="\t z":
"",
);
}
#[test]
fn show_suggestion() {
integration_test(
&["--show", "hell"],
r#"
hello a b='B ' c='C':
echo {{a}} {{b}} {{c}}
a Z="\t z":
"#,
255,
"",
"Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
);
}
#[test]
fn show_no_suggestion() {
integration_test(
&["--show", "hell"],
r#"
helloooooo a b='B ' c='C':
echo {{a}} {{b}} {{c}}
a Z="\t z":
"#,
255,
"",
"Justfile does not contain recipe `hell`.\n",
);
}
#[test]
fn run_suggestion() {
integration_test(
&["hell"],
r#"
hello a b='B ' c='C':
echo {{a}} {{b}} {{c}}
a Z="\t z":
"#,
255,
"",
"error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
);
}

View File

@ -15,6 +15,7 @@ extern crate tempdir;
extern crate itertools;
extern crate ansi_term;
extern crate unicode_width;
extern crate edit_distance;
use std::io::prelude::*;
@ -1064,6 +1065,19 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
self.recipes.keys().cloned().collect()
}
fn suggest(&self, name: &str) -> Option<&'a str> {
let mut suggestions = self.recipes.keys()
.map(|suggestion| (edit_distance::edit_distance(suggestion, name), suggestion))
.collect::<Vec<_>>();
suggestions.sort();
if let Some(&(distance, suggestion)) = suggestions.first() {
if distance < 3 {
return Some(suggestion)
}
}
None
}
fn run(
&'a self,
arguments: &[&'a str],
@ -1117,7 +1131,12 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
}
}
if !missing.is_empty() {
return Err(RunError::UnknownRecipes{recipes: missing});
let suggestion = if missing.len() == 1 {
self.suggest(missing.first().unwrap())
} else {
None
};
return Err(RunError::UnknownRecipes{recipes: missing, suggestion: suggestion});
}
for recipe in arguments.iter().map(|name| &self.recipes[name]) {
self.run_recipe(recipe, &[], &scope, &mut ran, options)?;
@ -1178,7 +1197,7 @@ enum RunError<'a> {
Signal{recipe: &'a str, signal: i32},
TmpdirIoError{recipe: &'a str, io_error: io::Error},
UnknownFailure{recipe: &'a str},
UnknownRecipes{recipes: Vec<&'a str>},
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>},
UnknownOverrides{overrides: Vec<&'a str>},
BacktickCode{token: Token<'a>, code: i32},
BacktickIoError{token: Token<'a>, io_error: io::Error},
@ -1196,9 +1215,12 @@ impl<'a> Display for RunError<'a> {
let mut error_token = None;
match *self {
RunError::UnknownRecipes{ref recipes} => {
write!(f, "Justfile does not contain recipe{} {}",
RunError::UnknownRecipes{ref recipes, ref suggestion} => {
write!(f, "Justfile does not contain recipe{} {}.",
maybe_s(recipes.len()), Or(&ticks(&recipes)))?;
if let Some(suggestion) = *suggestion {
write!(f, "\nDid you mean `{}`?", suggestion)?;
}
},
RunError::UnknownOverrides{ref overrides} => {
write!(f, "Variable{} {} overridden on the command line but not present in justfile",

View File

@ -711,7 +711,10 @@ fn range() {
#[test]
fn unknown_recipes() {
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() {
RunError::UnknownRecipes{recipes} => assert_eq!(recipes, &["x", "y", "z"]),
RunError::UnknownRecipes{recipes, suggestion} => {
assert_eq!(recipes, &["x", "y", "z"]);
assert_eq!(suggestion, None);
}
other => panic!("expected an unknown recipe error, but got: {}", other),
}
}