Standard library · System and tools

os/process

Running other programs, capturing their output, waiting for them and stopping them.

import os.process

The os.process package runs another program directly, with no shell in between. The program's path and its arguments are given separately, so nothing has to be quoted and spaces or special characters in an argument cause no trouble. Use it to call tools such as git, ffmpeg or a compiler with file names that came from a user. For a quick one-line shell command use os (Run, RunCapture) or os.shell; when a program cannot start at all, the reason is recorded in os.error.

A run has up to four steps: Start or StartCaptured launches the program and returns at once, Wait blocks until it exits and returns the exit code, ReadStdout and ReadStderr read what a captured run printed, and Cleanup deletes the two temporary files that held it. A captured program gets an empty standard input.

What people get wrong:

  • Treating IsRunning as a live check. It reports a flag that Start sets and only Wait, Kill or Terminate clear; it stays true after the program has exited on its own.
  • Killing without waiting. Kill and Terminate only send the signal. Call Wait afterwards, or the finished process stays in the process table as a zombie.
  • Expecting a timeout. Wait waits as long as the program runs. To give up after a while, wait in a thread and Kill from the main one.
  • Forgetting Cleanup. Each captured run leaves two files in the temporary folder until you call it.

On Windows the arguments are joined into one command line with quotes around those that contain spaces, and the ANSI CreateProcessA starts the program. An argument that itself contains a ", or text outside the system code page such as Persian, does not arrive intact there; Fork and WaitPid return -1.

Pass arguments exactly as they are

Each element of the argument vector reaches the program as one argument, byte for byte. Spaces, quotes, a $ or a ; mean nothing special, because no shell reads them. That makes this package the safe way to pass file names and other text that came from a user.

os-process-guide-args.salam
import os.process

func main:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("[%s]\n")
    args.push("two words")
    args.push("it's quoted")
    args.push("$HOME")
    args.push("*.txt; rm -rf x")
    mut p := process.StartCaptured("printf", args)
    process.Wait(p)
    print process.ReadStdout(p)
    process.Cleanup(p)
end
Output[two words] [it's quoted] [$HOME] [*.txt; rm -rf x]

The program name is looked up on the PATH like in a terminal, so printf works without a full path. The vector does not include the program name itself; the package adds it. This exactness holds on Linux and macOS; see the note above for Windows.

Exit code, output and error output

A program reports success with exit code zero and anything else on failure, and it writes its complaints to the error output. After Wait, read both: ReadStdout for the results and ReadStderr for the reason it failed. Call Cleanup after reading, because the captured output is kept in temporary files until then.

os-process-guide-exitcode.salam
import os.process

func main:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("-c")
    args.push("echo copying files; echo disk is full >&2; exit 3")
    mut p := process.StartCaptured("sh", args)
    code := process.Wait(p)
    out := process.ReadStdout(p).trim()
    err := process.ReadStderr(p).trim()
    process.Cleanup(p)
    println "output:", out
    if code != 0:
        println "failed with code", code, "and said:", err
        ret 0
    end
    println "all good"
end
Outputoutput: copying files failed with code 3 and said: disk is full

Run several programs at once

StartCaptured returns as soon as the program has started, so you can start several and wait for them afterwards. The total time is about that of the slowest one rather than the sum of all. Waiting in the order you started them keeps the output in a fixed order, even when a later job finished first.

os-process-guide-parallel.salam
import os.process

func job(name: str, seconds: str): process.Process:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("-c")
    args.push("sleep " + seconds + "; echo " + name + " done")
    ret process.StartCaptured("sh", args)
end

func main:
    mut jobs := Vector {} as Vector<process.Process>
    defer jobs.free()
    jobs.push(job("thumbnails", "0.3"))
    jobs.push(job("search index", "0.1"))
    jobs.push(job("sitemap", "0.2"))
    println "started", jobs.len(), "jobs"
    repeat jobs.len() in i:
        mut p := jobs.get(i)
        code := process.Wait(p)
        println code, process.ReadStdout(p).trim()
        process.Cleanup(p)
    end
end
Outputstarted 3 jobs 0 thumbnails done 0 search index done 0 sitemap done

Keep the number of programs you run at once reasonable; os.platform's CpuCount is a good limit for work that keeps the processor busy.

A helper that runs a program and returns its output

Most programs need the same few lines every time: start, check that it started, wait, read, clean up. Put them in one function. A defer right after the start makes sure Cleanup runs on every way out of the function, and an out-parameter tells the caller whether the program succeeded.

os-process-guide-helper.salam
import os.error
import os.process

func capture(program: str, args: Vector<str>, ok &: bool): str:
    error.Clear()
    mut p := process.StartCaptured(program, args)
    if error.HasError():
        ok = false
        ret ""
    end
    defer process.Cleanup(p)
    ok = process.Wait(p) == 0
    ret process.ReadStdout(p)
end

func main:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("-c")
    args.push("printf 'pear\\napple\\nfig\\n' | sort")
    mut ok := false
    out := capture("sh", args, ok)
    println "worked:", ok
    print out
    capture("/no/such/tool", args, ok)
    println "missing tool worked:", ok
end
Outputworked: true apple fig pear missing tool worked: false

Stop a program

Terminate sends SIGTERM, which a well-behaved program catches to save its work and exit; Kill sends SIGKILL, which cannot be caught. Either way, Wait collects the program afterwards. A program ended by a signal reports 128 plus the signal number as its exit code, so 143 means SIGTERM and 137 means SIGKILL, the same numbers a shell shows.

os-process-guide-stop.salam
import os.process

func main:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("30")
    mut worker := process.Start("sleep", args)
    println "started, running:", process.IsRunning(worker)
    if process.Terminate(worker) != 0:
        println "could not signal it"
        ret 1
    end
    code := process.Wait(worker)
    println "stopped with exit code", code, "= 128 + signal", code - 128
    println "running:", process.IsRunning(worker)
    ret 0
end
Outputstarted, running: true stopped with exit code 143 = 128 + signal 15 running: false
NeedUse
Run a program and see its output on the screenStart, then Wait
Run a program and use its outputStartCaptured, Wait, ReadStdout, Cleanup
Stop a program politely, or at onceTerminate, Kill
Pipes or redirectionrun sh with -c and a script, as in the examples above
A one-line command and its resultos.shell

Types

Process

struct Process
A process, running or finished.
NameTypeDescription
pidint = 0The process id.
handlevoid*The process handle on Windows.
runningbool = falseSet by Start and cleared by Wait, Kill and Terminate; it is not updated when the program exits on its own.
exit_codeint = 0The exit code, once Wait has returned; 128 plus the signal number when a signal ended it.
stdout_pathstr = ""The temporary file holding the captured output.
stderr_pathstr = ""The temporary file holding the captured error output.
os-process-Start.salam
import os.process

func main:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("hello from the child process")
    mut p := process.StartCaptured("/bin/echo", args)
    println process.Pid(p) > 0
    code := process.Wait(p)
    println code, process.ExitCode(p), process.IsRunning(p)
    print process.ReadStdout(p)
    println "[" + process.ReadStderr(p) + "]"
    process.Cleanup(p)
end
Outputtrue 0 0 false hello from the child process []

Functions

Start

func Start(path: str, args: Vector<str>): Process
Runs a program with its arguments and does not wait; its output goes straight to your own program's output. The name is searched on PATH. When it cannot start, pid is 0 and os.error holds ProcessFailed.

The example for this item is shown with the ones above: os-process-Start.salam

StartCaptured

func StartCaptured(path: str, args: Vector<str>): Process
Like Start, but sends the program's output and error output to two temporary files for you to read after Wait, and gives it an empty standard input.

The example for this item is shown with the ones above: os-process-Start.salam

WaitPid

func WaitPid(pid: int): int
Waits for the process with this id to finish and returns its exit code, or -1. Unix only; on Windows it returns -1.
os-process-Kill.salam
import os.process

func main:
    mut args := Vector {} as Vector<str>
    defer args.free()
    args.push("30")
    mut p := process.Start("/bin/sleep", args)
    println process.IsRunning(p)
    process.Terminate(p)
    process.Wait(p)
    println process.IsRunning(p)
    mut p2 := process.Start("/bin/sleep", args)
    process.Kill(p2)
    process.WaitPid(process.Pid(p2))
    println "both have stopped"
end
Outputtrue false both have stopped

Wait

func Wait(p &: Process): int
Waits for the process to finish and returns its exit code: 128 plus the signal number when a signal ended it, -1 when it never started. There is no timeout.

The example for this item is shown with the ones above: os-process-Start.salam

Fork

func Fork(): int
Splits the current process in two: it returns zero in the child and the child's id in the parent. On Unix systems only; its example is not run here.
os-process-Fork.salam
import os.process

func main:
    pid := process.Fork()
    if pid == 0:
        println "I am the child"
        process.ExitNow(0)
    end
    process.WaitPid(pid)
    println "the child has finished"
end

ExitNow

func ExitNow(code: int)
Ends the process immediately with no cleanup at all, for finishing a child after Fork.

The example for this item is shown with the ones above: os-process-Fork.salam

Kill

func Kill(p &: Process): int
Kills the process at once (SIGKILL); 0 when the signal was sent. Call Wait afterwards to collect it.

The example for this item is shown with the ones above: os-process-Kill.salam

Terminate

func Terminate(p &: Process): int
Asks the process to finish gracefully (SIGTERM) so it has a chance to clean up; call Wait afterwards. On Windows it is the same as Kill.

The example for this item is shown with the ones above: os-process-Kill.salam

ReadStdout

func ReadStdout(p: Process): str
Reads the captured output once the process has finished; empty for a process started with Start. The text stops at the first zero byte.

The example for this item is shown with the ones above: os-process-Start.salam

ReadStderr

func ReadStderr(p: Process): str
Reads the captured error output; empty for a process started with Start.

The example for this item is shown with the ones above: os-process-Start.salam

Cleanup

func Cleanup(p &: Process)
Removes the temporary capture files. Read the output first; call it once per StartCaptured.

The example for this item is shown with the ones above: os-process-Start.salam

ExitCode

func ExitCode(p: Process): int
The process's exit code.

The example for this item is shown with the ones above: os-process-Start.salam

Pid

func Pid(p: Process): int
The process id.

The example for this item is shown with the ones above: os-process-Start.salam

IsRunning

func IsRunning(p: Process): bool
The running flag: true from Start until Wait, Kill or Terminate. It does not ask the system, so it stays true after the program exits by itself.

The example for this item is shown with the ones above: os-process-Start.salam