use std::path::{Path, PathBuf};
use std::sync::Arc;
use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
use crate::script_ctx::ScriptCtx;
use crate::stdlib::env::str_arg;
pub fn install(interp: &mut Interpreter<ScriptCtx>) {
interp.register_fn(
"glob",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let pattern = str_arg(&args[0], "glob", sp)?;
let entries = simple_glob(&pattern).map_err(|e| EvalError::native_fn("glob", e, sp))?;
Ok(Value::list(
entries
.into_iter()
.map(|p| Value::Str(Arc::from(p.to_string_lossy().into_owned())))
.collect::<Vec<_>>(),
))
},
);
interp.register_fn(
"walk-dir",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let root = str_arg(&args[0], "walk-dir", sp)?;
let mut out = Vec::new();
walk_collect(Path::new(&*root), &mut out)
.map_err(|e| EvalError::native_fn("walk-dir", e.to_string(), sp))?;
Ok(Value::list(
out.into_iter()
.map(|p| Value::Str(Arc::from(p.to_string_lossy().into_owned())))
.collect::<Vec<_>>(),
))
},
);
interp.register_fn(
"ls",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let dir = str_arg(&args[0], "ls", sp)?;
let mut entries: Vec<PathBuf> = std::fs::read_dir(&*dir)
.map_err(|e| EvalError::native_fn("ls", format!("{dir}: {e}"), sp))?
.filter_map(|r| r.ok().map(|e| e.path()))
.collect();
entries.sort();
Ok(Value::list(
entries
.into_iter()
.map(|p| Value::Str(Arc::from(p.to_string_lossy().into_owned())))
.collect::<Vec<_>>(),
))
},
);
interp.register_fn(
"mkdir",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let path = str_arg(&args[0], "mkdir", sp)?;
match std::fs::create_dir(&*path) {
Ok(()) => Ok(Value::Nil),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(Value::Nil),
Err(e) => Err(EvalError::native_fn("mkdir", format!("{path}: {e}"), sp)),
}
},
);
interp.register_fn(
"mkdir-p",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let path = str_arg(&args[0], "mkdir-p", sp)?;
std::fs::create_dir_all(&*path)
.map_err(|e| EvalError::native_fn("mkdir-p", format!("{path}: {e}"), sp))?;
Ok(Value::Nil)
},
);
interp.register_fn(
"rm",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let path = str_arg(&args[0], "rm", sp)?;
std::fs::remove_file(&*path)
.map_err(|e| EvalError::native_fn("rm", format!("{path}: {e}"), sp))?;
Ok(Value::Nil)
},
);
interp.register_fn(
"rm-rf",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let path = str_arg(&args[0], "rm-rf", sp)?;
if Path::new(&*path).is_dir() {
std::fs::remove_dir_all(&*path)
.map_err(|e| EvalError::native_fn("rm-rf", format!("{path}: {e}"), sp))?;
} else if Path::new(&*path).exists() {
std::fs::remove_file(&*path)
.map_err(|e| EvalError::native_fn("rm-rf", format!("{path}: {e}"), sp))?;
}
Ok(Value::Nil)
},
);
interp.register_fn(
"cwd",
Arity::Exact(0),
|_args: &[Value], _ctx: &mut ScriptCtx, sp| {
let d = std::env::current_dir()
.map_err(|e| EvalError::native_fn("cwd", e.to_string(), sp))?;
Ok(Value::Str(Arc::from(d.to_string_lossy().into_owned())))
},
);
interp.register_fn(
"chdir",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let path = str_arg(&args[0], "chdir", sp)?;
std::env::set_current_dir(&*path)
.map_err(|e| EvalError::native_fn("chdir", format!("{path}: {e}"), sp))?;
Ok(Value::Nil)
},
);
interp.register_fn(
"path-join",
Arity::AtLeast(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let mut buf = PathBuf::new();
for v in args {
let s = str_arg(v, "path-join", sp)?;
buf.push(&*s);
}
Ok(Value::Str(Arc::from(buf.to_string_lossy().into_owned())))
},
);
interp.register_fn(
"path-basename",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "path-basename", sp)?;
let base = Path::new(&*p)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Ok(Value::Str(Arc::from(base)))
},
);
interp.register_fn(
"path-dirname",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "path-dirname", sp)?;
let dir = Path::new(&*p)
.parent()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Ok(Value::Str(Arc::from(dir)))
},
);
interp.register_fn(
"path-extension",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "path-extension", sp)?;
let ext = Path::new(&*p)
.extension()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Ok(Value::Str(Arc::from(ext)))
},
);
interp.register_fn(
"path-absolute",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "path-absolute", sp)?;
let abs = std::fs::canonicalize(&*p)
.map_err(|e| EvalError::native_fn("path-absolute", format!("{p}: {e}"), sp))?;
Ok(Value::Str(Arc::from(abs.to_string_lossy().into_owned())))
},
);
interp.register_fn(
"file-size",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "file-size", sp)?;
let meta = std::fs::metadata(&*p)
.map_err(|e| EvalError::native_fn("file-size", format!("{p}: {e}"), sp))?;
Ok(Value::Int(meta.len() as i64))
},
);
interp.register_fn(
"is-dir?",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "is-dir?", sp)?;
Ok(Value::Bool(Path::new(&*p).is_dir()))
},
);
interp.register_fn(
"is-file?",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let p = str_arg(&args[0], "is-file?", sp)?;
Ok(Value::Bool(Path::new(&*p).is_file()))
},
);
interp.register_fn(
"tmp-dir",
Arity::Exact(0),
|_args: &[Value], _ctx: &mut ScriptCtx, sp| {
let base = std::env::temp_dir();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = base.join(format!("tatara-script-{now:x}"));
std::fs::create_dir_all(&dir)
.map_err(|e| EvalError::native_fn("tmp-dir", e.to_string(), sp))?;
Ok(Value::Str(Arc::from(dir.to_string_lossy().into_owned())))
},
);
interp.register_fn(
"tmp-file",
Arity::Exact(0),
|_args: &[Value], _ctx: &mut ScriptCtx, sp| {
let base = std::env::temp_dir();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = base.join(format!("tatara-script-{now:x}.tmp"));
std::fs::write(&path, b"")
.map_err(|e| EvalError::native_fn("tmp-file", e.to_string(), sp))?;
Ok(Value::Str(Arc::from(path.to_string_lossy().into_owned())))
},
);
}
fn walk_collect(root: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
let mut stack = vec![root.to_path_buf()];
while let Some(cur) = stack.pop() {
if cur.is_file() {
out.push(cur);
continue;
}
if !cur.is_dir() {
continue;
}
for entry in std::fs::read_dir(&cur)? {
let entry = entry?;
stack.push(entry.path());
}
}
Ok(())
}
fn simple_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
let (prefix, remainder) = split_glob_prefix(pattern);
let base = if prefix.is_empty() {
PathBuf::from(".")
} else {
PathBuf::from(&prefix)
};
let parts: Vec<&str> = remainder.split('/').filter(|s| !s.is_empty()).collect();
if parts.is_empty() {
return Ok(vec![base]);
}
let mut out = Vec::new();
walk_glob(&base, &parts, 0, &mut out);
Ok(out)
}
fn split_glob_prefix(pattern: &str) -> (String, String) {
let mut literal = String::new();
let mut rest = String::new();
let mut found_glob = false;
for (i, component) in pattern.split('/').enumerate() {
if !found_glob && !component.contains('*') {
if i > 0 && !literal.is_empty() {
literal.push('/');
}
literal.push_str(component);
} else {
found_glob = true;
if !rest.is_empty() {
rest.push('/');
}
rest.push_str(component);
}
}
if literal.is_empty() && !found_glob {
literal = pattern.to_string();
}
(literal, rest)
}
fn walk_glob(dir: &Path, parts: &[&str], idx: usize, out: &mut Vec<PathBuf>) {
if idx >= parts.len() {
out.push(dir.to_path_buf());
return;
}
let pat = parts[idx];
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
if pat == "**" {
walk_glob(dir, parts, idx + 1, out);
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
walk_glob(&p, parts, idx, out);
}
}
return;
}
for entry in entries.flatten() {
let name = entry.file_name();
let name_s = name.to_string_lossy();
if glob_match(pat, &name_s) {
let p = entry.path();
if idx + 1 == parts.len() {
out.push(p);
} else if p.is_dir() {
walk_glob(&p, parts, idx + 1, out);
}
}
}
}
fn glob_match(pattern: &str, name: &str) -> bool {
let mut pi = 0;
let mut ni = 0;
let pbytes = pattern.as_bytes();
let nbytes = name.as_bytes();
let mut star: Option<(usize, usize)> = None;
while ni < nbytes.len() {
if pi < pbytes.len() && (pbytes[pi] == b'?' || pbytes[pi] == nbytes[ni]) {
pi += 1;
ni += 1;
} else if pi < pbytes.len() && pbytes[pi] == b'*' {
star = Some((pi, ni));
pi += 1;
} else if let Some((sp, sn)) = star {
pi = sp + 1;
ni = sn + 1;
star = Some((sp, ni));
} else {
return false;
}
}
while pi < pbytes.len() && pbytes[pi] == b'*' {
pi += 1;
}
pi == pbytes.len()
}