1use regex::Regex;
2
3#[derive(Debug)]
6pub struct GrepPredicate {
7 regexes: Vec<Regex>,
8}
9
10impl GrepPredicate {
11 pub fn compile(
17 patterns: &[String],
18 case_mode: crate::viewport::CaseMode,
19 ) -> Result<Self, String> {
20 let mut regexes = Vec::with_capacity(patterns.len());
21 for p in patterns {
22 let compiled = case_mode.apply_to_pattern(p);
23 let r = Regex::new(&compiled).map_err(|e| format!("--grep `{p}`: {e}"))?;
24 regexes.push(r);
25 }
26 Ok(Self { regexes })
27 }
28
29 pub fn is_empty(&self) -> bool { self.regexes.is_empty() }
30
31 pub fn matches(&self, line: &[u8]) -> bool {
35 let s = match std::str::from_utf8(line) {
36 Ok(s) => s,
37 Err(_) => return false,
38 };
39 self.regexes.iter().all(|r| r.is_match(s))
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46
47 #[test]
48 fn empty_predicate_is_empty() {
49 let g = GrepPredicate::compile(&[], crate::viewport::CaseMode::Sensitive).unwrap();
50 assert!(g.is_empty());
51 }
52
53 #[test]
54 fn single_pattern_matches() {
55 let g = GrepPredicate::compile(&["error".to_string()], crate::viewport::CaseMode::Sensitive).unwrap();
56 assert!(g.matches(b"something failed: error 42"));
57 assert!(!g.matches(b"all good"));
58 }
59
60 #[test]
61 fn multiple_patterns_are_anded() {
62 let g = GrepPredicate::compile(
63 &["error".to_string(), r"^\[\d{4}".to_string()],
64 crate::viewport::CaseMode::Sensitive,
65 ).unwrap();
66 assert!(g.matches(b"[2026-05-13] error occurred"));
67 assert!(!g.matches(b"[2026-05-13] all good"));
68 assert!(!g.matches(b"error occurred (no timestamp)"));
69 }
70
71 #[test]
72 fn invalid_regex_is_reported_with_arg() {
73 let err = GrepPredicate::compile(&["[unclosed".to_string()], crate::viewport::CaseMode::Sensitive).unwrap_err();
74 assert!(err.contains("--grep `[unclosed`"), "{err}");
75 }
76
77 #[test]
78 fn non_utf8_line_never_matches() {
79 let g = GrepPredicate::compile(&[".".to_string()], crate::viewport::CaseMode::Sensitive).unwrap();
80 assert!(!g.matches(&[0xFF, b'a', b'b']));
82 }
83
84 #[test]
85 fn insensitive_mode_matches_uppercase_input() {
86 let g = GrepPredicate::compile(
87 &["foo".to_string()],
88 crate::viewport::CaseMode::Insensitive,
89 )
90 .unwrap();
91 assert!(g.matches(b"FooBar"));
92 assert!(g.matches(b"FOO"));
93 assert!(g.matches(b"foo"));
94 }
95
96 #[test]
97 fn smart_mode_lowercase_is_insensitive() {
98 let g = GrepPredicate::compile(
99 &["foo".to_string()],
100 crate::viewport::CaseMode::Smart,
101 )
102 .unwrap();
103 assert!(g.matches(b"FOO bar"));
104 }
105
106 #[test]
107 fn smart_mode_uppercase_is_sensitive() {
108 let g = GrepPredicate::compile(
109 &["Foo".to_string()],
110 crate::viewport::CaseMode::Smart,
111 )
112 .unwrap();
113 assert!(g.matches(b"Foo bar"));
114 assert!(!g.matches(b"foo bar"));
115 assert!(!g.matches(b"FOO bar"));
116 }
117}