How to add a description to boost::program_options' positional options?
Asked Answered
S

1

12

I would like to make a positional, list program option with boost_program_options that do not allow named program options (like --files).

I have the following snippet of code:

#include <boost/program_options.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace po = boost::program_options;

int main(int argc, const char* argv[]) {
  po::options_description desc("Allowed options");
  desc.add_options()("help", "produce help message")
  ( "files", po::value<std::vector<std::string>>()->required(), "list of files");

  po::positional_options_description pos;
  pos.add("files", -1);

  po::variables_map vm;
  try {
    po::store(po::command_line_parser(argc, argv).options(desc).positional(pos).run(), vm);
    po::notify(vm);
  } catch(const po::error& e) {
    std::cerr << "Couldn't parse command line arguments properly:\n";
    std::cerr << e.what() << '\n' << '\n';
    std::cerr << desc << '\n';
    return 1;
  }

  if(vm.count("help") || !vm.count("files")) {
    std::cout << desc << "\n";
    return 1;
  }
}

The problem is that I can read files list as positional arguments lists as follows:

./a.out file1 file2 file3

but unfortunately like this as well ( which I would like to disable )

./a.out --files file1 file2 file3

The problem is also with the help which yields:

./a.out
Couldn't parse command line arguments properly:
the option '--files' is required but missing

Allowed options:
  --help                produce help message
  --files arg           list of files

So my desired scenario would be more like (os similar):

./a.out
Couldn't parse command line arguments properly:
[FILES ...] is required but missing

Allowed options:
  --help                produce help message
  --optionx             some random option used in future
  [FILE ...]            list of files

After I remove files options from desc.add_option()(...) it stop working so I believe I need it there.

Scholz answered 8/10, 2016 at 12:1 Comment(4)
Why do you need to remove the ability to specify the input files with the named parameter? It's not detrimental to anything, so why go out of your to disable it?Cooke
@DanMašek I believe it's not that clear to the user since this allows both ways to be valid input strategies (where I specifically want only one and have help to support it)Scholz
OK. There you have what seems to me the least invasive solution. It still allows the positional options to be scattered all throughout the argument list, but you could easily add more validation to restrict it more.Cooke
BTW, I think you should change the title something that better reflects the body of your question. It's a tough one, but something like "Using positional options with their explicit variant forbidden" or something along those lines might be better.Cooke
E
7

As to the question posed in the title, "How to add a description to boost::program_options' positional options?", there's no functionality provided for this in the library. You need to handle that part yourself.

As for the body of the question... it's possible, but in a slightly round-about way.

The positional options map each position to a name, and the names need to exist. From what I can tell in the code (cmdline.cpp), the unregistered flag won't be set for arguments that are positional. [1], [2]

So, to do what you want, we can do the following:

  • Hide the --files option from showing up in the help. You will need to display appropriate help for the positional options yourself, but this is no different than before.
  • Add our own validation between parsing and storing of the parsed options to the variables_map.

Hiding --files from help

Here we take advantage of the the fact that we can create composite options_description using the add(...) member function:

po::options_description desc_1;
// ...
po::options_description desc_2;
// ...
po::options_description desc_composite;
desc_composite.add(desc_1).add(desc_2);

We can therefore place our files option into a hidden options_description, and create a composite that we will use only for the parsing stage. (see code below)

Preventing explicit --files

We need to intercept the list of options between parsing and storing them into the variables_map.

The run() method of command_line_parser returns an instance of basic_parsed_options, whose member options holds a vector of basic_options. There is an element for each parsed argument, and any positional options are enumerated starting from 0, any non-positional options have position -1. We can use this to perform our own validation and raise an error when we see --files as an explicit (non-positional) argument.

Example Source Code

See on Coliru

#include <boost/program_options.hpp>
#include <iostream>
#include <string>
#include <vector>

namespace po = boost::program_options;

int main(int argc, const char* argv[])
{
    std::vector<std::string> file_names;

    po::options_description desc("Allowed options");
    desc.add_options()
        ("help", "produce help message")
        ("test", "test option");

    std::string const FILES_KEY("files");

    // Hide the `files` options in a separate description
    po::options_description desc_hidden("Hidden options");
    desc_hidden.add_options()
        (FILES_KEY.c_str(), po::value(&file_names)->required(), "list of files");

    // This description is used for parsing and validation
    po::options_description cmdline_options;
    cmdline_options.add(desc).add(desc_hidden);

    // And this one to display help
    po::options_description visible_options;
    visible_options.add(desc);

    po::positional_options_description pos;
    pos.add(FILES_KEY.c_str(), -1);

    po::variables_map vm;
    try {
        // Only parse the options, so we can catch the explicit `--files`
        auto parsed = po::command_line_parser(argc, argv)
            .options(cmdline_options)
            .positional(pos)
            .run();

        // Make sure there were no non-positional `files` options
        for (auto const& opt : parsed.options) {
            if ((opt.position_key == -1) && (opt.string_key == FILES_KEY)) {
                throw po::unknown_option(FILES_KEY);
            }
        }

        po::store(parsed, vm);
        po::notify(vm);
    } catch(const po::error& e) {
        std::cerr << "Couldn't parse command line arguments properly:\n";
        std::cerr << e.what() << '\n' << '\n';
        std::cerr << visible_options << '\n';
        return 1;
    }

    if (vm.count("help") || !vm.count("files")) {
        std::cout << desc << "\n";
        return 1;
    }

    if (!file_names.empty()) {
        std::cout << "Files: \n";
        for (auto const& file_name : file_names) {
            std::cout << " * " << file_name << "\n";
        }
    }
}

Test Output

Valid options:

>example a b c --test d e
Files:
 * a
 * b
 * c
 * d
 * e

Invalid options:

>example a b c --files d e
Couldn't parse command line arguments properly:
unrecognised option 'files'


Allowed options:
  --help                 produce help message
  --test                 test option
Ethelethelbert answered 8/10, 2016 at 15:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.