Skip to main content

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.