1use regex::Regex;
2
3#[derive(Debug)]
6pub struct GrepPredicate {
7 regexes: Vec<Regex>,
8}
9
10impl GrepPredicate {
11 pub fn compile(patterns: &[String]) -> Result<Self, String> {
14 let mut regexes = Vec::with_capacity(patterns.len());
15 for p in patterns {
16 let r = Regex::new(p).map_err(|e| format!("--grep `{p}`: {e}"))?;
17 regexes.push(r);
18 }
19 Ok(Self { regexes })
20 }
21
22 pub fn is_empty(&self) -> bool { self.regexes.is_empty() }
23
24 pub fn matches(&self, line: &[u8]) -> bool {
28 let s = match std::str::from_utf8(line) {
29 Ok(s) => s,
30 Err(_) => return false,
31 };
32 self.regexes.iter().all(|r| r.is_match(s))
33 }
34}
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39
40 #[test]
41 fn empty_predicate_is_empty() {
42 let g = GrepPredicate::compile(&[]).unwrap();
43 assert!(g.is_empty());
44 }
45
46 #[test]
47 fn single_pattern_matches() {
48 let g = GrepPredicate::compile(&["error".to_string()]).unwrap();
49 assert!(g.matches(b"something failed: error 42"));
50 assert!(!g.matches(b"all good"));
51 }
52
53 #[test]
54 fn multiple_patterns_are_anded() {
55 let g = GrepPredicate::compile(
56 &["error".to_string(), r"^\[\d{4}".to_string()],
57 ).unwrap();
58 assert!(g.matches(b"[2026-05-13] error occurred"));
59 assert!(!g.matches(b"[2026-05-13] all good"));
60 assert!(!g.matches(b"error occurred (no timestamp)"));
61 }
62
63 #[test]
64 fn invalid_regex_is_reported_with_arg() {
65 let err = GrepPredicate::compile(&["[unclosed".to_string()]).unwrap_err();
66 assert!(err.contains("--grep `[unclosed`"), "{err}");
67 }
68
69 #[test]
70 fn non_utf8_line_never_matches() {
71 let g = GrepPredicate::compile(&[".".to_string()]).unwrap();
72 assert!(!g.matches(&[0xFF, b'a', b'b']));
74 }
75}