os/shell
Running a command and getting its exit code, its output and its errors together.
import os.shell
The os.shell package runs a command, waits for it and gives back the whole result at once in a CmdResult: the exit code, the standard output and the error output. It is a thin layer over os.process (StartCaptured, Wait, read, Cleanup) for the common case where you want all three and do not need to run anything in parallel. Compared with os.RunCapture in os, it also captures the error output and, through Run, passes arguments without a shell.
The three functions differ in who reads the command:
| Function | Shell | Arguments | Result |
|---|---|---|---|
Run(program, args) | none | a vector, passed exactly | CmdResult; code -1 when the program cannot start |
Exec(command) | none | the string split on single spaces | CmdResult |
System(command) | /bin/sh -c, or cmd on Windows | the shell parses them | exit code only; output goes to the screen |
Exec splits the command on spaces alone and no shell is involved: pipes (|), redirection (>), quoting and tabs do not work. For those, call sh -c through Run, as the first example does.
Every call waits for the command to finish, with no timeout, and keeps the whole output in memory. The command gets an empty standard input, so a tool that asks a question reads end of file.
Pipes and redirection through sh
For a pipeline, hand the whole script to sh -c. Wrapping that in a small function keeps the call sites short, and the result still has all three parts: the code of the last command, the output and the error output.
import os.shell
func sh(script: str): shell.CmdResult:
mut args := Vector {} as Vector<str>
defer args.free()
args.push("-c")
args.push(script)
ret shell.Run("sh", args)
end
func main:
r := sh("printf 'red\\nblue\\nred\\ngreen\\nred\\n' | sort | uniq -c | sort -rn | head -n 1")
if r.code != 0:
println "the pipeline failed:", r.err
ret 0
end
println "most common:", r.out.trim()
r2 := sh("echo saved > notes.txt && cat notes.txt && rm notes.txt")
println r2.code, r2.out.trim()
endOnly put text you wrote yourself into such a script. Values from users belong in separate arguments of Run, where no shell can read them.
Work with the output line by line
Most tools print one item per line. Trim the output, split it on newlines and handle each line. The program below lists the current directory with ls and picks out the CSV files.
import os
import os.shell
func main:
os.WriteFile("a.csv", "1,2\n")
os.WriteFile("b.csv", "3,4\n")
os.WriteFile("notes.txt", "hi\n")
r := shell.Exec("ls")
mut csv := 0
each name in r.out.trim().split("\n"):
if name.ends_with(".csv"):
println "found", name
csv += 1
end
end
println csv, "csv files"
os.Remove("a.csv")
os.Remove("b.csv")
os.Remove("notes.txt")
endTo list files in a real program, prefer fs.dir or os: they need no external program and they work the same on Windows. Running a tool is the right choice when the tool does the real work, such as git or ffmpeg.
Check whether a command worked
A command that fails returns a non-zero code and usually says why in the error output. Check code first; err is for the message you show or log. A program that cannot be found at all comes back with code -1 instead of stopping your program, and the reason is in os.error.
import os.shell
func attempt(command: str):
r := shell.Exec(command)
if r.code == 0:
println "ok:", command
ret
end
println "failed:", command, "with code", r.code
println " has an error message:", r.err.len() > 0
end
func main:
attempt("ls .")
attempt("ls no-such-folder")
println "exit code of false:", shell.System("false")
endThe exact codes and messages belong to each tool and differ between systems: ls returns 2 for a missing folder on Linux and 1 on macOS. Test for zero or non-zero unless the tool documents its codes.
Exec does not understand quotes
Exec only splits on spaces, so quotes stay in the text and a quoted phrase still becomes two arguments. When an argument contains a space, use Run with the arguments in a vector, or let sh -c read the quotes.
import os.shell
func main:
r := shell.Exec("echo 'two words'")
println "Exec:", r.out.trim()
mut args := Vector {} as Vector<str>
defer args.free()
args.push("two words")
r2 := shell.Run("echo", args)
println "Run: ", r2.out.trim()
mut script := Vector {} as Vector<str>
defer script.free()
script.push("-c")
script.push("echo 'two words'")
r3 := shell.Run("sh", script)
println "sh -c:", r3.out.trim()
end| Need | Use |
|---|---|
| A simple command with no quotes or special characters | Exec |
| Arguments with spaces or text from users | Run with a vector |
Pipes, redirection, && | Run("sh", ...) with -c |
| Only the exit code, output straight to the screen | System |
| Start without waiting, or stop a program | os.process |
Types
CmdResult
struct CmdResult| Name | Type | Description |
|---|---|---|
code | int = 0 | The exit code; zero means success, 128 plus the signal number when a signal ended it, and -1 when the program could not start. |
out | str = "" | The standard output, newlines included; it stops at the first zero byte. |
err | str = "" | The error output. |
import os.shell
func main:
mut args := Vector {} as Vector<str>
defer args.free()
args.push("-n")
args.push("hello")
r := shell.Run("echo", args)
println r.code, "[" + r.out + "]"
r2 := shell.Exec("echo one two three")
println r2.code, "[" + r2.out + "]"
mut args2 := Vector {} as Vector<str>
defer args2.free()
args2.push("-c")
args2.push("echo oops >&2; exit 3")
r3 := shell.Run("sh", args2)
println r3.code, "[" + r3.err + "]"
println shell.System("true"), shell.System("false") != 0
endFunctions
Run
func Run(command: str, args: Vector<str>): CmdResultPATH), waits for it to finish and returns the code, the output and the error output. Code -1 when it cannot start.The example for this item is shown with the ones above: os-shell-Run.salam
Exec
func Exec(command: str): CmdResultRun, with no shell. Quotes are kept as ordinary characters.The example for this item is shown with the ones above: os-shell-Run.salam
System
func System(command: str): int/bin/sh -c, or cmd on Windows) and returns only the exit code; the output is printed directly. 127 when the shell cannot start.The example for this item is shown with the ones above: os-shell-Run.salam