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
40 changes: 36 additions & 4 deletions src/searcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use memchr::memmem::Finder;
use memchr::{memchr, memchr_iter, memrchr};
use std::ffi::OsStr;
use std::fs::File;
use std::io;
use std::io::{self, Seek as _, SeekFrom};
use std::mem::ManuallyDrop;
use std::ops::ControlFlow;
use std::path::Path;
Expand All @@ -33,6 +33,10 @@ pub struct Searcher<'a> {
session_after_remaining: usize,
session_last_printed_line: u64, // 0 = nothing yet
session_binary_detected: bool,
/// Byte offset, relative to where this session started reading, just past
/// the terminator of the last selected line. Used to rewind standard input
/// when `-m` stops the search early.
session_last_match_end: u64,
}

impl<'a> Searcher<'a> {
Expand All @@ -54,6 +58,7 @@ impl<'a> Searcher<'a> {
session_after_remaining: 0,
session_last_printed_line: 0,
session_binary_detected: false,
session_last_match_end: 0,
}
}

Expand Down Expand Up @@ -107,7 +112,20 @@ impl<'a> Searcher<'a> {
self.writer.set_line_number_width(19);
}

// Where standard input was before we started buffering ahead of it.
let start_offset = stdin.stream_position();

let result = self.session_run(lb, path, &mut stdin);

// `-m` stops mid-input, but we have read further than the last selected
// line. Rewind so whoever reads standard input next sees the rest of it,
// as GNU does. A pipe cannot seek; there is nothing to hand back then.
if !self.session_can_match()
&& let Ok(start) = start_offset
{
let _ = stdin.seek(SeekFrom::Start(start + self.session_last_match_end));
Comment on lines +120 to +126

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work if stdin cannot seek?

}

self.record_result(OsStr::new(&self.config.label), result)
}

Expand Down Expand Up @@ -284,6 +302,8 @@ impl<'a> Searcher<'a> {
reader: &mut File,
) -> io::Result<bool> {
lb.reset();
self.session_match_count = 0;
self.session_last_match_end = 0;
if self.config.quiet
|| self.config.files_with_matches
|| self.config.files_without_match
Expand Down Expand Up @@ -317,7 +337,7 @@ impl<'a> Searcher<'a> {

let mut count: u64 = 0;
let mut matched = false;
'outer: while let Some((chunk, _)) = lb.read_chunk(reader)? {
'outer: while let Some((chunk, chunk_off)) = lb.read_chunk(reader)? {
let mut p = 0;
while p < chunk.len() {
let Some(rel) = leftmost_match(finders, &chunk[p..]) else {
Expand All @@ -329,14 +349,17 @@ impl<'a> Searcher<'a> {
let (_, line_end) = line_bounds(chunk, p + rel);
count += 1;
matched = true;
// Each line counts once: resume past this line's terminator.
p = line_end + 1;
self.session_last_match_end = chunk_off + p as u64;
if stop_at_first {
break 'outer;
}
// Each line counts once: resume past this line's terminator.
p = line_end + 1;
}
}

self.session_match_count = count;

// `-l`/`-L` take precedence over `-c`, matching the line-at-a-time path.
if self.config.quiet {
// Exit status only.
Expand Down Expand Up @@ -442,6 +465,7 @@ impl<'a> Searcher<'a> {
count += 1;
matched = true;
p = line_end + 1;
self.session_last_match_end = chunk_off + p as u64;
}

// Carry NUL detection and the line tally across the chunk boundary.
Expand All @@ -453,6 +477,8 @@ impl<'a> Searcher<'a> {
}
}

self.session_match_count = count;

if binary && notice_enabled && matched {
self.writer.report_binary_match(path);
}
Expand All @@ -475,12 +501,16 @@ impl<'a> Searcher<'a> {
self.session_after_remaining = 0;
self.session_last_printed_line = 0;
self.session_binary_detected = false;
self.session_last_match_end = 0;
lb.reset();

let mut line_number: u64 = 0;

while let Some((line, line_start)) = lb.read_line(reader)? {
line_number += 1;
// Offset of the next line. A final line without a terminator makes
// this one byte past the input, which reads the same as its end.
let after_line = line_start + line.len() as u64 + 1;

// Handle `-U, --binary` On Windows.
#[cfg(windows)]
Expand All @@ -504,6 +534,8 @@ impl<'a> Searcher<'a> {
return Ok(false);
}

self.session_last_match_end = after_line;

// Print the match and context, and update session state accordingly.
if !self.session_handle_match(path, line_number, line_start, line, &positions)? {
return Ok(true);
Expand Down
26 changes: 26 additions & 0 deletions tests/test_grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,32 @@ fn reversed_range_endpoints_match_gnu_message() {
}
}

#[test]
fn max_count_rewinds_seekable_stdin() {
// -m stops mid-input after reading ahead. When standard input can seek,
// the unread remainder must be left in place for the next reader.
use std::fs::File;
use std::io::Read as _;
use std::process::{Command, Stdio};

let (scenario, _) = ucmd();
scenario.fixtures.write("lines", "a\nq\nb\nq\nc\n");
let mut input = File::open(scenario.fixtures.plus("lines")).unwrap();

// A dup of the descriptor shares its file offset with the child.
let status = Command::new(env!("CARGO_BIN_EXE_grep"))
.args(["-m1", "q"])
.stdin(Stdio::from(input.try_clone().unwrap()))
.stdout(Stdio::null())
.status()
.unwrap();
assert!(status.success());

let mut rest = String::new();
input.read_to_string(&mut rest).unwrap();
assert_eq!(rest, "b\nq\nc\n");
}

#[test]
fn quiet_match_overrides_file_error() {
// With -q, a match makes grep exit 0 even if an earlier file could not be
Expand Down
Loading