Skip to content

Commit 0f16075

Browse files
committed
grep: warn on leading ERE repeat operators
1 parent e925ff4 commit 0f16075

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

src/matcher.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use onig::{
1111
};
1212
use onig_sys::{OnigEncCtype_ONIGENC_CTYPE_WORD, OnigEncodingUTF8};
1313
use uucore::error::{UResult, USimpleError};
14+
use uucore::show_warning;
1415

1516
pub struct Matcher<'a> {
1617
config: &'a Config<'a>,
@@ -273,6 +274,18 @@ impl CompiledPattern {
273274
// GNU grep supports \` and \' as buffer anchors in BRE and ERE.
274275
syntax.enable_operators(SyntaxOperator::SYNTAX_OPERATOR_ESC_GNU_BUF_ANCHOR);
275276
}
277+
278+
let mut normalized_pattern = None;
279+
let pattern = if config.regex_mode == RegexMode::Extended {
280+
if let Some((op, rest)) = strip_leading_repeat_operator(pattern) {
281+
show_warning!("{op} at start of expression");
282+
normalized_pattern = Some(rest.to_string());
283+
}
284+
normalized_pattern.as_deref().unwrap_or(pattern)
285+
} else {
286+
pattern
287+
};
288+
276289
if config.regex_mode == RegexMode::Perl {
277290
// GNU grep supports `(?P<name>...)`.
278291
// Unfortunately, the onig crate defines the OP2 flag without the
@@ -354,6 +367,25 @@ impl CompiledPattern {
354367
}
355368
}
356369

370+
fn strip_leading_repeat_operator(pattern: &str) -> Option<(&'static str, &str)> {
371+
match pattern.as_bytes().first()? {
372+
b'?' => Some(("?", &pattern[1..])),
373+
b'*' => Some(("*", &pattern[1..])),
374+
b'+' => Some(("+", &pattern[1..])),
375+
b'{' => strip_leading_interval_repeat(pattern).map(|rest| ("{...}", rest)),
376+
_ => None,
377+
}
378+
}
379+
380+
fn strip_leading_interval_repeat(pattern: &str) -> Option<&str> {
381+
let close = pattern.as_bytes().iter().position(|&b| b == b'}')?;
382+
let body = &pattern[1..close];
383+
let is_interval = !body.is_empty()
384+
&& body.bytes().all(|b| b.is_ascii_digit() || b == b',')
385+
&& body.bytes().any(|b| b.is_ascii_digit());
386+
is_interval.then_some(&pattern[close + 1..])
387+
}
388+
357389
#[cfg(test)]
358390
mod tests {
359391
use super::plain_literal;

tests/test_grep.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,32 @@ fn initial_tab_skips_empty_lines() {
159159
.stdout_is("in:\tx\nin:\t \n");
160160
}
161161

162+
#[test]
163+
fn ere_leading_repeat_operators_warn_and_match_empty() {
164+
let cases = [
165+
("?", "warning: ? at start of expression"),
166+
("*", "warning: * at start of expression"),
167+
("+", "warning: + at start of expression"),
168+
("{2}", "warning: {...} at start of expression"),
169+
("{,2}", "warning: {...} at start of expression"),
170+
];
171+
172+
for (pattern, warning) in cases {
173+
let (_s, mut c) = ucmd();
174+
c.args(&["-E", "-e", pattern])
175+
.pipe_in("abc\n")
176+
.succeeds()
177+
.stdout_is("abc\n")
178+
.stderr_contains(warning);
179+
}
180+
181+
let (_s, mut c) = ucmd();
182+
c.args(&["*foo"])
183+
.pipe_in("*foo\nfoo\n")
184+
.succeeds()
185+
.stdout_only("*foo\n");
186+
}
187+
162188
#[test]
163189
fn fixed_string_is_literal() {
164190
// Metacharacters are not interpreted.

0 commit comments

Comments
 (0)