-
-
Save randomphrase/10801888 to your computer and use it in GitHub Desktop.
#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_, "./"); | |
} |
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.
This is exactly what I was looking for! Thanks!
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;
}
@lfreist Nice one! I'll update the code above
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.