#3508·ripgrep

-U/--multiline with PCRE2 (-P): a top-level anchor (^/$) inside a lookaround silently drops an unrelated match elsewhere in the same pattern

Author: simonfossomCreated Aug 8, 2026Updated Sep 1, 2026

What version of ripgrep are you using?

ripgrep 15.1.0

features:+pcre2
simd(compile):+NEON
simd(runtime):+NEON

PCRE2 10.45 is available (JIT is available)

How did you install ripgrep?

Homebrew

What operating system are you using ripgrep on?

macOS 26.5.2 (arm64)

Describe your bug.

Under -P -U --passthru -r (multiline PCRE2 search-and-replace), adding a top-level anchor (^ or $) as one alternative inside a lookaround causes ripgrep to silently fail to find/replace a different, unrelated match elsewhere in the buffer — even though the anchor itself never matches at that other position. Removing the anchor from the lookaround (keeping everything else identical) fixes the match. No error, no warning; the affected occurrence is just left untouched, as if it were never there.

I believe this is related to the mechanism @BurntSushi described in #1878 — ripgrep's heuristic for deciding whether it can avoid buffering/mmap-ing the full multiline input because it "knows the pattern can't match through a line terminator," which can misjudge when anchors are involved. #1878 itself is fixed on this version (verified below), but this looks like a related, uncovered edge of the same heuristic — the anchor here is nested inside a lookaround, alongside another lookaround, rather than appearing at the top level of the pattern.

I verified this is not a PCRE2 bug: the identical pattern against the identical input, run directly through libpcre2 10.47 via pcre2test (with and without JIT), and independently through Perl's regex engine, both find the match correctly. Only ripgrep's own multiline search path drops it.

What are the steps to reproduce the behavior?

$ printf 'The shape that\nworked, and that Si established.\n' > f.txt

# control: no anchor in the lookaround — correctly joins into one line
$ rg -P -U --passthru --no-line-number '(?<=[^\n])\n[ \t]*(?!\n)' -r ' ' f.txt

# bug: add "|$" to the lookahead — the SAME interior \n stops matching
$ rg -P -U --passthru --no-line-number '(?<=[^\n])\n[ \t]*(?!\n|$)' -r ' ' f.txt

# same defect with "^" instead of "$" (not $-specific)
$ rg -P -U --passthru --no-line-number '(?<=[^\n])\n[ \t]*(?!\n|^)' -r ' ' f.txt

Confirming $/^ don't actually match at the affected position (so their mere presence in the alternation, not an actual match, is what breaks things):

$ rg -P -U --passthru --no-line-number '$' -r '<EOL>' f.txt
The shape that<EOL>
worked, and that Si established.<EOL>
# <EOL> only appears at the two real line-ends — never at the position
# right after the interior \n, which is exactly where the bug manifests.

Cross-check against raw PCRE2 (not ripgrep) — same pattern, same subject, correct result:

$ printf '/(?<=[^\n])\n[ \t]*(?!\n|$)/gm\nThe shape that\nworked, and that Si established.\n\n\n' | pcre2test
PCRE2 version 10.47 2025-10-21 (8-bit)
/(?<=[^\n])\n[ \t]*(?!\n|$)/gm
The shape that\nworked, and that Si established.\n
 0: \x0a

(Same result with the jit modifier added — not a JIT artifact either.)

What is the actual behavior?

$ rg --debug -P -U --passthru --no-line-number '(?<=[^\n])\n[ \t]*(?!\n)' -r ' ' f.txt
The shape that worked, and that Si established.
$ rg --debug -P -U --passthru --no-line-number '(?<=[^\n])\n[ \t]*(?!\n|$)' -r ' ' f.txt
The shape that
worked, and that Si established.

(--debug output otherwise identical between the two runs — no extra diagnostic signal from it in this case; happy to gather more if there's a specific flag/build that would surface the internal buffering decision.)

What is the expected behavior?

Both invocations should produce identical output — The shape that worked, and that Si established. — since $/^ never match at the position where the join is being decided. Adding an anchor to an alternation should only change behavior at positions where that anchor itself can match, never at unrelated positions elsewhere in the pattern's alternation.