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
