process

tape
import process from "process";
import io from "io";
import os from "os";
import time from "time";

// Build app.tape with the tape that runs this script, logging to a file
var build: process.Command = process.command(os.env("TAPE")!);
build.args(["build", "app.tape", "-o", "app"]);
build.stdout_to("build.log");
build.stderr_to_stdout();
let s: process.Status = build.run() or { return 1; };
if (!s.success()) { io.println("build {s.describe()}"); return 1; }

// Run something that may hang: give it 60 seconds
var vm: process.Command = process.command("qemu-system-x86_64");
vm.args(["-nographic", "-kernel", "kernel.elf"]);
vm.stdout_to("serial.log");
var child: process.Child = vm.spawn() or { return 1; };
let done: ?process.Status = child.wait_timeout(time.seconds(60)) or { return 1; };
if (done == null) {
    child.kill() or {};
    child.wait() or {};
    io.println("timed out");
}

process starts other programs and waits for them. It is a capability module. There is no shell: the program gets exactly the arguments you give it, and nothing is split, quoted or expanded on the way.

Commands

A Command describes what to start. Make one with command, then add to it:

FunctionDoes
command(program) -> CommandA command for program: a path, or a bare name looked up on PATH
c.arg(a)Append one argument
c.args(list)Append several (c.args(["-o", "out"]))
c.env(name, value)Set an environment variable for the child only
c.cwd(dir)The child’s working directory
c.stdin_from(path) / c.stdin_null()Read stdin from a file / from nothing
c.stdin_piped()Read stdin from what this program writes to child.stdin()
c.stdout_to(path) / c.stdout_append(path) / c.stdout_null()Send stdout to a file (emptied first), to the end of a file, or nowhere
c.stderr_to(path) / c.stderr_append(path) / c.stderr_null()The same for stderr
c.stderr_to_stdout()stderr goes where stdout goes, like 2>&1
c.detached()Share nothing of this program’s console (below)

Every stream is inherited unless you say otherwise, so by default the child prints to the same console. The child gets this program’s environment plus your env settings, which also apply to the PATH that program is looked up in.

A detached child gets nothing of this program’s console or terminal. On Windows it opens a console window of its own: when no stream is redirected its streams are that console’s, otherwise the ones not redirected go nowhere. On Linux and macOS it starts a session of its own, and the streams not redirected go nowhere.

A relative program path (./tool, bin/tool) is relative to this program’s working directory even when cwd gives the child another one.

The builder functions don’t fail. Text containing a NUL byte, or an environment name that is empty or contains =, makes spawn report InvalidInput.

Starting and waiting

FunctionDoes
c.spawn() -> Child or io.IoErrorStart it and return at once
c.run() -> Status or io.IoErrorStart it and wait for it
run(program, args) -> Status or io.IoErrorThe same, everything inherited
child.wait() -> Status or io.IoErrorWait until it ends
child.wait_timeout(d) -> ?Status or io.IoErrorWait at most d; null if it’s still running
child.try_wait() -> ?Status or io.IoErrorCheck without waiting
child.kill() -> void or io.IoErrorEnd it now
child.close()Stop tracking it without waiting
child.stdin() -> io.WriterWrite to its stdin (stdin_piped)
child.close_stdin()Close its stdin: it reads the end of its input
child.pid() -> i64Its process id

spawn fails with NotFound when there is no such program (or no such cwd or stdin_from file), PermissionDenied when it can’t be run, NotADirectory when cwd is a file, and the errors of opening a redirection file.

Once wait (or wait_timeout, try_wait) has seen the child end, the status is kept and the child’s resources are released: waiting again returns the same status. A child you start must be waited for or closed. close leaves it running.

With stdin_piped, what you write to child.stdin() is the child’s input; close_stdin() ends it. wait() and close() close it first, so a child that reads to the end of its input doesn’t wait for you. A write fails with BrokenPipe once the child has stopped reading:

tape
var c = process.command("ffmpeg");
c.args(["-f", "gdigrab", "-i", "desktop", "out.mp4"]);
c.stdin_piped();
var rec: process.Child = c.spawn() or return;
// ... later: ask ffmpeg to stop, then wait for it
rec.stdin().write_all("q") or {};
let s: process.Status = rec.wait() or return;

kill doesn’t wait: call wait afterwards for the status. Killing a child that has already ended does nothing.

Status

tape
pub tagged Status {
    Exited { code: i64; }
    Signaled { signal: i64; }
}
FunctionReturns
s.success() -> boolExited with code 0
s.code() -> ?i64The exit code, null when a signal ended the child
s.describe() -> stringexited with code 3, killed by signal 9
tape
match (s) {
    process.Status.Exited { code } => { io.println("exit code {code}"); }
    process.Status.Signaled { signal } => { io.println("signal {signal}"); }
}

Exit codes are 0-255 on Linux and macOS and 32 bits on Windows. Signaled happens only on Linux and macOS (kill gives signal 9); on Windows a killed child has exited with code 1.

Platforms

  • Windows takes one command line rather than a list of arguments. process quotes each argument so that a program reading its command line the way the C runtime does (C and C++ programs, tape programs, most others) gets it back exactly. Batch files (.bat, .cmd) are refused with InvalidInput, because cmd.exe reads their arguments by other rules; run cmd.exe yourself if you need one. A bare program name is searched in tape’s (or your program’s) directory, the current directory, the system directories and then PATH, with .exe added. That is Windows’s own search, and it honours Windows’s switch: when the environment variable NoDefaultCurrentDirectoryInExePath is set, the current directory is skipped, as cmd.exe skips it, and a program in the current directory is run as ./name.
  • Linux and macOS pass the arguments as they are and search PATH. wait_timeout notices the child’s end at once on Linux 5.3 and newer; on macOS (and older Linux) it checks every few milliseconds, so it can return up to 10 ms late.
  • Uusi has no processes: everything reports Unsupported.

Starting tape itself

tape run and tape test put the path of the running tape binary in the environment variable TAPE, so scripts and tests start the same compiler with process.command(os.env("TAPE")!). Capturing output works through files: send it to a file with stdout_to, then read the file with fs.read_file.

Last modified: