From afc820c9e9e47a2a0e339a948f4da607e3f71a65 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Sun, 27 Jul 2025 15:08:09 -0400 Subject: [PATCH] cli: make `rg -vf file` behave sensibly Previously, when `file` is empty (literally empty, as in, zero byte), `rg -f file` and `rg -vf file` would behave identically. This is odd and also doesn't match how GNU grep behaves. It's also not logically correct. An empty file means _zero_ patterns which is an empty set. An empty set matches nothing. Inverting the empty set should result in matching everything. This was because of an errant optimization that lets ripgrep quit early if it can statically detect that no matches are possible. Moreover, there was *also* a bug in how we constructed the PCRE2 pattern when there are zero patterns. PCRE2 doesn't have a concept of sets of patterns (unlike the `regex` crate), so we need to fake it with an empty character class. Fixes #1332, Fixes #3001, Closes #3041 --- CHANGELOG.md | 3 +++ crates/core/flags/hiargs.rs | 2 +- crates/pcre2/src/matcher.rs | 7 ++++++- tests/regression.rs | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7553cf77..605e9ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Bug fixes: [BUG #2836](https://github.com/BurntSushi/ripgrep/issues/2836), [BUG #2933](https://github.com/BurntSushi/ripgrep/pull/2933): Fix bug related to gitignores from parent directories. +* [BUG #1332](https://github.com/BurntSushi/ripgrep/issues/1332), + [BUG #3001](https://github.com/BurntSushi/ripgrep/issues/3001): + Make `rg -vf file` where `file` is empty match everything. * [BUG #2177](https://github.com/BurntSushi/ripgrep/issues/2177): Ignore a UTF-8 BOM marker at the start of `.gitignore` (and similar files). diff --git a/crates/core/flags/hiargs.rs b/crates/core/flags/hiargs.rs index 1fd93708..53490a6d 100644 --- a/crates/core/flags/hiargs.rs +++ b/crates/core/flags/hiargs.rs @@ -517,7 +517,7 @@ impl HiArgs { /// When this returns false, it is impossible for ripgrep to ever report /// a match. pub(crate) fn matches_possible(&self) -> bool { - if self.patterns.patterns.is_empty() { + if self.patterns.patterns.is_empty() && !self.invert_match { return false; } if self.max_count == Some(0) { diff --git a/crates/pcre2/src/matcher.rs b/crates/pcre2/src/matcher.rs index 56c9356d..9a6710c0 100644 --- a/crates/pcre2/src/matcher.rs +++ b/crates/pcre2/src/matcher.rs @@ -55,7 +55,12 @@ impl RegexMatcherBuilder { format!("(?:{})", p.as_ref()) }); } - let mut singlepat = pats.join("|"); + let mut singlepat = if patterns.is_empty() { + // A way to spell a pattern that can never match anything. + r"[^\S\s]".to_string() + } else { + pats.join("|") + }; if self.case_smart && !has_uppercase_literal(&singlepat) { builder.caseless(true); } diff --git a/tests/regression.rs b/tests/regression.rs index 4c087844..bd845905 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -955,6 +955,43 @@ rgtest!(r1319, |dir: Dir, mut cmd: TestCommand| { ); }); +// See: https://github.com/BurntSushi/ripgrep/issues/1332 +rgtest!(r1334_invert_empty_patterns, |dir: Dir, _cmd: TestCommand| { + dir.create("zero-patterns", ""); + dir.create("one-pattern", "\n"); + dir.create("haystack", "one\ntwo\nthree\n"); + + // zero patterns matches nothing + { + let mut cmd = dir.command(); + cmd.arg("-f").arg("zero-patterns").arg("haystack").assert_err(); + } + // one pattern that matches empty string matches everything + { + let mut cmd = dir.command(); + eqnice!( + "one\ntwo\nthree\n", + cmd.arg("-f").arg("one-pattern").arg("haystack").stdout() + ); + } + + // inverting zero patterns matches everything + // (This is the regression. ripgrep used to match nothing because of an + // incorrect optimization.) + { + let mut cmd = dir.command(); + eqnice!( + "one\ntwo\nthree\n", + cmd.arg("-vf").arg("zero-patterns").arg("haystack").stdout() + ); + } + // inverting one pattern that matches empty string matches nothing + { + let mut cmd = dir.command(); + cmd.arg("-vf").arg("one-pattern").arg("haystack").assert_err(); + } +}); + // See: https://github.com/BurntSushi/ripgrep/issues/1334 rgtest!(r1334_crazy_literals, |dir: Dir, mut cmd: TestCommand| { dir.create("patterns", &"1.208.0.0/12\n".repeat(40));