C++: Parsing command line arguments with Boost

Still feeling excited about modern C++, I decided to install Boost with MS Visual Studio 2015 RC. Of course, most of Boost is header-only, but there are some libraries with binary components. One of those is Boost.Program_options whose purpose is to provide nice interface to reading program options from either the command line or a config file.

Feeling ambitious, I decided to build as much as I can without re-compiling a bunch of other libraries such as zlib, and bzip2, and libjpeg etc. After all, this is just for experimenting around.

It took a few trials and errors to get to a point where I could compile and link the following simple program from the command line:

#include <iostream>
#include <boost/program_options.hpp>

namespace po = boost::program_options;

void
process_program_options(const int argc, const char *const argv[])
{
    po::variables_map args;
    return;
}

int main(int argc, char *argv[])
{
    parse_program_options(argc, argv);
    return 0;
}

using the command line cl /MT /EHsc /O1 /favor:INTEL64 /Ic:\...\boost\include\boost-1_58 test1.cpp /link /LIBPATH:c:\...\boost\lib.

Things tend to have more friction on Windows. On top of that, I am using the release candidate version of a compiler that is not recognized by boost. Ultimately, I settled on the following command to build the binary components of boost:

C:\...\src\boost_1_58_0> b2 --prefix=c:\...\boost --toolset=msvc-14.0 architecture=x86 threading=multi variant=release address-model=64 link=static runtime-link=static --with-date_time --with-filesystem --with-serialization --with-test --with-thread --with-system --with-regex --with-iostreams --with-atomic --with-chrono --with-exception --with-graph --with-timer --with-program_options --with-math --with-log --with-container --with-coroutine --with-context --with-random install

after running bootstrap.bat. I added the static linking options after I could not figure out why the linker kept looking for static versions of the libs, and failing to find them (because I had asked for shared). That is something to think about later. For now, my only purpose was to build executables using VS 2015 RC given that it has much better coverage of modern C++.

So, let's add some command line options which will be recognized by our little program. For now, let's start with a simplest command line option: help.

It's also time to give the program a name. I am using this as an excuse to build a small program that will help me find duplicates in my photo collection. I am going to call it file-stat-collector. Yup, that is a mouthful, but so what? These days, all shells have tab-completion.

First, we need to describe the options our program accepts. We do that via a boost::program_options::options_description object. With the aliased namespace, the declaration and instantiation of the object for a single help option becomes:

po::options_description desc("Allowed options");
    ("help", "Show brief usage information")
;

The options_description object does not do much by itself. To actually process the command line arguments, we need to actually parse the command line, and store the results in our boost::program_options::variables_map:

#include <iostream>
#include <boost/program_options.hpp>

namespace po = boost::program_options;

void
process_program_options(const int argc, const char *const argv[])
{
    po::options_description desc("Allowed options");
    desc.add_options()
        ("help", "Show brief usage message")
    ;

    po::variables_map args;
    po::store(
        po::parse_command_line(argc, argv, desc),
        args
    );
    po::notify(args);
    return args;
}

int main(int argc, char *argv[])
{
    parse_program_options(argc, argv);
    return 0;
}

“The notify function runs the user-specified notify functions and stores the values into regular variable.” Just in case you were wondering.

The program still doesn't seem to do anything at this point. Giving it an unrecognized option throws an exception, and running it with the --help option doesn't produce any output.

The exceptions that can be thrown inherit from std::logic_error which inherits the what member function from std::exception, so we can wrap the parsing and storing of the option values in a try/catch block to print out an error message, and exit instead of Windows trying to report the crash to Microsoft (in case you haven't turned that setting off ;-)

#include <cstdlib>
#include <iostream>
#include <boost/program_options.hpp>

namespace po = boost::program_options;

void
process_program_options(const int argc, const char *const argv[])
{
    po::options_description desc("Allowed options");
    desc.add_options()
        ("help", "Show brief usage message")
    ;

    po::variables_map args;

    try {
        po::store(
            po::parse_command_line(argc, argv, desc),
            args
        );
    }
    catch (po::error const& e) {
        std::cerr << e.what() << '\n';
        exit( EXIT_FAILURE );
    }
    po::notify(args);
    return;
}

int main(int argc, char *argv[])
{
    process_program_options(argc, argv);
    return 0;
}

With that in place, the program now produces:

C:\...\fsc> file-stats-collector -u
unrecognised option '-u'
C:\...\fsc> file-stats-collector --help

We need to do something about that missing help message!

For now, the following function will take care of showing the help message:

void
show_help(const po::options_description& desc, const std::string& topic = "")
{
    std::cout << desc << '\n';
    if (topic != "") {
        std::cout << "You asked for help on: " << topic << '\n';
    }
    exit( EXIT_SUCCESS );
}

For now, this just stringifies to printing out all the help strings given in desc. The topic argument can later be used to specialize the messages produced.

We will call this if there are no command line arguments:

if (argc == 1) {
    show_help(desc);
}

But, more importantly, we will modify the help option specifier a bit:

"help,h",
po::value< std::string >()
    ->implicit_value("")
    ->notifier(
        [&desc](const std::string& topic) { show_help(desc, topic); }
    ),
"Show help. If given, show help on the specified topic."

Now the help option optionally takes a string argument which the user can use to specify a topic. We specify an implicit_value so our program does not complain if a value is not given on the command line. Finally, we add a notifier that will be automatically called with the value of the help option.

The argument for notifier is required to be a function taking a single argument, but we want to pass to it both the topic, and the current options_description instance desc. So, we use a closure which captures desc by reference. It simply calls show_help with this instance, and the topic argument to the help option.

The complete program becomes:

#include <cstdlib>
#include <iostream>
#include <boost/program_options.hpp>

namespace po = boost::program_options;

void
show_help(const po::options_description& desc, const std::string& topic = "")
{
    std::cout << desc << '\n';
    if (topic != "") {
        std::cout << "You asked for help on: " << topic << '\n';
    }
    exit( EXIT_SUCCESS );
}

void
process_program_options(const int argc, const char *const argv[])
{
    po::options_description desc("Usage");
    desc.add_options()
        (
            "help,h",
            po::value< std::string >()
                ->implicit_value("")
                ->notifier(
                    [&desc](const std::string& topic) {
                        show_help(desc, topic);
                    }
                ),
            "Show help. If given, show help on the specified topic."
        )
    ;

    if (argc == 1) {
        show_help(desc); // does not return
    }

    po::variables_map args;

    try {
        po::store(
            po::parse_command_line(argc, argv, desc),
            args
        );
    }
    catch (po::error const& e) {
        std::cerr << e.what() << '\n';
        exit( EXIT_FAILURE );
    }
    po::notify(args);
    return;
}

int main(int argc, char *argv[])
{
    process_program_options(argc, argv);
    return 0;
}

Now, when we run the program, we get:

C:\...\fsc> file-stats-collector --verbose
unrecognised option '--verbose'

C:\...\fsc> file-stats-collector -h
Allowed options:
  -h [ --help ] arg     Show help. If given, show help on the specified topic.


C:\...\fsc> file-stats-collector --help 42
Allowed options:
  -h [ --help ] arg     Show help. If given, show help on the specified topic.

You asked for help on: 42

Obviously, this is not earth-shattering. But, compared with 20th century C++, it is a phenomenal improvement. Foremost, there is a not a single explicit memory allocation in sight. In less than 60 lines, we have the basic machinery of parsing command line options, and providing a brief usage message. There is also not a single platform specific line in what I wrote. I haven't tried yet, but presumably what wrote will work the same way on any system where it compiles.

Next time, we'll add the ability to specify the directories to be searched, and use Boost.Filesystem to traverse those directories.

PS: See how to specify starting directories and walk them in the second part of this post.

PPS: You can discuss this post on /r/cpp.

PPPS: ~~Could you please vote for Microsoft to fix this bug in the VS2015 C compiler?~~ The Erroneous error C2099 bug in the Visual Studio 2015 RC C compiler was fixed this evening.