Rust · 6444 bytes Raw Blame History
1 use std::env;
2 use std::fs;
3 use std::path::PathBuf;
4
5 /// Provides helpful error messages and suggestions
6 ///
7 /// Features:
8 /// - "Did you mean?" suggestions for command-not-found errors
9 /// - Common typo detection
10 /// - Helpful hints for common mistakes
11 pub struct ErrorHints;
12
13 impl ErrorHints {
14 /// Get a helpful error message when a command is not found
15 pub fn command_not_found(command: &str) -> String {
16 let mut message = format!("command not found: {}", command);
17
18 // Try to find similar commands
19 if let Some(suggestion) = Self::find_similar_command(command) {
20 message.push_str(&format!("\n\nDid you mean '{}'?", suggestion));
21 }
22
23 // Check for common typos/mistakes
24 if let Some(hint) = Self::get_common_hint(command) {
25 message.push_str(&format!("\n\nHint: {}", hint));
26 }
27
28 message
29 }
30
31 /// Find a similar command in PATH using string distance
32 fn find_similar_command(command: &str) -> Option<String> {
33 let mut best_match: Option<(String, usize)> = None;
34 const MAX_DISTANCE: usize = 3; // Maximum Levenshtein distance to consider
35
36 // Get all commands from PATH
37 let commands = Self::get_all_commands();
38
39 for cmd in commands {
40 let distance = Self::levenshtein_distance(command, &cmd);
41
42 // Only consider commands with reasonable similarity
43 if distance <= MAX_DISTANCE {
44 if let Some((_, best_dist)) = &best_match {
45 if distance < *best_dist {
46 best_match = Some((cmd, distance));
47 }
48 } else {
49 best_match = Some((cmd, distance));
50 }
51 }
52 }
53
54 best_match.map(|(cmd, _)| cmd)
55 }
56
57 /// Get all available commands from PATH and built-ins
58 fn get_all_commands() -> Vec<String> {
59 let mut commands = Vec::new();
60
61 // Add built-ins
62 commands.extend_from_slice(&[
63 "cd".to_string(),
64 "pwd".to_string(),
65 "exit".to_string(),
66 "jobs".to_string(),
67 "fg".to_string(),
68 "bg".to_string(),
69 "test".to_string(),
70 ]);
71
72 // Add commands from PATH
73 if let Some(path_var) = env::var_os("PATH") {
74 for dir in env::split_paths(&path_var) {
75 if let Ok(entries) = fs::read_dir(dir) {
76 for entry in entries.flatten() {
77 if let Ok(file_type) = entry.file_type() {
78 if file_type.is_file() {
79 if let Ok(metadata) = entry.metadata() {
80 #[cfg(unix)]
81 {
82 use std::os::unix::fs::PermissionsExt;
83 if metadata.permissions().mode() & 0o111 != 0 {
84 if let Some(name) = entry.file_name().to_str() {
85 commands.push(name.to_string());
86 }
87 }
88 }
89 #[cfg(not(unix))]
90 {
91 if let Some(name) = entry.file_name().to_str() {
92 commands.push(name.to_string());
93 }
94 }
95 }
96 }
97 }
98 }
99 }
100 }
101 }
102
103 commands
104 }
105
106 /// Calculate Levenshtein distance between two strings
107 fn levenshtein_distance(s1: &str, s2: &str) -> usize {
108 let len1 = s1.len();
109 let len2 = s2.len();
110
111 if len1 == 0 {
112 return len2;
113 }
114 if len2 == 0 {
115 return len1;
116 }
117
118 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
119
120 // Initialize first column and row
121 for i in 0..=len1 {
122 matrix[i][0] = i;
123 }
124 for j in 0..=len2 {
125 matrix[0][j] = j;
126 }
127
128 // Fill the matrix
129 for (i, c1) in s1.chars().enumerate() {
130 for (j, c2) in s2.chars().enumerate() {
131 let cost = if c1 == c2 { 0 } else { 1 };
132 matrix[i + 1][j + 1] = std::cmp::min(
133 std::cmp::min(
134 matrix[i][j + 1] + 1, // deletion
135 matrix[i + 1][j] + 1, // insertion
136 ),
137 matrix[i][j] + cost, // substitution
138 );
139 }
140 }
141
142 matrix[len1][len2]
143 }
144
145 /// Get helpful hints for common mistakes
146 fn get_common_hint(command: &str) -> Option<String> {
147 // Common typos and hints
148 match command {
149 "sl" => Some("Did you mean 'ls'? (Common typo)".to_string()),
150 "gti" => Some("Did you mean 'git'? (Common typo)".to_string()),
151 "clea" | "claer" => Some("Did you mean 'clear'? (Common typo)".to_string()),
152 "exti" => Some("Did you mean 'exit'? (Common typo)".to_string()),
153 "grpe" => Some("Did you mean 'grep'? (Common typo)".to_string()),
154 _ if command.ends_with(".sh") && PathBuf::from(command).exists() => {
155 Some(format!("File exists. Try: rush {} or ./{}",command, command))
156 }
157 _ if PathBuf::from(format!("./{}", command)).exists() => {
158 Some(format!("File exists in current directory. Try: ./{}", command))
159 }
160 _ => None,
161 }
162 }
163 }
164
165 #[cfg(test)]
166 mod tests {
167 use super::*;
168
169 #[test]
170 fn test_levenshtein_distance() {
171 assert_eq!(ErrorHints::levenshtein_distance("cat", "cat"), 0);
172 assert_eq!(ErrorHints::levenshtein_distance("cat", "bat"), 1);
173 assert_eq!(ErrorHints::levenshtein_distance("cat", "cats"), 1);
174 assert_eq!(ErrorHints::levenshtein_distance("cat", "dog"), 3);
175 }
176
177 #[test]
178 fn test_common_hints() {
179 assert!(ErrorHints::get_common_hint("sl").is_some());
180 assert!(ErrorHints::get_common_hint("gti").is_some());
181 assert!(ErrorHints::get_common_hint("unknown_cmd_xyz").is_none());
182 }
183 }
184