4

I want to use boost::program_options to specify the required verbosity as is pretty common. E.g.

./test -v # verbosity = 1 
./test -vvv # verbosity = 3 
./test -v blah blah -v # verbosity = 2 

I know how to do multiple occurences of options that require a value, what I want though is multiple occurances of a switch. A single switch can be done with something like

desc.add_options()
   ("verbosity,v", bool_switch(), "Increase verbosity");

but this fails with a multiple_occurrences exception if more than one -v option is supplied.

Multiple boolean options can be done with something like

desc.add_options()
   ("verbose,v", value<std::vector<int> >(), "Increase verbosity");

but this requires that each option is given a value, such as

./test -v 1 -v 1 -v 1

2 Answers2

1

I had this exact problem, and came up with the following. Hopefully it will be useful to someone, even if it's too late for the original poster.

#include "StdAfx.h"
#include <boost\program_options.hpp>
#include <iostream>

namespace po = boost::program_options;

class CountValue : public po::typed_value<std::size_t>
{
public:
    CountValue():
        CountValue(nullptr)
    {
    }

    CountValue(std::size_t* store):
        po::typed_value<std::size_t>(store)
    {
        // Ensure that no tokens may be passed as a value.
        default_value(0);
        zero_tokens();
    }

    virtual ~CountValue()
    {
    }

    virtual void xparse(boost::any& store, const std::vector<std::string>& /*tokens*/) const
    {
        // Replace the stored value with the access count.
        store = boost::any(++count_);
    }

private:
    mutable std::size_t count_{ 0 };
};

int main(int argc, char** argv)
{
    // Define the command line options (we create a new CountValue
    // instance, on the understanding that it will be deleted for us
    // when the options description instance goes out of scope).
    po::options_description options("Options");
    std::size_t verbose = 0;
    options.add_options()
        ("verbose,v", new CountValue(&verbose), "Increase verbosity");

    // Parse the command line options.
    po::command_line_parser parser(argc, argv);
    po::variables_map variables;
    po::store(parser.options(options).run(), variables);
    po::notify(variables);

    // Show the verbosity level.
    std::cout << "Verbosity " << verbose << "\n";
}

This compiles with C++20 and Boost 1.77. And here it is in action:

$ test
Verbosity 0

$ test -v
Verbosity 1

$ test -vvv
Verbosity 3

$ test -v blah blah -v
Verbosity 2
Huw Walters
  • 1,888
  • 20
  • 20
0

The answer by Huw Walters has been very useful for me. I have extended a bit on it to allow also to set the verbosity level explicitely by a number argument. So all these calls will set verbosity to 4:

app -v -v -v -v
app -v4
app -vvvv
app -vvv -v
app -v3 -v

Unfortunately, the backdraw of that solution is that mixing in other options after an -v doesn't work. So app -vx will yield an error, even, if x is also a legal option.

Below is the code. To use it, provide: ("verbose,v", new po_CountAndAddValue(&verbose), "Increase verbosity"); as operator to add_options().

class po_CountAndAddValue : public po::typed_value<std::size_t>
{
public:
    po_CountAndAddValue(std::size_t* store)
      : po::typed_value<std::size_t>(store)
    {
        default_value(0);
    }
    po_CountAndAddValue() : po_CountAndAddValue(nullptr) {}
    ~po_CountAndAddValue() override {}

    // Allow zero or one tokens:
    unsigned min_tokens() const override { return 0; }
    unsigned max_tokens() const override { return 1; }

    void xparse(boost::any& store,
        const std::vector<std::string>& tokens) const override
    {
    if(tokens.empty()) {
        count_++;
    } else {
        const std::string& tok = tokens[0];
        if(std::all_of(tok.begin(), tok.end(), [](char c){return c=='v';})){
        count_ += 1 + tok.size();
        } else {
        // Add explicitely specified verbosity count:
        count_ += boost::lexical_cast<int>(tok);
        }
    }

        // Replace the stored value with the updated access count:
        store = boost::any(count_);
    }

private:
    mutable std::size_t count_{ 0 };
};

In case, that you don't want to allow the mixed use of the verbosity option with and without a number argument, you can add error reporting for that case as follows:

class po_CountOrSetValue : public po::typed_value<std::size_t>
{
public:
    po_CountOrSetValue(std::size_t* store)
      : po::typed_value<std::size_t>(store)
    {
        default_value(0);
    }
    po_CountOrSetValue() : po_CountOrSetValue(nullptr) {}
    ~po_CountOrSetValue() override {}

    // Allow zero or one tokens:
    unsigned min_tokens() const override { return 0; }
    unsigned max_tokens() const override { return 1; }

    class mixed_types_error : public po::error_with_option_name {
    public:
        mixed_types_error()
         : po::error_with_option_name("option '%canonical_option%' wrongly used both with and without value"){}
        ~mixed_types_error() throw() {}
    };

    void xparse(boost::any& store,
        const std::vector<std::string>& tokens) const override
    {
    if(tokens.empty() ||
       std::all_of(tokens[0].begin(), tokens[0].end(),
               [](char c){return c=='v';}))
    {
        if(set_) {
        throw mixed_types_error();
        }
        count_ += tokens.empty() ? 1 : 1 + tokens[0].size();
    } else {
        if(std::exchange(set_, true)) {
        throw po::multiple_occurrences();
        }
        if(count_ != 0) {
        throw mixed_types_error();
        }
        count_ = boost::lexical_cast<int>(tokens[0]);
    }

        // Replace the stored value with the updated count:
        store = boost::any(count_);
    }

private:
    mutable std::size_t count_{ 0 };
    mutable bool set_{ false };
};

All of these specifiers work with the first version to set verbosity to 4, but fail with the second version:

app -v3 -v
app -v3 -v1
app -vv -v2

Everything was tested with C++17 and Boost 1.74.

Kai Petzke
  • 2,150
  • 21
  • 29