- Add new `doot-utils` crate with `xdg` module for consistent cross-platform directory resolution - Replace `dirs` crate usage with `doot_utils::xdg` functions in cli, core, and lang crates - Use XDG layout on all platforms (including macOS) for config, data, cache, and state directories - Add home_dir(), config_home(), data_home(), cache_home(), and state_home() helpers - Update dependencies to use doot-utils workspace reference - Remove unused dirs and related crates from Cargo.lock - Improve error handling in template rendering and package installation - Add InstalledCache for package managers to reduce process spawning - Optimize brew package installation with parallel fetching and sequential installing - Fix path canonicalization in e2e tests for consistent symlink handling - Add clippy allowance for large parser errors in doot-lang
428 lines
14 KiB
Rust
428 lines
14 KiB
Rust
use crate::evaluator::{EvalError, Value};
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use walkdir::WalkDir;
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn read_file(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
let content = std::fs::read_to_string(&path)?;
|
|
Ok(Value::Str(content))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn read_file_lines(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
let content = std::fs::read_to_string(&path)?;
|
|
let lines: Vec<Value> = content.lines().map(|l| Value::Str(l.to_string())).collect();
|
|
Ok(Value::List(lines))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn write_file(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
let content = match args.get(1) {
|
|
Some(Value::Str(s)) => s,
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"write_file requires content string".to_string(),
|
|
));
|
|
}
|
|
};
|
|
std::fs::write(&path, content)?;
|
|
Ok(Value::Bool(true))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn copy_file(args: &[Value]) -> Result<Value, EvalError> {
|
|
let src = get_path(args)?;
|
|
let dst = match args.get(1) {
|
|
Some(Value::Path(p)) => p.clone(),
|
|
Some(Value::Str(s)) => expand_path(s),
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"copy_file requires destination path".to_string(),
|
|
));
|
|
}
|
|
};
|
|
std::fs::copy(&src, &dst)?;
|
|
Ok(Value::Bool(true))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn delete_file(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
std::fs::remove_file(&path)?;
|
|
Ok(Value::Bool(true))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn file_exists(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
Ok(Value::Bool(path.is_file()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn dir_exists(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
Ok(Value::Bool(path.is_dir()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn create_dir_all(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
std::fs::create_dir_all(&path)?;
|
|
Ok(Value::Bool(true))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn list_dir(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
let entries: Vec<Value> = std::fs::read_dir(&path)?
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| Value::Path(e.path()))
|
|
.collect();
|
|
Ok(Value::List(entries))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn walk_dir(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
let entries: Vec<Value> = WalkDir::new(&path)
|
|
.into_iter()
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| Value::Path(e.path().to_path_buf()))
|
|
.collect();
|
|
Ok(Value::List(entries))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn temp_dir() -> Result<Value, EvalError> {
|
|
Ok(Value::Path(std::env::temp_dir()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn temp_file(args: &[Value]) -> Result<Value, EvalError> {
|
|
let prefix = match args.first() {
|
|
Some(Value::Str(s)) => s.as_str(),
|
|
_ => "doot",
|
|
};
|
|
let suffix = match args.get(1) {
|
|
Some(Value::Str(s)) => s.as_str(),
|
|
_ => "",
|
|
};
|
|
let path = std::env::temp_dir().join(format!("{}_{}{}", prefix, uuid_simple(), suffix));
|
|
Ok(Value::Path(path))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn is_symlink(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
Ok(Value::Bool(path.is_symlink()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn read_link(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
let target = std::fs::read_link(&path)?;
|
|
Ok(Value::Path(target))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn path_join(args: &[Value]) -> Result<Value, EvalError> {
|
|
let mut result = PathBuf::new();
|
|
for arg in args {
|
|
match arg {
|
|
Value::Path(p) => result.push(p),
|
|
Value::Str(s) => result.push(s),
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"path_join expects paths or strings".to_string(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
Ok(Value::Path(result))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn path_parent(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
Ok(Value::Path(
|
|
path.parent().map(|p| p.to_path_buf()).unwrap_or_default(),
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn path_filename(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
Ok(Value::Str(
|
|
path.file_name()
|
|
.map(|s| s.to_string_lossy().to_string())
|
|
.unwrap_or_default(),
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn path_extension(args: &[Value]) -> Result<Value, EvalError> {
|
|
let path = get_path(args)?;
|
|
Ok(Value::Str(
|
|
path.extension()
|
|
.map(|s| s.to_string_lossy().to_string())
|
|
.unwrap_or_default(),
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn home_dir() -> Result<Value, EvalError> {
|
|
Ok(Value::Path(doot_utils::xdg::home_dir()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn config_dir() -> Result<Value, EvalError> {
|
|
Ok(Value::Path(doot_utils::xdg::config_home()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn data_dir() -> Result<Value, EvalError> {
|
|
Ok(Value::Path(doot_utils::xdg::data_home()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn cache_dir() -> Result<Value, EvalError> {
|
|
Ok(Value::Path(doot_utils::xdg::cache_home()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn exec(args: &[Value]) -> Result<Value, EvalError> {
|
|
let cmd = match args.first() {
|
|
Some(Value::Str(s)) => s,
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"exec expects a command string".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let output = Command::new("sh").arg("-c").arg(cmd).output()?;
|
|
|
|
Ok(Value::Str(
|
|
String::from_utf8_lossy(&output.stdout).to_string(),
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn exec_with_status(args: &[Value]) -> Result<Value, EvalError> {
|
|
let cmd = match args.first() {
|
|
Some(Value::Str(s)) => s,
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"exec_with_status expects a command string".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let status = Command::new("sh").arg("-c").arg(cmd).status()?;
|
|
|
|
Ok(Value::Int(status.code().unwrap_or(-1) as i64))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn shell(args: &[Value]) -> Result<Value, EvalError> {
|
|
exec(args)
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn which(args: &[Value]) -> Result<Value, EvalError> {
|
|
let cmd = match args.first() {
|
|
Some(Value::Str(s)) => s,
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"which expects a command name".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let output = Command::new("which").arg(cmd).output()?;
|
|
|
|
if output.status.success() {
|
|
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
Ok(Value::Path(PathBuf::from(path)))
|
|
} else {
|
|
Ok(Value::None)
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn to_json(args: &[Value]) -> Result<Value, EvalError> {
|
|
let val = args.first().unwrap_or(&Value::None);
|
|
let json = value_to_json(val);
|
|
Ok(Value::Str(json.to_string()))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn from_json(args: &[Value]) -> Result<Value, EvalError> {
|
|
let s = match args.first() {
|
|
Some(Value::Str(s)) => s,
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"from_json expects a string".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let json: serde_json::Value = serde_json::from_str(s)
|
|
.map_err(|e| EvalError::TypeError(format!("invalid JSON: {}", e)))?;
|
|
|
|
Ok(json_to_value(&json))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn to_toml(args: &[Value]) -> Result<Value, EvalError> {
|
|
let val = args.first().unwrap_or(&Value::None);
|
|
let toml_val = value_to_toml(val);
|
|
let s = toml::to_string(&toml_val)
|
|
.map_err(|e| EvalError::TypeError(format!("TOML serialization error: {}", e)))?;
|
|
Ok(Value::Str(s))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn from_toml(args: &[Value]) -> Result<Value, EvalError> {
|
|
let s = match args.first() {
|
|
Some(Value::Str(s)) => s,
|
|
_ => {
|
|
return Err(EvalError::TypeError(
|
|
"from_toml expects a string".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let toml_val: toml::Value =
|
|
toml::from_str(s).map_err(|e| EvalError::TypeError(format!("invalid TOML: {}", e)))?;
|
|
|
|
Ok(toml_to_value(&toml_val))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn to_yaml(args: &[Value]) -> Result<Value, EvalError> {
|
|
let val = args.first().unwrap_or(&Value::None);
|
|
let json = value_to_json(val);
|
|
Ok(Value::Str(
|
|
serde_json::to_string_pretty(&json).unwrap_or_default(),
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(level = "trace", skip_all)]
|
|
pub fn from_yaml(args: &[Value]) -> Result<Value, EvalError> {
|
|
from_json(args)
|
|
}
|
|
|
|
fn get_path(args: &[Value]) -> Result<PathBuf, EvalError> {
|
|
match args.first() {
|
|
Some(Value::Path(p)) => Ok(p.clone()),
|
|
Some(Value::Str(s)) => Ok(expand_path(s)),
|
|
_ => Err(EvalError::TypeError("expected path or string".to_string())),
|
|
}
|
|
}
|
|
|
|
fn expand_path(s: &str) -> PathBuf {
|
|
if let Some(stripped) = s.strip_prefix('~') {
|
|
let home = doot_utils::xdg::home_dir();
|
|
home.join(stripped.strip_prefix('/').unwrap_or(stripped))
|
|
} else {
|
|
PathBuf::from(s)
|
|
}
|
|
}
|
|
|
|
fn uuid_simple() -> String {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
format!("{:x}", nanos)
|
|
}
|
|
|
|
fn value_to_json(val: &Value) -> serde_json::Value {
|
|
match val {
|
|
Value::Int(n) => serde_json::Value::Number(serde_json::Number::from(*n)),
|
|
Value::Float(n) => serde_json::Number::from_f64(*n)
|
|
.map(serde_json::Value::Number)
|
|
.unwrap_or(serde_json::Value::Null),
|
|
Value::Str(s) => serde_json::Value::String(s.clone()),
|
|
Value::Bool(b) => serde_json::Value::Bool(*b),
|
|
Value::Path(p) => serde_json::Value::String(p.display().to_string()),
|
|
Value::List(items) => serde_json::Value::Array(items.iter().map(value_to_json).collect()),
|
|
Value::Struct(_, fields) => {
|
|
let map: serde_json::Map<String, serde_json::Value> = fields
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), value_to_json(v)))
|
|
.collect();
|
|
serde_json::Value::Object(map)
|
|
}
|
|
Value::None => serde_json::Value::Null,
|
|
_ => serde_json::Value::Null,
|
|
}
|
|
}
|
|
|
|
fn json_to_value(json: &serde_json::Value) -> Value {
|
|
match json {
|
|
serde_json::Value::Null => Value::None,
|
|
serde_json::Value::Bool(b) => Value::Bool(*b),
|
|
serde_json::Value::Number(n) => {
|
|
if let Some(i) = n.as_i64() {
|
|
Value::Int(i)
|
|
} else if let Some(f) = n.as_f64() {
|
|
Value::Float(f)
|
|
} else {
|
|
Value::None
|
|
}
|
|
}
|
|
serde_json::Value::String(s) => Value::Str(s.clone()),
|
|
serde_json::Value::Array(arr) => Value::List(arr.iter().map(json_to_value).collect()),
|
|
serde_json::Value::Object(obj) => {
|
|
let fields: indexmap::IndexMap<String, Value> = obj
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), json_to_value(v)))
|
|
.collect();
|
|
Value::Struct("object".to_string(), fields)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn value_to_toml(val: &Value) -> toml::Value {
|
|
match val {
|
|
Value::Int(n) => toml::Value::Integer(*n),
|
|
Value::Float(n) => toml::Value::Float(*n),
|
|
Value::Str(s) => toml::Value::String(s.clone()),
|
|
Value::Bool(b) => toml::Value::Boolean(*b),
|
|
Value::Path(p) => toml::Value::String(p.display().to_string()),
|
|
Value::List(items) => toml::Value::Array(items.iter().map(value_to_toml).collect()),
|
|
Value::Struct(_, fields) => {
|
|
let map: toml::map::Map<String, toml::Value> = fields
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), value_to_toml(v)))
|
|
.collect();
|
|
toml::Value::Table(map)
|
|
}
|
|
_ => toml::Value::String(String::new()),
|
|
}
|
|
}
|
|
|
|
fn toml_to_value(toml: &toml::Value) -> Value {
|
|
match toml {
|
|
toml::Value::Boolean(b) => Value::Bool(*b),
|
|
toml::Value::Integer(i) => Value::Int(*i),
|
|
toml::Value::Float(f) => Value::Float(*f),
|
|
toml::Value::String(s) => Value::Str(s.clone()),
|
|
toml::Value::Array(arr) => Value::List(arr.iter().map(toml_to_value).collect()),
|
|
toml::Value::Table(table) => {
|
|
let fields: indexmap::IndexMap<String, Value> = table
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), toml_to_value(v)))
|
|
.collect();
|
|
Value::Struct("table".to_string(), fields)
|
|
}
|
|
toml::Value::Datetime(dt) => Value::Str(dt.to_string()),
|
|
}
|
|
}
|