Zig Command Line Args
Command-line arguments let users pass information to your program when they launch it from a terminal. A file path, a mode flag, a number — anything the user types after the program name becomes an argument your code can read. Zig provides clean, cross-platform access to these arguments through std.process.
How Arguments Work
Terminal command: ./myapp convert input.txt output.csv --verbose Arguments array: args[0] = "./myapp" ← always the program itself args[1] = "convert" ← first user argument args[2] = "input.txt" ← second args[3] = "output.csv" ← third args[4] = "--verbose" ← fourth (a flag)
Reading Arguments with argsAlloc
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const args = try std.process.argsAlloc(alloc);
defer std.process.argsFree(alloc, args);
std.debug.print("Program: {s}\n", .{args[0]});
std.debug.print("Argument count: {d}\n", .{args.len - 1});
for (args[1..], 1..) |arg, i| {
std.debug.print(" arg[{d}] = {s}\n", .{i, arg});
}
}
Run: ./myapp hello world
Output:
Program: ./myapp
Argument count: 2
arg[1] = hello
arg[2] = world
Checking Argument Count
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const args = try std.process.argsAlloc(alloc);
defer std.process.argsFree(alloc, args);
if (args.len < 2) {
const stderr = std.io.getStdErr().writer();
try stderr.print("Usage: {s} \n", .{args[0]});
std.process.exit(1);
}
const filename = args[1];
std.debug.print("Opening: {s}\n", .{filename});
}
Always write usage errors to stderr, not stdout. Exit with code 1 (or any non-zero) to signal failure. Exit with code 0 to signal success.
Parsing Flags
Flags are arguments that start with - or --. A simple flag parser checks each argument and responds:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const args = try std.process.argsAlloc(alloc);
defer std.process.argsFree(alloc, args);
var verbose = false;
var output: ?[]const u8 = null;
var input: ?[]const u8 = null;
var i: usize = 1;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (std.mem.eql(u8, arg, "--verbose") or
std.mem.eql(u8, arg, "-v"))
{
verbose = true;
} else if (std.mem.eql(u8, arg, "--output") or
std.mem.eql(u8, arg, "-o"))
{
i += 1;
if (i >= args.len) {
std.debug.print("Error: --output requires a value\n", .{});
std.process.exit(1);
}
output = args[i];
} else if (!std.mem.startsWith(u8, arg, "-")) {
input = arg;
} else {
std.debug.print("Unknown flag: {s}\n", .{arg});
}
}
if (verbose) std.debug.print("Verbose mode ON\n", .{});
std.debug.print("Input: {s}\n", .{input orelse "(none)"});
std.debug.print("Output: {s}\n", .{output orelse "(none)"});
}
Run: ./tool data.csv --output result.csv --verbose Verbose mode ON Input: data.csv Output: result.csv
Parsing Numeric Arguments
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const args = try std.process.argsAlloc(alloc);
defer std.process.argsFree(alloc, args);
if (args.len < 3) {
std.debug.print("Usage: {s} \n", .{args[0]});
std.process.exit(1);
}
const a = std.fmt.parseInt(i64, args[1], 10) catch {
std.debug.print("Error: '{s}' is not a valid integer\n", .{args[1]});
std.process.exit(1);
};
const b = std.fmt.parseInt(i64, args[2], 10) catch {
std.debug.print("Error: '{s}' is not a valid integer\n", .{args[2]});
std.process.exit(1);
};
std.debug.print("{d} + {d} = {d}\n", .{a, b, a + b});
}
Run: ./calc 42 58 Output: 42 + 58 = 100 Run: ./calc hello 5 Output: Error: 'hello' is not a valid integer
Environment Variables
Environment variables are key-value settings the operating system passes to every process. They complement arguments for configuration that does not change per-run:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
// Read a specific environment variable
const home = try std.process.getEnvVarOwned(alloc, "HOME");
defer alloc.free(home);
std.debug.print("HOME = {s}\n", .{home});
// Get the entire environment as a map
var env_map = try std.process.getEnvMap(alloc);
defer env_map.deinit();
if (env_map.get("PATH")) |path| {
std.debug.print("PATH starts with: {s}...\n",
.{path[0..@min(40, path.len)]});
}
}
Practical Example: Word Counter Tool
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const args = try std.process.argsAlloc(alloc);
defer std.process.argsFree(alloc, args);
if (args.len < 2) {
std.debug.print("Usage: wordcount \n", .{});
std.process.exit(1);
}
const file = std.fs.cwd().openFile(args[1], .{}) catch |err| {
std.debug.print("Cannot open '{s}': {}\n", .{args[1], err});
std.process.exit(1);
};
defer file.close();
const content = try file.readToEndAlloc(alloc, 10 * 1024 * 1024);
defer alloc.free(content);
var words: u64 = 0;
var lines: u64 = 0;
var in_word = false;
for (content) |ch| {
if (ch == '\n') lines += 1;
if (ch == ' ' or ch == '\n' or ch == '\t') {
in_word = false;
} else if (!in_word) {
in_word = true;
words += 1;
}
}
std.debug.print("{s}: {d} lines, {d} words, {d} bytes\n",
.{args[1], lines, words, content.len});
}
Run: ./wordcount hello.txt Output: hello.txt: 2 lines, 5 words, 28 bytes
