Many programs begin the same way: they read command-line arguments, decide what the user requested, then execute an operation.
Many programs begin the same way: they read command-line arguments, decide what the user requested, then execute an operation.
A command-line parser converts raw argument strings into structured program state.
Suppose we want a small utility named calc. It accepts two subcommands:
calc add 10 20
calc mul 4 5The first version can be written directly.
const std = @import("std");
pub fn main() !void {
var args = std.process.args();
_ = args.next();
const command = args.next() orelse {
std.debug.print("missing command\n", .{});
return;
};
const a_text = args.next() orelse {
std.debug.print("missing first number\n", .{});
return;
};
const b_text = args.next() orelse {
std.debug.print("missing second number\n", .{});
return;
};
const a = try std.fmt.parseInt(i32, a_text, 10);
const b = try std.fmt.parseInt(i32, b_text, 10);
if (std.mem.eql(u8, command, "add")) {
std.debug.print("{d}\n", .{a + b});
} else if (std.mem.eql(u8, command, "mul")) {
std.debug.print("{d}\n", .{a * b});
} else {
std.debug.print("unknown command: {s}\n", .{command});
}
}Run it:
zig run main.zig -- add 10 20The output is:
30The -- separates Zig’s own options from the program’s arguments.
The function:
std.process.args()returns an iterator over command-line arguments.
The first argument is normally the executable path. This program ignores it:
_ = args.next();The underscore assignment means: evaluate the value, but discard it.
Arguments are read one at a time:
const command = args.next() orelse {
...
};args.next() returns an optional slice:
?[]const u8If no argument exists, the value is null.
The orelse expression handles the null case immediately.
Strings in Zig are byte slices:
[]const u8To compare strings, Zig uses library functions:
std.mem.eql(u8, command, "add")This compares two slices byte-by-byte.
The arguments "10" and "20" are text. They must be converted into integers:
const a = try std.fmt.parseInt(i32, a_text, 10);The arguments are:
| Argument | Meaning |
|---|---|
i32 | destination type |
a_text | source text |
10 | numeric base |
If parsing fails, parseInt returns an error. try propagates the error to the caller.
The structure of this program is common in Zig:
- Read raw input.
- Validate it early.
- Convert text into typed values.
- Dispatch using explicit control flow.
- Report errors directly.
As the number of commands grows, nested if expressions become awkward.
An enum gives the commands names.
const Command = enum {
add,
mul,
};Now parsing can be separated from execution.
const std = @import("std");
const Command = enum {
add,
mul,
};
fn parseCommand(text: []const u8) !Command {
if (std.mem.eql(u8, text, "add")) {
return .add;
}
if (std.mem.eql(u8, text, "mul")) {
return .mul;
}
return error.UnknownCommand;
}
pub fn main() !void {
var args = std.process.args();
_ = args.next();
const command_text = args.next() orelse {
return error.MissingCommand;
};
const a_text = args.next() orelse {
return error.MissingArgument;
};
const b_text = args.next() orelse {
return error.MissingArgument;
};
const command = try parseCommand(command_text);
const a = try std.fmt.parseInt(i32, a_text, 10);
const b = try std.fmt.parseInt(i32, b_text, 10);
switch (command) {
.add => {
std.debug.print("{d}\n", .{a + b});
},
.mul => {
std.debug.print("{d}\n", .{a * b});
},
}
}This version divides the program into smaller operations:
| Function | Responsibility |
|---|---|
parseCommand | convert text into an enum |
main | coordinate program flow |
parseInt | convert numbers |
switch | dispatch execution |
This style scales well.
A larger program may add:
- flags such as
--verbose - optional arguments
- configuration files
- subcommands
- environment variables
The core structure remains the same: parse first, execute later.
Exercise 20-1. Add a sub command.
Exercise 20-2. Print a usage message when arguments are missing.
Exercise 20-3. Add support for hexadecimal input.
Exercise 20-4. Add a --help flag.
Exercise 20-5. Move argument parsing into a separate file.