parsing command-line options with sequence contain

2019-02-24 07:53发布

问题:

This question has come up before, but it seems that none of the answers provide alternatives with boost-style generic programming.

Like many I use boost:program_options to parse command line line options. My current project is a program to manipulate data (e.g. images) with operators whose order is not interchangeable, e.g.

add 3 then multiply by 2
$ manipulate -in someimage.tif -a 3 -m 2

is not generally the same as

multiply by 2 then add 3
$ manipulate -in someimage.tif -m 2 -a 3

The -in option loads the file contents into a vector current_image, and each option on the command line modifies current_image.

But the variable_map container does not retain the order in which options are added. Not explicitly, at least. The answer in this post comes closest to what I have in mind, but then the amount of extra parsing code needed is about the same as with getopt().

Does anyone know a way to store the order of program options in the boost-provided container? Is it fundamentally impossible? Or is it possible (maybe even implemented) with a sequence container?

EDIT 1 I did find this really old post which seems still valid, stating that yes you can just iterate over a variables_map. Of course the order is not actually specified to be the same as the order on the command line (left to the compiler writers), so I guess it does still classify as a hack.
EDIT 2 That is not enough as the options are sorted by option string so the iteration order is not the same as the insertion order.

回答1:

Actually what you have there is more akin to an expression grammar. I'd suggest writing a grammar/parser for that instead of (ab?)using program_options for this.

  • If your program takes options: use program options.

  • If your program takes an expression: use an expression parser.

An example:

Live On Coliru

// #define BOOST_SPIRIT_DEBUG
#include <boost/fusion/adapted/struct.hpp>
#include <boost/fusion/include/io.hpp>
#include <boost/spirit/include/qi.hpp>

namespace qi  = boost::spirit::qi;

struct Operation {
    enum Kind { add, multiply } kind;
    double operand;

    friend std::ostream& operator<<(std::ostream& os, Kind k) {
        switch (k) {
            case add:      return os << "--add";
            case multiply: return os << "--multiply";
        };
        return os << "??";
    }
};

BOOST_FUSION_ADAPT_STRUCT(Operation, (Operation::Kind,kind)(double,operand))

template <typename It, typename Skipper = qi::blank_type> 
   struct expression_grammar : qi::grammar<It, std::vector<Operation>(), Skipper> {
       expression_grammar() : expression_grammar::base_type(start) {
           using namespace qi;

           opkinds.add
               ("-a",         Operation::add)
               ("--add",      Operation::add)
               ("-m",         Operation::multiply)
               ("--multiply", Operation::multiply)
               ;

           option = opkinds > eol > double_;

           start  = *(option > eol);

           BOOST_SPIRIT_DEBUG_NODES((start)(option))
       }
     private:
       qi::symbols<char, Operation::Kind> opkinds;
       qi::rule<It, Operation(), Skipper> option;
       qi::rule<It, std::vector<Operation>(), Skipper> start;
   };

int main(int argc, char const** argv) {
    std::stringstream iss;
    if (argc)
        std::copy(argv+1, argv+argc, std::ostream_iterator<const char*>(iss, "\n"));

    typedef boost::spirit::istream_iterator It;
    expression_grammar<It> grammar;

    It first(iss >> std::noskipws), last;
    std::vector<Operation> operations;
    bool ok = qi::phrase_parse(first, last, grammar, qi::blank, operations);

    if (ok)
    {
        std::cout << "Parse success\n";
        for (auto const& op : operations)
            std::cout << boost::fusion::as_vector(op) << "\n";
    } else
        std::cout << "Parse failed\n";

    if (first!=last)
       std::cout << "Remaining input: '" << std::string(first,last) << "'\n";
}

Note

  • I choose, gratuitously, to use eol as the option separator. You might want to use '\0' instead. This was easiest because the blank skipper already skips whitespace /except/ eol. I'm lazy :)
  • You might wnat to mix-and-match (not treat all parameters as part of the expression). A common pattern would be

    myprogram -x option1 -v -o filename -- my expression grammar follows
    

    A common alternative pattern is to make the expression a single parameter:

    myprogram -e 'add 5; multiply 32;' -x option1
    

    See this approach Live on Coliru too

  • I was lazy again with the "Parse success" printing (I didn't want to implement operator<< for the Operation type)

  • enable debug info with #define BOOST_SPIRIT_DEBUG (uncomment the first line)

    <start>
      <try>-a\n8\n-m\n7\n-a\n32\n</try>
      <option>
        <try>-a\n8\n-m\n7\n-a\n32\n</try>
        <success>\n-m\n7\n-a\n32\n</success>
        <attributes>[[--add, 8]]</attributes>
      </option>
      <option>
        <try>-m\n7\n-a\n32\n</try>
        <success>\n-a\n32\n</success>
        <attributes>[[--multiply, 7]]</attributes>
      </option>
      <option>
        <try>-a\n32\n</try>
        <success>\n</success>
        <attributes>[[--add, 32]]</attributes>
      </option>
      <option>
        <try></try>
        <fail/>
      </option>
      <success></success>
      <attributes>[[[--add, 8], [--multiply, 7], [--add, 32]]]</attributes>
    </start>
    Parse success
    (--add 8)
    (--multiply 7)
    (--add 32)