n0s4 / flags

An effortless command-line argument parser for Zig.
MIT License
30 stars 2 forks source link

Define required positional arguments #2

Closed GigaGrunch closed 2 months ago

GigaGrunch commented 3 months ago

Currently, positional arguments are just a list of strings and it's up to the user to deal with those. It would be nice to be able to define required positional arguments with names and types.

n0s4 commented 3 months ago

This is something that Tigerbeetle's lib does quite nicely:

        pos: struct { flag: bool = false, positional: struct {
            p1: []const u8,
            p2: []const u8,
            p3: ?u32 = null,
            p4: ?u32 = null,
        } },

This seems like a good solution.

I think I should have done this in the first place since the user can still iterate over the remaining arguments after parsing, so there's no need to collect them into a buffer like we do...

Thanks for your interest. I should be able to do this pretty easily when I get round to it. I've been neglecting this library a bit though so I might get sidetracked by chores :sweat_smile:.

n0s4 commented 2 months ago

I think I should have done this in the first place since the user can still iterate over the remaining arguments after parsing, so there's no need to collect them into a buffer like we do...

On second thought, if we have an optional positional then we can't tell if a a given positional will match its type until after it is consumed, then the user could not access it via the iterator after parsing.

This could be an error case, but what if you want to allow an unknown number of positionals afterwards (e.g file paths)?

The positional struct could have a special optional 'rest' field of [][]u8, so once all the required positionals have been matched and the optional positionals stop being matched, the rest are stored in this field if it exists, otherwise it's an error.

const Flags = struct {
    flag: bool,

    positionals: struct {
        // At least 1 positional is required and the first will be stored here:
        required: []const u8,
        // If there is a second positional argument and it can be parsed to an `i32`, it will be stored here,
        // otherwise it will be appended to `rest`
        opt_num: ?i32,
        // If there is a third positional argument _and_ `opt_num` was successfully parsed to an `i32`
        // it will be stored here, otherwise it will be appended to `rest`
        third: ?[]const u8

        rest: [][]u8,
    },
};

Thoughts?

n0s4 commented 2 months ago

... You could also have

    positionals: [][]u8

If you don't need any "defined" positionals.

In any case I want to replace the current "chuck all the positional args back to the user" approach to give more control.

GigaGrunch commented 2 months ago

Thanks for looking into this! For my current use case, the tigerbeetle way would definitely be sufficient, but I see that you still want to have that "rest" list for stuff like a list of files. I really like the idea that the rest is optional so that I can just not have it to define an exact list of positionals. It wouldn't even need to be bound to that specific name, right? As soon as its a [][]u8, nothing can come after it.

n0s4 commented 2 months ago

Yes, the user should be able to pick a name.

I'm going to write a post on Ziggit to work out my ideas and request suggestions/feedback from other users.

GigaGrunch commented 2 months ago

FYI: This is the project where I am planning to use flags in. Right now, I went with zli because it has the positional argument feature that I need, but I would still switch to flags when possible. I also have a branch where flags is integrated. https://github.com/GigaGrunch/trecker

n0s4 commented 2 months ago

I've been working on handling positional arguments more flexibly, and what I've come up with is basically passing an interface (similar to std.io.AnyWriter or something) to the parse function which tells it how to handle them. This facilitates collecting positionals via buffer, allocator and causing an error if you don't want to handle them - and there are also wrapper functions for each of these cases. You can check out the latest examples (particularly positionals.zig). The API ideas above are a bit idealistic but something like it could be worked towards, I have some ideas.

This doesn't directly address your use case though so that's what I'm going to implement next.

n0s4 commented 2 months ago

This is now in v0.6.0! lmk what you think.

GigaGrunch commented 2 months ago

Cool! I'm happy with this and will be using it in my project now 👍 I'll open a separate issue for improved help texts for positional arguments.