Cmd

Cmd :: # (opaque)

Build and run child processes with native-safe programs, arguments, and environment values.

There are four ways to run a command:

  • [exec!] runs it on your terminal and requires it to succeed.
  • [capture!] requires it to succeed and returns what it printed, as text.
  • [run!] returns the exit status and the captured output as data.
  • [spawn!] starts it and returns a Child to wait on, read from and write to.
Cmd.new("cargo").args(["build", "--release"]).exec!()?

sha = Cmd.new("git").args(["rev-parse", "HEAD"]).capture!()?.trim()

output = Cmd.new("roc").arg("version").timeout(Duration.seconds(5)).run!()?
match output.status {
	Exited(0) => Stdout.write_bytes!(output.stdout_bytes)
	Exited(code) => Err(CommandFailed(code))
	Signaled(signal) => Err(CommandSignaled(signal))
}

Programs and arguments are passed to the operating system as they are: no shell is involved, so spaces, $FOO and *.txt stay literal.

Errors, to_str and Str.inspect never contain the value of an environment variable, so a command that carries a secret can be logged.

new : OsStr -> Cmd

Create a new command with the given program name. Execute it with [exec!], [capture!], [run!] or [spawn!].

cmd = Cmd.new("ls")

The program is an OsStr. A string literal can be passed directly, and so can a literal that interpolates: Cmd.new("${tool}").

arg : Cmd, OsStr -> Cmd

Add a single argument to the command. ❗ Shell features like variable substitution (e.g. $FOO), glob patterns (e.g. *.txt), ... are not available.

cmd = Cmd.new("ls").arg("-l")
args : Cmd, List(OsStr) -> Cmd

Add multiple arguments to the command. ❗ Shell features like variable substitution (e.g. $FOO), glob patterns (e.g. *.txt), ... are not available.

cmd = Cmd.new("ls").args(["-l", "-a"])

For a List(Str) use cmd.args(names.map(OsStr.from_str)).

env : Cmd, OsStr, OsStr -> Cmd

Add a single environment variable to the command.

cmd = Cmd.new("env").env("FOO", "bar") # add the environment variable "FOO" with value "bar"
envs : Cmd, List((OsStr, OsStr)) -> Cmd

Add multiple environment variables to the command.

cmd = Cmd.new("env").envs([("FOO", "bar"), ("BAZ", "qux")])
clear_envs : Cmd -> Cmd

Clear all environment variables before running the command. Only environment variables added via env or envs will be available. Useful if you want a clean command run that does not behave unexpectedly if the user has some env var set.

cmd =
    Cmd.new("env")
        .clear_envs()
        .env("ONLY_THIS", "visible")
cwd : Cmd, Path -> Cmd

Set the child working directory without changing the parent directory. Use an absolute executable path or a bare PATH name: resolving a relative executable against cwd is platform-specific.

stdin : Cmd, StdinMode -> Cmd

Choose the child's standard input; see StdinMode.

When this is not called, [run!] gives the child Null, and [exec!], [capture!] and [spawn!] let it inherit the parent's standard input.

stdout : Cmd, StdoutMode -> Cmd

Choose where the child's standard output goes; see StdoutMode.

When this is not called, [run!] and [capture!] capture it, and [exec!] and [spawn!] let it go to the parent's standard output.

stderr : Cmd, StderrMode -> Cmd

Choose where the child's standard error goes; see StderrMode.

When this is not called, [run!] captures it, and [exec!], [capture!] and [spawn!] let it go to the parent's standard error.

inherit_stdio : Cmd -> Cmd

Let the child use the parent's standard input, output and error, however the command is run. With [run!] this gives the exit status of a command that prints to the terminal:

status = Cmd.new("roc").args(["check", "main.roc"]).inherit_stdio().run!()?.status
timeout : Cmd, Duration -> Cmd

Stop the command when it has run for longer than duration. The deadline covers the whole run, including reading the rest of its output.

A command that reaches its deadline is killed, and the result is Err(CmdErr(TimedOut(partial))) with the output captured so far. That is also what [Child.wait!] and [Child.poll!] return for a child started with [spawn!]. When timeout is not called there is no deadline; Duration.zero expires immediately.

output_limit : Cmd, [NoLimit, MaxBytes(U64)] -> Cmd

Limit how many bytes of standard output and standard error are captured, counted together. The default is MaxBytes(16777216), 16 MiB.

A command that prints more is killed, and the result is Err(CmdErr(OutputLimitExceeded(partial))) with the output captured up to the limit. NoLimit captures whatever the command prints.

pending_limit : Cmd, U64 -> Cmd

Limit how many bytes of Pipe output may wait to be read with [Child.read!], counted over both streams (default 1 MiB). A child that gets further ahead of its reader is killed, and [Child.wait!] returns Err(CmdErr(OutputLimitExceeded(partial))).

manage_tree : Cmd, Bool -> Cmd

Also terminate descendants on cancellation, using a Unix process group or Windows Job Object. Disabled by default; descendants must not escape it.

exec! : Cmd => Try({}, [CmdErr(Err), BadExitStatus(Output)])

Run the command on the parent's terminal and require it to succeed.

Standard input, output and error are inherited unless the command sets them. Any status other than exit code 0 is Err(BadExitStatus(output)).

Cmd.new("cargo").args(["build", "--release"]).exec!()?
capture! : Cmd => Try(Str, [CmdErr(Err), BadExitStatus(Output), BadUtf8({ problem : Utf8Problem, index : U64 })])

Run the command, require it to succeed, and return its standard output as text.

Standard output is captured unless the command sets it. Standard input and standard error are inherited unless the command sets them, so warnings still reach the terminal, as with a shell's $(..). Any status other than exit code 0 is Err(BadExitStatus(output)), and output that is not UTF-8 is Err(BadUtf8(..)).

The text is returned as printed, including a final newline:

sha = Cmd.new("git").args(["rev-parse", "${branch}"]).capture!()?.trim()
run! : Cmd => Try(Output, [CmdErr(Err)])

Run the command and return its exit status and output as data. A non-zero exit code or a signal is not an error here: match on output.status, or call Output.require_success.

Standard output and standard error are captured, and standard input is Null, unless the command sets them. The result is returned once the command has exited and all of its output has been read.

output = Cmd.new("roc").args(["check", "main.roc"]).run!()?
spawn! : Cmd => Try(Child, [CmdErr(Err)])

Start the command and return a Child without waiting for it.

Standard input, output and error are inherited unless the command sets them. Set a stream to Pipe to exchange data with the child while it runs.

check_available! : Str => Bool

Report whether command can be found on the system as something runnable.

A bare name (like "git") is looked up across the PATH entries; a name containing a path separator is checked as-is. On Windows the candidate extensions come from %PATHEXT%.

On Unix this checks for an executable bit; a directory is not reported even though it carries one, though a symbolic link to a directory is a rare exception.

to_str : Cmd -> Str

The program and its arguments, separated by spaces, for messages and logs. An argument that is empty or contains a space or a quote is written as a quoted, escaped string. The environment, the working directory and the other settings are not included.

expect Cmd.new("git").args(["commit", "-m", "first draft"]).to_str() == "git commit -m \"first draft\""
to_inspect : Cmd -> Str

Customize command output for Str.inspect: the full configuration, with the names of the environment variables it sets but never their values, and the length of Bytes input but not its content.

StdinMode : [Inherit, Null, Bytes(List(U8)), Pipe]

Where the child's standard input comes from.

  • Inherit: the parent's standard input.
  • Null: an input that is already at its end.
  • Bytes(bytes): the given bytes, after which the input ends.
  • Pipe: a pipe written with [Child.write_bytes!] and ended with [Child.close_stdin!]. Only [spawn!] returns a Child, so with [exec!], [capture!] and [run!] it means Null.
StdoutMode : [Inherit, Null, Capture, Tee, Pipe]

Where the child's standard output goes.

  • Inherit: the parent's standard output.
  • Null: discarded.
  • Capture: kept in memory and returned in the Output.
  • Tee: captured and also forwarded to the parent's standard output. The child sees a pipe, not a terminal.
  • Pipe: queued as chunks for [Child.read!]. Only [spawn!] returns a Child, so with [exec!], [capture!] and [run!] it means Capture.
StderrMode : [Inherit, Null, Capture, Tee, Pipe, MergeWithStdout]

Where the child's standard error goes: the modes of StdoutMode, or MergeWithStdout, which sends it into the same OS pipe as standard output. That keeps the order in which the child wrote to the two streams, which two separate streams cannot. The merged stream follows the standard output mode and is reported as standard output.

ExitStatus : [Exited(I32), Signaled(I32)]

How a child ended: Exited(code) with its exit code, or Signaled(signal) when a signal terminated it. Signaled is reported on Unix only.

PartialOutput : { stdout_bytes : List(U8), stderr_bytes : List(U8) }

The output captured before a deadline or an output limit stopped a command.

Err

:= [StartFailed(IOErr, OsStr), TimedOut(PartialOutput), OutputLimitExceeded(PartialOutput), IOErr(IOErr)]

Why a command could not be started or did not run to completion. A non-zero exit code is not one of these: it is BadExitStatus, or the status of an Output.

  • StartFailed(err, program): the program could not be started, for example StartFailed(NotFound, program) when it does not exist.
  • TimedOut(partial): the command reached the deadline set with timeout and was killed.
  • OutputLimitExceeded(partial): the command printed more than output_limit or pending_limit allows and was killed.
  • IOErr(err): an operating-system failure while the command ran. A [Child.read!] or [Child.write_bytes!] that waited for its whole Duration is IOErr(TimedOut).
is_eq : _

Compare two errors, so results that contain one work with ==.

to_str : Err -> Str

Describe the error in a sentence, without the captured output.

to_inspect : Err -> Str

Customize error output for Str.inspect. Captured output is shown as text, with invalid UTF-8 replaced, not as a list of bytes.

Output

:= { command : Str, status : ExitStatus, stdout_bytes : List(U8), stderr_bytes : List(U8) }

The result of a command that ran to completion: how it ended and what it printed. command is the program and arguments, as to_str renders them. Output is empty for a stream that was not captured.

is_eq : _

Compare two outputs, so results that contain one work with ==.

require_success : Output -> Try(Output, [BadExitStatus(Output)])

Turn any status other than exit code 0 into Err(BadExitStatus(output)).

bytes = cmd.run!()?.require_success()?.stdout_bytes
stdout_utf8 : Output -> Try(Str, [BadUtf8({ problem : Utf8Problem, index : U64 })])

The captured standard output as text, or BadUtf8 when it is not valid UTF-8.

stdout_utf8_lossy : Output -> Str

The captured standard output as text, with invalid UTF-8 replaced by the Unicode replacement character.

stderr_utf8_lossy : Output -> Str

The captured standard error as text, with invalid UTF-8 replaced by the Unicode replacement character.

to_inspect : Output -> Str

Customize output for Str.inspect. Captured output is shown as text, with invalid UTF-8 replaced, not as a list of bytes.

Child

Cmd.Child :: # (opaque)

A running command started with [spawn!].

When the last reference to a child is released, a child that is still running is killed and reaped. Use [Child.close!] to do that at a chosen point. After close! every other operation returns Err(Closed).

pid : Child -> U32

The operating-system process identifier the child was started with.

wait! : Child => Try(Output, [CmdErr(Err), Closed])

Wait for the child to exit and for all of its output to be read, then return its status and captured output. A piped standard input is closed first, so exchange data with [write_bytes!] and [read!] before waiting. Waiting again returns the same result.

poll! : Child => Try([Running, Finished(Output)], [CmdErr(Err), Closed])

Check on the child without waiting: Running, or Finished(output) once it has exited and all of its output has been read.

kill! : Child => Try({}, [CmdErr(Err), Closed])

Ask the operating system to kill the child. This does not wait: use [wait!] to see it end. Killing a child that has exited does nothing.

close_stdin! : Child => Try({}, [CmdErr(Err), Closed])

Close a piped standard input so the child sees the end of its input. Closing it again does nothing.

close! : Child => Try({}, [CmdErr(Err)])

Kill a child that is still running, reap it, and close the handle for every reference to it. Closing again does nothing.

read! : Child, Duration => Try([Stdout(List(U8)), Stderr(List(U8))], [EndOfInput, CmdErr(Err), Closed])

Return the next chunk of Pipe output, from whichever stream has one, waiting up to wait for it. The host picks the chunk size.

Duration.zero does not wait: it returns a chunk that is ready, and otherwise times out. A wait that expires is Err(CmdErr(IOErr(TimedOut))); the child keeps running and may be read again. Once the child has exited and every chunk has been returned, the result is Err(EndOfInput).

chunk = child.read!(Duration.seconds(1))?
write_bytes! : Child, List(U8), Duration => Try({}, [CmdErr(Err), Closed])

Write bytes to a piped standard input, waiting up to wait for the child to accept all of them. Duration.zero makes one attempt that does not wait.

A wait that expires is Err(CmdErr(IOErr(TimedOut))). Part of the input may have been delivered by then, so writing all of it again can duplicate bytes. Writing when standard input is not a pipe, or after [close_stdin!], is Err(CmdErr(IOErr(BrokenPipe))).

write_utf8! : Child, Str, Duration => Try({}, [CmdErr(Err), Closed])

Write text to a piped standard input as UTF-8; see [write_bytes!].