# Working with the OS

### Working with the OS

Systems programming means working directly with the operating system.

The operating system gives your program access to files, processes, memory, clocks, terminals, environment variables, network sockets, permissions, and hardware-backed resources. Zig can use all of these, but it usually gives you two levels of access.

The first level is portable standard library code. This is what you should use first.

```zig
std.fs
std.io
std.process
std.Thread
std.time
std.net
```

The second level is operating-system-specific code. This is what you use when you need exact control.

```zig
std.posix
std.os.linux
std.os.windows
```

Good Zig code usually starts portable and only becomes OS-specific where it has to.

#### The Operating System Is Not One Thing

When beginners hear “the OS,” they often think of the desktop: windows, icons, menus, and apps.

For systems programming, the operating system mostly means the services your program can request.

Your program asks the OS to open a file.

Your program asks the OS to start another process.

Your program asks the OS what time it is.

Your program asks the OS to allocate virtual memory.

Your program asks the OS to listen on a network port.

Your program asks the OS which environment variables are set.

The operating system is the layer that owns these resources and controls access to them.

#### Paths and the Current Working Directory

Most programs need to work with paths.

A path is a name that points to a file or directory.

```text
notes.txt
src/main.zig
/tmp/output.log
C:\Users\name\Desktop\data.txt
```

Some paths are relative. A relative path is interpreted from the current working directory.

```text
data.txt
src/main.zig
```

Some paths are absolute. An absolute path starts from the root of the filesystem.

On Unix-like systems:

```text
/home/alice/project/data.txt
```

On Windows:

```text
C:\Users\Alice\project\data.txt
```

In Zig, `std.fs.cwd()` gives you a handle to the current working directory.

```zig
const std = @import("std");

pub fn main() !void {
    var file = try std.fs.cwd().openFile("data.txt", .{});
    defer file.close();

    std.debug.print("opened data.txt\n", .{});
}
```

This opens `data.txt` relative to the directory where the program is running.

That may not be the same directory where the executable lives. This matters. Programs are often launched from different directories.

#### Creating a File

To create or replace a file, use `createFile`.

```zig
const std = @import("std");

pub fn main() !void {
    var file = try std.fs.cwd().createFile("output.txt", .{});
    defer file.close();

    try file.writeAll("hello from zig\n");
}
```

This creates `output.txt` in the current working directory.

The important call is:

```zig
try file.writeAll("hello from zig\n");
```

Unlike a raw low-level `write`, `writeAll` keeps writing until all bytes are written or an error happens.

For normal application code, prefer `writeAll` over manual partial-write handling.

#### Reading a Whole File

For small files, reading the whole file is simple.

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const bytes = try std.fs.cwd().readFileAlloc(
        allocator,
        "data.txt",
        1024 * 1024,
    );
    defer allocator.free(bytes);

    std.debug.print("{s}\n", .{bytes});
}
```

The last argument is the maximum file size allowed.

```zig
1024 * 1024
```

This protects your program from accidentally reading a huge file into memory.

The file contents are allocated on the heap, so you must free them.

```zig
defer allocator.free(bytes);
```

This is a common Zig pattern: allocation is explicit, and cleanup is explicit.

#### Environment Variables

Environment variables are key-value strings passed to a process by its parent process or shell.

Examples:

```text
HOME=/home/alice
PATH=/usr/bin:/bin
EDITOR=vim
```

Programs use environment variables for configuration.

In Zig, you can read an environment variable with `std.process.getEnvVarOwned`.

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const home = std.process.getEnvVarOwned(allocator, "HOME") catch |err| switch (err) {
        error.EnvironmentVariableNotFound => {
            std.debug.print("HOME is not set\n", .{});
            return;
        },
        else => return err,
    };
    defer allocator.free(home);

    std.debug.print("HOME = {s}\n", .{home});
}
```

The returned string is allocated, so it must be freed.

On Windows, the equivalent variable may not be `HOME`. Portable programs should be careful with environment assumptions.

#### Command-Line Arguments

Command-line arguments are the words passed after the program name.

For example:

```bash
mytool input.txt output.txt
```

The arguments are:

```text
input.txt
output.txt
```

In Zig, you can get them with `std.process.argsAlloc`.

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    for (args, 0..) |arg, i| {
        std.debug.print("arg {} = {s}\n", .{ i, arg });
    }
}
```

The first argument is usually the program name or path.

This means a command like:

```bash
./mytool input.txt
```

often produces:

```text
arg 0 = ./mytool
arg 1 = input.txt
```

Do not assume there is always an argument at index `1`. Check first.

```zig
if (args.len < 2) {
    std.debug.print("usage: mytool <file>\n", .{});
    return;
}
```

#### Exit Codes

A program returns an exit code to the operating system.

By convention:

```text
0 means success
non-zero means failure
```

In Zig, returning an error from `main` usually reports failure. For explicit control, you can use `std.process.exit`.

```zig
const std = @import("std");

pub fn main() void {
    std.debug.print("fatal: missing input\n", .{});
    std.process.exit(1);
}
```

Use exit codes carefully in command-line tools. Other programs and scripts may depend on them.

#### Running Another Program

Programs can start other programs.

In Zig, `std.process.Child` gives you control over child processes.

```zig
const std = @import("std");

pub fn main() !void {
    var child = std.process.Child.init(&[_][]const u8{
        "zig",
        "version",
    }, std.heap.page_allocator);

    const result = try child.spawnAndWait();

    std.debug.print("child exited with: {}\n", .{result});
}
```

This starts:

```bash
zig version
```

The child process runs separately. Your program waits for it to finish.

For real tools, you often need to handle stdout, stderr, stdin, environment variables, and working directory. But the basic model is simple: create a child process, configure it, spawn it, wait for it.

#### Temporary Directories

Many programs need temporary files.

Temporary files are useful for intermediate output, tests, downloads, caches, and atomic file replacement.

The standard library has APIs for temporary directories and testing support. The exact choice depends on the situation.

A simple pattern is to create temporary work inside a known directory and clean it up with `defer`.

```zig
const std = @import("std");

pub fn main() !void {
    try std.fs.cwd().makeDir("tmp-work");
    defer std.fs.cwd().deleteTree("tmp-work") catch {};

    var file = try std.fs.cwd().createFile("tmp-work/output.txt", .{});
    defer file.close();

    try file.writeAll("temporary data\n");
}
```

The cleanup line uses `catch {}` because cleanup can fail, and this example chooses to ignore cleanup errors.

```zig
defer std.fs.cwd().deleteTree("tmp-work") catch {};
```

In production code, you may want to log cleanup errors instead of ignoring them.

#### Permissions

Operating systems use permissions to decide what programs may do.

A file may be readable but not writable.

A directory may allow listing but not creation.

A port may require special privileges.

A process may be unable to signal another process.

In Zig, permission failures appear as errors.

```zig
var file = std.fs.cwd().openFile("/root/secret.txt", .{}) catch |err| {
    std.debug.print("cannot open file: {}\n", .{err});
    return;
};
defer file.close();
```

Do not treat OS operations as guaranteed. Almost every OS interaction can fail.

#### OS Differences

A program that works on Linux may not work the same way on Windows.

Common differences include:

| Topic | Unix-like Systems | Windows |
|---|---|---|
| Path separator | `/` | `\` and often `/` accepted by APIs |
| Root path | `/` | Drive roots like `C:\` |
| Executable files | Permission bit matters | File extension often matters |
| Environment names | Often case-sensitive | Usually case-insensitive |
| Process model | POSIX-style APIs | Windows-specific APIs |
| Newlines | `\n` convention | Text tools may use `\r\n` |

Zig helps, but it cannot erase every OS difference.

For portable code, avoid hardcoding path separators. Prefer standard library path and filesystem APIs.

#### Prefer Handles Over Global Paths

A good systems-programming habit is to work relative to directory handles instead of constantly building global path strings.

This:

```zig
var dir = try std.fs.cwd().openDir("data", .{});
defer dir.close();

var file = try dir.openFile("input.txt", .{});
defer file.close();
```

is often better than manually constructing:

```text
data/input.txt
```

Directory handles can make code clearer, safer, and easier to adapt.

They also match how operating systems often work internally: open a resource, receive a handle, operate through that handle, close it when done.

#### Resource Lifetime

When you work with the OS, you work with resources.

Files must be closed.

Directories must be closed.

Allocated argument arrays must be freed.

Child processes must be waited for or otherwise managed.

Mapped memory must be unmapped.

Sockets must be closed.

The Zig pattern is:

```zig
const resource = try acquire();
defer release(resource);
```

Example:

```zig
var file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();
```

This is one of the most important patterns in Zig systems programming.

Acquire the resource. Immediately write the cleanup. Then use the resource.

#### Mental Model

Working with the OS means asking for resources, using handles, checking errors, and releasing everything you acquire.

Zig makes this explicit.

You can write high-level code with `std.fs`, `std.process`, `std.io`, and `std.net`. When needed, you can go lower with `std.posix` or OS-specific modules.

Start with the portable API. Drop down only when the portable API does not expose the control you need.

