Flags: an effortless command-line arguments parser

This has been a great project for getting familiar with Zig’s awesome comptime/meta-programming capabilities, and it’s gotten to a state where I think it could be useful for others :grin:.

There are already a few libraries that serve the same purpose [1] , so I’ll highlight some key differences:

  • Zero allocations.
  • Declaratively define your command as a plain Zig type.
  • Single-function API.

It’s mainly inspired by tigerbeetle’s library which blew my mind when I saw it as it’s exactly the kind of design I wanted. I copied some code for that initially and it now has a wider range of features:

  • Automatic help generation (at comptime) - customized with user-declared descriptions for each flag/command.
  • Switch (shorthand) declarations.
  • Multi-level subcommands.
  • Developer-friendly compile errors if you’re Command type is invalid.

Here’s an example, you can find more here:

const std = @import("std");
const flags = @import("flags");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var args = try std.process.argsWithAllocator(gpa.allocator());
    defer args.deinit();

    const result = flags.parse(&args, Overview, .{});

    try std.json.stringify(
        result,
        .{ .whitespace = .indent_2 },
        std.io.getStdOut().writer(),
    );
}

// The name of your type should match your executable name, e.g "my-program" -> "MyProgram".
// This can be overridden in the call to `flags.parse`.
const Overview = struct {
    // Optional description of the program.
    pub const description =
        \\This is a dummy command for testing purposes.
        \\There are a bunch of options for demonstration purposes.
    ;

    // Optional description of some or all of the flags (must match field names in the struct).
    pub const descriptions = .{
        .force = "Use the force",
        .optional = "You don't need this one",
        .override = "You can change this if you want",
        .required = "You have to set this!",
        .age = "How old?",
        .power = "How strong?",
        .size = "How big?",
    };

    force: bool, // Set to `true` only if '--force' is passed.

    optional: ?[]const u8, // Set to null if not passed.
    override: []const u8 = "defaulty", // "defaulty" if not passed.
    required: []const u8, // fatal error if not passed.

    // Integer and float types are parsed automatically with specific error messages for bad input.
    age: ?u8,
    power: f32 = 9000,

    // Restrict choice with enums:
    size: enum {
        small,
        medium,
        large,

        // Displayed in the '--help' message.
        pub const descriptions = .{
            .small = "The least big",
            .medium = "Not quite small, not quite big",
            .large = "The biggest",
        };
    } = .medium,

    // The 'positional' field is a special field that defines arguments that are not associated
    // with any --flag. Hence the name "positional" arguments.
    positional: struct {
        first: []const u8,
        second: u32,
        // Optional positional arguments must come at the end.
        third: ?u8,

        pub const descriptions = .{
            .first = "The first argument (required)",
            .second = "The second argument (required)",
            .third = "The third argument (optional)",
        };
    },

    // Subcommands can be defined through the `command` field, which should be a union with struct
    // fields which are defined the same way this struct is. Subcommands may be nested.
    command: union(enum) {
        frobnicate: struct {
            pub const descriptions = .{
                .level = "Frobnication level",
            };

            level: u8,
        },
        defrabulise: struct {
            supercharge: bool,
        },

        pub const descriptions = .{
            .frobnicate = "Frobnicate everywhere",
            .defrabulise = "Defrabulise everyone",
        };
    },

    // Optional declaration to define shorthands. These can be chained e.g '-fs large'.
    pub const switches = .{
        .force = 'f',
        .age = 'a',
        .power = 'p',
        .size = 's',
    };
};

Here’s the generated help message:

$ zig build run-example -- --help
Usage: overview [-f | --force] [--optional <optional>] [--override <override>]
                --required <required> [-a | --age <age>] [-p | --power <power>]
                [-s | --size <size>] <FIRST> <SECOND> [<THIRD>] <command>

This is a dummy command for testing purposes.
There are a bunch of options for demonstration purposes.

Options:

  -f, --force  Use the force
  --optional   You don't need this one
  --override   You can change this if you want
  --required   You have to set this!
  -a, --age    How old?
  -p, --power  How strong?
  -s, --size   How big?
    small      The least big
    medium     Not quite small, not quite big
    large      The biggest
  -h, --help   Show this help and exit

Arguments:

  <FIRST>   The first argument (required)
  <SECOND>  The second argument (required)
  <THIRD>   The third argument (optional)

Commands:

  frobnicate   Frobnicate everywhere
  defrabulise  Defrabulise everyone

  1. mainly ‘zig-clap’, ‘yazap’, and ‘cova’. ↩︎

18 Likes

Thought I’d also showcase the generated help for that example:

$ zig build run-example -- --help
Usage: example [options]

Options:
  -f, --force Use the force
  --optional  You don't need this one
  --override  You can change this if you want
  --required  You have to set this!
  -a, --age   How old?
  -p, --power How strong?
  -s, --size  How big?
    small     The least big
    medium    Not quite small, not quite big
    large     The biggest
  -h, --help  Show this help and exit
2 Likes

Just as a heads up, there’s also Zli - A friendly fork of TigerBeetle's flag module

Which is good! In OSS, I personally find that “let’s work on one true solution” works out worse than letting everyone explore what they are excited about!

cc @ilx

7 Likes

You got me at “zero allocations” ^^ I just started playing with this; I might have overlooked this but is there a way to add a description for the positional args, as you can do with zig-clap?

By description do you mean just for the help message?
I do want to add more options to control the help message like a top-level command description and a way to fully override it with a custom one.

Exactly, just for the help message. So that a user knows upfront that they need to provide n arguments which mean x, y, z etc. So this could basically be just another text (const u8) field with a specific name I guess. We could also move this to a github issue if you like :wink:

Ok, yeah I think opening an issue to discuss this is a good idea.

Hacked your package into a toy I wrote recently:

Worked out nicely :slight_smile: Actually, I just realized that the only positional argument I have might be an option as well to make usage more consistent. Anyhow, I guess it won’t hurt to have that customization option for the help menu.

2 Likes

A few more niceties:

  • Supports ‘help’ declaration for a free-form description of your command.
  • Supports ‘full_help’ to override all auto-generated help with a hand-crafted one.
  • Commands are now detected in kebab-case (build_exe: struct {…}, => $ mycommand build-exe …).
  • All arguments after a -- will be treated as positional arguments, not flags.
2 Likes

Very nice interface!

I have a question: what if I have a argument called switches or descriptions?

Thank you.

That’s not a problem, those are declarations - not fields, so you can still have fields by those names. You cannot however have a flag named “help”, but I don’t think that’s a significant problem.

Big update!

4 Likes

Freakin awesome! I got up and running with this library in less than 5 minutes thanks to your examples at

and instructions at

my usage of the library:

1 Like
3 Likes

I added full usage generation. Here’s what it looks like for the overview example:

$ zig build run-example -- --help
Usage: overview [-f | --force] [--optional <optional>] [--override <override>]
                --required <required> [-a | --age <age>] [-p | --power <power>]
                [-s | --size <size>] <FIRST> <SECOND> [<THIRD>] <command>
4 Likes

This is great! Would it be possible to template help messages?

Can you explain that more?

Something like this Usage & Help Messages · 00JCIV00/cova Wiki · GitHub

I would need to see a real use case for adding more complexity to the api.

A more powerful middle ground you might find easier to implement could be the callback function approach. The next section down in that wiki page shows how I implemented it if you were curious.

Just my 2 cents. I def understand your desire for API simplicity and I think flags pulls that off very well.

2 Likes