Skip to content

The language

The os module

os is built in, so there’s nothing to import: write os.cwd(). Every function that can fail returns a Result whose Err holds the operating system’s message, so a missing file is a value you handle, never a crash. Functions that change things outside the program can’t be called inside par.

fn main() -> Result[(), str]:
let dir = os.temp_dir() + "/os-demo-" + str(os.pid())
os.make_dir(dir + "/logs")?
write_file(dir + "/b.txt", "bee")?
write_file(dir + "/a.txt", "ay")?
print(os.list_dir(dir)?)
print(os.is_dir(dir + "/logs"), os.is_file(dir + "/a.txt"), os.is_file(dir + "/nope"))
os.rename(dir + "/a.txt", dir + "/c.txt")?
os.remove_file(dir + "/b.txt")?
print(os.list_dir(dir)?)
print(os.remove_file(dir + "/b.txt"))
return Ok(())
Output (Windows)
["a.txt", "b.txt", "logs"]
true true false
["c.txt", "logs"]
Err("The system cannot find the file specified. (os error 2)")

The error text comes from the operating system, so it differs between platforms (on Linux: No such file or directory (os error 2)). Match on Ok/Err, not on the message.

Function Result What it does
os.list_dir(path: str) Result[[str], str] entry names (not full paths), sorted, without . and ..
os.is_dir(path: str) bool true if path is a directory
os.is_file(path: str) bool true if path is a regular file
os.make_dir(path: str) Result[(), str] create a directory and any missing parents; Ok if it already exists
os.remove_file(path: str) Result[(), str] delete a file
os.rename(src: str, dst: str) Result[(), str] move or rename; replaces dst if it exists
os.write_bytes(path: str, bytes: [u8]) Result[(), str] write a binary file, byte for byte
  • is_dir and is_file return false for a path that doesn’t exist.
  • make_dir over an existing file is an Err.
  • There is no remove_dir; run a program for that, or leave temporary directories under os.temp_dir().
  • For text files use the built-ins read_file, write_file and file_exists (built-in functions). read_file needs UTF-8 text, so os.write_bytes is the way to write binary data. There is no binary read yet.

list_dir and is_dir are enough to walk a tree:

fn walk(dir: str, ref out: [str]):
match os.list_dir(dir):
case Ok(names):
for name in names:
let path = dir + "/" + name
if os.is_dir(path):
walk(path, ref out)
elif name.ends_with(".txt"):
out.push(path)
case Err(e):
eprint("skipping", dir, ":", e)
fn main() -> Result[(), str]:
let root = os.temp_dir() + "/walk-demo-" + str(os.pid())
os.make_dir(root + "/a/b")?
write_file(root + "/one.txt", "1")?
write_file(root + "/a/b/two.txt", "2")?
write_file(root + "/a/skip.md", "3")?
var found: [str] = []
walk(root, ref found)
for p in found:
print(p[len(root):])
return Ok(())
Output
/a/b/two.txt
/one.txt
Function Result What it does
os.cwd() str the current working directory
os.set_cwd(path: str) Result[(), str] change the working directory
os.temp_dir() str the system’s temporary directory, with no trailing separator
os.exe_path() str the absolute path of the running program
os.abs_path(path: str) Result[str, str] the canonical absolute path; Err if it doesn’t exist

On Windows, abs_path returns the extended form \\?\C:\.... / works as a separator on every platform, so os.temp_dir() + "/name" is fine on Windows too.

Function Result What it does
os.name() str "windows", "macos", "linux", "freebsd", …
os.arch() str "x86_64", "aarch64", …
os.pid() int this process’s id
os.cpu_count() int logical CPUs, at least 1
os.sleep(seconds: float) pause this thread
os.is_terminal(fd: int) bool is file descriptor fd (0 stdin, 1 stdout, 2 stderr) a terminal?
os.enable_ansi() on Windows, let the console show ANSI color codes; does nothing elsewhere
os.free_ports(n: int) Result[[int], str] n distinct TCP ports free on localhost

is_terminal(1) is how a program decides whether to print colors: it’s false when the output goes to a file or a pipe.

fn main():
let color = os.is_terminal(1)
if color:
os.enable_ansi()
let ok = "\u{1b}[32mok\u{1b}[0m" if color else "ok"
print("build", ok, "on", os.name(), os.arch())
Output (Windows, piped to a file)
build ok on windows x86_64

os.spawn starts a program and returns at once with a handle. Use the handle to wait for the program, check on it, stop it, or collect its output.

spawn.br
fn main() -> Result[(), str]:
let a = args()
if len(a) > 0 and a[0] == "child":
print("hello from the child,", getenv("GREETING").unwrap_or("?"))
eprint("a warning")
exit(7)
# run this same program again, as a child
let p = os.spawn([os.exe_path(), "child"], {"GREETING": "hi"}, true, false)?
let code = os.wait(p)
let (out, err) = os.output(p)
print("exit code:", code)
print("stdout:", out.strip())
print("stderr:", err.strip())
return Ok(())
Output
exit code: 7
stdout: hello from the child, hi
stderr: a warning
Function Result What it does
os.spawn(cmd: [str], env: {str: str}, capture: bool, stdin: bool) Result[int, str] start cmd; Ok(handle), or Err if it can’t start
os.wait(h: int) int wait for the process to exit; its exit code
os.poll(h: int) Option[int] Some(exit code) if it has exited, None if it’s still running
os.kill(h: int) stop the process
os.output(h: int) (str, str) captured stdout and stderr, after it has exited

The arguments of os.spawn:

  • cmd: the program and its arguments. cmd[0] is searched on PATH. Arguments go to the program as they are, with no shell in between, so spaces and quotes need no escaping. An empty list is an Err.
  • env: variables to add to (or replace in) this process’s environment for the child. Pass {} for none.
  • capture: true collects the child’s stdout and stderr for os.output. false lets the child write straight to this program’s terminal.
  • stdin: true shares this program’s stdin with the child. false gives it an empty stdin, so it can never wait for input.

More details:

  • os.wait can be called again after the process exits; it returns the same code. A process stopped by a signal (on Linux and macOS) has exit code 101; on Windows, a process stopped by os.kill has exit code 1.
  • os.output panics if the process is still running (call os.wait or check os.poll first). Without capture, both strings are "".
  • Captured output is collected while the child runs, so a child that prints a lot can’t block.
  • A handle that didn’t come from os.spawn panics: os.wait: 12345 is not a process handle from os.spawn.
  • To run a program and just wait for it, sharing the terminal, the built-in run(cmd) is shorter.

Several children at once, polled until all finish:

fn main() -> Result[(), str]:
let a = args()
if len(a) > 0:
let n = int(a[0])
os.sleep(0.05 * float(n))
print(f"job {n} done")
exit(n)
var handles: [int] = []
for n in 1..4:
handles.push(os.spawn([os.exe_path(), str(n)], {}, true, false)?)
var codes = [-1] * len(handles)
var left = len(handles)
while left > 0:
for i in 0..len(handles):
if codes[i] < 0:
match os.poll(handles[i]):
case Some(code):
codes[i] = code
left -= 1
case None:
pass
os.sleep(0.01)
for i, h in enumerate(handles):
let (out, _) = os.output(h)
print(codes[i], out.strip())
return Ok(())
Output
1 job 1 done
2 job 2 done
3 job 3 done

These functions change the world outside the program, so the order they run in would matter. They are compile errors inside a par body (E0503):

os.spawn, os.wait, os.poll, os.kill, os.make_dir, os.remove_file, os.rename, os.set_cwd, os.write_bytes, os.sleep, os.enable_ansi

The rest (list_dir, is_dir, is_file, cwd, temp_dir, exe_path, abs_path, name, arch, pid, cpu_count, is_terminal, free_ports, output) only read, and are allowed:

fn main():
par for i in 0..4:
os.sleep(0.1)
error[E0503]: `os.sleep` can't be called inside a `par` body
--> main.br:3:9
|
3 | os.sleep(0.1)
| ^^^^^^^^^^^^^ not allowed here
|
= help: its effects would happen in an unpredictable order; do it before or after the `par`
= run `brine explain E0503` for more about this error
error: could not compile due to 1 error

In a program run as several processes (world), every rank must make the same world calls in the same order. The results of os functions can differ from rank to rank (each rank has its own pid, and ranks may run on different machines), so the compiler treats them as rank-dependent, like world.rank(). Only os.name() and os.arch() count as the same on every rank.

fn main():
if os.cpu_count() > 4:
world.barrier()
error[E0510]: some ranks might not reach this communication
--> main.br:3:9
|
3 | world.barrier()
| ^^^^^^^^^^^^^^^ calls `world.barrier`
2 | if os.cpu_count() > 4:
| ------------------ this may differ between ranks
|
= note: every rank must make the same `world` calls in the same order, otherwise the program could hang
= help: move the call out of the rank-dependent branch or loop; compute rank-specific values first, then communicate on every rank
= run `brine explain E0510` for more about this error
error: could not compile due to 1 error

To agree on a value, reduce it first: world.min(os.cpu_count()) is the same on every rank, so it can decide a branch. See E0510.

Related: built-in functions, testing (tests that start processes), error handling, parallelism.