6 ms·
C Posix-compliant argument parsing in 42 LoC, inspired by Duff's device
- gabrielsroka 4y agoCompliant
- classified 4y agoStill, complaint argument parsing is an important and undervalued skill, POSIX or not.
- metadat 4y agoI clicked through to the link before reading any comments: "In C? I wonder if it's English only?" Then proceeded to become extremely confused: "How is this going to help me parse a complaint? It looks like arg parsing..." ^^
- camel-cdr 4y agoWhoops, is it possible to edit the title?
- deleted 4y ago[deleted]
- planede 4y agoI appreciate the macromancy, but it's not very readable. I'm sure it's very efficient though.
- deleted 4y ago[deleted]
- joosters 4y agoPerfect for the all-too-common situation where your performance bottleneck is in command line argument parsing...
- chii 4y agodon't people just use https://www.gnu.org/software/libc/manual/html_node/Getopt.html https://www.gnu.org/software/libc/manual/html_node/Getopt.ht... , rather than reinvent the wheel?
- zokier 4y agoWhat does posix compliant mean in this context?
- klyrs 4y agoIt (edit: claims to) conforms to the POSIX spec for command line arguments https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...
- planede 4y agoThat document does not describe long options. I guess that can be treated as a conforming extension. The C implementation does not seem to have support for optional option arguments. The C implementation also does not seem to support "--" as a delimiter for option arguments, although that's only a guideline. So I guess it can be used to implement POSIX compliant interfaces that don't use optional option arguments. edit: supporting "--" as delimiter might not be a guideline: The utilities in the Shell and Utilities volume of POSIX.1-2017 that claim conformance to these guidelines shall conform completely to these guidelines as if these guidelines contained the term "shall" instead of "should". edit2: Seems like I was completely wrong about supporting "--", when tried out it does seem to support it. One weird corner cases is the call `<prog> -- -`, where "-" isn't interpreted as enabling stdin, but treated as a regular positional argument. `echo asd | cat -- -` reads from stdin with GNU cat.
- klyrs 4y agoOn a closer read, the comment mentions plan9's arg(3): https://9fans.github.io/plan9port/man/man3/arg.html https://9fans.github.io/plan9port/man/man3/arg.html Generally, I think it's pretty normal to interpret "POSIX compliant" as a minimum requirement, and then extend that to your own whims as long as you don't break the POSIX part.
- mtlmtlmtlmtl 4y ago"--" is a GNU originated thing afaik.
- metadat 4y agoFor the initiated, Duff's Device is a technique for manually implementing loop unrolling. https://en.wikipedia.org/wiki/Duff%27s_device https://en.wikipedia.org/wiki/Duff%27s_device
- Arnavion 4y ago(uninitiated. The initiated would already know what it is.)
- metadat 4y agoThanks for the typo correction.
- rramadass 4y agoPSA: Read the "simplified explanation" near the bottom of the wiki page (https://en.wikipedia.org/wiki/Duff%27s_device#Simplified_explanation https://en.wikipedia.org/wiki/Duff%27s_device#Simplified_exp...) before looking at the actual code. I always found Duff's device confusing to understand (i.e. walk-through in my mind) and this section was what made it click.
- xfennec 4y agoHow this code can enter the "if (0)" part?
- pcwalton 4y agoA switch can jump inside an if body that contains case labels, causing the condition not to be evaluated. C switch/case semantics are... not the most obvious.
- mark_undoio 4y agoBecause there's really a switch statement (hidden behind a macro) that will jump to labels within that block. The fact that the if condition is false means it won't just run the whole block straight through but you can still jump to a label in it. A goto statement would also allow you to jump into an otherwise-unreachable block.
- mtlmtlmtlmtl 4y agoARG_BEGIN opens a switch statement. So it can enter any of the cases, or it'll jump to the else if part.
- kreetx 4y agoIs there any reason this is not written as `switch(..) { <case statements>; default: <else statements> }`. I.e is there a reason for `if(0)`?
- jsmith45 4y agoThe whole `if` portion is for if the second character of the argument (after the initial `-`) is a second `-` [0]. In that case, the switch jumped to `case '-':` hidden within the macro, and the if statement is about processing long arguments (like `--help`). arguments only available as a short flag should never get executed as part of a short flag, so putting them inside the if(0) case is an easy option. An alternative without if(0) of any variety would be: ARG_BEGIN { if (ARG_LONG("reverse")) case 'r': { reverse = 1; ARG_FLAG(); } else if (ARG_LONG("input")) case 'i': { input = ARG_VAL(); } else if (ARG_LONG("output")) case 'o': { output = ARG_VAL(); } else if (ARG_LONG("help")) case 'h': case '?': { printf("Usage: %s [OPTION...] [STRING...]\n", argv0); puts("Example usage of arg.h\n"); puts("Options:"); puts(" -a, set a to true"); puts(" -b, set a to true"); puts(" -c, set a to true"); puts(" -r, --reverse set reverse to true"); puts(" -i, --input=STR set input string to STR"); puts(" -o, --output=STR set output string to STR"); puts(" -h, --help display this help and exit"); return EXIT_SUCCESS; } else { default: fprintf(stderr, "%s: invalid option '%s'\n" "Try '%s --help' for more information.\n", argv0, *argv, argv0); return EXIT_FAILURE; } break; case 'a': a = 1; ARG_FLAG(); break; case 'b': b = 1; ARG_FLAG(); break; case 'c': c = 1; ARG_FLAG(); break; case '\0': readstdin = 1; break; } ARG_END; The downside is that those few short flag only arguments no longer line up nicely with the others, and they come after the default, which could be a little bit confusing. Footnote: [0] Except if that second dash is the last character of the argument, in which case -- is the end of flags marker, meaning any further arguments that begin with `-` are just funky positional paramaters
- camel-cdr 4y agoThis uses switch abuse to handle short and long arguments in one place: ... else if (ARG_LONG("option")) case 'o': { } ... The idea was to leave all the decisions to the library user, so there are no automatic help pages and error messages. The library essentially just gives you a new language primitive to work with. Example usage (also included in the file): // assumes argv and argc exist ARG_BEGIN { if (0) { case 'a': a = 1; ARG_FLAG(); break; case 'b': b = 1; ARG_FLAG(); break; case 'c': c = 1; ARG_FLAG(); break; case '\0': readstdin = 1; break; } else if (ARG_LONG("reverse")) case 'r': { reverse = 1; ARG_FLAG(); } else if (ARG_LONG("input")) case 'i': { input = ARG_VAL(); } else if (ARG_LONG("output")) case 'o': { output = ARG_VAL(); } else if (ARG_LONG("help")) case 'h': case '?': { printf("Usage: %s [OPTION...] [STRING...]\n", argv0); puts("Example usage of arg.h\n"); puts("Options:"); puts(" -a, set a to true"); puts(" -b, set a to true"); puts(" -c, set a to true"); puts(" -r, --reverse set reverse to true"); puts(" -i, --input=STR set input string to STR"); puts(" -o, --output=STR set output string to STR"); puts(" -h, --help display this help and exit"); return EXIT_SUCCESS; } else { default: fprintf(stderr, "%s: invalid option '%s'\n" "Try '%s --help' for more information.\n", argv0, *argv, argv0); return EXIT_FAILURE; } } ARG_END; // argv and argc now hold the non-option arguments
- cperciva 4y agoAnother option with similar levels of macro (ab)use is my "magic getopt" which lets you write code like this: const char * ch; while ((ch = GETOPT(argc, argv)) != NULL) { GETOPT_SWITCH(ch) { GETOPT_OPT("-a"): aflag = 1; break; GETOPT_OPTARG("--bar"): printf("bar: %s\n", optarg); break; GETOPT_OPTARG("-f"): printf("foo: %s\n", optarg); break; GETOPT_MISSING_ARG: printf("missing argument to %s\n", ch); /* FALLTHROUGH */ GETOPT_DEFAULT: usage(); } }
- chungy 4y agoswitch options falling through doesn't seem like magic to me. Rather, a common practice.
- cperciva 4y agoThat's not the magic part...
- ufo 4y agoI'm intrigued. Is there somewhere where we could find the full definition of these macros?
- cperciva 4y agohttps://github.com/Tarsnap/libcperciva/blob/master/util/getopt.h https://github.com/Tarsnap/libcperciva/blob/master/util/geto... https://github.com/Tarsnap/libcperciva/blob/master/util/getopt.c https://github.com/Tarsnap/libcperciva/blob/master/util/geto... BSD licensed, of course.
- ZephyrP 4y agoVery neat. Do you implement option parsing in "magic getopt" or can you (somehow?) handle setting up the option string arguments used by the more familiar variants in getopt(3)?
- asveikau 4y agoThis is off topic, but your stretchy buffer does one of my pet peeves: (a)->at = realloc((a)->at, (a)->_cap * sizeof *(a)->at)) When realloc fails, the old value of (a)->at will be leaked. And memory allocation failure also leads to null pointer dereference in this code.
- deleted 4y ago[deleted]
- camel-cdr 4y agoAh, my way of dealing with memory allocation failure is to write a xmalloc/xcalloc/xrealloc that exit on error. So the idea would be that the library users can just: #unded malloc #define malloc xmalloc ... I suppose I could make this the default behavior, and make it overwritable.
- mtlmtlmtlmtl 4y agoDoesn't this break things like Valgrind?
- asveikau 4y agoFirstly, an xmalloc will call malloc and add error checks. So malloc is still called. But more fundamentally, Valgrind hooks into the allocator (and the program at large) much more deeply. You could write your own allocator (many people deploy something other than libc's) and it would still figure it out.
- kens 4y agoI realized that I essentially never use a "switch" statement. It seems like a control-flow construct that made sense in the 1970s to help the compiler generate jump tables, but doesn't seem particularly useful now. Moreover, it seems error-prone with accidental fall-through. And doing "fancy" things with it makes code very hard to read. Does anyone else find "switch" kind of redundant?
- aidenn0 4y agoI use switch statements all the time for state-machines. There are linters that will warn on fallthrough; you should use one of them.
- oweiler 4y agoI still find switch statements easier on the eye than a chain of if-else-if-...else.
- tristan957 4y agoswitch statements are better for certain cases since the compiler can warn if you aren't handling an enum variant within it.
- silisili 4y agoI typically use them with more than 2 or 3 options as I find them much easier to read. In addition, the compiler/vm typically can optimize them better than long if chains, depending on implementation, if that matters to you
- Arch-TK 4y agoIf you don't need long options and don't want to use getopt (for whatever misguided reason) then I wrote a (rare) blog post about doing this without any macro abuse: https://the-tk.com/post/2021/07/29/option-parsing-on-a-budget.html https://the-tk.com/post/2021/07/29/option-parsing-on-a-budge... .
- kazinator 4y agoThis invokes UB, unfortunately. It works where it works. ISO C says that: "The parameters argc and argv and the strings pointed to by the argv array shall be modifiable by the program, and retain their last-stored values between program startup and program termination." Thus within the boundaries of defined ISO C, you can do this: argv++; // argv pointer to array element modifiable and you can do this: argv[0][0]++; // the string pointed to by the array is modifiable. but not this: argv[0]++; // array itself (the pointers) not required to be modifiable I seem to recall finding a problem like this in the K&R2 book also, probably close to thirty years ago. I don't have the book now, but the page number 117 seems to be coming up in my dim memory.
- noyoudumbdolt 4y agoI'm skeptical there's a single C compiler out there that makes the first two behave as expected but not the third. In fact, I wouldn't be surprised if the real oversight is that the standard meant to say all three, or that the authors thought it implied.
- kazinator 4y agoIt's not necessarily up to a compiler, or its program setup stub code, but possibly up to the host environment. Of course argv is mutable since it's a local variable/param. But what if the pointers are placed, by the OS, into read-only VM pages? Begrudgingly, the string data is made writable though, to conform to the standard.
- noyoudumbdolt 4y agoPlease name one platform where this is the case.
- kazinator 4y agoAnother thing to consider is that the pointers may be writable, but changing them causes harm. E.g. suppose argv[] is prepared by a function which dynamically allocates each pointer, and then the run-time calls free on it when the program exits. According to ISO C, that would be conforming. I don't know of any platform where there are any ill effects. But the number of platforms out there with C implementations is much larger than my direct experience. Maybe this is just some historic baggage that is overdue for a revision. It was discussed in 1998, but no changes were accepted at the time. In most coding situations, I probably wouldn't care, the same way I don't care about, say, platforms where for some reason you cannot compare pointers to distinct objects using inequality (another thing not required by ISO C). It's better to break the rules knowingly, though. You want to be badass, not dumbass.