Zig Project: Command-Line Tool
A command-line tool is a program driven by text arguments instead of buttons. This lesson builds one that parses a fixed argument string, so its output never depends on how the program is launched.
Parsing a command
The fixed string holds a command and two operands. Splitting on spaces gives the command first, then each number, which is parsed and added.
const std = @import("std");
pub fn main() !void {
const input = "add 12 30";
var words = std.mem.splitScalar(u8, input, ' ');
const command = words.next().?;
const first = try std.fmt.parseInt(i32, words.next().?, 10);
const second = try std.fmt.parseInt(i32, words.next().?, 10);
const out = std.io.getStdOut().writer();
if (std.mem.eql(u8, command, "add")) {
try out.print("{d} + {d} = {d}\n", .{ first, second, first + second });
}
}Parsing flags
Flags change how the tool behaves. The name flag takes the next word as its value, and the shout flag switches the greeting to upper case.
const std = @import("std");
pub fn main() !void {
const input = "greet --name Ada --shout";
var words = std.mem.splitScalar(u8, input, ' ');
_ = words.next();
var name: []const u8 = "world";
var shout = false;
while (words.next()) |word| {
if (std.mem.eql(u8, word, "--name")) {
name = words.next().?;
} else if (std.mem.eql(u8, word, "--shout")) {
shout = true;
}
}
const out = std.io.getStdOut().writer();
if (shout) {
var buf: [16]u8 = undefined;
const upper = std.ascii.upperString(buf[0..name.len], name);
try out.print("HELLO, {s}!\n", .{upper});
} else {
try out.print("Hello, {s}!\n", .{name});
}
}Reporting bad input
The second operand here is a letter, not a number. The parse fails, so the tool prints a plain error line and stops instead of crashing.
const std = @import("std");
pub fn main() !void {
const input = "add 7 x";
var words = std.mem.splitScalar(u8, input, ' ');
_ = words.next();
const first = try std.fmt.parseInt(i32, words.next().?, 10);
const out = std.io.getStdOut().writer();
const second = std.fmt.parseInt(i32, words.next().?, 10) catch {
try out.print("error: bad number\n", .{});
return;
};
try out.print("{d} + {d} = {d}\n", .{ first, second, first + second });
}Example
const std = @import("std");
pub fn main() !void {
const text = "hello world\nhello zig\n";
var lines: usize = 0;
for (text) |c| {
if (c == '\n') lines += 1;
}
var words: usize = 0;
var tokens = std.mem.tokenizeAny(u8, text, " \n");
while (tokens.next()) |_| words += 1;
const out = std.io.getStdOut().writer();
try out.print("lines: {d}\nwords: {d}\nchars: {d}\n", .{ lines, words, text.len });
}Key points
- Command-line tool: a command-line tool is a program whose input arrives as words typed after its name.
- The first word is the command, and the words after it are the operands or flags the command acts on.
- Flag: a flag is a word starting with dashes that switches a behavior on or takes a following value.
- Splitting the argument string on spaces gives each word in order, ready to be read one by one.
- Numbers in the argument string are text until they are parsed, and a failed parse is an error to report.
- A tool that reads a fixed input string is deterministic, while real launch arguments would vary between runs.
- Printing a plain error line and stopping is kinder than crashing on bad input.
This tiny word-count tool processes a fixed piece of text instead of a file. Counting newline bytes gives the line count, and splitting on spaces and newlines gives the word count. The character count is simply the length of the text, so all three numbers are exact and repeatable.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A tool parses the fixed string "add 3 4" and prints the sum. Why is its output deterministic?