参数解析

命令行的参数解析是一个相当常见的需求, 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;
}

20250127233250

可以在 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 种宏

[!NOTE] NOTE
组即不需要 -i --input 长短参数前缀即可指定接收的参数

以上面的例子为例, 如果命令行传参为: ./argparse abc 100, 其中 "abc" 会被 dest 组接收, 100 会被 src 组接收, 需要注意的是组是有前后顺序的

上述参数宏分别对应不同的使用场景, 您应该合理的使用和传参, 第一个位置需要绑定的参数类型与宏对应

参数信息

上述的宏有6个参数位置, 分别是 (bind, short_name, long_name, help_info, append_info, name)

如果不需要则传 NULL 即可

其中需要注意如下几点

之后您只需要构建对象, 初始化并解析即可

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 即可, 具体用法会在下文 解析扩展选项 中介绍

[!TIP] TIP
上例中的每一个选项中都使用了 NAME, 但其实这并不是一个必选项, 除了使用 arg_ismatch 来判断是否接收到了参数, 也可以直接判断绑定的参数的值.

例如直接判断 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

下面是对这几个参数的详细说明

忽略未知参数

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);

bool参数粘连

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 编译器参数解析的实现

20250130022533

您可复制如下代码进行编译, 也可以直接运行编译得到的 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 工具的实现来说很有效

匹配失败

传入的参数与int不匹配

$ ./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

参考

zood