EazyArgs: a simple argument parsing library with a comptime twist

Hello hello!

I am fully aware that there are several parsing arguments libraries, but nevertheless it’s time for another one!

I wrote this due to my frustration with zig-clap api design, which I do not enjoy a lot, and I replicated EasyArgs’ brilliant idea of C’s preprocessor usage with a compile time struct generation! The idea is just to define what do you want to parse in an anonymous struct like this:

const definitions = .{
    .required = .{
      Arg(u32, "limit", "Limits are meant to be broken"),
      Arg([]const u8, "username", "who are you dear?"),
    },
    .optional = .{
      // type, field_name, short, default, description
      OptArg(u32, "break", "b", 100, "Stop before the limit"),
    },
    .flag = .{
      // default as false, type is bool
      // field_name, short, description
      Flag("verbose", "v", "Print a little, print a lot"),
    }
};

const arguments: InputStruct = try parseArgs(allocator, definitions, stdout, stderr);

and then the compiler will generate a struct with those field names and fill them with whatever arguments you provided! :))

It just works on current master version of zig (0.16) due to the @Type function split up! Also idk how ergonomic it does feel, if you want to try it out or check the code and tell me pain points or bugs I would be very happy!

Thank you and have a nice day :slight_smile:

5 Likes

Welcome to Ziggit @PauSolerValades!

There’s room for as many as we want to write.

This approach seems promising to me, it could likely be extended to support ‘commands’ by returning a union of the commands, each one of which is struct-constructed the way you’re already doing. Quite Zon-like in a way.

Reified types have the limitation that it isn’t possible to add declarations, so they can’t have member functions. But options work well as plain-old-data, so not much of a limitation in practice here.

2 Likes

thank you for your reply and the edit :smiley:

Actually i was not familiar with Zon until you menctioned, and indeed it looks very similar with commands added (i had the commands in sight when finished the argument parsing) those are the sketches i was planning to do try to implement. Would be converting this:

const definitions = .{
    .flags = .{
        eaz.Flag("verbose", "v", "Enable detailed debug logging"),
        eaz.Flag("version", "V", "Print version and exit"),
    },
    .commands = .{
        .commit = .{ // this is a definition all over again
            .required = .{ ... },
            .flags = .{ ... },
            .options = .{ ... },
        },
        .push = .{
            .required = .{
                Arg([]const u8, "remote", "The remote registry (e.g. origin)"),
                Arg([]const u8, "branch", "The branch to push"),
            },
            .flags = .{
                Flag("force", "f", "Force overwrite remote branch"),
            },
            .options = .{},
        },
    },
    .required = .{ // mix command with normal arg (made up arg)
        Arg([]const u8, "path", "Check status of this specific path"),
    },
    .options = .{}, 
};

into this, using the union as you said

const Result = struct {
    verbose: bool,
    path: []const u8,
    cmd: union(enum) {
        commit: struct {
            message: []const u8,
            amend: bool,
        },
        push: struct {
            remote: []const u8,
            branch: []const u8,
            force: bool
        },
    },
};

which actually looks very clean imo, i’ll get to do fs!

never even thought about the possibility of generating declarations on the generated struct, and I struggle to came up for a well thought use for that anyway, if one comes to mind please tell me :))

1 Like

This deserves a book, or a long chapter in a book anyway.

For what you’re doing it would be possible to generate the help string as a .format method on the constructed type, then help would just be

try stdout.print("{f}", .{opts});

Of course there’s another way to do that, there always is, and individually they tend to seem sufficient.

The effect is that reified types are second-class subjects. It has to be container.function(&instance, args..), it can never be instance.function(args...) like for “real” types. I find that unsatisfactory.

1 Like

This does come with the downside of never being able to see the generated struct definition yourself… I wonder if there could be an api that would take in a struct and a set of customizations so you could still have a human-readable struct definition but the parsing options (required, positional, etc), could still be applied.

hello :))

yep, totally. It was very difficult for to convince myself the stuff had worked and the function did actually returned a real struct. If you check src/main.zig in the repo you’ll find my attempts to convince myself that the output was indeed a struct despite not appearing in the code anywhere xD

then i realized that not seeing the struct written in code was kinda the point of the library, not doing it yourself, but it took some time to settle in haha

1 Like

okay gotcha it would be more elegant the way you say for sure.

In fact, printUsage function (which prints the help) does exactly this: needs a the object and the writer. so your example is literally a thing in the library :rofl:

1 Like

This is exactly what my library does (not posting here so as not to hijack OP’s thread). Overall it works pretty well. It has the same declarative nature seen here, but with concrete types.

@PauSolerValades fun project! Welcome to the Zig CLI club!

4 Likes

let’s go CLI club!!! haha

actually I would be very curious on what other people did on this topic! do you have the link of your library (and others if you have) somewhere? If you don’t want to post it here I can message you and you send me the link? Thank you

1 Like

Sure thing! My library is cova. The repo has links at the bottom to a few other Zig CLI libs as well.

You can also look at some of the Zig Awesome Lists and Zigistry.dev for other examples.

1 Like