Skip to content

Defining Options

gershnik edited this page Apr 5, 2022 · 8 revisions

General

Defining options is conceptually very simple. You need to create and configure an Option (or WOption) object and add it to a parser via its add() method. An option definition consists of:

  • A set of names such as: "-c", "--cd", "--directory". The names are the only required part of an option, so they must be passed in the constructor. Order of names doesn't matter except for which one is the first one. The first name (also called the main name) is used as the identifier of the option when it needs to be referred to in other contexts. It is also the one displayed in the usage string (produced by formatUsage). All names are shown in help (produced by formatHelp). Names must start with one of the option prefixes configured for the parser.

  • A handler, which is a function object (often a lambda) that is invoked when the option is encountered during parsing out. A handler is not strictly required - if you don't provide one a default handler simply does nothing. Since doing nothing for an option is rare (but happens) usually you do need to provide one. The handler also indicates whether the option accepts an argument and, if so, whether the argument is optional. It works like this:

    • If the handler doesn't have arguments then the option doesn't have an argument.
    • If the handler has an argument that is convertible from std::string_view (or std::wstring_view for WOption) then the option has a required argument. Its value will be passed as the argument to the handler.
    • If the handler has an argument that is convertible from std::optional<std::string_view> (substitute "w" as usual for wide options) then the option has an optional argument. Its value or std::nullopt will be passed as the argument to the handler.

    Pretty straightforward isn't it?

  • A help string. This is the description of the option produced when generating help via formatHelp method. Not required but obviously highly desired if you use Argum's generated help.

  • A name for the argument. This name is used as a placeholder for the argument in the generated usage string and help (e.g. output of formatSyntax and formatHelp methods). If not specified, by default it is set to ARG. Obviously this is only relevant for options that have an argument.

  • Allowed number of occurrences. By default it is "zero or more", that is unrestricted. You can change that to anything you want including "once", "from 2 to 5" etc. Note that some people consider a "required option" to be a violation of normal command line expectations.

  • Whether the option should require its argument to be "attached". Given options -f and --foo usually the arguments can be specified either detached like: -f ARG and --foo ARG or attached like: -fARG and --foo=ARG. You can choose to only allow the second syntax. This can be handy if your option argument is itself optional and you also have positional arguments. See below

Putting it all together here is an example of an option with one arguments that must happen exactly once.

std::string name;

parser.add(
        Option("--name", "-n").   //two names, --level is the main one
        argName("NAME").          //argument is named NAME in help output
        help("some kind of name"). 
        occurs(once).             //must occur exactly once
        handler([&](const std::string_view & value) {
            name = value;
    }));

Note that the configuration methods can be conveniently chained together. Assuming nothing else, other than help option is defining running this program will produce:

$ ./prog --help                                         
Usage: ./prog [--help] --name NAME

options:
  --help, -h           show this help message and exit
  --name NAME, -n NAME some kind of name

Quantifiers

The occurs() call takes a Quantifier object that specifies minimum and maximum number of times the option can occur. There are some predefined quantifiers such as: zeroOrOneTime or neverOrOnce (both mean the same), oneTime or once, zeroOrMoreTimes (this is the default for options) and oneOrMoreTimes or onceOrMore. These generally cover most common scenarios. For anything else you can pass a Quantifer(min, max). To specify "unlimited" for max pass Quantifer::infinity.

Generally, unless the option has some very special semantics it is recommended to use the default - any number of occurrences. Then have a sensible default (if the option isn't used) and override or accumulate semantics if the option occurs more than once. Here is an example of accumulative option that has no arguments:

unsigned verbosity = 0;

parser.add(
        Option("-v", "--verbose"). //note that short -v is the main name 
        help("produce verbose output. Specify more than once, e.g. -vv to make it more verbose"). 
        handler([&]() {
            ++verbosity;
    }));

The help for this will look like this

$ ./prog --help                                         
Usage: ./prog [-v] [--help]

options:
  -v, --verbose  produce verbose output. Specify more than once, e.g. -vv to
                 make it more verbose
  --help, -h     show this help message and exit

Finally here is an example with an optional argument

std::string name = "tgz";

parser.add(
        Option("--format").   
        argName("FORMAT"). 
        help("some kind of format, default is tgz"). 
        handler([&](const std::optional<std::string_view> & value) {
            if (value)
              name = *value;
    }));

This will produce the following help

$ ./prog --help                                         
Usage: ./prog [--format [FORMAT]] [--help]

options:
  --format [FORMAT]  some kind of format, default is tgz
  --help, -h         show this help message and exit

Requiring attached argument

Consider what would happen if you have an option, say, --compress that accepts an optional argument (perhaps compression algorithm) and you also have positional arguments like filenames. What does command line like

$ ./prog --compress file1.txt file2.txt

mean? Is file1.txt an argument to --compress or a filename? Most other libraries will say it is a detached argument to --compress and by default Argum does the same. Using command line above will likely result in "unknown algorithm file1.txt" error from your application. The usual workaround if you want file1.txt to be positional is to put -- before positional arguments, like this:

$ ./prog --compress -- file1.txt file2.txt

This works but is kind of ugly and can be unexpected to some users.

Argum allows you to require that option argument be specified only using attached syntax. A detached argument will be interpreted as positional in this case. So both of these will work

$ ./prog --compress file1.txt file2.txt
$ ./prog --compress=gzip file1.txt file2.txt

To configure the option like this all you need to do is to call requireAttachedArgument(true) when configuring an option.

parser.add(
        Option("--compress").   
        argName("ALGORITHM"). 
        requireAttachedArgument(true).
        help("compress output using given algorithm (gzip by default)"). 
        handler([&](const std::optional<std::string_view> & value) {
            ...
    }));

Now the help for this option will also indicate the required syntax

$ ./prog --help                                         
Usage: ./prog [--compress [ALGORITHM]] [--help]

options:
  --compress[=ALGORITHM]  compress output using given algorithm (gzip by default)
  --help, -h              show this help message and exit

Further reading

Reporting errors from handlers Parsing common data types Validation