Skip to main content

jtool_grep/
matcher.rs

1//! Pattern matching logic for grep
2
3use anyhow::{Context, Result};
4use regex::Regex;
5
6/// Pattern matcher for grep searches
7pub struct Matcher {
8    regex: Regex,
9    only_matching: bool,
10    invert_match: bool,
11}
12
13impl Matcher {
14    /// Create a new matcher from a pattern string
15    pub fn new(
16        pattern: &str,
17        case_insensitive: bool,
18        word_regexp: bool,
19        fixed_strings: bool,
20        only_matching: bool,
21        invert_match: bool,
22    ) -> Result<Self> {
23        // Build the pattern
24        let mut final_pattern = if fixed_strings {
25            regex::escape(pattern)
26        } else {
27            pattern.to_string()
28        };
29
30        // Add word boundaries if needed
31        if word_regexp {
32            final_pattern = format!(r"\b{final_pattern}\b");
33        }
34
35        // Add case insensitive flag if needed
36        if case_insensitive {
37            final_pattern = format!("(?i){final_pattern}");
38        }
39
40        let regex = Regex::new(&final_pattern).context("Failed to compile regex pattern")?;
41
42        Ok(Self {
43            regex,
44            only_matching,
45            invert_match,
46        })
47    }
48
49    /// Find a match in a line of text
50    /// Returns Some((matched_text, start, end)) if found
51    pub fn find<'a>(&self, text: &'a str) -> Option<(&'a str, usize, usize)> {
52        let has_match = self.regex.is_match(text);
53
54        // Handle invert match
55        if self.invert_match {
56            if !has_match {
57                // For inverted match, return the whole line as the "match"
58                return Some((text, 0, text.len()));
59            } else {
60                return None;
61            }
62        }
63
64        // Normal matching
65        if !has_match {
66            return None;
67        }
68
69        // If only_matching, return just the matched portion
70        if self.only_matching {
71            self.regex
72                .find(text)
73                .map(|m| (m.as_str(), m.start(), m.end()))
74        } else {
75            // Return the full line but with match position
76            self.regex.find(text).map(|m| (text, m.start(), m.end()))
77        }
78    }
79
80    /// Check if the pattern matches the given text
81    pub fn is_match(&self, text: &str) -> bool {
82        if self.invert_match {
83            !self.regex.is_match(text)
84        } else {
85            self.regex.is_match(text)
86        }
87    }
88
89    /// Find all matches in a line of text
90    pub fn find_all<'a>(&self, text: &'a str) -> Vec<(&'a str, usize, usize)> {
91        self.regex
92            .find_iter(text)
93            .map(|m| (m.as_str(), m.start(), m.end()))
94            .collect()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_matcher_basic() {
104        let matcher = Matcher::new("import", false, false, false, false, false);
105        assert!(matcher.is_ok());
106        if let Ok(matcher) = matcher {
107            assert!(matcher.is_match("import pandas"));
108            assert!(!matcher.is_match("define function"));
109        }
110    }
111
112    #[test]
113    fn test_matcher_case_insensitive() {
114        let matcher = Matcher::new("IMPORT", true, false, false, false, false);
115        assert!(matcher.is_ok());
116        if let Ok(matcher) = matcher {
117            assert!(matcher.is_match("import pandas"));
118            assert!(matcher.is_match("Import numpy"));
119            assert!(matcher.is_match("IMPORT os"));
120        }
121    }
122
123    #[test]
124    fn test_matcher_find() {
125        let matcher = Matcher::new("import", false, false, false, true, false);
126        assert!(matcher.is_ok());
127        if let Ok(matcher) = matcher {
128            let result = matcher.find("import pandas as pd");
129            assert!(result.is_some());
130            if let Some((matched, start, end)) = result {
131                assert_eq!(matched, "import");
132                assert_eq!(start, 0);
133                assert_eq!(end, 6);
134            }
135        }
136    }
137
138    #[test]
139    fn test_matcher_regex() {
140        let matcher = Matcher::new(r"import \w+", false, false, false, true, false);
141        assert!(matcher.is_ok());
142        if let Ok(matcher) = matcher {
143            let result = matcher.find("import pandas as pd");
144            assert!(result.is_some());
145            if let Some((matched, _, _)) = result {
146                assert_eq!(matched, "import pandas");
147            }
148        }
149    }
150}