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");
const prettyPrint = @import("prettyprint.zig").prettyPrint;

// Optionally, you can specify the size of the buffer for positional arguments if you wish to
// impose a specific limit or you expect more than the default (32).
pub const max_positional_flags = 3;

pub fn main() !void {
    var args = std.process.args();
    const result = flags.parse(&args, Command);

    prettyPrint(
        result.flags, // result, has type of `Command`
        result.args, // extra positional arguments
    );
}

const Command = struct {
    // This field is required for your top-level command, and is used in help messages.
    pub const name = "example";

    // bool fields will be true if their flag (e.g "--force") is passed.
    // Note that you don't need to provide a default value for bools or optionals.
    force: bool,

    // All other field types will either be optional, provide a default value, or be required.
    optional: ?[]const u8, // this is set to null if this is not passed
    override: []const u8 = "defaulty",
    required: []const u8, // an error is caused if this is not passed

    // All int types are parsed automatically, with specific runtime errors if the value passed is invalid:
    age: ?u8,
    power: i32 = 9000,

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

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

    // This optional declaration defines shorthands which can be chained e.g '-fs large'.
    pub const switches = .{
        .force = 'f',
        .age = 'a',
        .power = 'p',
        .size = 's',
    };

    // These are used in the '--help' message
    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?",
    };
};

I plan to continue improving on this, especially if I get some feedback.

Thanks to everyone on this forum who helped me along the way!


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

10 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

5 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