Read/Write 带有 boost::{program_options,property_tree} 的 ini 文件

Read/Write inifiles with boost::{program_options,property_tree}

利用boost,我想

  1. 从 ini 文件中读取选项,如果在 ini 文件中遇到未知选项则中止并且
  2. 稍后将它们保存在另一个 ini 文件中。

第一部分可以用boost::program_options完成:

try{
    inifile_options.add_options()
    ("ops1.i0", po::value<int>(&p.nx)->default_value(1), "test integer")
    ;

    po::variables_map vm;
    po::store(po::parse_config_file(pthfnini, inifile_options), vm);
    po::notify(vm);
}   
catch(exception& e){
    cerr << "error: " << e.what() << "\n";
    errorflag=1;
}

据我所知,boost::program_options 无法编写 ini 文件,但 boost::property_tree 可以:

pt::ptree iniPropTree;
pt::ini_parser::write_ini("./used0.ini",iniPropTree);

现在的问题是如何将 po::variables_map 中存储的数据转换为 pt::ptree?

阅读 boost 文档给我的印象是这是不可能的。以下是唯一可行的方法吗?

iniPropTree.put<int>("ops1.i0",vm["ops1.i0"].as<int>();

根据我的口味,它引入了相当多的冗余。但是,从头开始将数据读入 属性 树似乎不支持检查 undefined/misspelled 选项。

或者,是否可以迭代 variables_map 的内容并以某种方式推断出每个元素的相应数据类型?

完整代码在这里:

/*
 * g++ iniOps_test.cpp -Wall -std=c++11 -O3 -lboost_system -lboost_program_options -o iniOps_test.exe
 * 
 */

// C++11 & Boost libraries
#include <boost/program_options.hpp>            // po::options_description, po::variables_map, ...
#include <boost/property_tree/ptree.hpp>        // pt::ptree
#include <boost/property_tree/ini_parser.hpp>   // write_ini()
#include <iostream>                             // cout
#include <fstream>                              // ofstream, ifstream


// namespaces
namespace po = boost::program_options;
namespace pt = boost::property_tree;
using namespace std;


struct params{
    std::string inipthfn;
    int i0;
};


void read_inifile(params &p, po::variables_map &vm){

    // initialize variables
    int errorflag=0;
    std::ifstream pthfnini("./testini.ini");
    po::options_description inifile_options("Allowed inifile options");

    try{
        inifile_options.add_options()
        ("ops1.i0", po::value<int>(&p.i0)->default_value(1), "test integer")
        ;

        ;
        po::store(po::parse_config_file(pthfnini, inifile_options), vm);
        po::notify(vm);
    }
    catch(exception& e){
        cerr << "error: " << e.what() << "\n";
        errorflag=1;
    }

    pthfnini.close();
    if(errorflag){ std::cout<<"--- program shutdown due to error in read_inifile ---"<<std::endl; exit(1); }
}


int main(){

    params p;
    po::variables_map vm;
    pt::ptree iniPropTree;

    read_inifile(p,vm);                                     // get options from inifile

    // ??? conversion from vm -> pt ???

    pt::ini_parser::write_ini("./used0.ini",iniPropTree);   // save options to used.ini
    cout << p.i0 << endl;

    return 0;
}

ini 文件 "testini.ini" 的内容是:

[ops1]
i0=2

这里有一个概念问题。

命令行参数本质上是文本的。

变量映射中的值不是。使用的类型在值语义中配置(选项描述的一部分)。

如果您的所有选项都具有相同的类型,您可以"cheat"并对转换进行硬编码:

pt::ptree to_ptree(po::variables_map const& vm) {
    pt::ptree tree;
    for (auto& v : vm) {
        if (!v.second.empty() && !v.second.defaulted())
            tree.put(v.first, v.second.as<int>());
    }

    return tree;
}

节省:

[ops1]
i0=1

如果您需要更大的灵活性,您至少需要访问选项说明。这不是该库的预期用途,您可能很快就会 运行 进入实施的未记录部分。

However, reading data into a property tree from the beginning does not seem to support checking for undefined/misspelled options

嗯。这不完全正确。您可以创建自己的解析函数来添加逻辑。如果需要,请使用 属性 树翻译器。

这是一个扩展示例,显示了要验证的三个不同类型的参数:

enum class restricted { value1, value2 };

struct params {
    int        i0  = 1;
    restricted r1  = restricted::value2;
    std::string s2 = "some default";
};

我们想要一个像这样的解析函数:

params read_inifile(std::string filename) {
    params p;
    pt::ptree tree;
    std::ifstream file(filename);

    read_ini(file, tree);
    p.i0 = tree.get("ops1.i0", 1);
    p.r1 = tree.get("ops1.r1", restricted::value2);
    p.s2 = tree.get("ops1.s2", "some default");

    return p;
}

流媒体类型

要翻译和验证枚举,您只需要实现流操作符:

static inline std::istream& operator>>(std::istream& is, restricted& r) {
    std::string v;
    if (is >> std::ws >> v) {
        if (boost::iequals("value1", v))
            r = restricted::value1;
        else if (boost::iequals("value2", v))
            r = restricted::value2;
        else
            throw std::runtime_error("invalid restricted value");
    }
    return is;
}

static inline std::ostream& operator<<(std::ostream& os, restricted r) {
    switch(r) {
        case restricted::value1: return os << "value1";
        case restricted::value2: return os << "value2";
        default:                 return os << "invalid";
    }
}

自定义翻译器

假设 i0 需要自定义验证。在这个例子中,让我们要求它是一个奇数:

namespace translators {

    template <typename T>
    struct must_be_odd {
        typedef T internal_type;
        typedef T external_type;

        boost::optional<T> get_value(const std::string& str) const {
            if (str.empty()) return boost::none;

            T v = boost::lexical_cast<T>(str);

            if (v % 2 == 0)
                throw std::runtime_error("value must be odd");

            return boost::make_optional(v);
        }

        boost::optional<std::string> put_value(const T& i0) {
            assert(i0 % 2); // assert that the value was odd
            return boost::lexical_cast<std::string>(i0);
        }
    };

    static const must_be_odd<int> i0;
} 

现在我们可以简单地提供翻译器(在这里,更像是自定义验证器,如 Boost Program Options 也有它们):

    p.i0 = tree.get("ops1.i0", 1, translators::i0);

See it Live On Coliru

不支持的选项

这有点多。您必须迭代树,根据已知集检查结果路径。这是一个相当通用的实现(它应该适用于任何(宽)字符串类型的区分大小写的树):

template <typename Tree, 
         typename Path = typename Tree::path_type,
         typename Key = typename Path::key_type,
         typename Cmp = typename Tree::key_compare>
std::size_t unsupported(Tree const& tree, std::set<Key, Cmp> const& supported, Path prefix = "") {
    if (tree.size()) {
        std::size_t n = 0;
        for (auto& node : tree) {
            Path sub = prefix;
            sub /= node.first;
            n += unsupported(node.second, supported, sub);
        }
        return n;
    } else {
        if (!supported.count(prefix.dump()) && tree.template get_value_optional<std::string>())
            return 1;
    }
    return 0;
}

你可以这样使用它:

if (auto n = unsupported(tree, {"ops1.i0", "ops1.r1", "ops2.s2"})) {
    throw std::runtime_error(std::to_string(n) + " unsupported options");
}

完整演示

Live On Coliru

#include <boost/algorithm/string.hpp>
#include <iostream>
#include <set>

enum class restricted { value1, value2 };

static inline std::istream& operator>>(std::istream& is, restricted& r) {
    std::string v;
    if (is >> std::ws >> v) {
        if (boost::iequals("value1", v))
            r = restricted::value1;
        else if (boost::iequals("value2", v))
            r = restricted::value2;
        else
            throw std::runtime_error("invalid restricted value");
    }
    return is;
}

static inline std::ostream& operator<<(std::ostream& os, restricted r) {
    switch(r) {
        case restricted::value1: return os << "value1";
        case restricted::value2: return os << "value2";
        default:                 return os << "invalid";
    }
}

struct params {
    int        i0  = 1;
    restricted r1  = restricted::value2;
    std::string s2 = "some default";
};

#include <boost/property_tree/ini_parser.hpp>
#include <boost/lexical_cast.hpp>
#include <fstream>
namespace pt = boost::property_tree;

namespace translators {

    template <typename T>
    struct must_be_odd {
        typedef T internal_type;
        typedef T external_type;

        boost::optional<T> get_value(const std::string& str) const {
            if (str.empty()) return boost::none;

            T v = boost::lexical_cast<T>(str);

            if (v % 2 == 0)
                throw std::runtime_error("value must be odd");

            return boost::make_optional(v);
        }

        boost::optional<std::string> put_value(const T& i0) {
            assert(i0 % 2); // assert that the value was odd
            return boost::lexical_cast<std::string>(i0);
        }
    };

    static const must_be_odd<int> i0;
} 

template <typename Tree, 
         typename Path = typename Tree::path_type,
         typename Key = typename Path::key_type,
         typename Cmp = typename Tree::key_compare>
std::size_t unsupported(Tree const& tree, std::set<Key, Cmp> const& supported, Path prefix = "") {
    if (tree.size()) {
        std::size_t n = 0;
        for (auto& node : tree) {
            Path sub = prefix;
            sub /= node.first;
            n += unsupported(node.second, supported, sub);
        }
        return n;
    } else {
        if (!supported.count(prefix.dump()) && tree.template get_value_optional<std::string>())
            return 1;
    }
    return 0;
}

params read_inifile(std::string filename) {
    params p;
    try {
        pt::ptree tree;
        std::ifstream file(filename);

        read_ini(file, tree);

        p.i0 = tree.get("ops1.i0", 1, translators::i0);
        p.r1 = tree.get("ops1.r1", restricted::value2);
        p.s2 = tree.get("ops1.s2", "some default");

        if (auto n = unsupported(tree, {"ops1.i0", "ops1.r1", "ops2.s2"})) {
            throw std::runtime_error(std::to_string(n) + " unsupported options");
        }
    } catch (std::exception const& e) {
        std::cerr << "error: " << e.what() << "\n";
        throw std::runtime_error("read_inifile");
    }

    return p;
}

pt::ptree to_ptree(params const& p) {
    pt::ptree tree;

    tree.put("ops1.i0", p.i0, translators::i0);
    tree.put("ops1.r1", p.r1);
    tree.put("ops1.s2", p.s2);

    return tree;
}

int main() {
    params const p = read_inifile("./testini.ini"); // get options from filename
    write_ini("./used0.ini", to_ptree(p)); // save options to used.ini

    std::cout << p.i0 << std::endl;
}

输入类似

[ops1]
i0=17
i99=oops
[oops1]
also=oops

版画

error: 2 unsupported options
terminate called after throwing an instance of 'std::runtime_error'
  what():  read_inifile

changing 17 to 18 prints

error: value must be odd
terminate called after throwing an instance of 'std::runtime_error'
  what():  read_inifile

在有效输入上,used0.ini 将按预期写入:

[ops1]
i0=1
r1=value2
s2=some default

经过更多时间解决这个问题,我找到了一个合适的紧凑型解决方案:

关键是编写一个函数,根据它们的数据类型将 variables_map 中的条目转换为 propTree(感谢 sehe 让我走上正轨):

void translate_variables_map_to_ptree(po::variables_map &vm, pt::ptree &propTree){

    for(po::variables_map::iterator it=vm.begin(); it!=vm.end(); it++){
        if( it->second.value().type() == typeid(int) ){ propTree.put<int>(it->first,vm[it->first].as<int>()); }
        else if( it->second.value().type() == typeid(float) ){ propTree.put<float>(it->first,vm[it->first].as<float>()); }
        else if( it->second.value().type() == typeid(double) ){ propTree.put<double>(it->first,vm[it->first].as<double>()); }
        else if( it->second.value().type() == typeid(std::string) ){ propTree.put<std::string>(it->first,vm[it->first].as<std::string>()); }
        else if( it->second.value().type() == typeid(size_t) ){ propTree.put<size_t>(it->first,vm[it->first].as<size_t>()); }
        else{ printf("Error: unknown datatype. Abort!\n"); exit(EXIT_FAILURE); }
    }
}

完整的工作示例写入包含所有读取信息的正确 ini 文件:

/*
 * g++ iniOps_test.cpp -Wall -std=c++11 -O3 -lboost_system -lboost_program_options -o iniOps_test.exe
 * 
 */

// C++11 & Boost libraries
#include <boost/program_options.hpp>            // po::options_description, po::variables_map, ...
#include <boost/property_tree/ptree.hpp>        // pt::ptree
#include <boost/property_tree/ini_parser.hpp>   // write_ini()
#include <iostream>                             // cout
#include <fstream>                              // ofstream, ifstream


// namespaces
namespace po = boost::program_options;
namespace pt = boost::property_tree;
using namespace std;


struct params{
    std::string s0;
    int i0;
};


void read_inifile(params &p, po::variables_map &vm){

    // initialize variables
    int errorflag=0;
    std::ifstream pthfnini("./testini.ini");
    po::options_description inifile_options("Allowed inifile options");

    try{
        inifile_options.add_options()
        ("ops1.i0", po::value<int>(&p.i0)->default_value(1), "test integer")
        ("ops1.s0", po::value<std::string>(&p.s0)->default_value("default"), "test string")
        ;

        ;
        po::store(po::parse_config_file(pthfnini, inifile_options), vm);
        po::notify(vm);
    }
    catch(exception& e){
        cerr << "error: " << e.what() << "\n";
        errorflag=1;
    }

    pthfnini.close();
    if(errorflag){ std::cout<<"--- program shutdown due to error in read_inifile ---"<<std::endl; exit(1); }
}

void translate_variables_map_to_ptree(po::variables_map &vm, pt::ptree &propTree){

    for(po::variables_map::iterator it=vm.begin(); it!=vm.end(); it++){
        if( it->second.value().type() == typeid(int) ){ propTree.put<int>(it->first,vm[it->first].as<int>()); }
        else if( it->second.value().type() == typeid(float) ){ propTree.put<float>(it->first,vm[it->first].as<float>()); }
        else if( it->second.value().type() == typeid(double) ){ propTree.put<double>(it->first,vm[it->first].as<double>()); }
        else if( it->second.value().type() == typeid(std::string) ){ propTree.put<std::string>(it->first,vm[it->first].as<std::string>()); }
        else if( it->second.value().type() == typeid(size_t) ){ propTree.put<size_t>(it->first,vm[it->first].as<size_t>()); }
        else{ printf("Error: unknown datatype. Abort!\n"); exit(EXIT_FAILURE); }
    }
}


int main(){

    params p;
    po::variables_map vm;
    pt::ptree iniPropTree;

    read_inifile(p,vm);                                     // get options from inifile
    translate_variables_map_to_ptree(vm,iniPropTree);       // conversion from vm -> pt 
    pt::ini_parser::write_ini("./used0.ini",iniPropTree);   // save options to used.ini

    cout << p.i0 << endl;
    cout << p.s0 << endl;

    return 0;
}

通过读取命令行获取 variables_map vm,也可以更新 属性 树中的值(通过读取 inifile):

string opsName = "ops1.i0"; if(vm.count(opsName)) p.i0 = vm[opsName].as<int>();