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(())["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.
Files and directories
Section titled “Files and directories”| 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_dirandis_filereturnfalsefor a path that doesn’t exist.make_dirover an existing file is anErr.- There is no
remove_dir; run a program for that, or leave temporary directories underos.temp_dir(). - For text files use the built-ins
read_file,write_fileandfile_exists(built-in functions).read_fileneeds UTF-8 text, soos.write_bytesis 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(())/a/b/two.txt/one.txtPaths and the working directory
Section titled “Paths and the working directory”| 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.
System information
Section titled “System information”| 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())build ok on windows x86_64Processes
Section titled “Processes”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.
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(())exit code: 7stdout: hello from the child, histderr: 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 onPATH. Arguments go to the program as they are, with no shell in between, so spaces and quotes need no escaping. An empty list is anErr.env: variables to add to (or replace in) this process’s environment for the child. Pass{}for none.capture:truecollects the child’s stdout and stderr foros.output.falselets the child write straight to this program’s terminal.stdin:trueshares this program’s stdin with the child.falsegives it an empty stdin, so it can never wait for input.
More details:
os.waitcan 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 byos.killhas exit code 1.os.outputpanics if the process is still running (callos.waitor checkos.pollfirst). Withoutcapture, 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.spawnpanics: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(())1 job 1 done2 job 2 done3 job 3 doneInside par
Section titled “Inside par”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 errorerror: could not compile due to 1 erroros in world programs
Section titled “os in world programs”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 errorerror: could not compile due to 1 errorTo 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.