1use regex::Regex;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct RegexMatch {
11 pub matched: bool,
13 pub full_match: Option<String>,
15 pub captures: Vec<Option<String>>,
17 pub match_start: Option<usize>,
19 pub match_end: Option<usize>,
21 pub capture_starts: Vec<Option<usize>>,
23 pub capture_ends: Vec<Option<usize>>,
25}
26
27impl RegexMatch {
28 pub fn no_match() -> Self {
30 Self {
31 matched: false,
32 full_match: None,
33 captures: Vec::new(),
34 match_start: None,
35 match_end: None,
36 capture_starts: Vec::new(),
37 capture_ends: Vec::new(),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct RegexOptions {
45 pub case_insensitive: bool,
47 pub bash_rematch: bool,
49 pub ksh_arrays: bool,
51}
52
53pub fn regex_match(
59 text: &str,
60 pattern: &str,
61 options: &RegexOptions,
62) -> Result<RegexMatch, String> {
63 let re = if options.case_insensitive {
64 Regex::new(&format!("(?i){}", pattern))
65 } else {
66 Regex::new(pattern)
67 }
68 .map_err(|e| format!("failed to compile regex: {}", e))?;
69
70 let caps = match re.captures(text) {
71 Some(c) => c,
72 None => return Ok(RegexMatch::no_match()),
73 };
74
75 let full_match = caps.get(0).map(|m| m.as_str().to_string());
76 let match_start = caps.get(0).map(|m| m.start());
77 let match_end = caps.get(0).map(|m| m.end());
78
79 let mut captures = Vec::new();
80 let mut capture_starts = Vec::new();
81 let mut capture_ends = Vec::new();
82
83 for i in 1..caps.len() {
84 if let Some(m) = caps.get(i) {
85 captures.push(Some(m.as_str().to_string()));
86 capture_starts.push(Some(m.start()));
87 capture_ends.push(Some(m.end()));
88 } else {
89 captures.push(None);
90 capture_starts.push(None);
91 capture_ends.push(None);
92 }
93 }
94
95 Ok(RegexMatch {
96 matched: true,
97 full_match,
98 captures,
99 match_start,
100 match_end,
101 capture_starts,
102 capture_ends,
103 })
104}
105
106fn byte_to_char_offset(s: &str, byte_offset: usize) -> usize {
108 s[..byte_offset].chars().count()
109}
110
111pub fn get_match_variables(
119 result: &RegexMatch,
120 text: &str,
121 options: &RegexOptions,
122) -> HashMap<String, String> {
123 let mut vars = HashMap::new();
124
125 if !result.matched {
126 return vars;
127 }
128
129 if options.bash_rematch {
130 if let Some(ref full) = result.full_match {
131 vars.insert("BASH_REMATCH[0]".to_string(), full.clone());
132 }
133 for (i, cap) in result.captures.iter().enumerate() {
134 if let Some(c) = cap {
135 vars.insert(format!("BASH_REMATCH[{}]", i + 1), c.clone());
136 }
137 }
138 } else {
139 if let Some(ref full) = result.full_match {
140 vars.insert("MATCH".to_string(), full.clone());
141 }
142
143 let base = if options.ksh_arrays { 0 } else { 1 };
144
145 if let Some(start) = result.match_start {
146 let char_start = byte_to_char_offset(text, start);
147 vars.insert("MBEGIN".to_string(), (char_start + base).to_string());
148 }
149
150 if let Some(end) = result.match_end {
151 let char_end = byte_to_char_offset(text, end);
152 vars.insert("MEND".to_string(), (char_end + base - 1).to_string());
153 }
154
155 for (i, cap) in result.captures.iter().enumerate() {
156 if let Some(c) = cap {
157 vars.insert(format!("match[{}]", i + base), c.clone());
158 }
159 }
160
161 for (i, start) in result.capture_starts.iter().enumerate() {
162 if let Some(s) = start {
163 let char_start = byte_to_char_offset(text, *s);
164 vars.insert(
165 format!("mbegin[{}]", i + base),
166 (char_start + base).to_string(),
167 );
168 } else {
169 vars.insert(format!("mbegin[{}]", i + base), "-1".to_string());
170 }
171 }
172
173 for (i, end) in result.capture_ends.iter().enumerate() {
174 if let Some(e) = end {
175 let char_end = byte_to_char_offset(text, *e);
176 vars.insert(
177 format!("mend[{}]", i + base),
178 (char_end + base - 1).to_string(),
179 );
180 } else {
181 vars.insert(format!("mend[{}]", i + base), "-1".to_string());
182 }
183 }
184 }
185
186 vars
187}
188
189pub fn cond_regex_match(lhs: &str, rhs: &str, options: &RegexOptions) -> (bool, RegexMatch) {
195 match regex_match(lhs, rhs, options) {
196 Ok(result) => (result.matched, result),
197 Err(_) => (false, RegexMatch::no_match()),
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_regex_match_simple() {
207 let _g = crate::test_util::global_state_lock();
208 let opts = RegexOptions::default();
209 let result = regex_match("hello world", "hello", &opts).unwrap();
210 assert!(result.matched);
211 assert_eq!(result.full_match, Some("hello".to_string()));
212 }
213
214 #[test]
215 fn test_regex_match_no_match() {
216 let _g = crate::test_util::global_state_lock();
217 let opts = RegexOptions::default();
218 let result = regex_match("hello world", "goodbye", &opts).unwrap();
219 assert!(!result.matched);
220 }
221
222 #[test]
223 fn test_regex_match_captures() {
224 let _g = crate::test_util::global_state_lock();
225 let opts = RegexOptions::default();
226 let result = regex_match("hello world", "(hello) (world)", &opts).unwrap();
227 assert!(result.matched);
228 assert_eq!(result.full_match, Some("hello world".to_string()));
229 assert_eq!(result.captures.len(), 2);
230 assert_eq!(result.captures[0], Some("hello".to_string()));
231 assert_eq!(result.captures[1], Some("world".to_string()));
232 }
233
234 #[test]
235 fn test_regex_match_case_insensitive() {
236 let _g = crate::test_util::global_state_lock();
237 let opts = RegexOptions {
238 case_insensitive: true,
239 ..Default::default()
240 };
241 let result = regex_match("HELLO WORLD", "hello", &opts).unwrap();
242 assert!(result.matched);
243 }
244
245 #[test]
246 fn test_regex_match_case_sensitive() {
247 let _g = crate::test_util::global_state_lock();
248 let opts = RegexOptions::default();
249 let result = regex_match("HELLO WORLD", "hello", &opts).unwrap();
250 assert!(!result.matched);
251 }
252
253 #[test]
254 fn test_regex_match_positions() {
255 let _g = crate::test_util::global_state_lock();
256 let opts = RegexOptions::default();
257 let result = regex_match("foo bar baz", "bar", &opts).unwrap();
258 assert!(result.matched);
259 assert_eq!(result.match_start, Some(4));
260 assert_eq!(result.match_end, Some(7));
261 }
262
263 #[test]
264 fn test_regex_match_invalid_pattern() {
265 let _g = crate::test_util::global_state_lock();
266 let opts = RegexOptions::default();
267 let result = regex_match("test", "[invalid", &opts);
268 assert!(result.is_err());
269 }
270
271 #[test]
272 fn test_get_match_variables_zsh() {
273 let _g = crate::test_util::global_state_lock();
274 let opts = RegexOptions::default();
275 let result = regex_match("hello world", "(hello) (world)", &opts).unwrap();
276 let vars = get_match_variables(&result, "hello world", &opts);
277
278 assert_eq!(vars.get("MATCH"), Some(&"hello world".to_string()));
279 assert_eq!(vars.get("MBEGIN"), Some(&"1".to_string()));
280 assert_eq!(vars.get("MEND"), Some(&"11".to_string()));
281 }
282
283 #[test]
284 fn test_get_match_variables_bash() {
285 let _g = crate::test_util::global_state_lock();
286 let opts = RegexOptions {
287 bash_rematch: true,
288 ..Default::default()
289 };
290 let result = regex_match("hello world", "(hello) (world)", &opts).unwrap();
291 let vars = get_match_variables(&result, "hello world", &opts);
292
293 assert_eq!(
294 vars.get("BASH_REMATCH[0]"),
295 Some(&"hello world".to_string())
296 );
297 assert_eq!(vars.get("BASH_REMATCH[1]"), Some(&"hello".to_string()));
298 assert_eq!(vars.get("BASH_REMATCH[2]"), Some(&"world".to_string()));
299 }
300
301 #[test]
302 fn test_cond_regex_match() {
303 let _g = crate::test_util::global_state_lock();
304 let opts = RegexOptions::default();
305 let (matched, _) = cond_regex_match("hello world", "hello", &opts);
306 assert!(matched);
307
308 let (matched, _) = cond_regex_match("hello world", "goodbye", &opts);
309 assert!(!matched);
310 }
311
312 #[test]
313 fn test_byte_to_char_offset_ascii() {
314 let _g = crate::test_util::global_state_lock();
315 assert_eq!(byte_to_char_offset("hello", 0), 0);
316 assert_eq!(byte_to_char_offset("hello", 5), 5);
317 }
318
319 #[test]
320 fn test_byte_to_char_offset_unicode() {
321 let _g = crate::test_util::global_state_lock();
322 let s = "héllo";
323 assert_eq!(byte_to_char_offset(s, 0), 0);
324 assert_eq!(byte_to_char_offset(s, 1), 1);
325 assert_eq!(byte_to_char_offset(s, 3), 2);
326 }
327}