Fortran · 30606 bytes Raw Blame History
1 module ferp_cli
2 !> Command-line argument parsing for FERP
3 use ferp_kinds
4 use ferp_options
5 use, intrinsic :: iso_fortran_env, only: error_unit
6 use, intrinsic :: iso_c_binding, only: c_int
7 implicit none
8 private
9
10 public :: parse_arguments, print_help, print_version
11
12 character(len=*), parameter :: VERSION = '0.1.0'
13
14 interface
15 subroutine c_exit(status) bind(C, name="exit")
16 import :: c_int
17 integer(c_int), value :: status
18 end subroutine c_exit
19 end interface
20
21 contains
22
23 subroutine parse_arguments(opts, patterns, files, ierr)
24 !> Parse command-line arguments into options, patterns, and files
25 type(grep_options), intent(out) :: opts
26 character(len=max_pattern_len), allocatable, intent(out) :: patterns(:)
27 character(len=max_path_len), allocatable, intent(out) :: files(:)
28 integer, intent(out) :: ierr
29
30 integer :: nargs, i, arg_len
31 character(len=max_path_len) :: arg
32 logical :: has_explicit_pattern
33 logical :: end_of_options
34 logical :: need_arg
35 character(len=32) :: pending_option
36
37 ierr = 0
38 nargs = command_argument_count()
39 has_explicit_pattern = .false.
40 end_of_options = .false.
41 need_arg = .false.
42 pending_option = ''
43
44 allocate(patterns(0))
45 allocate(files(0))
46
47 i = 1
48 do while (i <= nargs)
49 call get_command_argument(i, arg, arg_len)
50
51 ! Handle pending option that needs an argument
52 if (need_arg) then
53 call handle_option_argument(opts, patterns, pending_option, arg, &
54 has_explicit_pattern, ierr)
55 if (ierr /= 0) return
56 need_arg = .false.
57 pending_option = ''
58 i = i + 1
59 cycle
60 end if
61
62 ! After --, everything is a file/pattern argument
63 if (end_of_options) then
64 if (.not. has_explicit_pattern .and. size(patterns) == 0) then
65 ! Use exact length from get_command_argument to preserve whitespace patterns
66 call append_pattern(patterns, arg(1:arg_len))
67 has_explicit_pattern = .true.
68 else
69 call append_file(files, trim(arg))
70 end if
71 i = i + 1
72 cycle
73 end if
74
75 ! Check for options
76 if (arg(1:1) == '-' .and. arg_len > 1) then
77 if (arg(1:2) == '--') then
78 ! Long option
79 if (arg_len == 2) then
80 ! -- marks end of options
81 end_of_options = .true.
82 else
83 call parse_long_option(opts, patterns, arg(3:), &
84 has_explicit_pattern, need_arg, pending_option, ierr)
85 if (ierr /= 0) return
86 end if
87 else if (is_numeric_option(arg(2:))) then
88 ! -NUM shorthand for context (e.g., -3 = -C 3)
89 call handle_numeric_context(opts, arg(2:), ierr)
90 if (ierr /= 0) return
91 else
92 ! Short option(s)
93 call parse_short_options(opts, patterns, arg(2:), &
94 has_explicit_pattern, need_arg, pending_option, ierr)
95 if (ierr /= 0) return
96 end if
97 else
98 ! Non-option argument
99 if (.not. has_explicit_pattern .and. size(patterns) == 0) then
100 ! Use exact length from get_command_argument to preserve whitespace patterns
101 call append_pattern(patterns, arg(1:arg_len))
102 has_explicit_pattern = .true.
103 else
104 call append_file(files, trim(arg))
105 end if
106 end if
107
108 i = i + 1
109 end do
110
111 ! Check for missing required argument
112 if (need_arg) then
113 write(error_unit, '(A)') 'ferp: option requires an argument -- ' // trim(pending_option)
114 ierr = 2
115 return
116 end if
117
118 ! Validate: need at least one pattern source (but empty pattern file is valid)
119 if (.not. has_explicit_pattern) then
120 write(error_unit, '(A)') 'ferp: no pattern specified'
121 write(error_unit, '(A)') "Try 'ferp --help' for more information."
122 ierr = 2
123 return
124 end if
125
126 ! Set multiple_files flag
127 opts%multiple_files = (size(files) > 1)
128
129 ! Default filename display behavior
130 if (.not. opts%hide_filename) then
131 if (opts%multiple_files .or. opts%recursive) then
132 opts%show_filename = .true.
133 end if
134 end if
135
136 end subroutine parse_arguments
137
138 subroutine parse_short_options(opts, patterns, optstr, has_pattern, need_arg, pending, ierr)
139 type(grep_options), intent(inout) :: opts
140 character(len=max_pattern_len), allocatable, intent(inout) :: patterns(:)
141 character(len=*), intent(in) :: optstr
142 logical, intent(inout) :: has_pattern
143 logical, intent(out) :: need_arg
144 character(len=32), intent(out) :: pending
145 integer, intent(out) :: ierr
146
147 integer :: j
148 character(len=1) :: c
149
150 ierr = 0
151 need_arg = .false.
152 pending = ''
153
154 do j = 1, len_trim(optstr)
155 c = optstr(j:j)
156
157 select case (c)
158 ! Pattern type
159 case ('E')
160 opts%pattern_type = PATTERN_ERE
161 case ('F')
162 opts%pattern_type = PATTERN_FIXED
163 case ('G')
164 opts%pattern_type = PATTERN_BRE
165 case ('P')
166 opts%pattern_type = PATTERN_PERL
167
168 ! Matching control
169 case ('i', 'y') ! -y is obsolete synonym for -i
170 opts%ignore_case = .true.
171 case ('v')
172 opts%invert_match = .true.
173 case ('w')
174 opts%word_regexp = .true.
175 case ('x')
176 opts%line_regexp = .true.
177
178 ! Output control
179 case ('c')
180 opts%count_only = .true.
181 case ('l')
182 opts%files_with_matches = .true.
183 case ('L')
184 opts%files_without_match = .true.
185 case ('o')
186 opts%only_matching = .true.
187 case ('q')
188 opts%quiet = .true.
189 case ('s')
190 opts%no_messages = .true.
191
192 ! Line prefix
193 case ('n')
194 opts%show_line_number = .true.
195 case ('b')
196 opts%show_byte_offset = .true.
197 case ('H')
198 opts%show_filename = .true.
199 case ('h')
200 opts%hide_filename = .true.
201 case ('Z')
202 opts%null_after_filename = .true.
203 case ('T')
204 opts%initial_tab = .true.
205
206 ! File selection
207 case ('r')
208 opts%recursive = .true.
209 case ('R')
210 opts%recursive = .true.
211 opts%dereference_recursive = .true.
212
213 ! Binary
214 case ('a')
215 opts%text_mode = .true.
216 case ('I')
217 opts%ignore_binary = .true.
218 case ('U')
219 opts%text_mode = .false.
220
221 ! Null-data mode
222 case ('z')
223 opts%null_data = .true.
224
225 ! Options requiring arguments for directory/device action
226 case ('d')
227 if (j < len_trim(optstr)) then
228 call handle_option_argument(opts, patterns, 'd', optstr(j+1:), has_pattern, ierr)
229 return
230 end if
231 need_arg = .true.
232 pending = 'd'
233 return
234 case ('D')
235 if (j < len_trim(optstr)) then
236 call handle_option_argument(opts, patterns, 'D', optstr(j+1:), has_pattern, ierr)
237 return
238 end if
239 need_arg = .true.
240 pending = 'D'
241 return
242
243 ! Options requiring arguments
244 case ('e')
245 if (j < len_trim(optstr)) then
246 call handle_option_argument(opts, patterns, 'e', optstr(j+1:), has_pattern, ierr)
247 return
248 end if
249 need_arg = .true.
250 pending = 'e'
251 return
252 case ('f')
253 if (j < len_trim(optstr)) then
254 call handle_option_argument(opts, patterns, 'f', optstr(j+1:), has_pattern, ierr)
255 return
256 end if
257 need_arg = .true.
258 pending = 'f'
259 return
260 case ('m')
261 if (j < len_trim(optstr)) then
262 call handle_option_argument(opts, patterns, 'm', optstr(j+1:), has_pattern, ierr)
263 return
264 end if
265 need_arg = .true.
266 pending = 'm'
267 return
268 case ('A')
269 if (j < len_trim(optstr)) then
270 call handle_option_argument(opts, patterns, 'A', optstr(j+1:), has_pattern, ierr)
271 return
272 end if
273 need_arg = .true.
274 pending = 'A'
275 return
276 case ('B')
277 if (j < len_trim(optstr)) then
278 call handle_option_argument(opts, patterns, 'B', optstr(j+1:), has_pattern, ierr)
279 return
280 end if
281 need_arg = .true.
282 pending = 'B'
283 return
284 case ('C')
285 if (j < len_trim(optstr)) then
286 call handle_option_argument(opts, patterns, 'C', optstr(j+1:), has_pattern, ierr)
287 return
288 end if
289 need_arg = .true.
290 pending = 'C'
291 return
292
293 ! Help/version
294 case ('V')
295 call print_version()
296 call c_exit(0_c_int)
297
298 case default
299 write(error_unit, '(A)') "ferp: invalid option -- '" // c // "'"
300 write(error_unit, '(A)') "Try 'ferp --help' for more information."
301 ierr = 2
302 return
303 end select
304 end do
305 end subroutine parse_short_options
306
307 subroutine parse_long_option(opts, patterns, optstr, has_pattern, need_arg, pending, ierr)
308 type(grep_options), intent(inout) :: opts
309 character(len=max_pattern_len), allocatable, intent(inout) :: patterns(:)
310 character(len=*), intent(in) :: optstr
311 logical, intent(inout) :: has_pattern
312 logical, intent(out) :: need_arg
313 character(len=32), intent(out) :: pending
314 integer, intent(out) :: ierr
315
316 character(len=256) :: opt_name, opt_value
317 integer :: eq_pos
318
319 ierr = 0
320 need_arg = .false.
321 pending = ''
322
323 ! Split on '=' if present
324 eq_pos = index(optstr, '=')
325 if (eq_pos > 0) then
326 opt_name = optstr(1:eq_pos-1)
327 opt_value = optstr(eq_pos+1:)
328 else
329 opt_name = optstr
330 opt_value = ''
331 end if
332
333 select case (trim(opt_name))
334 ! Pattern type
335 case ('extended-regexp')
336 opts%pattern_type = PATTERN_ERE
337 case ('fixed-strings')
338 opts%pattern_type = PATTERN_FIXED
339 case ('basic-regexp')
340 opts%pattern_type = PATTERN_BRE
341 case ('perl-regexp')
342 opts%pattern_type = PATTERN_PERL
343
344 ! Matching control
345 case ('ignore-case')
346 opts%ignore_case = .true.
347 case ('no-ignore-case')
348 opts%ignore_case = .false.
349 case ('invert-match')
350 opts%invert_match = .true.
351 case ('word-regexp')
352 opts%word_regexp = .true.
353 case ('line-regexp')
354 opts%line_regexp = .true.
355
356 ! Output control
357 case ('count')
358 opts%count_only = .true.
359 case ('files-with-matches')
360 opts%files_with_matches = .true.
361 case ('files-without-match')
362 opts%files_without_match = .true.
363 case ('only-matching')
364 opts%only_matching = .true.
365 case ('quiet', 'silent')
366 opts%quiet = .true.
367 case ('no-messages')
368 opts%no_messages = .true.
369
370 ! Line prefix
371 case ('line-number')
372 opts%show_line_number = .true.
373 case ('byte-offset')
374 opts%show_byte_offset = .true.
375 case ('with-filename')
376 opts%show_filename = .true.
377 case ('no-filename')
378 opts%hide_filename = .true.
379 case ('null')
380 opts%null_after_filename = .true.
381 case ('initial-tab')
382 opts%initial_tab = .true.
383
384 ! Context
385 case ('after-context')
386 if (eq_pos > 0) then
387 read(opt_value, *, iostat=ierr) opts%after_context
388 else
389 need_arg = .true.
390 pending = 'after-context'
391 end if
392 case ('before-context')
393 if (eq_pos > 0) then
394 read(opt_value, *, iostat=ierr) opts%before_context
395 else
396 need_arg = .true.
397 pending = 'before-context'
398 end if
399 case ('context')
400 if (eq_pos > 0) then
401 read(opt_value, *, iostat=ierr) opts%before_context
402 opts%after_context = opts%before_context
403 else
404 need_arg = .true.
405 pending = 'context'
406 end if
407 case ('group-separator')
408 if (eq_pos > 0) then
409 opts%group_separator = opt_value(1:min(len(opt_value), 8))
410 else
411 need_arg = .true.
412 pending = 'group-separator'
413 end if
414 case ('no-group-separator')
415 opts%no_group_separator = .true.
416
417 ! File selection
418 case ('recursive')
419 opts%recursive = .true.
420 case ('dereference-recursive')
421 opts%recursive = .true.
422 opts%dereference_recursive = .true.
423 case ('include')
424 if (eq_pos > 0) then
425 if (opts%num_include_globs < MAX_GLOBS) then
426 opts%num_include_globs = opts%num_include_globs + 1
427 opts%include_globs(opts%num_include_globs) = trim(opt_value)
428 end if
429 else
430 need_arg = .true.
431 pending = 'include'
432 end if
433 case ('exclude')
434 if (eq_pos > 0) then
435 if (opts%num_exclude_globs < MAX_GLOBS) then
436 opts%num_exclude_globs = opts%num_exclude_globs + 1
437 opts%exclude_globs(opts%num_exclude_globs) = trim(opt_value)
438 end if
439 else
440 need_arg = .true.
441 pending = 'exclude'
442 end if
443 case ('exclude-dir')
444 if (eq_pos > 0) then
445 if (opts%num_exclude_dirs < MAX_GLOBS) then
446 opts%num_exclude_dirs = opts%num_exclude_dirs + 1
447 opts%exclude_dirs(opts%num_exclude_dirs) = trim(opt_value)
448 end if
449 else
450 need_arg = .true.
451 pending = 'exclude-dir'
452 end if
453 case ('exclude-from')
454 if (eq_pos > 0) then
455 opts%exclude_from_file = trim(opt_value)
456 else
457 need_arg = .true.
458 pending = 'exclude-from'
459 end if
460 case ('include-from')
461 if (eq_pos > 0) then
462 opts%include_from_file = trim(opt_value)
463 else
464 need_arg = .true.
465 pending = 'include-from'
466 end if
467
468 ! Pattern specification
469 case ('regexp')
470 if (eq_pos > 0) then
471 call append_pattern(patterns, trim(opt_value))
472 has_pattern = .true.
473 else
474 need_arg = .true.
475 pending = 'regexp'
476 end if
477
478 ! Binary
479 case ('text')
480 opts%text_mode = .true.
481 case ('binary')
482 opts%text_mode = .false.
483
484 ! Color
485 case ('color', 'colour')
486 if (eq_pos > 0) then
487 select case (trim(opt_value))
488 case ('never')
489 opts%color_mode = COLOR_NEVER
490 case ('always')
491 opts%color_mode = COLOR_ALWAYS
492 case ('auto')
493 opts%color_mode = COLOR_AUTO
494 end select
495 else
496 opts%color_mode = COLOR_ALWAYS
497 end if
498
499 ! Max count
500 case ('max-count')
501 if (eq_pos > 0) then
502 read(opt_value, *, iostat=ierr) opts%max_count
503 else
504 need_arg = .true.
505 pending = 'max-count'
506 end if
507
508 ! Label for stdin
509 case ('label')
510 if (eq_pos > 0) then
511 opts%label = trim(opt_value)
512 else
513 need_arg = .true.
514 pending = 'label'
515 end if
516
517 ! Binary file handling
518 case ('binary-files')
519 if (eq_pos > 0) then
520 select case (trim(opt_value))
521 case ('binary')
522 opts%text_mode = .false.
523 opts%ignore_binary = .false.
524 case ('without-match')
525 opts%ignore_binary = .true.
526 case ('text')
527 opts%text_mode = .true.
528 case default
529 write(error_unit, '(A)') "ferp: invalid --binary-files type: " // trim(opt_value)
530 ierr = 2
531 return
532 end select
533 else
534 need_arg = .true.
535 pending = 'binary-files'
536 end if
537
538 ! Output mode
539 case ('line-buffered')
540 opts%line_buffered = .true.
541
542 ! Null-data mode
543 case ('null-data')
544 opts%null_data = .true.
545
546 ! Directory/device action
547 case ('directories')
548 if (eq_pos > 0) then
549 select case (trim(opt_value))
550 case ('read')
551 opts%dir_action = DIR_READ
552 case ('skip')
553 opts%dir_action = DIR_SKIP
554 case ('recurse')
555 opts%dir_action = DIR_RECURSE
556 opts%recursive = .true.
557 case default
558 write(error_unit, '(A)') "ferp: invalid --directories action: " // trim(opt_value)
559 ierr = 2
560 return
561 end select
562 else
563 need_arg = .true.
564 pending = 'directories'
565 end if
566 case ('devices')
567 if (eq_pos > 0) then
568 select case (trim(opt_value))
569 case ('read')
570 opts%dev_action = DEV_READ
571 case ('skip')
572 opts%dev_action = DEV_SKIP
573 case default
574 write(error_unit, '(A)') "ferp: invalid --devices action: " // trim(opt_value)
575 ierr = 2
576 return
577 end select
578 else
579 need_arg = .true.
580 pending = 'devices'
581 end if
582
583 ! Help/version
584 case ('help')
585 call print_help()
586 call c_exit(0_c_int)
587 case ('version')
588 call print_version()
589 call c_exit(0_c_int)
590
591 case default
592 write(error_unit, '(A)') "ferp: unrecognized option '--" // trim(opt_name) // "'"
593 write(error_unit, '(A)') "Try 'ferp --help' for more information."
594 ierr = 2
595 return
596 end select
597 end subroutine parse_long_option
598
599 subroutine handle_option_argument(opts, patterns, opt, arg, has_pattern, ierr)
600 type(grep_options), intent(inout) :: opts
601 character(len=max_pattern_len), allocatable, intent(inout) :: patterns(:)
602 character(len=*), intent(in) :: opt, arg
603 logical, intent(inout) :: has_pattern
604 integer, intent(out) :: ierr
605
606 ierr = 0
607
608 select case (trim(opt))
609 case ('e', 'regexp')
610 call append_pattern(patterns, trim(arg))
611 has_pattern = .true.
612 case ('f', 'file')
613 call read_patterns_from_file(patterns, trim(arg), ierr)
614 if (ierr == 0) has_pattern = .true.
615 case ('m', 'max-count')
616 read(arg, *, iostat=ierr) opts%max_count
617 case ('A', 'after-context')
618 read(arg, *, iostat=ierr) opts%after_context
619 case ('B', 'before-context')
620 read(arg, *, iostat=ierr) opts%before_context
621 case ('C', 'context')
622 read(arg, *, iostat=ierr) opts%before_context
623 opts%after_context = opts%before_context
624 case ('group-separator')
625 opts%group_separator = arg(1:min(len_trim(arg), 8))
626 case ('include')
627 if (opts%num_include_globs < MAX_GLOBS) then
628 opts%num_include_globs = opts%num_include_globs + 1
629 opts%include_globs(opts%num_include_globs) = trim(arg)
630 end if
631 case ('exclude')
632 if (opts%num_exclude_globs < MAX_GLOBS) then
633 opts%num_exclude_globs = opts%num_exclude_globs + 1
634 opts%exclude_globs(opts%num_exclude_globs) = trim(arg)
635 end if
636 case ('exclude-dir')
637 if (opts%num_exclude_dirs < MAX_GLOBS) then
638 opts%num_exclude_dirs = opts%num_exclude_dirs + 1
639 opts%exclude_dirs(opts%num_exclude_dirs) = trim(arg)
640 end if
641 case ('exclude-from')
642 opts%exclude_from_file = trim(arg)
643 case ('include-from')
644 opts%include_from_file = trim(arg)
645 case ('label')
646 opts%label = trim(arg)
647 case ('binary-files')
648 select case (trim(arg))
649 case ('binary')
650 opts%text_mode = .false.
651 opts%ignore_binary = .false.
652 case ('without-match')
653 opts%ignore_binary = .true.
654 case ('text')
655 opts%text_mode = .true.
656 case default
657 write(error_unit, '(A)') "ferp: invalid --binary-files type: " // trim(arg)
658 ierr = 2
659 end select
660 case ('d', 'directories')
661 select case (trim(arg))
662 case ('read')
663 opts%dir_action = DIR_READ
664 case ('skip')
665 opts%dir_action = DIR_SKIP
666 case ('recurse')
667 opts%dir_action = DIR_RECURSE
668 opts%recursive = .true.
669 case default
670 write(error_unit, '(A)') "ferp: invalid --directories action: " // trim(arg)
671 ierr = 2
672 end select
673 case ('D', 'devices')
674 select case (trim(arg))
675 case ('read')
676 opts%dev_action = DEV_READ
677 case ('skip')
678 opts%dev_action = DEV_SKIP
679 case default
680 write(error_unit, '(A)') "ferp: invalid --devices action: " // trim(arg)
681 ierr = 2
682 end select
683 end select
684
685 if (ierr /= 0) then
686 write(error_unit, '(A)') 'ferp: invalid argument for option: ' // trim(opt)
687 ierr = 2
688 end if
689 end subroutine handle_option_argument
690
691 subroutine read_patterns_from_file(patterns, filename, ierr)
692 !> Read patterns from file, preserving exact line lengths (including whitespace-only lines)
693 character(len=max_pattern_len), allocatable, intent(inout) :: patterns(:)
694 character(len=*), intent(in) :: filename
695 integer, intent(out) :: ierr
696
697 integer :: unit_num, ios, line_len
698 character(len=max_pattern_len) :: line
699 character(len=1) :: ch
700
701 ierr = 0
702 ! Use unformatted stream access for byte-by-byte reading
703 open(newunit=unit_num, file=filename, status='old', action='read', &
704 access='stream', form='unformatted', iostat=ios)
705 if (ios /= 0) then
706 write(error_unit, '(A)') 'ferp: ' // trim(filename) // ': No such file or directory'
707 ierr = 2
708 return
709 end if
710
711 line_len = 0
712 line = ''
713
714 do
715 read(unit_num, iostat=ios) ch
716 if (ios /= 0) then
717 ! EOF or error - save current line if non-empty
718 if (line_len > 0) then
719 call append_pattern(patterns, line(1:line_len))
720 end if
721 exit
722 end if
723
724 if (ch == char(10)) then
725 ! Newline - save pattern with exact length (even if zero for empty lines)
726 if (line_len > 0) then
727 call append_pattern(patterns, line(1:line_len))
728 else
729 ! Empty line - skip (grep ignores empty pattern lines)
730 end if
731 line_len = 0
732 line = ''
733 else if (ch == char(13)) then
734 ! Carriage return - ignore (handle Windows line endings)
735 else
736 ! Regular character - add to line
737 if (line_len < max_pattern_len) then
738 line_len = line_len + 1
739 line(line_len:line_len) = ch
740 end if
741 end if
742 end do
743
744 close(unit_num)
745 end subroutine read_patterns_from_file
746
747 subroutine append_pattern(patterns, pattern)
748 !> Append a pattern to the patterns array, preserving its exact length
749 !> Uses null terminator to mark the true end of the pattern
750 character(len=max_pattern_len), allocatable, intent(inout) :: patterns(:)
751 character(len=*), intent(in) :: pattern
752
753 character(len=max_pattern_len), allocatable :: temp(:)
754 integer :: n, plen
755
756 n = size(patterns)
757 allocate(temp(n + 1))
758 if (n > 0) temp(1:n) = patterns
759
760 ! Store pattern with null terminator to preserve exact length
761 plen = len(pattern)
762 if (plen > 0 .and. plen < max_pattern_len) then
763 temp(n + 1) = pattern
764 temp(n + 1)(plen + 1:plen + 1) = char(0) ! Null terminator
765 else if (plen == 0) then
766 ! Empty pattern - store just null terminator
767 temp(n + 1) = char(0)
768 else
769 ! Pattern too long - truncate
770 temp(n + 1) = pattern(1:max_pattern_len)
771 end if
772
773 call move_alloc(temp, patterns)
774 end subroutine append_pattern
775
776 subroutine append_file(files, filename)
777 character(len=max_path_len), allocatable, intent(inout) :: files(:)
778 character(len=*), intent(in) :: filename
779
780 character(len=max_path_len), allocatable :: temp(:)
781 integer :: n
782
783 n = size(files)
784 allocate(temp(n + 1))
785 if (n > 0) temp(1:n) = files
786 temp(n + 1) = filename
787 call move_alloc(temp, files)
788 end subroutine append_file
789
790 subroutine print_help()
791 write(*, '(A)') 'Usage: ferp [OPTION]... PATTERN [FILE]...'
792 write(*, '(A)') 'Search for PATTERN in each FILE.'
793 write(*, '(A)') 'Example: ferp -i "hello world" menu.h main.c'
794 write(*, '(A)') ''
795 write(*, '(A)') 'Pattern selection and interpretation:'
796 write(*, '(A)') ' -E, --extended-regexp PATTERN is an extended regular expression'
797 write(*, '(A)') ' -F, --fixed-strings PATTERN is a set of newline-separated strings'
798 write(*, '(A)') ' -G, --basic-regexp PATTERN is a basic regular expression (default)'
799 write(*, '(A)') ' -P, --perl-regexp PATTERN is a Perl regular expression'
800 write(*, '(A)') ' -e, --regexp=PATTERN use PATTERN for matching'
801 write(*, '(A)') ' -f, --file=FILE obtain PATTERN from FILE'
802 write(*, '(A)') ' -i, --ignore-case ignore case distinctions'
803 write(*, '(A)') ' --no-ignore-case do not ignore case (default)'
804 write(*, '(A)') ' -w, --word-regexp force PATTERN to match only whole words'
805 write(*, '(A)') ' -x, --line-regexp force PATTERN to match only whole lines'
806 write(*, '(A)') ''
807 write(*, '(A)') 'Miscellaneous:'
808 write(*, '(A)') ' -s, --no-messages suppress error messages'
809 write(*, '(A)') ' -v, --invert-match select non-matching lines'
810 write(*, '(A)') ' --help display this help text and exit'
811 write(*, '(A)') ' -V, --version display version information and exit'
812 write(*, '(A)') ''
813 write(*, '(A)') 'Output control:'
814 write(*, '(A)') ' -m, --max-count=NUM stop after NUM matches'
815 write(*, '(A)') ' -b, --byte-offset print the byte offset with output lines'
816 write(*, '(A)') ' -n, --line-number print line number with output lines'
817 write(*, '(A)') ' -H, --with-filename print the file name for each match'
818 write(*, '(A)') ' -h, --no-filename suppress the file name prefix on output'
819 write(*, '(A)') ' -o, --only-matching show only the part of a line matching PATTERN'
820 write(*, '(A)') ' -q, --quiet, --silent suppress all normal output'
821 write(*, '(A)') ' -c, --count print only a count of matching lines per FILE'
822 write(*, '(A)') ' -l, --files-with-matches print only names of FILEs containing matches'
823 write(*, '(A)') ' -L, --files-without-match print only names of FILEs containing no match'
824 write(*, '(A)') ' --line-buffered flush output on every line'
825 write(*, '(A)') ' --color[=WHEN] use markers to highlight the matching strings;'
826 write(*, '(A)') ' WHEN is "always", "never", or "auto"'
827 write(*, '(A)') ' --label=LABEL use LABEL as the standard input file name'
828 write(*, '(A)') ' -T, --initial-tab make tabs line up (if needed)'
829 write(*, '(A)') ' -Z, --null print 0 byte after FILE name'
830 write(*, '(A)') ' -z, --null-data treat input/output data as NUL-terminated lines'
831 write(*, '(A)') ''
832 write(*, '(A)') 'Context control:'
833 write(*, '(A)') ' -B, --before-context=NUM print NUM lines of leading context'
834 write(*, '(A)') ' -A, --after-context=NUM print NUM lines of trailing context'
835 write(*, '(A)') ' -C, --context=NUM print NUM lines of output context'
836 write(*, '(A)') ' --group-separator=SEP use SEP as group separator (default: --)'
837 write(*, '(A)') ' --no-group-separator suppress group separator'
838 write(*, '(A)') ''
839 write(*, '(A)') 'File selection:'
840 write(*, '(A)') ' -d, --directories=ACTION how to handle directories;'
841 write(*, '(A)') ' ACTION is "read", "recurse", or "skip"'
842 write(*, '(A)') ' -D, --devices=ACTION how to handle devices; ACTION is "read" or "skip"'
843 write(*, '(A)') ' -r, --recursive equivalent to --directories=recurse'
844 write(*, '(A)') ' -R, --dereference-recursive likewise, but follow all symlinks'
845 write(*, '(A)') ' --include=GLOB search only files that match GLOB'
846 write(*, '(A)') ' --include-from=FILE read include patterns from FILE'
847 write(*, '(A)') ' --exclude=GLOB skip files that match GLOB'
848 write(*, '(A)') ' --exclude-from=FILE read exclude patterns from FILE'
849 write(*, '(A)') ' --exclude-dir=GLOB skip directories that match GLOB'
850 write(*, '(A)') ''
851 write(*, '(A)') 'Binary file handling:'
852 write(*, '(A)') ' -a, --text equivalent to --binary-files=text'
853 write(*, '(A)') ' -I equivalent to --binary-files=without-match'
854 write(*, '(A)') ' -U, --binary do not strip CR at EOL (default)'
855 write(*, '(A)') ' --binary-files=TYPE assume binary files are TYPE;'
856 write(*, '(A)') ' TYPE is "binary", "text", or "without-match"'
857 write(*, '(A)') ''
858 write(*, '(A)') 'Exit status is 0 if any line is selected, 1 otherwise;'
859 write(*, '(A)') 'if any error occurs and -q is not given, the exit status is 2.'
860 end subroutine print_help
861
862 subroutine print_version()
863 write(*, '(A)') 'ferp (Fortran Expression Regular Print) ' // VERSION
864 write(*, '(A)') 'Written in Modern Fortran.'
865 end subroutine print_version
866
867 function is_numeric_option(str) result(is_num)
868 !> Check if string is all digits (for -NUM context shorthand)
869 character(len=*), intent(in) :: str
870 logical :: is_num
871 integer :: i, ic
872
873 is_num = .false.
874 if (len_trim(str) == 0) return
875
876 do i = 1, len_trim(str)
877 ic = ichar(str(i:i))
878 if (ic < ichar('0') .or. ic > ichar('9')) return
879 end do
880
881 is_num = .true.
882 end function is_numeric_option
883
884 subroutine handle_numeric_context(opts, numstr, ierr)
885 !> Handle -NUM shorthand for context lines (e.g., -3 = -C 3)
886 type(grep_options), intent(inout) :: opts
887 character(len=*), intent(in) :: numstr
888 integer, intent(out) :: ierr
889
890 integer :: num, ios
891
892 ierr = 0
893 read(numstr, *, iostat=ios) num
894
895 if (ios /= 0 .or. num < 0) then
896 write(error_unit, '(A)') 'ferp: invalid context length: ' // trim(numstr)
897 ierr = 2
898 return
899 end if
900
901 opts%before_context = num
902 opts%after_context = num
903 end subroutine handle_numeric_context
904
905 end module ferp_cli
906