forked from extern/nushell
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:
parent
63e220a763
commit
84fae6e07e
@ -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");
|
||||
|
||||
|
@ -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"));
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
|
@ -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};
|
||||
|
Loading…
Reference in New Issue
Block a user