Most programs spend their time moving bytes.
They read files, transform data, and write results somewhere else. Zig exposes these operations directly. There is no hidden runtime. A file is an operating system resource, and Zig’s standard library provides functions that work close to the system interface.
This chapter begins with the simplest possible file operation: opening a file and reading its contents.
const std = @import("std");
pub fn main() !void {
const cwd = std.fs.cwd();
const file = try cwd.openFile("message.txt", .{});
defer file.close();
var buffer: [128]u8 = undefined;
const n = try file.readAll(&buffer);
try std.io.getStdOut().writer().print(
"{s}\n",
.{buffer[0..n]},
);
}Suppose message.txt contains:
hello from zigRunning the program prints:
hello from zigThe program begins by getting the current working directory:
const cwd = std.fs.cwd();cwd is a filesystem handle. Most filesystem operations begin from a directory handle rather than from global functions.
The next statement opens the file:
const file = try cwd.openFile("message.txt", .{});The first argument is the path. The second argument is a structure containing open options. Here the structure is empty:
.{}The defaults are enough for a simple read-only open.
The return value is a file handle. A file handle owns an operating system resource, so it must eventually be closed.
defer file.close();defer schedules the call to happen when the surrounding scope ends. No matter how the function exits, file.close() runs before the function returns.
The buffer declaration creates space for file data:
var buffer: [128]u8 = undefined;This is an array of 128 bytes.
undefined means the memory begins with unspecified contents. Zig does not automatically clear stack memory.
The next statement reads bytes into the buffer:
const n = try file.readAll(&buffer);readAll fills the buffer and returns the number of bytes actually read.
The expression:
&bufferpasses a pointer to the array.
The returned value n is important. A file may contain fewer bytes than the buffer size, so only the range:
buffer[0..n]contains valid data.
This expression creates a slice. A slice is a pointer plus a length.
The final statement prints the slice as a string:
try std.io.getStdOut().writer().print(
"{s}\n",
.{buffer[0..n]},
);{s} formats a byte slice as a string.
The function returns:
!voidThe ! means the function may return an error. Every filesystem operation can fail:
- the file may not exist
- permissions may deny access
- the disk may fail
- the path may refer to a directory
The try operator handles these failures by returning the error immediately to the caller.
A slightly larger example copies one file into another.
const std = @import("std");
pub fn main() !void {
const cwd = std.fs.cwd();
const input = try cwd.openFile("input.txt", .{});
defer input.close();
const output = try cwd.createFile(
"output.txt",
.{},
);
defer output.close();
var buffer: [1024]u8 = undefined;
while (true) {
const n = try input.read(&buffer);
if (n == 0)
break;
try output.writeAll(buffer[0..n]);
}
}The loop continues until read returns zero bytes:
if (n == 0)
break;A return value of zero means end-of-file.
This style appears often in systems programs:
- open resources
- allocate a buffer
- read bytes
- process bytes
- write bytes
- close resources
Zig keeps each step visible.
Exercise 13-1. Modify the first program so it prints the file size.
Exercise 13-2. Write a program that copies standard input to standard output.
Exercise 13-3. Change the copy program so it counts lines while copying.
Exercise 13-4. Write a program that prints the first 10 bytes of a file in hexadecimal.