3

How can I provide an option to the boost program_options, which will accept 3 values with different types: [int, int, float]?

For example:

./app_name --check-integrity 1 2.2 --do_something 2 2 2.5

I've tried to achieve that with a vector of boost::any.

namespace po = boost::program_options;
po::option_descriptions desc("");

desc.add_options()
 ("opt", po::value<std::vector<boost::any> >()->multitoken(), "description");

But without success. This code causes:

/usr/include/boost/lexical_cast/detail/converter_lexical.hpp:243:13: error: static assertion failed: Target type is neither std::istream`able nor std::wistream`able
BOOST_STATIC_ASSERT_MSG((result_t::value || boost::has_right_shift<std::basic_istream<wchar_t>, T >::value),

Any other idea?

user3193620
  • 173
  • 1
  • 11
  • Please clarify what you mean by `"which will accept 3 values with different types: [int, int, float]"`. Can you provide some examples of what would be typed/accepted at the command line? – G.M. Dec 04 '20 at 16:27

3 Answers3

1

This is my recommended, simpler approach. I have added a true multi-token option value parsing approach as a separate answer for completeness.

You can use any streamable type. So, if you have types:

struct check {
    int i;
    double d;
};

struct something {
    int a;
    int b;
    double c;
};

You can define the options like:

opt.add_options()
    ("do_something,s", bpo::value<something>()->default_value(something{1,2,3}, "1 2 3"), "")
    ("check-integrity,c", bpo::value<check>()->default_value(check{1,0.1}, "1 0.1"), "")
    ("help", "");

Note how you need to supply the stringified default value only if the type cannot be output-streamed (or the streamed format doesn't match the expected input format)

bpo::variables_map vm;

try {
    bpo::store(parse_command_line(argc, argv, opt), vm);

    bpo::notify(vm);

    if (vm.count("help")) {
        std::cout << opt << std::endl;
        return 0;
    }

    std::cout << vm["do_something"].as<something>() << "\n";
    std::cout << vm["check-integrity"].as<check>() << "\n";
} catch (std::exception const& e) {
    std::cerr << e.what() << "\n";
}

Output Streaming: Simple Stuff

That's the usual stuff:

static inline std::ostream& operator<<(std::ostream& os, check const& v) {
    return os << "check {" << v.i << ", " << v.d << "}";
}
static inline std::ostream& operator<<(std::ostream& os, something const& v) {
    return os << "something {" << v.a << ", " << v.b << ", " << v.c << "}";
}

Input Streaming: Get As Fancy As You Want

Here there are some slight caveats. First of them is that the istream& supplied from program_options is - by default - modified with the std::noskipws manipulator. You have to undo that in order to be able to read space separated data.

Next up, for any non-trivial parsing/validation task I'd suggest using something more sophisticated. But for the demo, this would suffice:

static inline std::istream& operator>>(std::istream& is, check& into) {
    return is >> std::skipws >> into.i >> into.d;
}
static inline std::istream& operator>>(std::istream& is, something& into) {
    return is >> std::skipws >> into.a >> into.b >> into.c;
}

Demo Time

Live On Coliru

#include <boost/program_options.hpp>
#include <iostream>
#include <iomanip>

namespace bpo = boost::program_options;
struct check {
    int i;
    double d;
};

struct something {
    int a;
    int b;
    double c;
};

static inline std::ostream& operator<<(std::ostream& os, check const& v) {
    return os << "check {" << v.i << ", " << v.d << "}";
}
static inline std::ostream& operator<<(std::ostream& os, something const& v) {
    return os << "something {" << v.a << ", " << v.b << ", " << v.c << "}";
}
static inline std::istream& operator>>(std::istream& is, check& into) {
    return is >> std::skipws >> into.i >> into.d;
}
static inline std::istream& operator>>(std::istream& is, something& into) {
    return is >> std::skipws >> into.a >> into.b >> into.c;
}

int main(int argc, char* argv[]) {
    bpo::options_description opt("all options");

    opt.add_options()
        ("do_something,s", bpo::value<something>()->default_value(something{1,2,3}, "1 2 3"), "")
        ("check-integrity,c", bpo::value<check>()->default_value(check{1,0.1}, "1 0.1"), "")
        ("help", "");

    bpo::variables_map vm;

    try {
        bpo::store(parse_command_line(argc, argv, opt), vm);

        bpo::notify(vm);

        if (vm.count("help")) {
            std::cout << opt << std::endl;
            return 0;
        }

        std::cout << vm["do_something"].as<something>() << "\n";
        std::cout << vm["check-integrity"].as<check>() << "\n";
    } catch (std::exception const& e) {
        std::cerr << e.what() << "\n";
    }
}

Prints

+ ./sotest --help
all options:
  -s [ --do_something ] arg (=1 2 3)
  -c [ --check-integrity ] arg (=1 0.1)
  --help 

+ ./sotest
something {1, 2, 3}
check {1, 0.1}
+ ./sotest --check ''
the argument for option '--check-integrity' is invalid
+ ./sotest --check oops
the argument ('oops') for option '--check-integrity' is invalid
+ ./sotest --check '8 8'
something {1, 2, 3}
check {8, 8}
+ ./sotest --do_something '11 22 .33'
something {11, 22, 0.33}
check {1, 0.1}
+ ./sotest --do_something '11 22 .33' --check '80 -8'
something {11, 22, 0.33}
check {80, -8}
sehe
  • 374,641
  • 47
  • 450
  • 633
  • This is my recommended, simpler approach. I have added a true [multi-token option value parsing approach](https://stackoverflow.com/a/65152635/85371) as a separate answer for completeness. – sehe Dec 05 '20 at 00:44
1

I posted another answer earlier, but I can just predict the follow-up question:

But what if I want to allow --check 1 3.14 instead of --check "1 3.14" or --check '1 3.14'?

That's not a bad question, and it's actually fair because that's the close reading of the original post.

I consider this not the best strategy because:

  • it requires parsing into some intermediate vector of tokens
  • requires manual conversion from these to the target type
  • it makes for brittle command lines
    • it postpones validation of the option value until after command line parsing, the multitoken could have an unexpected number of value tokens and you wouldn't know until converting to the target type
    • it doesn't work at all when e.g. one of the token is negative. Because --check 8 -8 will try to parse -8 as an option

In general, it's a whole lot more work for less functionality.

HOW would you do it anyways?

Let's assume the same target types as earlier:

struct check {
    int i;
    double d;
    friend std::ostream& operator<<(std::ostream& os, check const& v) {
        return os << "check {" << v.i << ", " << v.d << "}";
    }
};

struct something {
    int a;
    int b;
    double c;
    friend std::ostream& operator<<(std::ostream& os, something const& v) {
        return os << "something {" << v.a << ", " << v.b << ", " << v.c << "}";
    }
};

Parsing Multi-Token

Now, you suggested boost::any, but we can be more specific. We know we only expect integers or doubles:

using ArgVal = boost::variant<int, double>;
using ArgVec = std::vector<ArgVal>;

By being more specific, you catch more errors earlier (see e.g. the parse error when supplying 'oops' as the value)

Now define the options as multi-token:

opt.add_options()
    ("do_something,s", bpo::value<ArgVec>()
        ->multitoken()
        ->default_value({1,2,3}, "1 2 3"), "")
    ("check-integrity,c", bpo::value<ArgVec>()
         ->multitoken()
         ->default_value({1,0.1}, "1 0.1"), "")
    ("help", "");

So far, I think that's actually pretty elegant.

Writing the rest of the demo program the way we'd like it to read:

bpo::variables_map vm;

try {
    bpo::store(parse_command_line(argc, argv, opt), vm);

    bpo::notify(vm);

    if (vm.count("help")) {
        std::cout << opt << std::endl;
        return 0;
    }

    std::cout << as_check(vm["check-integrity"].as<ArgVec>()) << "\n";
    std::cout << as_something(vm["do_something"].as<ArgVec>()) << "\n";
} catch (std::exception const& e) {
    std::cerr << "ERROR " << e.what() << "\n";
}

This leaves a few loose ends:

  • parsing
  • conversion (what are as_check and as_something?)

Parsing ArgVal

Like before we can simply provide an input streaming operator for the type. And as we said back then you can "Get As Fancy As You Want".

Let's employ Spirit X3 to get a lot of mileage for little effort:

static inline std::istream& operator>>(std::istream& is, ArgVal& into) {
    namespace x3 = boost::spirit::x3;
    std::string arg;
    getline(is, arg);

    x3::real_parser<double, x3::strict_real_policies<double> > real_;
    if (!phrase_parse(
            begin(arg), end(arg),
            (real_ | x3::int_) >> x3::eoi,
            x3::blank,
            into))
    {
        is.setstate(std::ios::failbit);
    }
    return is;
}

This time we embrace the noskipws because it convenes us, getting the full argument into a string.

Then, we parse it into a strict double OR an int.

By using strict real policies, we avoid parsing any integer number as a double, because later down the road we don't want to allow conversion from double to int (potentially losing information).

Converting

We need accessors to extract integer or double values. We will allow conversion from int to double since that never loses precision (--check 8 8 is as valid as --check 8 8.0).

static int as_int(ArgVal const& a) {
    return boost::get<int>(a);
}

static double as_double(ArgVal const& a) {
    if (auto pi = boost::get<int>(&a))
        return *pi; // non-lossy conversion allowed
    return boost::get<double>(a);
}

Now we can express the higher level conversions to target types in terms of the as_int and as_double helpers:

static check as_check(ArgVec const& av) {
    try { 
        if (av.size() != 2) throw "up"; 
        return { as_int(av.at(0)), as_double(av.at(1)) };
    } catch(...) {
        throw std::invalid_argument("expected check (int, double)");
    }
}

static something as_something(ArgVec const& av) {
    try { 
        if (av.size() != 3) throw "up"; 
        return { as_int(av.at(0)), as_int(av.at(1)), as_double(av.at(2)) };
    } catch(...) {
        throw std::invalid_argument("expected something (int, int, double)");
    }
}

I tried to write defensively, but not in particular good style. The code is safe, though.

DEMO TIME

Live On Coliru

#include <boost/program_options.hpp>
#include <iostream>
#include <iomanip>

namespace bpo = boost::program_options;

    struct check {
        int i;
        double d;
        friend std::ostream& operator<<(std::ostream& os, check const& v) {
            return os << "check {" << v.i << ", " << v.d << "}";
        }
    };

    struct something {
        int a;
        int b;
        double c;
        friend std::ostream& operator<<(std::ostream& os, something const& v) {
            return os << "something {" << v.a << ", " << v.b << ", " << v.c << "}";
        }
    };

#include <boost/spirit/home/x3.hpp>
using ArgVal = boost::variant<int, double>;
using ArgVec = std::vector<ArgVal>;

static inline std::istream& operator>>(std::istream& is, ArgVal& into) {
    namespace x3 = boost::spirit::x3;
    std::string arg;
    getline(is, arg);

    x3::real_parser<double, x3::strict_real_policies<double> > real_;
    if (!phrase_parse(
            begin(arg), end(arg),
            (real_ | x3::int_) >> x3::eoi,
            x3::blank,
            into))
    {
        is.setstate(std::ios::failbit);
    }
    return is;
}

static int as_int(ArgVal const& a) {
    return boost::get<int>(a);
}

static double as_double(ArgVal const& a) {
    if (auto pi = boost::get<int>(&a))
        return *pi; // non-lossy conversion allowed
    return boost::get<double>(a);
}

static check as_check(ArgVec const& av) {
    try { 
        if (av.size() != 2) throw "up"; 
        return { as_int(av.at(0)), as_double(av.at(1)) };
    } catch(...) {
        throw std::invalid_argument("expected check (int, double)");
    }
}

static something as_something(ArgVec const& av) {
    try { 
        if (av.size() != 3) throw "up"; 
        return { as_int(av.at(0)), as_int(av.at(1)), as_double(av.at(2)) };
    } catch(...) {
        throw std::invalid_argument("expected something (int, int, double)");
    }
}

int main(int argc, char* argv[]) {
    bpo::options_description opt("all options");

    opt.add_options()
        ("do_something,s", bpo::value<ArgVec>()
            ->multitoken()
            ->default_value({1,2,3}, "1 2 3"), "")
        ("check-integrity,c", bpo::value<ArgVec>()
             ->multitoken()
             ->default_value({1,0.1}, "1 0.1"), "")
        ("help", "");

    bpo::variables_map vm;

    try {
        bpo::store(parse_command_line(argc, argv, opt), vm);

        bpo::notify(vm);

        if (vm.count("help")) {
            std::cout << opt << std::endl;
            return 0;
        }

        std::cout << as_check(vm["check-integrity"].as<ArgVec>()) << "\n";
        std::cout << as_something(vm["do_something"].as<ArgVec>()) << "\n";
    } catch (std::exception const& e) {
        std::cerr << "ERROR " << e.what() << "\n";
    }
}

Prints

+ ./sotest --help
all options:
  -s [ --do_something ] arg (=1 2 3)
  -c [ --check-integrity ] arg (=1 0.1)
  --help 

+ ./sotest
check {1, 0.1}
something {1, 2, 3}
+ ./sotest --check ''
ERROR the argument for option '--check-integrity' is invalid
+ ./sotest --check oops
ERROR the argument ('oops') for option '--check-integrity' is invalid
+ ./sotest --check 8 8
check {8, 8}
something {1, 2, 3}
+ ./sotest --do_something 11 22 .33
check {1, 0.1}
something {11, 22, 0.33}
+ ./sotest --do_something 11 22 .33 --check 80 8
check {80, 8}
something {11, 22, 0.33}
sehe
  • 374,641
  • 47
  • 450
  • 633
0

This means that you need to define type priority.

Which should be considered to deserialise first.

Consider making it of a string type and cast back using boost::lexical_cast.

Sergei Krivonos
  • 4,217
  • 3
  • 39
  • 54