引言

几乎每个非平凡的程序都需要配置参数:输入文件路径、算法阈值、输出格式、模型参数……硬编码这些值会让程序失去灵活性,而手写解析逻辑又繁琐且容易出错。

getoptionutilsxx 库中的轻量级配置解析组件。它采用声明式设计理念:你先声明程序需要哪些参数、哪些是必填的、哪些参数属于互斥组,然后从文件或字符串向量中读取配置,getoption 会自动完成解析、校验和类型转换。本文将带你从基础用法到高阶技巧,彻底掌握这个工具。


一、设计理念:声明式配置管理

传统的配置解析往往是"命令式"的:读一行、判断键名、手动转换类型、检查是否缺失。getoption 将其转变为"声明式":

// 1. 声明:我的程序需要这些参数
getoption gopt;
gopt.add_options({"range", "interval", "output"}, {true, true, false});

// 2. 读取:从配置文件加载
gopt.read_options("config.txt");

// 3. 校验:自动检查必填项和互斥组
gopt.check_mandatory();

// 4. 使用:直接获取已转换类型的值
auto range = gopt.get_value<std::string>("range");
auto weight = gopt.get_values<double>("weight", '|', '/');

这种设计的优势:

  • 集中管理:所有参数定义在一处,一目了然
  • 自动校验:必填项、互斥组自动检查,无需手写 if 判断
  • 类型安全:模板函数自动完成字符串到目标类型的转换
  • 别名支持:一个参数可以有多个名称(如 model-file/ModelFile/model_file

二、快速上手

2.1 配置文件格式

getoption 使用简洁的 key-value 格式:

# 以 # 开头的行是注释,会被自动跳过
range = 100/1000/200/2000/full_range
interval = 10/10
weight = 120.12/232/114.2
model-file = test.txt

# 空行也会被跳过,不影响解析

# 同一个 key 可以出现多次,值会自动合并
model-tag = model1
model-tag = model2
model-tag = model3

out-model = example.msh

格式规则:

  1. 注释行:以 # 开头(可自定义)
  2. 空行:自动跳过
  3. 参数行key <分隔符> value,默认分隔符为 =
  4. 多值合并:同一 key 多次出现,值用连接符拼接(默认逗号 ,

2.2 基础代码示例

#include "get_option.hpp"
#include <iostream>

int main() try {
    // 步骤1:声明参数
    utilsxx::getoption gopt;
    gopt.add_options(
        {"range", "interval", "weight", "model-file", "output"},  // 参数名
        {true, true, false, true, false}                            // 是否必填
    );

    // 步骤2:读取配置文件
    gopt.read_options("config.txt");

    // 步骤3:校验必填项
    gopt.check_mandatory();

    // 步骤4:获取参数值
    std::string model = gopt.get_value<std::string>("model-file");
    std::vector<double> w = gopt.get_values<double>("weight", '|', '/');

    std::cout << "Model: " << model << "\n";
    std::cout << "Weight count: " << w.size() << "\n";

    return 0;
}
catch(utilsxx::error_handler &e) {
    e.show();
    return 1;
}

三、参数声明详解

3.1 逐个添加参数

gopt.add_option("range", true);      // 必填参数
gopt.add_option("output", false);    // 可选参数
gopt.add_option("verbose", false, 1); // 可选参数,属于组1

参数说明:

参数类型说明
opt_namestring参数名称
mandabool是否必填(true = 必填)
gpint所属组号(-1 = 不属于任何组)

3.2 批量添加参数

gopt.add_options(
    {"range", "interval", "weight", "model-file", "model-tag", "out-model", "save-model"},
    {true, true, true, true, true, true, false}
);

两个 initializer_list 必须长度一致,否则会抛出异常。

3.3 设置参数分组(互斥/至少选一)

分组用于实现"至少选一个"的校验逻辑:

// out-model 和 save-model 属于组1,两者至少填一个
gopt.set_group(1, {"out-model", "save-model"});

校验时:

gopt.check_group(1);    // 检查组1:至少一个参数被设置
gopt.check_groups();    // 检查所有组
gopt.check_mandatory(); // 检查所有必填项(组内参数不检查)

注意:属于分组的参数在 check_mandatory()不会被检查,因为组校验已经保证了"至少一个"的存在性。


四、读取配置

4.1 从文件读取

gopt.read_options(
    "data/option_sample",     // 文件名(自动追加 .txt)
    "=",                      // key-value 分隔符(默认 =)
    ",",                      // 多值连接符(默认 ,)
    '#'                       // 注释符(默认 #)
);

4.2 从字符串向量读取

适合从命令行参数或其他内存数据源读取:

std::vector<std::string> opts = {
    "# this is an annotation",
    "range = 100/1000/200/2000/full_range",
    "interval = 10/10",
    "weight = 120.12/232/114.2",
    "model-file = test.txt",
    "",  // 空行会被跳过
    "model-tag = model1",
    "model-tag = model2",
    "model-tag = model3",
    "out-model = example.msh"
};

gopt.read_options(opts);  // 参数与文件版相同

4.3 多值合并行为

当同一个 key 在配置文件中多次出现时,值会自动合并:

model-tag = model1
model-tag = model2
model-tag = model3

读取后 model-tag 的值为:model1,model2,model3(默认用逗号连接)

你可以自定义连接符:

// 用分号连接多值
gopt.read_options("config.txt", "=", ";");

五、获取参数值

5.1 获取单个值

// 获取字符串值
std::string model = gopt.get_value<std::string>("model-file");

// 获取 double 值(自动类型转换)
double val = gopt.get_value<double>("threshold");

// 获取 int 值
int count = gopt.get_value<int>("max-iterations");

// 获取 bool 值
bool verbose = gopt.get_value<bool>("verbose");

5.2 获取多值(向量)

配置文件中的值本身可以包含分隔符,用 get_values() 解析为向量:

weight = 120.12/232/114.2
// 用 '/' 作为值内分隔符,解析为 double 向量
std::vector<double> weight = gopt.get_values<double>("weight", '|', '/');
// 结果: {120.12, 232.0, 114.2}

参数说明:

参数说明
key参数名(可含别名,如 "weight|Weight"
delimiterkey 别名之间的分隔符(默认 |
sep值内部的分隔符(默认空格)

5.3 参数别名

一个参数可以有多个名称,用 | 分隔:

// 以下调用都会匹配同一个参数
auto w1 = gopt.get_value<std::string>("model-file");
auto w2 = gopt.get_value<std::string>("Model-File");
auto w3 = gopt.get_value<std::string>("model_file");

// 更简洁的写法:一次声明多个别名
auto w = gopt.get_value<std::string>("model-file|Model-File|model_file");

这在处理不同命名风格的配置文件时非常有用(如用户可能写 model-fileModelFilemodel_file)。

5.4 检查参数是否存在

if (gopt.has_value("output")) {
    std::string out = gopt.get_value<std::string>("output");
}

has_value() 同样支持别名:

if (gopt.has_value("output|Output|out")) {
    // ...
}

六、校验机制

6.1 必填项校验

gopt.add_options({"input", "output", "threshold"}, {true, true, false});
gopt.read_options("config.txt");
gopt.check_mandatory();  // 如果 input 或 output 未设置,抛出异常

错误输出示例:

getoption: mandatory option not set: input
getoption: mandatory option not set: output

6.2 分组校验(至少选一)

gopt.add_options({"out-model", "save-model"}, {false, false});
gopt.set_group(1, {"out-model", "save-model"});
gopt.read_options("config.txt");
gopt.check_group(1);  // 两者至少填一个,否则抛出异常

错误输出示例:

getoption: need at least one of grouped options:
out-model
save-model

6.3 完整校验流程

gopt.read_options("config.txt");
gopt.check_mandatory();  // 先检查必填项
gopt.check_groups();     // 再检查组约束

最佳实践:总是在 read_options() 之后立即调用校验函数,尽早发现配置错误。


七、高级技巧

7.1 结合 str_utils 解析复杂值

getoptionstr_utils.hpp 中的工具函数配合,可以解析更复杂的值格式:

// 配置文件:range = 100/1000/200/2000/full_range
std::string range_str = gopt.get_value<std::string>("range");

// 用 parse_string_to_value 解析多个变量
double xmin, xmax, ymin, ymax;
std::string cover_str;
utilsxx::parse_string_to_value(range_str, '/', true, 
    xmin, xmax, ymin, ymax, cover_str);

// 结果:xmin=100, xmax=1000, ymin=200, ymax=2000, cover_str="full_range"
std::cout << xmin << " " << xmax << " " << ymin << " " << ymax 
          << " " << cover_str << std::endl;

7.2 显示所有参数

调试时查看所有已读取的参数:

gopt.show_options();           // 只显示已设置的参数
gopt.show_options(std::clog, true);  // 显示所有参数(包括未设置的)

输出示例:

range:  100/1000/200/2000/full_range
interval:   10/10
weight: 120.12/232/114.2
model-file: test.txt
model-tag:  model1,model2,model3
out-model:  example.msh
======================

7.3 自定义分隔符

根据配置文件的格式灵活调整:

// YAML 风格(冒号分隔)
gopt.read_options("config.yaml", ":");

// INI 风格(等号分隔,分号注释)
gopt.read_options("config.ini", "=", ",", ';');

// 空格分隔(极简格式)
gopt.read_options("config.txt", " ");

7.4 多配置文件分层加载

实现类似"默认配置 + 用户配置"的覆盖机制:

// 先加载默认配置
gopt.read_options("default_config.txt");

// 再加载用户配置,同名参数会覆盖
gopt.read_options("user_config.txt");

因为 read_options() 对已存在的 key 会追加值(用连接符),要实现覆盖需要配合 clear()

// 方案:分开管理
gopt.read_options("default_config.txt");
auto defaults = gopt;  // 保存默认值

gopt.clear();
gopt.read_options("user_config.txt");
// 然后手动合并...

7.5 与 CLI11 结合使用

utilsxx 同时集成了 CLI11 库用于命令行参数解析。两者可以互补:

// 1. 用 CLI11 解析命令行参数
CLI::App app("My Program");
std::string config_file;
app.add_option("-c,--config", config_file, "Config file");
CLI11_PARSE(app, argc, argv);

// 2. 用 getoption 解析配置文件
utilsxx::getoption gopt;
gopt.add_options({...}, {...});
gopt.read_options(config_file);
gopt.check_mandatory();

八、完整示例

以下是一个科学计算程序的完整配置解析示例:

#include "get_option.hpp"
#include "vector.hpp"
#include <iostream>
#include <string>

int main() try {
    utilsxx::getoption gopt;

    // 1. 声明所有参数
    gopt.add_options({
        "range",        // 计算范围 (必填)
        "interval",     // 采样间隔 (必填)
        "weight",       // 权重参数 (必填)
        "model-file",   // 模型文件 (必填)
        "model-tag",    // 模型标签 (必填,可多值)
        "out-model",    // 输出模型
        "save-model"    // 保存模型
    }, {
        true, true, true, true, true, false, false
    });

    // 2. 设置互斥组:out-model 和 save-model 至少选一个
    gopt.set_group(1, {"out-model", "save-model"});

    // 3. 读取配置(可从文件或内存向量)
    gopt.read_options("data/option_sample.txt");

    // 4. 校验
    gopt.check_mandatory();
    gopt.check_group(1);

    // 5. 显示配置
    gopt.show_options();

    // 6. 获取并使用参数
    std::string model_file = gopt.get_value<std::string>("model-file");
    std::cout << "Model file: " << model_file << std::endl;

    // 获取多值权重
    std::vector<double> weight = gopt.get_values<double>("weight", '|', '/');
    std::cout << "Weights (" << weight.size() << "): ";
    for (auto w : weight) std::cout << w << " ";
    std::cout << std::endl;

    // 获取多值标签
    std::vector<std::string> tags = gopt.get_values<std::string>("model-tag", '|', ',');
    std::cout << "Tags: " << tags << std::endl;

    // 解析范围参数
    double xmin, xmax, ymin, ymax;
    std::string cover_str;
    utilsxx::parse_string_to_value(
        gopt.get_value<std::string>("range|Range"), 
        '/', true, xmin, xmax, ymin, ymax, cover_str);
    std::cout << "Range: [" << xmin << ", " << xmax << "] x ["
              << ymin << ", " << ymax << "], mode: " << cover_str << std::endl;

    return 0;
}
catch(utilsxx::error_handler &e) {
    e.show();
    return 1;
}

对应配置文件 option_sample.txt

# this is an annotation
range = 100/1000/200/2000/full_range
interval = 10/10
weight = 120.12/232/114.2
model-file = test.txt
# you can insert empty lines too

model-tag = model1
model-tag = model2
model-tag = model3

out-model = example.msh

九、API 速查表

参数声明

方法说明
add_option(name, mandatory, group)添加单个参数
add_options(names, mandatories)批量添加参数
set_group(gp, names)将参数归入互斥组

配置读取

方法说明
read_options(filename, sep, conn, annotate)从文件读取
read_options(str_vec, sep, conn, annotate)从字符串向量读取

校验

方法说明
check_mandatory()检查必填项(组内参数除外)
check_group(gp)检查组内至少一个参数已设置
check_groups()检查所有组

查询

方法说明
has_value(key, delimiter)检查参数是否已设置
get_value<T>(key, delimiter, sep)获取单个值(自动类型转换)
get_values<T>(key, delimiter, sep)获取多值向量
show_options(out, show_all)显示所有参数
clear()清空所有参数

辅助函数

函数说明
parse_key_value(str, sep, key, value)解析 key-value 字符串

十、设计哲学与最佳实践

10.1 何时使用 getoption?

场景推荐方案
程序需要大量配置参数getoption
参数有复杂的校验规则(必填、互斥)getoption
配置文件需要注释和空行getoption
纯命令行工具,参数少CLI11
需要子命令支持CLI11
两者结合CLI11 解析命令行 + getoption 解析配置文件

10.2 命名建议

  • 使用 kebab-case(短横线连接):model-filemax-iterations
  • 同时注册 camelCase 别名:model-file|modelFile
  • 避免使用下划线开头(可能与内部变量冲突)

10.3 错误处理

getoption 在以下情况会抛出 error_handler 异常:

  • 必填参数未设置(check_mandatory()
  • 组内参数全部缺失(check_group()
  • 读取了不存在的 key(get_value()
  • 初始化列表长度不匹配(add_options()

建议始终用 try-catch 包裹:

try {
    // ... 配置解析代码 ...
}
catch(utilsxx::error_handler &e) {
    e.show();  // 打印友好的错误信息
    return 1;
}

结语

getoption 以声明式的设计简化了 C++ 程序的配置管理。你只需声明"程序需要什么参数",剩下的解析、校验、类型转换都由它自动完成。配合别名支持、分组校验和多值解析,它能优雅地处理从简单脚本到复杂科学计算程序的各种配置需求。


项目地址utilsxx