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)", "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)", "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)", "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)", "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)", "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)", "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)", "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]] [[package]]
name = "either" name = "either"
version = "1.0.1" 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 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 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 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 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 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" "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" atty = "^0.2.1"
brev = "^0.1.6" brev = "^0.1.6"
clap = "^2.0.0" clap = "^2.0.0"
edit-distance = "^1.0.0"
itertools = "^0.5.5" itertools = "^0.5.5"
lazy_static = "^0.2.1" lazy_static = "^0.2.1"
regex = "^0.1.77" regex = "^0.1.77"

View File

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

View File

@ -711,7 +711,10 @@ fn range() {
#[test] #[test]
fn unknown_recipes() { fn unknown_recipes() {
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() { 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), other => panic!("expected an unknown recipe error, but got: {}", other),
} }
} }