Skip to main content

robinpath_modules/modules/
glob_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    // glob.match pattern cwd? → array of file paths
5    rp.register_builtin("glob.match", |args, _| {
6        let pattern = args.first().map(|v| v.to_display_string()).unwrap_or_default();
7        let cwd = args.get(1).map(|v| v.to_display_string())
8            .unwrap_or_else(|| std::env::current_dir().unwrap_or_default().to_string_lossy().to_string());
9        let regex_str = glob_to_regex(&pattern);
10        let re = regex::Regex::new(&regex_str).map_err(|e| format!("glob regex error: {}", e))?;
11        let mut matches = Vec::new();
12        collect_files(&std::path::Path::new(&cwd), &re, &cwd, &mut matches);
13        Ok(Value::Array(matches.into_iter().map(Value::String).collect()))
14    });
15
16    // glob.isMatch filePath pattern → bool
17    rp.register_builtin("glob.isMatch", |args, _| {
18        let file_path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
19        let pattern = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
20        let regex_str = glob_to_regex(&pattern);
21        let re = regex::Regex::new(&regex_str).map_err(|e| format!("glob regex error: {}", e))?;
22        // Normalize path separators
23        let normalized = file_path.replace('\\', "/");
24        Ok(Value::Bool(re.is_match(&normalized)))
25    });
26
27    // glob.toRegex pattern → regex string
28    rp.register_builtin("glob.toRegex", |args, _| {
29        let pattern = args.first().map(|v| v.to_display_string()).unwrap_or_default();
30        Ok(Value::String(glob_to_regex(&pattern)))
31    });
32
33    // glob.hasMagic str → bool
34    rp.register_builtin("glob.hasMagic", |args, _| {
35        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
36        let has_magic = s.contains('*') || s.contains('?') || s.contains('[') || s.contains('{');
37        Ok(Value::Bool(has_magic))
38    });
39
40    // glob.base pattern → non-glob base directory
41    rp.register_builtin("glob.base", |args, _| {
42        let pattern = args.first().map(|v| v.to_display_string()).unwrap_or_default();
43        let mut base = String::new();
44        for part in pattern.split('/') {
45            if part.contains('*') || part.contains('?') || part.contains('[') || part.contains('{') {
46                break;
47            }
48            if !base.is_empty() { base.push('/'); }
49            base.push_str(part);
50        }
51        if base.is_empty() { base = ".".to_string(); }
52        Ok(Value::String(base))
53    });
54
55    // glob.expand pattern → expand braces
56    rp.register_builtin("glob.expand", |args, _| {
57        let pattern = args.first().map(|v| v.to_display_string()).unwrap_or_default();
58        let expanded = expand_braces(&pattern);
59        Ok(Value::Array(expanded.into_iter().map(Value::String).collect()))
60    });
61}
62
63fn glob_to_regex(pattern: &str) -> String {
64    let mut regex = String::from("^");
65    let mut chars = pattern.chars().peekable();
66    while let Some(c) = chars.next() {
67        match c {
68            '*' => {
69                if chars.peek() == Some(&'*') {
70                    chars.next();
71                    if chars.peek() == Some(&'/') {
72                        chars.next();
73                        regex.push_str("(.*/)?");
74                    } else {
75                        regex.push_str(".*");
76                    }
77                } else {
78                    regex.push_str("[^/]*");
79                }
80            }
81            '?' => regex.push_str("[^/]"),
82            '.' => regex.push_str("\\."),
83            '[' => {
84                regex.push('[');
85                while let Some(c) = chars.next() {
86                    if c == ']' { regex.push(']'); break; }
87                    regex.push(c);
88                }
89            }
90            '{' => {
91                regex.push('(');
92                while let Some(c) = chars.next() {
93                    if c == '}' { regex.push(')'); break; }
94                    if c == ',' { regex.push('|'); }
95                    else { regex.push(c); }
96                }
97            }
98            '(' | ')' | '+' | '^' | '$' | '|' | '\\' => {
99                regex.push('\\');
100                regex.push(c);
101            }
102            _ => regex.push(c),
103        }
104    }
105    regex.push('$');
106    regex
107}
108
109fn expand_braces(pattern: &str) -> Vec<String> {
110    if let Some(start) = pattern.find('{') {
111        if let Some(end) = pattern[start..].find('}') {
112            let prefix = &pattern[..start];
113            let suffix = &pattern[start + end + 1..];
114            let alternatives = &pattern[start + 1..start + end];
115            let mut results = Vec::new();
116            for alt in alternatives.split(',') {
117                let expanded = format!("{}{}{}", prefix, alt.trim(), suffix);
118                results.extend(expand_braces(&expanded));
119            }
120            return results;
121        }
122    }
123    vec![pattern.to_string()]
124}
125
126fn collect_files(dir: &std::path::Path, re: &regex::Regex, base: &str, matches: &mut Vec<String>) {
127    if let Ok(entries) = std::fs::read_dir(dir) {
128        for entry in entries.flatten() {
129            let path = entry.path();
130            let relative = path.to_string_lossy()
131                .replace('\\', "/")
132                .trim_start_matches(&base.replace('\\', "/"))
133                .trim_start_matches('/')
134                .to_string();
135            if path.is_file() && re.is_match(&relative) {
136                matches.push(relative);
137            }
138            if path.is_dir() {
139                collect_files(&path, re, base, matches);
140            }
141        }
142    }
143}