From 103c65a4d188cad6d4c983ed7a5b65939389ed4b Mon Sep 17 00:00:00 2001 From: Sichen Date: Mon, 28 Sep 2026 05:32:29 +0000 Subject: [PATCH] grep: separate context groups across files Track whether a previous file printed a context group so the first group in a later file receives a separator. Within a file, continue to separate only groups with a gap between their printed lines. Cover the default, disabled and custom separators, including recursive searches across two directories. --- src/searcher.rs | 12 ++++--- tests/test_grep.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/searcher.rs b/src/searcher.rs index 8a17223..3eca035 100644 --- a/src/searcher.rs +++ b/src/searcher.rs @@ -26,6 +26,8 @@ pub struct Searcher<'a> { any_match: bool, had_error: bool, binary_notice_enabled: bool, + /// Whether a context group was printed by any file in this search. + any_group_seen: bool, // Per-session state session_context_buf: ContextBuffer, @@ -52,6 +54,7 @@ impl<'a> Searcher<'a> { && !config.count && !config.files_with_matches && !config.files_without_match, + any_group_seen: false, session_context_buf: ContextBuffer::new(config.before_context), session_match_count: 0, @@ -671,11 +674,11 @@ impl<'a> Searcher<'a> { .map_or(view.line_number, |ctx| ctx.line_number); // Group separator between non-adjacent groups. - // `last_printed_line == 0` means we haven't printed anything yet. - // = first group = skip the separator + // `last_printed_line == 0` marks the first group in this file. + // Separate it only when a previous file has printed a group. if self.config.has_context - && last_printed_line > 0 - && group_start_line > last_printed_line + 1 + && ((last_printed_line == 0 && self.any_group_seen) + || (last_printed_line > 0 && group_start_line > last_printed_line + 1)) { self.writer.write_group_separator()?; } @@ -687,6 +690,7 @@ impl<'a> Searcher<'a> { self.writer.write_line(view, path)?; self.session_last_printed_line = view.line_number; + self.any_group_seen = true; Ok(()) } diff --git a/tests/test_grep.rs b/tests/test_grep.rs index a38d570..c77d0d5 100644 --- a/tests/test_grep.rs +++ b/tests/test_grep.rs @@ -1221,6 +1221,86 @@ fn group_separator_behavior() { .stdout_only("M\n***\nM\n"); } +#[test] +fn context_groups_are_separated_across_files() { + let (scene, _) = ucmd(); + scene.fixtures.write("first", "hit\nno\nno\nhit\n"); + scene.fixtures.write("second", "hit\n"); + scene.fixtures.write("b", "hit three\nno\nfar\nhit four\n"); + scene.fixtures.write("c", "no\nhit five\n"); + scene.fixtures.write("d", "nothing\n"); + + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-n", "-C0", "hit", "first", "second"]) + .succeeds() + .stdout_only("first:1:hit\n--\nfirst:4:hit\n--\nsecond:1:hit\n"); + + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-n", "-C1", "hit", "b", "c"]) + .succeeds() + .stdout_only("b:1:hit three\nb-2-no\nb-3-far\nb:4:hit four\n--\nc-1-no\nc:2:hit five\n"); + + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-n", "-C0", "hit", "d", "first"]) + .succeeds() + .stdout_only("first:1:hit\n--\nfirst:4:hit\n"); + + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-n", "-C0", "hit", "first", "d", "second"]) + .succeeds() + .stdout_only("first:1:hit\n--\nfirst:4:hit\n--\nsecond:1:hit\n"); +} + +#[test] +fn context_group_separator_options_across_files() { + let (scene, _) = ucmd(); + scene.fixtures.write("first", "hit\nno\nno\nhit\n"); + scene.fixtures.write("second", "hit\n"); + + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&[ + "-n", + "-C0", + "--no-group-separator", + "hit", + "first", + "second", + ]) + .succeeds() + .stdout_only("first:1:hit\nfirst:4:hit\nsecond:1:hit\n"); + + scene + .cmd(env!("CARGO_BIN_EXE_grep")) + .args(&["-n", "-C0", "--group-separator=X", "hit", "first", "second"]) + .succeeds() + .stdout_only("first:1:hit\nX\nfirst:4:hit\nX\nsecond:1:hit\n"); +} + +#[test] +fn recursive_context_groups_are_separated_across_files() { + let (scene, mut c) = ucmd(); + scene.fixtures.mkdir("tree-a"); + scene.fixtures.mkdir("tree-b"); + scene.fixtures.write("tree-a/one", "hit\n"); + scene.fixtures.write("tree-b/two", "hit\n"); + let first = std::path::Path::new("tree-a").join("one"); + let second = std::path::Path::new("tree-b").join("two"); + let expected = format!( + "{}:1:hit\n--\n{}:1:hit\n", + first.display(), + second.display() + ); + + c.args(&["-r", "-n", "-C0", "hit", "tree-a", "tree-b"]) + .succeeds() + .stdout_only(&expected); +} + #[test] fn adjacent_context_groups_do_not_get_separator() { let (_s, mut c) = ucmd();