基于 C++ 的命令行参数解析

基于 C++ 的命令行参数解析

1
test --port 50051 --path /
1
2
3
4
5
6
7
cmdline::parse(
argc, argv,
cmdline::option("--port", true,
[](std::string value) { global::port = std::stod(value); }),
cmdline::option("--path", true,
[](std::string value) { global::path = value; }),
)

这里只是做了封装,值的处理还需传入回调函数来处理。

可以处理 option value or option 这样的命令行参数,对于多个参数 option value0 value1 value2, 可以使用 option "value0, value1 value2" 然后在回调函数中处理。

C 库中也有这样的函数 getopt, getopt_long

思路

C/C++ 中怎么得到命令行参数

1
int main(int argc, char **argv)

argc 是参数个数,argv 是一个字符串数组, 参数被空格分割。

1
test --port 50051 --path /
1
2
argc : 5
argv : ["test", "--port", "50051", "--path", "/"]

argv 的第一个是运行的二进制文件的文件名, 后面是参数按空格分割。

从上面的例子可以发现,解析命令行参数只需要遍历参数数组,匹配对应的 option, 如果由 value 的话,对应的 valueoption 的下一个。

封装命令行参数选项

对一个选项,它有一个名字,也可能有一个 value,对于这个选项在代码中的作用每个选项都可以不同,不同项目的也不同,因此传入一个回调函数来处理这件事更方便。

1
2
3
4
5
6
7
8
9
10
struct option {
const std::string opt_name;
// 判断是否有 `value`
const bool flag;
std::function<void(std::string)> callback;
option(const std::string &&opt_name, bool flag, std::function<void(std::string) &&callback)
: opt_name(std::move(opt_name)),
flag(flag),
callback(std::move(callback)) {}
};

解析命令行参数

定义一个解析函数,首先需要 argc, argv 这两个参数,然后是 option, option 在各个项目里也可能是各不相同的,因此使用变长参数传入。

1
void parse(int argc, char** argv, option opts, ....);

在匹配 argvopts 时,因为 opt 的参数是不确定的,那就不知到要写多少个 if 来判断了。

如果能遍历可变参数的话就能解决这个问题了,可以传入一个数组指针 :

1
2
3
4
5
6
7
for (int i = 1; i < argc; ++i) {
for (int j = 0; j < len; ++j) {
if (argv[i] == opts[j]) {
...
}
}
}

在 C++11 后引入了 模版形参包 可以接受任意数量的形参,而且有多种方式遍历参数,函数定义可以改为:

1
2
template <typename ... Args>
void parse(int argc, char** argv, Args ... opts);

这里使用 初始化列表和逗号表达式 的方式来展开形参包 :

1
int arr[] = {(parse_opt(i, argc, argv, opts), 0) ...};

C++17 之后可以直接使用逗号表达式来解决 :

1
((parse_opt(i, argc, argv, opts)), ...);

把匹配的过程写到了 parse_opt 中。

完整代码

cmdline.h :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <format>
#include <functional>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <type_traits>

namespace cmdline {
struct option {
const std::string opt_name;
const bool flag; // true , has value
std::function<void(std::string)> callback;
template <typename T>
requires std::constructible_from<std::function<void(std::string)>, T>
constexpr option(const std::string &&opt_name, bool flag, T &&callback)
: opt_name(std::move(opt_name)), flag(flag),
callback(std::forward<T>(callback)){};
};

namespace details {
inline int parse_opt(int &index, int argc, char **argv, option &opt) {
if (argv[index] == opt.opt_name) {
if (not opt.flag) {
opt.callback("");
return 1;
}
if (index + 1 >= argc) {
std::cerr << std::format("missing value to option {}\n", opt.opt_name);
exit(-1);
}
opt.callback(argv[++index]);
return 1;
}
return 0;
}
} // namespace details

template <typename T>
concept Opt = std::is_same_v<T, option>;

template <Opt... Opts> void parse(int argc, char **argv, Opts &&...opts) {
for (int i = 1; i < argc; ++i) {
int size = 0;
int arr[] = {(++size, details::parse_opt(i, argc, argv, opts))...};
if (std::accumulate(arr, arr + size, 0) == 0) {
std::cerr << std::format("invalid option : {}\n", argv[i]);
exit(-1);
}
}
}
} // namespace cmdline