sh
stdlib/extra/sh.hk: running a program, the way a script wants to.
This module does not parse shell strings. The name says sh and the surface refuses the thing that implies: there is no run!("ls -la"), only an argument list. Splitting a command on spaces is how injection bugs get written, and a scripting layer is where that temptation sits. The spelling is not offered at all.
extra/process is the systems API: it reports how a child finished (ProcessOutcome) so a caller can tell "exited 3" from "the program is not installed" and branch on it. That is right for a library and wrong for a script, where the second case is a bug in the script and no condition to handle. The calls here therefore crash when the program cannot be spawned, and hand back a plain Run otherwise. extra/process is unchanged and remains the surface for the cases that need to branch ("try this, fall back if absent"); this is a narrowing by design and no oversight.
Secrets go on stdin, never in the argument list
argv is world-readable: any user on the box can read another process's arguments out of ps for as long as it runs. A credential passed as an argument leaks for the life of the call, and the leak is invisible in the source, where it looks like ordinary code.
run_stdin! therefore takes the child's stdin as a parameter of its own, and it is the spelling to reach for whenever a value must not be seen. Most programs that accept a secret can read it this way; curl -K - reads its whole option set from stdin, gpg --passphrase-fd 0, and so on.
# leaks the key to every user on the box: run!("curl", ["-H", "AccessKey: #{key}", url]) # leaves it out of the process table: run_stdin!("curl", ["-K", "-", url], "header = \"AccessKey: #{key}\"\n")
Delegates straight to the sys native seam, like every other extra module - none of them wraps another, and a scripting layer that wrapped extra/process in place of sitting beside it would drift from what it wrapped.
Run
struct Run
cmd: string
out: string
err: string
code: i32
end
A finished subprocess: what was run, what it wrote, and how it ended.
code is the exit status, and 0 is success. There is no spawn-failure case to represent: a program that could not start crashed the run before this value existed.
impl Run
ok?
prop ok?(self) -> bool
Whether the program succeeded.
Run(cmd="true", out="", err="", code=0i32).ok? => true
Run(cmd="false", out="", err="", code=1i32).ok? => false
text
prop text(self) -> string
Stdout with surrounding whitespace removed: what a script means when it asks a program for "the answer", since almost every one of them ends its output with a newline.
Run(cmd="echo", out="hi\n", err="", code=0i32).text => "hi"
lines
prop lines(self) -> List<string>
Stdout as a list of lines, trimmed first so a trailing newline does not produce a final empty entry.
Run(cmd="ls", out="a\nb\n", err="", code=0i32).lines.length => 2
run!
def run!(cmd: string, args: List<string>) -> Run [process, Crash]
Run cmd with args and no stdin, returning what it did.
Crashes if the program cannot be started; see the module note. Use run_stdin! when the child needs input, and reach for extra/process when a missing program is a case to handle and no bug.
@no-doctest: runs a real subprocess, which a doctest cannot depend on
run_stdin!
def run_stdin!(cmd: string, args: List<string>, stdin_input: string) -> Run [process, Crash]
run! with stdin_input written to the child's standard input.
The spelling for a secret: a value passed here never reaches argv, where any user on the box could read it out of ps. See the module note.
@no-doctest: runs a real subprocess, which a doctest cannot depend on
capture!
def capture!(cmd: string, args: List<string>) -> string [process, Crash]
Run cmd, demand success, and return its trimmed stdout.
What most script lines want: "run this, give me the answer, and stop if it failed". A non-zero exit crashes with the program's own stderr, which is the message a human needs to see anyway.
@no-doctest: runs a real subprocess, which a doctest cannot depend on