Skip to content

Commit 0ca96e0

Browse files
zyctreeBurntSushi
authored andcommitted
printer: fix context bug when --max-count is used
In the case where after-context is requested with a match count limit, we need to be careful not to reset the state tracking the remaining context lines. Fixes #1380, Closes #1642
1 parent 2295061 commit 0ca96e0

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Bug fixes:
4444

4545
* [BUG #1277](https://github.com/BurntSushi/ripgrep/issues/1277):
4646
Document cygwin path translation behavior in the FAQ.
47+
* [BUG #1642](https://github.com/BurntSushi/ripgrep/issues/1642):
48+
Fixes a bug where using `-m` and `-A` printed more matches than the limit.
4749
* [BUG #1703](https://github.com/BurntSushi/ripgrep/issues/1703):
4850
Clarify the function of `-u/--unrestricted`.
4951
* [BUG #1708](https://github.com/BurntSushi/ripgrep/issues/1708):

crates/printer/src/json.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,16 @@ impl<'p, 's, M: Matcher, W: io::Write> JSONSink<'p, 's, M, W> {
644644
self.after_context_remaining == 0
645645
}
646646

647+
/// Returns whether the current match count exceeds the configured limit.
648+
/// If there is no limit, then this always returns false.
649+
fn match_more_than_limit(&self) -> bool {
650+
let limit = match self.json.config.max_matches {
651+
None => return false,
652+
Some(limit) => limit,
653+
};
654+
self.match_count > limit
655+
}
656+
647657
/// Write the "begin" message.
648658
fn write_begin_message(&mut self) -> io::Result<()> {
649659
if self.begin_printed {
@@ -667,7 +677,20 @@ impl<'p, 's, M: Matcher, W: io::Write> Sink for JSONSink<'p, 's, M, W> {
667677
self.write_begin_message()?;
668678

669679
self.match_count += 1;
670-
self.after_context_remaining = searcher.after_context() as u64;
680+
// When we've exceeded our match count, then the remaining context
681+
// lines should not be reset, but instead, decremented. This avoids a
682+
// bug where we display more matches than a configured limit. The main
683+
// idea here is that 'matched' might be called again while printing
684+
// an after-context line. In that case, we should treat this as a
685+
// contextual line rather than a matching line for the purposes of
686+
// termination.
687+
if self.match_more_than_limit() {
688+
self.after_context_remaining =
689+
self.after_context_remaining.saturating_sub(1);
690+
} else {
691+
self.after_context_remaining = searcher.after_context() as u64;
692+
}
693+
671694
self.record_matches(mat.bytes())?;
672695
self.stats.add_matches(self.json.matches.len() as u64);
673696
self.stats.add_matched_lines(mat.lines().count() as u64);
@@ -871,6 +894,38 @@ and exhibited clearly, with a label attached.\
871894
assert_eq!(got.lines().count(), 3);
872895
}
873896

897+
#[test]
898+
fn max_matches_after_context() {
899+
let haystack = "\
900+
a
901+
b
902+
c
903+
d
904+
e
905+
d
906+
e
907+
d
908+
e
909+
d
910+
e
911+
";
912+
let matcher = RegexMatcher::new(r"d").unwrap();
913+
let mut printer =
914+
JSONBuilder::new().max_matches(Some(1)).build(vec![]);
915+
SearcherBuilder::new()
916+
.after_context(2)
917+
.build()
918+
.search_reader(
919+
&matcher,
920+
haystack.as_bytes(),
921+
printer.sink(&matcher),
922+
)
923+
.unwrap();
924+
let got = printer_contents(&mut printer);
925+
926+
assert_eq!(got.lines().count(), 5);
927+
}
928+
874929
#[test]
875930
fn no_match() {
876931
let matcher = RegexMatcher::new(r"DOES NOT MATCH").unwrap();

crates/printer/src/standard.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,16 @@ impl<'p, 's, M: Matcher, W: WriteColor> StandardSink<'p, 's, M, W> {
724724
}
725725
self.after_context_remaining == 0
726726
}
727+
728+
/// Returns whether the current match count exceeds the configured limit.
729+
/// If there is no limit, then this always returns false.
730+
fn match_more_than_limit(&self) -> bool {
731+
let limit = match self.standard.config.max_matches {
732+
None => return false,
733+
Some(limit) => limit,
734+
};
735+
self.match_count > limit
736+
}
727737
}
728738

729739
impl<'p, 's, M: Matcher, W: WriteColor> Sink for StandardSink<'p, 's, M, W> {
@@ -735,7 +745,19 @@ impl<'p, 's, M: Matcher, W: WriteColor> Sink for StandardSink<'p, 's, M, W> {
735745
mat: &SinkMatch,
736746
) -> Result<bool, io::Error> {
737747
self.match_count += 1;
738-
self.after_context_remaining = searcher.after_context() as u64;
748+
// When we've exceeded our match count, then the remaining context
749+
// lines should not be reset, but instead, decremented. This avoids a
750+
// bug where we display more matches than a configured limit. The main
751+
// idea here is that 'matched' might be called again while printing
752+
// an after-context line. In that case, we should treat this as a
753+
// contextual line rather than a matching line for the purposes of
754+
// termination.
755+
if self.match_more_than_limit() {
756+
self.after_context_remaining =
757+
self.after_context_remaining.saturating_sub(1);
758+
} else {
759+
self.after_context_remaining = searcher.after_context() as u64;
760+
}
739761

740762
self.record_matches(mat.bytes())?;
741763
self.replace(mat.bytes())?;
@@ -3413,4 +3435,40 @@ and xxx clearly, with a label attached.
34133435
let got = printer_contents_ansi(&mut printer);
34143436
assert!(!got.is_empty());
34153437
}
3438+
3439+
#[test]
3440+
fn regression_after_context_with_match() {
3441+
let haystack = "\
3442+
a
3443+
b
3444+
c
3445+
d
3446+
e
3447+
d
3448+
e
3449+
d
3450+
e
3451+
d
3452+
e
3453+
";
3454+
3455+
let matcher = RegexMatcherBuilder::new().build(r"d").unwrap();
3456+
let mut printer = StandardBuilder::new()
3457+
.max_matches(Some(1))
3458+
.build(NoColor::new(vec![]));
3459+
SearcherBuilder::new()
3460+
.line_number(true)
3461+
.after_context(2)
3462+
.build()
3463+
.search_reader(
3464+
&matcher,
3465+
haystack.as_bytes(),
3466+
printer.sink(&matcher),
3467+
)
3468+
.unwrap();
3469+
3470+
let got = printer_contents(&mut printer);
3471+
let expected = "4:d\n5-e\n6:d\n";
3472+
assert_eq_printed!(expected, got);
3473+
}
34163474
}

tests/regression.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,28 @@ rgtest!(r1334_crazy_literals, |dir: Dir, mut cmd: TestCommand| {
763763
);
764764
});
765765

766+
// See: https://github.com/BurntSushi/ripgrep/issues/1380
767+
rgtest!(r1380, |dir: Dir, mut cmd: TestCommand| {
768+
dir.create(
769+
"foo",
770+
"\
771+
a
772+
b
773+
c
774+
d
775+
e
776+
d
777+
e
778+
d
779+
e
780+
d
781+
e
782+
",
783+
);
784+
785+
eqnice!("d\ne\nd\n", cmd.args(&["-A2", "-m1", "d", "foo"]).stdout());
786+
});
787+
766788
// See: https://github.com/BurntSushi/ripgrep/issues/1389
767789
rgtest!(r1389_bad_symlinks_no_biscuit, |dir: Dir, mut cmd: TestCommand| {
768790
dir.create_dir("mydir");

0 commit comments

Comments
 (0)