Skip to content

Where to Go From Here

The programs in this chapter are small, but they have the shape of larger Zig programs.

The programs in this chapter are small, but they have the shape of larger Zig programs.

A command-line tool reads arguments, validates them, and calls ordinary functions.

A file program opens resources, copies bytes, and closes everything with defer.

A filter reads input in pieces and writes selected output.

A network client treats the socket as a byte stream.

A library exposes public declarations and keeps tests near the code.

These examples use the same habits:

declare data clearly
check errors where they occur
release resources in the same scope that acquired them
keep I/O at the edge
move decisions into small functions

Zig rewards plain structure. There are no hidden constructors, destructors, exceptions, or global allocator rules. The program says what it uses.

A good next program is one that touches several parts of the language at once. For example:

loggrep pattern file

It should:

read command-line arguments
open a file
read it line by line
match text
print matching lines
return useful errors
include tests for matching logic

The matching logic can be separated from I/O:

const std = @import("std");

fn matches(line: []const u8, pattern: []const u8) bool {
    return std.mem.indexOf(u8, line, pattern) != null;
}

test "matches finds substring" {
    try std.testing.expect(matches("hello zig", "zig"));
}

test "matches rejects missing substring" {
    try std.testing.expect(!matches("hello zig", "rust"));
}

The I/O code can stay in main:

const std = @import("std");

fn matches(line: []const u8, pattern: []const u8) bool {
    return std.mem.indexOf(u8, line, pattern) != null;
}

pub fn main() !void {
    var args = std.process.args();

    _ = args.next();

    const pattern = args.next() orelse return error.MissingPattern;
    const path = args.next() orelse return error.MissingPath;

    const cwd = std.fs.cwd();

    var file = try cwd.openFile(path, .{});
    defer file.close();

    var reader = file.reader();
    var out = std.io.getStdOut().writer();

    var buffer: [4096]u8 = undefined;

    while (try reader.readUntilDelimiterOrEof(&buffer, '\n')) |line| {
        if (matches(line, pattern)) {
            try out.print("{s}\n", .{line});
        }
    }
}

This program is not large, but it is complete. It has arguments, files, buffers, slices, errors, tests, and output.

From here, make programs larger by adding one feature at a time.

Add line numbers:

loggrep -n pattern file

Add inverted matching:

loggrep -v pattern file

Add standard input:

cat file | loggrep pattern

Add case-insensitive matching.

Add tests before changing the matching function.

This is the safest way to learn Zig. Each new feature forces one new rule of the language into use.

The rest of this book should now be read in both directions. Earlier chapters explain the parts. This chapter shows how the parts form a program.

Exercise 20-41. Build loggrep.

Exercise 20-42. Add -n for line numbers.

Exercise 20-43. Add -v for inverted matching.

Exercise 20-44. Add tests for each flag.

Exercise 20-45. Package the program with build.zig and build.zig.zon.