pay-respects/src/suggestions.rs

269 lines
6.9 KiB
Rust
Raw Normal View History

2023-08-11 00:41:16 +02:00
use std::process::{exit, Stdio};
use std::time::{Duration, Instant};
2023-08-08 20:33:49 +02:00
2023-08-01 20:29:02 +02:00
use regex_lite::Regex;
use rule_parser::parse_rules;
2023-08-07 17:12:38 +02:00
use crate::files::{get_best_match_file, get_path_files};
use crate::shell::PRIVILEGE_LIST;
2023-08-01 20:29:02 +02:00
2023-08-08 20:33:49 +02:00
pub fn suggest_command(shell: &str, last_command: &str, error_msg: &str) -> Option<String> {
2023-08-01 20:29:02 +02:00
let split_command = split_command(last_command);
let executable = match PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
true => split_command.get(1).expect("No command found.").as_str(),
false => split_command.first().expect("No command found.").as_str(),
};
if !PRIVILEGE_LIST.contains(&executable) {
2023-08-08 20:33:49 +02:00
let suggest = match_pattern("privilege", last_command, error_msg, shell);
2023-08-03 21:26:41 +02:00
if suggest.is_some() {
return suggest;
2023-08-01 20:29:02 +02:00
}
}
let last_command = match PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
true => &last_command[split_command[0].len() + 1..],
2023-08-11 00:41:16 +02:00
false => last_command,
};
2023-08-08 20:33:49 +02:00
let suggest = match_pattern(executable, last_command, error_msg, shell);
2023-08-01 20:29:02 +02:00
if let Some(suggest) = suggest {
if PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
2023-08-01 20:29:02 +02:00
return Some(format!("{} {}", split_command[0], suggest));
}
return Some(suggest);
}
2023-08-11 00:41:16 +02:00
let suggest = match_pattern("general", last_command, error_msg, shell);
2023-08-01 20:29:02 +02:00
if let Some(suggest) = suggest {
if PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
return Some(format!("{} {}", split_command[0], suggest));
}
2023-08-01 20:29:02 +02:00
return Some(suggest);
}
None
}
2023-08-04 00:18:31 +02:00
fn match_pattern(
executable: &str,
last_command: &str,
error_msg: &str,
shell: &str,
) -> Option<String> {
2023-08-03 21:26:41 +02:00
parse_rules!("rules");
2023-08-01 20:29:02 +02:00
}
2023-08-08 16:57:14 +02:00
fn check_executable(shell: &str, executable: &str) -> bool {
match shell {
2023-08-08 20:33:49 +02:00
"nu" => std::process::Command::new(shell)
.arg("-c")
.arg(format!("if (which {} | is-empty) {{ exit 1 }}", executable))
.output()
.expect("failed to execute process")
.status
.success(),
_ => std::process::Command::new(shell)
.arg("-c")
.arg(format!("command -v {}", executable))
.output()
.expect("failed to execute process")
.status
.success(),
2023-08-08 16:57:14 +02:00
}
}
2023-08-03 21:26:41 +02:00
fn opt_regex(regex: &str, command: &mut String) -> String {
let regex = Regex::new(regex).unwrap();
let opts = regex
.find_iter(command)
.map(|cap| cap.as_str().to_owned())
.collect::<Vec<String>>();
for opt in opts.clone() {
*command = command.replace(&opt, "");
2023-08-01 20:29:02 +02:00
}
2023-08-03 21:26:41 +02:00
opts.join(" ")
2023-08-01 20:29:02 +02:00
}
2023-08-04 00:18:31 +02:00
fn err_regex(regex: &str, error_msg: &str) -> String {
let regex = Regex::new(regex).unwrap();
let err = regex
.find_iter(error_msg)
.map(|cap| cap.as_str().to_owned())
.collect::<Vec<String>>();
err.join(" ")
}
2023-08-07 18:34:52 +02:00
fn cmd_regex(regex: &str, command: &str) -> String {
let regex = Regex::new(regex).unwrap();
let err = regex
.find_iter(command)
.map(|cap| cap.as_str().to_owned())
.collect::<Vec<String>>();
err.join(" ")
}
2023-08-04 00:18:31 +02:00
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>>()
}
2023-08-01 20:29:02 +02:00
pub fn split_command(command: &str) -> Vec<String> {
// 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"'\\]+|"(?:\\.|[^"\\])*"|\\\s+|'(?:\\.|[^'\\])*'|\\\s)+"#;
2023-08-01 20:29:02 +02:00
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
}
2023-08-11 00:41:16 +02:00
fn suggest_typo(typos: &[String], candidates: &[String]) -> String {
let mut path_files = Vec::new();
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 path_files.is_empty() {
path_files = get_path_files();
};
if let Some(suggest) = find_similar(typo, &path_files) {
suggestions.push(suggest);
};
2023-08-01 20:29:02 +02:00
}
2023-08-11 00:41:16 +02:00
"file" => {
if let Some(suggest) = get_best_match_file(typo) {
suggestions.push(suggest);
}
2023-08-01 20:29:02 +02:00
}
2023-08-11 00:41:16 +02:00
_ => {}
2023-08-01 20:29:02 +02:00
}
2023-08-11 00:41:16 +02:00
} else if let Some(suggest) = find_similar(typo, candidates) {
suggestions.push(suggest);
2023-08-01 20:29:02 +02:00
}
}
2023-08-11 00:41:16 +02:00
suggestions.join(" ")
2023-08-01 20:29:02 +02:00
}
2023-08-11 00:41:16 +02:00
pub fn find_similar(typo: &str, candidates: &[String]) -> Option<String> {
2023-08-01 20:29:02 +02:00
let mut min_distance = 10;
let mut min_distance_index = None;
for (i, candidate) in candidates.iter().enumerate() {
let distance = compare_string(typo, candidate);
if distance < min_distance {
min_distance = distance;
min_distance_index = Some(i);
}
}
if let Some(min_distance_index) = min_distance_index {
return Some(candidates[min_distance_index].to_string());
}
None
}
#[allow(clippy::needless_range_loop)]
fn compare_string(a: &str, b: &str) -> usize {
let mut matrix = vec![vec![0; b.chars().count() + 1]; a.chars().count() + 1];
for i in 0..a.chars().count() + 1 {
matrix[i][0] = i;
}
for j in 0..b.chars().count() + 1 {
matrix[0][j] = j;
}
for (i, ca) in a.chars().enumerate() {
for (j, cb) in b.chars().enumerate() {
let cost = if ca == cb { 0 } else { 1 };
matrix[i + 1][j + 1] = std::cmp::min(
std::cmp::min(matrix[i][j + 1] + 1, matrix[i + 1][j] + 1),
matrix[i][j] + cost,
);
}
}
matrix[a.chars().count()][b.chars().count()]
}
2023-08-08 20:33:49 +02:00
pub fn confirm_suggestion(shell: &str, command: &str, highlighted: &str) -> Result<(), String> {
println!("{}\n", highlighted);
2023-08-01 20:29:02 +02:00
println!("Press enter to execute the suggestion. Or press Ctrl+C to exit.");
std::io::stdin().read_line(&mut String::new()).unwrap();
for p in PRIVILEGE_LIST {
let _p = p.to_owned() + " ";
if command.starts_with(&_p) {
let command = command.replacen(p, "", 1);
let now = Instant::now();
2023-08-07 19:58:19 +02:00
let process = std::process::Command::new(p)
2023-08-01 20:29:02 +02:00
.arg(shell)
.arg("-c")
2023-08-09 20:47:28 +02:00
.arg(&command)
2023-08-08 21:48:29 +02:00
.stdout(Stdio::inherit())
2023-08-09 20:47:28 +02:00
.stderr(Stdio::inherit())
.spawn()
.expect("failed to execute process")
.wait()
.unwrap();
2023-08-07 19:58:19 +02:00
2023-08-09 20:47:28 +02:00
if process.success() {
2023-08-07 19:58:19 +02:00
return Ok(());
} else {
if now.elapsed() > Duration::from_secs(3) {
exit(1);
}
2023-08-09 20:47:28 +02:00
let process = std::process::Command::new(p)
.arg(shell)
.arg("-c")
.arg(command)
.output()
.expect("failed to execute process");
2023-08-08 20:33:49 +02:00
let error_msg = String::from_utf8_lossy(&process.stderr);
return Err(error_msg.to_string());
2023-08-07 19:58:19 +02:00
}
2023-08-01 20:29:02 +02:00
}
}
let now = Instant::now();
2023-08-07 19:58:19 +02:00
let process = std::process::Command::new(shell)
2023-08-01 20:29:02 +02:00
.arg("-c")
.arg(command)
.stdout(Stdio::inherit())
2023-08-09 20:47:28 +02:00
.stderr(Stdio::inherit())
.spawn()
.expect("failed to execute process")
.wait()
.unwrap();
2023-08-07 19:58:19 +02:00
2023-08-09 20:47:28 +02:00
if process.success() {
2023-08-07 20:21:02 +02:00
Ok(())
2023-08-07 19:58:19 +02:00
} else {
if now.elapsed() > Duration::from_secs(3) {
exit(1);
}
2023-08-09 20:47:28 +02:00
let process = std::process::Command::new(shell)
.arg("-c")
.arg(command)
.output()
.expect("failed to execute process");
2023-08-08 20:33:49 +02:00
let error_msg = String::from_utf8_lossy(&process.stderr);
Err(error_msg.to_string())
2023-08-07 19:58:19 +02:00
}
2023-08-01 20:29:02 +02:00
}