# The os module

> Reference for Brine's built-in os module, covering files and directories, paths, system information, and starting, polling, killing and capturing child processes.

Source: https://docs.brinelang.org/language/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`.

```brine
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)

```text
["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”](#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_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](/language/builtins/#files-environment-and-processes)). `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:

```brine
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

```text
/a/b/two.txt
/one.txt
```

## Paths and the working directory

[Section titled “Paths and the working directory”](#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”](#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.

```brine
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)

```text
build ok on windows x86_64
```

## Processes

[Section titled “Processes”](#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.

spawn.br

```brine
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

```text
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:

```brine
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

```text
1 job 1 done
2 job 2 done
3 job 3 done
```

## Inside `par`

[Section titled “Inside par”](#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](/reference/errors/#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:

```brine
fn main():
    par for i in 0..4:
        os.sleep(0.1)
```

```text
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
```

## `os` in `world` programs

[Section titled “os in world programs”](#os-in-world-programs)

In a program run as several processes ([`world`](/parallelism/overview/)), 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.

```brine
fn main():
    if os.cpu_count() > 4:
        world.barrier()
```

```text
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](/reference/errors/#e0510).

Related: [built-in functions](/language/builtins/), [testing](/language/testing/) (tests that start processes), [error handling](/language/error-handling/), [parallelism](/parallelism/overview/).
