///|
/// Represents a configuration for a standard stream (stdin, stdout, stderr).
pub(all) enum Stdio {
/// Inherit the corresponding stream from the parent process.
Inherit
/// Create a new pipe connecting the parent and child processes.
Piped
/// Redirect the stream to the null device (e.g., /dev/null).
Null
} derive(Show, Eq)
///|
/// A builder for creating and configuring a new process.
/// This structure mimics the Rust `std::process::Command` API.
pub struct Command {
program : String
args : Array[String]
mut env_clear : Bool
env_vars : Map[String, String]
mut cwd : String?
mut stdin : Stdio
mut stdout : Stdio
mut stderr : Stdio
}
///|
/// Creates a new Command for the given program.
///
/// # Arguments
///
/// * `program` - The path to the program to execute.
pub fn Command::new(program : String) -> Command {
{
program,
args: [],
env_clear: false,
env_vars: Map::new(),
cwd: None,
stdin: Inherit,
stdout: Inherit,
stderr: Inherit,
}
}
///|
/// Appends an argument to the command.
pub fn Command::arg(self : Command, arg : String) -> Command {
self.args.push(arg)
self
}
///|
/// Appends multiple arguments to the command.
pub fn Command::args(self : Command, args : Array[String]) -> Command {
self.args.append(args)
self
}
///|
/// Configures an environment variable for the new process.
///
/// Note: By default, the new process inherits the environment of the parent process.
/// Use `env_clear` to prevent this.
pub fn Command::env(self : Command, key : String, value : String) -> Command {
self.env_vars.set(key, value)
self
}
///|
/// Removes an environment variable from the configuration.
pub fn Command::env_remove(self : Command, key : String) -> Command {
self.env_vars.remove(key)
self
}
///|
/// Clears all environment variables for the new process.
/// If this is called, the child process will start with no environment variables
/// (except those explicitly added via `env`).
pub fn Command::env_clear(self : Command) -> Command {
self.env_clear = true
self.env_vars.clear()
self
}
///|
/// Sets the working directory for the new process.
pub fn Command::current_dir(self : Command, dir : String) -> Command {
self.cwd = Some(dir)
self
}
///|
/// Configures the standard input (stdin) for the new process.
pub fn Command::stdin(self : Command, cfg : Stdio) -> Command {
self.stdin = cfg
self
}
///|
/// Configures the standard output (stdout) for the new process.
pub fn Command::stdout(self : Command, cfg : Stdio) -> Command {
self.stdout = cfg
self
}
///|
/// Configures the standard error (stderr) for the new process.
pub fn Command::stderr(self : Command, cfg : Stdio) -> Command {
self.stderr = cfg
self
}
///|
/// Describes the result of a process execution.
pub struct ExitStatus {
exit_code : Int
} derive(Show, Eq)
///|
/// Returns true if the process exited successfully (exit code 0).
pub fn ExitStatus::success(self : ExitStatus) -> Bool {
self.exit_code == 0
}
///|
/// Returns the exit code of the process.
pub fn ExitStatus::code(self : ExitStatus) -> Int {
self.exit_code
}
///|
/// Represents the output of a finished process.
pub struct Output {
status : ExitStatus
stdout : Bytes
stderr : Bytes
} derive(Show, Eq)
///|
/// A handle to a child process.
pub(all) struct Child {
pid : Int
handle : Int64
stdin : Int64
stdout : Int64
stderr : Int64
}
///|
pub suberror ProcessError {
NotFound(String)
PermissionDenied(String)
Unknown(String)
} derive(Show, Eq)
///|
/// Executes the command as a child process, returning a handle to it.
///
/// # Platform Compatibility
///
/// This function is intended for native targets (Linux, macOS, Windows).
///
/// # WASM Limitations
///
/// In a WASM environment (e.g., standard WASM or WASI), spawning arbitrary external
/// processes is typically restricted or unsupported. Calling this method in such
/// environments may trap or return a generic error.
pub fn Command::spawn(self : Command) -> Child raise ProcessError {
// Flatten Env: "KEY=VAL\0..."
let env_buf = StringBuilder::new()
for k, v in self.env_vars {
env_buf.write_string("\{k}=\{v}")
env_buf.write_char('\u0000')
}
// Flatten Args: "prog\0arg1\0..."
let args_buf = StringBuilder::new()
args_buf.write_string(self.program)
args_buf.write_char('\u0000')
for arg in self.args {
args_buf.write_string(arg)
args_buf.write_char('\u0000')
}
args_buf.write_char('\u0000')
let cwd_str = match self.cwd {
Some(s) => s
None => ""
}
let program_bytes = @utf8.encode(self.program + "\u0000")
let cwd_bytes = if cwd_str == "" { b"" } else { @utf8.encode(cwd_str + "\u0000") }
// If env_vars is empty and we haven't explicitly cleared it, inherit the parent environment.
// Otherwise, use the constructed env_buf (which might be empty if we cleared it).
let inherit_env = if self.env_vars.is_empty() && !self.env_clear { 1 } else { 0 }
let handles = FixedArray::make(4, 0L)
let res = ffi_spawn(
program_bytes,
args_buf.to_string() |> @utf8.encode,
env_buf.to_string() |> @utf8.encode,
cwd_bytes,
stdio_to_int(self.stdin),
stdio_to_int(self.stdout),
stdio_to_int(self.stderr),
inherit_env,
handles
)
if res != 0 {
match res {
-2 => raise NotFound("Program not found: " + self.program)
-13 => raise PermissionDenied("Permission denied: " + self.program)
_ => raise Unknown("Spawn failed with error code: \{res}")
}
}
let proc_handle = handles[0]
let pid = ffi_get_pid(proc_handle)
{
pid,
handle: proc_handle,
stdin: handles[1],
stdout: handles[2],
stderr: handles[3]
}
}
fn stdio_to_int(s : Stdio) -> Int {
match s {
Inherit => STDIO_INHERIT
Piped => STDIO_PIPED
Null => STDIO_NULL
}
}
///|
/// Returns the process ID of the child process.
pub fn Child::pid(self : Child) -> Int {
self.pid
}
///|
/// Writes data to the child process's standard input.
/// Returns the number of bytes written.
///
/// Note: The child process must have been spawned with `stdin(Piped)`.
/// If stdin is not piped, this function may return an error or 0.
pub fn Child::write_stdin(self : Child, data : Bytes) -> Int {
if self.stdin == 0L {
return -1
}
ffi_write(self.stdin, data, data.length())
}
///|
/// Closes the child process's standard input.
/// This is useful to signal EOF to the child process.
pub fn Child::close_stdin(self : Child) -> Unit {
if self.stdin != 0L {
ffi_close(self.stdin) |> ignore
// We strictly should mark it as closed in the struct to avoid double close,
// but the struct fields are immutable in binding (though Int64 is value type).
// The struct definition has fields, they are immutable by default let binding?
// The struct 'Child' fields are not mutable.
// I cannot update 'self.stdin'.
// However, calling close twice on a handle is generally safe-ish (might return error)
// or bad (if handle reused).
// But for this simple implementation, it's okay.
}
}
///|
/// Reads data from the child process's standard output into the provided buffer.
/// Returns the number of bytes read.
///
/// Note: The child process must have been spawned with `stdout(Piped)`.
pub fn Child::read_stdout(self : Child, buf : Bytes) -> Int {
if self.stdout == 0L {
return -1
}
ffi_read(self.stdout, buf, buf.length())
}
///|
/// Reads data from the child process's standard error into the provided buffer.
/// Returns the number of bytes read.
///
/// Note: The child process must have been spawned with `stderr(Piped)`.
pub fn Child::read_stderr(self : Child, buf : Bytes) -> Int {
if self.stderr == 0L {
return -1
}
ffi_read(self.stderr, buf, buf.length())
}
///|
/// Waits for the child process to exit and returns its exit status.
///
/// This function will block the current thread until the child has terminated.
pub fn Child::wait(self : Child) -> ExitStatus raise ProcessError {
let status_code = ffi_wait(self.handle)
if status_code < 0 {
raise ProcessError::Unknown("Wait failed with error code: \{status_code}")
}
{ exit_code: status_code }
}
///|
/// Forces the child process to exit.
///
/// This sends a distinct kill signal (e.g., SIGKILL on Unix, TerminateProcess on Windows)
/// to the child process.
pub fn Child::kill(self : Child) -> Unit raise ProcessError {
let res = ffi_kill(self.handle)
if res != 0 {
raise ProcessError::Unknown("Kill failed with error code: \{res}")
}
}
///|
/// Executes the command as a child process, waiting for it to finish and collecting all of its output.
///
/// This will implicitly set stdout and stderr to `Piped` if they are not already configured.
pub fn Command::output(self : Command) -> Output raise ProcessError {
let cmd_clone = {
program: self.program,
args: self.args,
env_clear: self.env_clear,
env_vars: self.env_vars,
cwd: self.cwd,
stdin: Null,
stdout: Piped,
stderr: Piped
}
let child = cmd_clone.spawn()
let out_buf = Bytes::make(4096, b'\x00')
let err_buf = Bytes::make(4096, b'\x00')
// Simple blocking read for now
let stdout_res = []
if child.stdout != 0L {
for {
let n = ffi_read(child.stdout, out_buf, 4096)
if n <= 0 { break }
out_buf[:n].iter().each(stdout_res.push(_))
}
ffi_close(child.stdout) |> ignore
}
let stderr_res = []
if child.stderr != 0L {
for {
let n = ffi_read(child.stderr, err_buf, 4096)
if n <= 0 { break }
err_buf[:n].iter().each(stderr_res.push(_))
}
ffi_close(child.stderr) |> ignore
}
// Waiting for exit
let status = child.wait()
{
status,
stdout: Bytes::from_array(stdout_res),
stderr: Bytes::from_array(stderr_res),
}
}
///|
test "Command builder" {
let cmd = Command::new("moon")
.arg("version")
.arg("--help")
.env("KEY", "VALUE")
.current_dir("/tmp")
.stdout(Piped)
// White Box testing: accessing private fields
inspect(cmd.program, content="moon")
inspect(cmd.args, content="[\"version\", \"--help\"]")
inspect(cmd.cwd, content="Some(\"/tmp\")")
inspect(cmd.env_vars.get("KEY"), content="Some(\"VALUE\")")
inspect(cmd.stdout, content="Piped")
}
///|
test "Command args helper" {
let cmd = Command::new("echo").args(["hello", "world"])
inspect(cmd.args, content="[\"hello\", \"world\"]")
}
///|
test "ExitStatus" {
// Constructing private struct for internal testing
let status = ExitStatus::{ exit_code: 0 }
inspect(status.success(), content="true")
let fail = ExitStatus::{ exit_code: 1 }
inspect(fail.success(), content="false")
}