### grep_basic
# Basic pattern match
printf 'foo\nbar\nbaz\n' | grep bar
### expect
bar
### end

### grep_multiple
# Multiple matches
printf 'foo\nbar\nfoo\n' | grep foo
### expect
foo
foo
### end

### grep_no_match
# No match returns exit code 1
printf 'foo\nbar\n' | grep xyz
### exit_code: 1
### expect
### end

### grep_case_insensitive
# Case insensitive search
printf 'Hello\nWORLD\n' | grep -i hello
### expect
Hello
### end

### grep_invert
# Invert match
printf 'foo\nbar\nbaz\n' | grep -v bar
### expect
foo
baz
### end

### grep_line_numbers
# Show line numbers
printf 'foo\nbar\nbaz\n' | grep -n bar
### expect
2:bar
### end

### grep_count
# Count matches
printf 'foo\nbar\nfoo\n' | grep -c foo
### expect
2
### end

### grep_fixed_string
# Fixed string (no regex)
printf 'a.b\na*b\n' | grep -F 'a.b'
### expect
a.b
### end

### grep_regex
# Regex pattern
printf 'cat\ncar\nbar\n' | grep 'ca.'
### expect
cat
car
### end

### grep_anchor_start
# Start anchor
printf 'foo\nbar\nfoobar\n' | grep '^foo'
### expect
foo
foobar
### end

### grep_anchor_end
# End anchor
printf 'foo\nbar\nfoobar\n' | grep 'bar$'
### expect
bar
foobar
### end

### grep_extended
# Extended regex
printf 'color\ncolour\n' | grep -E 'colou?r'
### expect
color
colour
### end

### grep_word
# Word boundary match
printf 'foo\nfoobar\nbar foo baz\n' | grep -w foo
### expect
foo
bar foo baz
### end

### grep_only_matching
# Only show matching part
printf 'hello world\n' | grep -o 'world'
### expect
world
### end

### grep_files_with_matches
# List matching files. GNU grep names stdin "(standard input)" under -l too,
# not "(stdin)".
printf 'foo\nbar\n' | grep -l foo
### expect
(standard input)
### end

### grep_quiet
# Quiet mode - no output, just exit status
printf 'foo\nbar\n' | grep -q foo
### exit_code: 0
### expect
### end

### grep_quiet_no_match
# Quiet mode with no match
printf 'foo\nbar\n' | grep -q xyz
### exit_code: 1
### expect
### end

### grep_max_count
# Stop after N matches
printf 'foo\nfoo\nfoo\n' | grep -m 2 foo
### expect
foo
foo
### end

### grep_after_context
# Show N lines after match
printf 'a\nfoo\nb\nc\n' | grep -A 1 foo
### expect
foo
b
### end

### grep_before_context
# Show N lines before match
printf 'a\nb\nfoo\nc\n' | grep -B 1 foo
### expect
b
foo
### end

### grep_context
# Show N lines before and after match
printf 'a\nb\nfoo\nc\nd\n' | grep -C 1 foo
### expect
b
foo
c
### end

### grep_recursive
# Recursive grep through directory
mkdir -p /tmp/greprecur/sub && printf 'match\n' > /tmp/greprecur/a.txt && printf 'nope\n' > /tmp/greprecur/sub/b.txt && printf 'match here\n' > /tmp/greprecur/sub/c.txt && grep -r match /tmp/greprecur | sort
### expect
/tmp/greprecur/a.txt:match
/tmp/greprecur/sub/c.txt:match here
### end

### grep_multiple_patterns
# Multiple -e patterns (OR matching)
printf 'foo\nbar\nbaz\n' | grep -e foo -e bar
### expect
foo
bar
### end

### grep_pattern_file
# Read patterns from file
printf 'foo\nbar\n' > /tmp/greppatterns.txt
printf 'foo\nbar\nbaz\n' | grep -f /tmp/greppatterns.txt
### expect
foo
bar
### end

### grep_recursive_no_match
# Recursive grep with no matches
mkdir -p /tmp/greprecur2 && printf 'nope\n' > /tmp/greprecur2/a.txt
grep -r zzz /tmp/greprecur2
echo $?
### expect
1
### end

### grep_pattern_file_no_match
# Pattern file with no matching patterns
printf 'zzz\n' > /tmp/greppat2.txt
printf 'foo\nbar\n' | grep -f /tmp/greppat2.txt
echo $?
### expect
1
### end

### grep_null_data
# -z is null-terminated *records*, on output as well as input: GNU grep emits
# `foo\0`, not `foo\n`. Rendered through tr so the NUL is visible here.
# The trailing `echo` supplies the newline the spec format needs; grep -z
# itself terminates the record with the NUL, not a newline.
printf 'foo\0bar\0' | grep -z foo | tr '\0' '@'; echo
### expect
foo@
### end

### grep_only_matching_multiple
# Multiple matches per line
printf 'foo bar foo\n' | grep -o foo
### expect
foo
foo
### end

### grep_regex_star
# Zero or more
printf 'ac\nabc\nabbc\n' | grep 'ab*c'
### expect
ac
abc
abbc
### end

### grep_regex_plus_extended
# One or more with -E
printf 'ac\nabc\nabbc\n' | grep -E 'ab+c'
### expect
abc
abbc
### end

### grep_regex_question_extended
# Zero or one with -E
printf 'ac\nabc\nabbc\n' | grep -E 'ab?c'
### expect
ac
abc
### end

### grep_regex_alternation
# Alternation with -E
printf 'cat\ndog\nbird\n' | grep -E 'cat|dog'
### expect
cat
dog
### end

### grep_regex_group
# Grouping with -E
printf 'ab\naba\nabab\n' | grep -E '(ab)+'
### expect
ab
aba
abab
### end

### grep_character_class
# Character class
printf 'a1\nb2\nc3\n' | grep '[0-9]'
### expect
a1
b2
c3
### end

### grep_negated_class
# Negated character class
printf 'a1\n12\nb2\n' | grep '^[^0-9]'
### expect
a1
b2
### end

### grep_word_boundary_extended
# Word boundary in extended regex
printf 'foo\nfoobar\nbar foo baz\n' | grep -E '\bfoo\b'
### expect
foo
bar foo baz
### end

### grep_dot_metachar
# Dot matches any char
printf 'cat\ncar\ncab\n' | grep 'ca.'
### expect
cat
car
cab
### end

### grep_escape_special
# Escape special chars with -F
printf 'a.b\na*b\na+b\n' | grep -F 'a.b'
### expect
a.b
### end

### grep_empty_pattern
# Empty pattern matches all lines
printf 'foo\nbar\n' | grep ''
### expect
foo
bar
### end

### grep_whole_line
# Match whole line only
printf 'foo\nfoobar\nbar foo\n' | grep -x foo
### expect
foo
### end

### grep_byte_offset
# Show byte offset
printf 'foo\nbar\n' | grep -b bar
### expect
4:bar
### end

### grep_show_filename
# Show filename for stdin with -H
printf 'foo\n' | grep -H foo
### expect
(standard input):foo
### end

### grep_no_filename
# No filename prefix
printf 'foo\n' | grep -h foo
### expect
foo
### end

### grep_color
### bash_diff: --color=always is accepted but not implemented; GNU grep wraps
### bash_diff: the match in SGR escapes. Tracked as a limitation, not a bug.
# Color flag accepted (no-op, outputs plain text)
printf 'foo\n' | grep --color=always foo
### expect
foo
### end

### grep_perl_regex
# Perl regex support (uses regex crate which supports PCRE features)
printf 'foo123\n' | grep -P 'foo\d+'
### expect
foo123
### end

### grep_binary_detect
# Binary file detection. The message is a diagnostic, so GNU grep >= 3.5
# writes `grep: FILE: binary file matches` to stderr and leaves stdout empty;
# merged here so the spec asserts the text as well as the stream.
printf 'foo\0bar\n' | grep foo 2>&1
### expect
grep: (standard input): binary file matches
### end

### grep_binary_with_a_flag
# -a treats binary as text; it does not delete the NUL bytes. GNU grep passes
# the line through byte for byte. Rendered through tr so the NUL is visible.
printf 'foo\0bar\n' | grep -a foo | tr '\0' '@'
### expect
foo@bar
### end

### grep_include_pattern
# Include only .txt files in recursive search
mkdir -p /tmp/grepdir && printf 'hello\n' > /tmp/grepdir/a.txt && printf 'hello\n' > /tmp/grepdir/b.log && grep -r --include='*.txt' hello /tmp/grepdir
### expect
/tmp/grepdir/a.txt:hello
### end

### grep_exclude_pattern
# Exclude .log files in recursive search
mkdir -p /tmp/grepdir2 && printf 'hello\n' > /tmp/grepdir2/a.txt && printf 'hello\n' > /tmp/grepdir2/b.log && grep -r --exclude='*.log' hello /tmp/grepdir2
### expect
/tmp/grepdir2/a.txt:hello
### end

### grep_include_no_match
# Include pattern that matches no files
mkdir -p /tmp/grepdir3 && printf 'hello\n' > /tmp/grepdir3/a.log && grep -r --include='*.txt' hello /tmp/grepdir3
echo $?
### expect
1
### end

### grep_line_buffered
# Line-buffered flag accepted (no-op, output is already line-oriented)
printf 'foo\nbar\n' | grep --line-buffered foo
### expect
foo
### end

### grep_ignore_case_extended
# Case insensitive with extended regex
printf 'Hello\nWORLD\nhello\n' | grep -iE 'hello'
### expect
Hello
hello
### end

### grep_regex_range
# Character range
printf 'a\nb\nc\nd\n' | grep '[a-c]'
### expect
a
b
c
### end

### grep_empty_input
# Empty input
printf '' | grep foo
### exit_code: 1
### expect
### end

### grep_binary_files_text
# Same as grep_binary_with_a_flag from the --binary-files=text angle: the NUL
# survives.
printf 'foo\0bar\n' | grep -a foo | tr '\0' '@'
### expect
foo@bar
### end

### grep_count_multiple_matches
# Count with multiple lines
printf 'foo\nfoo\nbar\nfoo\n' | grep -c foo
### expect
3
### end

### grep_invert_count
# Count inverted matches
printf 'foo\nbar\nbaz\n' | grep -vc foo
### expect
2
### end

### grep_combined_flags
# Multiple flags combined
printf 'FOO\nfoo\nfoobar\n' | grep -ic foo
### expect
3
### end

### grep_regex_quantifier_extended
# Exact quantifier with -E
printf 'a\naa\naaa\naaaa\n' | grep -E 'a{2,3}'
### expect
aa
aaa
aaaa
### end

### grep_max_count_exact
# Max count equals matches
printf 'foo\nfoo\n' | grep -m 2 foo
### expect
foo
foo
### end

### grep_max_count_one
# Max count of 1
printf 'foo\nfoo\nfoo\n' | grep -m 1 foo
### expect
foo
### end

### grep_context_multiple_matches
# Context with multiple matches close together
printf 'a\nfoo\nb\nfoo\nc\n' | grep -A 1 foo
### expect
foo
b
foo
c
### end

### grep_context_overlapping
# Overlapping context regions should merge
printf 'a\nfoo\nb\nbar\nc\n' | grep -C 1 -e foo -e bar
### expect
a
foo
b
bar
c
### end

### grep_whole_line_no_match
# Whole line with partial match should not match
printf 'foobar\n' | grep -x foo
### exit_code: 1
### expect
### end

### grep_whole_line_with_spaces
# Whole line including spaces
printf 'foo bar\nfoo\nbar\n' | grep -x 'foo bar'
### expect
foo bar
### end

### grep_multiple_patterns_no_match
# Multiple patterns with no match
printf 'baz\nqux\n' | grep -e foo -e bar
### exit_code: 1
### expect
### end

### grep_quiet_with_count
# Quiet should override count
printf 'foo\nfoo\n' | grep -qc foo
### exit_code: 0
### expect
### end

### grep_max_count_with_context
# Max count with context
printf 'a\nfoo\nb\nfoo\nc\nfoo\nd\n' | grep -m 2 -A 1 foo
### expect
foo
b
foo
c
### end

### grep_before_context_at_start
# Before context at file start
printf 'foo\nb\nc\n' | grep -B 2 foo
### expect
foo
### end

### grep_after_context_at_end
# After context at file end
printf 'a\nb\nfoo\n' | grep -A 2 foo
### expect
foo
### end

### grep_context_with_line_numbers
# Context with line numbers
printf 'a\nfoo\nb\n' | grep -n -C 1 foo
### expect
1-a
2:foo
3-b
### end

### grep_multiple_patterns_case_insensitive
# Multiple patterns with case insensitive
printf 'FOO\nBAR\nbaz\n' | grep -i -e foo -e bar
### expect
FOO
BAR
### end

### grep_whole_line_case_insensitive
# Whole line with case insensitive
printf 'FOO\nfoo\nFOObar\n' | grep -ix foo
### expect
FOO
foo
### end

### grep_max_count_zero
# Max count of zero should match nothing
printf 'foo\nfoo\n' | grep -m 0 foo
### exit_code: 1
### expect
### end

### grep_max_count_zero_readable_file
# -m0 selects nothing and exits 1, with or without -q.
printf 'foo\n' > /tmp/grep_qm0_readable.txt
grep -q -m0 foo /tmp/grep_qm0_readable.txt
echo $?
grep -m0 foo /tmp/grep_qm0_readable.txt
echo $?
### expect
1
1
### end

### grep_max_count_zero_missing_file_option_orders
# Every spelling exits before opening an unreadable operand, without a
# diagnostic. GNU grep 3.11 does not require -q for this.
grep -qm0 foo /tmp/nonexistent_grep_qm0 2>&1
echo $?
grep -q -m0 foo /tmp/nonexistent_grep_qm0 2>&1
echo $?
grep -m0 -q foo /tmp/nonexistent_grep_qm0 2>&1
echo $?
grep --quiet --max-count=0 foo /tmp/nonexistent_grep_qm0 2>&1
echo $?
grep -m0 foo /tmp/nonexistent_grep_qm0 2>&1
echo $?
grep --max-count=0 foo /tmp/nonexistent_grep_qm0 2>&1
echo $?
### expect
1
1
1
1
1
1
### end

### grep_max_count_zero_skips_pattern_compilation
# The short-circuit happens before the pattern is compiled, so an invalid
# regex is not reported either. Matches GNU grep 3.11.
grep -m0 '[' /tmp/grep_qm0_readable.txt 2>&1
echo $?
grep -q -m0 '[' /tmp/nonexistent_grep_qm0 2>&1
echo $?
### expect
1
1
### end

### grep_max_count_zero_suppresses_other_output_modes
# -c, -l and -v all produce nothing under -m0.
grep -c -m0 foo /tmp/grep_qm0_readable.txt
echo $?
grep -l -m0 foo /tmp/grep_qm0_readable.txt
echo $?
grep -v -m0 foo /tmp/grep_qm0_readable.txt
echo $?
### expect
1
1
1
### end

### grep_max_count_does_not_truncate_files_without_match
# -L is the exception: "no line selected" is what makes every operand
# qualify, so it still opens each one and lists them all.
printf 'foo\n' > /tmp/grep_L_m.txt
printf 'bar\n' > /tmp/grep_L_n.txt
grep -L -m0 foo /tmp/grep_L_m.txt /tmp/grep_L_n.txt
echo $?
grep -L -m1 foo /tmp/grep_L_m.txt /tmp/grep_L_n.txt
echo $?
### expect
/tmp/grep_L_m.txt
/tmp/grep_L_n.txt
1
/tmp/grep_L_n.txt
0
### end

### grep_max_count_is_per_file_not_cumulative
# GNU -m NUM stops reading *each file* after NUM matching lines. The budget is
# per operand, so a file that spends it does not silence the next one.
printf 'foo1\nfoo2\nfoo3\n' > /tmp/grep_mm_p.txt
printf 'foo4\nfoo5\n'       > /tmp/grep_mm_q.txt
grep -m1 foo /tmp/grep_mm_p.txt /tmp/grep_mm_q.txt
echo "rc=$?"
grep -m2 foo /tmp/grep_mm_p.txt /tmp/grep_mm_q.txt
echo "rc=$?"
### expect
/tmp/grep_mm_p.txt:foo1
/tmp/grep_mm_q.txt:foo4
rc=0
/tmp/grep_mm_p.txt:foo1
/tmp/grep_mm_p.txt:foo2
/tmp/grep_mm_q.txt:foo4
/tmp/grep_mm_q.txt:foo5
rc=0
### end

### grep_max_count_per_file_with_count_and_invert
# -c reports a count for every operand, not just until a shared budget runs
# out, and -v applies the same per-file budget to non-matching lines.
# Each spec block gets a fresh VFS, so this recreates its own inputs.
printf 'foo1\nfoo2\nfoo3\n' > /tmp/grep_mm_p.txt
printf 'foo4\nfoo5\n'       > /tmp/grep_mm_q.txt
printf 'foo\nbar\nbaz\n'   > /tmp/grep_mm_v.txt
grep -c -m1 foo /tmp/grep_mm_p.txt /tmp/grep_mm_q.txt
echo "rc=$?"
grep -v -m1 foo /tmp/grep_mm_v.txt /tmp/grep_mm_q.txt
echo "rc=$?"
### expect
/tmp/grep_mm_p.txt:1
/tmp/grep_mm_q.txt:1
rc=0
/tmp/grep_mm_v.txt:bar
rc=0
### end

### grep_files_without_match
# -L prints files that have no matches
printf 'foo\n' > /tmp/grep_l_a.txt && printf 'bar\n' > /tmp/grep_l_b.txt && grep -L foo /tmp/grep_l_a.txt /tmp/grep_l_b.txt
### expect
/tmp/grep_l_b.txt
### end

### grep_files_without_match_no_output
# -L does not print files that match. The status still reports whether a
# *match* was found, so a matching file means exit 0 even though -L prints
# nothing; a non-matching file prints the name and exits 1.
printf 'foo\n' > /tmp/grep_l2_a.txt && grep -L foo /tmp/grep_l2_a.txt
### exit_code: 0
### expect
### end

### grep_exclude_dir
# --exclude-dir skips directories during recursive search
mkdir -p /tmp/grepexd/src /tmp/grepexd/vendor && printf 'match\n' > /tmp/grepexd/src/a.txt && printf 'match\n' > /tmp/grepexd/vendor/b.txt && grep -r --exclude-dir=vendor match /tmp/grepexd
### expect
/tmp/grepexd/src/a.txt:match
### end

### grep_suppress_errors
# -s suppresses the error message but not the status: GNU grep exits 2
# whenever an operand could not be read, matched or not.
grep -s foo /tmp/nonexistent_grep_file_xyz
echo $?
### expect
2
### end

### grep_null_filename_l
# -Z with -l uses null byte after filename
printf 'foo\n' > /tmp/grep_z_a.txt && grep -lZ foo /tmp/grep_z_a.txt | tr '\0' '\n'
### expect
/tmp/grep_z_a.txt
### end

### grep_null_filename_big_l
# -Z with -L uses null byte after filename
printf 'bar\n' > /tmp/grep_zl_a.txt && grep -LZ foo /tmp/grep_zl_a.txt | tr '\0' '\n'
### expect
/tmp/grep_zl_a.txt
### end

### grep_skip_binary_files
# -I skips files containing NUL bytes; -L lists them as non-matching
mkdir -p /tmp/gI && cd /tmp/gI
printf 'needle\n' > text.txt
printf 'needle\0bin\n' > data.bin
grep -rI needle . | sort
grep -c --binary-files=without-match needle data.bin; echo "rc=$?"
### expect
./text.txt:needle
0
rc=1
### end

### grep_recursive_directory_cycle
# Resolved ancestors are skipped; independent aliases must still be searched.
mkdir -p /tmp/greploop/d
printf 'x\n' > /tmp/greploop/d/f
ln -s . /tmp/greploop/d/a
grep -R x /tmp/greploop/d 2>&1 | sort
### expect
/tmp/greploop/d/f:x
grep: /tmp/greploop/d/a: warning: recursive directory loop
### end

### grep_recursive_sibling_aliases
mkdir -p /tmp/grepaliases/d /tmp/grepaliases/target
printf 'x\n' > /tmp/grepaliases/target/f
ln -s /tmp/grepaliases/target /tmp/grepaliases/d/a
ln -s /tmp/grepaliases/target /tmp/grepaliases/d/b
grep -R x /tmp/grepaliases/d | sort
### expect
/tmp/grepaliases/d/a/f:x
/tmp/grepaliases/d/b/f:x
### end
