pay-respects/src/suggestions.rs

309 lines
8.3 KiB
Rust
Raw Normal View History

2023-08-12 22:39:00 +02:00
use std::io::stderr;
use std::os::fd::AsFd;
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
2024-09-25 17:55:55 +02:00
use colored::Colorize;
2023-08-01 20:29:02 +02:00
use regex_lite::Regex;
2023-08-07 17:12:38 +02:00
use crate::files::{get_best_match_file, get_path_files};
use crate::rules::match_pattern;
2024-11-17 14:57:22 +01:00
use crate::shell::{expand_alias_multiline, 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()) {
2024-09-25 17:55:55 +02:00
true => split_command.get(1).expect(&t!("no-command")).as_str(),
false => split_command.first().expect(&t!("no-command")).as_str(),
2023-08-01 20:29:02 +02:00
};
if !PRIVILEGE_LIST.contains(&executable) {
let suggest = match_pattern("_PR_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);
}
let suggest = match_pattern("_PR_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);
}
2024-11-15 15:43:04 +01:00
#[cfg(feature = "runtime-rules")]
{
use crate::runtime_rules::runtime_match;
let suggest = runtime_match(executable, last_command, error_msg, shell);
if let Some(suggest) = suggest {
if PRIVILEGE_LIST.contains(&split_command[0].as_str()) {
return Some(format!("{} {}", split_command[0], suggest));
}
return Some(suggest);
}
}
2023-08-01 20:29:02 +02:00
None
}
pub fn check_executable(shell: &str, executable: &str) -> bool {
2023-08-08 16:57:14 +02:00
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
}
}
pub fn opt_regex(regex: &str, command: &mut String) -> String {
2023-08-03 21:26:41 +02:00
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
}
pub fn err_regex(regex: &str, error_msg: &str) -> String {
2023-08-04 00:18:31 +02:00
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(" ")
}
pub fn cmd_regex(regex: &str, command: &str) -> String {
2023-08-07 18:34:52 +02:00
let regex = Regex::new(regex).unwrap();
let err = regex
.find_iter(command)
.map(|cap| cap.as_str().to_owned())
.collect::<Vec<String>>();
err.join(" ")
}
pub fn eval_shell_command(shell: &str, command: &str) -> Vec<String> {
2023-08-04 00:18:31 +02:00
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"'\\]+|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\\ )+|\\|\n"#;
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
}
2024-11-15 15:43:04 +01:00
pub fn suggest_typo(typos: &[String], candidates: Vec<String>) -> String {
2023-08-11 00:41:16 +02:00
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
}
2024-11-15 15:43:04 +01:00
} else if let Some(suggest) = find_similar(typo, &candidates) {
2023-08-11 00:41:16 +02:00
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
}
2024-11-15 15:43:04 +01:00
pub fn find_similar(typo: &str, candidates: &Vec<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() {
2024-11-16 16:55:29 +01:00
if candidate.is_empty() {
continue;
}
2023-08-01 20:29:02 +02:00
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 {
2023-08-01 20:29:02 +02:00
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> {
2023-08-12 22:39:00 +02:00
eprintln!("{}\n", highlighted);
2024-09-25 17:55:55 +02:00
let confirm = format!("[{}]", t!("confirm-yes")).green();
eprintln!("{}: {} {}", t!("confirm"), confirm, "[Ctrl+C]".red());
2023-08-01 20:29:02 +02:00
std::io::stdin().read_line(&mut String::new()).unwrap();
for p in PRIVILEGE_LIST {
let _p = p.to_owned() + " ";
2024-09-25 18:30:03 +02:00
if !command.starts_with(&_p) {
continue;
}
2024-11-16 00:25:28 +01:00
let mut command = command.replacen(&_p, "", 1);
2024-11-17 14:57:22 +01:00
command = expand_alias_multiline(shell, &command);
2024-09-25 18:30:03 +02:00
let now = Instant::now();
let process = std::process::Command::new(p)
.arg(shell)
.arg("-c")
.arg(&command)
// .stdout(Stdio::inherit())
.stdout(stderr().as_fd().try_clone_to_owned().unwrap())
.stderr(Stdio::inherit())
.spawn()
.expect("failed to execute process")
.wait()
.unwrap();
2024-09-25 18:30:03 +02:00
if process.success() {
println!("{}", shell_evaluated_commands(&command));
return Ok(());
} else {
if now.elapsed() > Duration::from_secs(3) {
exit(1);
}
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")
2024-09-25 18:30:03 +02:00
.arg(command)
.env("LC_ALL", "C")
.output()
.expect("failed to execute process");
let error_msg = match process.stderr.is_empty() {
true => String::from_utf8_lossy(&process.stdout),
false => String::from_utf8_lossy(&process.stderr),
};
2024-09-25 18:30:03 +02:00
return Err(error_msg.to_string());
2023-08-01 20:29:02 +02:00
}
}
2024-11-17 14:57:22 +01:00
let command = expand_alias_multiline(shell, command);
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")
2024-11-16 00:25:28 +01:00
.arg(&command)
2023-08-12 22:39:00 +02:00
// .stdout(Stdio::inherit())
.stdout(stderr().as_fd().try_clone_to_owned().unwrap())
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() {
println!("{}", shell_evaluated_commands(&command));
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)
2023-08-11 15:39:51 +02:00
.env("LC_ALL", "C")
2023-08-09 20:47:28 +02:00
.output()
.expect("failed to execute process");
let error_msg = match process.stderr.is_empty() {
2024-09-26 02:26:40 +02:00
true => String::from_utf8_lossy(&process.stdout).to_lowercase(),
false => String::from_utf8_lossy(&process.stderr).to_lowercase(),
};
2023-08-08 20:33:49 +02:00
Err(error_msg.to_string())
2023-08-07 19:58:19 +02:00
}
2023-08-01 20:29:02 +02:00
}
fn shell_evaluated_commands(command: &str) -> String {
let lines = command
.lines()
.map(|line| line.trim().trim_end_matches(['\\', ';', '|', '&']))
.collect::<Vec<&str>>();
let mut commands = Vec::new();
for line in lines {
2024-09-25 18:30:03 +02:00
if line.starts_with("cd ") {
commands.push(line.to_string());
}
}
2024-11-16 00:25:28 +01:00
commands.join(" && \\ \n")
}