Skip to main content

tess/
grep.rs

1use regex::Regex;
2
3/// AND-combined regex predicate applied to raw line bytes. Lines that fail
4/// UTF-8 decoding never match (mirrors what the interactive `/` search does).
5#[derive(Debug)]
6pub struct GrepPredicate {
7    regexes: Vec<Regex>,
8}
9
10impl GrepPredicate {
11    /// Compile each pattern. Returns the first invalid pattern's error,
12    /// prefixed so the user can tell which `--grep` argument was bad.
13    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    /// True iff every compiled pattern matches the line. Empty predicate
25    /// vacuously matches (callers should treat that as "no grep configured"
26    /// via `is_empty` and not even ask).
27    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        // Lone 0xFF is invalid UTF-8.
73        assert!(!g.matches(&[0xFF, b'a', b'b']));
74    }
75}