odin-lang / Odin

Odin Programming Language
https://odin-lang.org
BSD 3-Clause "New" or "Revised" License
6.66k stars 584 forks source link

Add package `core:flags` #3700

Closed Feoramund closed 3 months ago

Feoramund commented 4 months ago

core:flags, the rewrite

It's time for the Joy of Parsing Arguments.

Do you want to be able to use a struct as your data format for your command-line arguments?

import "core:flags"
import "core:os"

Options :: struct {
    file:   os.Handle `args:"pos=0,required,file=r" usage:"Input file."`,
    output: os.Handle `args:"pos=1,required,file=cw,perms=600" usage:"Output file."`,
}

opts: Options

flags.parse_or_exit(&opt, os.args)

os.write_string(opt.output, "Hellope!\n")

Yes, it's that simple.

Demo

Have a look.

odin_core_flags_demo

Supported Types

What else can it do?

You can give it your own type setter proc for custom named types and a flag checker proc for the validation phase, to make sure all of the values are what you expect. The flag checker will only receive flags that have been set, so no need to add in extra logic to figure out what's what there.

How does it work?

core:flags makes extensive use of Odin's run-time type information system to determine where data in your struct is stored, what type it is, and it makes use of recursion to support dynamic arrays, maps, and maps of dynamic arrays (if a map[string][dynamic]T solves a problem for you).

The power of this is not to be underestimated. It's the same power that makes core:encoding/json work and what inspired this package in its original incarnation.

There are a myriad of subtags for specifying how each flag should be named, positioned, if it should be hidden from help, if it's required (and in what number), how to handle files, and such.

Documentation

Everything has been thoroughly documented, and there is a full example included in the example subdirectory of the package. I mostly avoided the use of short variable names to help improve code reading. Notes and comments are left around where I thought they might be useful to clarify some of my thought processes.

There is also a test suite including 50+ tests to ensure long-term stability and that expectations are met.

If asserts are not disabled, the parser will make sure the struct you give it is of a sensible construction. I.e. no file subtag on types that aren't os.Handle. That should help reduce confusion.

odin-varg

This package originally started as odin-varg, a small library I wrote in a few hours after realizing how core:encoding/json worked. This is the fully rewritten, vetted and tested version, for Odin proper.

If you used odin-varg at all, here's an incomplete summary of changes between then and now:

Notes

The bit_set parsing is experimental. In my mind at least, it made more sense to parse 1s and 0s from left to right as Least Significant Bit to Most Significant Bit. If it's a sensible way to parse them, I may be able to boot it into core:strconv at some point.

I know several programs have a "command" style of mode, Odin included, where they accept the first argument as a way to switch between different actions. Odin has test, build, et cetera. I just want to note that I'm aware of this method, and I've considered it as a possible future expansion to core:flags, pending how this goes.

I'm also aware of how some programs will have a -v to exit early and show the version information while disregarding other flags. That can kind of work here, if none of your flag values error, but there's no special handling for it. I'm more of the mind that showing version information makes more sense as a command, or perhaps a banner.

I made a special case for -h and -help because it's so ubiquitous and I could not fathom another way to display the usage documentation in the case where a program was started with no required arguments.

variadic flags being able to specify how many arguments they take at once was an easy feature, but I'm not sure how useful or fitting it is. Could probably use some feedback on that one.

I'm uncertain if a banner feature is necessary, like a string that joins up with the usage as a header, sort of like how I handle os.args[0].

I'm leaving this as a draft for a bit to hear some thoughts and ideas.

Feedback

I'm looking for feedback, of any sort, code or aesthetics. Some of this is aesthetic, of course, and people have their different preferences for flags and how they're interpreted and displayed. Any oversights I may have made would be nice to know too. It's not a huge package, but it's a bit feature-dense and it took some time to get just right. I've reviewed it from top to bottom a few times, and more eyes are always appreciated.

I spent a few days taking notes on some common use cases for command-line argument parsing, but I could've missed your particular use case. This is why I added filename parsing-and-opening to the package, since it's such a common use case, and I could imagine a lot of boilerplate code being written if I didn't include that feature. Simplifies the whole process and saves some developer time while making things more of a joy, hopefully.

blob1807 commented 3 months ago

With the current parser implementation, is it possible to add an Odin style option with no hyphens? Or is that outside of the scope of this package? I tend to have a preference towards no hyphens but I don't mind either way.

Feoramund commented 3 months ago

With the current parser implementation, is it possible to add an Odin style option with no hyphens? Or is that outside of the scope of this package? I tend to have a preference towards no hyphens but I don't mind either way.

With that style, how do you distinguish between positional and non-positional arguments, or do positionals just not exist in this method? Is there a program you have in mind that parses arguments like this that I could look at?

Feoramund commented 3 months ago

I'll be looking at this again in a bit. It's been a week, and I've only had one pressing idea that's come to mind: supporting the UNIX-style -- flag that consumes all following arguments. After that, I should be able to mark it as ready.

Feoramund commented 3 months ago

Ready to go.

blob1807 commented 3 months ago

With that style, how do you distinguish between positional and non-positional arguments, or do positionals just not exist in this method? Is there a program you have in mind that parses arguments like this that I could look at?

@Feoramund I'm sorry for the late reply. I have no idea honestly. I don't knowingly use them. Normally if I want this style I'll make a unique one for it. And I'll probably just continue to. Here's quick generic example, it may bugs. https://gist.github.com/blob1807/74d0d507887da913b2a109a6893dce75

gingerBill commented 3 months ago

@blob1807 Hyphenless flags is not something I want.

Feoramund commented 3 months ago

I have no idea honestly. I don't knowingly use them. Normally if I want this style I'll make a unique one for it. And I'll probably just continue to.

The internal organization of core:flags is modular and simple enough that I believe you could take a copy of it and just remove the checks for - and calls to push_positional to get what you want.