use std::process::{Command, Stdio};
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(
"exec-check",
Arity::AtLeast(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let (cmd, rest) = split_cmd(args, "exec-check", sp)?;
let status = Command::new(&*cmd)
.args(rest.iter().map(|s| s.as_ref()))
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| EvalError::native_fn("exec-check", e.to_string(), sp))?;
Ok(Value::Int(status.code().unwrap_or(-1) as i64))
},
);
interp.register_fn(
"exec-ok?",
Arity::AtLeast(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let (cmd, rest) = split_cmd(args, "exec-ok?", sp)?;
let status = Command::new(&*cmd)
.args(rest.iter().map(|s| s.as_ref()))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| EvalError::native_fn("exec-ok?", e.to_string(), sp))?;
Ok(Value::Bool(status.success()))
},
);
interp.register_fn(
"exec-capture",
Arity::AtLeast(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let (cmd, rest) = split_cmd(args, "exec-capture", sp)?;
let started = std::time::Instant::now();
let out = Command::new(&*cmd)
.args(rest.iter().map(|s| s.as_ref()))
.stdin(Stdio::null())
.output()
.map_err(|e| EvalError::native_fn("exec-capture", e.to_string(), sp))?;
let inv = Invocation::new(
&cmd,
rest.iter().map(|s| s.as_ref()).collect(),
started.elapsed().as_millis(),
);
Ok(capture_result(&inv, &out))
},
);
interp.register_fn(
"exec-with-stdin",
Arity::AtLeast(2),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
use std::io::Write;
let payload = str_arg(&args[0], "exec-with-stdin", sp)?;
let (cmd, rest) = split_cmd(&args[1..], "exec-with-stdin", sp)?;
let started = std::time::Instant::now();
let mut child = Command::new(&*cmd)
.args(rest.iter().map(|s| s.as_ref()))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| EvalError::native_fn("exec-with-stdin", e.to_string(), sp))?;
if let Some(mut sink) = child.stdin.take() {
sink.write_all(payload.as_bytes())
.map_err(|e| EvalError::native_fn("exec-with-stdin", e.to_string(), sp))?;
}
let out = child
.wait_with_output()
.map_err(|e| EvalError::native_fn("exec-with-stdin", e.to_string(), sp))?;
let inv = Invocation::new(
&cmd,
rest.iter().map(|s| s.as_ref()).collect(),
started.elapsed().as_millis(),
);
Ok(capture_result(&inv, &out))
},
);
interp.register_fn(
"exec-with-env",
Arity::AtLeast(2),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let pairs = env_pairs(&args[0], "exec-with-env", sp)?;
let (cmd, rest) = split_cmd(&args[1..], "exec-with-env", sp)?;
let mut c = Command::new(&*cmd);
c.args(rest.iter().map(|s| s.as_ref()))
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in &pairs {
c.env(k.as_ref(), v.as_ref());
}
let started = std::time::Instant::now();
let out = c
.output()
.map_err(|e| EvalError::native_fn("exec-with-env", e.to_string(), sp))?;
let inv = Invocation::new(
&cmd,
rest.iter().map(|s| s.as_ref()).collect(),
started.elapsed().as_millis(),
);
Ok(capture_result(&inv, &out))
},
);
interp.register_fn(
"sh-exec",
Arity::Exact(1),
|args: &[Value], _ctx: &mut ScriptCtx, sp| {
let script = str_arg(&args[0], "sh-exec", sp)?;
let started = std::time::Instant::now();
let out = Command::new("sh")
.arg("-c")
.arg(&*script)
.stdin(Stdio::null())
.output()
.map_err(|e| EvalError::native_fn("sh-exec", e.to_string(), sp))?;
let inv = Invocation::new("sh", vec!["-c", &script], started.elapsed().as_millis());
Ok(capture_result(&inv, &out))
},
);
}
fn split_cmd(
args: &[Value],
fname: &'static str,
sp: tatara_lisp::Span,
) -> Result<(Arc<str>, Vec<Arc<str>>), EvalError> {
let mut it = args.iter();
let cmd = str_arg(
it.next().ok_or_else(|| {
EvalError::native_fn(fname, "expected at least 1 argument".to_string(), sp)
})?,
fname,
sp,
)?;
let rest = it
.map(|v| str_arg(v, fname, sp))
.collect::<Result<Vec<_>, _>>()?;
Ok((cmd, rest))
}
fn env_pairs(
v: &Value,
fname: &'static str,
sp: tatara_lisp::Span,
) -> Result<Vec<(Arc<str>, Arc<str>)>, EvalError> {
let items = match v {
Value::List(items) => items,
_ => {
return Err(EvalError::native_fn(
fname,
"first argument must be an alist of (KEY VALUE) pairs".to_string(),
sp,
));
}
};
let mut out = Vec::with_capacity(items.len());
for it in items.iter() {
match it {
Value::List(kv) if kv.len() == 2 => {
out.push((str_arg(&kv[0], fname, sp)?, str_arg(&kv[1], fname, sp)?));
}
_ => {
return Err(EvalError::native_fn(
fname,
"each env entry must be a 2-element (KEY VALUE) list".to_string(),
sp,
));
}
}
}
Ok(out)
}
struct Invocation<'a> {
program: &'a str,
args: Vec<&'a str>,
elapsed_ms: u128,
}
impl<'a> Invocation<'a> {
fn new(program: &'a str, args: Vec<&'a str>, elapsed_ms: u128) -> Self {
Self {
program,
args,
elapsed_ms,
}
}
}
fn resolved_program(program: &str) -> String {
if program.contains(std::path::MAIN_SEPARATOR) {
return program.to_string();
}
which::which(program)
.map(|p| p.display().to_string())
.unwrap_or_default()
}
fn capture_result(inv: &Invocation<'_>, out: &std::process::Output) -> Value {
let mut argv = vec![Value::Str(Arc::from(inv.program))];
argv.extend(inv.args.iter().map(|a| Value::Str(Arc::from(*a))));
let cwd = std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_default();
Value::list(vec![
Value::list(vec![
Value::Keyword(Arc::from("status")),
Value::Int(out.status.code().unwrap_or(-1) as i64),
]),
Value::list(vec![
Value::Keyword(Arc::from("stdout")),
Value::Str(Arc::from(String::from_utf8_lossy(&out.stdout).as_ref())),
]),
Value::list(vec![
Value::Keyword(Arc::from("stderr")),
Value::Str(Arc::from(String::from_utf8_lossy(&out.stderr).as_ref())),
]),
Value::list(vec![Value::Keyword(Arc::from("argv")), Value::list(argv)]),
Value::list(vec![
Value::Keyword(Arc::from("program")),
Value::Str(Arc::from(inv.program)),
]),
Value::list(vec![
Value::Keyword(Arc::from("resolved")),
Value::Str(Arc::from(resolved_program(inv.program).as_str())),
]),
Value::list(vec![
Value::Keyword(Arc::from("cwd")),
Value::Str(Arc::from(cwd.as_str())),
]),
Value::list(vec![
Value::Keyword(Arc::from("duration-ms")),
Value::Int(inv.elapsed_ms.min(i64::MAX as u128) as i64),
]),
])
}