Standard library · System and tools

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:

FunctionShellArgumentsResult
Run(program, args)nonea vector, passed exactlyCmdResult; code -1 when the program cannot start
Exec(command)nonethe string split on single spacesCmdResult
System(command)/bin/sh -c, or cmd on Windowsthe shell parses themexit 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.

os-shell-guide-pipe.salam
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()
end
Outputmost common: 3 red 0 saved

Only 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.

os-shell-guide-lines.salam
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")
end
Outputfound a.csv found b.csv 2 csv files

To 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.

os-shell-guide-check.salam
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")
end
Outputok: ls . failed: ls no-such-folder with code 2 has an error message: true exit code of false: 1

The 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.

os-shell-guide-quote.salam
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
OutputExec: 'two words' Run: two words sh -c: two words
NeedUse
A simple command with no quotes or special charactersExec
Arguments with spaces or text from usersRun with a vector
Pipes, redirection, &&Run("sh", ...) with -c
Only the exit code, output straight to the screenSystem
Start without waiting, or stop a programos.process

Types

CmdResult

struct CmdResult
The result of a command.
NameTypeDescription
codeint = 0The exit code; zero means success, 128 plus the signal number when a signal ended it, and -1 when the program could not start.
outstr = ""The standard output, newlines included; it stops at the first zero byte.
errstr = ""The error output.
os-shell-Run.salam
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
end
Output0 [hello] 0 [one two three ] 3 [oops ] 0 true

Functions

Run

func Run(command: str, args: Vector<str>): CmdResult
Runs a program with its arguments given separately (no shell, the name searched on PATH), 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): CmdResult
Splits a command string on single spaces into a program and its arguments and runs it with Run, 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
Runs a command in the real shell (/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