Getopt() but Friendlier

jandeboevrie1 pts0 comments

getopt() but friendlier | ~yosh

getopt() but friendlier

posted 2026-07-31T14:20:00Z

I’ve been thinking a lot about “making C generally nicer to use” recently. The language has been stuck in The Dark Ages in terms of both standard behavior and programmer practice for a while, and I feel like only recently—with the works of people like ThePhD on the C standard or Wellons blogging about his C practices—has some amount of momentum and public discussion been put on improving the ergonomics of C internally and externally.1

I feel like there’s a happy middle ground to be found between the average 90s C codebase and the Greenspun’s lament of projects like metalang99.2 Of course, this middle ground is already found in languages like Zig or Hare, but those benefit from being new and unencumbered by backwards compatibility right now.3 I want to know what can be done to make ragtag old C nicer to use.

I came across fanf’s article on option parsing recently, and a bored enough me paid enough attention to think about the problem space myself during the night. I’m not sure how much use this might have for me, as most of the time I interact with C is contributing to other projects, but this exists now, and I don’t really want to make it un-exist, so it’s here now. Gives me an excuse to ramble.

what makes nice CLI argument handling?

For me, compared to getopt(), not much, really. The biggest divergence that my opinion has from POSIX is that I’d like to not have to specify flags before positional arguments, i.e. ./prog -a file should be the same as ./prog file -a. Arguments would of course be treated as positional after a --, and a lone - should be treated as positional since it’s a common idiom for stdin/stdout. This seems to be the common consensus among Modern Terminal Programs™, so it’s nice to get up to speed.

It’s also nice when short and long option arguments have a single consistent separator. This is mainly something I noticed with GNU tools, as they accept either = or the next *argv as the long option argument. Not like, the end of the world, but it just feels like unnecessary leniency. It also means I don’t have to use a form of string slices to separate an option from its argument, which keeps things slightly simpler.4 There’s also a common practice for short options to allow no separator, i.e. -Wall -Wextra -Wpedantic. I’m not personally a fan of this, since with sufficiently many options it might conflate flag chaining with an argument, but others seem to like it, and it made the API for what I wrote a bit more consistent, so I decided to keep it.

what makes a nice argument parser?

If the language is ripe for it, I find a mostly-declarative argument parser to be quite nice. They can coalesce long opts, short opts, help text, whatever in one neat bundle. I was actually initially wanting to make one of these for C a few months ago. Alas, C is not a language ripe for declarative-style compositions, and any time I worked my brain on the macro hell that’d emerge from such a library, I’d shunt the project idea to the archives for a few more weeks.

fanf’s article (and the rust crate it derives from) opted for an iterator-esque immediate parser. Duh! It’s the pretty obvious approach for an imperative language. Not sure why I didn’t view it from that angle before. If you ask me, the Rust crate is close to the ideal form of this style of argument parsing, with a loop that iterates over argv and matches against argument types, leaving the rest of the logic to the programmer. Without pattern matching, it means using some ugly if blocks, but that’s the cost of C.

implementation

I’m personally a fan of modeling iterators in C after C#/Rust iterators as opposed to C++ iterators. Mainly since the latter’s use in a for loop doesn’t translate to C too nicely, requiring bundling the value in the iterator state. In any case, iterators require keeping some kind of state somewhere for knowing where you are. fanf uses global variables for this at the cost of not being re-entrant, but in line with the “object-y” nature of iterators in general, I find it more idiomatic to shunt state to a struct that gets passed to the next() function:

typedef struct {<br>bool posonly;<br>int short_idx;<br>char **vec;<br>} ArgCtx;

static ArgCtx<br>arg_init(char **argv) {<br>return (ArgCtx) {<br>.posonly = false,<br>.short_idx = 0,<br>.vec = argv[0] == NULL ? argv : argv + 1,<br>};

static Arg<br>arg_next(ArgCtx *ctx) {<br>// ...

The iterator keeps extra state for whether the end of options mark was found (posonly) and the current index in a chain of short options (short_idx). The implementation of arg_next() is roughly similar to that of fnaf’s, just that I use a tagged union for arguments:

typedef struct {<br>ArgType type;<br>union {<br>char chr;<br>char *str;<br>};<br>} Arg;

If the argument is short, look at .chr. Otherwise, look at .str. There’s some macros for helping achieve the desired loop feel, and there’s an arg_err() function that works just like fanf’s:

#include...

like argument from argv nice short

Related Articles