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}