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
IsRunningas a live check. It reports a flag thatStartsets and onlyWait,KillorTerminateclear; it stays true after the program has exited on its own. - Killing without waiting.
KillandTerminateonly send the signal. CallWaitafterwards, or the finished process stays in the process table as a zombie. - Expecting a timeout.
Waitwaits as long as the program runs. To give up after a while, wait in a thread andKillfrom 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.
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)
endThe 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.
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"
endRun 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.
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
endKeep 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.
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
endStop 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.
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| Need | Use |
|---|---|
| Run a program and see its output on the screen | Start, then Wait |
| Run a program and use its output | StartCaptured, Wait, ReadStdout, Cleanup |
| Stop a program politely, or at once | Terminate, Kill |
| Pipes or redirection | run sh with -c and a script, as in the examples above |
| A one-line command and its result | os.shell |
Types
Process
struct Process| Name | Type | Description |
|---|---|---|
pid | int = 0 | The process id. |
handle | void* | The process handle on Windows. |
running | bool = false | Set by Start and cleared by Wait, Kill and Terminate; it is not updated when the program exits on its own. |
exit_code | int = 0 | The exit code, once Wait has returned; 128 plus the signal number when a signal ended it. |
stdout_path | str = "" | The temporary file holding the captured output. |
stderr_path | str = "" | The temporary file holding the captured error output. |
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)
endFunctions
Start
func Start(path: str, args: Vector<str>): ProcessPATH. 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>): ProcessStart, 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): intimport 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"
endWait
func Wait(p &: Process): intThe example for this item is shown with the ones above: os-process-Start.salam
Fork
func Fork(): intimport 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"
endExitNow
func ExitNow(code: int)Fork.The example for this item is shown with the ones above: os-process-Fork.salam
Kill
func Kill(p &: Process): intWait afterwards to collect it.The example for this item is shown with the ones above: os-process-Kill.salam
Terminate
func Terminate(p &: Process): intWait 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): strStart. 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): strStart.The example for this item is shown with the ones above: os-process-Start.salam
Cleanup
func Cleanup(p &: Process)StartCaptured.The example for this item is shown with the ones above: os-process-Start.salam
ExitCode
func ExitCode(p: Process): intThe example for this item is shown with the ones above: os-process-Start.salam
Pid
func Pid(p: Process): intThe example for this item is shown with the ones above: os-process-Start.salam
IsRunning
func IsRunning(p: Process): boolrunning 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