diff --git a/src/corrections.rs b/src/corrections.rs index 9b03cdc..e69de29 100644 --- a/src/corrections.rs +++ b/src/corrections.rs @@ -1,382 +0,0 @@ -use std::collections::HashMap; -use regex_lite::Regex; - -use rule_parser::parse_rules; - -use crate::shell::{command_output, PRIVILEGE_LIST}; -use crate::style::highlight_difference; - -pub fn correct_command(shell: &str, last_command: &str) -> Option { - let err = command_output(shell, last_command); - - let split_command = split_command(last_command); - let executable = match PRIVILEGE_LIST.contains(&split_command[0].as_str()) { - true => split_command.get(1).expect("No command found.").as_str(), - false => split_command.first().expect("No command found.").as_str(), - }; - - if !PRIVILEGE_LIST.contains(&executable) { - let suggest = match_pattern("privilege", last_command, &err); - if let Some(suggest) = suggest { - let suggest = eval_suggest(&suggest, last_command); - return Some(suggest); - } - } - let suggest = match_pattern(executable, last_command, &err); - if let Some(suggest) = suggest { - let suggest = eval_suggest(&suggest, last_command); - if PRIVILEGE_LIST.contains(&executable) { - return Some(format!("{} {}", split_command[0], suggest)); - } - return Some(suggest); - } - - let suggest = match_pattern("general", last_command, &err); - if let Some(suggest) = suggest { - let suggest = eval_suggest(&suggest, last_command); - return Some(suggest); - } - None -} - -fn match_pattern(executable: &str, command: &str, error_msg: &str) -> Option { - let rules = parse_rules!("rules"); - if rules.contains_key(executable) { - let suggest = rules.get(executable).unwrap(); - for (pattern, suggest) in suggest { - for pattern in pattern { - if error_msg.contains(pattern) { - for suggest in suggest { - if let Some(suggest) = check_condition(suggest, command, error_msg) { - return Some(suggest); - } - } - } - } - } - None - } else { - None - } -} - -fn check_condition(suggest: &str, command: &str, error_msg: &str) -> Option { - if !suggest.starts_with('#') { - return Some(suggest.to_owned()); - } - let mut lines = suggest.lines().collect::>(); - let mut conditions = String::new(); - for (i, line) in lines[0..].iter().enumerate() { - conditions.push_str(line); - if line.ends_with(']') { - lines = lines[i..].to_vec(); - break; - } - } - let conditions = conditions - .trim_start_matches(['#', '[']) - .trim_end_matches(']') - .split(',') - .collect::>(); - - for condition in conditions { - let (mut condition, arg) = condition.split_once('(').unwrap(); - condition = condition.trim(); - let arg = arg.trim_start_matches('(').trim_end_matches(')'); - let reverse = match condition.starts_with('!') { - true => { - condition = condition.trim_start_matches('!'); - true - } - false => false, - }; - if eval_condition(condition, arg, command, error_msg) == reverse { - return None; - } - } - Some(lines[1..].join("\n")) -} - -fn eval_condition(condition: &str, arg: &str, command: &str, error_msg: &str) -> bool { - match condition { - "executable" => { - let output = std::process::Command::new("which") - .arg(arg) - .output() - .expect("failed to execute process"); - output.status.success() - } - "err_contains" => error_msg.contains(arg), - "cmd_contains" => command.contains(arg), - _ => unreachable!("Unknown condition when evaluation condition: {}", condition), - } -} - -fn eval_suggest(suggest: &str, last_command: &str) -> String { - let mut suggest = suggest.to_owned(); - let mut last_command = last_command.to_owned(); - if suggest.contains("{{command}}") { - suggest = suggest.replace("{{command}}", &last_command); - } - - while suggest.contains("{{opt::") { - let placeholder_start = "{{opt::"; - 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..end_index; - - let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); - let opt = &suggest[args.to_owned()]; - let regex = opt.trim(); - let regex = Regex::new(regex).unwrap(); - let opts = regex - .find_iter(&last_command) - .map(|cap| cap.as_str().to_owned()) - .collect::>(); - - suggest.replace_range(placeholder, &opts.join(" ")); - for opt in opts { - last_command = last_command.replace(&opt, ""); - } - } - - let split_command = split_command(&last_command); - - while suggest.contains("{{command") { - let placeholder_start = "{{command"; - 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..end_index; - - let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); - let range = suggest[args.to_owned()].trim_matches(|c| c == '[' || c == ']'); - if let Some((start, end)) = range.split_once(':') { - let start = { - let mut start = start.parse::().unwrap_or(0); - if start < 0 { - start += split_command.len() as i32; - } - start as usize - }; - let end = { - let mut end = end.parse::().unwrap_or(split_command.len() as i32 - 1) + 1; - if end < 0 { - end += split_command.len() as i32; - } - end as usize - }; - let command = split_command[start..end].join(" "); - - suggest.replace_range(placeholder, &command); - } else { - let range = range.parse::().unwrap_or(0); - let command = split_command[range].to_owned(); - suggest.replace_range(placeholder, &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..end_index; - let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); - - let mut command_index; - let mut match_list = vec![]; - if suggest.contains('[') { - let split = suggest[args.to_owned()] - .split(&['[', ']']) - .collect::>(); - command_index = split[1].parse::().unwrap(); - if command_index < 0 { - command_index += split_command.len() as i32; - } - } else { - unreachable!("Typo suggestion must have a command index"); - } - if suggest.contains('(') { - let split = suggest[args.to_owned()] - .split(&['(', ')']) - .collect::>(); - match_list = split[1].split(',').collect::>(); - } - - let match_list = match_list - .iter() - .map(|s| s.trim().to_string()) - .collect::>(); - let command_index = command_index as usize; - let suggestion = suggest_typo(&split_command[command_index], match_list.clone()); - - suggest.replace_range(placeholder, &suggestion); - } - - suggest -} - -pub fn split_command(command: &str) -> Vec { - let regex = r#"([^\s"\\]+|"(?:\\.|[^"\\])*"|\\.)+"#; - let regex = Regex::new(regex).unwrap(); - let split_command = regex - .find_iter(command) - .map(|cap| cap.as_str().to_owned()) - .collect::>(); - split_command -} - -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" => { - let files = get_directory_files(typo); - if let Some(suggest) = find_fimilar(typo, files) { - suggestion = suggest; - } - } - _ => {} - } - } 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::>(); - 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 get_directory_files(input: &str) -> Vec { - let mut input = input.trim_matches(|c| c == '\'' || c == '"').to_owned(); - let files = loop { - match std::fs::read_dir(&input) { - Ok(files) => break files, - Err(_) => { - if let Some((dirs, _)) = input.rsplit_once('/') { - input = dirs.to_owned(); - } else { - break std::fs::read_dir("./").unwrap() - } - } - } - }; - - let mut all_files = vec![]; - for file in files { - let file = file.unwrap(); - let file_name = file.path().to_str().unwrap().to_owned(); - all_files.push(file_name); - } - all_files -} - -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 {}?", - highlight_difference(command, last_command) - ); - println!("Press enter to execute the corrected command. Or press Ctrl+C to exit."); - std::io::stdin().read_line(&mut String::new()).unwrap(); - - for p in PRIVILEGE_LIST { - let _p = p.to_owned() + " "; - if command.starts_with(&_p) { - let command = command.replace(p, ""); - std::process::Command::new(p) - .arg(shell) - .arg("-c") - .arg(command) - .spawn() - .expect("failed to execute process") - .wait() - .expect("failed to wait on process"); - return; - } - } - - std::process::Command::new(shell) - .arg("-c") - .arg(command) - .spawn() - .expect("failed to execute process") - .wait() - .expect("failed to wait on process"); -} diff --git a/src/main.rs b/src/main.rs index 0936de4..56a540e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use colored::Colorize; mod args; -mod corrections; mod shell; mod style; +mod suggestions; fn main() { std::env::set_var("LC_ALL", "C"); @@ -13,18 +13,18 @@ fn main() { "No _PR_SHELL in environment. Did you aliased the binary with the correct arguments?", ); let last_command = shell::last_command_expanded_alias(&shell); - let corrected_command = corrections::correct_command(&shell, &last_command); + let corrected_command = suggestions::suggest_command(&shell, &last_command); if let Some(corrected_command) = corrected_command { if corrected_command != last_command { - corrections::confirm_correction(&shell, &corrected_command, &last_command); + suggestions::confirm_suggestion(&shell, &corrected_command, &last_command); return; } } println!( "No correction found for the command: {}\n", - last_command.red().bold() + last_command.red() ); println!( "If you think there should be a correction, please open an issue or send a pull request!" diff --git a/src/style.rs b/src/style.rs index 13f7461..a10715d 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,32 +1,31 @@ -use crate::corrections::split_command; +use crate::suggestions::split_command; use colored::*; -pub fn highlight_difference(corrected_command: &str, last_command: &str) -> String { - let mut highlighted_command = String::new(); - - let split_corrected_command = split_command(corrected_command); +pub fn highlight_difference(suggested_command: &str, last_command: &str) -> String { + let split_suggested_command = split_command(suggested_command); let split_last_command = split_command(last_command); - for new in split_corrected_command { - if new.is_empty() { + let mut old_entries = Vec::new(); + for command in &split_suggested_command { + if command.is_empty() { continue; } - let mut changed = true; - for old in split_last_command.clone() { - if new == old { - changed = false; - break; + 'old: for old in split_last_command.clone() { + if command == &old { + old_entries.push(command.clone()); + break 'old; } } - if changed { - let colored = new.red().bold(); - highlighted_command = format!("{}{}", highlighted_command, colored); - } else { - let colored = new.green(); - highlighted_command = format!("{}{}", highlighted_command, colored); - } - highlighted_command.push(' '); } - highlighted_command.trim().to_string() + let mut highlighted = suggested_command.to_string(); + for entry in &split_suggested_command { + if old_entries.contains(entry) { + highlighted = highlighted.replace(entry, &entry.cyan()); + } else { + highlighted = highlighted.replace(entry, &entry.red().bold()); + } + } + + highlighted } diff --git a/src/suggestions.rs b/src/suggestions.rs new file mode 100644 index 0000000..beb8a7e --- /dev/null +++ b/src/suggestions.rs @@ -0,0 +1,378 @@ +use regex_lite::Regex; +use std::collections::HashMap; + +use rule_parser::parse_rules; + +use crate::shell::{command_output, PRIVILEGE_LIST}; +use crate::style::highlight_difference; + +pub fn suggest_command(shell: &str, last_command: &str) -> Option { + let err = command_output(shell, last_command); + + let split_command = split_command(last_command); + let executable = match PRIVILEGE_LIST.contains(&split_command[0].as_str()) { + true => split_command.get(1).expect("No command found.").as_str(), + false => split_command.first().expect("No command found.").as_str(), + }; + + if !PRIVILEGE_LIST.contains(&executable) { + let suggest = match_pattern("privilege", last_command, &err); + if let Some(suggest) = suggest { + let suggest = eval_suggest(&suggest, last_command); + return Some(suggest); + } + } + let suggest = match_pattern(executable, last_command, &err); + if let Some(suggest) = suggest { + let suggest = eval_suggest(&suggest, last_command); + if PRIVILEGE_LIST.contains(&executable) { + return Some(format!("{} {}", split_command[0], suggest)); + } + return Some(suggest); + } + + let suggest = match_pattern("general", last_command, &err); + if let Some(suggest) = suggest { + let suggest = eval_suggest(&suggest, last_command); + return Some(suggest); + } + None +} + +fn match_pattern(executable: &str, command: &str, error_msg: &str) -> Option { + let rules = parse_rules!("rules"); + if rules.contains_key(executable) { + let suggest = rules.get(executable).unwrap(); + for (pattern, suggest) in suggest { + for pattern in pattern { + if error_msg.contains(pattern) { + for suggest in suggest { + if let Some(suggest) = check_condition(suggest, command, error_msg) { + return Some(suggest); + } + } + } + } + } + None + } else { + None + } +} + +fn check_condition(suggest: &str, command: &str, error_msg: &str) -> Option { + if !suggest.starts_with('#') { + return Some(suggest.to_owned()); + } + let mut lines = suggest.lines().collect::>(); + let mut conditions = String::new(); + for (i, line) in lines[0..].iter().enumerate() { + conditions.push_str(line); + if line.ends_with(']') { + lines = lines[i..].to_vec(); + break; + } + } + let conditions = conditions + .trim_start_matches(['#', '[']) + .trim_end_matches(']') + .split(',') + .collect::>(); + + for condition in conditions { + let (mut condition, arg) = condition.split_once('(').unwrap(); + condition = condition.trim(); + let arg = arg.trim_start_matches('(').trim_end_matches(')'); + let reverse = match condition.starts_with('!') { + true => { + condition = condition.trim_start_matches('!'); + true + } + false => false, + }; + if eval_condition(condition, arg, command, error_msg) == reverse { + return None; + } + } + Some(lines[1..].join("\n")) +} + +fn eval_condition(condition: &str, arg: &str, command: &str, error_msg: &str) -> bool { + match condition { + "executable" => { + let output = std::process::Command::new("which") + .arg(arg) + .output() + .expect("failed to execute process"); + output.status.success() + } + "err_contains" => error_msg.contains(arg), + "cmd_contains" => command.contains(arg), + _ => unreachable!("Unknown condition when evaluation condition: {}", condition), + } +} + +fn eval_suggest(suggest: &str, last_command: &str) -> String { + let mut suggest = suggest.to_owned(); + let mut last_command = last_command.to_owned(); + if suggest.contains("{{command}}") { + suggest = suggest.replace("{{command}}", &last_command); + } + + while suggest.contains("{{opt::") { + let placeholder_start = "{{opt::"; + 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..end_index; + + let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); + let opt = &suggest[args.to_owned()]; + let regex = opt.trim(); + let regex = Regex::new(regex).unwrap(); + let opts = regex + .find_iter(&last_command) + .map(|cap| cap.as_str().to_owned()) + .collect::>(); + + suggest.replace_range(placeholder, &opts.join(" ")); + for opt in opts { + last_command = last_command.replace(&opt, ""); + } + } + + let split_command = split_command(&last_command); + + while suggest.contains("{{command") { + let placeholder_start = "{{command"; + 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..end_index; + + let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); + let range = suggest[args.to_owned()].trim_matches(|c| c == '[' || c == ']'); + if let Some((start, end)) = range.split_once(':') { + let start = { + let mut start = start.parse::().unwrap_or(0); + if start < 0 { + start += split_command.len() as i32; + } + start as usize + }; + let end = { + let mut end = end.parse::().unwrap_or(split_command.len() as i32 - 1) + 1; + if end < 0 { + end += split_command.len() as i32; + } + end as usize + }; + let command = split_command[start..end].join(" "); + + suggest.replace_range(placeholder, &command); + } else { + let range = range.parse::().unwrap_or(0); + let command = split_command[range].to_owned(); + suggest.replace_range(placeholder, &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..end_index; + let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); + + let mut command_index; + let mut match_list = vec![]; + if suggest.contains('[') { + let split = suggest[args.to_owned()] + .split(&['[', ']']) + .collect::>(); + command_index = split[1].parse::().unwrap(); + if command_index < 0 { + command_index += split_command.len() as i32; + } + } else { + unreachable!("Typo suggestion must have a command index"); + } + if suggest.contains('(') { + let split = suggest[args.to_owned()] + .split(&['(', ')']) + .collect::>(); + match_list = split[1].split(',').collect::>(); + } + + let match_list = match_list + .iter() + .map(|s| s.trim().to_string()) + .collect::>(); + let command_index = command_index as usize; + let suggestion = suggest_typo(&split_command[command_index], match_list.clone()); + + suggest.replace_range(placeholder, &suggestion); + } + + suggest +} + +pub fn split_command(command: &str) -> Vec { + let regex = r#"([^\s"\\]+|"(?:\\.|[^"\\])*"|\\.)+"#; + let regex = Regex::new(regex).unwrap(); + let split_command = regex + .find_iter(command) + .map(|cap| cap.as_str().to_owned()) + .collect::>(); + split_command +} + +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" => { + let files = get_directory_files(typo); + if let Some(suggest) = find_fimilar(typo, files) { + suggestion = suggest; + } + } + _ => {} + } + } 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::>(); + 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 get_directory_files(input: &str) -> Vec { + let mut input = input.trim_matches(|c| c == '\'' || c == '"').to_owned(); + let files = loop { + match std::fs::read_dir(&input) { + Ok(files) => break files, + Err(_) => { + if let Some((dirs, _)) = input.rsplit_once('/') { + input = dirs.to_owned(); + } else { + break std::fs::read_dir("./").unwrap(); + } + } + } + }; + + let mut all_files = vec![]; + for file in files { + let file = file.unwrap(); + let file_name = file.path().to_str().unwrap().to_owned(); + all_files.push(file_name); + } + all_files +} + +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 +} + +#[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_suggestion(shell: &str, command: &str, last_command: &str) { + println!("{}\n", highlight_difference(command, last_command)); + println!("Press enter to execute the suggestion. Or press Ctrl+C to exit."); + std::io::stdin().read_line(&mut String::new()).unwrap(); + + for p in PRIVILEGE_LIST { + let _p = p.to_owned() + " "; + if command.starts_with(&_p) { + let command = command.replace(p, ""); + std::process::Command::new(p) + .arg(shell) + .arg("-c") + .arg(command) + .spawn() + .expect("failed to execute process") + .wait() + .expect("failed to wait on process"); + return; + } + } + + std::process::Command::new(shell) + .arg("-c") + .arg(command) + .spawn() + .expect("failed to execute process") + .wait() + .expect("failed to wait on process"); +}