如何使用Boost在.ini文件中扩展环境变量

3
我有一个像这样的INI文件
[Section1]
Value1 = /home/%USER%/Desktop
Value2 = /home/%USER%/%SOME_ENV%/Test

想使用Boost解析它。我尝试使用Boost property_tree,例如:
boost::property_tree::ptree pt;
boost::property_tree::ini_parser::read_ini("config.ini", pt);

std::cout << pt.get<std::string>("Section1.Value1") << std::endl;
std::cout << pt.get<std::string>("Section1.Value2") << std::endl;

但它没有扩展环境变量。输出看起来像这样。
/home/%USER%/Desktop
/home/%USER%/%SOME_ENV%/Test

我原本期望的是类似于……的东西。
/home/Maverick/Desktop
/home/Maverick/Doc/Test

我不确定使用boost property_tree是否可能。
我会感激任何提示,以便使用boost解析这种类型的文件。

为了解析Inifiles,我之前也使用boost-spirit编写了一个非常全面的Inifile解析器,但我认为我会像以前一样推荐boost-propertytree,并且它实际上与这个问题无关。 - sehe
@Maverick:请接受一个答案或说明为何这些答案不符合您的需求。 - Lightness Races in Orbit
2个回答

2

这里是另一种使用旧技巧的方法:

  • 不要求Spirit,或者Boost
  • 不将接口硬编码到std::string(而是允许任何组合的输入迭代器输出迭代器
  • 正确处理%%作为单个%1

本质:

#include <string>
#include <algorithm>

static std::string safe_getenv(std::string const& macro) {
    auto var = getenv(macro.c_str());
    return var? var : macro;
}

template <typename It, typename Out>
Out expand_env(It f, It l, Out o)
{
    bool in_var = false;
    std::string accum;
    while (f!=l)
    {
        switch(auto ch = *f++)
        {
            case '%':
                if (in_var || (*f!='%'))
                {
                    in_var = !in_var;
                    if (in_var) 
                        accum.clear();
                    else
                    {
                        accum = safe_getenv(accum);
                        o = std::copy(begin(accum), end(accum), o);
                    }
                    break;
                } else 
                    ++f; // %% -> %
            default:
                if (in_var)
                    accum += ch;
                else
                    *o++ = ch;
        }
    }
    return o;
}

#include <iterator>

std::string expand_env(std::string const& input)
{
    std::string result;
    expand_env(begin(input), end(input), std::back_inserter(result));
    return result;
}

#include <iostream>
#include <sstream>
#include <list>

int main()
{
    // same use case as first answer, show `%%` escape
    std::cout << "'" << expand_env("Greeti%%ng is %HOME% world!")  << "'\n";

    // can be done streaming, to any container
    std::istringstream iss("Greeti%%ng is %HOME% world!");
    std::list<char> some_target;

    std::istreambuf_iterator<char> f(iss), l;
    expand_env(f, l, std::back_inserter(some_target));
    std::cout << "Streaming results: '" << std::string(begin(some_target), end(some_target)) << "'\n";

    // some more edge cases uses to validate the algorithm (note `%%` doesn't
    // act as escape if the first ends a 'pending' variable)
    std::cout << "'" << expand_env("")                           << "'\n";
    std::cout << "'" << expand_env("%HOME%")                     << "'\n";
    std::cout << "'" << expand_env(" %HOME%")                    << "'\n";
    std::cout << "'" << expand_env("%HOME% ")                    << "'\n";
    std::cout << "'" << expand_env("%HOME%%HOME%")               << "'\n";
    std::cout << "'" << expand_env(" %HOME%%HOME% ")             << "'\n";
    std::cout << "'" << expand_env(" %HOME% %HOME% ")            << "'\n";
}

在我的电脑上运行,输出:

'Greeti%ng is /home/sehe world!'
Streaming results: 'Greeti%ng is /home/sehe world!'
''
'/home/sehe'
' /home/sehe'
'/home/sehe '
'/home/sehe/home/sehe'
' /home/sehe/home/sehe '
' /home/sehe /home/sehe '

1 当然,“正确”是主观的。至少,我认为这个

  • 会很有用(否则你怎么配置一个包含%的合法值?)
  • 是Windows上cmd.exe的做法

1

我相信这可以通过手写解析器轻松实现(请参考我的新答案),但我个人更喜欢Spirit:

grammar %= (*~char_("%")) % as_string ["%" >> +~char_("%") >> "%"] 
                                      [ _val += phx::bind(safe_getenv, _1) ];

含义:

  • 取出所有非%字符(如果有的话)
  • 然后从%中取出任何一个单词,并在附加之前通过safe_getenv进行处理

现在,safe_getenv是一个简单的包装器:

static std::string safe_getenv(std::string const& macro) {
    auto var = getenv(macro.c_str());
    return var? var : macro;
}

这是一个完整的最小实现:

Here's a complete minimal implementation:

#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>

static std::string safe_getenv(std::string const& macro) {
    auto var = getenv(macro.c_str());
    return var? var : macro;
}

std::string expand_env(std::string const& input) 
{
    using namespace boost::spirit::qi;
    using boost::phoenix::bind;

    static const rule<std::string::const_iterator, std::string()> compiled =
          *(~char_("%"))                         [ _val+=_1 ] 
        % as_string ["%" >> +~char_("%") >> "%"] [ _val += bind(safe_getenv, _1) ];

    std::string::const_iterator f(input.begin()), l(input.end());
    std::string result;

    parse(f, l, compiled, result);
    return result;
}

int main()
{
    std::cout << expand_env("Greeting is %HOME% world!\n");
}

这将打印

Greeting is /home/sehe world!

在我的电脑上

注释

  • 这并不是优化的(好吧,至少只编译了一次规则)
  • replace_regex_copy同样可以,并且更有效率(?)
  • 参见此答案,其中涉及稍微复杂的“扩展”引擎:使用Boost.Spirit编译简单解析器

    • 使用输出迭代器而非std::string进行累积
    • 允许嵌套变量
    • 允许转义

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接