grep: map invalid back-reference errors to GNU's wording

A back-reference to a non-existent group, e.g. (.)\2, makes oniguruma
fail with 'invalid backref number/name'. GNU words this per engine:
'reference to non-existent subpattern' under -P (PCRE2) and 'Invalid
back reference' for basic/extended (gnulib regex). Translate
ONIGERR_INVALID_BACKREF accordingly (gnu_error_message now takes the
regex mode). Fixes the GNU testsuite 'pcre-wx-backref' test.

Also drop pipe_in() from the compile-error tests added in the previous
commits: those patterns are rejected before stdin is read, so feeding
input raced with the child exiting and intermittently panicked the test
harness with a broken pipe under parallel execution.
This commit is contained in:
Sylvestre Ledru
2026-05-30 18:02:32 +02:00
parent 9c21a7d2f0
commit da32a63663
2 changed files with 48 additions and 8 deletions
+18 -5
View File
@@ -9,7 +9,7 @@ use onig::{
SyntaxBehavior, SyntaxOperator,
};
use onig_sys::{
ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS, ONIGERR_RETRY_LIMIT_IN_MATCH_OVER,
ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS, ONIGERR_INVALID_BACKREF, ONIGERR_RETRY_LIMIT_IN_MATCH_OVER,
ONIGERR_RETRY_LIMIT_IN_SEARCH_OVER, OnigEncCtype_ONIGENC_CTYPE_WORD, OnigEncodingUTF8,
};
use std::io;
@@ -248,22 +248,29 @@ impl CompiledPattern {
options |= RegexOptions::REGEX_OPTION_IGNORECASE;
}
fn compile_with(pattern: &str, syntax: &Syntax, options: RegexOptions) -> UResult<Regex> {
let mode = config.regex_mode;
fn compile_with(
pattern: &str,
syntax: &Syntax,
options: RegexOptions,
mode: RegexMode,
) -> UResult<Regex> {
Regex::with_options_and_encoding(pattern, options, syntax).map_err(|err| {
// Prefer GNU grep's wording for the errors it has a dedicated
// message for; fall back to oniguruma's text otherwise.
match gnu_error_message(err.code()) {
match gnu_error_message(err.code(), mode) {
Some(msg) => USimpleError::new(2, msg.to_string()),
None => USimpleError::new(2, format!("invalid pattern \"{pattern}\": {err}")),
}
})
}
let leftmost = compile_with(pattern, &syntax, options)?;
let leftmost = compile_with(pattern, &syntax, options, mode)?;
let longest_anchored = compile_with(
pattern,
&syntax,
options | RegexOptions::REGEX_OPTION_FIND_LONGEST,
mode,
)?;
Ok(Self {
leftmost,
@@ -336,10 +343,16 @@ fn match_error(err: Error) -> io::Error {
/// `Invalid range end`) rather than oniguruma's phrasing, so translating keeps
/// us byte-compatible. Returns `None` for errors with no GNU equivalent, where
/// the caller falls back to oniguruma's own message.
fn gnu_error_message(code: i32) -> Option<&'static str> {
fn gnu_error_message(code: i32, mode: RegexMode) -> Option<&'static str> {
match code {
// e.g. `[b-a]`: a range whose end precedes its start.
ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS => Some("Invalid range end"),
// e.g. `(.)\2`: a back-reference to a group that does not exist. GNU
// (via PCRE2) and gnulib's regex word this differently.
ONIGERR_INVALID_BACKREF if mode == RegexMode::Perl => {
Some("reference to non-existent subpattern")
}
ONIGERR_INVALID_BACKREF => Some("Invalid back reference"),
_ => None,
}
}
+30 -3
View File
@@ -130,10 +130,11 @@ fn ere_invalid_pattern_is_error() {
fn confusing_bracket_class_is_error() {
// GNU grep rejects the misspelled `[:name:]` form (meant to be
// `[[:name:]]`) with a dedicated diagnostic and exit code 2.
// No piped input: the pattern is rejected at compile time, before stdin is
// read, so feeding stdin would race with the child exiting (broken pipe).
for pattern in ["[:space:]", "[:digit:]", "[^:space:]", "x[:space:]y"] {
let (_s, mut c) = ucmd();
c.args(&[pattern])
.pipe_in("x\n")
.fails_with_code(2)
.stderr_is("grep: character class syntax is [[:space:]], not [:space:]\n");
}
@@ -141,7 +142,6 @@ fn confusing_bracket_class_is_error() {
// The same diagnostic applies in extended mode.
let (_s, mut c) = ucmd();
c.args(&["-E", "[:space:]"])
.pipe_in("x\n")
.fails_with_code(2)
.stderr_is("grep: character class syntax is [[:space:]], not [:space:]\n");
}
@@ -184,10 +184,10 @@ fn lookalike_brackets_are_not_confusing() {
fn reversed_range_uses_gnu_wording() {
// A range like `[b-a]` is an error; GNU prints the bare POSIX diagnostic
// "Invalid range end" (not oniguruma's phrasing) and exits 2.
// No piped input: the pattern is rejected before stdin is read.
for args in [&["[b-a]"][..], &["-E", "[b-a]"][..]] {
let (_s, mut c) = ucmd();
c.args(args)
.pipe_in("x\n")
.fails_with_code(2)
.stderr_is("grep: Invalid range end\n");
}
@@ -205,6 +205,33 @@ fn pcre_backtracking_limit_does_not_abort() {
.stderr_contains("backtracking limit");
}
#[test]
fn invalid_backreference_uses_gnu_wording() {
// A back-reference to a non-existent group is worded differently by GNU
// depending on the engine: PCRE (-P) vs gnulib regex (BRE/ERE).
// No piped input: these patterns are rejected before stdin is read.
let (_s, mut c) = ucmd();
c.args(&["-P", r"(.)\2"])
.fails_with_code(2)
.stderr_is("grep: reference to non-existent subpattern\n");
for args in [&["-E", r"(.)\2"][..], &[r"\(.\)\2"][..]] {
let (_s, mut c) = ucmd();
c.args(args)
.fails_with_code(2)
.stderr_is("grep: Invalid back reference\n");
}
// A valid back-reference with -Pw / -Px must still match.
for flag in ["-Pw", "-Px"] {
let (_s, mut c) = ucmd();
c.args(&[flag, r"(.)\1"])
.pipe_in("aa\n")
.succeeds()
.stdout_is("aa\n");
}
}
#[test]
fn fixed_string_is_literal() {
// Metacharacters are not interpreted.