I recently had a minor task involving changing an option - on one of our
command line tools - from taking a required argument, to taking an optional
argument. This should be easy they said; just change the respective option
struct to take an optional argument, add a colon to the optstring
, and get
on with your life.
Well, it proved to be easier said than done. My initial expectation was that a solution similar to the one below should just work. And it does work, just not in the way I expected.
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
int main(int argc, char *argv[])
{
int opt;
const struct option options[] =
{
{"no-arg", no_argument, 0, 'n'},
{"opt-arg", optional_argument, 0, 'o'},
{"req-arg", required_argument, 0, 'r'},
{NULL, 0, 0, '\0'}
};
while ((opt = getopt_long(argc, argv, "no::r:", options, NULL))
!= -1)
{
switch(opt)
{
case 'n': // option with no argument
printf("No argument\n");
break;
case 'o': // option with optional argument
printf("Optional argument: %s\n",
(optarg == NULL) ? "default" : optarg);
break;
case 'r': // option with required argument
printf("Required argument: %s\n", optarg);
break;
default:
fprintf(stderr, "Usage: %s [-n] [-o [arg]] [-r arg]\n",
argv[0]);
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
After making these minor changes - I manually tested the code - and to my despair, the code kept giving me the default argument on the options with optional arguments, using short options.
$ ./main -r testarg # -r with required argument 'testarg'
Required argument: testarg
$ ./main -o testarg # -o with optional argument 'testarg'
Optional argument: default
Taking the red pill; I went head first into the Linux man pages, in order to figure out what was wrong with my code. It turned out there was nothing wrong with the code at all.
Two colons mean an option takes an optional arg; if there is text in the current argv-element (i.e., in the same word as the option name itself, for example, “-oarg”), then it is returned in optarg, otherwise optarg is set to zero.
Confusing, right? In other words, getopt_long
expects no whitespace between
an option and its optional argument. For options with required arguments on
the other hand, it simply does not care. Who would have guessed?
$ ./main -otestarg # -o with optional argument 'testarg'
Optional argument: testarg
Yes it works! But we cannot really expect our users to magically know that optional arguments have to be passed in this special way, right? Thus, we’ll need to do something about it!
Solution
The solution I arrived at to solve this issue is as follows:
-
If
optarg
isNULL
, it means there are either no arguments, or the option and the argument is separated by a whitespace character. In this case, the potential argument - if present - would be the next string inargv
. -
The variable
optind
has the index of the next argument to be processed inargv
, thus we can use it to get a hold of our potential argument. But before accessingargv
, we need to make sure we don’t read out of bounds of the array. We can do this by simply checking thatoptind
is less than the argument countargc
, before accessingargv
at this index. -
We determine whether or not the next string in
argv
is actually an argument, based on the fact that another option would have to start with the'-'
character. Thus we compare the first character of the next string inargv
with the'-'
character, assuming it’s an argument in case of inequalilty. -
When all these conditions are met, our conclusion is that the next string in
argv
is in fact an argument. We can extract the argument by settingoptarg
to point at the next string inargv
, this followed by advancingoptind
accordingly.
The following is an example of how you could implement and use this.
case 'o': // option with optional argument
if (optarg == NULL && optind < argc
&& argv[optind][0] != '-')
{
optarg = argv[optind++];
}
if (optarg == NULL)
{
// Handle is present
}
else
{
// Handle is not present
}
break;
Hooray! The options now work as previously expected.
$ ./main -r testarg # -r with required argument 'testarg'
Required argument: testarg
$ ./main -o testarg # -o with optional argument 'testarg'
Optional argument: testarg
I have to admit, the solution above is kind of hacky. For a neater solution, that improves readability and avoids great code duplication, you can wrap it all up in a macro like I did below.
#define OPTIONAL_ARGUMENT_IS_PRESENT \
((optarg == NULL && optind < argc && argv[optind][0] != '-') \
? (bool) (optarg = argv[optind++]) \
: (optarg != NULL))
(We use all caps to signify that this is a macro, not a normal function or variable). And use the macro like this:
case 'o': // option with optional argument
if (OPTIONAL_ARGUMENT_IS_PRESENT)
{
// Handle is present
}
else
{
// Handle is not present
}
break;
This macro, and many other macros, parsing functions, data structures, etc. are available in our standalone library, libntech. Feel free to use it in your own C/C++ projects.
If this blog post was helpful in any way, or if you know of any better ways to get options with optional arguments working like one would expect. Please let us know over at GitHub Discussions! Remember, sharing is caring.