mirror of
https://github.com/TECHNOFAB11/pay-respects.git
synced 2025-12-12 06:20:09 +01:00
refactor: split runtime rules into module
This commit is contained in:
parent
000b50c211
commit
5aebf867c1
20 changed files with 514 additions and 249 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
pay-respects-module-runtime-rules/Cargo.toml
Normal file
11
pay-respects-module-runtime-rules/Cargo.toml
Normal 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"}
|
||||
16
pay-respects-module-runtime-rules/src/main.rs
Normal file
16
pay-respects-module-runtime-rules/src/main.rs
Normal 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(())
|
||||
}
|
||||
186
pay-respects-module-runtime-rules/src/replaces.rs
Normal file
186
pay-respects-module-runtime-rules/src/replaces.rs
Normal 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, ¤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::<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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<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);
|
||||
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::<usize>().unwrap(),
|
||||
|
|
@ -190,3 +194,4 @@ fn get_rule(executable: &str) -> Option<String> {
|
|||
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ fn parse_conditions(suggest: &str) -> (String, Vec<TokenStream2>) {
|
|||
|
||||
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::<usize>().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),*});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
pay-respects-utils/Cargo.toml
Normal file
8
pay-respects-utils/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "pay-respects-utils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
regex-lite = "0.1"
|
||||
|
||||
155
pay-respects-utils/src/evals.rs
Normal file
155
pay-respects-utils/src/evals.rs
Normal 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()]
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::suggestions::find_similar;
|
||||
use crate::evals::find_similar;
|
||||
|
||||
pub fn get_path_files() -> Vec<String> {
|
||||
let path_env = path_env();
|
||||
|
|
@ -164,3 +164,4 @@ fn path_env() -> String {
|
|||
fn path_env_sep() -> &'static str {
|
||||
":"
|
||||
}
|
||||
|
||||
2
pay-respects-utils/src/lib.rs
Normal file
2
pay-respects-utils/src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod evals;
|
||||
pub mod files;
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::suggestions::*;
|
||||
use pay_respects_utils::evals::*;
|
||||
|
||||
fn tag(name: &str, x: i32) -> String {
|
||||
format!("{{{}{}}}", name, x)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub error: String,
|
||||
pub executables: Vec<String>,
|
||||
pub modules: Vec<String>,
|
||||
pub fallbacks: Vec<String>,
|
||||
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<String> {
|
||||
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<String>) {
|
||||
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<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) {
|
||||
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<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 {
|
||||
let last_command = match std::env::var("_PR_LAST_COMMAND") {
|
||||
Ok(command) => command,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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> {
|
||||
let shell = &data.shell;
|
||||
let command = &data.suggest.clone().unwrap();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ pub fn get_package_manager(data: &mut Data) -> Option<String> {
|
|||
];
|
||||
|
||||
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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue