tatara_lisp_script/stdlib/
process.rs1use std::process::{Command, Stdio};
30use std::sync::Arc;
31
32use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
33
34use crate::script_ctx::ScriptCtx;
35use crate::stdlib::env::str_arg;
36
37pub fn install(interp: &mut Interpreter<ScriptCtx>) {
38 interp.register_fn(
39 "exec-check",
40 Arity::AtLeast(1),
41 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
42 let (cmd, rest) = split_cmd(args, "exec-check", sp)?;
43 let status = Command::new(&*cmd)
44 .args(rest.iter().map(|s| s.as_ref()))
45 .stdin(Stdio::inherit())
46 .stdout(Stdio::inherit())
47 .stderr(Stdio::inherit())
48 .status()
49 .map_err(|e| EvalError::native_fn("exec-check", e.to_string(), sp))?;
50 Ok(Value::Int(status.code().unwrap_or(-1) as i64))
51 },
52 );
53
54 interp.register_fn(
55 "exec-ok?",
56 Arity::AtLeast(1),
57 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
58 let (cmd, rest) = split_cmd(args, "exec-ok?", sp)?;
59 let status = Command::new(&*cmd)
60 .args(rest.iter().map(|s| s.as_ref()))
61 .stdin(Stdio::null())
62 .stdout(Stdio::null())
63 .stderr(Stdio::null())
64 .status()
65 .map_err(|e| EvalError::native_fn("exec-ok?", e.to_string(), sp))?;
66 Ok(Value::Bool(status.success()))
67 },
68 );
69
70 interp.register_fn(
71 "exec-capture",
72 Arity::AtLeast(1),
73 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
74 let (cmd, rest) = split_cmd(args, "exec-capture", sp)?;
75 let out = Command::new(&*cmd)
76 .args(rest.iter().map(|s| s.as_ref()))
77 .stdin(Stdio::null())
78 .output()
79 .map_err(|e| EvalError::native_fn("exec-capture", e.to_string(), sp))?;
80 Ok(capture_result(&out))
81 },
82 );
83
84 interp.register_fn(
85 "exec-with-stdin",
86 Arity::AtLeast(2),
87 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
88 use std::io::Write;
89 let payload = str_arg(&args[0], "exec-with-stdin", sp)?;
90 let (cmd, rest) = split_cmd(&args[1..], "exec-with-stdin", sp)?;
91 let mut child = Command::new(&*cmd)
92 .args(rest.iter().map(|s| s.as_ref()))
93 .stdin(Stdio::piped())
94 .stdout(Stdio::piped())
95 .stderr(Stdio::piped())
96 .spawn()
97 .map_err(|e| EvalError::native_fn("exec-with-stdin", e.to_string(), sp))?;
98 if let Some(mut sink) = child.stdin.take() {
101 sink.write_all(payload.as_bytes())
102 .map_err(|e| EvalError::native_fn("exec-with-stdin", e.to_string(), sp))?;
103 }
104 let out = child
105 .wait_with_output()
106 .map_err(|e| EvalError::native_fn("exec-with-stdin", e.to_string(), sp))?;
107 Ok(capture_result(&out))
108 },
109 );
110
111 interp.register_fn(
112 "exec-with-env",
113 Arity::AtLeast(2),
114 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
115 let pairs = env_pairs(&args[0], "exec-with-env", sp)?;
116 let (cmd, rest) = split_cmd(&args[1..], "exec-with-env", sp)?;
117 let mut c = Command::new(&*cmd);
118 c.args(rest.iter().map(|s| s.as_ref()))
119 .stdin(Stdio::null())
120 .stdout(Stdio::piped())
121 .stderr(Stdio::piped());
122 for (k, v) in &pairs {
123 c.env(k.as_ref(), v.as_ref());
124 }
125 let out = c
126 .output()
127 .map_err(|e| EvalError::native_fn("exec-with-env", e.to_string(), sp))?;
128 Ok(capture_result(&out))
129 },
130 );
131
132 interp.register_fn(
133 "sh-exec",
134 Arity::Exact(1),
135 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
136 let script = str_arg(&args[0], "sh-exec", sp)?;
137 let out = Command::new("sh")
138 .arg("-c")
139 .arg(&*script)
140 .stdin(Stdio::null())
141 .output()
142 .map_err(|e| EvalError::native_fn("sh-exec", e.to_string(), sp))?;
143 Ok(capture_result(&out))
144 },
145 );
146}
147
148fn split_cmd(
149 args: &[Value],
150 fname: &'static str,
151 sp: tatara_lisp::Span,
152) -> Result<(Arc<str>, Vec<Arc<str>>), EvalError> {
153 let mut it = args.iter();
154 let cmd = str_arg(
155 it.next().ok_or_else(|| {
156 EvalError::native_fn(fname, "expected at least 1 argument".to_string(), sp)
157 })?,
158 fname,
159 sp,
160 )?;
161 let rest = it
162 .map(|v| str_arg(v, fname, sp))
163 .collect::<Result<Vec<_>, _>>()?;
164 Ok((cmd, rest))
165}
166
167fn env_pairs(
173 v: &Value,
174 fname: &'static str,
175 sp: tatara_lisp::Span,
176) -> Result<Vec<(Arc<str>, Arc<str>)>, EvalError> {
177 let items = match v {
178 Value::List(items) => items,
179 _ => {
180 return Err(EvalError::native_fn(
181 fname,
182 "first argument must be an alist of (KEY VALUE) pairs".to_string(),
183 sp,
184 ));
185 }
186 };
187 let mut out = Vec::with_capacity(items.len());
188 for it in items.iter() {
189 match it {
190 Value::List(kv) if kv.len() == 2 => {
191 out.push((str_arg(&kv[0], fname, sp)?, str_arg(&kv[1], fname, sp)?));
192 }
193 _ => {
194 return Err(EvalError::native_fn(
195 fname,
196 "each env entry must be a 2-element (KEY VALUE) list".to_string(),
197 sp,
198 ));
199 }
200 }
201 }
202 Ok(out)
203}
204
205fn capture_result(out: &std::process::Output) -> Value {
206 Value::list(vec![
207 Value::list(vec![
208 Value::Keyword(Arc::from("status")),
209 Value::Int(out.status.code().unwrap_or(-1) as i64),
210 ]),
211 Value::list(vec![
212 Value::Keyword(Arc::from("stdout")),
213 Value::Str(Arc::from(String::from_utf8_lossy(&out.stdout).as_ref())),
214 ]),
215 Value::list(vec![
216 Value::Keyword(Arc::from("stderr")),
217 Value::Str(Arc::from(String::from_utf8_lossy(&out.stderr).as_ref())),
218 ]),
219 ])
220}