-
-
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.