How to implement subcommands using Boost.Program_options?
Asked Answered
B

3

36

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

Berm answered 21/3, 2013 at 7:12 Comment(0)
V
61

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.

Vocal answered 16/4, 2014 at 3:42 Comment(7)
Excellent answer, with full example to boot. Thank you! --DDDehumidify
It should be clearly stated in the answer that this requires 'allow_unregistered()`, which nullifies a big advantage of option parsing libraries.Bohon
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.Vocal
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.Bohon
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.Willms
for me "command" always is equal to argv[0] . what am I doing wrong?Rowenarowland
opts.erase(opts.begin()); works only if command is the first unrecognized option. E.g. it doesn't work for --unrecognized-opt command-nameHod
A
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.

Astronomy answered 20/4, 2013 at 0:41 Comment(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.Quillan
dead link :( can you provide another or better copy paste contents?Boutonniere
F
-1

It might also be worth considering the small "bpomodes" library, which takes some ideas from @Alastair's answer, and provides a bit more flexibility to programmatically build the menu of subcommands. So, given a set of options_description objects, you can do something like

BpoModes parser(generic_options);

parser.add("subcommand_one", options_one);
parser.add("subcommand_two", options_two);

const auto varmap = parser.parse(argc, argv);

Internally, this applies a two-stage parsing of the supplied array of strings, extracting the user's choice of subcommand into varmap["subcommand"]. The second stage of parsing then uses the specific options_description for that subcommand to consume the remaining command-line arguments.

The library also provides a mechanism via which a specific execution pathway can be triggered for the chosen subcommand. This operates via the BpoModes::ModeHandler interface, in which the run() method can be overloaded. For example, one can construct a mode-handler such as

struct ModeOne: public BpoModes::ModeHandler() {
  int run(const variables_map& vm) {
    std::cout << "Running mode one" << std::endl;
    return 0;
  }
}

and include this in the BpoModes::add() method as follows:

parser.add("subcommand_one", options_one, std::make_shared<ModeOne>());

This then allows the main() method to call parser.run_subcommand(vm); to invoke ModeOne::run() automatically.

Admittedly, this answer may be 10 years too late for the original questioner's use-case.

Footgear answered 29/5 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.