DEV Community

Cover image for Easy command line interfaces in C

Easy command line interfaces in C

Remo Dentato on September 21, 2023

Overview I already introduced the VRG library describing the support it provides for writing variadic functions in C. Here I'll discuss ...
Collapse
 
pauljlucas profile image
Paul J. Lucas

Yes, long-option support is a must, especially for programs that have lots of options (for example, my own wrap). While I personally always have a short-option equivalent for every long option, eventually you end up with collisions and have to use letters that aren't mnemonic β€”Β which is why having a long option helps the user. Additionally, all programs should accept the --help and --version options.

This may fall into personal taste, but when the user errs (e.g., forgets a required argument for an option), it's best to print an error message only about that and not a full dump of the complete usage leaving the user to have to scan a large output looking for the relevant portion of the usage for just that option.

Ideally, a robust CLI library should also have mechanisms for:

  • Being able to specify validator functions that parse and validate option arguments.
  • Being able to specify which options are mutually exclusive from all other options (e.g., if you specify --help, you may not specify any other option or program arguments).
  • Being able to specify which options are mutually exclusive with other options.
  • Being able to specify the same option more than once, for example -v for "verbose" output, but -vv for more verbose output. (Though you probably can support this now by having your handler code increment a counter.)

But I do agree that GNU's getopt_long() leaves a lot to be desired.

Collapse
 
rdentato profile image
Remo Dentato

Thanks for your comment! I'll think about long args.
Validation (including dependency between options) seems more complicated to add while keeping the current level of simplicity.
For example passing a check function vrgarg? SOmething like:
vrgarg("-x\tXmas",check)?

Collapse
 
pauljlucas profile image
Paul J. Lucas

It may be possible (paraphrasing Alan Kay) to have simple CLIs be simple, but complex CLIs be possible. I think it would be a neat trick if you could manage to do it all via a header-only library, but I think it's unlikely.

The GNU struct option ideally needs more fields. As for vrgarg, a _Bool check(char const*) function would be a start. You could have the library provide a few built-in checkers like vrgarg_check_int, vrgard_check_double, etc., but the user would be free to supply their own. The check function should be responsible for printing any error message since only it knows what the correct format is. The _Bool return value would only indicate to the library that a false value means "stop processing" and exit(EX_USAGE).

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

As you (and Alan Kay) said, It should be possible :)
It was just a dozen of lines of code, so I added the ability to specify a custom validator for each argument (possibly using additional parameters).
Have a look at the code on Github, or, if you feel inclined to do so, join the Discord server I just created to talk about vrg.

With this custom validators it should be quite easy to implement a logic of mutual exclusion between flags.

I'm not sure that providing pre-made validators would be useful, they would probably ending up being too simple and generic (and if this is what is needed, the users might write the validator themself).

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

IMHO, it's a bad idea to put full-blown function definitions into a header file, even if they are declared static.

It's also not clear why you're using char where _Bool is more appropriate. Similarly, for vrg_def_s:: hasarg, you're assuming char is signed β€” which it's not guaranteed to be. IMHO, you should use an enum, but if you insist on using signed integers, at least use signed char.

In general, the varargs stuff and the specific use-case for CLI arguments are too intertwined. If you want to make a varargs library, fine; if you want to make a CLI library, fine; but don't conflate them.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

Thanks for reminding me about the char signedness. I completely forgot.
I did not include <stdbool.h> as this would force anyone including "vrg.h" to have symbols like true defined even if they do not want to.
Yes, I might enforce that anyone should abide at least to the C99 standard but I can't see a benefit for me to put such a restriction.
Considering that the CLI functions will only be used where main() is, I thought it would be more convenient to ask the user to define a symbol before including "vrg.h" rather than having both to include "vrg.h" and link against a "vrg.o" object file. I might considering providing options for both usage. I need to think about it.
As for "mixing" things, I do see the topic of handling varidic "arguments" for functions and parsing arguments for CLI very related. But maybe it's just me, I'd love to hear from others. In any case, since I want to provide a flexible interface, I would need to define the variadic function piece anyway.
Thanks for your comments, I'm now adding the long options as you suggested and I will probably add a way to specify a full "usage string" because I recognize that the automatically information generated by vrgusage() might be not enough. Imagine you want to provide the help In other languages than English ...

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

I never said you needed to #include <stdbool.h> nor use true or false; I said to use _Bool which is a type built into C99.

Yes, I might enforce that anyone should abide at least to the C99 standard but I can't see a benefit for me to put such a restriction.

C99 is 24 years old. I think it's perfectly fine to assume that compilers support it.

Thread Thread
 
rdentato profile image
Remo Dentato

My fault, I always see using _Bool and including stdbool.h as a single thing.
In any case, looking at the current code, you'll notice that now that field is used to hold a set of bit flags, not just as boolean (actually this always was the case, the old name suggested it was a simple boolean, that's way I changed it).
Now, it could make sense to have vrg_argfound defined as _Bool but I don't see the need.
Rather, I would be very interested in your comments on the way the CLI is defined. I saw your article and your wrap repo.
The way the CLI is defined, the short/long options are specified, etc, do they seem simple to you? Or, on the contrary, they look too confusing? WHat do you think?

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

If you're using bit flags, you should definitely use an unsigned type; or a type like uint8_t.

I haven't given a lot of thought to how I would write an option parser, but I'd definitely have a distinct object for each option, then an array of pointers to option to pass to a function to parse all options. I don't see why varargs are needed at all.

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

Yes, it is an unsigned type, in fact.
The variadic functions are needed because I wanted to offer a flexible interface. One can define an option simply as:

   vararg("-n, --nodes num\tSet the number of nodes") {
   }
Enter fullscreen mode Exit fullscreen mode

or may want to add a custom validator (was one of your suggestions, right?):

   vararg("-n, --nodes num\tSet the number of nodes", isgreaterthan, 0) {
   }
Enter fullscreen mode Exit fullscreen mode

Similarly, for vrgcli() you can omit the argc and argv arguments if they are the idiomatic ones.
I wanted the functions to be simple to use yet flexible.

Under the hood, the argument definitions are kept in a linked list rather than an array.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

The general problem with varargs is they're not type-safe. I'd prefer always providing a validator even if it's a pre-defined do-nothing one (for string values).

Thread Thread
 
rdentato profile image
Remo Dentato • Edited

I'm not using stdarg.h. The technique I use enforces type checking like any regular call.
Validators can have additional arguments (like in the example I used isgreaterthen, 0). Yes you can have different validators (isgreaterthan20, islowerthan31, ...) but I thought that giving the ability to specify additional parameters would have been easier for the programmer.
Can you suggest a set of validators you think would be generally useful to have out of the box?

Collapse
 
rdentato profile image
Remo Dentato

I'm going to add the possibility of translating the various generated messages so to allow the creation of CLI in languages other than English.

I really don't like translated CLI (and I'm not a native English speaker) but I understand this might be a need for certain types of tools.
What do you think?

Collapse
 
andreashnida profile image
Anders

I'm abolutely loving it. Thank you so much. πŸ˜ƒ

Collapse
 
rdentato profile image
Remo Dentato

UPDATE: vrg now allows the configuration the user messages. An example for Japanese has been added to the code on GH

Collapse
 
rdentato profile image
Remo Dentato

UPDATE:
Just pushed a new version with support for long options and modified the article accordingly.
Thanks for your feedback. I'll be happy to hear any comment you may have.