Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@randomphrase
Last active February 17, 2023 05:51
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save randomphrase/10801888 to your computer and use it in GitHub Desktop.
Save randomphrase/10801888 to your computer and use it in GitHub Desktop.
Demonstration of how to do subcommand option processing with boost program_options
#define BOOST_TEST_MODULE subcommand options
#include <boost/test/unit_test.hpp>
#include <boost/program_options.hpp>
#include <boost/variant/variant.hpp>
#include <boost/variant/get.hpp>
struct GenericOptions {
bool debug_;
};
struct LsCommand : public GenericOptions {
bool hidden_;
std::string path_;
};
struct ChmodCommand : public GenericOptions {
bool recurse_;
std::string perms_;
std::string path_;
};
typedef boost::variant<LsCommand, ChmodCommand> Command;
Command ParseOptions(int argc, const char *argv[])
{
namespace po = boost::program_options;
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);
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);
LsCommand ls;
ls.debug_ = vm.count("debug");
ls.hidden_ = vm.count("hidden");
ls.path_ = vm["path"].as<std::string>();
return ls;
}
else if (cmd == "chmod")
{
// Something similar
}
// unrecognised command
throw po::invalid_option_value(cmd);
}
BOOST_AUTO_TEST_CASE(NoCommand)
{
const int argc = 2;
const char *argv[argc] = { "0", "nocommand" };
BOOST_CHECK_THROW(
ParseOptions(argc, argv),
boost::program_options::invalid_option_value);
}
BOOST_AUTO_TEST_CASE(LsTest)
{
const int argc = 5;
const char *argv[argc] = { "0", "--debug", "ls", "--hidden", "--path=./" };
Command c = ParseOptions(argc, argv);
BOOST_REQUIRE(boost::get<LsCommand>(&c));
const LsCommand& ls = boost::get<LsCommand>(c);
BOOST_CHECK(ls.debug_);
BOOST_CHECK(ls.hidden_);
BOOST_CHECK_EQUAL(ls.path_, "./");
}
@markusdr
Copy link

markusdr commented Oct 7, 2015

This is great! The only problem is that it doesn't allow a global --help and specific --help options for the subcommands. After adding --help to both options descriptions, a.out ls --help and a.out --help gives the same output.

@CD3
Copy link

CD3 commented Jun 28, 2017

I ended up preprocessing the arguments before handing them off to boost.

First create a vector of strings that have your command names in, then loop through argv and sort the arguments into "global" and "command" arguments. I actually am using a vector of string pairs so I can put the command description with the command.

std::vector<std::<std::string,std::string>> cmds = { {"cmd1","cmd1 desc"}
                                                   , {"cmd2", "cmd2 desc"}
                                                   /* etc */ };
std::vector<std::string> global_args;
std::vector<std::string> cmd_args;

bool cmd_found = false;
for( int i = 1; i < argc; i++ )
{
  std::string arg( argv[i] );
  if(!cmd_found)
    global_args.push_back( arg );
  if(cmd_found)
    cmd_args.push_back( arg );

  for( auto c : cmds )
  {
    if( c.first == arg )
    {
      cmd_found = true;
      break;
    }
  }
}

Now you can setup an option parser for the global arguments and pass it global_args. Note that this snippet puts the command name in the global_args vector, so you global argument parser can look for it as a positional parameter.

Then just put your commands in separate functions, pass the function cmd_args and use another option parser inside the function to parse it.

@bricerive
Copy link

This is exactly what I was looking for! Thanks!

@lfreist
Copy link

lfreist commented Aug 8, 2022

@markusdr

This is great! The only problem is that it doesn't allow a global --help and specific --help options for the subcommands. After adding --help to both options descriptions, a.out ls --help and a.out --help gives the same output.

I know this reply comes late but I just had the same issue...
You can fix this by checking if a command was provided.
If you just use if (vm.count("help")) std::cout << global << std::endl;, you get the same help output whenever a --help flag is set anywhere in your options.
You can use this instead and you will get your different outpus:

// ...
if (vm.count("help") && !vm.count("command")) {
    std::cout << bm_options << std::endl;
    return 0;
}
// ...
// within the command specific section:
if (vm.count("help")) {
  std::cout << ls_desc << std::endl;
  return 0;
}

@randomphrase
Copy link
Author

@lfreist Nice one! I'll update the code above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment