From 135abc75b6ce451d753e5eb89b6ef933eef561cf Mon Sep 17 00:00:00 2001 From: iff Date: Mon, 31 Jul 2023 18:46:35 +0200 Subject: [PATCH] feat: fuzzy search not found commands --- rules/no_command.toml | 10 +++ src/args.rs | 11 ++- src/corrections.rs | 167 ++++++++++++++++++++++++++++++++++++++---- src/main.rs | 5 +- src/shell.rs | 11 ++- 5 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 rules/no_command.toml diff --git a/rules/no_command.toml b/rules/no_command.toml new file mode 100644 index 0000000..94d49ad --- /dev/null +++ b/rules/no_command.toml @@ -0,0 +1,10 @@ +command = "no_command" + +[[match_err]] +pattern = [ + "command not found" +] +suggest = [ +''' +{{typo[0](path)}} {{command[1:]}}''' +] diff --git a/src/args.rs b/src/args.rs index b31f7fe..a85be7a 100644 --- a/src/args.rs +++ b/src/args.rs @@ -19,13 +19,16 @@ pub fn handle_args() { last_command = "$(history | head -n 1)"; alias = "$(alias)"; } - "nu" | "nush" | "nushell"=> { + "nu" | "nush" | "nushell" => { last_command = "(history | last).command"; alias = "\"\""; - println!("with-env {{ _PR_LAST_COMMAND : {},\ + println!( + "with-env {{ _PR_LAST_COMMAND : {},\ _PR_ALIAS : {},\ - _PR_SHELL : {} }} \ - {{ {} }}", last_command, alias, "nu", binary_path); + _PR_SHELL : nu }} \ + {{ {} }}", + last_command, alias, binary_path + ); std::process::exit(0); } _ => { diff --git a/src/corrections.rs b/src/corrections.rs index 8f9c7d3..e075eb6 100644 --- a/src/corrections.rs +++ b/src/corrections.rs @@ -29,6 +29,12 @@ pub fn correct_command(shell: &str, last_command: &str) -> Option { } return Some(suggest); } + + let suggest = match_pattern("no_command", last_command, &err); + if let Some(suggest) = suggest { + let suggest = eval_suggest(&suggest, last_command); + return Some(suggest); + } None } @@ -57,9 +63,16 @@ fn check_suggest(suggest: &str, command: &str, error_msg: &str) -> Option>(); + let mut lines = suggest.lines().collect::>(); let conditions = lines.first().unwrap().trim().replacen('#', "", 1); - let conditions = conditions.trim_start_matches('[').trim_end_matches(']'); + let mut conditions = conditions.trim_start_matches('[').to_string(); + for (i, line) in lines[1..].iter().enumerate() { + conditions.push_str(line); + if line.ends_with(']') { + lines = lines[i + 1..].to_vec(); + break; + } + } let conditions = conditions.split(',').collect::>(); for condition in conditions { @@ -91,6 +104,7 @@ fn eval_condition(condition: &str, arg: &str, command: &str, error_msg: &str) -> } "err_contains" => error_msg.contains(arg), "cmd_contains" => command.contains(arg), + "match_typo_command" => false, _ => unreachable!("Unknown condition when evaluation condition: {}", condition), } } @@ -103,33 +117,154 @@ fn eval_suggest(suggest: &str, last_command: &str) -> String { while suggest.contains("{{command") { let placeholder_start = "{{command"; let placeholder_end = "}}"; - let placeholder = suggest.find(placeholder_start).unwrap() - ..suggest.find(placeholder_end).unwrap() + placeholder_end.len(); + let start_index = suggest.find(placeholder_start).unwrap(); + let end_index = suggest[start_index..].find(placeholder_end).unwrap() + + start_index + + placeholder_end.len(); + + let placeholder = start_index + placeholder_start.len()..end_index - placeholder_end.len(); let range = suggest[placeholder.to_owned()].trim_matches(|c| c == '[' || c == ']'); if let Some((start, end)) = range.split_once(':') { - let start = match start { - "" => 0, - _ => start.parse::().unwrap(), - }; - let end = match end { - "" => last_command.split_whitespace().count(), - _ => end.parse::().unwrap(), - }; let split_command = last_command.split_whitespace().collect::>(); + let start = start.parse::().unwrap_or(0); + let end = end.parse::().unwrap_or(split_command.len() - 1) + 1; let command = split_command[start..end].join(" "); - suggest = suggest.replace(&suggest[placeholder], &command); + + // let command = match start == end { + // true => split_command[start].to_owned(), + // false => split_command[start..end].join(" ") + // }; + suggest = suggest.replace(&suggest[start_index..end_index], &command); } else { - let range = range.parse::().unwrap(); - let split_command = last_command.split_whitespace().collect::>(); + let range = range.parse::().unwrap_or(0); + let split_command = suggest.split_whitespace().collect::>(); let command = split_command[range].to_owned(); - suggest = suggest.replace(&suggest[placeholder], &command); + suggest = suggest.replace(&suggest[start_index..end_index], &command); } } + while suggest.contains("{{typo") { + let placeholder_start = "{{typo"; + let placeholder_end = "}}"; + + let start_index = suggest.find(placeholder_start).unwrap(); + let end_index = suggest[start_index..].find(placeholder_end).unwrap() + + start_index + + placeholder_end.len(); + + let placeholder = start_index + placeholder_start.len()..end_index - placeholder_end.len(); + + let mut command_index = 0; + let mut match_list = vec![]; + if suggest.contains('[') { + let split = suggest[placeholder.to_owned()] + .split(&['[', ']']) + .collect::>(); + command_index = split[1].parse::().unwrap(); + } + if suggest.contains('(') { + let split = suggest[placeholder.to_owned()] + .split(&['(', ')']) + .collect::>(); + match_list = split[1].split(',').collect::>(); + } + + let command = last_command.split_whitespace().collect::>()[command_index]; + let match_list = match_list + .iter() + .map(|s| s.to_string()) + .collect::>(); + let suggestion = suggest_typo(command, match_list.clone()); + + suggest = suggest.replace(&suggest[start_index..end_index], &suggestion); + } + suggest } +fn suggest_typo(typo: &str, candidates: Vec) -> String { + let mut suggestion = typo.to_owned(); + + if candidates.len() == 1 { + match candidates[0].as_str() { + "path" => { + let path_files = get_path_files(); + if let Some(suggest) = find_fimilar(typo, path_files) { + suggestion = suggest; + } + } + "file" => { + unimplemented!(); + } + _ => {} + } + } else if let Some(suggest) = find_fimilar(typo, candidates) { + suggestion = suggest; + } + + suggestion +} + +fn get_path_files() -> Vec { + let path = std::env::var("PATH").unwrap(); + let path = path.split(':').collect::>(); + // get all executable files in $PATH + let mut all_executable = vec![]; + for p in path { + let files = match std::fs::read_dir(p) { + Ok(files) => files, + Err(_) => continue, + }; + for file in files { + let file = file.unwrap(); + let file_name = file.file_name().into_string().unwrap(); + all_executable.push(file_name); + } + } + all_executable +} + +fn find_fimilar(typo: &str, candidates: Vec) -> Option { + let mut min_distance = 10; + let mut min_distance_index = None; + for (i, candidate) in candidates.iter().enumerate() { + let distance = compare_string(typo, candidate); + if distance < min_distance { + min_distance = distance; + min_distance_index = Some(i); + } + } + if let Some(min_distance_index) = min_distance_index { + return Some(candidates[min_distance_index].to_string()); + } + None +} + +// warning disable +#[allow(clippy::needless_range_loop)] +fn compare_string(a: &str, b: &str) -> usize { + let mut matrix = vec![vec![0; b.chars().count() + 1]; a.chars().count() + 1]; + + for i in 0..a.chars().count() + 1 { + matrix[i][0] = i; + } + for j in 0..b.chars().count() + 1 { + matrix[0][j] = j; + } + + for (i, ca) in a.chars().enumerate() { + for (j, cb) in b.chars().enumerate() { + let cost = if ca == cb { 0 } else { 1 }; + matrix[i + 1][j + 1] = std::cmp::min( + std::cmp::min(matrix[i][j + 1] + 1, matrix[i + 1][j] + 1), + matrix[i][j] + cost, + ); + } + } + matrix[a.chars().count()][b.chars().count()] +} + pub fn confirm_correction(shell: &str, command: &str, last_command: &str) { println!( "Did you mean {}?", diff --git a/src/main.rs b/src/main.rs index aa9fddb..de6dd67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,10 @@ fn main() { if let Some(corrected_command) = corrected_command { corrections::confirm_correction(&shell, &corrected_command, &last_command); } else { - println!("No correction found for the command: {}\n", last_command.red().bold()); + println!( + "No correction found for the command: {}\n", + last_command.red().bold() + ); println!("If you think there should be a correction, please open an issue or send a pull request!"); } } diff --git a/src/shell.rs b/src/shell.rs index 7fdd0f6..03c8231 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,4 +1,4 @@ -use std::{process::exit}; +use std::process::exit; pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"]; @@ -45,12 +45,11 @@ pub fn last_command_expanded_alias(shell: &str) -> String { } let split_command = last_command.split_whitespace().collect::>(); - let command; - if PRIVILEGE_LIST.contains(&split_command[0]) { - command = split_command[1]; + let command = if PRIVILEGE_LIST.contains(&split_command[0]) { + split_command[1] } else { - command = split_command[0]; - } + split_command[0] + }; let mut expanded_command = command.to_string();