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

Leave a Comment

Your email address will not be published. Required fields are marked *