Fortran · 14954 bytes Raw Blame History
1 module ferp_output
2 !> Output formatting for FERP
3 !> All output functions are thread-safe via OMP critical sections
4 use ferp_kinds
5 use ferp_options
6 use, intrinsic :: iso_fortran_env, only: output_unit, error_unit
7 use, intrinsic :: iso_c_binding, only: c_int
8 implicit none
9 private
10
11 public :: print_match, print_count, print_filename
12 public :: print_context_line, print_separator
13 public :: print_binary_match, print_only_match
14 public :: print_match_colored
15 public :: stdout_is_tty
16
17 ! C interface for isatty
18 interface
19 function c_isatty(fd) bind(C, name="isatty")
20 import :: c_int
21 integer(c_int), value :: fd
22 integer(c_int) :: c_isatty
23 end function c_isatty
24 end interface
25
26 ! ANSI color codes
27 character(len=*), parameter :: COLOR_MATCH = char(27) // '[01;31m' ! Bold red
28 character(len=*), parameter :: COLOR_RESET = char(27) // '[0m'
29 character(len=*), parameter :: COLOR_FILENAME = char(27) // '[35m' ! Magenta
30 character(len=*), parameter :: COLOR_LINENUM = char(27) // '[32m' ! Green
31 character(len=*), parameter :: COLOR_SEP = char(27) // '[36m' ! Cyan
32
33 contains
34
35 function stdout_is_tty() result(is_tty)
36 !> Check if stdout is a terminal (for --color=auto)
37 logical :: is_tty
38 integer(c_int), parameter :: STDOUT_FILENO = 1
39
40 is_tty = (c_isatty(STDOUT_FILENO) /= 0)
41 end function stdout_is_tty
42
43 subroutine print_match(line, filename, line_num, byte_off, opts)
44 !> Print a matching line with appropriate prefixes
45 !> Thread-safe via OMP critical section
46 character(len=*), intent(in) :: line
47 character(len=*), intent(in) :: filename
48 integer, intent(in) :: line_num
49 integer(i64), intent(in) :: byte_off
50 type(grep_options), intent(in) :: opts
51 character(len=16) :: num_fmt
52
53 ! Quiet mode - no output
54 if (opts%quiet) return
55
56 !$omp critical(output_lock)
57 ! Print filename prefix
58 if (opts%show_filename .and. .not. opts%hide_filename) then
59 if (opts%null_after_filename) then
60 write(output_unit, '(A,A)', advance='no') trim(filename), char(0)
61 else
62 write(output_unit, '(A,A)', advance='no') trim(filename), ':'
63 end if
64 end if
65
66 ! Print line number prefix (use width for -T alignment)
67 if (opts%show_line_number) then
68 if (opts%initial_tab .and. opts%line_number_width > 1) then
69 write(num_fmt, '(A,I0,A)') '(I', opts%line_number_width, ',A)'
70 write(output_unit, num_fmt, advance='no') line_num, ':'
71 else
72 write(output_unit, '(I0,A)', advance='no') line_num, ':'
73 end if
74 end if
75
76 ! Print byte offset prefix
77 if (opts%show_byte_offset) then
78 write(output_unit, '(I0,A)', advance='no') byte_off, ':'
79 end if
80
81 ! Print tab alignment if requested (only when there's a prefix)
82 if (opts%initial_tab .and. &
83 ((opts%show_filename .and. .not. opts%hide_filename) .or. &
84 opts%show_line_number .or. opts%show_byte_offset)) then
85 write(output_unit, '(A)', advance='no') char(9) ! TAB
86 end if
87
88 ! Print the line (preserve trailing whitespace)
89 if (opts%null_data) then
90 write(output_unit, '(A,A)', advance='no') line, char(0)
91 else
92 write(output_unit, '(A)') line
93 end if
94
95 ! Line-buffered mode
96 if (opts%line_buffered) flush(output_unit)
97 !$omp end critical(output_lock)
98
99 end subroutine print_match
100
101 subroutine print_context_line(line, filename, line_num, byte_off, opts)
102 !> Print a context line (uses - instead of : as separator)
103 !> Thread-safe via OMP critical section
104 character(len=*), intent(in) :: line
105 character(len=*), intent(in) :: filename
106 integer, intent(in) :: line_num
107 integer(i64), intent(in) :: byte_off
108 type(grep_options), intent(in) :: opts
109 character(len=16) :: num_fmt
110
111 if (opts%quiet) return
112
113 !$omp critical(output_lock)
114 ! Print filename prefix with - separator
115 if (opts%show_filename .and. .not. opts%hide_filename) then
116 if (opts%null_after_filename) then
117 write(output_unit, '(A,A)', advance='no') trim(filename), char(0)
118 else
119 write(output_unit, '(A,A)', advance='no') trim(filename), '-'
120 end if
121 end if
122
123 ! Print line number prefix with - separator (use width for -T alignment)
124 if (opts%show_line_number) then
125 if (opts%initial_tab .and. opts%line_number_width > 1) then
126 write(num_fmt, '(A,I0,A)') '(I', opts%line_number_width, ',A)'
127 write(output_unit, num_fmt, advance='no') line_num, '-'
128 else
129 write(output_unit, '(I0,A)', advance='no') line_num, '-'
130 end if
131 end if
132
133 ! Print byte offset prefix with - separator
134 if (opts%show_byte_offset) then
135 write(output_unit, '(I0,A)', advance='no') byte_off, '-'
136 end if
137
138 ! Print tab alignment if requested (only when there's a prefix)
139 if (opts%initial_tab .and. &
140 ((opts%show_filename .and. .not. opts%hide_filename) .or. &
141 opts%show_line_number .or. opts%show_byte_offset)) then
142 write(output_unit, '(A)', advance='no') char(9)
143 end if
144
145 ! Print the line (preserve trailing whitespace)
146 if (opts%null_data) then
147 write(output_unit, '(A,A)', advance='no') line, char(0)
148 else
149 write(output_unit, '(A)') line
150 end if
151
152 ! Line-buffered mode
153 if (opts%line_buffered) flush(output_unit)
154 !$omp end critical(output_lock)
155
156 end subroutine print_context_line
157
158 subroutine print_separator(opts)
159 !> Print group separator between context groups
160 !> Thread-safe via OMP critical section
161 type(grep_options), intent(in) :: opts
162
163 if (opts%quiet) return
164 if (opts%no_group_separator) return
165
166 !$omp critical(output_lock)
167 write(output_unit, '(A)') trim(opts%group_separator)
168
169 ! Line-buffered mode
170 if (opts%line_buffered) flush(output_unit)
171 !$omp end critical(output_lock)
172
173 end subroutine print_separator
174
175 subroutine print_count(count, filename, opts)
176 !> Print match count (for -c option)
177 !> Thread-safe via OMP critical section
178 integer, intent(in) :: count
179 character(len=*), intent(in) :: filename
180 type(grep_options), intent(in) :: opts
181
182 if (opts%quiet) return
183
184 !$omp critical(output_lock)
185 if (opts%show_filename .and. .not. opts%hide_filename) then
186 if (opts%null_after_filename) then
187 write(output_unit, '(A,A,I0)') trim(filename), char(0), count
188 else
189 write(output_unit, '(A,A,I0)') trim(filename), ':', count
190 end if
191 else
192 write(output_unit, '(I0)') count
193 end if
194
195 ! Line-buffered mode
196 if (opts%line_buffered) flush(output_unit)
197 !$omp end critical(output_lock)
198
199 end subroutine print_count
200
201 subroutine print_filename(filename, opts)
202 !> Print just filename (for -l, -L options)
203 !> Thread-safe via OMP critical section
204 character(len=*), intent(in) :: filename
205 type(grep_options), intent(in) :: opts
206
207 if (opts%quiet) return
208
209 !$omp critical(output_lock)
210 if (opts%null_after_filename) then
211 write(output_unit, '(A,A)', advance='no') trim(filename), char(0)
212 else
213 write(output_unit, '(A)') trim(filename)
214 end if
215
216 ! Line-buffered mode
217 if (opts%line_buffered) flush(output_unit)
218 !$omp end critical(output_lock)
219
220 end subroutine print_filename
221
222 subroutine print_binary_match(filename, opts)
223 !> Print binary file match message
224 !> Thread-safe via OMP critical section
225 character(len=*), intent(in) :: filename
226 type(grep_options), intent(in) :: opts
227
228 if (opts%quiet) return
229
230 !$omp critical(output_lock)
231 write(output_unit, '(A)') 'Binary file ' // trim(filename) // ' matches'
232
233 ! Line-buffered mode
234 if (opts%line_buffered) flush(output_unit)
235 !$omp end critical(output_lock)
236
237 end subroutine print_binary_match
238
239 subroutine print_only_match(line, match_start, match_end, filename, line_num, byte_off, opts)
240 !> Print only the matched portion of a line (for -o option)
241 !> Thread-safe via OMP critical section
242 character(len=*), intent(in) :: line
243 integer, intent(in) :: match_start, match_end
244 character(len=*), intent(in) :: filename
245 integer, intent(in) :: line_num
246 integer(i64), intent(in) :: byte_off
247 type(grep_options), intent(in) :: opts
248 character(len=16) :: num_fmt
249
250 if (opts%quiet) return
251
252 !$omp critical(output_lock)
253 ! Print filename prefix
254 if (opts%show_filename .and. .not. opts%hide_filename) then
255 if (opts%null_after_filename) then
256 write(output_unit, '(A,A)', advance='no') trim(filename), char(0)
257 else
258 write(output_unit, '(A,A)', advance='no') trim(filename), ':'
259 end if
260 end if
261
262 ! Print line number prefix (use width for -T alignment)
263 if (opts%show_line_number) then
264 if (opts%initial_tab .and. opts%line_number_width > 1) then
265 write(num_fmt, '(A,I0,A)') '(I', opts%line_number_width, ',A)'
266 write(output_unit, num_fmt, advance='no') line_num, ':'
267 else
268 write(output_unit, '(I0,A)', advance='no') line_num, ':'
269 end if
270 end if
271
272 ! Print byte offset prefix (offset to start of match)
273 if (opts%show_byte_offset) then
274 write(output_unit, '(I0,A)', advance='no') byte_off + match_start - 1, ':'
275 end if
276
277 ! Print tab alignment if requested (only when there's a prefix)
278 if (opts%initial_tab .and. &
279 ((opts%show_filename .and. .not. opts%hide_filename) .or. &
280 opts%show_line_number .or. opts%show_byte_offset)) then
281 write(output_unit, '(A)', advance='no') char(9)
282 end if
283
284 ! Print just the matched portion
285 if (match_start >= 1 .and. match_end >= match_start .and. match_end <= len(line)) then
286 if (opts%null_data) then
287 write(output_unit, '(A,A)', advance='no') line(match_start:match_end), char(0)
288 else
289 write(output_unit, '(A)') line(match_start:match_end)
290 end if
291 end if
292
293 ! Line-buffered mode
294 if (opts%line_buffered) flush(output_unit)
295 !$omp end critical(output_lock)
296
297 end subroutine print_only_match
298
299 subroutine print_match_colored(line, filename, line_num, byte_off, opts, &
300 match_starts, match_ends, num_matches)
301 !> Print a matching line with colored highlighting of matches
302 !> Thread-safe via OMP critical section
303 character(len=*), intent(in) :: line
304 character(len=*), intent(in) :: filename
305 integer, intent(in) :: line_num
306 integer(i64), intent(in) :: byte_off
307 type(grep_options), intent(in) :: opts
308 integer, intent(in) :: match_starts(:), match_ends(:)
309 integer, intent(in) :: num_matches
310
311 integer :: i, pos, line_len
312 logical :: use_color
313 character(len=16) :: num_fmt
314
315 if (opts%quiet) return
316
317 ! Determine if we should use color
318 use_color = .false.
319 if (opts%color_mode == COLOR_ALWAYS) then
320 use_color = .true.
321 else if (opts%color_mode == COLOR_AUTO) then
322 use_color = stdout_is_tty()
323 end if
324
325 !$omp critical(output_lock)
326 ! Print filename prefix
327 if (opts%show_filename .and. .not. opts%hide_filename) then
328 if (use_color) then
329 write(output_unit, '(A)', advance='no') COLOR_FILENAME
330 end if
331 if (opts%null_after_filename) then
332 write(output_unit, '(A,A)', advance='no') trim(filename), char(0)
333 else
334 write(output_unit, '(A)', advance='no') trim(filename)
335 end if
336 if (use_color) then
337 write(output_unit, '(A)', advance='no') COLOR_RESET
338 end if
339 if (.not. opts%null_after_filename) then
340 if (use_color) then
341 write(output_unit, '(A,A,A)', advance='no') COLOR_SEP, ':', COLOR_RESET
342 else
343 write(output_unit, '(A)', advance='no') ':'
344 end if
345 end if
346 end if
347
348 ! Print line number prefix (use width for -T alignment)
349 if (opts%show_line_number) then
350 if (opts%initial_tab .and. opts%line_number_width > 1) then
351 write(num_fmt, '(A,I0,A)') '(I', opts%line_number_width, ')'
352 if (use_color) then
353 write(output_unit, '(A)', advance='no') COLOR_LINENUM
354 write(output_unit, num_fmt, advance='no') line_num
355 write(output_unit, '(A)', advance='no') COLOR_RESET
356 write(output_unit, '(A,A,A)', advance='no') COLOR_SEP, ':', COLOR_RESET
357 else
358 write(output_unit, num_fmt, advance='no') line_num
359 write(output_unit, '(A)', advance='no') ':'
360 end if
361 else
362 if (use_color) then
363 write(output_unit, '(A,I0,A)', advance='no') COLOR_LINENUM, line_num, COLOR_RESET
364 write(output_unit, '(A,A,A)', advance='no') COLOR_SEP, ':', COLOR_RESET
365 else
366 write(output_unit, '(I0,A)', advance='no') line_num, ':'
367 end if
368 end if
369 end if
370
371 ! Print byte offset prefix
372 if (opts%show_byte_offset) then
373 if (use_color) then
374 write(output_unit, '(A,I0,A)', advance='no') COLOR_LINENUM, byte_off, COLOR_RESET
375 write(output_unit, '(A,A,A)', advance='no') COLOR_SEP, ':', COLOR_RESET
376 else
377 write(output_unit, '(I0,A)', advance='no') byte_off, ':'
378 end if
379 end if
380
381 ! Print tab alignment if requested (only when there's a prefix)
382 if (opts%initial_tab .and. &
383 ((opts%show_filename .and. .not. opts%hide_filename) .or. &
384 opts%show_line_number .or. opts%show_byte_offset)) then
385 write(output_unit, '(A)', advance='no') char(9)
386 end if
387
388 ! Print line with highlighted matches
389 line_len = len_trim(line)
390 if (num_matches > 0 .and. use_color) then
391 pos = 1
392 do i = 1, num_matches
393 ! Print text before match
394 if (match_starts(i) > pos) then
395 write(output_unit, '(A)', advance='no') line(pos:match_starts(i)-1)
396 end if
397 ! Print highlighted match
398 if (match_starts(i) >= 1 .and. match_ends(i) <= line_len) then
399 write(output_unit, '(A)', advance='no') COLOR_MATCH
400 write(output_unit, '(A)', advance='no') line(match_starts(i):match_ends(i))
401 write(output_unit, '(A)', advance='no') COLOR_RESET
402 end if
403 pos = match_ends(i) + 1
404 end do
405 ! Print text after last match
406 if (pos <= line_len) then
407 if (opts%null_data) then
408 write(output_unit, '(A,A)', advance='no') line(pos:line_len), char(0)
409 else
410 write(output_unit, '(A)') line(pos:line_len)
411 end if
412 else
413 if (opts%null_data) then
414 write(output_unit, '(A)', advance='no') char(0)
415 else
416 write(output_unit, '(A)') ''
417 end if
418 end if
419 else
420 ! No color - just print the line (preserve trailing whitespace)
421 if (opts%null_data) then
422 write(output_unit, '(A,A)', advance='no') line, char(0)
423 else
424 write(output_unit, '(A)') line
425 end if
426 end if
427
428 ! Line-buffered mode
429 if (opts%line_buffered) flush(output_unit)
430 !$omp end critical(output_lock)
431
432 end subroutine print_match_colored
433
434 end module ferp_output
435