Readers and Writers
const std = @import("std");
const expect = std.testing.expect;
test "iterate lines with takeDelimiter" {
var reader: std.Io.Reader = .fixed("line one\nline two\n");
// `takeDelimiter` consumes the delimiter and returns null at the end,
// which is what you want for a line loop.
var count: usize = 0;
while (try reader.takeDelimiter('\n')) |line| {
count += 1;
try expect(line.len == 8);
}
try expect(count == 2);
}
test "the Exclusive variant leaves the delimiter behind" {
var reader: std.Io.Reader = .fixed("line one\nline two\n");
const first = try reader.takeDelimiterExclusive('\n');
try expect(std.mem.eql(u8, first, "line one"));
// The '\n' has NOT been consumed, so the next call sees it immediately
// and returns an empty slice. Use `takeDelimiter` to loop over lines,
// or `toss(1)` to step past the delimiter yourself.
const second = try reader.takeDelimiterExclusive('\n');
try expect(second.len == 0);
reader.toss(1);
const third = try reader.takeDelimiterExclusive('\n');
try expect(std.mem.eql(u8, third, "line two"));
}
test "take a fixed number of bytes" {
var reader: std.Io.Reader = .fixed("abcdef");
const chunk = try reader.take(3);
try expect(std.mem.eql(u8, chunk, "abc"));
}
test "write into a caller-owned buffer" {
var buf: [32]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try writer.writeAll("hello");
try writer.print(" {d}", .{42});
try expect(std.mem.eql(u8, writer.buffered(), "hello 42"));
}
test "allocating writer grows as needed" {
const gpa = std.testing.allocator;
var writer: std.Io.Writer.Allocating = .init(gpa);
defer writer.deinit();
for (0..5) |i| try writer.writer.print("{d},", .{i});
try expect(std.mem.eql(u8, writer.written(), "0,1,2,3,4,"));
}
test "a fixed writer reports when it runs out" {
var buf: [4]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try std.testing.expectError(error.WriteFailed, writer.writeAll("too long"));
}Zig 0.15 replaced the old generic Reader/Writer with non-generic
std.Io.Reader and std.Io.Writer. Two things changed that matter every day:
- The buffer belongs to the interface, not the implementation. You supply it, so there is no hidden allocation and you control the size.
- Writers are buffered, so you must
flush. Forgetting is the single most common mistake with the new API.
Constructing them
var reader: std.Io.Reader = .fixed("some bytes");
var writer: std.Io.Writer = .fixed(&buf);
var growable: std.Io.Writer.Allocating = .init(gpa);
For files, file.writerStreaming(io, &buf) gives you a File.Writer whose
.interface field is the std.Io.Writer to use.
Reading lines: mind the variant
This one catches people, including an earlier draft of this page:
| Method | Consumes delimiter | At end of stream |
|---|---|---|
takeDelimiter | yes | returns null |
takeDelimiterExclusive | no | error |
takeDelimiterInclusive | yes, and includes it | error |
So takeDelimiterExclusive called twice on "a\nb\n" returns "a" and then
an empty slice, because the \n is still there. For a line loop you want:
while (try reader.takeDelimiter('\n')) |line| { ... }
Use toss(n) to skip bytes manually if you do use the exclusive form.
Running out of room
A .fixed writer fails with error.WriteFailed when the buffer is full. It
does not truncate silently.