命令行的参数解析是一个相当常见的需求, C 标准库提供了 getopt 等函数解析参数, 但是使用起来还是太麻烦了...
一个简单示例如下
#include <clib/clib.h>
int main(int argc, const char **argv) {
int integer;
char *str = NULL, *dest;
int src;
int *other_numbers;
argparse_option options[] = {
ARG_BOOLEAN(NULL, "-h", "--help", "show help information", NULL, "help"),
ARG_BOOLEAN(NULL, "-v", "--version", "show version", NULL, "version"),
ARG_INT(&integer, "-i", "--input", "input file", " <NUMBER>", "input"),
ARG_STR(&str, "-s", "--string", NULL, " <STRING>", NULL),
ARG_STR_GROUP(&dest, NULL, NULL, "destination", NULL, "dest"),
ARG_INT_GROUP(&src, NULL, NULL, "source", NULL, "src"),
ARG_INTS_GROUP(&other_numbers, NULL, NULL, "catch the other number ...", NULL, "other-number"),
ARG_END()};
argparse parser;
argparse_init(&parser, options, 0);
argparse_describe(&parser,
"main",
"\nA brief description of what the program does and how it works.",
"\nAdditional description of the program after the description of the arguments.");
argparse_parse(&parser, argc, argv);
if (arg_ismatch(&parser, "help")) {
argparse_info(&parser);
}
if (arg_ismatch(&parser, "version")) {
printf("v0.0.1\n");
}
if (arg_ismatch(&parser, "input")) {
printf("i = %d\n", integer);
}
if (str) {
printf("s = %s\n", str);
}
if (arg_ismatch(&parser, "dest")) {
printf("dest = %s\n", dest);
}
if (arg_ismatch(&parser, "src")) {
printf("src = %d\n", src);
}
int n = arg_ismatch(&parser, "other-number");
for (int i = 0; i < n; i++) {
printf("other number[%d] = %d\n", i, other_numbers[i]);
}
free_argparse(&parser);
return 0;
}
可以在 examples/argparse.c 中找到该代码
$ ./argparse -v
v0.0.1
$ ./argparse -i
[Args Parse Error]: option [--input] needs one argument
$ ./argparse -i 100
i = 100
$ ./argparse -i 100 -s 200
i = 100
s = 200
$ ./argparse -i 100 -s 200 300
i = 100
s = 200
dest = 300
$ ./argparse -i 100 -s 200 300 400
i = 100
s = 200
dest = 300
src = 400
$ ./argparse -i 100 -s 200 300 "400"
i = 100
s = 200
dest = 300
src = 400
$ ./argparse -i 100 -s 200 "300" "400"
i = 100
s = 200
dest = 300
src = 400
$ ./argparse -i 100 -s 200 "300" "400" "500"
i = 100
s = 200
dest = 300
src = 400
other number[0] = 500
$ ./argparse -i 100 -s 200 "300" "400" abc
[Args Parse Error]: argument assign to be int but get [abc]
以上面的示例为例, 我们需要先定义变量然后构建一个 options 并与变量绑定
当然方便起见也可以直接全局变量, 或者与自定义参数结构体内部的字段绑定
int integer;
char *s, *dest;
int src;
int *other_numbers;
argparse_option options[] = {
ARG_BOOLEAN(NULL, "-h", "--help", "show help information", NULL, "help"),
ARG_BOOLEAN(NULL, "-v", "--version", "show version", NULL, "version"),
ARG_INT(&integer, "-i", "--input", "input file", " <NUMBER>", "input"),
ARG_STR(&str, "-s", "--string", NULL, " <STRING>", "string"),
ARG_STR_GROUP(&dest, NULL, NULL, "destination", NULL, "dest"),
ARG_INT_GROUP(&src, NULL, NULL, "source", NULL, "src"),
ARG_INTS_GROUP(&other_numbers, NULL, NULL, "catch the other number ...", NULL, "other-number"),
ARG_END()
};
以 ARG*
开头的是宏, 第一个位置用于参数绑定
一共有 8 种宏
-h
, 后面不需要跟其他参数信息-i 100
-s "hello world"
-s nihao
./main 100
./main 100 200 300
./main hi
./main hello world
组即不需要NOTE
-i
--input
长短参数前缀即可指定接收的参数以上面的例子为例, 如果命令行传参为:
./argparse abc 100
, 其中 "abc" 会被 dest 组接收, 100 会被 src 组接收, 需要注意的是组是有前后顺序的
上述参数宏分别对应不同的使用场景, 您应该合理的使用和传参, 第一个位置需要绑定的参数类型与宏对应
int
对应 ARG_INT 和 ARG_INT_GROUPchar*
对应 ARG_STR 和 ARG_STR_GROUPint*
对应 ARG_INTS_GROUPchar **
对应 ARG_STRS_GROUP上述的宏有6个参数位置, 分别是 (bind, short_name, long_name, help_info, append_info, name)
如果不需要则传 NULL 即可
-h
-v
-s
--h
不合法, --info
--space
--config
合法-O<number>
, 则添加 "<number>"
即可其中需要注意如下几点
a-z_-
的组合, 如果您希望使用其他字符你可手动修改其中 check_valid_character
函数之后您只需要构建对象, 初始化并解析即可
argparse parser;
argparse_init(&parser, options, 0);
argparse_describe(&parser, "main", "\ndescription", "\naaa.");
argparse_parse(&parser, argc, argv);
其中有几点需要说明:
argparse_describe
的2 3 4位置的参数分别为 解析器的名字, 简要描述, 结尾描述
argparse_init
第三个参数为 flag 标记位, 用于控制解析时的选项, 默认传 0 即可, 具体用法会在下文 解析扩展选项 中介绍
上例中的每一个选项中都使用了 NAME, 但其实这并不是一个必选项, 除了使用 arg_ismatch 来判断是否接收到了参数, 也可以直接判断绑定的参数的值.TIP
例如直接判断 str 是否为 NULL 来判断是否接收到了参数
arg_ismatch
函数用于判断是否接收到了参数, 并返回匹配的个数. 第二个参数是您定义的名字或长参数的名字
int arg_ismatch(argparse *parser, char *name);
int arg_match_pos(argparse *parser, char *name);
如果您使用的是若干参数的组, 您可以通过接收其返回值获取,如下所示
if (arg_ismatch(&parser, "help")) {
argparse_info(&parser);
}
if (arg_ismatch(&parser, "dest")) {
printf("dest = %s\n", dest);
}
if (arg_ismatch(&parser, "src")) {
printf("src = %d\n", src);
}
int n = arg_ismatch(&parser, "other-number");
for (int i = 0; i < n; i++) {
printf("other number[%d] = %d\n", i, other_numbers[i]);
}
free_argparse(&parser);
arg_match_pos
用于判断接收到的参数的匹配位置
./main 100 -i 200
| | |
1 2 3
./main -abc -d 100
||| | |
123 4 5
函数 argparse_info
可以用于帮助信息的输出, 建议您在 options 中添加 -h 选项并将其绑定到 argparse_info 函数, 用于辅助帮助信息的输出, 当然您也可以编写您自定义的 help 信息文档. 默认的帮助信息会自动为您实现对齐和说明
$ ./argparse --help
Usage: main [dest] [src] [other-number]... [-h --help][-v --version]
[-i <NUMBER> --input <NUMBER>]
[-s <STRING> --string <STRING>]
Description:
A brief description of what the program does and how it works.
-h --help show help information
-v --version show version
-i <NUMBER> --input <NUMBER> input file
-s <STRING> --string <STRING>
Additional description of the program after the description of the arguments.
参数解析时使用了动态内存分配, 所以最后请注意使用 free_argparse
释放内存
接下来您利用这些参数去处理您程序真正想要完成的事情了
如果您希望扩展解析时选项, 您可修改 flag 标记位, 使用 |
运算将他们组合传入 flag
-O1 -Iinclude/
-i=123
curl -Ls xxx
, 此选项仅支持 BOOLEAN 类型的粘连传递, 请不要加入其他类型./main -- cmd 100 200
下面是对这几个参数的详细说明
ARGPARSE_IGNORE_UNKNOWN
: 正常来说如果传入未定义过的参数会报如下错误
$ ./argparse -w
[Args Parse Error]: no match options for [-w]
在 flag 中加入此选项后不会报错, 只会单纯的忽略未知参数
argparse_init(&parser, options, ARGPARSE_IGNORE_UNKNOWN);
ARGPARSE_ENABLE_STICK
: 允许参数与值粘连
正常情况下会被认为是粘连参数
$ ./argparse -i100
[Args Parse Error]: no match options for [-i100]
需要修改 flag 并重新编译
argparse_init(&parser, options, ARGPARSE_ENABLE_STICK);
$ ./argparse -i100 -sabcd
i = 100
s = abcd
ARGPARSE_ENABLE_EQUAL
: 允许等号赋值
argparse_init(&parser, options, ARGPARSE_ENABLE_EQUAL);
$ ./argparse -s="hello world"
s = hello world
$ ./argparse -s=hello
s = hello
允许参数与值粘连与允许等号赋值参数赋值通常一起使用, 并优先考虑 =
赋值, 即对于 ./argparse -s="hello world"
来说, 结果是 s = hello world
而不是 s = =hello world
argparse_init(&parser, options, ARGPARSE_ENABLE_EQUAL|ARGPARSE_ENABLE_STICK);
ARGPARSE_ENABLE_ARG_STICK
: 支持bool类型的参数粘连, 注意与 ARGPARSE_ENABLE_STICK
(选项与参数粘连) 不同
$ ./argparse -hv
[Args Parse Error]: Boolean argument [-h] is sticky in [-hv], do you mean ARGPARSE_ENABLE_ARG_STICK?
argparse_init(&parser, options, ARGPARSE_ENABLE_ARG_STICK);
$ ./argparse -hv
Usage: main [dest] [src] [other-number]... [-h --help][-v --version]
[-i <NUMBER> --input <NUMBER>]
[-s <STRING> --string <STRING>]
Description:
A brief description of what the program does and how it works.
-h --help show help information
-v --version show version
-i <NUMBER> --input <NUMBER> input file
-s <STRING> --string <STRING>
Additional description of the program after the description of the arguments.
v0.0.1
一些程序可能会将传递给他的参数作为程序执行, 例如 bear -- make all clean test
, bear 内部会执行 --
后面的参数, 此时我们希望将 make all clean test
看作一个参数整体, 我们可以使用 ARGPARSE_ENABLE_CMD
来实现
使用 ARG_STR 匹配一个字符串, 长参数标记为 --
, flag 设置为 ARGPARSE_ENABLE_CMD
int main(int argc, const char **argv) {
char *cmd;
argparse_option options[] = {
ARG_STR(&cmd, NULL, "--", NULL, NULL, "cmd"),
ARG_END()};
argparse parser;
argparse_init(&parser, options, ARGPARSE_ENABLE_CMD);
}
此时 cmd
会匹配 --
后面的所有参数, 并将他们拼接成一个字符串
$ ./argparse_ls -- ./main -h -v 100 200
cmd: ./main -h -v 100 200
一个仿照 gcc 编译器参数解析的实现
您可复制如下代码进行编译, 也可以直接运行编译得到的 kcc 可执行文件
#include <clib/clib.h>
char *output = NULL;
char **include_path = NULL;
char **files = NULL;
char *optimize = NULL;
char **warning = NULL;
char **library_path = NULL;
char **library_name = NULL;
char *c_standard = NULL;
int debug = 0;
int main(int argc, const char **argv) {
argparse_option options[] = {
ARG_BOOLEAN(NULL, "-c", NULL, "Compile and assemble, but do not link.", NULL, "compile"),
ARG_STR(&output, "-o", NULL, "Place the output into <file>.", " <file>", NULL),
ARG_STRS(&include_path, "-I", NULL, "Add <dir> to the end of the main include path.", "<dir>", "include"),
ARG_STRS(
&library_path, "-L", NULL, "Add <dir> to the end of the main library path.", "<dir>", "library_path"),
ARG_STRS(&library_name, "-l", NULL, "Search <lib> in library path", "<lib>", "library_name"),
ARG_BOOLEAN(&debug, "-g", NULL, "Generate debug information in default format", NULL, "debug"),
ARG_STRS(&warning, "-W", NULL, "Warning information option", NULL, NULL),
ARG_STR(&optimize, "-O", "--optimize", "optimization level to <number>.", "<number>", NULL),
ARG_STR(&c_standard, NULL, "--std", "C Compile standard, supported: {c99}", "=<standard>", NULL),
ARG_BOOLEAN(NULL, "-h", "--help", "show help information", NULL, "help"),
ARG_BOOLEAN(NULL, "-v", "--version", "show version", NULL, "version"),
ARG_STRS_GROUP(&files, NULL, NULL, NULL, NULL, "src"),
ARG_END()};
argparse parser;
argparse_init(
&parser, options, ARGPARSE_ENABLE_ARG_STICK | ARGPARSE_ENABLE_STICK | ARGPARSE_ENABLE_EQUAL);
argparse_describe(&parser,
"kcc",
"Kamilu's C Compiler",
"document: <https://github.com/luzhixing12345/kcc>\nbug report: "
"<https://github.com/luzhixing12345/kcc/issues>");
argparse_parse(&parser, argc, argv);
if (arg_ismatch(&parser, "help")) {
argparse_info(&parser);
free_argparse(&parser);
return 0;
}
if (arg_ismatch(&parser, "version")) {
printf("%s\n", VERSION);
free_argparse(&parser);
return 0;
}
if (arg_ismatch(&parser, "compile")) {
printf("use -c\n");
}
if (debug) {
printf("use -g\n");
}
int n = arg_ismatch(&parser, "src");
for (int i = 0; i < n; i++) {
printf("file[%d] = [%s]\n", i, files[i]);
}
n = arg_ismatch(&parser, "include");
for (int i = 0; i < n; i++) {
printf("include path[%d] = [%s]\n", i, include_path[i]);
}
n = arg_ismatch(&parser, "library_path");
for (int i = 0; i < n; i++) {
printf("library path[%d] = [%s]\n", i, library_path[i]);
}
n = arg_ismatch(&parser, "library_name");
for (int i = 0; i < n; i++) {
printf("library name[%d] = [%s]\n", i, library_name[i]);
}
if (c_standard) {
printf("C standard = [%s]\n", c_standard);
}
if (optimize) {
printf("optimize = [%s]\n", optimize);
}
if (output) {
printf("output file = [%s]\n", output);
}
free_argparse(&parser);
return 0;
}
kcc 提供了与 gcc 类似的参数传递方式
$ ./examples/kcc a.c -o a
file[0] = [a.c]
output file = [a]
$ ./examples/kcc a.c -I include1 -Iinclude2 -I=include3 -o a
file[0] = [a.c]
include path[0] = [include1]
include path[1] = [include2]
include path[2] = [include3]
output file = [a]
$ ./examples/kcc a.c -I include1 -Iinclude2 -I=include3 -labc -l=ccc -o a
file[0] = [a.c]
include path[0] = [include1]
include path[1] = [include2]
include path[2] = [include3]
library name[0] = [abc]
library name[1] = [ccc]
output file = [a]
这对于一些 GNU 工具的实现来说很有效
$ ./argparse 123 abc
[Args Parse Error]: argument assign to be int but get [abc]
$ ./argparse 123 123a1
[Args Parse Error]: argument assign to be int but get [123a1]
$ ./argparse 123 -s
[Args Parse Error]: option [--string] needs one argument