use regex_lite::Regex; use std::collections::HashMap; use rule_parser::parse_rules; use crate::shell::{command_output, PRIVILEGE_LIST}; use crate::style::highlight_difference; pub fn suggest_command(shell: &str, last_command: &str) -> Option { let err = command_output(shell, last_command); 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) { let suggest = match_pattern("privilege", last_command, &err); if let Some(suggest) = suggest { let suggest = eval_suggest(&suggest, last_command); return Some(suggest); } } let suggest = match_pattern(executable, last_command, &err); if let Some(suggest) = suggest { let suggest = eval_suggest(&suggest, last_command); if PRIVILEGE_LIST.contains(&executable) { return Some(format!("{} {}", split_command[0], suggest)); } return Some(suggest); } let suggest = match_pattern("general", last_command, &err); if let Some(suggest) = suggest { let suggest = eval_suggest(&suggest, last_command); return Some(suggest); } None } fn match_pattern(executable: &str, command: &str, error_msg: &str) -> Option { let rules = parse_rules!("rules"); if rules.contains_key(executable) { let suggest = rules.get(executable).unwrap(); for (pattern, suggest) in suggest { for pattern in pattern { if error_msg.contains(pattern) { for suggest in suggest { if let Some(suggest) = check_condition(suggest, command, error_msg) { return Some(suggest); } } } } } None } else { None } } fn check_condition(suggest: &str, command: &str, error_msg: &str) -> Option { if !suggest.starts_with('#') { return Some(suggest.to_owned()); } let mut lines = suggest.lines().collect::>(); let mut conditions = String::new(); for (i, line) in lines[0..].iter().enumerate() { conditions.push_str(line); if line.ends_with(']') { lines = lines[i..].to_vec(); break; } } let conditions = conditions .trim_start_matches(['#', '[']) .trim_end_matches(']') .split(',') .collect::>(); for condition in conditions { let (mut condition, arg) = condition.split_once('(').unwrap(); condition = condition.trim(); let arg = arg.trim_start_matches('(').trim_end_matches(')'); let reverse = match condition.starts_with('!') { true => { condition = condition.trim_start_matches('!'); true } false => false, }; if eval_condition(condition, arg, command, error_msg) == reverse { return None; } } Some(lines[1..].join("\n")) } fn eval_condition(condition: &str, arg: &str, command: &str, error_msg: &str) -> bool { match condition { "executable" => { let output = std::process::Command::new("which") .arg(arg) .output() .expect("failed to execute process"); output.status.success() } "err_contains" => error_msg.contains(arg), "cmd_contains" => command.contains(arg), _ => unreachable!("Unknown condition when evaluation condition: {}", condition), } } fn eval_suggest(suggest: &str, last_command: &str) -> String { let mut suggest = suggest.to_owned(); let mut last_command = last_command.to_owned(); if suggest.contains("{{command}}") { suggest = suggest.replace("{{command}}", &last_command); } while suggest.contains("{{opt::") { let placeholder_start = "{{opt::"; let placeholder_end = "}}"; let start_index = suggest.find(placeholder_start).unwrap(); let end_index = suggest[start_index..].find(placeholder_end).unwrap() + start_index + placeholder_end.len(); let placeholder = start_index..end_index; let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); let opt = &suggest[args.to_owned()]; let regex = opt.trim(); let regex = Regex::new(regex).unwrap(); let opts = regex .find_iter(&last_command) .map(|cap| cap.as_str().to_owned()) .collect::>(); suggest.replace_range(placeholder, &opts.join(" ")); for opt in opts { last_command = last_command.replace(&opt, ""); } } let split_command = split_command(&last_command); while suggest.contains("{{command") { let placeholder_start = "{{command"; let placeholder_end = "}}"; let start_index = suggest.find(placeholder_start).unwrap(); let end_index = suggest[start_index..].find(placeholder_end).unwrap() + start_index + placeholder_end.len(); let placeholder = start_index..end_index; let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); let range = suggest[args.to_owned()].trim_matches(|c| c == '[' || c == ']'); if let Some((start, end)) = range.split_once(':') { let start = { let mut start = start.parse::().unwrap_or(0); if start < 0 { start += split_command.len() as i32; } start as usize }; let end = { let mut end = end.parse::().unwrap_or(split_command.len() as i32 - 1) + 1; if end < 0 { end += split_command.len() as i32; } end as usize }; let command = split_command[start..end].join(" "); suggest.replace_range(placeholder, &command); } else { let range = range.parse::().unwrap_or(0); let command = split_command[range].to_owned(); suggest.replace_range(placeholder, &command); } } while suggest.contains("{{typo") { let placeholder_start = "{{typo"; let placeholder_end = "}}"; let start_index = suggest.find(placeholder_start).unwrap(); let end_index = suggest[start_index..].find(placeholder_end).unwrap() + start_index + placeholder_end.len(); let placeholder = start_index..end_index; let args = start_index + placeholder_start.len()..end_index - placeholder_end.len(); let mut command_index; let mut match_list = vec![]; if suggest.contains('[') { let split = suggest[args.to_owned()] .split(&['[', ']']) .collect::>(); command_index = split[1].parse::().unwrap(); if command_index < 0 { command_index += split_command.len() as i32; } } else { unreachable!("Typo suggestion must have a command index"); } if suggest.contains('(') { let split = suggest[args.to_owned()] .split(&['(', ')']) .collect::>(); match_list = split[1].split(',').collect::>(); } let match_list = match_list .iter() .map(|s| s.trim().to_string()) .collect::>(); let command_index = command_index as usize; let suggestion = suggest_typo(&split_command[command_index], match_list.clone()); suggest.replace_range(placeholder, &suggestion); } suggest } pub fn split_command(command: &str) -> Vec { let regex = r#"([^\s"\\]+|"(?:\\.|[^"\\])*"|\\.)+"#; let regex = Regex::new(regex).unwrap(); let split_command = regex .find_iter(command) .map(|cap| cap.as_str().to_owned()) .collect::>(); split_command } fn suggest_typo(typo: &str, candidates: Vec) -> String { let mut suggestion = typo.to_owned(); if candidates.len() == 1 { match candidates[0].as_str() { "path" => { let path_files = get_path_files(); if let Some(suggest) = find_fimilar(typo, path_files) { suggestion = suggest; } } "file" => { let files = get_directory_files(typo); if let Some(suggest) = find_fimilar(typo, files) { suggestion = suggest; } } _ => {} } } else if let Some(suggest) = find_fimilar(typo, candidates) { suggestion = suggest; } suggestion } fn get_path_files() -> Vec { let path = std::env::var("PATH").unwrap(); let path = path.split(':').collect::>(); let mut all_executable = vec![]; for p in path { let files = match std::fs::read_dir(p) { Ok(files) => files, Err(_) => continue, }; for file in files { let file = file.unwrap(); let file_name = file.file_name().into_string().unwrap(); all_executable.push(file_name); } } all_executable } fn get_directory_files(input: &str) -> Vec { let mut input = input.trim_matches(|c| c == '\'' || c == '"').to_owned(); let files = loop { match std::fs::read_dir(&input) { Ok(files) => break files, Err(_) => { if let Some((dirs, _)) = input.rsplit_once('/') { input = dirs.to_owned(); } else { break std::fs::read_dir("./").unwrap(); } } } }; let mut all_files = vec![]; for file in files { let file = file.unwrap(); let file_name = file.path().to_str().unwrap().to_owned(); all_files.push(file_name); } all_files } fn find_fimilar(typo: &str, candidates: Vec) -> Option { 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()] } pub fn confirm_suggestion(shell: &str, command: &str, last_command: &str) { println!("{}\n", highlight_difference(command, last_command)); 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.replace(p, ""); std::process::Command::new(p) .arg(shell) .arg("-c") .arg(command) .spawn() .expect("failed to execute process") .wait() .expect("failed to wait on process"); return; } } std::process::Command::new(shell) .arg("-c") .arg(command) .spawn() .expect("failed to execute process") .wait() .expect("failed to wait on process"); }