⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

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:

  1. The buffer belongs to the interface, not the implementation. You supply it, so there is no hidden allocation and you control the size.
  2. 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:

MethodConsumes delimiterAt end of stream
takeDelimiteryesreturns null
takeDelimiterExclusivenoerror
takeDelimiterInclusiveyes, and includes iterror

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.