Reference

std/process/lib

std/process/src/lib.trb

The process itself, and the child processes it starts: Process for arguments and exiting, Process.run for a program run to its end, Child for a running program's pipes, and ProcessOutput for what a finished one left behind.

A child process's three pipes are the same Source and Sink a file or a socket has, so a pipeline between two programs is first.output().into(second.input()).

type ProcessOutput

type ProcessOutput

What a program run with Process.run or Process.runBlocking left behind.

field exitCode

exitCode: Int

0 for success, anything else for the code the program exited with.

field standardOutput

standardOutput: String

Everything the program wrote to its standard output.

field standardError

standardError: String

Everything the program wrote to its standard error, kept apart from ProcessOutput.standardOutput.

fn isSuccess

fn isSuccess(): Bool

The usual question about a child process: did it do what it was asked.

type Child

shared type Child with Close

A child process that is still running, started with Process.start. Its three pipes are the same Source and Sink a file or a socket has, so a pipeline between two programs is first.output().into(second.input()).

Examples

fn sortTwo(): Task<Result<Void, IoError>> {
  using child = Process.start("sort", ["-u"])?
  var input = child.input()
  input.addLine("banana").await()?
  input.addLine("apple").await()?
  input.end().await()?
  const chunks = child.output().toList().await()?
  const code = child.wait().await()?
  print "{chunks.length()} chunks, exit code {code}"
  Ok void
}

Every line is .await()?: the ? is the failure of the pipe, and a cancellation needs nothing - it stops the task at the await() it meets, and the using closes the child's pipes on the way out. A program whose whole input is known up front needs no pipe at all: Process.run feeds it.

Pitfalls

Child.close releases the pipes and stops waiting for the child; it does not kill it, because ending somebody else's program is a decision and not a cleanup.

fn of

static fn of(handle: Int, program: String): Child

A child over a handle the runtime answered.

fn input

fn input(): Sink<Bytes, IoError>

What the child reads. end() closes its standard input, which is how most filters learn that they are done.

fn output

fn output(): Source<Bytes, IoError>

What the child writes to standard output.

fn errors

fn errors(): Source<Bytes, IoError>

What the child writes to standard error, kept apart from Child.output.

fn wait

fn wait(): Task<Result<Int, IoError>>

The exit code, once the program has ended; the child is waited for on a thread of the blocking pool.

fn close

var fn close()

Releases the three pipes and stops waiting for the child; see the type's own pitfall about killing it.

fn read

fn read(which: Int, maximum: Int): Task<Result<Bytes?, IoError>>

At most maximum bytes of one of its two outputs: None at its end.

fn write

fn write(bytes: Bytes): Task<Result<Void, IoError>>

All of bytes into its input.

fn endInput

fn endInput(): Result<Void, IoError>

Closes its input: the child reads the end of it.

type Process

native type Process

The running program: its command line, how it exits, and the child processes it starts.

fn arguments

native static fn arguments(): List<String>

The command line arguments, without the program itself.

fn exit

native static fn exit(code: Int): Never

Ends the program. Open resources are not closed: prefer to return from the entry file.

fn executablePath

native static fn executablePath(): String?

The absolute path of the running program's own executable, with / between its parts, or None where the operating system does not say. It is native because only the operating system knows it, and it is how a program finds the files it was installed beside - torb finds its std/ and runtime/ this way.

Examples

if const Some(path) = Process.executablePath() {
  print path
}

Pitfalls

Windows and Linux answer; an operating system without a way to ask (macOS, a BSD without /proc) answers None rather than a guess from the command line, which names whatever the caller typed.

fn run

static fn run(program: String, arguments: List<String>, input: String = ""): Task<Result<ProcessOutput, IoError>>

Runs a program to its end as a task: feeds it input as the whole of its standard input, and collects its exit code and the two streams it wrote into a ProcessOutput. The arguments are passed as they are: nothing about them is interpreted and nothing has to be quoted.

fn sorted(): Task<Result<String, IoError>> {
  const output = Process.run("sort", [], input: "banana\napple\n").await()?
  Ok output.standardOutput
}

A program that could not be started at all is an IoError; a program that ran and failed is an exit code, which is how a caller tells "there is no such program" from "the program said no".

Pitfalls

  • The input ends after input, and an empty one ends at once: the program never reads the terminal this program was started from, so a program that would ask a question gets the end of its input instead of hanging.
  • The two streams arrive apart, so the order in which the program interleaved them is lost: a line of ProcessOutput.standardError cannot be placed between two lines of ProcessOutput.standardOutput. A caller that needs the output while it arrives reaches for Process.start instead, which hands out real pipes.
  • The wait for the program runs on the blocking pool where the arguments may move there (offload), and blocks the worker that asked where they may not. Cancelling the task does not stop the program: it runs to its end, and what it wrote is thrown away.

Related

fn runBlocking

static fn runBlocking(program: String, arguments: List<String>): Result<ProcessOutput, IoError>

Runs a program to its end and collects everything about it, blocking the thread that asks - the form for code that is not a task, such as a driver that runs the C compiler. The child reads this program's own standard input. The arguments are passed as they are, like Process.run's.

A program that could not be started at all is an IoError; a program that ran and failed is an exit code, which is why torb build can tell "there is no C compiler" from "the C compiler said no".

Pitfalls

  • Inside a task it stalls every other task of the worker that runs it until the program ends: a task writes Process.run(program, arguments).await()?.
  • The two streams arrive apart, as they do for Process.run.

fn runFeeding

native static fn runFeeding(command: String, arguments: ArrayList<String>, input: String, var output: String, var failure: String): Int

Process.runCollecting with input as all the child reads: the one shape a runtime function can have for Process.run, for the same reasons.

fn runCollecting

native static fn runCollecting(command: String, arguments: ArrayList<String>, var output: String, var failure: String): Int

The exit code of the program, with what it wrote to standard output in output and what it wrote to standard error in failure - or -1 where it could not be started at all, and then failure says why. The one shape a runtime function can have for this: a Result and a ProcessOutput are types of the program, and starting a process is the operating system.

fn runPassingThrough

static fn runPassingThrough(program: String, arguments: List<String>): Result<Int, IoError>

Runs a program to its end with this program's own three streams, and answers its exit code. The arguments are passed as they are, like Process.run's.

This is what a driver runs a program with: nothing is collected, so what the child writes appears where this program's output appears while it writes it, its two streams stay apart and in their order, and a program that reads standard input reads the one the user is typing into. Process.run answers what a program wrote, which is the other question and needs a pipe for it.

A program that could not be started at all is an IoError; a program that ran and failed is an exit code.

Examples

const code = Process.runPassingThrough("git", ["status", "--short"])?
Process.exit code

Errors

  • The program is not on PATH, or it is not a program at all.

Related

fn runInheriting

native static fn runInheriting(command: String, arguments: ArrayList<String>, var failure: String): Int

The exit code of the program, or -1 where it could not be started at all, and then failure says why. The same shape Process.runCollecting has, without the output: there is none to answer with.

fn start

static fn start(program: String, arguments: List<String>): Result<Child, IoError>

Starts a program and hands back its pipes, for output that is too big to collect or that has to be read while it arrives. Process.run is the short form for everything else. The program is started through no shell, and a program that cannot be started at all is the failure.