I have been spending time recently writing command line apps in C++11. Each time I wanted a way of handling command line arguments flexibly. I chose to use the boost::program_options library. The documentation is pretty good but there are some assumptions (aliased namespace) and the example code is broken up with paragraphs of text explaining what the code does.

When I start to explore an API to a library I start by writing tests. After working with the program_options library on a few small applications the same pattern came up each time so I thought I would share the results as a complete app, although admittedly the app does nothing other than accept command line parameters.

The full source code can be found at http://github.com/grahambrooks/boost-program-options

The application is also linked up to a continuous integration server that triggers a build and test on each commit.

To keep the dependencies as low as possible the tests are written using the boost unit testing framework.

The tests are somewhat artificial in that they test the command line options results in an application state and not an observable behavior but are perfectly valid test cases.

Basic use case

 app -v --version -username [value] file file

The basic use case offers two variations of switch (-s –switch) a numeric argument option and a list of files.

== Test: No parameter behavior

So the first test verifies that we can handle (detect) that no command line parameters were supplied.

#include <boost/test/unit_test.hpp>

#include "command_line_argument_parser.hpp"

BOOST_AUTO_TEST_CASE(recognises_no_supplied_arguments) {
  command_line_argument_parser parser;
  const char *argv[] = {"app"};
  auto args = parser.parse(sizeof(argv)/sizeof(*argv), argv);
  
  BOOST_CHECK(args.no_arguments());
}

Test: Invalid argument

Not everyone is going to get the parameters right so we need to know when something goes wrong.

#include <boost/test/unit_test.hpp>

#include "command_line_argument_parser.hpp"

BOOST_AUTO_TEST_CASE(raise_exception_on_invalid_argument) {
  command_line_argument_parser parser;
  const char *argv[] = {"app", "--silly-argument"};

  BOOST_CHECK_EXCEPTION(parser.parse(sizeof(argv)/sizeof(*argv), argv),
			std::logic_error,
			[] (std::logic_error const& e) -> bool {return strcmp(e.what(), "unrecognised option '--silly-argument'") == 0;});
}

Test: Accepting a switch

Accept switch arguments for binary options

#include <boost/test/unit_test.hpp>

#include "command_line_argument_parser.hpp"

BOOST_AUTO_TEST_CASE(recognises_a_switch_argument) {
  command_line_argument_parser parser;
  const char *argv[] = {"app", "--verbose"};
  auto args = parser.parse(sizeof(argv)/sizeof(*argv), argv);
  
  BOOST_CHECK(args.verbose());
}

Test: Accepting a value argument

Accept values on the command line

#include <boost/test/unit_test.hpp>

#include "command_line_argument_parser.hpp"

BOOST_AUTO_TEST_CASE(recognises_username_argument) {
  command_line_argument_parser parser;
  const char *argv[] = {"app", "--username","graham"};
  auto args = parser.parse(sizeof(argv)/sizeof(*argv), argv);
  
  BOOST_CHECK_EQUAL("graham", args.username());
}

Test: Filename parameter list

Accept a list of filenames positionally at the end of the command line

#include <boost/test/unit_test.hpp>

#include "command_line_argument_parser.hpp"

BOOST_AUTO_TEST_CASE(captures_filenames_from_the_commandline) {
  command_line_argument_parser parser;
  const char *argv[] = {"app", "a", "b", "c"};
  auto args = parser.parse(sizeof(argv)/sizeof(*argv), argv);
  
  auto filename_arguments =  args.filenames();

  BOOST_CHECK_EQUAL(3, filename_arguments.size());
  BOOST_CHECK_EQUAL("a", filename_arguments[0]);
  BOOST_CHECK_EQUAL("b", filename_arguments[1]);
  BOOST_CHECK_EQUAL("c", filename_arguments[2]);
}

Test: Main method to run the tests

#define BOOST_TEST_MAIN
#ifndef MAKEFILE_BUILD
// Work around for xcode's default to always link against dynamic libraries
// and the need to static linking for distribution.
#define BOOST_TEST_DYN_LINK
#endif
#define BOOST_TEST_MODULE DupsTests

#include <boost/test/unit_test.hpp>

Application bootstrap

Bootstrapping the app. Main methods need to be as small as possible and responsible for wiring up the application and passing command line parameters into the application.

#include "application.hpp"

int main(int argc, const char *argv[]) {
  application app;

  return app.run(argc, argv);
}

Main application class

For anything more than this trivial application the application class would contain the main application algorithm or defer processing to a collection of other classes.

#pragma once

#include "command_line_argument_parser.hpp"

class application {
public:
  int run(int argc, const char* argv[]) {
    command_line_argument_parser parser;

    parser.parse(argc, argv);

    return 0;
  }
};

The command line processing classes

Class responsible for capturing runtime arguments.

#pragma once

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

using namespace std;

class arguments {
  constexpr static auto username_option_name = "username";
  constexpr static auto username_option = "username,u";
  constexpr static auto verbose_option_name = "verbose";
  constexpr static auto verbose_option = "verbose,v";
  constexpr static auto files_option_name = "input-files";
  
  boost::program_options::variables_map variables;
  friend class command_line_argument_parser;
public:
  arguments(boost::program_options::variables_map variables) : variables(variables) {}

  bool no_arguments() {
    return variables.size() == 0;
  }

  bool verbose() {
    return variables.count(verbose_option_name) > 0;
  }

  string username() {
    return (variables.count(username_option_name) > 0) ? variables[username_option_name].as<string>() : "";
  }
  
  const vector<string>& filenames() {
    return variables[files_option_name].as<vector<string>>();
  }
};

class command_line_argument_parser {
    boost::program_options::options_description desc;
  public:
    command_line_argument_parser() {
      desc.add_options()
	(arguments::verbose_option, "Typical long and short switch argument")
        (arguments::username_option, boost::program_options::value<std::string>(), "username to use")
	(arguments::files_option_name,  boost::program_options::value<vector<string>>(), "input file");
    }

  arguments parse(int argc, const char *argv[]) {
    boost::program_options::variables_map variables;
    
    boost::program_options::positional_options_description p;
    p.add(arguments::files_option_name, -1);

    boost::program_options::store(boost::program_options::command_line_parser(argc, argv).options(desc).positional(p).run(), variables);
    
    boost::program_options::notify(variables);

    return arguments(variables);
  }
};