Search code examples
c++boostcommand-line-argumentsgetoptboost-program-options

parsing command-line options with sequence containers?


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.


Solution

  • 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)