Search code examples
c++boostc++20command-line-argumentsboost-program-options

Boost::program_options "option cannot be specified more than once" error when implementing a program with multiple modes


I am attempting to create a program with the following command line usage:

test_program [General Options] <mode_1_option_1> <required_option_1>

OR

test_program [General Options] --toggle-mode <mode_2_option_1> <mode_2_option_2> <mode_2_option_3> <required_option_1>

Where the inclusion of '--toggle-mode' dictates which mode the program runs in.


The following is my attempt at implementing Boost::program_options to accomplish this:

namespace po = boost::program_options;

bool use_mode_2 = false;

po::options_description general_options_desc("General Options");
general_options_desc.add_options()
("help,h", "this help message");

po::options_description mode_options_desc("Mode options");
mode_options_desc.add_options()
    ("toggle-mode,M", po::bool_switch(&use_mode_2), "specify mode 2 operations");

po::options_description required_options_desc("Required Options");
required_options_desc.add_options()
    ("required_option_1", po::value<std::string>()->required());

po::options_description mode_1_options_desc("Mode #1 Options");
mode_1_options_desc.add_options()
    ("mode_1_option_1", po::value<std::string>()->required());

po::options_description mode_2_options_desc("Mode #2 Options");
mode_2_options_desc.add_options()
    ("mode_2_option_1", po::value<std::string>()->required())
    ("mode_2_option_2", po::value<std::string>()->required())
    ("mode_2_option_3", po::value<std::string>()->required());

/* Sets my "use_mode_2" variable  */
{
    po::variables_map mode_vm;
    po::store(po::command_line_parser(argc, argv).options(mode_options_desc).allow_unregistered().run(), mode_vm);
    po::notify(mode_vm);
}

po::options_description visible_options_desc("Visible Options");
visible_options_desc.add(general_options_desc);
visible_options_desc.add(mode_options_desc);

po::options_description all_options_desc("All Options");
all_options_desc.add(visible_options_desc);

int positional_index = 1;
po::positional_options_description positional_options_desc;
if (use_mode_2)
{
    positional_options_desc.add("mode_2_option_1", positional_index++);
    positional_options_desc.add("mode_2_option_2", positional_index++);
    positional_options_desc.add("mode_2_option_3", positional_index++);
    all_options_desc.add(mode_2_options_desc);
}
else
{
    positional_options_desc.add("mode_1_option_1", positional_index++);
    all_options_desc.add(mode_1_options_desc);
}

positional_options_desc.add("required_option_1", positional_index);
all_options_desc.add(required_options_desc);

po::variables_map vm;
po::store(po::command_line_parser(argc, argv).options(all_options_desc).positional(positional_options_desc).run(), vm);

if (vm.count("help"))
{
    fprintf(stderr, "Usage:\n");
    fprintf(stderr, "   test_program [General Options] <mode_1_option_1> <required_option_1>\n");
    fprintf(stderr, "   test_program [General Options] --toggle-mode <mode_2_option_1> <mode_2_option_2> <mode_2_option_3> <required_option_1>\n\n");
    visible_options_desc.print(std::cout);
    return EXIT_SUCCESS;
}

Running my program in Mode #1 works as expected:

.\test_program mode_1_option_1 required_option_1 

But I run into issues while running it in Mode #2:

.\test_program -M mode_2_option_1 mode_2_option_2 mode_2_option_3 required_option_1
std_exception: option '--mode_2_option_2' cannot be specified more than once

Does anyone have any idea what I may be missing?

Thanks!


Solution

  • At the core is the problem that positional_options_descriptions::add does not take a "position index". It takes a max_count!

    In your case, it should be 1 always. If you want to actually take multiple, the underlying option should support it:

    ("mode_2_option_2", po::value<std::vector<std::string>>()->required())
    

    Compare

    Live On Coliru

    #include <boost/program_options.hpp>
    #include <iomanip>
    #include <iostream>
    namespace po = boost::program_options;
    
    int main(int argc, char* argv[]) {
        bool modeB = false;
    
        po::options_description general_options_desc("General Options");
        general_options_desc.add_options()
            ("help,h", "this help message");
    
        po::options_description mode_options_desc("Mode options");
        mode_options_desc.add_options()
            ("modeB,B", po::bool_switch(&modeB), "specify mode 2 operations");
    
        po::options_description required("Required Options");
        required.add_options()
            ("req_last1", po::value<std::string>()->required());
    
        { /* Set modeB */
            po::variables_map mode_vm;
            store(po::command_line_parser(argc, argv) //
                      .options(mode_options_desc)
                      .allow_unregistered()
                      .run(),
                  mode_vm);
            notify(mode_vm);
        }
    
        po::options_description visible_options_desc("Visible Options");
        visible_options_desc.add(general_options_desc);
        visible_options_desc.add(mode_options_desc);
    
        po::options_description optsA("Mode #1 Options");
        optsA.add_options()
            ("A1", po::value<std::string>()->required());
    
        po::options_description optsB("Mode #2 Options");
        optsB.add_options()
            ("B1", po::value<std::string>()->required())
            ("B2", po::value<std::vector<std::string>>()->required())
            ("B3", po::value<std::string>()->required());
    
        po::options_description all_options_desc("All Options");
        all_options_desc.add(visible_options_desc);
    
        po::positional_options_description positionals;
    
        if (!modeB) {
            positionals.add("A1", 1);
            all_options_desc.add(optsA);
        } else {
            positionals.add("B1", 1);
            positionals.add("B2", 1);
            positionals.add("B3", 1);
            all_options_desc.add(optsB);
        }
    
        positionals.add("req_last1", 1);
        {
            std::cout << "positional(" << positionals.max_total_count() << "):";
            for (size_t i = 0; i < positionals.max_total_count(); i++)
                std::cout << " " << positionals.name_for_position(i);
            std::cout << "\n";
        }
        all_options_desc.add(required);
    
        po::variables_map vm;
        store(po::command_line_parser(argc, argv) //
                  .options(all_options_desc)
                  .positional(positionals)
                  .run(),
              vm);
    
        if (vm.count("help")) {
            fprintf(stderr, "Usage:\n");
            fprintf(stderr, "   test_program [General Options] <A1> <req_last1>\n");
            fprintf(stderr, "   test_program [General Options] --modeB <B1> <B2> <B3> <req_last1>\n\n");
            if (modeB) {
                optsB.print(std::cout);
            } else {
                optsA.print(std::cout);
            }
            visible_options_desc.print(std::cout);
            return 0;
        }
    
        for (auto [k, v] : vm) {
            if (v.value().type() == typeid(std::string)) {
                std::cout << k << ": " << quoted(v.as<std::string>()) << "\n";
            } else if (v.value().type() == typeid(bool)) {
                std::cout << k << ": " << std::boolalpha << v.as<bool>() << "\n";
            } else if (v.value().type() == typeid(std::vector<std::string>)) {
                std::cout << k << ":";
                for (auto& s : v.as<std::vector<std::string>>())
                    std::cout << " " << quoted(s);
                std::cout << "\n";
            } else {
                std::cout << k << ": " << "???\n";
            }
        }
    }
    

    Printing e.g.

    Now, make that

     positionals.add("B2", 3); // expect B2 three time
    

    Now we see Live On Coliru

    $ ./a.out foo this is really too much -B
    positional(6): B1 B2 B2 B2 B3 req_last1
    B1: "foo"
    B2: "this" "is" "really"
    B3: "too"
    modeB: true
    req_last1: "much"