/parg

Lightweight argument parser for Zig

Primary LanguageZigBSD Zero Clause License0BSD

parg

parg is a lightweight argument parser for Zig which focuses on a single task: Parsing command-line arguments into positional arguments and long/short flags. It doesn't concern itself anything else. You may find this useful as a quick way of parsing some arguments, or use it as a building block for a more elaborate CLI toolkit.

Features / non-features

  • Parses command-line arguments into positional arguments, long flags and short flags.
  • Provides an iterator interface (while (parser.next()) |token| …).
  • Supports boolean flags (--force, -f).
  • Supports multiple short flags (-avz).
  • Values can be provided as separate arguments (--message Hello), with a delimiter (--message=Hello) and also part of short flag (-mHello).
  • Automatically detects -- and skips any further parsing.
  • Licensed under 0BSD.

Usage

The principles of parg are as follows:

  • Use parseProcess, parseSlice or parse to create a new parser.
  • Remember to call deinit() when you're done with the parser.
  • Call next() in a loop to parse arguments.
  • Call nextValue() whenever you need a plain value.
  • There's a few more knobs you can tweak with.

Let's go over these steps a bit more in detail.

Create a new parser instance

There's three ways of creating a parser instance. All of these accept some options as the last argument.

const parg = @import("parg");

// (1) Parse arguments given to the current process:
var p = try parg.parseProcess(allocator, .{});

// (2) Parse arguments from a `[]const []const u8`:
var p = parg.parseSlice(slice, .{});

// (3) Parse arguments from an iterator (advanced usage):
var p = parg.parse(it, .{});

// Always remember to deinit:
defer p.deinit();

In addition, remember that the first parameter given to a process is the file name of the executable. You typically want to call nextValue() to retrieve this value before you continue parsing any arguments.

const program_name = p.nextValue() orelse @panic("no executable name");

Parsing boolean flags and positional arguments

Once you have a parser you want to call next() in a loop. This returns a token which has three different possibilities:

  • .flag when it encounters a flag (e.g. --verbose or -v). This flag has a .name field which contains the name of the flag (without the dashes) and a .kind field if you need to distinguish between long and short flags. There are also a helper functions isLong and isShort to easily check the name of the field.
  • .arg when it encounters a positional argument.
  • .unexpected_value when it encounters an unexpected value. You should just quit the program with an error when this happens. We'll come back to this in the next section.

Also note that this will automatically split up short flags as expected: If you give the program -fv then next() will first return a flag with name f, and then a flag with name v.

// See examples/ex1.zig for full example.

var verbose = false;
var force = false;
var arg: ?[]const u8 = null;

while (p.next()) |token| {
    switch (token) {
        .flag => |flag| {
            if (flag.isLong("force") or flag.isShort("f")) {
                force = true;
            } else if (flag.isLong("verbose") or flag.isShort("v")) {
                verbose = true;
            } else if (flag.isLong("version")) {
                std.debug.print("v1\n", .{});
                std.os.exit(0);
            }
        },
        .arg => |val| {
            if (arg != null) @panic("only one argument supported");
            arg = val;
        },
        .unexpected_value => @panic("unexpected value"),
    }
}

Parsing flags with values

When you find a flag which require a value you need to invoke nextValue(). This returns an optional slice:

// See examples/ex2.zig for full example.

while (p.next()) |token| {
    switch (token) {
        .flag => |flag| {
            if (flag.isLong("file") or flag.isShort("f")) {
                file = p.nextValue() orelse @panic("--file requires value");
            } else if (flag.isLong("verbose") or flag.isShort("v")) {
                verbose = true;
            } else if (flag.isLong("version")) {
                std.debug.print("v1\n", .{});
                std.os.exit(0);
            }
        },
        .arg => @panic("unexpected argument"),
        .unexpected_value => @panic("unexpected value"),
    }
}

All of these will be treated the same way:

  • --file build.zig
  • --file=build.zig
  • -f build.zig
  • -f=build.zig
  • -fbuild.zig

Most notably, notice that when you call nextValue() it will "break out" of parsing short flags. Without the call to nextValue() the code would parse -fbuild.zig as the short flags -f, -b, -u, and so on.

This also explains the need for .unexpected_value in next(): If you pass --force=yes to the first example it will parse the --force as a long flag. When you then don't invoke nextValue() (since it's a boolean flag) then we need to later bail out since we didn't expect a value.

Options and other functionality

There's currently only one option (which you configure when instantiate the parser):

  • auto_double_dash (defaults to true). When this is true it will look for -- and then stop parsing anything as a flag. Your program will not observe the -- token at all, and all tokens after this point will be returned as .arg (even though they start with a dash). When this is false it will return -- as a regular argument (.arg) and argument parsing will continue as usual.

There's also one additional method:

  • p.skipFlagParsing(). This turns off any further argument parsing. All tokens after this point will be returned as .arg (even though they start with a dash).