Fortran · 10702 bytes Raw Blame History
1 ! ==============================================================================
2 ! Module: better_errors
3 ! Purpose: Enhanced error messages with helpful suggestions
4 ! ==============================================================================
5 module better_errors
6 use iso_fortran_env, only: error_unit
7 use system_interface, only: get_environment_var, c_isatty
8 use io_helpers, only: write_stderr
9 use iso_c_binding, only: c_int
10 implicit none
11 private
12
13 ! Public interface
14 public :: show_command_not_found_error
15 public :: suggest_similar_commands
16 public :: levenshtein_distance
17
18 ! ANSI color codes for errors
19 integer, parameter :: COLOR_RED = 31
20 integer, parameter :: COLOR_YELLOW = 33
21 integer, parameter :: COLOR_CYAN = 36
22 integer, parameter :: COLOR_GREEN = 32
23 integer, parameter :: COLOR_RESET = 0
24
25 ! Maximum suggestions to show
26 integer, parameter :: MAX_SUGGESTIONS = 3
27 integer, parameter :: MAX_EDIT_DISTANCE = 3
28
29 contains
30
31 ! Show enhanced "command not found" error with suggestions
32 subroutine show_command_not_found_error(command)
33 use system_interface, only: file_exists
34 character(len=*), intent(in) :: command
35 character(len=256), allocatable :: suggestions(:)
36 integer :: num_suggestions, i
37 character(len=10) :: shell_name
38 character(len=64) :: error_msg
39
40 ! Use "sh" for POSIX compliance in non-interactive mode
41 if (stderr_is_tty()) then
42 shell_name = "fortsh"
43 else
44 shell_name = "sh"
45 end if
46
47 ! POSIX: For paths (containing /), use "No such file or directory" if file doesn't exist
48 ! Use "command not found" only for bare command names searched in PATH
49 if (index(command, '/') > 0) then
50 if (.not. file_exists(trim(command))) then
51 error_msg = "No such file or directory"
52 else
53 error_msg = "Permission denied"
54 end if
55 else
56 error_msg = "command not found"
57 end if
58
59 ! Print main error message in red (POSIX format)
60 if (stderr_is_tty()) then
61 call write_stderr(trim(color_code(COLOR_RED)) // &
62 trim(shell_name) // ': ' // trim(command) // ': ' // trim(error_msg))
63 call write_stderr(trim(color_code(COLOR_RESET)))
64 else
65 call write_stderr(trim(shell_name) // ': ' // trim(command) // ': ' // trim(error_msg))
66 end if
67
68 ! Only show suggestions if stderr is a TTY (interactive mode)
69 if (.not. stderr_is_tty()) return
70
71 ! Try to find similar commands
72 call suggest_similar_commands(command, suggestions, num_suggestions)
73
74 if (num_suggestions > 0) then
75 ! Print suggestions
76 write(error_unit, '(a)', advance='no') trim(color_code(COLOR_CYAN))
77 write(error_unit, '(a)', advance='no') "Did you mean"
78
79 if (num_suggestions == 1) then
80 write(error_unit, '(a)', advance='no') " '"
81 write(error_unit, '(a)', advance='no') trim(suggestions(1))
82 write(error_unit, '(a)') "'?"
83 else
84 write(error_unit, '(a)') ":"
85 do i = 1, num_suggestions
86 write(error_unit, '(a)', advance='no') " "
87 write(error_unit, '(a)', advance='no') trim(color_code(COLOR_GREEN))
88 write(error_unit, '(a)', advance='no') trim(suggestions(i))
89 write(error_unit, '(a)') trim(color_code(COLOR_CYAN))
90 end do
91 end if
92 write(error_unit, '(a)') trim(color_code(COLOR_RESET))
93 end if
94
95 ! Cleanup
96 if (allocated(suggestions)) deallocate(suggestions)
97 end subroutine
98
99 ! Find similar commands in PATH and builtins
100 subroutine suggest_similar_commands(command, suggestions, num_suggestions)
101 character(len=*), intent(in) :: command
102 character(len=256), allocatable, intent(out) :: suggestions(:)
103 integer, intent(out) :: num_suggestions
104
105 character(len=256), allocatable :: candidates(:)
106 integer, allocatable :: distances(:)
107 integer :: num_candidates, i, min_dist
108 character(len=256) :: temp_suggestions(MAX_SUGGESTIONS)
109
110 ! Get candidate commands
111 call get_command_candidates(candidates, num_candidates)
112
113 if (num_candidates == 0) then
114 num_suggestions = 0
115 return
116 end if
117
118 ! Allocate distances array
119 allocate(distances(num_candidates))
120
121 ! Calculate edit distance for each candidate
122 do i = 1, num_candidates
123 distances(i) = levenshtein_distance(command, candidates(i))
124 end do
125
126 ! Find commands within acceptable edit distance
127 min_dist = minval(distances)
128 num_suggestions = 0
129
130 ! Only suggest if distance is reasonable
131 if (min_dist > MAX_EDIT_DISTANCE) then
132 deallocate(candidates, distances)
133 return
134 end if
135
136 ! Collect suggestions (up to MAX_SUGGESTIONS)
137 do i = 1, num_candidates
138 if (num_suggestions >= MAX_SUGGESTIONS) exit
139
140 ! Include commands with distance <= min_dist + 1
141 if (distances(i) <= min(min_dist + 1, MAX_EDIT_DISTANCE)) then
142 num_suggestions = num_suggestions + 1
143 temp_suggestions(num_suggestions) = trim(candidates(i))
144 end if
145 end do
146
147 ! Copy to output
148 if (num_suggestions > 0) then
149 allocate(suggestions(num_suggestions))
150 do i = 1, num_suggestions
151 suggestions(i) = temp_suggestions(i)
152 end do
153 end if
154
155 ! Cleanup
156 deallocate(candidates, distances)
157 end subroutine
158
159 ! Get list of candidate commands (builtins + PATH)
160 subroutine get_command_candidates(candidates, num_candidates)
161 character(len=256), allocatable, intent(out) :: candidates(:)
162 integer, intent(out) :: num_candidates
163
164 character(len=256), allocatable :: temp_candidates(:)
165 character(len=:), allocatable :: path_env
166 character(len=1024) :: dir
167 integer :: max_candidates, path_start, path_end, colon_pos
168 logical :: dir_exists
169
170 max_candidates = 1000
171 allocate(temp_candidates(max_candidates))
172 num_candidates = 0
173
174 ! Add common builtins
175 call add_builtins(temp_candidates, num_candidates, max_candidates)
176
177 ! Get PATH
178 path_env = get_environment_var('PATH')
179 if (.not. allocated(path_env) .or. len_trim(path_env) == 0) then
180 ! Just return builtins
181 allocate(candidates(num_candidates))
182 candidates(1:num_candidates) = temp_candidates(1:num_candidates)
183 deallocate(temp_candidates)
184 return
185 end if
186
187 ! Search PATH directories for executables
188 path_start = 1
189 do while (path_start <= len_trim(path_env) .and. num_candidates < max_candidates)
190 ! Find next colon
191 colon_pos = index(path_env(path_start:), ':')
192 if (colon_pos > 0) then
193 path_end = path_start + colon_pos - 2
194 else
195 path_end = len_trim(path_env)
196 end if
197
198 ! Extract directory
199 dir = path_env(path_start:path_end)
200
201 ! Check if directory exists (simple check)
202 inquire(file=trim(dir), exist=dir_exists)
203 if (dir_exists) then
204 ! Try to list files in directory using ls
205 ! This is a simplified version - in production, use directory listing
206 ! For now, just add a few common commands
207 if (num_candidates < max_candidates) then
208 ! Just add some known commands for demonstration
209 ! In full implementation, would scan directory
210 end if
211 end if
212
213 ! Move to next directory
214 if (colon_pos > 0) then
215 path_start = path_start + colon_pos
216 else
217 exit
218 end if
219 end do
220
221 ! Copy to output
222 allocate(candidates(num_candidates))
223 candidates(1:num_candidates) = temp_candidates(1:num_candidates)
224 deallocate(temp_candidates)
225 end subroutine
226
227 ! Add builtin commands to candidate list
228 subroutine add_builtins(candidates, num, max_count)
229 character(len=*), intent(inout) :: candidates(:)
230 integer, intent(inout) :: num
231 integer, intent(in) :: max_count
232
233 character(len=20) :: builtins(50)
234 integer :: i, n_builtins
235
236 ! Common builtins that users might typo
237 builtins = [ &
238 'cd ', 'ls ', 'echo ', 'pwd ', 'exit ', &
239 'export ', 'set ', 'unset ', 'alias ', 'unalias ', &
240 'source ', 'history ', 'jobs ', 'fg ', 'bg ', &
241 'kill ', 'wait ', 'read ', 'printf ', 'test ', &
242 'type ', 'command ', 'builtin ', 'declare ', 'local ', &
243 'return ', 'shift ', 'break ', 'continue', 'eval ', &
244 'exec ', 'trap ', 'ulimit ', 'umask ', 'getopts ', &
245 'hash ', 'help ', 'fc ', 'complete', 'compgen ', &
246 'git ', 'grep ', 'find ', 'sed ', 'awk ', &
247 'cat ', 'less ', 'more ', 'vim ', 'nano ' &
248 ]
249 n_builtins = 50
250
251 do i = 1, n_builtins
252 if (num >= max_count) exit
253 num = num + 1
254 candidates(num) = trim(builtins(i))
255 end do
256 end subroutine
257
258 ! Calculate Levenshtein distance (edit distance) between two strings
259 function levenshtein_distance(s1, s2) result(distance)
260 character(len=*), intent(in) :: s1, s2
261 integer :: distance
262
263 integer :: len1, len2, i, j, cost
264 integer, allocatable :: matrix(:,:)
265
266 len1 = len_trim(s1)
267 len2 = len_trim(s2)
268
269 ! Handle empty strings
270 if (len1 == 0) then
271 distance = len2
272 return
273 end if
274 if (len2 == 0) then
275 distance = len1
276 return
277 end if
278
279 ! Allocate matrix (0:len1, 0:len2)
280 allocate(matrix(0:len1, 0:len2))
281
282 ! Initialize first row and column
283 do i = 0, len1
284 matrix(i, 0) = i
285 end do
286 do j = 0, len2
287 matrix(0, j) = j
288 end do
289
290 ! Fill matrix using dynamic programming
291 do j = 1, len2
292 do i = 1, len1
293 if (s1(i:i) == s2(j:j)) then
294 cost = 0
295 else
296 cost = 1
297 end if
298
299 matrix(i, j) = min( &
300 matrix(i-1, j) + 1, & ! Deletion
301 matrix(i, j-1) + 1, & ! Insertion
302 matrix(i-1, j-1) + cost & ! Substitution
303 )
304 end do
305 end do
306
307 distance = matrix(len1, len2)
308 deallocate(matrix)
309 end function
310
311 ! Check if stderr is a TTY (terminal)
312 function stderr_is_tty() result(is_tty)
313 logical :: is_tty
314 integer(c_int) :: result
315
316 ! error_unit is typically 0 or 2 depending on implementation
317 ! Standard error is file descriptor 2
318 result = c_isatty(int(2, c_int))
319 is_tty = (result /= 0)
320 end function
321
322 ! Generate ANSI color code
323 function color_code(color) result(code)
324 integer, intent(in) :: color
325 character(len=20) :: code ! Increased to handle i15 format + escape sequences
326
327 ! Only use colors if stderr is a TTY
328 if (.not. stderr_is_tty()) then
329 code = ''
330 return
331 end if
332
333 if (color == COLOR_RESET) then
334 code = char(27) // '[0m'
335 else
336 write(code, '(a,i15,a)') char(27) // '[', color, 'm'
337 end if
338 end function
339
340 end module better_errors
341