83

I'm using Boost Program Options Library to parse the command line arguments.

I have the following requirements:

  1. Once "help" is provided, all the other options are optional;
  2. Once "help" is not provided, all the other options are required.

How I can deal with this? Here is the my code handling this, and I found it's very redundant, and I think there must be an easy to do, right?

#include <boost/program_options.hpp>
#include <iostream>
#include <sstream>
namespace po = boost::program_options;

bool process_command_line(int argc, char** argv,
                          std::string& host,
                          std::string& port,
                          std::string& configDir)
{
    int iport;

    try
    {
        po::options_description desc("Program Usage", 1024, 512);
        desc.add_options()
          ("help",     "produce help message")
          ("host,h",   po::value<std::string>(&host),      "set the host server")
          ("port,p",   po::value<int>(&iport),             "set the server port")
          ("config,c", po::value<std::string>(&configDir), "set the config path")
        ;

        po::variables_map vm;
        po::store(po::parse_command_line(argc, argv, desc), vm);
        po::notify(vm);

        if (vm.count("help"))
        {
            std::cout << desc << "\n";
            return false;
        }

        // There must be an easy way to handle the relationship between the
        // option "help" and "host"-"port"-"config"
        if (vm.count("host"))
        {
            std::cout << "host:   " << vm["host"].as<std::string>() << "\n";
        }
        else
        {
            std::cout << "\"host\" is required!" << "\n";
            return false;
        }

        if (vm.count("port"))
        {
            std::cout << "port:   " << vm["port"].as<int>() << "\n";
        }
        else
        {
            std::cout << "\"port\" is required!" << "\n";
            return false;
        }

        if (vm.count("config"))
        {
            std::cout << "config: " << vm["config"].as<std::string>() << "\n";
        }
        else
        {
            std::cout << "\"config\" is required!" << "\n";
            return false;
        }
    }
    catch(std::exception& e)
    {
        std::cerr << "Error: " << e.what() << "\n";
        return false;
    }
    catch(...)
    {
        std::cerr << "Unknown error!" << "\n";
        return false;
    }

    std::stringstream ss;
    ss << iport;
    port = ss.str();

    return true;
}

int main(int argc, char** argv)
{
  std::string host;
  std::string port;
  std::string configDir;

  bool result = process_command_line(argc, argv, host, port, configDir);
  if (!result)
      return 1;

  // Do the main routine here
}
rcollyer
  • 10,047
  • 4
  • 45
  • 70
Peter Lee
  • 11,507
  • 7
  • 65
  • 97

4 Answers4

103

I've run into this issue myself. The key to a solution is that the function po::store populates the variables_map while po::notify raises any errors encountered, so vm can be used prior to any notifications being sent.

So, as per Tim, set each option to required, as desired, but run po::notify(vm) after you've dealt with the help option. This way it will exit without any exceptions thrown. Now, with the options set to required, a missing option will cause a required_option exception to be thrown and using its get_option_name method you can reduce your error code to a relatively simple catch block.

As an additional note, your option variables are set directly via the po::value< -type- >( &var_name ) mechanism, so you don't have to access them through vm["opt_name"].as< -type- >().

A code example is provided in Peters answer

Jonas Stein
  • 5,932
  • 5
  • 35
  • 67
rcollyer
  • 10,047
  • 4
  • 45
  • 70
  • thanks for your reply. I think it works as expected. I have also posted the complete program below for the folks who need a good example. – Peter Lee Apr 01 '11 at 21:46
  • 5
    Excellent solution! The official documentation should make it clear with an example. – russoue May 01 '14 at 20:57
  • @rcollyer could you provide a full working example, please? – Jonas Stein Dec 07 '19 at 09:14
  • @JonasStein I could, but [Peter's](https://stackoverflow.com/a/5519200/198315) appears to be just fine. Let me know if that is insufficient. – rcollyer Dec 08 '19 at 05:10
  • 1
    @rcollyer The sx website does not visually connect the two answers, so I missed that. I have added a note. Please revert, if you are not comfortable with it. – Jonas Stein Dec 08 '19 at 10:34
46

Here is complete program as per rcollyer and Tim, whom the credits go to:

#include <boost/program_options.hpp>
#include <iostream>
#include <sstream>
namespace po = boost::program_options;

bool process_command_line(int argc, char** argv,
                          std::string& host,
                          std::string& port,
                          std::string& configDir)
{
    int iport;

    try
    {
        po::options_description desc("Program Usage", 1024, 512);
        desc.add_options()
          ("help",     "produce help message")
          ("host,h",   po::value<std::string>(&host)->required(),      "set the host server")
          ("port,p",   po::value<int>(&iport)->required(),             "set the server port")
          ("config,c", po::value<std::string>(&configDir)->required(), "set the config path")
        ;

        po::variables_map vm;
        po::store(po::parse_command_line(argc, argv, desc), vm);

        if (vm.count("help"))
        {
            std::cout << desc << "\n";
            return false;
        }

        // There must be an easy way to handle the relationship between the
        // option "help" and "host"-"port"-"config"
        // Yes, the magic is putting the po::notify after "help" option check
        po::notify(vm);
    }
    catch(std::exception& e)
    {
        std::cerr << "Error: " << e.what() << "\n";
        return false;
    }
    catch(...)
    {
        std::cerr << "Unknown error!" << "\n";
        return false;
    }

    std::stringstream ss;
    ss << iport;
    port = ss.str();

    return true;
}

int main(int argc, char** argv)
{
  std::string host;
  std::string port;
  std::string configDir;

  bool result = process_command_line(argc, argv, host, port, configDir);
  if (!result)
      return 1;

  // else
  std::cout << "host:\t"   << host      << "\n";
  std::cout << "port:\t"   << port      << "\n";
  std::cout << "config:\t" << configDir << "\n";

  // Do the main routine here
}

/* Sample output:

C:\Debug>boost.exe --help
Program Usage:
  --help                produce help message
  -h [ --host ] arg     set the host server
  -p [ --port ] arg     set the server port
  -c [ --config ] arg   set the config path


C:\Debug>boost.exe
Error: missing required option config

C:\Debug>boost.exe --host localhost
Error: missing required option config

C:\Debug>boost.exe --config .
Error: missing required option host

C:\Debug>boost.exe --config . --help
Program Usage:
  --help                produce help message
  -h [ --host ] arg     set the host server
  -p [ --port ] arg     set the server port
  -c [ --config ] arg   set the config path


C:\Debug>boost.exe --host 127.0.0.1 --port 31528 --config .
host:   127.0.0.1
port:   31528
config: .

C:\Debug>boost.exe -h 127.0.0.1 -p 31528 -c .
host:   127.0.0.1
port:   31528
config: .
*/
Jonas Stein
  • 5,932
  • 5
  • 35
  • 67
Peter Lee
  • 11,507
  • 7
  • 65
  • 97
13

You can specify that an option is required easily enough [1], e.g.,:

..., value<string>()->required(), ...

but as far as I know there's no way to represent relationships between different options to the program_options library.

One possibility is to parse the command line multiple times with different option sets, then if you've already checked for "help" you can parse again with the three other options all set as required. I'm not sure I'd consider that an improvement over what you have, though.

Tim Sylvester
  • 21,850
  • 2
  • 69
  • 92
  • 2
    yes, u r right that I could put `->required()`, but then the user cannot get the help info by `--help` (without providing all other required options), because other options are required. – Peter Lee Apr 01 '11 at 19:58
  • @Peter You would look for *only* help the first time, the other options wouldn't even be in the list. Then, if they didn't pass in the help option, only then would you run the parsing again, this time passing in the other three options, set to required, and *not* help. This approach would probably require a third set of options, with all of them combined, to use for printing the usage information. I'm pretty sure it would work, but rcollyer's approach is cleaner. – Tim Sylvester Apr 02 '11 at 00:16
1
    std::string conn_mngr_id;
    std::string conn_mngr_channel;
    int32_t priority;
    int32_t timeout;

    boost::program_options::options_description p_opts_desc("Program options");
    boost::program_options::variables_map p_opts_vm;

    try {

        p_opts_desc.add_options()
            ("help,h", "produce help message")
            ("id,i", boost::program_options::value<std::string>(&conn_mngr_id)->required(), "Id used to connect to ConnectionManager")
            ("channel,c", boost::program_options::value<std::string>(&conn_mngr_channel)->required(), "Channel to attach with ConnectionManager")
            ("priority,p", boost::program_options::value<int>(&priority)->default_value(1), "Channel to attach with ConnectionManager")
            ("timeout,t", boost::program_options::value<int>(&timeout)->default_value(15000), "Channel to attach with ConnectionManager")
        ;

        boost::program_options::store(boost::program_options::parse_command_line(argc, argv, p_opts_desc), p_opts_vm);

        boost::program_options::notify(p_opts_vm);

        if (p_opts_vm.count("help")) {
            std::cout << p_opts_desc << std::endl;
            return 1;
        }

    } catch (const boost::program_options::required_option & e) {
        if (p_opts_vm.count("help")) {
            std::cout << p_opts_desc << std::endl;
            return 1;
        } else {
            throw e;
        }
    }
Edgard Lima
  • 367
  • 2
  • 11
  • That is certainly an interesting alternative. But, it forces you to repeat the help handling code, and while small, I'd tend to avoid it. – rcollyer Mar 02 '17 at 04:39