Zig File IO

File input/output lets your program read data from disk and write results back. Zig's std.fs module provides a complete file system API — creating, reading, writing, appending, deleting, and directory traversal. Every operation that can fail returns an error, and Zig's type system forces you to handle it.

Writing a File

const std = @import("std");

pub fn main() !void {
    // Create (or overwrite) a file
    const file = try std.fs.cwd().createFile("hello.txt", .{});
    defer file.close();

    try file.writeAll("Hello, Zig!\n");
    try file.writeAll("Second line.\n");
    std.debug.print("File written.\n", .{});
}
  std.fs.cwd()           → the current working directory
  .createFile("name", .) → create file; truncates if exists
  .writeAll(data)        → write all bytes, error if partial
  defer file.close()     → close when scope ends (always)

Reading an Entire File

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

    const file = try std.fs.cwd().openFile("hello.txt", .{});
    defer file.close();

    // Read up to 1 MB into a heap-allocated buffer
    const content = try file.readToEndAlloc(alloc, 1024 * 1024);
    defer alloc.free(content);

    std.debug.print("{s}", .{content});
}
  File on disk: "Hello, Zig!\nSecond line.\n"
       |
  readToEndAlloc → allocates exactly what's needed
       |
  content: []u8 → "Hello, Zig!\nSecond line.\n"

Reading Line by Line

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

    const file = try std.fs.cwd().openFile("hello.txt", .{});
    defer file.close();

    const reader = file.reader();
    var line_num: u32 = 0;

    while (true) {
        const line = try reader.readUntilDelimiterOrEofAlloc(
            alloc, '\n', 4096,
        ) orelse break;
        defer alloc.free(line);

        line_num += 1;
        std.debug.print("{d}: {s}\n", .{line_num, line});
    }
}
  Reads one line at a time until EOF.
  Output:
  1: Hello, Zig!
  2: Second line.

Appending to a File

const file = try std.fs.cwd().createFile("log.txt", .{ .truncate = false });
defer file.close();

// Seek to end before writing
try file.seekFromEnd(0);
try file.writeAll("New log entry\n");

Setting .truncate = false opens the file without clearing its content. Seeking to the end with seekFromEnd(0) positions the write cursor at the end of the existing data, so new writes append rather than overwrite.

Checking if a File Exists

fn fileExists(path: []const u8) bool {
    std.fs.cwd().access(path, .{}) catch return false;
    return true;
}

if (fileExists("config.json")) {
    std.debug.print("Config found.\n", .{});
} else {
    std.debug.print("No config, using defaults.\n", .{});
}

Deleting Files and Directories

// Delete a file
try std.fs.cwd().deleteFile("temp.txt");

// Delete an empty directory
try std.fs.cwd().deleteDir("old_folder");

// Delete a directory and all its contents
try std.fs.cwd().deleteTree("cache");

Listing a Directory

pub fn main() !void {
    var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var it = dir.iterate();
    while (try it.next()) |entry| {
        const kind = switch (entry.kind) {
            .file      => "FILE",
            .directory => "DIR ",
            else       => "OTHER",
        };
        std.debug.print("[{s}] {s}\n", .{kind, entry.name});
    }
}
  Current directory listing:
  [FILE] build.zig
  [FILE] build.zig.zon
  [DIR ] src
  [DIR ] zig-out

Working with Paths

const std = @import("std");
var buf: [std.fs.max_path_bytes]u8 = undefined;

// Get absolute path of current directory
const cwd = try std.fs.cwd().realpath(".", &buf);
std.debug.print("CWD: {s}\n", .{cwd});

// Join path components
const joined = try std.fs.path.join(alloc, &.{"src", "main.zig"});
defer alloc.free(joined);
// joined = "src/main.zig" on Linux, "src\main.zig" on Windows

Reading and Writing with Buffering

Unbuffered I/O makes a system call for every read or write. Buffering collects small operations into larger chunks, dramatically improving performance for many small writes:

const file = try std.fs.cwd().createFile("output.txt", .{});
defer file.close();

// Wrap with a buffered writer (4 KB buffer)
var bw = std.io.bufferedWriter(file.writer());
const writer = bw.writer();

for (0..1000) |i| {
    try writer.print("Line {d}\n", .{i});
}

try bw.flush();  // write remaining buffered data to disk
  Without buffering:
  1000 writes → 1000 system calls → slow

  With buffering (4 KB buffer):
  1000 writes → fill buffer → flush → ~few system calls → fast

Practical Example: Config File Reader

const std = @import("std");

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

    // Write a config file
    {
        const f = try std.fs.cwd().createFile("app.conf", .{});
        defer f.close();
        try f.writeAll("host=localhost\nport=8080\ndebug=true\n");
    }

    // Read it back line by line
    const f = try std.fs.cwd().openFile("app.conf", .{});
    defer f.close();
    const reader = f.reader();

    while (true) {
        const line = try reader.readUntilDelimiterOrEofAlloc(
            alloc, '\n', 256,
        ) orelse break;
        defer alloc.free(line);

        if (std.mem.indexOfScalar(u8, line, '=')) |eq| {
            const key = line[0..eq];
            const val = line[eq+1..];
            std.debug.print("  {s} = {s}\n", .{key, val});
        }
    }
}

Output:

  host = localhost
  port = 8080
  debug = true

Leave a Comment

Your email address will not be published. Required fields are marked *