Bash · 49648 bytes Raw Blame History
1 #!/bin/bash
2 #
3 # FERP Extended Grep Test Suite
4 # Tests edge cases, corner cases, and obscure grep behaviors
5 #
6 # This test suite covers:
7 # 1. Multiple pattern flags (-e, -f)
8 # 2. Backreferences in BRE
9 # 3. Null-data mode (-z)
10 # 4. Binary file handling
11 # 5. Recursive directory options
12 # 6. Context edge cases
13 # 7. Output format options (-T, -Z, --label)
14 # 8. Regex edge cases (empty matches, unicode, long patterns)
15 # 9. BRE vs ERE differences
16 # 10. Error handling scenarios
17 # 11. Unusual input (long lines, mixed line endings, etc.)
18 # 12. Flag interaction edge cases
19 # 13. PCRE-specific features (-P)
20 #
21 # Usage: ./extended_grep_test.sh [--verbose] [--stop-on-fail] [--filter PATTERN]
22 #
23
24 set -uo pipefail
25
26 #------------------------------------------------------------------------------
27 # Configuration
28 #------------------------------------------------------------------------------
29
30 RED='\033[0;31m'
31 GREEN='\033[0;32m'
32 YELLOW='\033[1;33m'
33 BLUE='\033[0;34m'
34 CYAN='\033[0;36m'
35 NC='\033[0m'
36
37 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
38 FERP="${SCRIPT_DIR}/../ferp"
39 FIXTURES="${SCRIPT_DIR}/extended_fixtures"
40
41 # Test counters
42 TESTS_RUN=0
43 TESTS_PASSED=0
44 TESTS_FAILED=0
45 TESTS_SKIPPED=0
46
47 # Options
48 VERBOSE=false
49 STOP_ON_FAIL=false
50 FILTER=""
51
52 # Track failures for summary
53 declare -a FAILED_TESTS=()
54
55 #------------------------------------------------------------------------------
56 # Argument Parsing
57 #------------------------------------------------------------------------------
58
59 while [[ $# -gt 0 ]]; do
60 case $1 in
61 --verbose|-v)
62 VERBOSE=true
63 shift
64 ;;
65 --stop-on-fail|-x)
66 STOP_ON_FAIL=true
67 shift
68 ;;
69 --filter|-f)
70 FILTER="$2"
71 shift 2
72 ;;
73 --help|-h)
74 echo "Usage: $0 [--verbose] [--stop-on-fail] [--filter PATTERN]"
75 exit 0
76 ;;
77 *)
78 echo "Unknown option: $1"
79 exit 1
80 ;;
81 esac
82 done
83
84 #------------------------------------------------------------------------------
85 # Test Framework
86 #------------------------------------------------------------------------------
87
88 log() {
89 if [[ "$VERBOSE" == "true" ]]; then
90 echo -e "$1"
91 fi
92 }
93
94 section() {
95 echo ""
96 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
97 echo -e "${BLUE} $1${NC}"
98 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
99 }
100
101 pass() {
102 ((TESTS_PASSED++))
103 ((TESTS_RUN++))
104 echo -e "${GREEN}PASS${NC}: $1"
105 }
106
107 fail() {
108 local name="$1"
109 local reason="${2:-}"
110 ((TESTS_FAILED++))
111 ((TESTS_RUN++))
112 echo -e "${RED}FAIL${NC}: $name"
113 if [[ -n "$reason" ]]; then
114 echo -e " ${YELLOW}Reason:${NC} $reason"
115 fi
116 FAILED_TESTS+=("$name")
117 if [[ "$STOP_ON_FAIL" == "true" ]]; then
118 echo -e "${RED}Stopping on first failure${NC}"
119 print_summary
120 exit 1
121 fi
122 }
123
124 skip() {
125 local name="$1"
126 local reason="${2:-}"
127 ((TESTS_SKIPPED++))
128 echo -e "${YELLOW}SKIP${NC}: $name${reason:+ ($reason)}"
129 }
130
131 should_run() {
132 local name="$1"
133 if [[ -n "$FILTER" && ! "$name" =~ $FILTER ]]; then
134 return 1
135 fi
136 return 0
137 }
138
139 #------------------------------------------------------------------------------
140 # Core Comparison Function
141 #------------------------------------------------------------------------------
142
143 compare_with_grep() {
144 local flags="$1"
145 local pattern="$2"
146 local file="$3"
147
148 local grep_out grep_exit ferp_out ferp_exit
149
150 grep_out=$(grep $flags -- "$pattern" "$file" 2>/dev/null) && grep_exit=0 || grep_exit=$?
151 ferp_out=$("$FERP" $flags -- "$pattern" "$file" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
152
153 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
154 return 0
155 else
156 if [[ "$VERBOSE" == "true" ]]; then
157 echo " grep output ($grep_exit): $(echo "$grep_out" | head -3 | cat -A)"
158 echo " ferp output ($ferp_exit): $(echo "$ferp_out" | head -3 | cat -A)"
159 fi
160 return 1
161 fi
162 }
163
164 compare_with_grep_stdin() {
165 local flags="$1"
166 local pattern="$2"
167 local input="$3"
168
169 local grep_out grep_exit ferp_out ferp_exit
170
171 grep_out=$(printf '%s' "$input" | grep $flags -- "$pattern" 2>/dev/null) && grep_exit=0 || grep_exit=$?
172 ferp_out=$(printf '%s' "$input" | "$FERP" $flags -- "$pattern" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
173
174 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
175 return 0
176 else
177 if [[ "$VERBOSE" == "true" ]]; then
178 echo " grep output ($grep_exit): $(echo "$grep_out" | head -3 | cat -A)"
179 echo " ferp output ($ferp_exit): $(echo "$ferp_out" | head -3 | cat -A)"
180 fi
181 return 1
182 fi
183 }
184
185 test_grep_compat() {
186 local name="$1"
187 local flags="$2"
188 local pattern="$3"
189 local file="$4"
190
191 should_run "$name" || return 0
192
193 log "${CYAN}Testing:${NC} $name"
194 log " Command: grep $flags -- '$pattern' $file"
195
196 if compare_with_grep "$flags" "$pattern" "$file" "$name"; then
197 pass "$name"
198 else
199 fail "$name" "Output differs from grep"
200 fi
201 }
202
203 test_grep_compat_stdin() {
204 local name="$1"
205 local flags="$2"
206 local pattern="$3"
207 local input="$4"
208
209 should_run "$name" || return 0
210
211 log "${CYAN}Testing:${NC} $name"
212
213 if compare_with_grep_stdin "$flags" "$pattern" "$input" "$name"; then
214 pass "$name"
215 else
216 fail "$name" "Output differs from grep"
217 fi
218 }
219
220 # Compare with multiple files
221 compare_multi_files() {
222 local name="$1"
223 local flags="$2"
224 local pattern="$3"
225 shift 3
226 local files=("$@")
227
228 should_run "$name" || return 0
229
230 log "${CYAN}Testing:${NC} $name"
231
232 local grep_out grep_exit ferp_out ferp_exit
233
234 grep_out=$(grep $flags -- "$pattern" "${files[@]}" 2>/dev/null) && grep_exit=0 || grep_exit=$?
235 ferp_out=$("$FERP" $flags -- "$pattern" "${files[@]}" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
236
237 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
238 pass "$name"
239 else
240 fail "$name" "Output differs from grep"
241 if [[ "$VERBOSE" == "true" ]]; then
242 echo " grep ($grep_exit): $(echo "$grep_out" | head -3)"
243 echo " ferp ($ferp_exit): $(echo "$ferp_out" | head -3)"
244 fi
245 fi
246 }
247
248 #------------------------------------------------------------------------------
249 # Fixture Generation
250 #------------------------------------------------------------------------------
251
252 generate_fixtures() {
253 echo -e "${BLUE}Generating extended test fixtures...${NC}"
254
255 rm -rf "$FIXTURES"
256 mkdir -p "$FIXTURES"
257 mkdir -p "$FIXTURES/subdir"
258 mkdir -p "$FIXTURES/recursive/level1/level2"
259
260 # Basic test file
261 cat > "$FIXTURES/basic.txt" << 'EOF'
262 hello world
263 Hello World
264 HELLO WORLD
265 goodbye world
266 the quick brown fox
267 testing 123 testing
268 line with hello in middle
269 EOF
270
271 # File for backreference tests
272 cat > "$FIXTURES/backref.txt" << 'EOF'
273 aa
274 bb
275 ab
276 aaa
277 abba
278 abab
279 noon
280 deed
281 level
282 hello
283 abcabc
284 EOF
285
286 # File for multiple pattern tests
287 cat > "$FIXTURES/multi_pattern.txt" << 'EOF'
288 apple pie
289 banana bread
290 cherry cake
291 apple sauce
292 grape juice
293 banana split
294 EOF
295
296 # Pattern file for -f flag
297 cat > "$FIXTURES/patterns.txt" << 'EOF'
298 apple
299 cherry
300 grape
301 EOF
302
303 # File with special characters
304 cat > "$FIXTURES/special.txt" << 'EOF'
305 price is $100
306 50% off
307 path/to/file
308 array[0]
309 func()
310 a+b=c
311 a*b
312 hello.world
313 start^here
314 end$there
315 back\slash
316 pipe|char
317 question?
318 curly{brace}
319 (parens)
320 EOF
321
322 # Unicode test file
323 cat > "$FIXTURES/unicode.txt" << 'EOF'
324 cafe
325 café
326 résumé
327 naïve
328 NAÏVE
329 Ñoño
330 日本語
331 EOF
332
333 # File with various line endings
334 printf 'unix line\n' > "$FIXTURES/line_endings.txt"
335 printf 'windows line\r\n' >> "$FIXTURES/line_endings.txt"
336 printf 'old mac line\r' >> "$FIXTURES/line_endings.txt"
337 printf 'final line\n' >> "$FIXTURES/line_endings.txt"
338
339 # File for context tests (overlapping matches)
340 cat > "$FIXTURES/context.txt" << 'EOF'
341 line 1
342 line 2
343 MATCH A
344 line 4
345 MATCH B
346 line 6
347 line 7
348 line 8
349 MATCH C
350 line 10
351 EOF
352
353 # File with empty lines
354 cat > "$FIXTURES/empty_lines.txt" << 'EOF'
355 first
356
357 second
358
359 third
360 EOF
361
362 # File with only whitespace lines
363 cat > "$FIXTURES/whitespace_lines.txt" << 'EOF'
364 normal line
365
366
367 normal again
368 EOF
369
370 # Very long line
371 printf 'start ' > "$FIXTURES/longline.txt"
372 printf 'x%.0s' {1..5000} >> "$FIXTURES/longline.txt"
373 printf ' MATCH ' >> "$FIXTURES/longline.txt"
374 printf 'y%.0s' {1..5000} >> "$FIXTURES/longline.txt"
375 printf ' end\n' >> "$FIXTURES/longline.txt"
376
377 # Binary file with text
378 printf 'text before\x00binary\x00text after\nmore text\n' > "$FIXTURES/binary.bin"
379
380 # File for -T tab alignment tests
381 cat > "$FIXTURES/tabs.txt" << 'EOF'
382 short match here
383 verylongfilename match here
384 x match here
385 EOF
386
387 # Files for recursive tests
388 echo "match in root" > "$FIXTURES/recursive/root.txt"
389 echo "match in level1" > "$FIXTURES/recursive/level1/file1.txt"
390 echo "no match here" > "$FIXTURES/recursive/level1/file2.txt"
391 echo "match in level2" > "$FIXTURES/recursive/level1/level2/deep.txt"
392 echo "match in c file" > "$FIXTURES/recursive/code.c"
393 echo "match in header" > "$FIXTURES/recursive/code.h"
394 echo "compiled code" > "$FIXTURES/recursive/code.o"
395
396 # Symlink for -R tests (if supported)
397 ln -sf "$FIXTURES/basic.txt" "$FIXTURES/recursive/link_to_basic.txt" 2>/dev/null || true
398
399 # Empty file
400 touch "$FIXTURES/empty.txt"
401
402 # Single line, no newline
403 printf 'no trailing newline' > "$FIXTURES/no_newline.txt"
404
405 # File with many matches per line
406 echo "match match match match match" > "$FIXTURES/many_matches.txt"
407
408 # Null-separated data
409 printf 'record1\0record2\0record3\0' > "$FIXTURES/null_data.txt"
410
411 # Multiple files for multi-file tests
412 echo "has pattern here" > "$FIXTURES/multi1.txt"
413 echo "nothing special" > "$FIXTURES/multi2.txt"
414 echo "pattern again" > "$FIXTURES/multi3.txt"
415
416 echo -e "${GREEN}Generated extended fixtures in $FIXTURES${NC}"
417 }
418
419 #------------------------------------------------------------------------------
420 # Test: Multiple Pattern Flags (-e, -f)
421 #------------------------------------------------------------------------------
422
423 test_multiple_patterns() {
424 section "Multiple Pattern Flags (-e, -f)"
425
426 local name
427
428 # -e with single pattern (should work like no -e)
429 test_grep_compat "-e: single pattern" "-e hello" "hello" "$FIXTURES/basic.txt"
430
431 # -e with multiple patterns
432 name="-e: two patterns"
433 should_run "$name" && {
434 local grep_out ferp_out grep_exit ferp_exit
435 grep_out=$(grep -e "hello" -e "goodbye" "$FIXTURES/basic.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
436 ferp_out=$("$FERP" -e "hello" -e "goodbye" "$FIXTURES/basic.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
437 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
438 pass "$name"
439 else
440 fail "$name" "Output differs"
441 log " grep: $grep_out"
442 log " ferp: $ferp_out"
443 fi
444 }
445
446 # -e with three patterns
447 name="-e: three patterns"
448 should_run "$name" && {
449 local grep_out ferp_out grep_exit ferp_exit
450 grep_out=$(grep -e "apple" -e "cherry" -e "grape" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
451 ferp_out=$("$FERP" -e "apple" -e "cherry" -e "grape" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
452 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
453 pass "$name"
454 else
455 fail "$name" "Output differs"
456 fi
457 }
458
459 # -e with -i (case insensitive)
460 name="-e: with -i flag"
461 should_run "$name" && {
462 local grep_out ferp_out grep_exit ferp_exit
463 grep_out=$(grep -i -e "HELLO" -e "GOODBYE" "$FIXTURES/basic.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
464 ferp_out=$("$FERP" -i -e "HELLO" -e "GOODBYE" "$FIXTURES/basic.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
465 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
466 pass "$name"
467 else
468 fail "$name" "Output differs"
469 fi
470 }
471
472 # -f: patterns from file
473 name="-f: patterns from file"
474 should_run "$name" && {
475 local grep_out ferp_out grep_exit ferp_exit
476 grep_out=$(grep -f "$FIXTURES/patterns.txt" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
477 ferp_out=$("$FERP" -f "$FIXTURES/patterns.txt" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
478 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
479 pass "$name"
480 else
481 fail "$name" "Output differs"
482 fi
483 }
484
485 # -f with -i
486 name="-f: with -i flag"
487 should_run "$name" && {
488 # Create uppercase patterns file
489 echo -e "APPLE\nCHERRY" > "$FIXTURES/patterns_upper.txt"
490 local grep_out ferp_out grep_exit ferp_exit
491 grep_out=$(grep -if "$FIXTURES/patterns_upper.txt" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
492 ferp_out=$("$FERP" -if "$FIXTURES/patterns_upper.txt" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
493 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
494 pass "$name"
495 else
496 fail "$name" "Output differs"
497 fi
498 }
499
500 # -e and -f combined
501 name="-e and -f combined"
502 should_run "$name" && {
503 local grep_out ferp_out grep_exit ferp_exit
504 grep_out=$(grep -e "banana" -f "$FIXTURES/patterns.txt" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
505 ferp_out=$("$FERP" -e "banana" -f "$FIXTURES/patterns.txt" "$FIXTURES/multi_pattern.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
506 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
507 pass "$name"
508 else
509 fail "$name" "Output differs"
510 fi
511 }
512
513 # -f with empty pattern file
514 name="-f: empty pattern file"
515 should_run "$name" && {
516 touch "$FIXTURES/empty_patterns.txt"
517 local grep_out ferp_out grep_exit ferp_exit
518 grep_out=$(grep -f "$FIXTURES/empty_patterns.txt" "$FIXTURES/basic.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
519 ferp_out=$("$FERP" -f "$FIXTURES/empty_patterns.txt" "$FIXTURES/basic.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
520 if [[ "$grep_exit" == "$ferp_exit" ]]; then
521 pass "$name"
522 else
523 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
524 fi
525 }
526 }
527
528 #------------------------------------------------------------------------------
529 # Test: Backreferences (BRE)
530 #------------------------------------------------------------------------------
531
532 test_backreferences() {
533 section "Backreferences (BRE)"
534
535 # Simple backreference - doubled character
536 test_grep_compat "backref: doubled char" "" '\(.\)\1' "$FIXTURES/backref.txt"
537
538 # Backreference - doubled lowercase letter
539 test_grep_compat "backref: doubled lowercase" "" '\([a-z]\)\1' "$FIXTURES/backref.txt"
540
541 # Palindrome-like pattern
542 test_grep_compat "backref: abba pattern" "" '\(.\)\(.\)\2\1' "$FIXTURES/backref.txt"
543
544 # Backreference with quantifier before group
545 test_grep_compat "backref: group with star" "" '\(ab\)*\1' "$FIXTURES/backref.txt"
546
547 # Multiple groups
548 test_grep_compat "backref: two groups" "" '\(a\)\(b\)\1\2' "$FIXTURES/backref.txt"
549
550 # Backreference at word boundary
551 test_grep_compat "backref: with word boundary" "-w" '\([a-z]\)\1' "$FIXTURES/backref.txt"
552
553 # Backreference with -i (case insensitive)
554 test_grep_compat "backref: case insensitive" "-i" '\([a-z]\)\1' "$FIXTURES/backref.txt"
555
556 # Repeated group captures last match
557 test_grep_compat_stdin "backref: repeated group" "" '\(ab*\)*\1' $'ababbabb\nababbab\n'
558 }
559
560 #------------------------------------------------------------------------------
561 # Test: Null-data Mode (-z)
562 #------------------------------------------------------------------------------
563
564 test_null_data() {
565 section "Null-data Mode (-z)"
566
567 local name
568
569 # Basic null-terminated matching
570 name="-z: basic null-terminated"
571 should_run "$name" && {
572 local grep_out ferp_out grep_exit ferp_exit
573 grep_out=$(printf 'foo\0bar\0baz\0' | grep -z 'bar' 2>/dev/null | cat -A) && grep_exit=0 || grep_exit=$?
574 ferp_out=$(printf 'foo\0bar\0baz\0' | "$FERP" -z 'bar' 2>/dev/null | cat -A) && ferp_exit=0 || ferp_exit=$?
575 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
576 pass "$name"
577 else
578 fail "$name" "Output differs"
579 log " grep: $grep_out"
580 log " ferp: $ferp_out"
581 fi
582 }
583
584 # -z with multiple matches
585 name="-z: multiple matches"
586 should_run "$name" && {
587 local grep_out ferp_out grep_exit ferp_exit
588 grep_out=$(printf 'match1\0nomatch\0match2\0' | grep -z 'match' 2>/dev/null | cat -A) && grep_exit=0 || grep_exit=$?
589 ferp_out=$(printf 'match1\0nomatch\0match2\0' | "$FERP" -z 'match' 2>/dev/null | cat -A) && ferp_exit=0 || ferp_exit=$?
590 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
591 pass "$name"
592 else
593 fail "$name" "Output differs"
594 fi
595 }
596
597 # -z with -c (count)
598 name="-z: with count"
599 should_run "$name" && {
600 local grep_out ferp_out grep_exit ferp_exit
601 grep_out=$(printf 'match\0nomatch\0match\0' | grep -zc 'match' 2>/dev/null) && grep_exit=0 || grep_exit=$?
602 ferp_out=$(printf 'match\0nomatch\0match\0' | "$FERP" -zc 'match' 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
603 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
604 pass "$name"
605 else
606 fail "$name" "Output differs: grep='$grep_out' ferp='$ferp_out'"
607 fi
608 }
609
610 # -z with -v (invert)
611 name="-z: with invert"
612 should_run "$name" && {
613 local grep_out ferp_out grep_exit ferp_exit
614 grep_out=$(printf 'match\0nomatch\0other\0' | grep -zv 'match' 2>/dev/null | cat -A) && grep_exit=0 || grep_exit=$?
615 ferp_out=$(printf 'match\0nomatch\0other\0' | "$FERP" -zv 'match' 2>/dev/null | cat -A) && ferp_exit=0 || ferp_exit=$?
616 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
617 pass "$name"
618 else
619 fail "$name" "Output differs"
620 fi
621 }
622 }
623
624 #------------------------------------------------------------------------------
625 # Test: Binary File Handling
626 #------------------------------------------------------------------------------
627
628 test_binary_files() {
629 section "Binary File Handling"
630
631 local name
632
633 # Default behavior with binary file
634 name="binary: default behavior"
635 should_run "$name" && {
636 local grep_out ferp_out grep_exit ferp_exit
637 grep_out=$(grep 'text' "$FIXTURES/binary.bin" 2>/dev/null) && grep_exit=0 || grep_exit=$?
638 ferp_out=$("$FERP" 'text' "$FIXTURES/binary.bin" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
639 # Both should either show "Binary file matches" or similar
640 if [[ "$grep_exit" == "$ferp_exit" ]]; then
641 pass "$name"
642 else
643 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
644 fi
645 }
646
647 # -a/--text: treat as text
648 name="binary: -a treat as text"
649 should_run "$name" && {
650 local grep_out ferp_out grep_exit ferp_exit
651 grep_out=$(grep -a 'text' "$FIXTURES/binary.bin" 2>/dev/null | head -1) && grep_exit=0 || grep_exit=$?
652 ferp_out=$("$FERP" -a 'text' "$FIXTURES/binary.bin" 2>/dev/null | head -1) && ferp_exit=0 || ferp_exit=$?
653 if [[ "$grep_exit" == "$ferp_exit" ]]; then
654 pass "$name"
655 else
656 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
657 fi
658 }
659
660 # -I: ignore binary files
661 name="binary: -I ignore binary"
662 should_run "$name" && {
663 local grep_out ferp_out grep_exit ferp_exit
664 grep_out=$(grep -I 'text' "$FIXTURES/binary.bin" 2>/dev/null) && grep_exit=0 || grep_exit=$?
665 ferp_out=$("$FERP" -I 'text' "$FIXTURES/binary.bin" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
666 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
667 pass "$name"
668 else
669 fail "$name" "Output differs"
670 fi
671 }
672
673 # --binary-files=without-match
674 name="binary: --binary-files=without-match"
675 should_run "$name" && {
676 local grep_out ferp_out grep_exit ferp_exit
677 grep_out=$(grep --binary-files=without-match 'text' "$FIXTURES/binary.bin" 2>/dev/null) && grep_exit=0 || grep_exit=$?
678 ferp_out=$("$FERP" --binary-files=without-match 'text' "$FIXTURES/binary.bin" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
679 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
680 pass "$name"
681 else
682 fail "$name" "Output differs"
683 fi
684 }
685 }
686
687 #------------------------------------------------------------------------------
688 # Test: Recursive Directory Options
689 #------------------------------------------------------------------------------
690
691 test_recursive() {
692 section "Recursive Directory Options"
693
694 local name
695
696 # -r: recursive search
697 name="-r: basic recursive"
698 should_run "$name" && {
699 local grep_out ferp_out grep_exit ferp_exit
700 grep_out=$(grep -r 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
701 ferp_out=$("$FERP" -r 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
702 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
703 pass "$name"
704 else
705 fail "$name" "Output differs"
706 log " grep: $grep_out"
707 log " ferp: $ferp_out"
708 fi
709 }
710
711 # -r with -l
712 name="-r: with -l list files"
713 should_run "$name" && {
714 local grep_out ferp_out grep_exit ferp_exit
715 grep_out=$(grep -rl 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
716 ferp_out=$("$FERP" -rl 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
717 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
718 pass "$name"
719 else
720 fail "$name" "Output differs"
721 fi
722 }
723
724 # -r with -c
725 name="-r: with -c count"
726 should_run "$name" && {
727 local grep_out ferp_out grep_exit ferp_exit
728 grep_out=$(grep -rc 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
729 ferp_out=$("$FERP" -rc 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
730 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
731 pass "$name"
732 else
733 fail "$name" "Output differs"
734 fi
735 }
736
737 # --include pattern
738 name="--include: only .c files"
739 should_run "$name" && {
740 local grep_out ferp_out grep_exit ferp_exit
741 grep_out=$(grep -r --include="*.c" 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
742 ferp_out=$("$FERP" -r --include="*.c" 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
743 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
744 pass "$name"
745 else
746 fail "$name" "Output differs"
747 fi
748 }
749
750 # --exclude pattern
751 name="--exclude: skip .o files"
752 should_run "$name" && {
753 local grep_out ferp_out grep_exit ferp_exit
754 grep_out=$(grep -r --exclude="*.o" 'match\|compiled' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
755 ferp_out=$("$FERP" -r --exclude="*.o" 'match\|compiled' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
756 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
757 pass "$name"
758 else
759 fail "$name" "Output differs"
760 fi
761 }
762
763 # --exclude-dir
764 name="--exclude-dir: skip level2"
765 should_run "$name" && {
766 local grep_out ferp_out grep_exit ferp_exit
767 grep_out=$(grep -r --exclude-dir="level2" 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
768 ferp_out=$("$FERP" -r --exclude-dir="level2" 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
769 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
770 pass "$name"
771 else
772 fail "$name" "Output differs"
773 fi
774 }
775
776 # Multiple --include patterns
777 name="--include: multiple patterns"
778 should_run "$name" && {
779 local grep_out ferp_out grep_exit ferp_exit
780 grep_out=$(grep -r --include="*.c" --include="*.h" 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && grep_exit=0 || grep_exit=$?
781 ferp_out=$("$FERP" -r --include="*.c" --include="*.h" 'match' "$FIXTURES/recursive" 2>/dev/null | sort) && ferp_exit=0 || ferp_exit=$?
782 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
783 pass "$name"
784 else
785 fail "$name" "Output differs"
786 fi
787 }
788 }
789
790 #------------------------------------------------------------------------------
791 # Test: Context Edge Cases
792 #------------------------------------------------------------------------------
793
794 test_context_edge_cases() {
795 section "Context Edge Cases"
796
797 # Overlapping context (matches close together)
798 test_grep_compat "context: overlapping -C1" "-C1" "MATCH" "$FIXTURES/context.txt"
799 test_grep_compat "context: overlapping -C2" "-C2" "MATCH" "$FIXTURES/context.txt"
800
801 # Context at file boundaries
802 test_grep_compat "context: at start -B3" "-B3" "line 1" "$FIXTURES/context.txt"
803 test_grep_compat "context: at end -A3" "-A3" "line 10" "$FIXTURES/context.txt"
804
805 # Context with -v (inverted)
806 test_grep_compat "context: -A2 with -v" "-A2 -v" "MATCH" "$FIXTURES/context.txt"
807 test_grep_compat "context: -B2 with -v" "-B2 -v" "MATCH" "$FIXTURES/context.txt"
808
809 # --group-separator
810 local name="context: custom group separator"
811 should_run "$name" && {
812 local grep_out ferp_out grep_exit ferp_exit
813 grep_out=$(grep --group-separator="===" -C1 "MATCH" "$FIXTURES/context.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
814 ferp_out=$("$FERP" --group-separator="===" -C1 "MATCH" "$FIXTURES/context.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
815 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
816 pass "$name"
817 else
818 fail "$name" "Output differs"
819 fi
820 }
821
822 # --no-group-separator
823 name="context: no group separator"
824 should_run "$name" && {
825 local grep_out ferp_out grep_exit ferp_exit
826 grep_out=$(grep --no-group-separator -C1 "MATCH" "$FIXTURES/context.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
827 ferp_out=$("$FERP" --no-group-separator -C1 "MATCH" "$FIXTURES/context.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
828 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
829 pass "$name"
830 else
831 fail "$name" "Output differs"
832 fi
833 }
834
835 # Context with -n (line numbers)
836 test_grep_compat "context: -C2 with -n" "-C2 -n" "MATCH" "$FIXTURES/context.txt"
837
838 # Context with -b (byte offset)
839 test_grep_compat "context: -C1 with -b" "-C1 -b" "MATCH" "$FIXTURES/context.txt"
840 }
841
842 #------------------------------------------------------------------------------
843 # Test: Output Format Options
844 #------------------------------------------------------------------------------
845
846 test_output_format() {
847 section "Output Format Options"
848
849 # -T: initial tab for alignment
850 test_grep_compat "-T: initial tab" "-T" "match" "$FIXTURES/tabs.txt"
851 test_grep_compat "-T: with -n" "-Tn" "match" "$FIXTURES/tabs.txt"
852 test_grep_compat "-T: with -H" "-TH" "match" "$FIXTURES/tabs.txt"
853
854 # -Z: null after filename
855 local name="-Z: null after filename"
856 should_run "$name" && {
857 local grep_out ferp_out grep_exit ferp_exit
858 grep_out=$(grep -Z "pattern" "$FIXTURES/multi1.txt" 2>/dev/null | cat -A) && grep_exit=0 || grep_exit=$?
859 ferp_out=$("$FERP" -Z "pattern" "$FIXTURES/multi1.txt" 2>/dev/null | cat -A) && ferp_exit=0 || ferp_exit=$?
860 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
861 pass "$name"
862 else
863 fail "$name" "Output differs"
864 fi
865 }
866
867 # -Z with -l
868 name="-Z: with -l"
869 should_run "$name" && {
870 local grep_out ferp_out grep_exit ferp_exit
871 grep_out=$(grep -lZ "pattern" "$FIXTURES/multi1.txt" "$FIXTURES/multi3.txt" 2>/dev/null | cat -A) && grep_exit=0 || grep_exit=$?
872 ferp_out=$("$FERP" -lZ "pattern" "$FIXTURES/multi1.txt" "$FIXTURES/multi3.txt" 2>/dev/null | cat -A) && ferp_exit=0 || ferp_exit=$?
873 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
874 pass "$name"
875 else
876 fail "$name" "Output differs"
877 fi
878 }
879
880 # --label for stdin
881 name="--label: custom stdin label"
882 should_run "$name" && {
883 local grep_out ferp_out grep_exit ferp_exit
884 grep_out=$(echo "hello world" | grep -H --label="MYSTDIN" "hello" 2>/dev/null) && grep_exit=0 || grep_exit=$?
885 ferp_out=$(echo "hello world" | "$FERP" -H --label="MYSTDIN" "hello" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
886 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
887 pass "$name"
888 else
889 fail "$name" "Output differs: grep='$grep_out' ferp='$ferp_out'"
890 fi
891 }
892 }
893
894 #------------------------------------------------------------------------------
895 # Test: Regex Edge Cases
896 #------------------------------------------------------------------------------
897
898 test_regex_edge_cases() {
899 section "Regex Edge Cases"
900
901 # Empty pattern matches everything
902 test_grep_compat "regex: empty pattern" "" "" "$FIXTURES/basic.txt"
903
904 # Pattern matching empty string
905 test_grep_compat "regex: a* matches empty" "-E" "a*" "$FIXTURES/basic.txt"
906
907 # Empty line matching
908 test_grep_compat "regex: match empty lines" "" "^$" "$FIXTURES/empty_lines.txt"
909
910 # Very long line
911 test_grep_compat "regex: very long line" "" "MATCH" "$FIXTURES/longline.txt"
912
913 # Many matches per line with -o
914 test_grep_compat "regex: many matches -o" "-o" "match" "$FIXTURES/many_matches.txt"
915
916 # Unicode patterns (if supported)
917 test_grep_compat "regex: unicode literal" "" "café" "$FIXTURES/unicode.txt"
918 test_grep_compat "regex: unicode case -i" "-i" "CAFÉ" "$FIXTURES/unicode.txt"
919
920 # Anchors with -o
921 test_grep_compat "regex: ^ anchor with -o" "-o" "^hello" "$FIXTURES/basic.txt"
922
923 # Newline in character class (should not match)
924 test_grep_compat_stdin "regex: dot doesn't match newline" "" "a.b" $'a\nb\nacb\n'
925
926 # Greedy vs non-greedy (ERE)
927 test_grep_compat_stdin "regex: greedy quantifier" "-Eo" "a.*b" "aXXbYYb"
928 }
929
930 #------------------------------------------------------------------------------
931 # Test: BRE vs ERE Differences
932 #------------------------------------------------------------------------------
933
934 test_bre_ere_differences() {
935 section "BRE vs ERE Differences"
936
937 # BRE: + and ? are literal
938 test_grep_compat "BRE: + is literal" "" "a+b" "$FIXTURES/special.txt"
939 test_grep_compat "BRE: ? is literal" "" "question?" "$FIXTURES/special.txt"
940
941 # BRE: | is literal (not alternation)
942 test_grep_compat "BRE: | is literal" "" "pipe|char" "$FIXTURES/special.txt"
943
944 # BRE: () need escaping for grouping
945 test_grep_compat "BRE: escaped parens group" "" '\(hello\)' "$FIXTURES/basic.txt"
946
947 # BRE: {} need escaping for quantifiers
948 test_grep_compat "BRE: escaped braces quantify" "" 'l\{2\}' "$FIXTURES/basic.txt"
949
950 # ERE: + and ? are special
951 test_grep_compat "ERE: + is quantifier" "-E" "hel+" "$FIXTURES/basic.txt"
952 test_grep_compat "ERE: ? is quantifier" "-E" "hell?o" "$FIXTURES/basic.txt"
953
954 # ERE: | is alternation
955 test_grep_compat "ERE: | is alternation" "-E" "hello|goodbye" "$FIXTURES/basic.txt"
956
957 # ERE: () don't need escaping
958 test_grep_compat "ERE: unescaped parens" "-E" "(hello)" "$FIXTURES/basic.txt"
959
960 # ERE: {} don't need escaping
961 test_grep_compat "ERE: unescaped braces" "-E" "l{2}" "$FIXTURES/basic.txt"
962
963 # BRE with GNU extensions: \| for alternation
964 test_grep_compat "BRE GNU: \\| alternation" "" 'hello\|goodbye' "$FIXTURES/basic.txt"
965
966 # BRE with GNU extensions: \+ and \?
967 test_grep_compat "BRE GNU: \\+ quantifier" "" 'hel\+' "$FIXTURES/basic.txt"
968 test_grep_compat "BRE GNU: \\? quantifier" "" 'hell\?' "$FIXTURES/basic.txt"
969 }
970
971 #------------------------------------------------------------------------------
972 # Test: Error Handling
973 #------------------------------------------------------------------------------
974
975 test_error_handling() {
976 section "Error Handling"
977
978 local name grep_exit ferp_exit
979
980 # Invalid regex
981 name="error: invalid regex ERE"
982 should_run "$name" && {
983 grep -E "[" "$FIXTURES/basic.txt" 2>/dev/null && grep_exit=0 || grep_exit=$?
984 "$FERP" -E "[" "$FIXTURES/basic.txt" 2>/dev/null && ferp_exit=0 || ferp_exit=$?
985 if [[ "$grep_exit" == "2" && "$ferp_exit" == "2" ]]; then
986 pass "$name"
987 else
988 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
989 fi
990 }
991
992 # Invalid regex BRE
993 name="error: invalid regex BRE"
994 should_run "$name" && {
995 grep '\(' "$FIXTURES/basic.txt" 2>/dev/null && grep_exit=0 || grep_exit=$?
996 "$FERP" '\(' "$FIXTURES/basic.txt" 2>/dev/null && ferp_exit=0 || ferp_exit=$?
997 if [[ "$grep_exit" == "2" && "$ferp_exit" == "2" ]]; then
998 pass "$name"
999 else
1000 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
1001 fi
1002 }
1003
1004 # Non-existent file (exit 2)
1005 name="error: non-existent file"
1006 should_run "$name" && {
1007 local grep_err ferp_err
1008 grep "pattern" "/nonexistent/file/12345" 2>/dev/null && grep_exit=0 || grep_exit=$?
1009 "$FERP" "pattern" "/nonexistent/file/12345" 2>/dev/null && ferp_exit=0 || ferp_exit=$?
1010 # grep returns 2 for errors, ferp should too
1011 if [[ "$grep_exit" == "$ferp_exit" ]]; then
1012 pass "$name"
1013 else
1014 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
1015 fi
1016 }
1017
1018 # Mix of existing and non-existing files
1019 name="error: partial file access"
1020 should_run "$name" && {
1021 local grep_out ferp_out
1022 grep_out=$(grep "hello" "$FIXTURES/basic.txt" "/nonexistent" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1023 ferp_out=$("$FERP" "hello" "$FIXTURES/basic.txt" "/nonexistent" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1024 # Should still find matches in existing file
1025 if [[ -n "$grep_out" && -n "$ferp_out" ]]; then
1026 pass "$name"
1027 else
1028 fail "$name" "Should still output matches from existing file"
1029 fi
1030 }
1031
1032 # -s suppresses error messages
1033 name="error: -s suppresses errors"
1034 should_run "$name" && {
1035 local grep_err ferp_err
1036 grep_err=$(grep -s "pattern" "/nonexistent" 2>&1)
1037 ferp_err=$("$FERP" -s "pattern" "/nonexistent" 2>&1)
1038 if [[ -z "$grep_err" && -z "$ferp_err" ]]; then
1039 pass "$name"
1040 else
1041 fail "$name" "Error messages not suppressed"
1042 fi
1043 }
1044
1045 # Directory without -r
1046 name="error: directory without -r"
1047 should_run "$name" && {
1048 grep "pattern" "$FIXTURES" 2>/dev/null && grep_exit=0 || grep_exit=$?
1049 "$FERP" "pattern" "$FIXTURES" 2>/dev/null && ferp_exit=0 || ferp_exit=$?
1050 # Both should handle this gracefully
1051 if [[ "$grep_exit" == "$ferp_exit" ]]; then
1052 pass "$name"
1053 else
1054 fail "$name" "Exit codes differ: grep=$grep_exit ferp=$ferp_exit"
1055 fi
1056 }
1057 }
1058
1059 #------------------------------------------------------------------------------
1060 # Test: Unusual Input
1061 #------------------------------------------------------------------------------
1062
1063 test_unusual_input() {
1064 section "Unusual Input"
1065
1066 # File with only newlines
1067 local name="input: only newlines"
1068 should_run "$name" && {
1069 printf '\n\n\n' > "$FIXTURES/only_newlines.txt"
1070 local grep_out ferp_out grep_exit ferp_exit
1071 grep_out=$(grep "." "$FIXTURES/only_newlines.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1072 ferp_out=$("$FERP" "." "$FIXTURES/only_newlines.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1073 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
1074 pass "$name"
1075 else
1076 fail "$name" "Output differs"
1077 fi
1078 }
1079
1080 # Single character file
1081 name="input: single character"
1082 should_run "$name" && {
1083 printf 'x' > "$FIXTURES/single_char.txt"
1084 local grep_out ferp_out grep_exit ferp_exit
1085 grep_out=$(grep "x" "$FIXTURES/single_char.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1086 ferp_out=$("$FERP" "x" "$FIXTURES/single_char.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1087 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
1088 pass "$name"
1089 else
1090 fail "$name" "Output differs"
1091 fi
1092 }
1093
1094 # Empty file
1095 test_grep_compat "input: empty file" "" "pattern" "$FIXTURES/empty.txt"
1096
1097 # No trailing newline
1098 test_grep_compat "input: no trailing newline" "" "newline" "$FIXTURES/no_newline.txt"
1099
1100 # Mixed line endings
1101 test_grep_compat "input: mixed line endings" "" "line" "$FIXTURES/line_endings.txt"
1102
1103 # Lines with only whitespace
1104 test_grep_compat "input: whitespace-only lines" "" "^[ \t]*$" "$FIXTURES/whitespace_lines.txt"
1105
1106 # Extremely long pattern
1107 name="input: long pattern"
1108 should_run "$name" && {
1109 local long_pattern
1110 long_pattern=$(printf 'x%.0s' {1..100})
1111 local grep_out ferp_out grep_exit ferp_exit
1112 grep_out=$(grep "$long_pattern" "$FIXTURES/longline.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1113 ferp_out=$("$FERP" "$long_pattern" "$FIXTURES/longline.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1114 if [[ "$grep_exit" == "$ferp_exit" ]]; then
1115 pass "$name"
1116 else
1117 fail "$name" "Exit codes differ"
1118 fi
1119 }
1120 }
1121
1122 #------------------------------------------------------------------------------
1123 # Test: Flag Interaction Edge Cases
1124 #------------------------------------------------------------------------------
1125
1126 test_flag_interactions() {
1127 section "Flag Interaction Edge Cases"
1128
1129 # -c with -l (count vs list)
1130 compare_multi_files "-c with -l" "-cl" "pattern" "$FIXTURES/multi1.txt" "$FIXTURES/multi2.txt" "$FIXTURES/multi3.txt"
1131
1132 # -o with -c (count only matching parts)
1133 test_grep_compat "-o with -c" "-oc" "match" "$FIXTURES/many_matches.txt"
1134
1135 # -m with -c (max count affects total)
1136 test_grep_compat "-m with -c" "-m2 -c" "match" "$FIXTURES/many_matches.txt"
1137
1138 # -v with -o (only matching of non-matches - undefined?)
1139 test_grep_compat "-v with -o" "-vo" "match" "$FIXTURES/many_matches.txt"
1140
1141 # -w with -o
1142 test_grep_compat "-w with -o" "-wo" "match" "$FIXTURES/many_matches.txt"
1143
1144 # -x with -o
1145 test_grep_compat "-x with -o" "-xo" "hello world" "$FIXTURES/basic.txt"
1146
1147 # -l with -L (conflicting - last wins?)
1148 local name="-l with -L"
1149 should_run "$name" && {
1150 # This is undefined behavior, just check they don't crash
1151 "$FERP" -lL "pattern" "$FIXTURES/multi1.txt" 2>/dev/null
1152 if [[ $? -le 2 ]]; then
1153 pass "$name"
1154 else
1155 fail "$name" "Unexpected exit code"
1156 fi
1157 }
1158
1159 # -q with -c (quiet but count?)
1160 name="-q with -c"
1161 should_run "$name" && {
1162 local grep_out ferp_out grep_exit ferp_exit
1163 grep_out=$(grep -qc "hello" "$FIXTURES/basic.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1164 ferp_out=$("$FERP" -qc "hello" "$FIXTURES/basic.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1165 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
1166 pass "$name"
1167 else
1168 fail "$name" "Output differs"
1169 fi
1170 }
1171
1172 # -n with -b with -o (all position info)
1173 test_grep_compat "-n -b -o combo" "-nbo" "hello" "$FIXTURES/basic.txt"
1174
1175 # -H with -h (conflicting)
1176 name="-H with -h"
1177 should_run "$name" && {
1178 local grep_out ferp_out grep_exit ferp_exit
1179 grep_out=$(grep -Hh "hello" "$FIXTURES/basic.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1180 ferp_out=$("$FERP" -Hh "hello" "$FIXTURES/basic.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1181 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
1182 pass "$name"
1183 else
1184 fail "$name" "Output differs"
1185 fi
1186 }
1187 }
1188
1189 #------------------------------------------------------------------------------
1190 # Test: PCRE Features (-P)
1191 #------------------------------------------------------------------------------
1192
1193 test_pcre_features() {
1194 section "PCRE Features (-P)"
1195
1196 # Check if PCRE is available
1197 if ! grep -P "test" "$FIXTURES/basic.txt" &>/dev/null; then
1198 skip "PCRE: not available in grep" "grep -P not supported"
1199 return
1200 fi
1201
1202 if ! "$FERP" -P "test" "$FIXTURES/basic.txt" &>/dev/null; then
1203 skip "PCRE: not available in ferp" "ferp -P not supported"
1204 return
1205 fi
1206
1207 # Basic PCRE
1208 test_grep_compat "PCRE: basic pattern" "-P" "hello" "$FIXTURES/basic.txt"
1209
1210 # Lookahead
1211 test_grep_compat "PCRE: positive lookahead" "-P" 'hello(?= world)' "$FIXTURES/basic.txt"
1212 test_grep_compat "PCRE: negative lookahead" "-P" 'hello(?! there)' "$FIXTURES/basic.txt"
1213
1214 # Lookbehind
1215 test_grep_compat "PCRE: positive lookbehind" "-P" '(?<=hello )world' "$FIXTURES/basic.txt"
1216 test_grep_compat "PCRE: negative lookbehind" "-P" '(?<!good)bye' "$FIXTURES/basic.txt"
1217
1218 # Non-greedy quantifiers
1219 test_grep_compat_stdin "PCRE: non-greedy *?" "-Po" "a.*?b" "aXXbYYb"
1220 test_grep_compat_stdin "PCRE: non-greedy +?" "-Po" "a.+?b" "aXXbYYb"
1221
1222 # Word boundary \b
1223 test_grep_compat "PCRE: word boundary \\b" "-P" '\btest\b' "$FIXTURES/basic.txt"
1224
1225 # Character classes \d, \w, \s
1226 test_grep_compat "PCRE: \\d digit" "-P" '\d+' "$FIXTURES/basic.txt"
1227 test_grep_compat "PCRE: \\w word" "-P" '\w+' "$FIXTURES/basic.txt"
1228 test_grep_compat "PCRE: \\s space" "-P" '\s+' "$FIXTURES/basic.txt"
1229
1230 # Negated classes \D, \W, \S
1231 test_grep_compat "PCRE: \\D non-digit" "-Po" '\D+' "$FIXTURES/basic.txt"
1232
1233 # PCRE with -i
1234 test_grep_compat "PCRE: with -i" "-Pi" "HELLO" "$FIXTURES/basic.txt"
1235
1236 # PCRE with -o
1237 test_grep_compat "PCRE: with -o" "-Po" '\w+' "$FIXTURES/basic.txt"
1238
1239 # PCRE with -v
1240 test_grep_compat "PCRE: with -v" "-Pv" "hello" "$FIXTURES/basic.txt"
1241
1242 # PCRE backreferences
1243 test_grep_compat "PCRE: backreference" "-P" '(\w)\1' "$FIXTURES/backref.txt"
1244 }
1245
1246 #------------------------------------------------------------------------------
1247 # Test: Fixed String Edge Cases (-F)
1248 #------------------------------------------------------------------------------
1249
1250 test_fixed_string_edge_cases() {
1251 section "Fixed String Edge Cases (-F)"
1252
1253 # All regex metacharacters as literals
1254 test_grep_compat "-F: dot literal" "-F" "hello.world" "$FIXTURES/special.txt"
1255 test_grep_compat "-F: star literal" "-F" "a*b" "$FIXTURES/special.txt"
1256 test_grep_compat "-F: plus literal" "-F" "a+b" "$FIXTURES/special.txt"
1257 test_grep_compat "-F: question literal" "-F" "question?" "$FIXTURES/special.txt"
1258 test_grep_compat "-F: brackets literal" "-F" "array[0]" "$FIXTURES/special.txt"
1259 test_grep_compat "-F: parens literal" "-F" "func()" "$FIXTURES/special.txt"
1260 test_grep_compat "-F: braces literal" "-F" "curly{brace}" "$FIXTURES/special.txt"
1261 test_grep_compat "-F: caret literal" "-F" "start^here" "$FIXTURES/special.txt"
1262 test_grep_compat "-F: dollar literal" "-F" 'end$there' "$FIXTURES/special.txt"
1263 test_grep_compat "-F: pipe literal" "-F" "pipe|char" "$FIXTURES/special.txt"
1264 test_grep_compat "-F: backslash literal" "-F" 'back\slash' "$FIXTURES/special.txt"
1265
1266 # -F with -i
1267 test_grep_compat "-F with -i" "-Fi" "HELLO.WORLD" "$FIXTURES/special.txt"
1268
1269 # -F with -w
1270 test_grep_compat "-F with -w" "-Fw" "hello" "$FIXTURES/basic.txt"
1271
1272 # -F with -x
1273 test_grep_compat "-F with -x" "-Fx" "hello world" "$FIXTURES/basic.txt"
1274
1275 # -F with multiple patterns via -e
1276 local name="-F with -e multiple"
1277 should_run "$name" && {
1278 local grep_out ferp_out grep_exit ferp_exit
1279 grep_out=$(grep -F -e "a+b" -e "a*b" "$FIXTURES/special.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1280 ferp_out=$("$FERP" -F -e "a+b" -e "a*b" "$FIXTURES/special.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1281 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
1282 pass "$name"
1283 else
1284 fail "$name" "Output differs"
1285 fi
1286 }
1287
1288 # -F with newline-separated patterns
1289 name="-F with newline in pattern"
1290 should_run "$name" && {
1291 local grep_out ferp_out grep_exit ferp_exit
1292 grep_out=$(grep -F $'hello\ngoodbye' "$FIXTURES/basic.txt" 2>/dev/null) && grep_exit=0 || grep_exit=$?
1293 ferp_out=$("$FERP" -F $'hello\ngoodbye' "$FIXTURES/basic.txt" 2>/dev/null) && ferp_exit=0 || ferp_exit=$?
1294 if [[ "$grep_out" == "$ferp_out" && "$grep_exit" == "$ferp_exit" ]]; then
1295 pass "$name"
1296 else
1297 fail "$name" "Output differs"
1298 fi
1299 }
1300 }
1301
1302 #------------------------------------------------------------------------------
1303 # Summary
1304 #------------------------------------------------------------------------------
1305
1306 print_summary() {
1307 echo ""
1308 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1309 echo -e "${BLUE} Summary${NC}"
1310 echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
1311 echo ""
1312 echo " Tests run: $TESTS_RUN"
1313 echo -e " ${GREEN}Passed: $TESTS_PASSED${NC}"
1314 echo -e " ${RED}Failed: $TESTS_FAILED${NC}"
1315 echo -e " ${YELLOW}Skipped: $TESTS_SKIPPED${NC}"
1316 echo ""
1317
1318 if [[ $TESTS_FAILED -gt 0 ]]; then
1319 echo -e "${RED}Failed tests:${NC}"
1320 for test in "${FAILED_TESTS[@]}"; do
1321 echo " - $test"
1322 done
1323 echo ""
1324 echo -e "${RED}Some tests failed!${NC}"
1325 return 1
1326 else
1327 echo -e "${GREEN}All tests passed!${NC}"
1328 return 0
1329 fi
1330 }
1331
1332 #------------------------------------------------------------------------------
1333 # Main
1334 #------------------------------------------------------------------------------
1335
1336 main() {
1337 echo -e "${BLUE}╔════════════════════════════════════════════════════════════════════════════╗${NC}"
1338 echo -e "${BLUE}║ FERP Extended Grep Test Suite ║${NC}"
1339 echo -e "${BLUE}╚════════════════════════════════════════════════════════════════════════════╝${NC}"
1340 echo ""
1341
1342 # Check prerequisites
1343 if [[ ! -x "$FERP" ]]; then
1344 echo -e "${RED}Error: ferp binary not found at $FERP${NC}"
1345 echo "Run 'make' first to build ferp"
1346 exit 1
1347 fi
1348
1349 if ! command -v grep &> /dev/null; then
1350 echo -e "${RED}Error: grep not found${NC}"
1351 exit 1
1352 fi
1353
1354 echo "ferp: $FERP"
1355 echo "grep: $(which grep) ($(grep --version | head -1))"
1356 echo ""
1357
1358 # Generate fixtures
1359 generate_fixtures
1360
1361 # Run test suites
1362 test_multiple_patterns
1363 test_backreferences
1364 test_null_data
1365 test_binary_files
1366 test_recursive
1367 test_context_edge_cases
1368 test_output_format
1369 test_regex_edge_cases
1370 test_bre_ere_differences
1371 test_error_handling
1372 test_unusual_input
1373 test_flag_interactions
1374 test_pcre_features
1375 test_fixed_string_edge_cases
1376
1377 # Print summary
1378 print_summary
1379 }
1380
1381 main "$@"