Skip to content

Commit 21cf159

Browse files
committed
grep: warn on leading ERE repeat operators
1 parent f4798cb commit 21cf159

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

src/matcher.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use onig::{
1010
};
1111
use onig_sys::{OnigEncCtype_ONIGENC_CTYPE_WORD, OnigEncodingUTF8};
1212
use uucore::error::{UResult, USimpleError};
13+
use uucore::show_warning;
1314

1415
pub struct Matcher<'a> {
1516
config: &'a Config<'a>,
@@ -215,6 +216,17 @@ impl CompiledPattern {
215216
// GNU grep supports `{,n}` as an alias for `{0,n}`.
216217
syntax.enable_behavior(SyntaxBehavior::SYNTAX_BEHAVIOR_ALLOW_INTERVAL_LOW_ABBREV);
217218
}
219+
let mut normalized_pattern = None;
220+
let pattern = if config.regex_mode == RegexMode::Extended {
221+
if let Some((op, rest)) = strip_leading_repeat_operator(pattern) {
222+
show_warning!("{op} at start of expression");
223+
normalized_pattern = Some(rest.to_string());
224+
}
225+
normalized_pattern.as_deref().unwrap_or(pattern)
226+
} else {
227+
pattern
228+
};
229+
218230
if config.regex_mode == RegexMode::Perl {
219231
// GNU grep supports `(?P<name>...)`.
220232
// Unfortunately, the onig crate defines the OP2 flag without the
@@ -289,3 +301,22 @@ impl CompiledPattern {
289301
.is_some()
290302
}
291303
}
304+
305+
fn strip_leading_repeat_operator(pattern: &str) -> Option<(&'static str, &str)> {
306+
match pattern.as_bytes().first()? {
307+
b'?' => Some(("?", &pattern[1..])),
308+
b'*' => Some(("*", &pattern[1..])),
309+
b'+' => Some(("+", &pattern[1..])),
310+
b'{' => strip_leading_interval_repeat(pattern).map(|rest| ("{...}", rest)),
311+
_ => None,
312+
}
313+
}
314+
315+
fn strip_leading_interval_repeat(pattern: &str) -> Option<&str> {
316+
let close = pattern.as_bytes().iter().position(|&b| b == b'}')?;
317+
let body = &pattern[1..close];
318+
let is_interval = !body.is_empty()
319+
&& body.bytes().all(|b| b.is_ascii_digit() || b == b',')
320+
&& body.bytes().any(|b| b.is_ascii_digit());
321+
is_interval.then_some(&pattern[close + 1..])
322+
}

tests/test_grep.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,32 @@ fn ere_invalid_pattern_is_error() {
126126
.stderr_contains("invalid pattern");
127127
}
128128

129+
#[test]
130+
fn ere_leading_repeat_operators_warn_and_match_empty() {
131+
let cases = [
132+
("?", "warning: ? at start of expression"),
133+
("*", "warning: * at start of expression"),
134+
("+", "warning: + at start of expression"),
135+
("{2}", "warning: {...} at start of expression"),
136+
("{,2}", "warning: {...} at start of expression"),
137+
];
138+
139+
for (pattern, warning) in cases {
140+
let (_s, mut c) = ucmd();
141+
c.args(&["-E", "-e", pattern])
142+
.pipe_in("abc\n")
143+
.succeeds()
144+
.stdout_is("abc\n")
145+
.stderr_contains(warning);
146+
}
147+
148+
let (_s, mut c) = ucmd();
149+
c.args(&["*foo"])
150+
.pipe_in("*foo\nfoo\n")
151+
.succeeds()
152+
.stdout_only("*foo\n");
153+
}
154+
129155
#[test]
130156
fn fixed_string_is_literal() {
131157
// Metacharacters are not interpreted.

0 commit comments

Comments
 (0)