Optional arguments with getopt_long(3)

Posted by Lars Erik Wik
August 13, 2021

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 is NULL, 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 in argv.

  • The variable optind has the index of the next argument to be processed in argv, thus we can use it to get a hold of our potential argument. But before accessing argv, we need to make sure we don’t read out of bounds of the array. We can do this by simply checking that optind is less than the argument count argc, before accessing argv 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 in argv 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 setting optarg to point at the next string in argv, this followed by advancing optind 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.