tancore/process.rs
1use std::collections::HashMap;
2// use std::io::{BufRead, BufReader};
3use std::io::Write;
4use std::process::Stdio;
5
6use tan::error::Error;
7use tan::util::module_util::require_module;
8use tan::{context::Context, expr::Expr};
9
10// https://doc.rust-lang.org/std/env/index.html
11
12// #todo process/env, (let version (process/env :TAN-VERSION))
13// #todo process/args, (let file (process/args 1)) (let file (1 (process/args)))
14
15// #todo move env-vars and args to an env package, like Rust?
16
17// #todo process/vars or process/env or process/env-vars
18
19// (let debug (process/vars "DEBUG" true))
20// (let args (process/args))
21// (for (arg (process/args))
22// (writeln arg)
23// )
24// (let args (process/args->map))
25// (let debug (args "debug"))
26
27/// Terminates the current process with the specified exit code.
28pub fn process_exit(args: &[Expr]) -> Result<Expr, Error> {
29 // #todo Investigate if using Tokio in FFI somehow messes the flushing of streams, especially compiler errors.
30 // Flush the standard streams.
31 std::io::stdout().flush().expect("stdout flushed");
32 std::io::stderr().flush().expect("stderr flushed");
33
34 if let Some(code) = args.first() {
35 let Some(code) = code.as_int() else {
36 return Err(Error::invalid_arguments(
37 "expected Int argument",
38 code.range(),
39 ));
40 };
41
42 std::process::exit(code as i32);
43 } else {
44 // Exit with code=0 by default.
45 std::process::exit(0);
46 }
47}
48
49// #todo probably these FFI functions should just return an Expr, no Result.
50
51// #insight not used yet.
52/// Return the process arguments as an array, includes the foreign ('host') arguments.
53pub fn process_foreign_args(_args: &[Expr]) -> Result<Expr, Error> {
54 let mut args = Vec::new();
55
56 for arg in std::env::args() {
57 args.push(Expr::string(arg))
58 }
59
60 Ok(Expr::array(args))
61}
62
63/// Return the process arguments as an array
64pub fn process_args(_args: &[Expr], context: &mut Context) -> Result<Expr, Error> {
65 // #todo warn in arguments passed!
66 // #todo add unit-test
67
68 let process_args = context.top_scope.get("**process-args**").unwrap();
69 let process_args = process_args.as_array().unwrap();
70
71 // #todo consider a ref expression!
72 // #todo #hack #nasty crappy temp solution.
73 let mut args = Vec::new();
74 for arg in process_args.iter() {
75 args.push(arg.clone());
76 }
77 Ok(Expr::array(args))
78}
79
80// #todo consider renaming to just `env`?
81// #todo optionally support key/name argument to return the value of a specific env variable.
82/// Return the process environment variables as a Map/Map.
83pub fn process_env_vars(_args: &[Expr]) -> Result<Expr, Error> {
84 let mut env_vars = HashMap::new();
85
86 for (key, value) in std::env::vars() {
87 env_vars.insert(key, Expr::string(value));
88 }
89
90 Ok(Expr::map(env_vars))
91}
92
93// #todo spawn
94// #todo shell
95
96// #todo
97// pub fn process_spawn(args: &[Expr]) -> Result<Expr, Error> {
98// let [cmd] = args else {
99// return Err(Error::invalid_arguments(
100// "`exec` requires `cmd` argument",
101// None,
102// ));
103// };
104
105// let Expr::String(cmd_string) = cmd.unpack() else {
106// return Err(Error::invalid_arguments(
107// "`cmd` argument should be a String", // #todo mention `Stringable` or `Stringer`
108// cmd.range(),
109// ));
110// };
111
112// let mut args = cmd_string.split(" ");
113
114// let Some(cmd) = args.next() else {
115// return Err(Error::invalid_arguments(
116// "`cmd` argument can't be empty",
117// cmd.range(),
118// ));
119// };
120
121// let Ok(output) = std::process::Command::new(cmd).args(args).output() else {
122// // #todo should be runtime error.
123// // #todo even more it should be a Tan error.
124// return Err(Error::general("failed to execute cmd `{cmd}`"));
125// };
126
127// // #todo also return status and stderr.
128// // #todo proper conversion of stdout output.
129// // #todo could return map {status, stdout, stderr}
130
131// Ok(Expr::string(
132// String::from_utf8(output.stdout).unwrap_or_default(),
133// ))
134// }
135
136// #todo rename to shell? or exec shell?
137// #todo shortcut?
138/// Similar to C's system function:
139/// The command specified by string is passed to the host environment to be
140/// executed by the command processor.
141pub fn process_exec(args: &[Expr]) -> Result<Expr, Error> {
142 let [cmd] = args else {
143 return Err(Error::invalid_arguments(
144 "`exec` requires `cmd` argument",
145 None,
146 ));
147 };
148
149 let Expr::String(cmd_string) = cmd.unpack() else {
150 return Err(Error::invalid_arguments(
151 "`cmd` argument should be a String", // #todo mention `Stringable` or `Stringer`
152 cmd.range(),
153 ));
154 };
155
156 let Ok(output) = std::process::Command::new("sh")
157 .args(["-c", cmd_string])
158 .output()
159 else {
160 // #todo should be runtime error.
161 // #todo even more it should be a Tan error.
162 return Err(Error::general("failed to execute cmd `{cmd}`"));
163 };
164
165 // #todo also return status and stderr.
166 // #todo proper conversion of stdout output.
167 // #todo could return map {status, stdout, stderr}
168
169 Ok(Expr::string(
170 String::from_utf8(output.stdout).unwrap_or_default(),
171 ))
172}
173
174// #todo also implement a version that supports piping, with Tan streams/ports/channels.
175// #todo the piping version should use Tokio/Async to implement multiplexing of STDOUT and STDERR.
176
177// #todo find a better name, e.g. `exec-streaming`.
178// #ref https://stackoverflow.com/questions/31992237/how-would-you-stream-output-from-a-process
179/// The command specified by string is passed to the host environment to be
180/// executed by the command processor.
181/// The STDOUT and STDERR of the process are 'streamed' to the caller STDOUT, STDERR.
182pub fn process_shell(args: &[Expr]) -> Result<Expr, Error> {
183 let [cmd] = args else {
184 return Err(Error::invalid_arguments(
185 "`exec` requires `cmd` argument",
186 None,
187 ));
188 };
189
190 let Expr::String(cmd_string) = cmd.unpack() else {
191 return Err(Error::invalid_arguments(
192 "`cmd` argument should be a String", // #todo mention `Stringable` or `Stringer`
193 cmd.range(),
194 ));
195 };
196
197 let Ok(mut process) = std::process::Command::new("sh")
198 .arg("-c")
199 .arg(cmd_string)
200 // .stdout(Stdio::piped())
201 .stdout(Stdio::inherit())
202 .stderr(Stdio::inherit())
203 .spawn()
204 else {
205 // #todo should be runtime error.
206 // #todo even more it should be a Tan error.
207 return Err(Error::general("failed to spawn cmd `{cmd}`"));
208 };
209
210 // let stdout = process.stdout.as_mut().unwrap();
211 // let stdout_reader = BufReader::new(stdout);
212 // let stdout_lines = stdout_reader.lines();
213
214 // for line in stdout_lines {
215 // println!("{}", line.unwrap());
216 // }
217
218 let Ok(status) = process.wait() else {
219 // #todo should be runtime error.
220 // #todo even more it should be a Tan error.
221 return Err(Error::general("failed to spawn cmd `{cmd}`"));
222 };
223
224 let Some(code) = status.code() else {
225 // #todo how to handle this?
226 return Err(Error::general("process terminated by signal, cmd `{cmd}`"));
227 };
228
229 Ok(Expr::Int(code as i64))
230}
231
232// https://stackoverflow.com/questions/21011330/how-do-i-invoke-a-system-command-and-capture-its-output
233
234// #todo use one spawn for both string and ProcessSpec?
235// #todo (process/spawn-cmd "ls -al") ; spawn-str, spawn-command, cmd, sh, shell, exec, run
236// #todo (process/spawn-child child-process) -> Process(-Handle) ; just `spawn`
237// #todo (Process env args id stdin, stdout stderr status current-dir)
238
239// #todo consider removing the `std` prefix from module paths, like haskell.
240// #todo find a better prefix than setup_
241// #todo use Rc/Arc consistently
242// #todo some helpers are needed here, to streamline the code.
243
244pub fn import_lib_process(context: &mut Context) {
245 let module = require_module("process", context);
246
247 module.insert_invocable("exit", Expr::foreign_func(&process_exit));
248 module.insert_invocable("exit$$", Expr::foreign_func(&process_exit)); // #todo is this needed?
249
250 // (let file (process/args 1))
251 module.insert_invocable("args", Expr::foreign_func_mut_context(&process_args));
252 module.insert_invocable("args$$", Expr::foreign_func_mut_context(&process_args)); // #todo is this needed?
253
254 // #todo
255 // Better API:
256 // - process/env -> Map with all variables
257 // - process/env-var -> Query a variable by name, e.g. `(let var (process/env-var "VAR-NAME"))`
258
259 // #todo (let tan-path (process/env :TANPATH))
260 // (let tan-path ((process/env-vars) :TANPATH))
261 module.insert_invocable("env-vars", Expr::foreign_func(&process_env_vars));
262 module.insert_invocable("env-vars$$", Expr::foreign_func(&process_env_vars)); // #todo is this needed?
263
264 // (let output (process/exec "ls -al"))
265 module.insert_invocable("exec", Expr::foreign_func(&process_exec));
266 module.insert_invocable("exec$$String", Expr::foreign_func(&process_exec));
267
268 module.insert_invocable("shell", Expr::foreign_func(&process_shell));
269 module.insert_invocable("shell$$String", Expr::foreign_func(&process_shell));
270}
271
272// #todo add some tests, even without assertions, just to exercise these functions.