zig-skills/references/std-fs.md

7.3 KiB

File System API Reference (Zig 0.16.0)

Primary release-note source: https://ziglang.org/download/0.16.0/release-notes.html

Zig 0.16 migrates file-system operations to std.Io. Use std.Io.Dir, std.Io.File, and an explicit std.Io parameter. std.fs is now mostly path helpers and deprecated compatibility names.

The snippets below are focused fragments. They assume const std = @import("std"), a caller-supplied io: std.Io, a suitable allocator, and any named application values such as max_size, from, and to.

Core Types

std.Io.Dir       // Directory handle and file-system operations
std.Io.File      // File handle, readers, writers, stat, locking
std.Io.File.Atomic
std.Io.File.MemoryMap
std.Io.Dir.path  // Path helpers; std.fs.path is deprecated compatibility

Current Directory

const cwd = std.Io.Dir.cwd();

Do not use old std.fs.cwd() in new 0.16 code.

Opening Files

const file = try std.Io.Dir.cwd().openFile(io, "data.txt", .{});
defer file.close(io);

const rw = try std.Io.Dir.cwd().openFile(io, "data.txt", .{
    .mode = .read_write,
});
defer rw.close(io);

Creating Files

const file = try std.Io.Dir.cwd().createFile(io, "out.txt", .{});
defer file.close(io);

With default options, createFile truncates an existing regular file. Use the exclusive-create option when overwriting must fail. Other options cover read access, permissions, and locking where supported. File access mode is an openFile option, not the name of the creation-permissions field.

Reading Files

Stream With Reader

const file = try std.Io.Dir.cwd().openFile(io, "data.txt", .{});
defer file.close(io);

var buffer: [4096]u8 = undefined;
var file_reader = file.reader(io, &buffer);
const reader = &file_reader.interface;

while (try reader.takeDelimiter('\n')) |line| {
    _ = line;
}

Allocate Whole File

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

The limit uses std.Io.Limit. Hitting the limit returns error.StreamTooLong. readFileAlloc creates a file reader internally and reads until the supplied limit. Use an explicit File.Reader when streaming, reusing buffers, or controlling incremental consumption.

Read To End From Existing File

var buffer: [4096]u8 = undefined;
var file_reader = file.reader(io, &buffer);
const bytes = try file_reader.interface.allocRemaining(allocator, .limited(max_size));
defer allocator.free(bytes);

Writing Files

Stream With Writer

const file = try std.Io.Dir.cwd().createFile(io, "out.txt", .{});
defer file.close(io);

var buffer: [4096]u8 = undefined;
var file_writer = file.writer(io, &buffer);
const writer = &file_writer.interface;

try writer.print("value={d}\n", .{42});
try writer.writeAll("raw bytes\n");
try writer.flush();

Write Whole File

try std.Io.Dir.cwd().writeFile(io, .{
    .sub_path = "out.txt",
    .data = "hello\n",
});

Standard I/O

var stdout_buffer: [4096]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
try stdout_writer.interface.print("hello\n", .{});
try stdout_writer.interface.flush();

var stderr_buffer: [4096]u8 = undefined;
var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
try stderr_writer.interface.print("error: {s}\n", .{"message"});
try stderr_writer.interface.flush();
var stdin_buffer: [4096]u8 = undefined;
var stdin_reader = std.Io.File.stdin().reader(io, &stdin_buffer);

if (try stdin_reader.interface.takeDelimiter('\n')) |line| {
    _ = line;
}

Directories

var dir = try std.Io.Dir.cwd().openDir(io, "assets", .{ .iterate = true });
defer dir.close(io);

try std.Io.Dir.cwd().createDir(io, "new-dir", .default_dir);
try std.Io.Dir.cwd().createDirPath(io, "path/to/nested");

Both walking APIs require a directory opened with .iterate = true; iterating without that capability is illegal behavior. Use other openDir options for access/no-follow behavior as needed.

Walking

Use walk for full recursive walks and walkSelectively when you want to decide which directories to enter.

var walker = try dir.walk(allocator);
defer walker.deinit();

while (try walker.next(io)) |entry| {
    _ = entry;
}
var walker = try dir.walkSelectively(allocator);
defer walker.deinit();

while (try walker.next(io)) |entry| {
    if (entry.kind == .directory and shouldEnter(entry)) {
        try walker.enter(io, entry);
    }
}

Metadata

const stat = try file.stat(io);

const size = stat.size;
const kind = stat.kind;
const modified = stat.mtime;
const accessed = stat.atime orelse return error.FileAccessTimeUnavailable;
_ = .{ size, kind, modified, accessed };

atime is optional in 0.16.

Setting timestamps uses structured options:

try file.setTimestamps(io, .{
    .access_timestamp = .init(stat.atime),
    .modify_timestamp = .init(stat.mtime),
});

Paths

std.Io.Dir.path / std.fs.path functions handle Windows paths more consistently in 0.16.

The relative-path helpers are pure and receive the current directory rather than querying the OS. relative and relativeWindows also accept an optional environment map for Windows per-drive current-directory resolution; relativePosix has a distinct signature without that map.

const cwd_path = try std.process.currentPathAlloc(io, allocator);
defer allocator.free(cwd_path);

const rel = try std.fs.path.relative(allocator, cwd_path, environ_map, from, to);
defer allocator.free(rel);

Atomic Files

Obtain a std.Io.File.Atomic value through Dir.createFileAtomic, then use its public cleanup/materialization lifecycle. Do not construct File.Atomic directly or hand-roll random temporary names; the helper routes entropy and filesystem work through std.Io.

Absolute Operations

Many old std.fs.*Absolute functions moved to std.Io.Dir.*Absolute, for example:

  • std.fs.openFileAbsolute -> std.Io.Dir.openFileAbsolute
  • std.fs.createFileAbsolute -> std.Io.Dir.createFileAbsolute
  • std.fs.deleteFileAbsolute -> std.Io.Dir.deleteFileAbsolute
  • std.fs.renameAbsolute -> std.Io.Dir.renameAbsolute
  • std.fs.accessAbsolute -> std.Io.Dir.accessAbsolute

Many Z and W path-specific helpers were removed.

Migration Map

Old 0.15-style API Zig 0.16 API
std.fs.cwd() std.Io.Dir.cwd()
std.fs.File.stdout() std.Io.File.stdout()
file.close() file.close(io)
dir.close() dir.close(io)
file.reader(&buf) file.reader(io, &buf)
file.writer(&buf) file.writer(io, &buf)
dir.readFileAlloc(allocator, path, max) dir.readFileAlloc(io, path, allocator, .limited(max))
file.readToEndAlloc(allocator, max) file.reader(io, &buf).interface.allocRemaining(allocator, .limited(max))
std.process.getCwdAlloc(allocator) std.process.currentPathAlloc(io, allocator)

Application Guidance

  • Prefer an application's virtual or asset file-system abstraction for runtime reads when one exists.
  • New direct file operations still need an io parameter at the API boundary.
  • Writes should be explicit and use std.Io.Dir / std.Io.File until a project abstraction owns write policy.
  • Avoid local fallback Io.Threaded construction in low-level file helpers; pass or store io.