34

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

FelipeAls
  • 21,711
  • 8
  • 54
  • 74
Scintillo
  • 1,634
  • 1
  • 15
  • 29

2 Answers2

60

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.

Alastair
  • 4,475
  • 1
  • 26
  • 23
  • 2
    Excellent answer, with full example to boot. Thank you! --DD – ddevienne Jun 23 '14 at 11:56
  • It should be clearly stated in the answer that this requires 'allow_unregistered()`, which nullifies a big advantage of option parsing libraries. – Flow Nov 10 '21 at 18:07
  • Not sure how much more clearly it could be stated - it's right there in the code snippet and in the text. But I don't think it follows that the use of `allow_unregistered()` nullifies the use of Boost.Program_Options (let alone option parsing libraries in general!), mainly because the unrecognized options are parsed separately without the use of `allow_unregistered()`. If `allow_unregistered()` was used for the second parse then you might have a point. – Alastair Nov 10 '21 at 22:35
  • Fair point, so you still get warnings about unrecognized options. That said, isn't `po::notify(vm)` missing? OTOH, it is not required by the current code as far as I can tell. – Flow Mar 15 '22 at 08:38
  • I was wondering why "subargs" and `po::include_positional` were required. Why can't you leave off "subargs" and use `po::exclude_positional` instead? Well, what happens is that all the subcommand arguments that start with "-" are ignored during parsing and later included in `po::collect_unrecognized`, but any other arguments generate errors. For example, in the command `git commit -v -m "change message" -- myfile.c`, the `-v` would be fine, but the "change message" and "myfile.c" would both generate errors. – pavon Feb 07 '23 at 19:15
3

You can take the subcommand name off the command line using positional options - see this tutorial.

There doesn't seem to be any built-in support for subcommands - you will need to set the allow_unregistered option on the top-level parser, find the command name, then run it through a second parser to get any subcommand-specific options.

jwd
  • 10,837
  • 3
  • 43
  • 67
poolie
  • 9,289
  • 1
  • 47
  • 74
  • 2
    I'm having trouble getting this solution to work. In particular, Boost doesn't seem to want to allow anything to come *after* positional options. Thus, even with allow_unregistered, boost is complaining that there are too many positional options (i.e. "too many positional options have been specified on the command line"), even though these are the non-positional options which should be parsed by the sub-command. – nomad May 13 '13 at 19:27
  • dead link :( can you provide another or better copy paste contents? – Marek R Jun 09 '20 at 09:52