argp: GNU-style command line argument parser for Go
github.com
github.com
> options can have multiple values: -a 1 2 3 means that a is an array/slice/struct of three numbers of value [1,2,3]
Allowing multiple values is inconsistent because you can't tell in "./cmd -a 1 2 3" whether 2 and 3 are positional arguments or arguments for -a. This is not a GNU style. The GNU way is "./cmd -a 1 -a 2 -a 3" (or "./cmd -a 1,2,3"). This package supports that, which is good.
> option values can be separated by a space, equal sign, or nothing: -a1 -a=1 -a 1 are all equal
"--a=1" is a GNU style but "-a=1" is not. This is a minor issue, though.
Also, does this package support "--"? Everything following "--" should be treated as positional arguments.
talk is cheap. (I'm not saying that you're full of hot air, I'm saying that if you want to make Go's flag package better, make a proposal with good rationale behind it, include a good implementation, and see what the community says.)
But it doesn't need to be changed. Nothing stops anyone from using any of the several argument parsers for Go. The one included in the standard library is just that and nothing more: included in the standard library. It is not privileged in any manner (beyond that brute fact), it has no access to any sort of internal standard-library only functionality that gives it access to performance external packages can't have, it isn't particularly integrated forcibly into anything else included with Go, it is not mandatory, it is not even something the community deeply believes must be used and anyone who uses alternate mechanisms is violating any sort of nebulous community standard. It's just a package.
This is one of those cases where people often rather casually say "I don't like how Go handles X", or, indeed, any language with a standard library (this is not a Go-specific problem) when they really ought to say something more like "I don't like how the standard library flags package in Go handles X". But it's not the language doing it, it's the library. For a non-Go example that I've encountered in the wild, it isn't really appropriate to say "I don't like Python's GUI", referring to the built-in Tkinter binding. It's just a library. Python's got a dozen other choices, including direct bindings to every major toolkit in every major OS. It's not "Python's GUI", it's just one that ships in the standard library, not the sum total of all of Python's GUI capability.
You can add better things without breaking compatibility promises. See: many, many features added since 1.0. New IP address types, slog, etc.
Fortunately, that is not a prerequisite for their happiness.
The standard library is special. It sets the blessed way to do X, e.g. Context, the Writer and Reader interfaces, etc. Claiming that you may as well write your own is like claiming you can just fork Linux if you disagree with its direction. Good luck with that.
Bringing up Python is particularly unfortunate since its packaging story sucks, so whether a package is included in its stdlib or not really matters.
Sure, argv parsing is an isolated piece of functionality, so in that specific case it doesn't really matter what package you use, but the sentiment is incredibly wrong in general.
People can import their own flag parsing libraries, hence the HN submission we are discussing at the moment.
As for “talk is cheap” comment, I’ve written more open source code for working with software in the command line than most people. And that includes tools that parse other CLI tools and man pages to provide autocompletion hints. But that still doesnt mean that Go should make a breaking change to their standard library based on my personal preferences.
How did log/slog appear if the Go standard library already has a logger and a backwards compatibility promise? Because you can add stuff without breaking backwards compatibility.
Go’s backwards compatibility promise is good in the short term, but if it has no expiration, that promise will be the rope wrapped around Go’s neck as it is pushed off a small platform and is left to dangle as the crowd watches.
Do you think that backward compatibility promise will still be upheld in 500 years? If you think not, then you agree that there is a line somewhere between 0 and 500 years and that on the other side of that line that promise makes no sense.
What about 50 years? Do you think in 50 years we’ll still have the same flag package in the standard library that we have today? I don’t. I certainly hope we don’t.
The more one clings to that backward compatibility promise, the more the backward compatibility promise strangles you.
I hope we incorporate lessons learned from Go 1 into some new version of Go, whatever its name is. I certainly DO NOT hope that we ignore what we’ve learned and fail to act because of a promise to never abandon the mistakes that have been made.
Time is a finite resource and there are already other packages out there that satisfy the problem, so why add to the noise?
One could flip the argument and say: if you’re so convinced that a proposal is the right course of action, then why don’t you create one yourself?
because I'm not annoyed by the existing flag package, and the people I was replying to are, that's why. that's why I suggested that they submit a proposal in the first place.
this is apparently a contest to see who can misunderstand me the fastest and you're all winners in this race.
We aren’t annoyed. We said we dislike the one in standard library.
That’s also precisely why alternative packages exist. Like the one you’re commenting on right now. No proposal is needed.
I also admire your optimism regarding the ease in which people can push a proposal in Golang. ;) Gos famously hard in that regard.
there is no better person to produce a proposal than the person who best understands why it isn't sufficient or is wrong in one or more ways.
I didn't say it was easy to guide a proposal from the proposal stage through to acceptance, or that I was optimistic about doing it. (you think I am misunderstanding you?) I merely feel like the people who like something the least are probably the most qualified to produce a quality proposal which has a chance at improving the landscape. that's all.
By the way, if i haven't said something outright, I did not infer that thing. Don't read between my lines, there's nothing there.
Exactly. Dislike != annoyed
I can use a different package so what’s there to be annoyed about?
> there is no better person to produce a proposal than the person who best understands why it isn't sufficient or is wrong in one or more ways.
Dislike != insufficient
It’s a personal preference not a technical criticism.
> By the way, if i haven't said something outright, I did not infer that thing. Don't read between my lines, there's nothing there.
If you’re going to play the “you’ve misunderstood me” card then at least make sure you’ve understood people correctly yourself.
> I didn't say it was easy to guide a proposal from the proposal stage through to acceptance, or that I was optimistic about doing it
Changing the way the flag package would need to be changed to make myself (and others) happy couldn’t be done in a backwards compatible way. Thus it would have to be part of Go v2.0, which could be a decade away or might never happen.
And since all that the change would bring is an alternative, more GNU, way of parsing flags, it makes this hypothetical proposal a really low bar for improvement (unlike your other examples) thus this hypothetical proposal is unlikely to ever approved even if a hypothetical v2 of Go were to ever be released.
The whole premise of this changing is so remote that even suggesting I write a proposal in the first place is a massively optimistic take.
And for the record, I fully support Go not changing their behaviour here. Keeping the flags package as-is is the right thing for Go to do. So I wouldn’t even support my hypothetical proposal here even though I don’t personally like the existing flags package.
This might sound too nuanced for HN, but you can dislike something while still supporting it’s existence. Change here, in my opinion, would be worse than the status quo.
For, like, 95% of the tools out there, I’m going to use --long every single time.
Anyway, I don’t even know the options that a program takes until I read the program docs. The program docs will tell me that the option is -host=localhost or --host=localhost or --bind-addr=localhost:8000.
The other 5% are tools like ls, cp, mv. As far as I care, ancient tools are the only ones permitted to have short options that combine into a single option, like old-school getopt. Maybe a few exceptions now and then.
Please, do name one language that has.
Except it's not? There are tons of flag packages in real use, because the stdlib one kind of sucks. Even the "go" tool itself works around some of its limitations (things like "go test ./... -v" won't work out of the box, since it will stop parsing flags at the first non-flag, so the go command reorders os.Args before sending it off to the flag package).
Things like "-a 1 2 3" are not standard "Go style" at all. I've never seen that.
It doesn’t. It’s simpler, which makes it not suck in my books.
All the stuff about short vs long, grouping short flags together, allowing the last flag of a group of short flags to take an argument, allowing a flag to take multiple values, etc. is cool, but confusing and quite hard to do right.
In Go it’s just -flag[=value].
./cmd -a"$USER_CONTROLLED_DATA"
A program using this package would break that assumption, introducing a bug where this user-controlled data cannot start with an '='.This syntax is supported by argparse and clap, the most popular argument parsers for Python and Rust respectively, and it seems to have caused almost no problems for them. It's a problem for the uutils implementation of cut, since `cut -d=` is common, but that's the only instance I could find after a long time scouring search engines and bug trackers and asking for examples.
If anyone does know of other examples or other places this has been discussed I'd love to hear it though, maybe I just haven't found them.
(Also, the more reliable way to write this in general is `-a "$USER_CONTROLLED_DATA"`, since that'll behave correctly if $USER_CONTROLLED_DATA is empty. As will `-a="$USER_CONTROLLED_DATA"` if you know the command supports it.)
Anyway, one other alternative for the `cut` situation is to allow either ':' or '=' to optionally separate the key and the value. Then you can say `cut -d:=` or `cut -d=:` if you wanted to use either one. This is what https://github.com/c-blake/cligen does (for Nim, not Go).
So, in this case that would seem to imply a problem for any utilities with options taking strings-or-chars, not merely `cut -d`. If uutils really wants to be strictly drop-in compatible, they may well need to roll their own option parser or twist the arm of whoever's they use to provide a mode for them.
In the more general case, "cross compatibility" may just always be limited by the reality that people just disagree on this stuff "more than they seem to think they do" (at least in my experience) and definitely more than they wish they did. I surveyed my /usr/bin once and like half of thousands of commands did not work with --help (yes, running that took some confidence in backups! but anyone could replicate on a throwaway VM or something). Consistency is nice, but consistency with what? -l=foo is consistent with --long=foo, but not (some, but not other) historical things.
I'm not sure there will ever will be a world in which you don't need to know which PLang/CL toolkit was used to make a CLI utility if you really want to know its syntax. The article's lib is going its own way from the Go stdlib. POSIX is pretty darn calcified. A 15 year old Python stdlib thing is unlikely to ever change in this regard. Python also allows "--beg" for "--beginning-long-option" if nothing else starts with "--beg" even back in its optparse days and that also tends to be controversial. cligen tools actually provide a --help-syntax. Maybe something like that could take off?
> You write a bit as if we disagree, but I don't see any real point of contention. :-)
The `cut -d:=` spelling solves a different problem than the one I meant (and the one you're now talking about). But we're mostly on the same page!
That said, I think? 'nloomans means for USER_CONTROLLED_DATA to be a set of short flags, not flag values, as in:
root@08e9950d5bfd:/# export USER_CONTROLLED_DATA=lh
root@08e9950d5bfd:/# ls -a"$USER_CONTROLLED_DATA"
total 56K
drwxr-xr-x 1 root root 4.0K Mar 23 16:51 .
drwxr-xr-x 1 root root 4.0K Mar 23 16:51 ..
[...]
Not that I've seen this in the wild before. But everyone's use of bash is a unique personal hell anyway."Surprise" is observer-relative. From a blank slate, if --long=val and --long val both work, then why not -s=val and -sval and -s val for a short -s?
So, I think the right word here is "disagreement" (perhaps about "which consistency - history vs. internal"), not throwing shade by simply declaring/asserting it a "bug".
I wouldn't mind if GNU style was consistently extended to allow annotated required arguments. But that's about it.
That being said. As a rule, if your command line utility has enough positional arguments that you're forgetting which one is which, it's a badly designed command line interface. Most often it's because you're doing too much, not settling on sensible defaults for things which you shouldn't need to specify every time, or just doing something outright weird.
It's nice if things are simple enough that a hand-full or two of flags are sufficient. However more complex programs that do more or substantially different things from traditional bytestream filter programs often do need proper configuration files.
I'm so used to the GNU conventions that I'm not really aware of what the alternatives are or what their merits are.
I'd just use cobra.
I've had a much better experience with https://github.com/alecthomas/kong , and I've heard good things about https://github.com/urfave/cli , so I would recommend those over Cobra.
I’ve also tried all of them. cobra is more than good enough and what issues remain in 2025 are easily and reliably worked around to be non issues.
Kong 1.7 has a disadvantage when it comes to negative numbers. argp parses `foo --bar -1` as `foo --bar=-1` the way you expect when `--bar` takes a value, but Kong treats `-1` as a flag.
Edit: Expanded the first paragraph.
This seems a bug in Kong. Has someone reported it?
Not a fan.
Otherwise, I'm not really sure what this offers that alternatives don't, apart from the -a 1 2 3 -> [1,2,3] business which seems highly confusing and undesirable to me so I'd think of it as more of an anti-feature.
I think my only real point of "frustration" (mild at best) is that flag composability isn't always possible (eg embedding/sharing common non-root flags across flag structs), IIRC struct tag eval doesn't reach nested types.
Other than that though, it's one of those nice libraries that just does a thing and gets out of your way.
TBH I rarely do it much anyway, mostly the flag struct ends up being one anonymous struct which is operated on in the main package only. I do like writing custom types for parsing though - I've seen engineers do a bunch of post-hoc parsing logic which generally just ends up being messy.
What compile-time validation do I actually lose by using struct tags for CLI arg parsing?
Beware of microbenchmarks, but in my tests the above package only added around 0.001 ms over the standard library flag package. Even if real world usage increases that by several orders of magnitude, it isn't going to matter. No human will ever notice.
Anyway, I think you're overstating how hard reflection-based code is. Yes, it is tricky and should mostly be avoided, but it's not beyond the scope of human intellect to understand (especially Go's which is pretty sensible).
I does everything, including too complicated options.
in the past and was pretty happy with it. However sometimes it's good to try something New :-)
I know of UNIX parameters, with a single hyphen & a single letter, and GNU parameters, with a double parameter and a word. But a single hyphen and then a word: *mind broken*.
When I first saw that on Terraform, I was upset & conflicted.
OK, I know that Java also has mental parameter options like that ('-version', note the *single* hyphen followed by, gasp, a word!), but I just dismissed it as weird enterprise-ism.
Then I saw that this 'single hyphen + word' was an accepted convention for parameters, in the Go world & I was disappointed.
Glad that this package is fighting the good fight.
With fd, a modern 'equivalent', I barely think just type.
Same for grep.
I think "mental" is being too kind about java.
$ java -version
and $ java --version
both work, their outputs are different, and `-version` goes to STDERR whereas `--version` goes to STDOUT. type Conf struct {
Thing string `argp:”name:thing;short:t;desc:A thing you can do”`
}
This would follow go struct tag standards.People who ignore convention and insist on whatever they like are never any fun to work with.
I'm glad I don't use anything you wrote :)
[0]: https://docopt.org/
same to mycmd -v, mycmd --version
> * option values can be separated by a space, equal sign, or nothing: -a1 -a=1 -a 1 are all equal
> * options can have multiple values: -a 1 2 3 means that a is an array/slice/struct of three numbers of value [1,2,3]
while convenient, one of the reasons I dislike gnu style flags is just how inconsistent they can be and annoyingly hard to parse by humans
If you want that, the GNU style could be `-a "1 2 3"`.
Normally `-a 1 2 3` would be a = 1, and 2 and 3 being positional arguments. A clarified way to write the same would be `-a 1 -- 2 3`.