How to implement subcommands using Boost.Program_options?

Scintillo picture Scintillo · Mar 21, 2013 · Viewed 7.2k times · Source

I'd like to implement subcommands to my program. I also need the ability to have different argument options for different subcommands. What's the best way to do this using Boost.Program_options?

Subcommands are used in programs like svn, git and apt-get.

For example in GIT some of the available subcommands are:

git status  
git push  
git add  
git pull  

My question is basically the same as this guy's: http://boost.2283326.n4.nabble.com/subcommands-with-program-options-like-svn-command-td2585537.html

Answer

Alastair picture Alastair · Apr 16, 2014

If I understand the problem correctly, you want to parse command line options of the following form:

[--generic-option ...] cmd [--cmd-specific-option ... ] 

Here is my example solution. For clarity I'm going to omit any validation code, but hopefully you can see how it would be added fairly simply.

In this example, we have the "ls" subcommand, and possibly others. Each subcommand has some specific options, and in addition there are generic options. So let's start by parsing the generic options and the command name.

po::options_description global("Global options");
global.add_options()
    ("debug", "Turn on debug output")
    ("command", po::value<std::string>(), "command to execute")
    ("subargs", po::value<std::vector<std::string> >(), "Arguments for command");

po::positional_options_description pos;
pos.add("command", 1).
    add("subargs", -1);

po::variables_map vm;

po::parsed_options parsed = po::command_line_parser(argc, argv).
    options(global).
    positional(pos).
    allow_unregistered().
    run();

po::store(parsed, vm);

Notice that we've created a single positional option for the command name, and multiple positional options for the command options.

Now we branch on the relevant command name and re-parse. Instead of passing in the original argc and argv we now pass in the unrecognized options, in the form of an array of strings. The collect_unrecognized function can provide this - all we have to do is remove the (positional) command name and re-parse with the relevant options_description.

std::string cmd = vm["command"].as<std::string>();
if (cmd == "ls")
{
    // ls command has the following options:
    po::options_description ls_desc("ls options");
    ls_desc.add_options()
        ("hidden", "Show hidden files")
        ("path", po::value<std::string>(), "Path to list");

    // Collect all the unrecognized options from the first pass. This will include the
    // (positional) command name, so we need to erase that.
    std::vector<std::string> opts = po::collect_unrecognized(parsed.options, po::include_positional);
    opts.erase(opts.begin());

    // Parse again...
    po::store(po::command_line_parser(opts).options(ls_desc).run(), vm);

Note that we used the same variables_map for the command-specific options as for the generic ones. From this we can perform the relevant actions.

The code fragments here are taken from a compilable source file which includes some unit tests. You can find it on gist here. Please feel free to download and play with it.