From 5aebf867c118ea4b2b08015d13662e4c32449cfe Mon Sep 17 00:00:00 2001 From: iff Date: Sun, 8 Dec 2024 15:08:21 +0100 Subject: [PATCH] refactor: split runtime rules into module --- Cargo.lock | 20 +- Cargo.toml | 7 +- pay-respects-module-runtime-rules/Cargo.toml | 11 ++ pay-respects-module-runtime-rules/src/main.rs | 16 ++ .../src/replaces.rs | 186 ++++++++++++++++++ .../src/rules.rs | 43 ++-- pay-respects-parser/src/lib.rs | 4 +- pay-respects-utils/Cargo.toml | 8 + pay-respects-utils/src/evals.rs | 155 +++++++++++++++ .../src/files.rs | 3 +- pay-respects-utils/src/lib.rs | 2 + pay-respects/Cargo.toml | 1 + pay-respects/src/main.rs | 6 - pay-respects/src/modes.rs | 6 +- pay-respects/src/replaces.rs | 2 +- pay-respects/src/rules.rs | 11 +- pay-respects/src/shell.rs | 103 ++++++---- pay-respects/src/style.rs | 2 +- pay-respects/src/suggestions.rs | 167 +--------------- pay-respects/src/system.rs | 10 +- 20 files changed, 514 insertions(+), 249 deletions(-) create mode 100644 pay-respects-module-runtime-rules/Cargo.toml create mode 100644 pay-respects-module-runtime-rules/src/main.rs create mode 100644 pay-respects-module-runtime-rules/src/replaces.rs rename pay-respects/src/runtime_rules.rs => pay-respects-module-runtime-rules/src/rules.rs (89%) create mode 100644 pay-respects-utils/Cargo.toml create mode 100644 pay-respects-utils/src/evals.rs rename {pay-respects => pay-respects-utils}/src/files.rs (98%) create mode 100644 pay-respects-utils/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6f50fe7..5a6ed81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,7 @@ dependencies = [ "curl", "inquire", "pay-respects-parser", + "pay-respects-utils", "regex-lite", "rust-i18n", "serde", @@ -483,11 +484,19 @@ dependencies = [ "toml 0.8.19", ] +[[package]] +name = "pay-respects-module-runtime-rules" +version = "0.1.0" +dependencies = [ + "pay-respects-utils", + "regex-lite", + "serde", + "toml 0.8.19", +] + [[package]] name = "pay-respects-parser" version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6304475497712e6fbcbb0f947c4958ae336f1dcc3d7c8f470664a20e9ef175" dependencies = [ "proc-macro2", "quote", @@ -496,6 +505,13 @@ dependencies = [ "toml 0.8.19", ] +[[package]] +name = "pay-respects-utils" +version = "0.1.0" +dependencies = [ + "regex-lite", +] + [[package]] name = "pkg-config" version = "0.3.31" diff --git a/Cargo.toml b/Cargo.toml index 775652a..8dbf10a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,11 @@ resolver = "2" members = [ "pay-respects", "pay-respects-parser", + "pay-respects-utils", # optional modules - # "pay-respects-module-runtime-rules", - # "pay-respects-module-request-ai" -] + "pay-respects-module-runtime-rules", + # "pay-respects-module-request-ai", + ] [profile.release] strip = true diff --git a/pay-respects-module-runtime-rules/Cargo.toml b/pay-respects-module-runtime-rules/Cargo.toml new file mode 100644 index 0000000..9062594 --- /dev/null +++ b/pay-respects-module-runtime-rules/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pay-respects-module-runtime-rules" +version = "0.1.0" +edition = "2021" + +[dependencies] +regex-lite = "0.1" + +toml = { version = "0.8" } +serde = { version = "1.0", features = ["derive"] } +pay-respects-utils = {version = "0.1.0", path = "../pay-respects-utils"} diff --git a/pay-respects-module-runtime-rules/src/main.rs b/pay-respects-module-runtime-rules/src/main.rs new file mode 100644 index 0000000..4b5309c --- /dev/null +++ b/pay-respects-module-runtime-rules/src/main.rs @@ -0,0 +1,16 @@ +mod replaces; +mod rules; + +fn main() -> Result<(), std::io::Error>{ + let executable = std::env::var("_PR_COMMAND").unwrap(); + let shell = std::env::var("_PR_SHELL").unwrap(); + let last_command = std::env::var("_PR_LAST_COMMAND").unwrap(); + let error_msg = std::env::var("_PR_ERROR_MSG").unwrap(); + let executables: Vec = { + let executables = std::env::var("_PR_EXECUTABLES").unwrap(); + executables.split(",").map(|s| s.to_string()).collect() + }; + + rules::runtime_match(&executable, &shell, &last_command, &error_msg, &executables); + Ok(()) +} diff --git a/pay-respects-module-runtime-rules/src/replaces.rs b/pay-respects-module-runtime-rules/src/replaces.rs new file mode 100644 index 0000000..d5646fc --- /dev/null +++ b/pay-respects-module-runtime-rules/src/replaces.rs @@ -0,0 +1,186 @@ +use pay_respects_utils::evals::*; + +fn tag(name: &str, x: i32) -> String { + format!("{{{}{}}}", name, x) +} + +pub fn eval_placeholder( + string: &str, + start: &str, + end: &str, +) -> (std::ops::Range, std::ops::Range) { + let start_index = string.find(start).unwrap(); + let end_index = string[start_index..].find(end).unwrap() + start_index + end.len(); + + let placeholder = start_index..end_index; + + let args = start_index + start.len()..end_index - end.len(); + + (placeholder, args) +} + +pub fn opts(suggest: &mut String, last_command: &mut String, opt_list: &mut Vec<(String, String)>) { + let mut replace_tag = 0; + let tag_name = "opts"; + + while suggest.contains(" {{opt::") { + let (placeholder, args) = eval_placeholder(suggest, " {{opt::", "}}"); + + let opt = &suggest[args.to_owned()]; + let regex = opt.trim(); + let current_tag = tag(tag_name, replace_tag); + + opt_list.push((current_tag.clone(), opt_regex(regex, last_command))); + suggest.replace_range(placeholder, ¤t_tag); + + replace_tag += 1; + } +} + +pub fn cmd_reg(suggest: &mut String, last_command: &str) { + while suggest.contains("{{cmd::") { + let (placeholder, args) = eval_placeholder(suggest, "{{cmd::", "}}"); + + let regex = suggest[args.to_owned()].trim(); + + let command = cmd_regex(regex, last_command); + suggest.replace_range(placeholder, &command) + } +} + +pub fn err(suggest: &mut String, error_msg: &str) { + while suggest.contains("{{err::") { + let (placeholder, args) = eval_placeholder(suggest, "{{err::", "}}"); + + let regex = suggest[args.to_owned()].trim(); + + let command = err_regex(regex, error_msg); + suggest.replace_range(placeholder, &command) + } +} + +pub fn command(suggest: &mut String, split_command: &[String]) { + while suggest.contains("{{command") { + let (placeholder, args) = eval_placeholder(suggest, "{{command", "}}"); + + let range = suggest[args.to_owned()].trim_matches(|c| c == '[' || c == ']'); + if let Some((start, end)) = range.split_once(':') { + let mut start_index = start.parse::().unwrap_or(0); + if start_index < 0 { + start_index += split_command.len() as i32; + }; + let mut end_index; + let parsed_end = end.parse::(); + if parsed_end.is_err() { + end_index = split_command.len() as i32; + } else { + end_index = parsed_end.unwrap(); + if end_index < 0 { + end_index += split_command.len() as i32 + 1; + } else { + end_index += 1; + } + }; + + let command = split_command[start_index as usize..end_index as usize].join(" "); + + suggest.replace_range(placeholder, &command); + } else { + let range = range.parse::().unwrap_or(0); + let command = &split_command[range]; + + suggest.replace_range(placeholder, command); + } + } +} + +pub fn typo(suggest: &mut String, split_command: &[String], executables: &[String], shell: &str) { + while suggest.contains("{{typo") { + let (placeholder, args) = eval_placeholder(suggest, "{{typo", "}}"); + + let index = if suggest.contains('[') { + let split = suggest[args.to_owned()] + .split(&['[', ']']) + .collect::>(); + let command_index = split[1]; + if !command_index.contains(':') { + let command_index = command_index.parse::().unwrap(); + + let index = if command_index < 0 { + split_command.len() as i32 + command_index + } else { + command_index + }; + index as usize..index as usize + 1 + } else { + let (start, end) = command_index.split_once(':').unwrap(); + let start = start.parse::().unwrap_or(0); + let start_index = if start < 0 { + split_command.len() as i32 + start + } else { + start + }; + let end = end.parse::(); + let end_index = if end.is_err() { + split_command.len() as i32 + } else { + let end = end.unwrap(); + if end < 0 { + split_command.len() as i32 + end + 1 + } else { + end + 1 + } + }; + + start_index as usize..end_index as usize + } + } else { + unreachable!("Typo suggestion must have a command index"); + }; + + let match_list = if suggest.contains('(') { + let split = suggest[args.to_owned()] + .split_once("(") + .unwrap() + .1 + .rsplit_once(")") + .unwrap() + .0; + split.split(',').collect::>() + } else { + unreachable!("Typo suggestion must have a match list"); + }; + + let match_list = match_list + .iter() + .map(|s| s.trim().to_string()) + .collect::>(); + + let command = if match_list[0].starts_with("{{shell") { + let function = match_list.join(","); + let (_, args) = eval_placeholder(&function, "{{shell", "}}"); + let function = &function[args.to_owned()].trim_matches(|c| c == '(' || c == ')'); + suggest_typo( + &split_command[index], + eval_shell_command(shell, function), + executables, + ) + } else { + suggest_typo(&split_command[index], match_list, executables) + }; + + suggest.replace_range(placeholder, &command); + } +} + +pub fn shell(suggest: &mut String, shell: &str) { + while suggest.contains("{{shell") { + let (placeholder, args) = eval_placeholder(suggest, "{{shell", "}}"); + let range = suggest[args.to_owned()].trim_matches(|c| c == '(' || c == ')'); + + let command = eval_shell_command(shell, range); + + suggest.replace_range(placeholder, &command.join("\n")); + } +} + diff --git a/pay-respects/src/runtime_rules.rs b/pay-respects-module-runtime-rules/src/rules.rs similarity index 89% rename from pay-respects/src/runtime_rules.rs rename to pay-respects-module-runtime-rules/src/rules.rs index cf14c93..be15d31 100644 --- a/pay-respects/src/runtime_rules.rs +++ b/pay-respects-module-runtime-rules/src/rules.rs @@ -1,6 +1,5 @@ use crate::replaces; -use crate::shell::Data; -use crate::suggestions::*; +use pay_respects_utils::evals::*; #[derive(serde::Deserialize)] struct Rule { @@ -13,7 +12,13 @@ struct MatchError { suggest: Vec, } -pub fn runtime_match(executable: &str, data: &mut Data) { +pub fn runtime_match( + executable: &str, + shell: &str, + last_command: &str, + error_msg: &str, + executables: &[String], +) { let file = get_rule(executable); if file.is_none() { return; @@ -21,11 +26,7 @@ pub fn runtime_match(executable: &str, data: &mut Data) { let file = std::fs::read_to_string(file.unwrap()).unwrap(); let rule: Rule = toml::from_str(&file).unwrap(); - let split_command = &data.split.clone(); - let shell = &data.shell.clone(); - let last_command = &data.command.clone(); - let error_msg = &data.error.clone(); - let executables = &data.get_executables().clone(); + let split_command = split_command(last_command); let mut pure_suggest; @@ -66,8 +67,8 @@ pub fn runtime_match(executable: &str, data: &mut Data) { shell, last_command, error_msg, - split_command, - data, + &split_command, + executables, ) == reverse { continue 'suggest; @@ -82,13 +83,16 @@ pub fn runtime_match(executable: &str, data: &mut Data) { if pure_suggest.contains("{{command}}") { pure_suggest = pure_suggest.replace("{{command}}", last_command); } - data.add_candidate(&eval_suggest( - &pure_suggest, - last_command, - error_msg, - executables, - shell, - )); + print!("{}", + eval_suggest( + &pure_suggest, + last_command, + error_msg, + executables, + shell, + ) + ); + print!("{}", "<_PR_BR>"); } } } @@ -102,10 +106,10 @@ fn eval_condition( last_command: &str, error_msg: &str, split_command: &[String], - data: &mut Data, + executables: &[String], ) -> bool { match condition { - "executable" => data.has_executable(arg), + "executable" => executables.contains(&arg.to_string()), "err_contains" => error_msg.contains(arg), "cmd_contains" => last_command.contains(arg), "min_length" => split_command.len() >= arg.parse::().unwrap(), @@ -190,3 +194,4 @@ fn get_rule(executable: &str) -> Option { None } + diff --git a/pay-respects-parser/src/lib.rs b/pay-respects-parser/src/lib.rs index 12c1700..6325918 100644 --- a/pay-respects-parser/src/lib.rs +++ b/pay-respects-parser/src/lib.rs @@ -184,7 +184,7 @@ fn parse_conditions(suggest: &str) -> (String, Vec) { fn eval_condition(condition: &str, arg: &str) -> TokenStream2 { match condition { - "executable" => quote! {data.has_executable(#arg)}, + "executable" => quote! {executables.contains(&#arg.to_string())}, "err_contains" => quote! {error_msg.contains(#arg)}, "cmd_contains" => quote! {last_command.contains(#arg)}, "min_length" => quote! {(split.len() >= #arg.parse::().unwrap())}, @@ -215,6 +215,6 @@ fn eval_suggest(suggest: &str) -> TokenStream2 { quote! { #(#opt_list)* - data.add_candidate(&format!{#suggest, #(#replace_list),*}); + candidates.push(format!{#suggest, #(#replace_list),*}); } } diff --git a/pay-respects-utils/Cargo.toml b/pay-respects-utils/Cargo.toml new file mode 100644 index 0000000..0edc536 --- /dev/null +++ b/pay-respects-utils/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pay-respects-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +regex-lite = "0.1" + diff --git a/pay-respects-utils/src/evals.rs b/pay-respects-utils/src/evals.rs new file mode 100644 index 0000000..daf6fca --- /dev/null +++ b/pay-respects-utils/src/evals.rs @@ -0,0 +1,155 @@ +use regex_lite::Regex; +use crate::files::*; + +pub fn opt_regex(regex: &str, command: &mut String) -> String { + let regex = Regex::new(regex).unwrap(); + + let mut opts = Vec::new(); + for captures in regex.captures_iter(command) { + for cap in captures.iter().skip(1).flatten() { + opts.push(cap.as_str().to_owned()); + } + } + + for opt in opts.clone() { + *command = command.replace(&opt, ""); + } + opts.join(" ") +} + +pub fn err_regex(regex: &str, error_msg: &str) -> String { + let regex = Regex::new(regex).unwrap(); + + let mut err = Vec::new(); + for captures in regex.captures_iter(error_msg) { + for cap in captures.iter().skip(1).flatten() { + err.push(cap.as_str().to_owned()); + } + } + err.join(" ") +} + +pub fn cmd_regex(regex: &str, command: &str) -> String { + let regex = Regex::new(regex).unwrap(); + + let mut cmd = Vec::new(); + for captures in regex.captures_iter(command) { + for cap in captures.iter().skip(1).flatten() { + cmd.push(cap.as_str().to_owned()); + } + } + cmd.join(" ") +} + +pub fn eval_shell_command(shell: &str, command: &str) -> Vec { + let output = std::process::Command::new(shell) + .arg("-c") + .arg(command) + .output() + .expect("failed to execute process"); + let output = String::from_utf8_lossy(&output.stdout); + let split_output = output.split('\n').collect::>(); + split_output + .iter() + .map(|s| s.trim().to_string()) + .collect::>() +} + +pub fn split_command(command: &str) -> Vec { + if cfg!(debug_assertions) { + eprintln!("command: {command}") + } + // this regex splits the command separated by spaces, except when the space + // is escaped by a backslash or surrounded by quotes + let regex = r#"([^\s"'\\]+|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\\ )+|\\|\n"#; + let regex = Regex::new(regex).unwrap(); + let split_command = regex + .find_iter(command) + .map(|cap| cap.as_str().to_owned()) + .collect::>(); + split_command +} + +pub fn suggest_typo(typos: &[String], candidates: Vec, executables: &[String]) -> String { + let mut suggestions = Vec::new(); + for typo in typos { + let typo = typo.as_str(); + if candidates.len() == 1 { + match candidates[0].as_str() { + "path" => { + if let Some(suggest) = find_similar(typo, executables, Some(2)) { + suggestions.push(suggest); + } else { + suggestions.push(typo.to_string()); + } + } + "file" => { + if let Some(suggest) = get_best_match_file(typo) { + suggestions.push(suggest); + } else { + suggestions.push(typo.to_string()); + } + } + _ => {} + } + } else if let Some(suggest) = find_similar(typo, &candidates, Some(2)) { + suggestions.push(suggest); + } else { + suggestions.push(typo.to_string()); + } + } + suggestions.join(" ") +} + +pub fn best_match_path(typo: &str, executables: &[String]) -> Option { + find_similar(typo, executables, Some(3)) +} + +// higher the threshold, the stricter the comparison +// 1: anything +// 2: 50% +// 3: 33% +// ... etc +pub fn find_similar(typo: &str, candidates: &[String], threshold: Option) -> Option { + let threshold = threshold.unwrap_or(2); + let mut min_distance = typo.chars().count() / threshold + 1; + let mut min_distance_index = None; + for (i, candidate) in candidates.iter().enumerate() { + if candidate.is_empty() { + continue; + } + 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)] +pub 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()] +} + diff --git a/pay-respects/src/files.rs b/pay-respects-utils/src/files.rs similarity index 98% rename from pay-respects/src/files.rs rename to pay-respects-utils/src/files.rs index 00613e4..14f18ef 100644 --- a/pay-respects/src/files.rs +++ b/pay-respects-utils/src/files.rs @@ -1,4 +1,4 @@ -use crate::suggestions::find_similar; +use crate::evals::find_similar; pub fn get_path_files() -> Vec { let path_env = path_env(); @@ -164,3 +164,4 @@ fn path_env() -> String { fn path_env_sep() -> &'static str { ":" } + diff --git a/pay-respects-utils/src/lib.rs b/pay-respects-utils/src/lib.rs new file mode 100644 index 0000000..730ae90 --- /dev/null +++ b/pay-respects-utils/src/lib.rs @@ -0,0 +1,2 @@ +pub mod evals; +pub mod files; diff --git a/pay-respects/Cargo.toml b/pay-respects/Cargo.toml index ab2e87f..8026278 100644 --- a/pay-respects/Cargo.toml +++ b/pay-respects/Cargo.toml @@ -28,6 +28,7 @@ textwrap = { version = "0.16", features = ["terminal_size"], optional = true } inquire = "0.7.5" pay-respects-parser = { version = "0.3.2", path = "../pay-respects-parser" } +pay-respects-utils = {version = "0.1.0", path = "../pay-respects-utils"} [features] runtime-rules = ["dep:serde", "dep:toml"] diff --git a/pay-respects/src/main.rs b/pay-respects/src/main.rs index 906d9ec..93b6e3a 100644 --- a/pay-respects/src/main.rs +++ b/pay-respects/src/main.rs @@ -17,7 +17,6 @@ use sys_locale::get_locale; mod args; -mod files; mod modes; mod rules; mod shell; @@ -25,11 +24,6 @@ mod style; mod suggestions; mod system; -#[cfg(feature = "runtime-rules")] -mod replaces; -#[cfg(feature = "runtime-rules")] -mod runtime_rules; - #[cfg(feature = "request-ai")] mod requests; diff --git a/pay-respects/src/modes.rs b/pay-respects/src/modes.rs index 2327984..74a3e63 100644 --- a/pay-respects/src/modes.rs +++ b/pay-respects/src/modes.rs @@ -1,12 +1,14 @@ use crate::shell::Data; -use crate::suggestions::{best_match_path, suggest_candidates}; +use crate::suggestions::suggest_candidates; use crate::system; use crate::{shell, suggestions}; +use pay_respects_utils::evals::best_match_path; use colored::Colorize; use inquire::*; use std::path::Path; + pub fn suggestion(data: &mut Data) { let shell = data.shell.clone(); let mut last_command; @@ -65,7 +67,7 @@ pub fn cnf(data: &mut Data) { executable ); - let best_match = best_match_path(executable); + let best_match = best_match_path(executable, &data.executables); if best_match.is_some() { let best_match = best_match.unwrap(); split_command[0] = best_match; diff --git a/pay-respects/src/replaces.rs b/pay-respects/src/replaces.rs index 8e90e07..ab57ae8 100644 --- a/pay-respects/src/replaces.rs +++ b/pay-respects/src/replaces.rs @@ -1,4 +1,4 @@ -use crate::suggestions::*; +use pay_respects_utils::evals::*; fn tag(name: &str, x: i32) -> String { format!("{{{}{}}}", name, x) diff --git a/pay-respects/src/rules.rs b/pay-respects/src/rules.rs index 3474832..ccfc370 100644 --- a/pay-respects/src/rules.rs +++ b/pay-respects/src/rules.rs @@ -1,11 +1,12 @@ use crate::shell::Data; -use crate::suggestions::*; +use pay_respects_utils::evals::*; use pay_respects_parser::parse_rules; pub fn match_pattern(executable: &str, data: &mut Data) { - let error_msg = &data.error.clone(); - let shell = &data.shell.clone(); - let last_command = &data.command.clone(); - let executables = &data.get_executables().clone(); + let error_msg = &data.error; + let shell = &data.shell; + let last_command = &data.command; + let executables = &data.executables; + let candidates = &mut data.candidates; parse_rules!("rules"); } diff --git a/pay-respects/src/shell.rs b/pay-respects/src/shell.rs index 123a2bb..484d212 100644 --- a/pay-respects/src/shell.rs +++ b/pay-respects/src/shell.rs @@ -1,3 +1,5 @@ +use pay_respects_utils::evals::split_command; +use pay_respects_utils::files::get_path_files; use std::process::exit; use std::collections::HashMap; @@ -5,10 +7,6 @@ use std::sync::mpsc::channel; use std::thread; use std::time::Duration; -use regex_lite::Regex; - -use crate::files::get_path_files; - pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"]; pub enum Mode { @@ -45,6 +43,8 @@ pub struct Data { pub privilege: Option, pub error: String, pub executables: Vec, + pub modules: Vec, + pub fallbacks: Vec, pub mode: Mode, } @@ -54,6 +54,32 @@ impl Data { let command = last_command(&shell).trim().to_string(); let alias = alias_map(&shell); let mode = run_mode(); + let (executables, modules, fallbacks) = { + let path_executables = get_path_files(); + let mut executables = vec![]; + let mut modules = vec![]; + let mut fallbacks = vec![]; + for exe in path_executables { + if exe.starts_with("pay-respects-module-") { + modules.push(exe.to_string()); + } else if exe.starts_with("pay-respects-fallback-") { + fallbacks.push(exe.to_string()); + } else { + executables.push(exe.to_string()); + } + } + if alias.is_some() { + let alias = alias.as_ref().unwrap(); + for command in alias.keys() { + if executables.contains(command) { + continue; + } + executables.push(command.to_string()); + } + } + + (executables, modules, fallbacks) + }; let mut init = Data { shell, @@ -64,7 +90,9 @@ impl Data { split: vec![], privilege: None, error: "".to_string(), - executables: vec![], + executables, + modules, + fallbacks, mode, }; @@ -135,50 +163,19 @@ impl Data { self.candidates.push(candidate.to_string()); } } - - pub fn get_executables(&mut self) -> &Vec { - if self.executables.is_empty() { - self.executables = get_path_files(); - if self.alias.is_some() { - let alias = self.alias.as_ref().unwrap(); - for command in alias.keys() { - if self.executables.contains(command) { - continue; - } - self.executables.push(command.to_string()); - } + pub fn add_candidates(&mut self, candidates: &Vec) { + for candidate in candidates { + let candidate = candidate.trim(); + if candidate != self.command { + self.candidates.push(candidate.to_string()); } } - &self.executables } - - pub fn has_executable(&mut self, executable: &str) -> bool { - if self.executables.is_empty() { - self.executables = get_path_files(); - } - self.executables.contains(&executable.to_string()) - } -} - -pub fn split_command(command: &str) -> Vec { - #[cfg(debug_assertions)] - eprintln!("command: {command}"); - // this regex splits the command separated by spaces, except when the space - // is escaped by a backslash or surrounded by quotes - let regex = r#"([^\s"'\\]+|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\\ )+|\\|\n"#; - let regex = Regex::new(regex).unwrap(); - let split_command = regex - .find_iter(command) - .map(|cap| cap.as_str().to_owned()) - .collect::>(); - #[cfg(debug_assertions)] - eprintln!("split_command: {:?}", split_command); - split_command } pub fn elevate(data: &mut Data, command: &mut String) { for privilege in PRIVILEGE_LIST.iter() { - if data.has_executable(privilege) { + if data.executables.contains(&privilege.to_string()) { *command = format!("{} {}", privilege, command); break; } @@ -241,6 +238,28 @@ pub fn command_output(shell: &str, command: &str) -> String { } } +pub fn module_output(data: &Data, module: &str) -> Vec { + let shell = &data.shell; + let executable = &data.split[0]; + let last_command = &data.command; + let error_msg = &data.error; + let executables = data.executables.clone().join(","); + let output = std::process::Command::new(shell) + .arg("-c") + .arg(module) + .env("_PR_COMMAND", executable) + .env("_PR_LAST_COMMAND", last_command) + .env("_PR_ERROR_MSG", error_msg) + .env("_PR_EXECUTABLES", executables) + .output() + .expect("failed to execute process"); + + String::from_utf8_lossy(&output.stderr) + .split("<_PR_BR>") + .map(|s| s.trim().to_string()) + .collect::>() +} + pub fn last_command(shell: &str) -> String { let last_command = match std::env::var("_PR_LAST_COMMAND") { Ok(command) => command, diff --git a/pay-respects/src/style.rs b/pay-respects/src/style.rs index edd7636..ec2ddfd 100644 --- a/pay-respects/src/style.rs +++ b/pay-respects/src/style.rs @@ -1,5 +1,5 @@ +use pay_respects_utils::evals::split_command; use crate::shell::PRIVILEGE_LIST; -use crate::suggestions::split_command; use colored::*; // to_string() is necessary here, otherwise there won't be color in the output diff --git a/pay-respects/src/suggestions.rs b/pay-respects/src/suggestions.rs index 5cf5444..26be399 100644 --- a/pay-respects/src/suggestions.rs +++ b/pay-respects/src/suggestions.rs @@ -4,11 +4,9 @@ use std::time::{Duration, Instant}; use colored::Colorize; use inquire::*; -use regex_lite::Regex; -use crate::files::{get_best_match_file, get_path_files}; use crate::rules::match_pattern; -use crate::shell::{shell_evaluated_commands, Data}; +use crate::shell::{shell_evaluated_commands, Data, module_output}; use crate::style::highlight_difference; pub fn suggest_candidates(data: &mut Data) { @@ -21,10 +19,12 @@ pub fn suggest_candidates(data: &mut Data) { match_pattern(executable, data); match_pattern("_PR_general", data); - #[cfg(feature = "runtime-rules")] - { - use crate::runtime_rules::runtime_match; - runtime_match(executable, data); + let modules = &data.modules.clone(); + for module in modules { + let candidates = module_output(data, module); + if !candidates.is_empty() { + data.add_candidates(&candidates); + } } #[cfg(feature = "request-ai")] @@ -119,159 +119,6 @@ pub fn select_candidate(data: &mut Data) { data.candidates.clear(); } -pub fn opt_regex(regex: &str, command: &mut String) -> String { - let regex = Regex::new(regex).unwrap(); - - let mut opts = Vec::new(); - for captures in regex.captures_iter(command) { - for cap in captures.iter().skip(1).flatten() { - opts.push(cap.as_str().to_owned()); - } - } - - for opt in opts.clone() { - *command = command.replace(&opt, ""); - } - opts.join(" ") -} - -pub fn err_regex(regex: &str, error_msg: &str) -> String { - let regex = Regex::new(regex).unwrap(); - - let mut err = Vec::new(); - for captures in regex.captures_iter(error_msg) { - for cap in captures.iter().skip(1).flatten() { - err.push(cap.as_str().to_owned()); - } - } - err.join(" ") -} - -pub fn cmd_regex(regex: &str, command: &str) -> String { - let regex = Regex::new(regex).unwrap(); - - let mut cmd = Vec::new(); - for captures in regex.captures_iter(command) { - for cap in captures.iter().skip(1).flatten() { - cmd.push(cap.as_str().to_owned()); - } - } - cmd.join(" ") -} - -pub fn eval_shell_command(shell: &str, command: &str) -> Vec { - let output = std::process::Command::new(shell) - .arg("-c") - .arg(command) - .output() - .expect("failed to execute process"); - let output = String::from_utf8_lossy(&output.stdout); - let split_output = output.split('\n').collect::>(); - split_output - .iter() - .map(|s| s.trim().to_string()) - .collect::>() -} - -pub fn split_command(command: &str) -> Vec { - if cfg!(debug_assertions) { - eprintln!("command: {command}") - } - // this regex splits the command separated by spaces, except when the space - // is escaped by a backslash or surrounded by quotes - let regex = r#"([^\s"'\\]+|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\\ )+|\\|\n"#; - let regex = Regex::new(regex).unwrap(); - let split_command = regex - .find_iter(command) - .map(|cap| cap.as_str().to_owned()) - .collect::>(); - split_command -} - -pub fn suggest_typo(typos: &[String], candidates: Vec, executables: &[String]) -> String { - let mut suggestions = Vec::new(); - for typo in typos { - let typo = typo.as_str(); - if candidates.len() == 1 { - match candidates[0].as_str() { - "path" => { - if let Some(suggest) = find_similar(typo, executables, Some(2)) { - suggestions.push(suggest); - } else { - suggestions.push(typo.to_string()); - } - } - "file" => { - if let Some(suggest) = get_best_match_file(typo) { - suggestions.push(suggest); - } else { - suggestions.push(typo.to_string()); - } - } - _ => {} - } - } else if let Some(suggest) = find_similar(typo, &candidates, Some(2)) { - suggestions.push(suggest); - } else { - suggestions.push(typo.to_string()); - } - } - suggestions.join(" ") -} - -pub fn best_match_path(typo: &str) -> Option { - let path_files = get_path_files(); - find_similar(typo, &path_files, Some(3)) -} - -// higher the threshold, the stricter the comparison -// 1: anything -// 2: 50% -// 3: 33% -// ... etc -pub fn find_similar(typo: &str, candidates: &[String], threshold: Option) -> Option { - let threshold = threshold.unwrap_or(2); - let mut min_distance = typo.chars().count() / threshold + 1; - let mut min_distance_index = None; - for (i, candidate) in candidates.iter().enumerate() { - if candidate.is_empty() { - continue; - } - 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)] -pub 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(data: &Data) -> Result<(), String> { let shell = &data.shell; let command = &data.suggest.clone().unwrap(); diff --git a/pay-respects/src/system.rs b/pay-respects/src/system.rs index bf6736a..2d04dd8 100644 --- a/pay-respects/src/system.rs +++ b/pay-respects/src/system.rs @@ -11,7 +11,7 @@ pub fn get_package_manager(data: &mut Data) -> Option { ]; for package_manager in package_managers { - if data.has_executable(package_manager) { + if data.executables.contains(&package_manager.to_string()) { return Some(package_manager.to_string()); } } @@ -26,7 +26,7 @@ pub fn get_packages( let shell = &data.shell.clone(); match package_manager { "apt" => { - if !data.has_executable("apt-file") { + if !data.executables.contains(&"apt-file".to_string()) { eprintln!( "{}: apt-file is required to find packages", "pay-respects".yellow() @@ -67,7 +67,7 @@ pub fn get_packages( } } "emerge" => { - if !data.has_executable("e-file") { + if !data.executables.contains(&"e-file".to_string()) { eprintln!( "{}: pfl is required to find packages", "pay-respects".yellow() @@ -91,7 +91,7 @@ pub fn get_packages( } } "nix" => { - if !data.has_executable("nix-locate") { + if !data.executables.contains(&"nix-locate".to_string()) { eprintln!( "{}: nix-index is required to find packages", "pay-respects".yellow() @@ -121,7 +121,7 @@ pub fn get_packages( } } "pacman" => { - let result = if data.has_executable("pkgfile") { + let result = if data.executables.contains(&"pkgfile".to_string()) { command_output(shell, &format!("pkgfile -b {}", executable)) } else { command_output(shell, &format!("pacman -Fq /usr/bin/{}", executable))