///|
#cfg(all(target="native", not(platform="windows")))
async test "simple command execution" {
let result = @process.run("echo", ["Hello, World!"], stdout=Collect)
inspect(result.exit_code, content="0")
inspect(result.stdout_text().has_prefix("Hello"), content="true")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "command with exit code" {
let result = @process.run("sh", ["-c", "exit 42"])
inspect(result.exit_code, content="42")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "spawn command in background" {
@async.with_task_group(group => {
let process = @process.spawn(group, "sh", ["-c", "sleep 0.5; exit 42"])
// do other stuff while the process is running in the background
@async.sleep(250)
// use `.wait()` to wait for the child process, or `.try_wait()` to peek its status
inspect(process.wait(), content="42")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect stdout" {
let result = @process.run("printf", ["test output"], stdout=Collect)
inspect(result.exit_code, content="0")
inspect(result.stdout_text(), content="test output")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "collect stdout with args" {
let result = @process.run(
"sh",
["-c", "echo 'line 1'; echo 'line 2'"],
stdout=Collect,
)
let output = result.stdout_text()
inspect(result.exit_code, content="0")
inspect(output.contains("line 1"), content="true")
inspect(output.contains("line 2"), content="true")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect stderr" {
let result = @process.run(
"sh",
["-c", "printf 'error message' >&2"],
stderr=Collect,
)
inspect(result.exit_code, content="0")
inspect(result.stderr_text(), content="error message")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect both streams" {
let result = @process.run(
"sh",
["-c", "printf 'out msg'; printf 'err msg' >&2"],
stdout=Collect,
stderr=Collect,
)
inspect(result.exit_code, content="0")
inspect(result.stdout_text(), content="out msg")
inspect(result.stderr_text(), content="err msg")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect merged output" {
let result = @process.run(
"sh",
["-c", "printf 'ab'; printf 'cd' >&2; printf 'ef'"],
stdout=Collect,
stderr=DuplicateStdout,
)
inspect(result.exit_code, content="0")
inspect(result.stdout_text(), content="abcdef")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "read from process with pipe" {
@async.with_task_group(group => {
let child = @process.spawn(
group,
"echo",
["Hello from process"],
stdout=Collect,
)
defer child.stdout.close()
let output = child.stdout.text.read_all()
inspect(output.has_prefix("Hello"), content="true")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "write to process with pipe" {
@async.with_task_group(group => {
let child = @process.spawn(
group,
"cat",
["-"],
stdin=FeedFromParent,
stdout=Collect,
)
group.spawn_bg(() => {
defer child.stdin.close()
child.stdin.text.write("test input\n")
})
group.spawn_bg(() => {
defer child.stdout.close()
let output = child.stdout.text.read_all()
inspect(output.contains("test input"), content="true")
})
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "empty input and discarded output" {
let result = @process.run("cat", [], stdin=Empty, stdout=Discard)
inspect(result.exit_code, content="0")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "redirect input from file" {
@async.with_task_group(root => {
let input_file = "_build/process_test_input.txt"
@fs.write_text(input_file, "file content")
root.add_defer(() => @fs.remove(input_file))
let result = @process.run(
"cat",
[],
stdin=FromFile(input_file),
stdout=Collect,
)
inspect(result.exit_code, content="0")
inspect(result.stdout_text(), content="file content")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "redirect output to file" {
@async.with_task_group(root => {
let output_file = "_build/process_test_output.txt"
root.add_defer(() => @fs.remove(output_file))
let result = @process.run(
"echo",
["test output"],
stdout=ToFile(output_file),
)
inspect(result.exit_code, content="0")
let content = @fs.read_text(output_file)
inspect(content.has_prefix("test output"), content="true")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "file to file redirection" {
@async.with_task_group(root => {
let input_file = "_build/process_redirect_in.txt"
let output_file = "_build/process_redirect_out.txt"
@fs.write_text(input_file, "redirect test")
root.add_defer(() => @fs.remove(input_file))
root.add_defer(() => @fs.remove(output_file))
let _ = @process.run(
"cat",
[],
stdin=FromFile(input_file),
stdout=ToFile(output_file),
)
inspect(@fs.read_text(output_file), content="redirect test")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "redirect multiple processes to one file" {
let output_file = "_build/process_shared_output.txt"
defer @fs.remove(output_file)
let output = @process.redirect_to_file(output_file)
{
defer output.close()
@async.with_task_group(group => {
@process.spawn(group, "printf", ["first"], stdout=Shared(output))
|> ignore
@process.spawn(group, "printf", ["second"], stdout=Shared(output))
|> ignore
})
}
let content = @fs.read_text(output_file)
inspect(content.contains("first"), content="true")
inspect(content.contains("second"), content="true")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "set environment variable" {
let result = @process.run(
"sh",
["-c", "echo $MY_VAR"],
extra_env={ "MY_VAR": "my_value" },
stdout=Collect,
)
inspect(result.exit_code, content="0")
inspect(result.stdout_text().trim(), content="my_value")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "multiple environment variables" {
let result = @process.run(
"sh",
["-c", "echo $VAR1-$VAR2"],
extra_env={ "VAR1": "first", "VAR2": "second" },
stdout=Collect,
)
inspect(result.exit_code, content="0")
inspect(result.stdout_text().trim(), content="first-second")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "isolated environment" {
let result = @process.run(
"env",
[],
extra_env={ "ONLY_VAR": "only_value" },
inherit_env=false,
stdout=Collect,
)
let output = result.stdout_text()
inspect(result.exit_code, content="0")
inspect(output.contains("ONLY_VAR=only_value"), content="true")
// Parent environment variables won't be present
inspect(output.contains("PATH="), content="false")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "set working directory" {
let result = @process.run("pwd", [], cwd="async", stdout=Collect)
inspect(result.exit_code, content="0")
let text = result.stdout_text().trim()
// Check that the path ends with "src" (works across OSes)
inspect(text.has_suffix("async"), content="true")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "relative path in cwd" {
let result = @process.run("ls", [], cwd="async", stdout=Collect)
inspect(result.exit_code, content="0")
let text = result.stdout_text()
// Should list contents of src directory
let has_content = text.length() > 0
inspect(has_content, content="true")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "spawn and wait" {
@async.with_task_group(group => {
let child = @process.spawn(group, "sleep", ["0.1"])
inspect(child.pid > 0, content="true")
inspect(child.try_wait() is None, content="true")
inspect(child.wait(), content="0")
})
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "wait for specific exit code" {
let result = @process.run("sh", ["-c", "exit 5"])
inspect(result.exit_code, content="5")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "spawn orphan and wait later" {
let proc = @process.spawn_orphan("sh", ["-c", "sleep 0.1; exit 7"])
// Unlike `@process.spawn`,
// the handle returned by `@process.spawn_orphan` must be closed manually
defer proc.close()
// Do other work...
@async.sleep(50)
// Wait for the process to complete
let exit_code = proc.wait()
inspect(exit_code, content="7")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "merge stdout and stderr" {
@async.with_task_group(group => {
let child = @process.spawn(
group,
"sh",
["-c", "echo 'to stdout'; echo 'to stderr' >&2"],
stdout=Collect,
stderr=DuplicateStdout,
)
defer child.stdout.close()
let output = child.stdout.text.read_all()
inspect(output.contains("to stdout"), content="true")
inspect(output.contains("to stderr"), content="true")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "multiple processes to one pipe" {
@async.with_task_group(root => {
let (reader, writer) = @process.read_from_process()
root.spawn_bg(no_wait=true, () => {
defer reader.close()
let output = reader.text.read_all()
inspect(output.contains("first"), content="true")
inspect(output.contains("second"), content="true")
})
@async.with_task_group(group => {
// Close the shared child-side handle after spawning the last child.
defer writer.close()
@process.spawn(group, "echo", ["first"], stdout=Shared(writer)) |> ignore
@process.spawn(group, "echo", ["second"], stdout=Shared(writer)) |> ignore
})
})
}| API or value | Redirection role | Ownership and closing |
|---|---|---|
| Inherit | Inherit the corresponding stream from the current execution context | No caller-owned handle |
| Stdin, Stdout, Stderr | Select a parent-process standard stream explicitly | No caller-owned handle |
| FeedFromParent | Feed one child's stdin through Process.stdin | Close Process.stdin after writing |
| Empty | Give the child an empty input stream that immediately reaches EOF | Managed automatically |
| Collect | Collect one output stream through a process handle or ProcessResult | With spawn(), close Process.stdout or .stderr after reading |
| Discard | Ignore everything written to an output stream | Managed automatically |
| FromFile(path) | Read one child's stdin from a file | Managed automatically |
| ToFile(path), AppendFile(path) | Write one child's output to a file | Managed automatically |
| DuplicateStdout | Send a child's stderr to its stdout destination | No separate handle; valid only as stderr |
| Shared(endpoint) | Use a caller-owned input or output endpoint | Close the endpoint after passing it to its last child |
| write_to_process() | SharedProcessInput plus parent writer | Caller closes both ends |
| read_from_process() | Parent reader plus SharedProcessOutput | Caller closes both ends |
| pipe() | SharedProcessInput plus SharedProcessOutput | Caller closes both ends |
| redirect_from_file(path) | Shared file-backed input endpoint | Caller closes it after its last use |
| redirect_to_file(path, ...) | Shared file-backed output endpoint | Caller closes it after its last use |
///|
#cfg(any(target="native", target="wasm"))
async test "handle process errors" {
// Non-existent command fails
@test.assert_raise_async(() => @process.run("nonexistent_command", []))
}
///|
#cfg(any(target="native", target="wasm"))
async test "exit code indicates failure" {
let result = if @async.platform is Windows {
@process.run("cmd.exe", ["/c", "exit", "1"])
} else {
@process.run("sh", ["-c", "exit 1"])
}
let is_failure = result.exit_code != 0
inspect(is_failure, content="true")
}pub(all) struct CancellationHandler(async (Int) -> Unit)pub struct OrphanProcess {
pid : Int
stdin : WriteToProcess
stdout : ReadFromProcess
stderr : ReadFromProcess
// private fields
}pub struct Process {
pid : Int
stdin : WriteToProcess
stdout : ReadFromProcess
stderr : ReadFromProcess
// private fields
}pub(all) enum ProcessInput {
Inherit
Stdin
FeedFromParent
Empty
FromFile(StringView)
Shared(SharedProcessInput)
}pub(all) enum ProcessOutput {
Inherit
Stdout
Stderr
Collect
Discard
ToFile(StringView)
AppendFile(StringView)
DuplicateStdout
Shared(SharedProcessOutput)
}pub struct ProcessResult {
exit_code : Int
// private fields
}impl BinaryReader for ReadFromProcessasync fn ReadFromProcess::peek_until(self : ReadFromProcess, pattern : BytesView, include_delimiter? : Bool) -> BytesView?async fn ReadFromProcess::read_into(self : ReadFromProcess, dst : FixedArray[Byte], offset? : Int, max_len? : Int) -> Intasync fn ReadFromProcess::read_until(self : ReadFromProcess, pattern : BytesView, include_delimiter? : Bool) -> Bytes?type SharedProcessInputtype SharedProcessOutputimpl BinaryWriter for WriteToProcessasync fn redirect_to_file(path : String, append? : Bool, create_mode? : CreateMode, permission? : Int) -> SharedProcessOutputasync fn run(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : ProcessInput, stdout? : ProcessOutput, stderr? : ProcessOutput, cwd? : StringView, no_console_window? : Bool, cancel_handler? : CancellationHandler) -> ProcessResultasync fn spawn(group : TaskGroup, cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : ProcessInput, stdout? : ProcessOutput, stderr? : ProcessOutput, cwd? : StringView, no_console_window? : Bool, cancel_handler? : CancellationHandler, no_wait? : Bool) -> Processasync fn spawn_orphan(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : ProcessInput, stdout? : ProcessOutput, stderr? : ProcessOutput, cwd? : StringView, no_console_window? : Bool) -> OrphanProcessasync fn wait_pid(pid : Int) -> IntInstall
Installed by default