Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 205 additions & 25 deletions src/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
// file that was distributed with this source code.

use crate::{Config, RegexMode};
use memchr::memmem;
use memchr::{memchr, memmem};
use onig::{RegexOptions, Region, SearchOptions, Syntax, SyntaxBehavior, SyntaxOperator};
use onig_sys::{
ONIGERR_EMPTY_RANGE_IN_CHAR_CLASS, OnigEncCtype_ONIGENC_CTYPE_WORD, OnigEncodingUTF8,
};
use std::borrow::Cow;
use std::ptr::{null, null_mut};
use std::sync::Mutex;
use uucore::error::{UResult, USimpleError};
Expand Down Expand Up @@ -287,6 +288,13 @@ impl CompiledPattern {
));
}

let pattern = if config.regex_mode == RegexMode::Fixed {
Cow::Borrowed(pattern)
} else {
rewrite_equivalence_classes(pattern)
};
let pattern: &str = &pattern;

let mut normalized_pattern = None;
let pattern = if config.regex_mode == RegexMode::Extended {
if let Some((op, rest)) = strip_leading_repeat_operator(pattern) {
Expand Down Expand Up @@ -541,22 +549,39 @@ fn strip_leading_interval_repeat(pattern: &str) -> Option<&str> {
/// class or collating element.
fn has_confusing_bracket(pattern: &[u8]) -> bool {
let mut i = 0;
while i < pattern.len() {
match pattern[i] {
b'\\' => i += 2,
b'[' => {
let (confusing, next) = scan_bracket(pattern, i + 1);
if confusing {
return true;
}
i = next;
}
_ => i += 1,
while let Some(open) = next_unescaped_bracket(pattern, i) {
let (confusing, next) = scan_bracket(pattern, open + 1);
if confusing {
return true;
Comment on lines +552 to +555

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this a lot.

}
i = next;
}
false
}

/// Index of the first `[` at or after `from` that is not escaped by a
/// backslash, or `None` if the pattern holds no such bracket.
///
/// Only `[` is significant between bracket expressions, so jump straight to
/// the next one instead of walking the pattern a byte at a time. A backslash
/// run directly in front of the match decides whether it opens a bracket: an
/// odd count escapes it, an even one leaves the `[` itself unescaped (`\\[`).
fn next_unescaped_bracket(pattern: &[u8], from: usize) -> Option<usize> {
let mut search = from;
while let Some(offset) = memchr(b'[', &pattern[search..]) {
let at = search + offset;
let mut backslashes = 0;
while at - backslashes > from && pattern[at - backslashes - 1] == b'\\' {
backslashes += 1;
}
if backslashes % 2 == 0 {
return Some(at);
}
search = at + 1;
}
None
}
Comment on lines +569 to +583

@lhecker lhecker Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, what I had in mind was a scalar loop ("memchr style"):

fn next_unescaped_bracket_scalar(pattern: &[u8], mut from: usize) -> Option<usize> {
    let mut escape = false;
    while from < pattern.len() {
        match pattern[from] {
            b'[' => {
                if !escape {
                    return Some(from);
                }
            }
            b'\\' => escape = !escape,
            _ => escape = false,
        }
        from += 1;
    }
    None
}

Always keep in mind that LLMs were trained on tons of mostly average code, so the code they produce is similarly mostly average. Using a vectorized scanner on short inputs doesn't make much sense since the branch predictor just slaps it away. And the scalar loop produces less assembly.


/// Scan the body of a bracket expression starting at `start` (just past the
/// `[`). Returns whether it is a misspelled character class and the index just
/// past its closing `]`.
Expand Down Expand Up @@ -608,6 +633,119 @@ fn scan_bracket(pattern: &[u8], start: usize) -> (bool, usize) {
(false, pattern.len())
}

/// Rewrite POSIX equivalence classes (`[=c=]`) to their bare member `c`.
///
/// A POSIX bracket expression may contain an equivalence class `[=c=]` that
/// matches every character collating equal to `c`. In the single-byte C locale
/// that set is just `c` itself, and oniguruma has no syntax for equivalence
/// classes at all, so a pattern containing one cannot be compiled as-is. This
/// function rewrites the classes oniguruma cannot express into the nearest
/// equivalent it can, leaving everything else in the pattern untouched:
///
/// * `[[=a=]]` becomes `[a]` — the class collapses to its sole member.
/// * `[[=a=]b]` becomes `[ab]` — members compose with other entries.
/// * `[[=a=][=b=]]` becomes `[ab]` — several classes in one bracket.
/// * `x[[=a=]]y` becomes `x[a]y` — text outside the bracket is preserved.
///
/// Returns a borrowed `Cow` when there is nothing to rewrite (the common case,
/// so no allocation is needed) and an owned one otherwise.
///
/// A class whose body is not exactly one character is left in place, as is one
/// used as a range endpoint (an error in GNU grep, so it must not be quietly
/// turned into a range) or one holding `]`, `^`, `-` or `\`, whose meaning
/// inside a bracket expression depends on where it sits.
fn rewrite_equivalence_classes(pattern: &str) -> Cow<'_, str> {
let bytes = pattern.as_bytes();
let mut out: Option<String> = None;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Init as String::new() (no Option), then return Borrowed(pattern) if out.is_empty().

let mut copied = 0;
let mut i = 0;

while let Some(open) = next_unescaped_bracket(bytes, i) {
let (rewritten, next) = scan_equivalence_bracket(pattern, open);
if let Some(body) = rewritten {
let out = out.get_or_insert_with(String::new);
out.push_str(&pattern[copied..=open]);
out.push_str(&body);
copied = next;
}
i = next;
}

match out {
None => Cow::Borrowed(pattern),
Some(mut s) => {
s.push_str(&pattern[copied..]);
Cow::Owned(s)
}
}
}

/// Scan a single bracket expression whose opening `[` is at `open` and rewrite
/// every rewritable `[=c=]` equivalence class in its body to the bare member
/// `c`. Returns the rewritten body (including the closing `]`) together with
/// the index just past that `]`; the body is `None` when the bracket contains
/// no rewritable equivalence class, so the caller can leave the span alone.
/// Mirrors `scan_bracket`, which does the equivalent job for
/// `has_confusing_bracket`, so the two stay structurally identical.
fn scan_equivalence_bracket(pattern: &str, open: usize) -> (Option<String>, usize) {
let bytes = pattern.as_bytes();
let mut j = open + 1;
if bytes.get(j) == Some(&b'^') {
j += 1;
}
let body_start = j;
let mut out: Option<String> = None;
let mut copied = open + 1;
Comment on lines +691 to +698

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you ask an LLM to remodel this function based on first principles? I have an inkling it could be easier to read.


while j < bytes.len() {
// A `]` at the very start of the body is an ordinary character; any
// other `]` closes the bracket.
if bytes[j] == b']' && j != body_start {
if let Some(out) = out.as_mut() {
out.push_str(&pattern[copied..=j]);
}
return (out, j + 1);
}
// `[:`, `[.` and `[=` subexpressions inside the bracket.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape handling missing from what I can tell. Use next_unescaped_bracket. You'll need something like that for the closing bracket too. Perhaps make next_unescaped_bracket generic over the needle.

if bytes[j] == b'[' && matches!(bytes.get(j + 1), Some(b':' | b'.' | b'=')) {
let delimiter = bytes[j + 1];
if let Some(end) = find_bracket_subexpr_end(bytes, j + 2, delimiter) {
if delimiter == b'='
&& is_rewritable_equivalence(
&pattern[j + 2..end - 2],
bytes.get(end).copied(),
if j > 0 { Some(bytes[j - 1]) } else { None },
)
Comment on lines +714 to +718

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hard read in particular.

{
let body = &pattern[j + 2..end - 2];
let out = out.get_or_insert_with(String::new);
out.push_str(&pattern[copied..j]);
out.push_str(body);
copied = end;
}
j = end;
continue;
}
}
j += 1;
}
// Unterminated bracket: leave it for the regex engine to report, but still
// flush whatever rewrite was already written into `out`.
if let Some(out) = out.as_mut() {
out.push_str(&pattern[copied..]);
}
(out, pattern.len())
}

/// True when a `[=...=]` equivalence class whose body is `body` can be replaced
/// by that single character. GNU grep rejects equivalence classes used as range
/// endpoints, so a class directly preceded or followed by `-` (`prev`/`next`)
/// is left in place rather than silently producing a range.
fn is_rewritable_equivalence(body: &str, next: Option<u8>, prev: Option<u8>) -> bool {
let in_range = next == Some(b'-') || prev == Some(b'-');
body.chars().count() == 1 && !body.starts_with([']', '^', '-', '\\']) && !in_range
}

/// Index just past the `:]`, `.]` or `=]` closing a `[: [. [=` subexpression
/// whose body starts at `start`.
fn find_bracket_subexpr_end(pattern: &[u8], start: usize, delimiter: u8) -> Option<usize> {
Expand All @@ -618,8 +756,48 @@ fn find_bracket_subexpr_end(pattern: &[u8], start: usize, delimiter: u8) -> Opti

#[cfg(test)]
mod tests {
use super::{has_confusing_bracket, plain_literal};
use super::{has_confusing_bracket, plain_literal, rewrite_equivalence_classes};
use crate::RegexMode;
use std::borrow::Cow;

fn r(p: &str) -> Cow<'_, str> {
rewrite_equivalence_classes(p)
}

#[test]
fn equivalence_classes_reduce_to_their_member() {
// Rewrites allocate an owned string.
assert_eq!(&*r("[[=a=]]"), "[a]");
assert_eq!(&*r("[[=a=]b]"), "[ab]");
assert_eq!(&*r("[b[=a=]]"), "[ba]");
assert_eq!(&*r("[[=a=][=b=]]"), "[ab]");
assert_eq!(&*r("[^[=a=]]"), "[^a]");
assert_eq!(&*r("x[[=a=]]y"), "x[a]y");
assert_eq!(&*r("[[:alpha:][=a=]]"), "[[:alpha:]a]");
// The common case borrows the input unchanged: no allocation.
assert!(matches!(r("abc"), Cow::Borrowed("abc")));
}

#[test]
fn equivalence_class_rewrite_leaves_other_patterns_alone() {
// Nothing to do.
assert!(matches!(r("abc"), Cow::Borrowed(_)));
assert!(matches!(r("[abc]"), Cow::Borrowed(_)));
assert!(matches!(r("[[:alpha:]]"), Cow::Borrowed(_)));
assert!(matches!(r("[[.a.]]"), Cow::Borrowed(_)));
// Not a bracket expression: `[=a=]` outside `[...]` is literal in GNU.
assert!(matches!(r("\\[[=a=]"), Cow::Borrowed(_)));
// A range endpoint is an error in GNU; don't invent a valid range.
assert!(matches!(r("[[=a=]-c]"), Cow::Borrowed(_)));
assert!(matches!(r("[a-[=c=]]"), Cow::Borrowed(_)));
// Only single-character classes have an obvious C-locale member.
assert!(matches!(r("[[=ab=]]"), Cow::Borrowed(_)));
assert!(matches!(r("[[==]]"), Cow::Borrowed(_)));
// Members whose meaning depends on position inside the bracket.
assert!(matches!(r("[[=]=]]"), Cow::Borrowed(_)));
assert!(matches!(r("[[=^=]]"), Cow::Borrowed(_)));
assert!(matches!(r("[[=-=]]"), Cow::Borrowed(_)));
}

fn lit(p: &str, ic: bool, mode: RegexMode) -> Option<Vec<u8>> {
plain_literal(p, ic, mode)
Expand Down Expand Up @@ -670,6 +848,7 @@ mod tests {
"[:notaclass:]",
"[:x:]",
"ab[:blank:]",
"\\\\[:blank:]", // the backslash is escaped, so the bracket is not
] {
assert!(has_confusing_bracket(p.as_bytes()), "pattern {p:?}");
}
Expand All @@ -678,18 +857,19 @@ mod tests {
#[test]
fn accepts_bracket_expressions_that_are_not_confusing() {
for p in [
"[[:digit:]]", // the correct spelling
"[::]", // no character besides the colons
"[:digit]", // does not end with a colon
"[:digit:qrs]", // ends with an ordinary character
"[:dig-it:]", // holds a range
"[:x[:digit:]:]", // holds a character class
"[:x[.,.]:]", // holds a collating element
"[:x[=e=]:]", // holds an equivalence class
"\\[:digit:]", // the bracket is escaped
"[]:digit:]", // starts with a literal ']'
"[:digit:", // unterminated
"[a-z]+[0-9]", // no colons at all
"[[:digit:]]", // the correct spelling
"[::]", // no character besides the colons
"[:digit]", // does not end with a colon
"[:digit:qrs]", // ends with an ordinary character
"[:dig-it:]", // holds a range
"[:x[:digit:]:]", // holds a character class
"[:x[.,.]:]", // holds a collating element
"[:x[=e=]:]", // holds an equivalence class
"\\[:digit:]", // the bracket is escaped
"\\\\\\[:digit:]", // and still escaped after an escaped backslash
"[]:digit:]", // starts with a literal ']'
"[:digit:", // unterminated
"[a-z]+[0-9]", // no colons at all
] {
assert!(!has_confusing_bracket(p.as_bytes()), "pattern {p:?}");
}
Expand Down
38 changes: 38 additions & 0 deletions tests/test_grep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,44 @@ fn gnu_buffer_anchors() {
.stdout_only("cat\ntar\n");
}

#[test]
fn posix_equivalence_classes() {
// In the C locale `[[=a=]]` is equivalent to `[a]`.
let input = "a\nb\nc\n";

for args in [vec!["[[=a=]]"], vec!["-E", "[[=a=]]"], vec!["a[[=a=]]*"]] {
let (_s, mut c) = ucmd();
c.args(&args).pipe_in(input).succeeds().stdout_only("a\n");
}

// The class must not poison the rest of the bracket expression.
for args in [
vec!["[[=a=]b]"],
vec!["[b[=a=]]"],
vec!["[[=a=][=b=]]"],
vec!["-E", "[[=a=]b]"],
] {
let (_s, mut c) = ucmd();
c.args(&args)
.pipe_in(input)
.succeeds()
.stdout_only("a\nb\n");
}

let (_s, mut c) = ucmd();
c.args(&["[^[=a=]]"])
.pipe_in(input)
.succeeds()
.stdout_only("b\nc\n");

// `-F` takes the whole thing literally.
let (_s, mut c) = ucmd();
c.args(&["-F", "[[=a=]]"])
.pipe_in("[[=a=]]\na\n")
.succeeds()
.stdout_only("[[=a=]]\n");
}

#[test]
fn ere_metacharacters() {
let cases: &[(&[&str], &str, &str)] = &[
Expand Down