From 3d8d901968d32da95695b5307a4eba3c93835081 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sat, 12 Nov 2016 12:36:12 -0800 Subject: [PATCH] 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. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/app.rs | 8 ++++++- src/integration.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 30 ++++++++++++++++++++++---- src/unit.rs | 5 ++++- 6 files changed, 95 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc8e4ff..9a946fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index e763bfa..b2ffbc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/app.rs b/src/app.rs index 62e5fea..41028f8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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) + } } } diff --git a/src/integration.rs b/src/integration.rs index 5d054f5..e12c24f 100644 --- a/src/integration.rs +++ b/src/integration.rs @@ -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", + ); +} diff --git a/src/lib.rs b/src/lib.rs index 3fbb6e1..891eeb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::>(); + 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", diff --git a/src/unit.rs b/src/unit.rs index 2bf074e..f3df446 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -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), } }