Command Line Arguments in Zig

Most of the Zig programs on this blog begin with the same line that reads the command line arguments and I have never explained it. This post is about that line, what it gives you back and what you do with it in Zig 0.17.

const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const args = try init.minimal.args.toSlice(init.arena.allocator());

    std.debug.print("Program name: {s}\n", .{args[0]});
    std.debug.print("Number of arguments: {d}\n", .{args.len - 1});

    if (args.len == 1) {
        std.debug.print("Usage: {s} [-v] <integer> [<integer> ...]\n", .{args[0]});
        return;
    }

    var verbose = false;
    var sum: i64 = 0;
    for (args[1..], 1..) |arg, i| {
        if (std.mem.eql(u8, arg, "-v")) {
            verbose = true;
            continue;
        }
        const n = std.fmt.parseInt(i64, arg, 10) catch {
            std.debug.print("Argument {d} is not an integer: {s}\n", .{ i, arg });
            continue;
        };
        if (verbose) {
            std.debug.print("Argument {d}: {d}\n", .{ i, n });
        }
        sum += n;
    }
    std.debug.print("Sum: {d}\n", .{sum});
}

The init.minimal.args field holds the command line arguments of the process. The toSlice() function turns them into a slice of strings, with the program name at index 0, exactly as the shell gave it to us. The result may point to more than one allocation, so the Standard Library asks for an arena allocator and init.arena is the arena that the Zig runtime creates for the whole process. It is freed when the process exits, so there is no defer and nothing for us to free. Each argument has the type [:0]const u8, which is a slice of bytes with a zero byte after its end. That is the form the operating system uses anyway and it means you can pass an argument straight to a C function.

The value of args.len is the number of arguments plus one for the program name. When there is nothing else on the command line, the program prints a usage message and returns. The for (args[1..], 1..) |arg, i| loop visits everything except the program name and i starts from 1 so that it matches the position of each argument on the command line.

Zig has no == for strings, so std.mem.eql(u8, arg, "-v") compares the bytes one by one. The std.fmt.parseInt(i64, arg, 10) call converts an argument to a 64-bit integer in base 10 and returns an error when the text is not a number. The catch block prints a message and continues with the next argument instead of stopping the program. Everything that was a number is added to sum, which is printed at the end.

Save the code as args.zig. I build it with zig build-exe here instead of zig run so that the program name in the output is short:

$ zig version
0.17.0
$ zig build-exe args.zig
$ ./args
Program name: ./args
Number of arguments: 0
Usage: ./args [-v] <integer> [<integer> ...]
$ ./args -v 10 20 abc 30
Program name: ./args
Number of arguments: 5
Argument 2: 10
Argument 3: 20
Argument 4 is not an integer: abc
Argument 5: 30
Sum: 60

The zig run args.zig -- -v 10 20 abc 30 command works too. Everything after -- goes to the program and args[0] is then a long path inside the Zig cache directory.

Without an allocator Link to heading

If you only need to walk through the arguments once, you do not need a slice and you do not need an allocator at all. Declare main() with std.process.Init.Minimal instead of std.process.Init and the runtime skips the setup of the allocators and the I/O.

const std = @import("std");

pub fn main(init: std.process.Init.Minimal) void {
    var it = init.args.iterate();
    _ = it.skip();

    var count: usize = 0;
    while (it.next()) |arg| {
        count += 1;
        std.debug.print("{d}: {s}\n", .{ count, arg });
    }
    std.debug.print("Total: {d}\n", .{count});
}

The iterate() function returns an iterator over the arguments. The skip() function moves past the program name and the next() function returns the next argument, or null when there are no more, which is what ends the while loop. On Linux and macOS this is all you need. On Windows and WASI the iterator has to allocate, so there you call iterateAllocator() with an allocator and deinit() on the iterator when you are done.

Save the code as argsIter.zig and try it:

$ zig run argsIter.zig -- one two three
1: one
2: two
3: three
Total: 3

Happy coding in Zig!

Want to learn Zig? Look at my book Systems Programming with Zig.