boost program_options: 从配置文件中读取所需的参数

boost program_options: Read required parameter from config file

我想使用boost_program_options如下:

问题是:包含配置文件名的变量在调用 po::notify() 之前不会被填充,并且该函数还会为任何未实现的强制选项抛出异常。因此,如果未在命令行上指定强制选项(使配置文件没有实际意义),则不会读取配置文件。

不优雅的解决方案是在 add_options() 中不将选项标记为强制性的,然后在 'by hand' 中强制执行它们。 boost_program_options 库中有解决这个问题的方法吗?

MWE

bpo-mwe.conf:

db-hostname = foo
db-username = arthurdent
db-password = forty-two

代码:

#include <stdexcept>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <boost/program_options.hpp>

// enable/disable required() below
#ifndef WITH_REQUIRED
#define WITH_REQUIRED
#endif

namespace po = boost::program_options;
namespace fs = std::filesystem;

int main(int argc, char *argv[])
{

    std::string config_file;

    po::options_description generic("Generic options");
    generic.add_options()
    ("config,c", po::value<std::string>(&config_file)->default_value("bpo-mwe.conf"), "configuration file")
    ;

    // Declare a group of options that will be
    // allowed both on command line and in
    // config file
    po::options_description main_options("Main options");
    main_options.add_options()
    #ifdef WITH_REQUIRED
    ("db-hostname", po::value<std::string>()->required(), "database service name")
    ("db-username", po::value<std::string>()->required(), "database user name")
    ("db-password", po::value<std::string>()->required(), "database user password")
        #else
    ("db-hostname", po::value<std::string>(), "database service name")
    ("db-username", po::value<std::string>(), "database user name")
    ("db-password", po::value<std::string>(), "database user password")
    #endif
    ;

    // set options allowed on command line
    po::options_description cmdline_options;
    cmdline_options.add(generic).add(main_options);

    // set options allowed in config file
    po::options_description config_file_options;
    config_file_options.add(main_options);

    // set options shown by --help
    po::options_description visible("Allowed options");
    visible.add(generic).add(main_options);

    po::variables_map variable_map;

    // store command line options
    // Why not po::store?
    //po::store(po::parse_command_line(argc, argv, desc), vm);
    store(po::command_line_parser(argc, argv).options(cmdline_options).run(), variable_map);

    notify(variable_map); // <- here is the problem point

    // Problem: config_file is not set until notify() is called, and notify() throws exception for unfulfilled required variables

    std::ifstream ifs(config_file.c_str());
    if (!ifs)
    {
        std::cout << "can not open configuration file: " << config_file << "\n";
    }
    else
    {
        store(parse_config_file(ifs, config_file_options), variable_map);
        notify(variable_map);
    }

    std::cout << config_file << " was the config file\n";
    return 0;
}

区分配置文件和命令行参数,不要将两者解析到同一个映射中。

首先分别解析命令行参数,获取配置文件名(如果有的话),然后然后加载文件并将其解析到第二个映射中。


如果一些配置文件值也可以在命令行上提供,那么我个人会对命令行参数进行两次传递,使其成为一个三步过程:

  1. 解析命令行参数,忽略除 config 选项之外的所有参数
  2. 读取并解析配置文件
  3. 并再次传递命令行参数,忽略 config 选项

我只是使用通知值语义将值放入config_file。相反,直接从地图使用它:

auto config_file = variable_map.at("config").as<std::string>();

现在您可以按预期在最后执行通知:

Live On Coliru

#include <boost/program_options.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>

namespace po = boost::program_options;

int main(int argc, char *argv[])
{
    po::options_description generic("Generic options");
    generic.add_options()
        ("config,c", po::value<std::string>()->default_value("bpo-mwe.conf"), "configuration file")
    ;

    // Declare a group of options that will be allowed both on command line and
    // in config file
    struct {
        std::string host, user, pass;
    } dbconf;

    po::options_description main_options("Main options");
    main_options.add_options()
        ("db-hostname", po::value<std::string>(&dbconf.host)->required(), "database service name")
        ("db-username", po::value<std::string>(&dbconf.user)->required(), "database user name")
        ("db-password", po::value<std::string>(&dbconf.pass)->required(), "database user password")
    ;

    // set options allowed on command line
    po::options_description cmdline_options;
    cmdline_options.add(generic).add(main_options);

    // set options allowed in config file
    po::options_description config_file_options;
    config_file_options.add(main_options);

    // set options shown by --help
    po::options_description visible("Allowed options");
    visible.add(generic).add(main_options);

    po::variables_map variable_map;

    //po::store(po::parse_command_line(argc, argv, desc), vm);
    store(po::command_line_parser(argc, argv).options(cmdline_options).run(),
          variable_map);

    auto config_file = variable_map.at("config").as<std::string>();

    std::ifstream ifs(config_file.c_str());
    if (!ifs) {
        std::cout << "can not open configuration file: " << config_file << "\n";
    } else {
        store(parse_config_file(ifs, config_file_options), variable_map);
        notify(variable_map);
    }

    notify(variable_map);
    std::cout << config_file << " was the config file\n";

    std::cout << "dbconf: " << std::quoted(dbconf.host) << ", " 
        << std::quoted(dbconf.user)  << ", "
        << std::quoted(dbconf.pass)  << "\n"; // TODO REMOVE FOR PRODUCTION :)
}

打印例如

$ ./sotest
bpo-mwe.conf was the config file
dbconf: "foo", "arthurdent", "forty-two"

$ ./sotest -c other.conf 
other.conf was the config file
dbconf: "sbb", "neguheqrag", "sbegl-gjb"

$ ./sotest -c other.conf --db-user PICKME
other.conf was the config file
dbconf: "sbb", "PICKME", "sbegl-gjb"

您可能已经猜到了 other.conf 是由 ROT13 从 bpo-mwe.conf 派生的。