Skip to content

Instantly share code, notes, and snippets.

@randomphrase
Last active May 25, 2024 08:26
Show Gist options
  • 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_, "./");
}
@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