Skip to content

Commit 76fc0f9

Browse files
committed
diff: add -p and -F to show function lines in hunk headers
Should make test function-line-vs-leading-space pass.
1 parent 3576202 commit 76fc0f9

8 files changed

Lines changed: 645 additions & 9 deletions

File tree

‎src/bre.rs‎

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
// This file is part of the uutils diffutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE-*
4+
// files that was distributed with this source code.
5+
6+
//! Translation of POSIX basic regular expressions (the `grep` syntax that
7+
//! options such as `-F` take) into the syntax of the `regex` crate.
8+
//!
9+
//! In a basic regular expression, `\(`, `\)`, `\{`, `\}`, `\|`, `\+` and `\?`
10+
//! are operators while their unescaped forms are ordinary characters, `*` is
11+
//! literal where it has nothing to repeat, `^` only anchors at the start of
12+
//! the expression or of a group or alternative, and `$` only anchors at their
13+
//! end.
14+
15+
use regex::bytes::Regex;
16+
17+
/// A compiled basic regular expression. Two of them are equal when their
18+
/// patterns are.
19+
#[derive(Clone, Debug)]
20+
pub struct Bre {
21+
pattern: String,
22+
regex: Regex,
23+
}
24+
25+
impl Bre {
26+
/// Compiles `pattern`, or returns an error message describing what is
27+
/// wrong with it.
28+
pub fn new(pattern: &str) -> Result<Self, String> {
29+
let regex = Regex::new(&translate(pattern)?).map_err(|error| match error {
30+
regex::Error::CompiledTooBig(_) => TOO_BIG.to_string(),
31+
_ => "Invalid regular expression".to_string(),
32+
})?;
33+
Ok(Self {
34+
pattern: pattern.to_string(),
35+
regex,
36+
})
37+
}
38+
39+
pub fn is_match(&self, text: &[u8]) -> bool {
40+
self.regex.is_match(text)
41+
}
42+
}
43+
44+
impl PartialEq for Bre {
45+
fn eq(&self, other: &Self) -> bool {
46+
self.pattern == other.pattern
47+
}
48+
}
49+
50+
impl Eq for Bre {}
51+
52+
const TOO_BIG: &str = "Regular expression too big";
53+
/// The largest count of an interval, as in `a\{2,100\}`.
54+
const MAX_REPETITIONS: u32 = 0x7fff;
55+
56+
/// Translates a basic regular expression into `regex` crate syntax.
57+
fn translate(pattern: &str) -> Result<String, String> {
58+
let chars: Vec<char> = pattern.chars().collect();
59+
let mut translated = String::with_capacity(pattern.len() * 2);
60+
// Whether the previous token opens an expression, a group or an
61+
// alternative, where `^` anchors and repetition operators are literal.
62+
let mut at_expression_start = true;
63+
let mut open_groups = 0_usize;
64+
let mut index = 0;
65+
while index < chars.len() {
66+
let c = chars[index];
67+
index += 1;
68+
let mut opens_expression = false;
69+
match c {
70+
'\\' => {
71+
let Some(&escaped) = chars.get(index) else {
72+
return Err("Trailing backslash".to_string());
73+
};
74+
index += 1;
75+
match escaped {
76+
'(' => {
77+
open_groups += 1;
78+
translated.push('(');
79+
opens_expression = true;
80+
}
81+
')' => {
82+
open_groups = open_groups
83+
.checked_sub(1)
84+
.ok_or_else(|| r"Unmatched ) or \)".to_string())?;
85+
translated.push(')');
86+
}
87+
'|' => {
88+
translated.push('|');
89+
opens_expression = true;
90+
}
91+
'{' | '}' | '+' | '?' if at_expression_start => {
92+
push_literal(&mut translated, escaped);
93+
}
94+
'{' => index = translate_interval(&chars, index, &mut translated)?,
95+
'+' | '?' => translated.push(escaped),
96+
'<' | '>' | 'b' | 'B' | 'w' | 'W' | 's' | 'S' => {
97+
translated.push('\\');
98+
translated.push(escaped);
99+
}
100+
'`' => translated.push_str(r"\A"),
101+
'\'' => translated.push_str(r"\z"),
102+
'1'..='9' => return Err("Invalid back reference".to_string()),
103+
_ => push_literal(&mut translated, escaped),
104+
}
105+
}
106+
'^' if at_expression_start => {
107+
translated.push('^');
108+
opens_expression = true;
109+
}
110+
'$' if ends_expression(&chars[index..]) => translated.push('$'),
111+
'*' if at_expression_start => translated.push_str(r"\*"),
112+
'.' | '*' => translated.push(c),
113+
'[' => index = translate_bracket(&chars, index, &mut translated)?,
114+
_ => push_literal(&mut translated, c),
115+
}
116+
at_expression_start = opens_expression;
117+
}
118+
if open_groups > 0 {
119+
return Err(r"Unmatched ( or \(".to_string());
120+
}
121+
Ok(translated)
122+
}
123+
124+
/// Returns whether a `$` followed by `rest` ends an expression, a group or an
125+
/// alternative.
126+
fn ends_expression(rest: &[char]) -> bool {
127+
matches!(rest, [] | ['\\', ')' | '|', ..])
128+
}
129+
130+
/// Translates the interval whose content starts at `index` (just after the
131+
/// `\{`), and returns the index following its closing `\}`.
132+
fn translate_interval(
133+
chars: &[char],
134+
index: usize,
135+
translated: &mut String,
136+
) -> Result<usize, String> {
137+
let end = (index..chars.len().saturating_sub(1))
138+
.find(|&end| chars[end] == '\\' && chars[end + 1] == '}')
139+
.ok_or_else(|| r"Unmatched \{".to_string())?;
140+
let content: String = chars[index..end].iter().collect();
141+
let invalid = || r"Invalid content of \{\}".to_string();
142+
let count = |digits: &str| -> Result<Option<u32>, String> {
143+
if digits.is_empty() {
144+
return Ok(None);
145+
}
146+
if !digits.bytes().all(|b| b.is_ascii_digit()) {
147+
return Err(invalid());
148+
}
149+
match digits.parse::<u32>() {
150+
Ok(count) if count <= MAX_REPETITIONS => Ok(Some(count)),
151+
_ => Err(TOO_BIG.to_string()),
152+
}
153+
};
154+
match content.split_once(',') {
155+
None => {
156+
let exact = count(&content)?.ok_or_else(invalid)?;
157+
translated.push_str(&format!("{{{exact}}}"));
158+
}
159+
Some((min, max)) => {
160+
let min = count(min)?.unwrap_or(0);
161+
match count(max)? {
162+
Some(max) if max < min => return Err(invalid()),
163+
Some(max) => translated.push_str(&format!("{{{min},{max}}}")),
164+
None => translated.push_str(&format!("{{{min},}}")),
165+
}
166+
}
167+
}
168+
Ok(end + 2)
169+
}
170+
171+
/// Characters with a meaning in `regex` crate syntax, inside or outside of
172+
/// bracket expressions.
173+
const META_CHARACTERS: &str = r"\.+*?()|[]{}^$#&-~";
174+
175+
/// The names of the character classes of bracket expressions, as in
176+
/// `[[:alpha:]]`.
177+
const CLASS_NAMES: [&str; 12] = [
178+
"alnum", "alpha", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space",
179+
"upper", "xdigit",
180+
];
181+
182+
fn push_literal(translated: &mut String, c: char) {
183+
if META_CHARACTERS.contains(c) {
184+
translated.push('\\');
185+
}
186+
translated.push(c);
187+
}
188+
189+
/// Translates the bracket expression whose content starts at `index` (just
190+
/// after the `[`), and returns the index following its closing `]`.
191+
fn translate_bracket(
192+
chars: &[char],
193+
mut index: usize,
194+
translated: &mut String,
195+
) -> Result<usize, String> {
196+
const UNMATCHED: &str = "Unmatched [, [^, [:, [., or [=";
197+
translated.push('[');
198+
if chars.get(index) == Some(&'^') {
199+
translated.push('^');
200+
index += 1;
201+
}
202+
let content_start = index;
203+
// The last single character member, which can start a range.
204+
let mut range_start = None;
205+
loop {
206+
let Some(&c) = chars.get(index) else {
207+
return Err(UNMATCHED.to_string());
208+
};
209+
// A `]` first in the list is an ordinary member.
210+
if c == ']' && index > content_start {
211+
translated.push(']');
212+
return Ok(index + 1);
213+
}
214+
if c == '[' && matches!(chars.get(index + 1), Some(':' | '.' | '=')) {
215+
let delimiter = chars[index + 1];
216+
let end = (index + 2..chars.len().saturating_sub(1))
217+
.find(|&end| chars[end] == delimiter && chars[end + 1] == ']')
218+
.ok_or_else(|| UNMATCHED.to_string())?;
219+
let name: String = chars[index + 2..end].iter().collect();
220+
if delimiter == ':' {
221+
// A character class such as `[:alpha:]`, kept verbatim.
222+
if !CLASS_NAMES.contains(&name.as_str()) {
223+
return Err("Invalid character class name".to_string());
224+
}
225+
translated.extend(&chars[index..end + 2]);
226+
range_start = None;
227+
} else {
228+
// A collating element or equivalence class such as `[.a.]`
229+
// or `[=a=]`, which only stands for a single character here.
230+
let mut name_chars = name.chars();
231+
let (Some(member), None) = (name_chars.next(), name_chars.next()) else {
232+
return Err("Invalid collation character".to_string());
233+
};
234+
push_literal(translated, member);
235+
range_start = Some(member);
236+
}
237+
index = end + 2;
238+
continue;
239+
}
240+
let is_range = c == '-'
241+
&& index > content_start
242+
&& chars.get(index + 1).is_some_and(|&next| next != ']');
243+
if is_range {
244+
if let (Some(start), Some(&end)) = (range_start, chars.get(index + 1)) {
245+
if end < start {
246+
return Err("Invalid range end".to_string());
247+
}
248+
}
249+
translated.push('-');
250+
range_start = None;
251+
} else {
252+
// Backslashes are ordinary members of a bracket expression, and
253+
// the `regex` crate gives meaning to `[`, `&&`, `--` and `~~`.
254+
push_literal(translated, c);
255+
range_start = Some(c);
256+
}
257+
index += 1;
258+
}
259+
}
260+
261+
#[cfg(test)]
262+
mod tests {
263+
use super::*;
264+
use pretty_assertions::assert_eq;
265+
266+
fn matches(pattern: &str, text: &str) -> bool {
267+
Bre::new(pattern).unwrap().is_match(text.as_bytes())
268+
}
269+
270+
#[test]
271+
fn groups_and_alternatives() {
272+
assert!(matches(r"^ *\(sub\|fn\) ", " fn main"));
273+
assert!(matches(r"^ *\(sub\|fn\) ", "sub x"));
274+
assert!(!matches(r"^ *\(sub\|fn\) ", " let x"));
275+
// Unescaped operators of extended expressions are ordinary characters.
276+
assert!(matches(r"f(x)|y+?", "f(x)|y+?"));
277+
assert!(!matches(r"f(x)|y+?", "fx"));
278+
}
279+
280+
#[test]
281+
fn intervals_and_repetitions() {
282+
assert!(matches(r"^ab\{2,3\}c$", "abbc"));
283+
assert!(!matches(r"^ab\{2,3\}c$", "abc"));
284+
assert!(matches(r"^x\+$", "xxx"));
285+
assert!(matches(r"^y\?z$", "z"));
286+
// `*` with nothing to repeat is literal.
287+
assert!(matches(r"*q", "a*q"));
288+
assert!(matches(r"^*q", "*q"));
289+
assert!(matches(r"\(*\)", "*"));
290+
assert!(matches(r"to*k", "tk"));
291+
}
292+
293+
#[test]
294+
fn anchors_only_at_the_ends() {
295+
assert!(matches(r"a^b", "a^b"));
296+
assert!(matches(r"a$b", "a$b"));
297+
assert!(matches(r"end$", "the end"));
298+
assert!(!matches(r"end$", "endless"));
299+
assert!(matches(r"\(x$\|^y\)", "yes"));
300+
}
301+
302+
#[test]
303+
fn bracket_expressions() {
304+
assert!(matches(r"^[[:alpha:]$_]", "$var"));
305+
assert!(matches(r"^[[:alpha:]$_]", "_x"));
306+
assert!(!matches(r"^[[:alpha:]$_]", " x"));
307+
assert!(matches(r"[]x]", "]"));
308+
assert!(matches(r"[^]x]", "y"));
309+
assert!(matches(r"[a\]", "\\"));
310+
assert!(matches(r"[a-c]", "b"));
311+
assert!(matches(r"[a-]", "-"));
312+
assert!(matches(r"[&&~~[]", "["));
313+
}
314+
315+
#[test]
316+
fn errors() {
317+
for (pattern, message) in [
318+
("ab\\", "Trailing backslash"),
319+
("[ab", "Unmatched [, [^, [:, [., or [="),
320+
("[[:alpha:]", "Unmatched [, [^, [:, [., or [="),
321+
(r"\(a\)\1", "Invalid back reference"),
322+
(r"x\(y", r"Unmatched ( or \("),
323+
(r"x\)y", r"Unmatched ) or \)"),
324+
(r"k\{4", r"Unmatched \{"),
325+
(r"k\{\}", r"Invalid content of \{\}"),
326+
(r"k\{2,z\}", r"Invalid content of \{\}"),
327+
(r"k\{5,2\}", r"Invalid content of \{\}"),
328+
(r"k\{40000\}", "Regular expression too big"),
329+
("[[:vowel:]]", "Invalid character class name"),
330+
("[[.ab.]]", "Invalid collation character"),
331+
("[m-c]", "Invalid range end"),
332+
] {
333+
assert_eq!(
334+
Bre::new(pattern).err().as_deref(),
335+
Some(message),
336+
"{pattern}"
337+
);
338+
}
339+
}
340+
341+
#[test]
342+
fn literal_operators_with_nothing_to_repeat() {
343+
assert!(matches(r"\{3\}", "{3}"));
344+
assert!(matches(r"\(\+x\)", "+x"));
345+
assert!(matches(r"q\{,2\}r", "r"));
346+
assert!(matches(r"^q\{2,\}$", "qqqq"));
347+
assert!(matches(r"[[.-.][=k=]]", "-"));
348+
assert!(matches(r"[[.-.][=k=]]", "k"));
349+
}
350+
}

0 commit comments

Comments
 (0)