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 pcre2_octal_escapes(pat: &str) -> String {
206 let b: Vec<char> = pat.chars().collect();
207 let mut out = String::with_capacity(pat.len());
208 let mut i = 0usize;
209 while i < b.len() {
210 if b[i] != '\\' || i + 1 >= b.len() {
211 out.push(b[i]);
212 i += 1;
213 continue;
214 }
215 if b[i + 1] != '0' {
216 out.push(b[i]);
219 out.push(b[i + 1]);
220 i += 2;
221 continue;
222 }
223 let mut val: u32 = 0;
225 let mut n = 0usize;
226 while n < 3 && i + 1 + n < b.len() && ('0'..='7').contains(&b[i + 1 + n]) {
227 val = val * 8 + (b[i + 1 + n] as u32 - '0' as u32);
228 n += 1;
229 }
230 out.push_str(&format!("\\x{{{:x}}}", val));
231 i += 1 + n;
232 }
233 out
234}
235
236pub fn cond_regex_match(lhs: &str, rhs: &str, options: &RegexOptions) -> (bool, RegexMatch) {
242 match regex_match(lhs, rhs, options) {
243 Ok(result) => (result.matched, result),
244 Err(_) => (false, RegexMatch::no_match()),
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_regex_match_simple() {
254 let _g = crate::test_util::global_state_lock();
255 let opts = RegexOptions::default();
256 let result = regex_match("hello world", "hello", &opts).unwrap();
257 assert!(result.matched);
258 assert_eq!(result.full_match, Some("hello".to_string()));
259 }
260
261 #[test]
262 fn test_regex_match_no_match() {
263 let _g = crate::test_util::global_state_lock();
264 let opts = RegexOptions::default();
265 let result = regex_match("hello world", "goodbye", &opts).unwrap();
266 assert!(!result.matched);
267 }
268
269 #[test]
270 fn test_regex_match_captures() {
271 let _g = crate::test_util::global_state_lock();
272 let opts = RegexOptions::default();
273 let result = regex_match("hello world", "(hello) (world)", &opts).unwrap();
274 assert!(result.matched);
275 assert_eq!(result.full_match, Some("hello world".to_string()));
276 assert_eq!(result.captures.len(), 2);
277 assert_eq!(result.captures[0], Some("hello".to_string()));
278 assert_eq!(result.captures[1], Some("world".to_string()));
279 }
280
281 #[test]
282 fn test_regex_match_case_insensitive() {
283 let _g = crate::test_util::global_state_lock();
284 let opts = RegexOptions {
285 case_insensitive: true,
286 ..Default::default()
287 };
288 let result = regex_match("HELLO WORLD", "hello", &opts).unwrap();
289 assert!(result.matched);
290 }
291
292 #[test]
293 fn test_regex_match_case_sensitive() {
294 let _g = crate::test_util::global_state_lock();
295 let opts = RegexOptions::default();
296 let result = regex_match("HELLO WORLD", "hello", &opts).unwrap();
297 assert!(!result.matched);
298 }
299
300 #[test]
301 fn test_regex_match_positions() {
302 let _g = crate::test_util::global_state_lock();
303 let opts = RegexOptions::default();
304 let result = regex_match("foo bar baz", "bar", &opts).unwrap();
305 assert!(result.matched);
306 assert_eq!(result.match_start, Some(4));
307 assert_eq!(result.match_end, Some(7));
308 }
309
310 #[test]
311 fn test_regex_match_invalid_pattern() {
312 let _g = crate::test_util::global_state_lock();
313 let opts = RegexOptions::default();
314 let result = regex_match("test", "[invalid", &opts);
315 assert!(result.is_err());
316 }
317
318 #[test]
319 fn test_get_match_variables_zsh() {
320 let _g = crate::test_util::global_state_lock();
321 let opts = RegexOptions::default();
322 let result = regex_match("hello world", "(hello) (world)", &opts).unwrap();
323 let vars = get_match_variables(&result, "hello world", &opts);
324
325 assert_eq!(vars.get("MATCH"), Some(&"hello world".to_string()));
326 assert_eq!(vars.get("MBEGIN"), Some(&"1".to_string()));
327 assert_eq!(vars.get("MEND"), Some(&"11".to_string()));
328 }
329
330 #[test]
331 fn test_get_match_variables_bash() {
332 let _g = crate::test_util::global_state_lock();
333 let opts = RegexOptions {
334 bash_rematch: true,
335 ..Default::default()
336 };
337 let result = regex_match("hello world", "(hello) (world)", &opts).unwrap();
338 let vars = get_match_variables(&result, "hello world", &opts);
339
340 assert_eq!(
341 vars.get("BASH_REMATCH[0]"),
342 Some(&"hello world".to_string())
343 );
344 assert_eq!(vars.get("BASH_REMATCH[1]"), Some(&"hello".to_string()));
345 assert_eq!(vars.get("BASH_REMATCH[2]"), Some(&"world".to_string()));
346 }
347
348 #[test]
349 fn test_cond_regex_match() {
350 let _g = crate::test_util::global_state_lock();
351 let opts = RegexOptions::default();
352 let (matched, _) = cond_regex_match("hello world", "hello", &opts);
353 assert!(matched);
354
355 let (matched, _) = cond_regex_match("hello world", "goodbye", &opts);
356 assert!(!matched);
357 }
358
359 #[test]
360 fn test_byte_to_char_offset_ascii() {
361 let _g = crate::test_util::global_state_lock();
362 assert_eq!(byte_to_char_offset("hello", 0), 0);
363 assert_eq!(byte_to_char_offset("hello", 5), 5);
364 }
365
366 #[test]
367 fn test_byte_to_char_offset_unicode() {
368 let _g = crate::test_util::global_state_lock();
369 let s = "héllo";
370 assert_eq!(byte_to_char_offset(s, 0), 0);
371 assert_eq!(byte_to_char_offset(s, 1), 1);
372 assert_eq!(byte_to_char_offset(s, 3), 2);
373 }
374}