Skip to main content

tatara_lisp_script/stdlib/
process.rs

1//! Process / shell integration.
2//!
3//!   (exec-check CMD ARG…)        → 0 on success, non-zero exit code otherwise
4//!                                   Streams stdin/stdout/stderr to the parent.
5//!   (exec-capture CMD ARG…)      → ((:status N) (:stdout "…") (:stderr "…"))
6//!                                   Captures stdout + stderr, exposes exit code.
7//!   (exec-ok? CMD ARG…)          → bool; true iff exit code is 0
8//!   (sh-exec STR)                → convenience: run STR through `sh -c`
9//!                                   returning the capture-form result
10//!
11//! No implicit shell interpolation. Arguments are passed literally to
12//! the underlying process; no glob / word-splitting / $VAR substitution.
13//! Scripts that want shell features use `sh-exec` explicitly.
14
15use std::process::{Command, Stdio};
16use std::sync::Arc;
17
18use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
19
20use crate::script_ctx::ScriptCtx;
21use crate::stdlib::env::str_arg;
22
23pub fn install(interp: &mut Interpreter<ScriptCtx>) {
24    interp.register_fn(
25        "exec-check",
26        Arity::AtLeast(1),
27        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
28            let (cmd, rest) = split_cmd(args, "exec-check", sp)?;
29            let status = Command::new(&*cmd)
30                .args(rest.iter().map(|s| s.as_ref()))
31                .stdin(Stdio::inherit())
32                .stdout(Stdio::inherit())
33                .stderr(Stdio::inherit())
34                .status()
35                .map_err(|e| EvalError::native_fn("exec-check", e.to_string(), sp))?;
36            Ok(Value::Int(status.code().unwrap_or(-1) as i64))
37        },
38    );
39
40    interp.register_fn(
41        "exec-ok?",
42        Arity::AtLeast(1),
43        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
44            let (cmd, rest) = split_cmd(args, "exec-ok?", sp)?;
45            let status = Command::new(&*cmd)
46                .args(rest.iter().map(|s| s.as_ref()))
47                .stdin(Stdio::null())
48                .stdout(Stdio::null())
49                .stderr(Stdio::null())
50                .status()
51                .map_err(|e| EvalError::native_fn("exec-ok?", e.to_string(), sp))?;
52            Ok(Value::Bool(status.success()))
53        },
54    );
55
56    interp.register_fn(
57        "exec-capture",
58        Arity::AtLeast(1),
59        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
60            let (cmd, rest) = split_cmd(args, "exec-capture", sp)?;
61            let out = Command::new(&*cmd)
62                .args(rest.iter().map(|s| s.as_ref()))
63                .stdin(Stdio::null())
64                .output()
65                .map_err(|e| EvalError::native_fn("exec-capture", e.to_string(), sp))?;
66            Ok(capture_result(&out))
67        },
68    );
69
70    interp.register_fn(
71        "sh-exec",
72        Arity::Exact(1),
73        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
74            let script = str_arg(&args[0], "sh-exec", sp)?;
75            let out = Command::new("sh")
76                .arg("-c")
77                .arg(&*script)
78                .stdin(Stdio::null())
79                .output()
80                .map_err(|e| EvalError::native_fn("sh-exec", e.to_string(), sp))?;
81            Ok(capture_result(&out))
82        },
83    );
84}
85
86fn split_cmd(
87    args: &[Value],
88    fname: &'static str,
89    sp: tatara_lisp::Span,
90) -> Result<(Arc<str>, Vec<Arc<str>>), EvalError> {
91    let mut it = args.iter();
92    let cmd = str_arg(
93        it.next().ok_or_else(|| {
94            EvalError::native_fn(fname, "expected at least 1 argument".to_string(), sp)
95        })?,
96        fname,
97        sp,
98    )?;
99    let rest = it
100        .map(|v| str_arg(v, fname, sp))
101        .collect::<Result<Vec<_>, _>>()?;
102    Ok((cmd, rest))
103}
104
105fn capture_result(out: &std::process::Output) -> Value {
106    Value::list(vec![
107        Value::list(vec![
108            Value::Keyword(Arc::from("status")),
109            Value::Int(out.status.code().unwrap_or(-1) as i64),
110        ]),
111        Value::list(vec![
112            Value::Keyword(Arc::from("stdout")),
113            Value::Str(Arc::from(String::from_utf8_lossy(&out.stdout).as_ref())),
114        ]),
115        Value::list(vec![
116            Value::Keyword(Arc::from("stderr")),
117            Value::Str(Arc::from(String::from_utf8_lossy(&out.stderr).as_ref())),
118        ]),
119    ])
120}