Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 11 additions & 21 deletions crates/bashkit/src/builtins/grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,6 @@ impl Builtin for Grep {
let mut read_failed = false;
let mut any_match = false;
let mut exit_code = 1; // 1 = no match
let mut total_matches = 0usize;

// Determine input sources
// GNU grep names stdin "(standard input)" everywhere it names it at
Expand Down Expand Up @@ -726,20 +725,15 @@ impl Builtin for Grep {
};
let has_context = opts.before_context > 0 || opts.after_context > 0;

let mut max_reached = false;

'file_loop: for (filename, content) in &inputs {
// Check if we already reached max count from previous files.
// `-L` is exempt: its output is driven by the files that *didn't*
// match, so a spent match budget must not stop the enumeration.
// GNU `grep -L -m1 foo match.txt other.txt` still lists other.txt.
if let Some(max) = opts.max_count
&& total_matches >= max
&& !opts.files_without_matches
{
break 'file_loop;
}

// GNU `-m NUM` stops reading *each file* after NUM matching lines;
// the budget is per operand, not shared across them. So
// `grep -m1 foo a b` prints one match from a *and* one from b, and
// `grep -c -m1 foo a b` reports a count for every operand.
// Only the `-o` closure needs this: it cannot break the enclosing
// line loop itself, so it signals the budget out. Every other exit
// path breaks directly.
let mut max_reached = false;
let mut match_count = 0;
let mut file_matched = false;

Expand Down Expand Up @@ -776,9 +770,8 @@ impl Builtin for Grep {
for (line_num, line) in lines.iter().enumerate() {
// Check max count limit before adding more matches
if let Some(max) = opts.max_count
&& total_matches >= max
&& match_count >= max
{
max_reached = true;
break; // Break inner loop, continue to output phase
}

Expand All @@ -789,14 +782,13 @@ impl Builtin for Grep {
file_matched = true;
any_match = true;
match_count += 1;
total_matches += 1;

if opts.files_with_matches || opts.files_without_matches || opts.quiet {
return false;
}

if let Some(max) = opts.max_count
&& total_matches >= max
&& match_count >= max
{
max_reached = true;
return false;
Expand All @@ -821,7 +813,6 @@ impl Builtin for Grep {
file_matched = true;
any_match = true;
match_count += 1;
total_matches += 1;
match_lines.push(line_num);

if opts.files_with_matches || opts.files_without_matches {
Expand All @@ -833,9 +824,8 @@ impl Builtin for Grep {

// Check max after recording this match
if let Some(max) = opts.max_count
&& total_matches >= max
&& match_count >= max
{
max_reached = true;
break;
}
}
Expand Down
39 changes: 39 additions & 0 deletions crates/bashkit/tests/spec_cases/grep/grep.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,45 @@ echo $?
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
Expand Down
Loading