What is New in Qt 5.2: QCommandLineParser

What's New in Qt 5.2: QCommandLineParser

By Jeff Tranter

Many programs need to accept command line options. For all but the simplest programs, the logic to validate and parse command line options can be significant. Support for handling this beyond what is provided by the standard C/C++ run-time library has been a long-missing Qt feature.

While it may appear simple, it is surprisingly hard to develop a clean API that will be both easy to use and satisfy most users' requirements. After several failed attempts in the past, thanks to the efforts of Qt and KDE developer David Faure, command line parsing support is now in Qt 5.2.0. It was based on similar code developed for the KDE project, and is an example of what will likely be several Qt modules that will migrate from the KDE Frameworks libraries into Qt.

Basics

The Qt documentation uses the term option for a command line parameter starting with a dash, and argument or positional argument for a parameter following the options, like a filename.

The new QCommandLineParser class provides facility for handling the command line options and arguments. The related class QCommandLineOption defines a single command-line option. Typically, you will create one QCommandLineParser instance in your application, and through it define the valid options and arguments, possibly creating several QCommandLineOption instances for specific options.

Once you define the valid options and arguments, the QCommandLineParser instance can parse and validate them and allow the programmer to get the options that have been parsed. It can also automatically handle help and version options. It supports single letter options (e.g. "-h") and long options, normally preceded by two dashes (e.g. "--help"). Options can also include values, specified in forms like -f foo, --file foo or --file=foo.

Some programs allow multiple single letter options to be combined together, for example, "-abc" as an alternative to "-a -b -c". It is also common that long options can use just a single dash rather than two, as in "-help". These two behaviors conflict, so to support both conventions there is a method in QCommandLineParser called setSingleDashWordOptionMode that can select the desired behavior. The default is to allow single letter options to be combined.

Example

The best way to illustrate the use of the new command line parsing facility is through an example. To try it out, I took a real-world existing application I had previously written for a hobby project and implemented command line support for the options and arguments it accepted. The details of what this particular application does are not important to this discussion, but it illustrates most of the common use cases for command line parsing. For this program, I wanted to support options like the following:

Usage: ./disasm8080 [options] filename
Disassembler for the Intel 8080 microprocessor.

Options:
  -h, --help               Displays this help.
  -v, --version            Displays version information.
  -n, --no-list            Don't list instruction bytes (make output suitable f
                           or assembler).
  -u, --uppercase          Use uppercase for mnemonics.
  -a, --address <address>  Specify decimal starting address (default is 0).
  -f, --format <format>    Use number format: 1=$1234 2=1234h 3=1234 4=177777 (
                           default is 1).

Arguments:
  filename                 Binary file to disassemble.

It needs to accept a filename positional argument as well as various options, some of which accept a value.

Let's look at the sample application code, line by line. First, we need to include the Qt headers for the classes that we will be using and define the standard C++ main function. As usual, we need a QCoreApplication (or QApplication if using widgets). The application name and version are used in the automatically generated help and version options, so it is important to set them.

#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDebug>
#include <QString>
#include <QStringList>

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QCoreApplication::setApplicationName("disasm8080");
    QCoreApplication::setApplicationVersion("1.0");

Typically, we are going to need a QCommandLineParser object. Like the application name and version, we also want set a description for the application using a method provided for that. Note that it is good practice to localize all the strings, as we are doing here and throughout the example. Because this code is not inside a class derived from QObject we have to use the more verbose QCoreApplication::translate() method rather than tr().

    QCommandLineParser parser;
    parser.setApplicationDescription(QCoreApplication::translate("main",
    "Disassembler for the Intel 8080 microprocessor."));

If we want to support the standard help ("-h" and "--help" and on Windows, "-?") and version ("-v" and "--version") options, we can do that with single method calls:

    parser.addHelpOption();
    parser.addVersionOption();

For positional arguments (as opposed to options), we specify them by calling addPositionalArgument(). In our case the application requires one, and only one, command line argument corresponding to the input filename. It is not possible here to indicate if the arguments are optional or required, so we need to enforce that by writing code to explicitly check (we'll see that later). The parameters to the method are the argument name and description. Optionally, a third parameter is a string to use in the usage output, but the default is to use the argument name, which works fine for us here.

    parser.addPositionalArgument("filename", QCoreApplication::translate("main", "Binary file to disassemble."));

That takes care of arguments, so now on to options. Our first option to support is "-n" or "--no-list" which takes no parameters. We need to create a QCommandLineOption object. We can do this in a variety of ways using different constructors and methods. This constructor I'm using takes a QStringList consisting of the names for the option (in this case we have two, a single letter and a long form), a description of the option, and an optional default value which is not required here since this option does not expect a value. Once the option is created, it is given to the parser using its addOption method. Notice that we don't localize the name of the option, as having the names of options change based on the current language could be confusing and likely to break scripts or other programs that call this one.

    QCommandLineOption noListOption(QStringList() << "n" << "no-list", QCoreApplication::translate("main", "Don't list instruction bytes (make output suitable for assembler)."));
    parser.addOption(noListOption);

The addOption method can fail, and it returns a Boolean status value indicating this. It will only fail if the parameters passed to it are not valid. This is not likely in tested code but we could perform a run-time check (perhaps an assert) if desired. Our next option to support, "-u" or "--uppercase" is the same and uses similar code:

    QCommandLineOption uppercaseOption(QStringList() << "u" << "uppercase", QCoreApplication::translate("main", "Use uppercase for mnemonics."));
    parser.addOption(uppercaseOption);

The next option, "-a" or "--address" takes an argument. It is added like the previous options but in this case, we can pass a suitable default value (0) as the third argument to the constructor. Note that the default value must be a string. We also have "-f" and "--format" options, which accept an argument, in this case a number in the range 1 through 4, defaulting to 1. We will have to write code later to check that the arguments are within the valid range.

    QCommandLineOption addressOption(QStringList() << "a" << "address", QCoreApplication::translate("main", "Specify decimal starting address (default is 0)."), QCoreApplication::translate("main", "address"), "0");
    parser.addOption(addressOption);

    QCommandLineOption formatOption(QStringList() << "f" << "format", QCoreApplication::translate("main", "Use number format: 1=$1234 2=1234h 3=1234 4=177777 (default is 1)."), QCoreApplication::translate("main", "format"), "1");
    parser.addOption(formatOption);

We've now defined the format of the parameters and options and can process the command line arguments. Note that when we created the QCoreApplication, Qt parsed and removed any standard Qt options. There is a potential for conflict if your options match any built-in options that Qt recognizes, since they will have been removed before parsing, so make sure your options are unique.

Now we can call the process method. If the options are not valid, the parser will display an error message and exit the application at this point. The method handles the standard version and help options here, if applicable.

    parser.process(app);

If we return from the method, the options and arguments were parsed as valid. We can get the positional arguments as a list of strings by calling:

    const QStringList args = parser.positionalArguments();

Now we can enforce further checking. In our case, there should be exactly one positional argument. If not, we can print a suitable error message and call showHelp() to infomr the user of the command usage and exit.

    if (args.size() != 1) {
        fprintf(stderr, "%s\n", qPrintable(QCoreApplication::translate("main", "Error: Must specify one filename argument.")));
        parser.showHelp(1);
    }

Another check we should make is that the format option is an integer in the range 1 through 4. We call value(), specifying the appropriate QCommandLineOption object, and convert it to an integer. Error handling is similar to what was done above:

    int format = parser.value(formatOption).toInt();
    if (format < 1 || format > 4) {
        fprintf(stderr, "%s\n", qPrintable(QCoreApplication::translate("main", "Error: Invalid format argument. Must be 1, 2, 3, or 4.")));
        parser.showHelp(1);
    }

We could do some similar checking on the address option as well but I leave that as an exercise for the reader. For now, we just get the value:

    int address = parser.value(addressOption).toInt();

To make the example useful, in the remaining code we can display all the options and argument values. In the real application, we could now continue with the application's main logic using the command line options and arguments we have parsed.

    qDebug() << "filename:  " << args.at(0);
    qDebug() << "no list:   " << parser.isSet(noListOption);
    qDebug() << "uppercase: " << parser.isSet(uppercaseOption);
    qDebug() << "address:   " << address;
    qDebug() << "format:    " << format;

    return 0;
}

Here are some examples of output from the sample program for various command lines (shown in bold):

% ./disasm8080 -h
Usage: ./disasm8080 [options] filename
Disassembler for the Intel 8080 microprocessor.

Options:
  -h, --help               Displays this help.
  -v, --version            Displays version information.
  -n, --no-list            Don't list instruction bytes (make output suitable f
                           or assembler).
  -u, --uppercase          Use uppercase for mnemonics.
  -a, --address <address>  Specify decimal starting address (default is 0).
  -f, --format <format>    Use number format: 1=$1234 2=1234h 3=1234 4=177777 (
                           default is 1).

Arguments:
  filename                 Binary file to disassemble.

% ./disasm8080 --help
Usage: ./disasm8080 [options] filename
Disassembler for the Intel 8080 microprocessor.

Options:
  -h, --help               Displays this help.
  -v, --version            Displays version information.
  -n, --no-list            Don't list instruction bytes (make output suitable f
                           or assembler).
  -u, --uppercase          Use uppercase for mnemonics.
  -a, --address <address>  Specify decimal starting address (default is 0).
  -f, --format <format>    Use number format: 1=$1234 2=1234h 3=1234 4=177777 (
                           default is 1).

Arguments:
  filename                 Binary file to disassemble.

% ./disasm8080 -v
disasm8080 1.0

% ./disasm8080 --version
disasm8080 1.0

% ./disasm8080 -n -u -a 1234 -f 2 program.bin
filename:   "program.bin" 
no list:    true 
uppercase:  true 
address:    1234 
format:     2 

% ./disasm8080 --no-list --uppercase --address=1234 --format 2 program.bin
filename:   "program.bin" 
no list:    true 
uppercase:  true 
address:    1234 
format:     2 

% ./disasm8080
Error: Must specify one filename argument.
Usage: ./disasm8080 [options] filename
Disassembler for the Intel 8080 microprocessor.

Options:
  -h, --help               Displays this help.
  -v, --version            Displays version information.
  -n, --no-list            Don't list instruction bytes (make output suitable f
                           or assembler).
  -u, --uppercase          Use uppercase for mnemonics.
  -a, --address <address>  Specify decimal starting address (default is 0).
  -f, --format <format>    Use number format: 1=$1234 2=1234h 3=1234 4=177777 (
                           default is 1).

Arguments:
  filename                 Binary file to disassemble.

% ./disasm8080 -x
Unknown option 'x'.

There are a few more methods provided by the QCommandLineParser and QCommandLineOption classes but we have covered all the critical ones in the example. You can consult the Qt documentation for more details. Because this facility is new in Qt 5.2.0, I suspect there may be new features added in the future. One possible enhancement that would be useful, for example, would be a facility to check that the arguments are in a valid range and of a given type (e.g. numeric).

Summary

The full source code and qmake project file for the example given can be downloaded from here. I hope this blog has familiarized you with one of the new features of Qt 5.2 and that you will make use of it in your own applications.