Extended program capabilities, empty string processing
I am trying to port an old command line tool to boost::program_options
. The tool is used in a lot of third-party scripts, some of which I cannot update, so changing the command line interface (CLI) is not suitable for me.
I have one positional argument, multiple flags, and regular arguments. But I ran into a problem with the argument ranges
. It should work like this:
> 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 to boost::po
stop parsing the argument value if another argument is encountered or if the type does not match. Is there a way to implement exactly this behavior?
I tried to use implicit_value
, but it doesn't work because it would require a CLI format change (the argument must be adjusted with a key):
> my_too.exe -ranges="1,2-3,7" some_file.txt
I've tried the trick multitoken, zero_tokens
, but it doesn't stop when a positional argument is encountered, or the 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?
source to share
It's not easy, but the required syntax is weird and certainly needs some manual tweaking, such as the syntax validator multitoken
to recognize the "extra" argument.
Let me 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 works even for a rather odd 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
Decision
After use, command_line_parser
you can manually change the recognized values ββbefore using store
.
Below is a draft. It handles one additional token at the end of the option --foo
multitoken
. It invokes custom validation and translates the last offending token into a positional argument. After the code, I describe a few caveats. I deliberately debugged cout
so everyone could easily play with it.
So here's a rough 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 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 as follows:
-
The custom validation must be the same as the one used by the parser for the option type. As you can see, it is
program_options
more solvable forbool
thanvalidate_foo
. You can make the last tokenfalse
and it will not move correctly. I didn't know how to pull out the validator used by the library for this option, so I provided a rough custom version. -
Adding an entry to it is
basic_parsed_options::option
quite difficult. This is basically at odds with the internal state of the object. As you can see, I made a rather rudimentary version, for example. it copiesvalue
but leaves only the vectororiginal_tokens
, creating a mismatch in the data structure. Other fields also remain as they are. -
Incredible things can happen if you ignore the arguments
positional
present elsewhere on the command line. This would mean it wouldcommand_line_parser
create one entry inbasic_parsed_options::option
, and the code would add another with the samestring_key
. I'm not sure about the implications, but it works with a weird example I used.
Solution to problem 1. might be a good solution. I think other things are for diagnostics. (Not sure, though 100%!). You can also define offensive tokens in other ways or in a loop.
You can just remove the offending token and store it on the side, but leaving that on boost_options
is still using its validation routines, which might be nice. (You can try changing positional
to value<std::vector<int>>()
)
source to share