refactor: split runtime rules into module

This commit is contained in:
iff 2024-12-08 15:08:21 +01:00
parent 000b50c211
commit 5aebf867c1
20 changed files with 514 additions and 249 deletions

20
Cargo.lock generated
View file

@ -474,6 +474,7 @@ dependencies = [
"curl", "curl",
"inquire", "inquire",
"pay-respects-parser", "pay-respects-parser",
"pay-respects-utils",
"regex-lite", "regex-lite",
"rust-i18n", "rust-i18n",
"serde", "serde",
@ -483,11 +484,19 @@ dependencies = [
"toml 0.8.19", "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]] [[package]]
name = "pay-respects-parser" name = "pay-respects-parser"
version = "0.3.2" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6304475497712e6fbcbb0f947c4958ae336f1dcc3d7c8f470664a20e9ef175"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -496,6 +505,13 @@ dependencies = [
"toml 0.8.19", "toml 0.8.19",
] ]
[[package]]
name = "pay-respects-utils"
version = "0.1.0"
dependencies = [
"regex-lite",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.31" version = "0.3.31"

View file

@ -3,10 +3,11 @@ resolver = "2"
members = [ members = [
"pay-respects", "pay-respects",
"pay-respects-parser", "pay-respects-parser",
"pay-respects-utils",
# optional modules # optional modules
# "pay-respects-module-runtime-rules", "pay-respects-module-runtime-rules",
# "pay-respects-module-request-ai" # "pay-respects-module-request-ai",
] ]
[profile.release] [profile.release]
strip = true strip = true

View file

@ -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"}

View file

@ -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<String> = {
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(())
}

View file

@ -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<usize>, std::ops::Range<usize>) {
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, &current_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::<i32>().unwrap_or(0);
if start_index < 0 {
start_index += split_command.len() as i32;
};
let mut end_index;
let parsed_end = end.parse::<i32>();
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::<usize>().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::<Vec<&str>>();
let command_index = split[1];
if !command_index.contains(':') {
let command_index = command_index.parse::<i32>().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::<i32>().unwrap_or(0);
let start_index = if start < 0 {
split_command.len() as i32 + start
} else {
start
};
let end = end.parse::<i32>();
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::<Vec<&str>>()
} else {
unreachable!("Typo suggestion must have a match list");
};
let match_list = match_list
.iter()
.map(|s| s.trim().to_string())
.collect::<Vec<String>>();
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"));
}
}

View file

@ -1,6 +1,5 @@
use crate::replaces; use crate::replaces;
use crate::shell::Data; use pay_respects_utils::evals::*;
use crate::suggestions::*;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct Rule { struct Rule {
@ -13,7 +12,13 @@ struct MatchError {
suggest: Vec<String>, suggest: Vec<String>,
} }
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); let file = get_rule(executable);
if file.is_none() { if file.is_none() {
return; 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 file = std::fs::read_to_string(file.unwrap()).unwrap();
let rule: Rule = toml::from_str(&file).unwrap(); let rule: Rule = toml::from_str(&file).unwrap();
let split_command = &data.split.clone(); let split_command = split_command(last_command);
let shell = &data.shell.clone();
let last_command = &data.command.clone();
let error_msg = &data.error.clone();
let executables = &data.get_executables().clone();
let mut pure_suggest; let mut pure_suggest;
@ -66,8 +67,8 @@ pub fn runtime_match(executable: &str, data: &mut Data) {
shell, shell,
last_command, last_command,
error_msg, error_msg,
split_command, &split_command,
data, executables,
) == reverse ) == reverse
{ {
continue 'suggest; continue 'suggest;
@ -82,13 +83,16 @@ pub fn runtime_match(executable: &str, data: &mut Data) {
if pure_suggest.contains("{{command}}") { if pure_suggest.contains("{{command}}") {
pure_suggest = pure_suggest.replace("{{command}}", last_command); pure_suggest = pure_suggest.replace("{{command}}", last_command);
} }
data.add_candidate(&eval_suggest( print!("{}",
eval_suggest(
&pure_suggest, &pure_suggest,
last_command, last_command,
error_msg, error_msg,
executables, executables,
shell, shell,
)); )
);
print!("{}", "<_PR_BR>");
} }
} }
} }
@ -102,10 +106,10 @@ fn eval_condition(
last_command: &str, last_command: &str,
error_msg: &str, error_msg: &str,
split_command: &[String], split_command: &[String],
data: &mut Data, executables: &[String],
) -> bool { ) -> bool {
match condition { match condition {
"executable" => data.has_executable(arg), "executable" => executables.contains(&arg.to_string()),
"err_contains" => error_msg.contains(arg), "err_contains" => error_msg.contains(arg),
"cmd_contains" => last_command.contains(arg), "cmd_contains" => last_command.contains(arg),
"min_length" => split_command.len() >= arg.parse::<usize>().unwrap(), "min_length" => split_command.len() >= arg.parse::<usize>().unwrap(),
@ -190,3 +194,4 @@ fn get_rule(executable: &str) -> Option<String> {
None None
} }

View file

@ -184,7 +184,7 @@ fn parse_conditions(suggest: &str) -> (String, Vec<TokenStream2>) {
fn eval_condition(condition: &str, arg: &str) -> TokenStream2 { fn eval_condition(condition: &str, arg: &str) -> TokenStream2 {
match condition { match condition {
"executable" => quote! {data.has_executable(#arg)}, "executable" => quote! {executables.contains(&#arg.to_string())},
"err_contains" => quote! {error_msg.contains(#arg)}, "err_contains" => quote! {error_msg.contains(#arg)},
"cmd_contains" => quote! {last_command.contains(#arg)}, "cmd_contains" => quote! {last_command.contains(#arg)},
"min_length" => quote! {(split.len() >= #arg.parse::<usize>().unwrap())}, "min_length" => quote! {(split.len() >= #arg.parse::<usize>().unwrap())},
@ -215,6 +215,6 @@ fn eval_suggest(suggest: &str) -> TokenStream2 {
quote! { quote! {
#(#opt_list)* #(#opt_list)*
data.add_candidate(&format!{#suggest, #(#replace_list),*}); candidates.push(format!{#suggest, #(#replace_list),*});
} }
} }

View file

@ -0,0 +1,8 @@
[package]
name = "pay-respects-utils"
version = "0.1.0"
edition = "2021"
[dependencies]
regex-lite = "0.1"

View file

@ -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<String> {
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::<Vec<&str>>();
split_output
.iter()
.map(|s| s.trim().to_string())
.collect::<Vec<String>>()
}
pub fn split_command(command: &str) -> Vec<String> {
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::<Vec<String>>();
split_command
}
pub fn suggest_typo(typos: &[String], candidates: Vec<String>, 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<String> {
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<usize>) -> Option<String> {
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()]
}

View file

@ -1,4 +1,4 @@
use crate::suggestions::find_similar; use crate::evals::find_similar;
pub fn get_path_files() -> Vec<String> { pub fn get_path_files() -> Vec<String> {
let path_env = path_env(); let path_env = path_env();
@ -164,3 +164,4 @@ fn path_env() -> String {
fn path_env_sep() -> &'static str { fn path_env_sep() -> &'static str {
":" ":"
} }

View file

@ -0,0 +1,2 @@
pub mod evals;
pub mod files;

View file

@ -28,6 +28,7 @@ textwrap = { version = "0.16", features = ["terminal_size"], optional = true }
inquire = "0.7.5" inquire = "0.7.5"
pay-respects-parser = { version = "0.3.2", path = "../pay-respects-parser" } pay-respects-parser = { version = "0.3.2", path = "../pay-respects-parser" }
pay-respects-utils = {version = "0.1.0", path = "../pay-respects-utils"}
[features] [features]
runtime-rules = ["dep:serde", "dep:toml"] runtime-rules = ["dep:serde", "dep:toml"]

View file

@ -17,7 +17,6 @@
use sys_locale::get_locale; use sys_locale::get_locale;
mod args; mod args;
mod files;
mod modes; mod modes;
mod rules; mod rules;
mod shell; mod shell;
@ -25,11 +24,6 @@ mod style;
mod suggestions; mod suggestions;
mod system; mod system;
#[cfg(feature = "runtime-rules")]
mod replaces;
#[cfg(feature = "runtime-rules")]
mod runtime_rules;
#[cfg(feature = "request-ai")] #[cfg(feature = "request-ai")]
mod requests; mod requests;

View file

@ -1,12 +1,14 @@
use crate::shell::Data; use crate::shell::Data;
use crate::suggestions::{best_match_path, suggest_candidates}; use crate::suggestions::suggest_candidates;
use crate::system; use crate::system;
use crate::{shell, suggestions}; use crate::{shell, suggestions};
use pay_respects_utils::evals::best_match_path;
use colored::Colorize; use colored::Colorize;
use inquire::*; use inquire::*;
use std::path::Path; use std::path::Path;
pub fn suggestion(data: &mut Data) { pub fn suggestion(data: &mut Data) {
let shell = data.shell.clone(); let shell = data.shell.clone();
let mut last_command; let mut last_command;
@ -65,7 +67,7 @@ pub fn cnf(data: &mut Data) {
executable executable
); );
let best_match = best_match_path(executable); let best_match = best_match_path(executable, &data.executables);
if best_match.is_some() { if best_match.is_some() {
let best_match = best_match.unwrap(); let best_match = best_match.unwrap();
split_command[0] = best_match; split_command[0] = best_match;

View file

@ -1,4 +1,4 @@
use crate::suggestions::*; use pay_respects_utils::evals::*;
fn tag(name: &str, x: i32) -> String { fn tag(name: &str, x: i32) -> String {
format!("{{{}{}}}", name, x) format!("{{{}{}}}", name, x)

View file

@ -1,11 +1,12 @@
use crate::shell::Data; use crate::shell::Data;
use crate::suggestions::*; use pay_respects_utils::evals::*;
use pay_respects_parser::parse_rules; use pay_respects_parser::parse_rules;
pub fn match_pattern(executable: &str, data: &mut Data) { pub fn match_pattern(executable: &str, data: &mut Data) {
let error_msg = &data.error.clone(); let error_msg = &data.error;
let shell = &data.shell.clone(); let shell = &data.shell;
let last_command = &data.command.clone(); let last_command = &data.command;
let executables = &data.get_executables().clone(); let executables = &data.executables;
let candidates = &mut data.candidates;
parse_rules!("rules"); parse_rules!("rules");
} }

View file

@ -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::process::exit;
use std::collections::HashMap; use std::collections::HashMap;
@ -5,10 +7,6 @@ use std::sync::mpsc::channel;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use regex_lite::Regex;
use crate::files::get_path_files;
pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"]; pub const PRIVILEGE_LIST: [&str; 2] = ["sudo", "doas"];
pub enum Mode { pub enum Mode {
@ -45,6 +43,8 @@ pub struct Data {
pub privilege: Option<String>, pub privilege: Option<String>,
pub error: String, pub error: String,
pub executables: Vec<String>, pub executables: Vec<String>,
pub modules: Vec<String>,
pub fallbacks: Vec<String>,
pub mode: Mode, pub mode: Mode,
} }
@ -54,6 +54,32 @@ impl Data {
let command = last_command(&shell).trim().to_string(); let command = last_command(&shell).trim().to_string();
let alias = alias_map(&shell); let alias = alias_map(&shell);
let mode = run_mode(); 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 { let mut init = Data {
shell, shell,
@ -64,7 +90,9 @@ impl Data {
split: vec![], split: vec![],
privilege: None, privilege: None,
error: "".to_string(), error: "".to_string(),
executables: vec![], executables,
modules,
fallbacks,
mode, mode,
}; };
@ -135,50 +163,19 @@ impl Data {
self.candidates.push(candidate.to_string()); self.candidates.push(candidate.to_string());
} }
} }
pub fn add_candidates(&mut self, candidates: &Vec<String>) {
pub fn get_executables(&mut self) -> &Vec<String> { for candidate in candidates {
if self.executables.is_empty() { let candidate = candidate.trim();
self.executables = get_path_files(); if candidate != self.command {
if self.alias.is_some() { self.candidates.push(candidate.to_string());
let alias = self.alias.as_ref().unwrap();
for command in alias.keys() {
if self.executables.contains(command) {
continue;
}
self.executables.push(command.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<String> {
#[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::<Vec<String>>();
#[cfg(debug_assertions)]
eprintln!("split_command: {:?}", split_command);
split_command
} }
pub fn elevate(data: &mut Data, command: &mut String) { pub fn elevate(data: &mut Data, command: &mut String) {
for privilege in PRIVILEGE_LIST.iter() { for privilege in PRIVILEGE_LIST.iter() {
if data.has_executable(privilege) { if data.executables.contains(&privilege.to_string()) {
*command = format!("{} {}", privilege, command); *command = format!("{} {}", privilege, command);
break; break;
} }
@ -241,6 +238,28 @@ pub fn command_output(shell: &str, command: &str) -> String {
} }
} }
pub fn module_output(data: &Data, module: &str) -> Vec<String> {
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::<Vec<String>>()
}
pub fn last_command(shell: &str) -> String { pub fn last_command(shell: &str) -> String {
let last_command = match std::env::var("_PR_LAST_COMMAND") { let last_command = match std::env::var("_PR_LAST_COMMAND") {
Ok(command) => command, Ok(command) => command,

View file

@ -1,5 +1,5 @@
use pay_respects_utils::evals::split_command;
use crate::shell::PRIVILEGE_LIST; use crate::shell::PRIVILEGE_LIST;
use crate::suggestions::split_command;
use colored::*; use colored::*;
// to_string() is necessary here, otherwise there won't be color in the output // to_string() is necessary here, otherwise there won't be color in the output

View file

@ -4,11 +4,9 @@ use std::time::{Duration, Instant};
use colored::Colorize; use colored::Colorize;
use inquire::*; use inquire::*;
use regex_lite::Regex;
use crate::files::{get_best_match_file, get_path_files};
use crate::rules::match_pattern; 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; use crate::style::highlight_difference;
pub fn suggest_candidates(data: &mut Data) { pub fn suggest_candidates(data: &mut Data) {
@ -21,10 +19,12 @@ pub fn suggest_candidates(data: &mut Data) {
match_pattern(executable, data); match_pattern(executable, data);
match_pattern("_PR_general", data); match_pattern("_PR_general", data);
#[cfg(feature = "runtime-rules")] let modules = &data.modules.clone();
{ for module in modules {
use crate::runtime_rules::runtime_match; let candidates = module_output(data, module);
runtime_match(executable, data); if !candidates.is_empty() {
data.add_candidates(&candidates);
}
} }
#[cfg(feature = "request-ai")] #[cfg(feature = "request-ai")]
@ -119,159 +119,6 @@ pub fn select_candidate(data: &mut Data) {
data.candidates.clear(); 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<String> {
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::<Vec<&str>>();
split_output
.iter()
.map(|s| s.trim().to_string())
.collect::<Vec<String>>()
}
pub fn split_command(command: &str) -> Vec<String> {
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::<Vec<String>>();
split_command
}
pub fn suggest_typo(typos: &[String], candidates: Vec<String>, 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<String> {
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<usize>) -> Option<String> {
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> { pub fn confirm_suggestion(data: &Data) -> Result<(), String> {
let shell = &data.shell; let shell = &data.shell;
let command = &data.suggest.clone().unwrap(); let command = &data.suggest.clone().unwrap();

View file

@ -11,7 +11,7 @@ pub fn get_package_manager(data: &mut Data) -> Option<String> {
]; ];
for package_manager in package_managers { 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()); return Some(package_manager.to_string());
} }
} }
@ -26,7 +26,7 @@ pub fn get_packages(
let shell = &data.shell.clone(); let shell = &data.shell.clone();
match package_manager { match package_manager {
"apt" => { "apt" => {
if !data.has_executable("apt-file") { if !data.executables.contains(&"apt-file".to_string()) {
eprintln!( eprintln!(
"{}: apt-file is required to find packages", "{}: apt-file is required to find packages",
"pay-respects".yellow() "pay-respects".yellow()
@ -67,7 +67,7 @@ pub fn get_packages(
} }
} }
"emerge" => { "emerge" => {
if !data.has_executable("e-file") { if !data.executables.contains(&"e-file".to_string()) {
eprintln!( eprintln!(
"{}: pfl is required to find packages", "{}: pfl is required to find packages",
"pay-respects".yellow() "pay-respects".yellow()
@ -91,7 +91,7 @@ pub fn get_packages(
} }
} }
"nix" => { "nix" => {
if !data.has_executable("nix-locate") { if !data.executables.contains(&"nix-locate".to_string()) {
eprintln!( eprintln!(
"{}: nix-index is required to find packages", "{}: nix-index is required to find packages",
"pay-respects".yellow() "pay-respects".yellow()
@ -121,7 +121,7 @@ pub fn get_packages(
} }
} }
"pacman" => { "pacman" => {
let result = if data.has_executable("pkgfile") { let result = if data.executables.contains(&"pkgfile".to_string()) {
command_output(shell, &format!("pkgfile -b {}", executable)) command_output(shell, &format!("pkgfile -b {}", executable))
} else { } else {
command_output(shell, &format!("pacman -Fq /usr/bin/{}", executable)) command_output(shell, &format!("pacman -Fq /usr/bin/{}", executable))