Suggest alternative when command not found (#6256)

* Suggest alternative when command not found

* Add tests for command-not-found suggestions

* Put suggestion in label

* Fix tests
This commit is contained in:
Reilly Wood 2022-08-07 11:40:41 -07:00 committed by GitHub
parent 63e220a763
commit 84fae6e07e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 7 deletions

View File

@ -2,6 +2,7 @@ use fancy_regex::Regex;
use itertools::Itertools;
use nu_engine::env_to_strings;
use nu_engine::CallExt;
use nu_protocol::did_you_mean;
use nu_protocol::engine::{EngineState, Stack};
use nu_protocol::{ast::Call, engine::Command, ShellError, Signature, SyntaxShape, Value};
use nu_protocol::{Category, Example, ListStream, PipelineData, RawStream, Span, Spanned};
@ -157,11 +158,21 @@ impl ExternalCommand {
}
match child {
Err(err) => Err(ShellError::ExternalCommand(
"can't run executable".to_string(),
err.to_string(),
self.name.span,
)),
Err(err) => {
// If we try to run an external but can't, there's a good chance
// that the user entered the wrong command name
let suggestion = suggest_command(&self.name.item, engine_state);
let label = match suggestion {
Some(s) => format!("did you mean '{s}'?"),
None => "can't run executable".into(),
};
Err(ShellError::ExternalCommand(
label,
err.to_string(),
self.name.span,
))
}
Ok(mut child) => {
if !input.is_nothing() {
let mut engine_state = engine_state.clone();
@ -513,6 +524,24 @@ impl ExternalCommand {
}
}
/// Given an invalid command name, try to suggest an alternative
fn suggest_command(attempted_command: &str, engine_state: &EngineState) -> Option<String> {
let commands = engine_state.get_signatures(false);
let command_name_lower = attempted_command.to_lowercase();
let search_term_match = commands.iter().find(|sig| {
sig.search_terms
.iter()
.any(|term| term.to_lowercase() == command_name_lower)
});
match search_term_match {
Some(sig) => Some(sig.name.clone()),
None => {
let command_names: Vec<String> = commands.iter().map(|sig| sig.name.clone()).collect();
did_you_mean(&command_names, attempted_command)
}
}
}
fn has_unsafe_shell_characters(arg: &str) -> bool {
let re: Regex = Regex::new(r"[^\w@%+=:,./-]").expect("regex to be valid");

View File

@ -65,5 +65,5 @@ fn checks_if_all_returns_error_with_invalid_command() {
"#
));
assert!(actual.err.contains("can't run executable") || actual.err.contains("type_mismatch"));
assert!(actual.err.contains("can't run executable") || actual.err.contains("did you mean"));
}

View File

@ -41,5 +41,5 @@ fn checks_if_any_returns_error_with_invalid_command() {
"#
));
assert!(actual.err.contains("can't run executable") || actual.err.contains("type_mismatch"));
assert!(actual.err.contains("can't run executable") || actual.err.contains("did you mean"));
}

View File

@ -107,6 +107,19 @@ fn passes_binary_data_between_externals() {
)
}
#[test]
fn command_not_found_error_suggests_search_term() {
// 'distinct' is not a command, but it is a search term for 'uniq'
let actual = nu!(cwd: ".", "ls | distinct");
assert!(actual.err.contains("uniq"));
}
#[test]
fn command_not_found_error_suggests_typo_fix() {
let actual = nu!(cwd: ".", "benhcmark { echo 'foo'}");
assert!(actual.err.contains("benchmark"));
}
mod it_evaluation {
use super::nu;
use nu_test_support::fs::Stub::{EmptyFile, FileWithContent, FileWithContentToBeTrimmed};