【问题标题】:boost::program_options: parameters with a fixed and a variable token?boost::program_options:带有固定和可变标记的参数?
【发布时间】:2013-03-21 18:01:06
【问题描述】:

是否可以在 boost::program_options 中使用此类参数?

program  -p1 123 -p2 234 -p3 345 -p12 678

即,是否可以使用第一个标记(例如-p)后跟数字来动态指定参数名称?
我想避免这种情况:

program  -p 1 123 -p 2 234 -p 3 345 -p 12 678

【问题讨论】:

    标签: c++ boost boost-program-options


    【解决方案1】:

    Boost.ProgramOptions 不为此提供直接支持。不过,有两种通用解决方案各有取舍:

    • 通配符选项。
    • 自定义解析器。

    通配符选项

    如果可以使用--p 而不是-p,则可以使用通配符选项。这需要在提取过程中遍历 variables_map,因为 Boost.ProgramOptions 不支持在重载的 validate() 函数中同时接收键和值。

    #include <iostream>
    #include <map>
    #include <string>
    #include <utility>
    #include <vector>
    
    #include <boost/algorithm/string.hpp>
    #include <boost/foreach.hpp>
    #include <boost/lexical_cast.hpp>
    #include <boost/program_options.hpp>
    
    typedef std::map<int, int> p_options_type;
    
    /// @brief Extract options from variable map with the a key of
    ///        <prefix>#*.
    p_options_type get_p_options(
      const boost::program_options::variables_map& vm,
      const std::string prefix)
    {
      p_options_type p_options;
    
      const std::size_t prefix_size = prefix.size();
    
      boost::iterator_range<std::string::const_iterator> range;
      namespace po = boost::program_options;
      BOOST_FOREACH(const po::variables_map::value_type& pair, vm)
      {
        const std::string& key = pair.first;
    
        // Key is too small to contain prefix and a value, continue to next.
        if (key.size() < (1 + prefix.size())) continue;
    
        // Create range that partitions key into two parts.  Given a key
        // of "p12" the resulting partitions would be:
        //
        //     ,--------- key.begin           -., prefix = "p"
        //    / ,-------- result.begin        -:, post-prefix = "12"
        //   / /   ,----- key.end, result.end -'
        //  |p|1|2|
        range = boost::make_iterator_range(key.begin() + prefix_size,
                                           key.end());
    
        // Iterate to next key if the key:
        // - does not start with prefix
        // - contains a non-digit after prefix
        if (!boost::starts_with(key, prefix) || 
            !boost::all(range, boost::is_digit()))
          continue;
    
        // Create pair and insert into map.
        p_options.insert(
          std::make_pair(
            boost::lexical_cast<int>(boost::copy_range<std::string>(range)),
            pair.second.as<int>())); 
      }
      return p_options;
    }
    
    int main(int ac, char* av[])
    {
      namespace po = boost::program_options;
      po::options_description desc;
      desc.add_options()
        ("p*", po::value<int>())
        ;
    
      po::variables_map vm;
      store(po::command_line_parser(ac, av).options(desc).run(), vm);
    
      BOOST_FOREACH(const p_options_type::value_type& p, get_p_options(vm, "p"))
      {
        std::cout << "p" << p.first << "=" << p.second << std::endl;
      }
    }
    

    及其用法:

    ./a.out --p1 123 --p2 234 --p3=345 --p12=678
    p1=123
    p2=234
    p3=345
    p12=678

    这种方法需要遍历整个地图以识别通配符匹配,从而导致O(n) 的复杂性。此外,它需要修改所需的语法,其中需要使用--p1 123 而不是-p1 123。此限制是 Boost.ProgramOptions 的默认解析器行为的结果,其中单个连字符应后跟单个字符。


    自定义解析器

    另一种方法是将custom parser 添加到command_line_parser。自定义解析器将允许-p1 语法以及其他常见形式,例如--p1 123-p1=123。有一些行为需要处理:

    • 解析器一次将接收一个令牌。因此,它将在个别调用中收到p1123。解析器负责将p1123 配对。
    • Boost.ProgramOptions 需要至少一个解析器来处理一个令牌。否则boost::program_options::unknown_option会被抛出。

    为了解决这些行为,自定义解析器将管理状态并执行编码/解码:

    • 当解析器接收到p1时,它会提取1,将状态存储在解析器中。此外,它还为p 编码了一个no operation 值。
    • 当解析器接收到123 时,它会将其与存储的状态一起编码为p 的值。

    因此,如果解析器接收到-p1123,则pvariables_map 中插入两个值:无操作 值和1:123

    { "p" : [ "无操作", “1:123”] }

    通过提供帮助函数将编码的p 向量转换为映射,这种编码对用户来说是透明的。解码结果为:

    { 1 : 123 }

    示例代码如下:

    #include <iostream>
    #include <map>
    #include <string>
    #include <utility> // std::pair, std::make_pair
    #include <vector>
    
    #include <boost/algorithm/string.hpp>
    #include <boost/foreach.hpp>
    #include <boost/lexical_cast.hpp>
    #include <boost/program_options.hpp>
    
    typedef std::map<int, int> p_options_type;
    
    /// @brief Parser that provides the ability to parse "-p# #" options.
    ///
    /// @note The keys and values are passed in separately to the parser.
    ///       Thus, the struct must be stateful.
    class p_parser
    {
    public:
    
      explicit
      p_parser(const std::string& prefix)
        : prefix_(prefix),
          hyphen_prefix_("-" + prefix)
      {}
    
      std::pair<std::string, std::string> operator()(const std::string& token)
      {
        // To support "-p#=#" syntax, split the token.
        std::vector<std::string> tokens(2);
        boost::split(tokens, token, boost::is_any_of("="));
    
        // If the split resulted in two tokens, then key and value were
        // provided as a single token.
        if (tokens.size() == 2)
          parse(tokens.front()); // Parse key.
    
        // Parse remaining token.
        // - If tokens.size() == 2, then the token is the value.
        // - Otherwise, it is a key.
        return parse(tokens.back());
      }
    
      /// @brief Decode a single encoded value.
      static p_options_type::value_type decode(const std::string& encoded)
      {
        // Decode.
        std::vector<std::string> decoded(field_count_);
        boost::split(decoded, encoded, boost::is_any_of(delimiter_));
    
        // If size is not equal to the field count, then encoding failed.
        if (field_count_ != decoded.size())
          throw boost::program_options::invalid_option_value(encoded);
    
        // Transform.
        return std::make_pair(boost::lexical_cast<int>(decoded[0]),
                              boost::lexical_cast<int>(decoded[1]));
      }
    
      /// @brief Decode multiple encoded values.
      static p_options_type decode(const std::vector<std::string>& encoded_values)
      {
        p_options_type p_options;
        BOOST_FOREACH(const std::string& encoded, encoded_values)
        {
          // If value is a no-op, then continue to next.
          if (boost::equals(encoded, noop_)) continue;
          p_options.insert(decode(encoded));
        }
        return p_options;
      }
    
    private:
    
      std::pair<std::string, std::string> parse(const std::string& token)
      {
        return key_.empty() ? parse_key(token)
                            : parse_value(token);
      }
    
      /// @brief Parse key portion of option: "p#"
      std::pair<std::string, std::string> parse_key(const std::string& key)
      {
        // Search for the prefix to obtain a range that partitions the key into
        // three parts.  Given --p12, the partitions are:
        //
        //      ,--------- key.begin    -., pre-prefix   = "-"
        //     / ,-------- result.begin -:, prefix       = "-p"
        //    / /   ,----- result.end   -:, post-prefix  = "12"
        //   / /   /   ,-- key.end      -'
        //  |-|-|p|1|2|
        //
        boost::iterator_range<std::string::const_iterator> result =
          boost::find_first(key, prefix_);
    
        // Do not handle the key if:
        // - Key end is the same as the result end.  This occurs when either
        //   either key not found or nothing exists beyond the key (--a or --p)
        // - The distance from start to prefix start is greater than 2 (---p)
        // - Non-hyphens exists before prefix (a--p)
        // - Non-numeric values are after result.
        if (result.end() == key.end() ||
            distance(key.begin(), result.begin()) > 2 ||
            !boost::all(
              boost::make_iterator_range(key.begin(), result.begin()),
              boost::is_any_of("-")) ||
            !boost::all(
              boost::make_iterator_range(result.end(), key.end()),
              boost::is_digit()))
        {
          // A different parser will handle this token.
          return make_pair(std::string(), std::string());
        }
    
        // Otherwise, key contains expected format.
        key_.assign(result.end(), key.end());
    
        // Return non-empty pair, otherwise Boost.ProgramOptions will
        // consume treat the next value as the complete value.  The
        // noop entries will be stripped in the decoding process.
        return make_pair(prefix_, noop_);
      }
    
      /// @brief Parse value portion of option: "#"
      std::pair<std::string, std::string> parse_value(const std::string& value)
      {
        std::pair<std::string, std::string> encoded =
          make_pair(prefix_, key_ + delimiter_ + value);
        key_.clear();
        return encoded;
      }
    
    private:
      static const int field_count_ = 2;
      static const std::string delimiter_;
      static const std::string noop_;
    private:
      const std::string prefix_;
      const std::string hyphen_prefix_;
      std::string key_;
    };
    
    const std::string p_parser::delimiter_ = ":";
    const std::string p_parser::noop_      = "noop";
    
    /// @brief Extract and decode options from variable map.
    p_options_type get_p_options(
      const boost::program_options::variables_map& vm,
      const std::string prefix)
    {
      return p_parser::decode(vm[prefix].as<std::vector<std::string> >());
    }
    
    int main(int ac, char* av[])
    {
      const char* p_prefix = "p";
      namespace po = boost::program_options;
    
      // Define options.
      po::options_description desc;
      desc.add_options()
        (p_prefix, po::value<std::vector<std::string> >()->multitoken())
        ;
    
      po::variables_map vm;
      store(po::command_line_parser(ac, av).options(desc)
              .extra_parser(p_parser(p_prefix)).run()
           , vm);
    
      // Extract -p options. 
      if (vm.count(p_prefix))
      {
        // Print -p options.
        BOOST_FOREACH(const p_options_type::value_type& p,
                      get_p_options(vm, p_prefix))
        {
          std::cout << "p" << p.first << "=" << p.second << std::endl;
        }
      }
    }
    

    及其用法:

    ./a.out -p1 123 --p2 234 -p3=345 --p12=678
    p1=123
    p2=234
    p3=345
    p12=678

    除了是一个更大的解决方案之外,一个缺点是需要通过解码过程才能获得所需的值。不能简单地以一种有意义的方式迭代 vm["p"] 的结果。

    【讨论】:

    • 谢谢坦纳。您的第一个解决方案(“通配符选项”)正是我所需要的。
    猜你喜欢
    • 2011-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-22
    • 2014-11-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多