Comparing CLI11 and Boost PO

CLI11 started years ago as a set of tools built on Boost Program Options (PO), and has since matured into the powerful, easy-to-use stand-alone library it is available today. If you would like to see the original inspiration for CLI11, look at Program.hpp in CLI11 0.1. The rest of the post will focus on a comparison between making a CLI app in the two libraries. I am going to assume that you are preparing fairly basic but non-trivial programs in the following comparison.

TL;DR: CLI11 is more concise, and provides more control with better defaults in many cases, but was inspired by Boost PO.


Build system

CLI11

CLI11 only requires a single header file and C++11. That’s it. You can use the multi-file version as well, and it comes with a CMake file, but you don’t have to use either if you don’t want to. Nothing special is required on macOS, Windows, or Linux.

Boost PO

Boost PO requires that you use Boost, and it is old enough that it requires linking to a compiled library (most new Boost libs are header only). If you are using C++11 or better, the chances that you’ll be using Boost are a bit lower than in the past, but there’s still a good chance you’ll be requiring it. However, linking to one of the few Boost libraries is a bit irritating for something as simple as command line parsing.


Initializing

CLI11

CLI11 expects you to create an App for your program. You can optionally add a description when you make the app.

#include "CLI11.hpp" // Single file version

...

CLI::App app{"Description"};

Boost PO

Using Boost PO requires that you manage several objects. Usage will vary depending on what features you want. You’ll probably want something like this:

#include <boost/program_options.hpp>
namespace po = boost::program_options;

...

po::options_description desc{"Description"};
po::positional_options_description p;
po::variables_map vm;

Adding a flag

CLI11

There are two ways. The first is similar to PO, and was inspired by it:

auto flag = app.add_flag("-f,--flag",
                         "A flag");

The preferred way would be to add a variable (bool or int) to bind to:

bool flag;
app.add_flag("-f,--flag", flag, "A flag");

You can have as many long or short names for each flag as you want.

Boost PO

You have to use an odd callable syntax to add options to Boost PO. However, you can keep adding one after the other, which might help keep the options close and saves a few characters. You also have to manually add a help flag.

desc.add_options()
  ("help,h", "produce help message")
  ("flag,f", "A flag")
;

Adding an option

CLI11

CLI11 only provides bound variables, since the values are pure values you can use with no run time lookup or translation penalty.

int value = 2;
double val;
app.add_option("--option", value,
               "Set an option", true);
app.add_option("positional", val,
               "A positional");

The final true here tells CLI11 that it can print the default value in help; if you are not worried about that, you don’t need it. You can make an option as required by adding ->required() to this statement. Positional arguments are marked by having a name with no dashes; a purely positional option is possible!

Boost PO

There are two ways to add options in Boost PO. The first way does not require an option to bind to, and is not mimicked in CLI11:

desc.add_options()
  ("something",
   po::value<int>(),
   "Non-bound option")
;

The second way, where you provide a variable to bind to, is what inspired the method used in CLI11 (and in the old Program.hpp):

int value = 2;
double val;
desc.add_options()
  ("option",
   po::value<int>(&value)
     ->default_value(value),
   "Set an option")
  ("positional",
   po::value<double>(&val),
   "A positional")
;
p.add("positional", -1);

You can also mark an option as required by chaining settings on the po::value (which does not provide good factorizability or readability, IMO).


Parsing

CLI11

CLI11 shines here, since it was designed to work correctly for most uses out of the box, and then provide ways to change defaults.

CLI11_PARSE(app, argc, argv);

If you feel like a macro is cheating, this is all you need to implement this without any macros (and is what the above macro expands to):

try {
  app.parse(argc, argv);
} catch (const CLI::ParseError& e) {
  return app.exit(e);
}

Boost PO

Here’s where Boost PO gets tricky. To correctly exit in case of a parse error or help message, there are quite a few steps you have to do in every program. For non-expert users, often these steps get skipped and the program can hard-crash or fail to print help if there are required options.

try {
  po::store(
    po::command_line_parser(argc, argv)
    .options(desc).positional(p).run(), vm);

  if(vm.count("help")){
    std::cout << desc;
    exit(0);
  }

  po::notify(vm);
} catch(const po::error& e) {
  std::cerr << "ERROR: " << e.what() << std::endl << std::endl; 
  std::cerr << desc << std::endl; 
  exit(1);
}

Accessing values

CLI11

Options and flags are bound directly to the results. You can also look up the count using Boost PO inspired syntax, either on the pointer to the option if you saved it or on the master app:

int iflag = flag->count();
int iflag = app.count("flag");

Boost PO

While regular options can be directly bound, flags and non-bound regular options must be looked up and converted manually, at a runtime cost.

int flag = vm.count("flag");
something = vm["something"].as<int>()

Subcommands

CLI11

CLI11 naturally supports subcommands, which are full App instances, and can be nested infinitely, using:

add_subcommand("name", "description");

Boost PO

Not directly supported by Boost PO. There is a project called Oberon to add these, but it doesn’t seem to be maintained. You can manually do it yourself by collecting the extra unrecognized options and reparsing them, as shown in this gist.


Other features

Supported by both libraries, though usually much more concisely in CLI11:

  • Groups
  • Required options
  • INI files
  • Environment variable input
  • Callback lambda functions
  • Hidden options
  • Extra argument polices (more in CLI11 though)

CLI11 only:

  • Subcommands (already mentioned)
  • Requires/excludes options
  • Case insensitive options
  • Sets
  • Vectors of options
  • Multi-option policies
  • Custom error printer function setting
  • Flags and subcommands in INI files
  • INI file generation
  • Option validators (not the same as a .validate function in Boost PO)
  • Option text transformers

Boost PO only:

  • Response files
  • Winmain splitter
  • Unicode support (in some parsers)
  • boost::optional support (planned in CLI11)
comments powered by Disqus