1use std::path::{Path, PathBuf};
24use std::sync::Arc;
25
26use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
27
28use crate::script_ctx::ScriptCtx;
29use crate::stdlib::env::str_arg;
30
31pub fn install(interp: &mut Interpreter<ScriptCtx>) {
32 interp.register_fn(
34 "glob",
35 Arity::Exact(1),
36 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
37 let pattern = str_arg(&args[0], "glob", sp)?;
38 let entries = simple_glob(&pattern).map_err(|e| EvalError::native_fn("glob", e, sp))?;
39 Ok(Value::list(
40 entries
41 .into_iter()
42 .map(|p| Value::Str(Arc::from(p.to_string_lossy().into_owned())))
43 .collect::<Vec<_>>(),
44 ))
45 },
46 );
47
48 interp.register_fn(
49 "walk-dir",
50 Arity::Exact(1),
51 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
52 let root = str_arg(&args[0], "walk-dir", sp)?;
53 let mut out = Vec::new();
54 walk_collect(Path::new(&*root), &mut out)
55 .map_err(|e| EvalError::native_fn("walk-dir", e.to_string(), sp))?;
56 Ok(Value::list(
57 out.into_iter()
58 .map(|p| Value::Str(Arc::from(p.to_string_lossy().into_owned())))
59 .collect::<Vec<_>>(),
60 ))
61 },
62 );
63
64 interp.register_fn(
65 "ls",
66 Arity::Exact(1),
67 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
68 let dir = str_arg(&args[0], "ls", sp)?;
69 let mut entries: Vec<PathBuf> = std::fs::read_dir(&*dir)
70 .map_err(|e| EvalError::native_fn("ls", format!("{dir}: {e}"), sp))?
71 .filter_map(|r| r.ok().map(|e| e.path()))
72 .collect();
73 entries.sort();
74 Ok(Value::list(
75 entries
76 .into_iter()
77 .map(|p| Value::Str(Arc::from(p.to_string_lossy().into_owned())))
78 .collect::<Vec<_>>(),
79 ))
80 },
81 );
82
83 interp.register_fn(
85 "mkdir",
86 Arity::Exact(1),
87 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
88 let path = str_arg(&args[0], "mkdir", sp)?;
89 match std::fs::create_dir(&*path) {
90 Ok(()) => Ok(Value::Nil),
91 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(Value::Nil),
92 Err(e) => Err(EvalError::native_fn("mkdir", format!("{path}: {e}"), sp)),
93 }
94 },
95 );
96
97 interp.register_fn(
98 "mkdir-p",
99 Arity::Exact(1),
100 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
101 let path = str_arg(&args[0], "mkdir-p", sp)?;
102 std::fs::create_dir_all(&*path)
103 .map_err(|e| EvalError::native_fn("mkdir-p", format!("{path}: {e}"), sp))?;
104 Ok(Value::Nil)
105 },
106 );
107
108 interp.register_fn(
109 "rm",
110 Arity::Exact(1),
111 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
112 let path = str_arg(&args[0], "rm", sp)?;
113 std::fs::remove_file(&*path)
114 .map_err(|e| EvalError::native_fn("rm", format!("{path}: {e}"), sp))?;
115 Ok(Value::Nil)
116 },
117 );
118
119 interp.register_fn(
120 "rm-rf",
121 Arity::Exact(1),
122 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
123 let path = str_arg(&args[0], "rm-rf", sp)?;
124 if Path::new(&*path).is_dir() {
125 std::fs::remove_dir_all(&*path)
126 .map_err(|e| EvalError::native_fn("rm-rf", format!("{path}: {e}"), sp))?;
127 } else if Path::new(&*path).exists() {
128 std::fs::remove_file(&*path)
129 .map_err(|e| EvalError::native_fn("rm-rf", format!("{path}: {e}"), sp))?;
130 }
131 Ok(Value::Nil)
132 },
133 );
134
135 interp.register_fn(
137 "cwd",
138 Arity::Exact(0),
139 |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
140 let d = std::env::current_dir()
141 .map_err(|e| EvalError::native_fn("cwd", e.to_string(), sp))?;
142 Ok(Value::Str(Arc::from(d.to_string_lossy().into_owned())))
143 },
144 );
145
146 interp.register_fn(
147 "chdir",
148 Arity::Exact(1),
149 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
150 let path = str_arg(&args[0], "chdir", sp)?;
151 std::env::set_current_dir(&*path)
152 .map_err(|e| EvalError::native_fn("chdir", format!("{path}: {e}"), sp))?;
153 Ok(Value::Nil)
154 },
155 );
156
157 interp.register_fn(
159 "path-join",
160 Arity::AtLeast(1),
161 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
162 let mut buf = PathBuf::new();
163 for v in args {
164 let s = str_arg(v, "path-join", sp)?;
165 buf.push(&*s);
166 }
167 Ok(Value::Str(Arc::from(buf.to_string_lossy().into_owned())))
168 },
169 );
170
171 interp.register_fn(
172 "path-basename",
173 Arity::Exact(1),
174 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
175 let p = str_arg(&args[0], "path-basename", sp)?;
176 let base = Path::new(&*p)
177 .file_name()
178 .map(|s| s.to_string_lossy().into_owned())
179 .unwrap_or_default();
180 Ok(Value::Str(Arc::from(base)))
181 },
182 );
183
184 interp.register_fn(
185 "path-dirname",
186 Arity::Exact(1),
187 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
188 let p = str_arg(&args[0], "path-dirname", sp)?;
189 let dir = Path::new(&*p)
190 .parent()
191 .map(|s| s.to_string_lossy().into_owned())
192 .unwrap_or_default();
193 Ok(Value::Str(Arc::from(dir)))
194 },
195 );
196
197 interp.register_fn(
198 "path-extension",
199 Arity::Exact(1),
200 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
201 let p = str_arg(&args[0], "path-extension", sp)?;
202 let ext = Path::new(&*p)
203 .extension()
204 .map(|s| s.to_string_lossy().into_owned())
205 .unwrap_or_default();
206 Ok(Value::Str(Arc::from(ext)))
207 },
208 );
209
210 interp.register_fn(
211 "path-absolute",
212 Arity::Exact(1),
213 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
214 let p = str_arg(&args[0], "path-absolute", sp)?;
215 let abs = std::fs::canonicalize(&*p)
216 .map_err(|e| EvalError::native_fn("path-absolute", format!("{p}: {e}"), sp))?;
217 Ok(Value::Str(Arc::from(abs.to_string_lossy().into_owned())))
218 },
219 );
220
221 interp.register_fn(
223 "file-size",
224 Arity::Exact(1),
225 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
226 let p = str_arg(&args[0], "file-size", sp)?;
227 let meta = std::fs::metadata(&*p)
228 .map_err(|e| EvalError::native_fn("file-size", format!("{p}: {e}"), sp))?;
229 Ok(Value::Int(meta.len() as i64))
230 },
231 );
232
233 interp.register_fn(
234 "is-dir?",
235 Arity::Exact(1),
236 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
237 let p = str_arg(&args[0], "is-dir?", sp)?;
238 Ok(Value::Bool(Path::new(&*p).is_dir()))
239 },
240 );
241
242 interp.register_fn(
243 "is-file?",
244 Arity::Exact(1),
245 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
246 let p = str_arg(&args[0], "is-file?", sp)?;
247 Ok(Value::Bool(Path::new(&*p).is_file()))
248 },
249 );
250
251 interp.register_fn(
253 "tmp-dir",
254 Arity::Exact(0),
255 |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
256 let base = std::env::temp_dir();
257 let now = std::time::SystemTime::now()
258 .duration_since(std::time::UNIX_EPOCH)
259 .map(|d| d.as_nanos())
260 .unwrap_or(0);
261 let dir = base.join(format!("tatara-script-{now:x}"));
262 std::fs::create_dir_all(&dir)
263 .map_err(|e| EvalError::native_fn("tmp-dir", e.to_string(), sp))?;
264 Ok(Value::Str(Arc::from(dir.to_string_lossy().into_owned())))
265 },
266 );
267
268 interp.register_fn(
269 "tmp-file",
270 Arity::Exact(0),
271 |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
272 let base = std::env::temp_dir();
273 let now = std::time::SystemTime::now()
274 .duration_since(std::time::UNIX_EPOCH)
275 .map(|d| d.as_nanos())
276 .unwrap_or(0);
277 let path = base.join(format!("tatara-script-{now:x}.tmp"));
278 std::fs::write(&path, b"")
279 .map_err(|e| EvalError::native_fn("tmp-file", e.to_string(), sp))?;
280 Ok(Value::Str(Arc::from(path.to_string_lossy().into_owned())))
281 },
282 );
283}
284
285fn walk_collect(root: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
287 let mut stack = vec![root.to_path_buf()];
288 while let Some(cur) = stack.pop() {
289 if cur.is_file() {
290 out.push(cur);
291 continue;
292 }
293 if !cur.is_dir() {
294 continue;
295 }
296 for entry in std::fs::read_dir(&cur)? {
297 let entry = entry?;
298 stack.push(entry.path());
299 }
300 }
301 Ok(())
302}
303
304fn simple_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
308 let (prefix, remainder) = split_glob_prefix(pattern);
309 let base = if prefix.is_empty() {
310 PathBuf::from(".")
311 } else {
312 PathBuf::from(&prefix)
313 };
314 let parts: Vec<&str> = remainder.split('/').filter(|s| !s.is_empty()).collect();
315 if parts.is_empty() {
316 return Ok(vec![base]);
317 }
318 let mut out = Vec::new();
319 walk_glob(&base, &parts, 0, &mut out);
320 Ok(out)
321}
322
323fn split_glob_prefix(pattern: &str) -> (String, String) {
324 let mut literal = String::new();
327 let mut rest = String::new();
328 let mut found_glob = false;
329 for (i, component) in pattern.split('/').enumerate() {
330 if !found_glob && !component.contains('*') {
331 if i > 0 && !literal.is_empty() {
332 literal.push('/');
333 }
334 literal.push_str(component);
335 } else {
336 found_glob = true;
337 if !rest.is_empty() {
338 rest.push('/');
339 }
340 rest.push_str(component);
341 }
342 }
343 if literal.is_empty() && !found_glob {
344 literal = pattern.to_string();
345 }
346 (literal, rest)
347}
348
349fn walk_glob(dir: &Path, parts: &[&str], idx: usize, out: &mut Vec<PathBuf>) {
350 if idx >= parts.len() {
351 out.push(dir.to_path_buf());
352 return;
353 }
354 let pat = parts[idx];
355 let entries = match std::fs::read_dir(dir) {
356 Ok(e) => e,
357 Err(_) => return,
358 };
359 if pat == "**" {
360 walk_glob(dir, parts, idx + 1, out);
362 for entry in entries.flatten() {
363 let p = entry.path();
364 if p.is_dir() {
365 walk_glob(&p, parts, idx, out);
366 }
367 }
368 return;
369 }
370 for entry in entries.flatten() {
371 let name = entry.file_name();
372 let name_s = name.to_string_lossy();
373 if glob_match(pat, &name_s) {
374 let p = entry.path();
375 if idx + 1 == parts.len() {
376 out.push(p);
377 } else if p.is_dir() {
378 walk_glob(&p, parts, idx + 1, out);
379 }
380 }
381 }
382}
383
384fn glob_match(pattern: &str, name: &str) -> bool {
385 let mut pi = 0;
387 let mut ni = 0;
388 let pbytes = pattern.as_bytes();
389 let nbytes = name.as_bytes();
390 let mut star: Option<(usize, usize)> = None;
391 while ni < nbytes.len() {
392 if pi < pbytes.len() && (pbytes[pi] == b'?' || pbytes[pi] == nbytes[ni]) {
393 pi += 1;
394 ni += 1;
395 } else if pi < pbytes.len() && pbytes[pi] == b'*' {
396 star = Some((pi, ni));
397 pi += 1;
398 } else if let Some((sp, sn)) = star {
399 pi = sp + 1;
400 ni = sn + 1;
401 star = Some((sp, ni));
402 } else {
403 return false;
404 }
405 }
406 while pi < pbytes.len() && pbytes[pi] == b'*' {
407 pi += 1;
408 }
409 pi == pbytes.len()
410}