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?

+3


source to share


1 answer


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 for bool

    than validate_foo

    . You can make the last token false

    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 copies value

    but leaves only the vector original_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 would command_line_parser

    create one entry in basic_parsed_options::option

    , and the code would add another with the same string_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>>()

)

+2


source







All Articles