I'm trying to port old command line tool to boost::program_options
. The tool is used in lots of 3rd-party scripts, some of them I cannot update, so changing of command line interface (CLI) is not the way for me.
I have one positional argument, several flag and regular arguments. But I run into a trouble with ranges
argument. It should work as follows:
> my_too.exe -ranges 1,2,4-7,4 some_file.txt # args[ranges]="1,2,4-7,4"
> my_too.exe -ranges -other_param some_file.txt # args[ranges]=""
> my_too.exe -ranges some_file.txt # args[ranges]=""
Basically, I want boost::po
to stop parsing argument value if other argument is met or if type doesn't match. Is there a way to implement exactly this behavior?
I tried using implicit_value
but it doesn't work because it will require CLI format change (argument is required to be adjusted with a key):
> my_too.exe -ranges="1,2-3,7" some_file.txt
I tried using multitoken, zero_tokens
trick, but it doesn't stop when positional argument is met or argument doesn't match.
> my_tool.exe -ranges 1,2-4,7 some_file.txt # args[ranges]=1,2-4,7,some_file.txt
Any ideas?
This is not simple, but the syntax you require is weird and surely needs some hand tweaking is needed, e.g. the validator for multitoken
syntax, to recognize "extra" argument.
I'll let myself start with the cool part:
./a.out 1st_positional --foo yes off false yes file.txt --bar 5 -- another positional
parsed foo values: 1, 0, 0, 1,
parsed bar values: 5
parsed positional values: 1st_positional, another, positional, file.txt,
So it seems to work even for a pretty weird combination of options. It also handled:
./a.out 1st_positional --foo --bar 5 -- another positional
./a.out 1st_positional --foo file.txt --bar 5 -- another positional
You can manually tamper with recognized values after running the command_line_parser
, before using store
.
Below is a crude draft. It handles one extra token at the end of --foo
multitoken
option. It calls custom validation and moves last offending token to a positional argument. There are few caveats I described after the code. I have left some debug cout
s intentionally so anyone can easily play with it.
So here is the draft:
#include <vector>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/variables_map.hpp>
#include <boost/program_options/positional_options.hpp>
#include <boost/program_options/option.hpp>
#include <algorithm>
using namespace boost::program_options;
#include <iostream>
using namespace std;
// A helper function to simplify the main part.
template<class T>
ostream& operator<<(ostream& os, const vector<T>& v)
{
copy(v.begin(), v.end(), ostream_iterator<T>(os, ", "));
return os;
}
bool validate_foo(const string& s)
{
return s == "yes" || s == "no";
}
int main(int ac, char* av[])
{
try {
options_description desc("Allowed options");
desc.add_options()
("help", "produce a help message")
("foo", value<std::vector<bool>>()->multitoken()->zero_tokens())
("bar", value<int>())
("positional", value<std::vector<string>>())
;
positional_options_description p;
p.add("positional", -1);
variables_map vm;
auto clp = command_line_parser(ac, av).positional(p).options(desc).run();
// ---------- Crucial part -----------
auto foo_itr = find_if( begin(clp.options), end(clp.options), [](const auto& opt) { return opt.string_key == string("foo"); });
if ( foo_itr != end(clp.options) ) {
auto& foo_opt = *foo_itr;
cout << foo_opt.string_key << '\n';
std::cout << "foo values: " << foo_opt.value << '\n';
if ( !validate_foo(foo_opt.value.back()) ) { // [1]
auto last_value = foo_opt.value.back(); //consider std::move
foo_opt.value.pop_back();
cout << "Last value of foo (`" << last_value << "`) seems wrong. Let's take care of it.\n";
clp.options.emplace_back(string("positional"), vector<string>{last_value} ); // [2]
}
}
// ~~~~~~~~~~ Crucial part ~~~~~~~~~~~~
auto pos = find_if( begin(clp.options), end(clp.options), [](const auto& opt) { return opt.string_key == string("positional"); });
if ( pos != end(clp.options)) {
auto& pos_opt = *pos;
cout << "positional pos_key: " << pos_opt.position_key << '\n';
cout << "positional string_key: " << pos_opt.string_key << '\n';
cout << "positional values: " << pos_opt.value << '\n';
cout << "positional original_tokens: " << pos_opt.original_tokens << '\n';
}
store(clp, vm);
notify(vm);
if (vm.count("help")) {
cout << desc;
}
if (vm.count("foo")) {
cout << "parsed foo values: "
<< vm["foo"].as<vector<bool>>() << "\n";
}
if (vm.count("bar")) {
cout << "parsed bar values: "
<< vm["bar"].as<int>() << "\n";
}
if (vm.count("positional")) {
cout << "parsed positional values: " <<
vm["positional"].as< vector<string> >() << "\n";
}
}
catch(exception& e) {
cout << e.what() << "\n";
}
}
So the problems I see are:
Custom validation should be the same as the one used by parser for the option type. As you can see program_options
is more permissive for bool
than validate_foo
. You can make last token false
and it would be wrongly moved. I didn't know how to pull out the validator used for the option by the library so I supplied a crude custom version.
Adding an entry to basic_parsed_options::option
is rather tricky. It basically messes with internal state of the object. As one can see I have made a rather rudimentary version, e.g. it copies value
, but leaves original_tokens
vector alone creating discrepancy in the data structure. Other fields are also left as-is.
Weird things might happen if you don't account for positional
arguments present at other places in command line. That would mean command_line_parser
would create one entry in basic_parsed_options::option
, and the code would add another with the same string_key
. I am not sure of consequences, but it does work with the weird example I used.
Resolving problem 1. could make it a nice solution. I guess other stuff is for diagnostics. (Not 100% sure though!). One could also identify offending tokens by other means, or in a loop.
You could just remove offending token and store them on side, but leaving this to boost_options
still uses it's validation routines, which can be nice. (You can try changing positional
to value<std::vector<int>>()
)