robinpath_modules/modules/
glob_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 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(®ex_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 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(®ex_str).map_err(|e| format!("glob regex error: {}", e))?;
22 let normalized = file_path.replace('\\', "/");
24 Ok(Value::Bool(re.is_match(&normalized)))
25 });
26
27 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 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 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 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: ®ex::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}