C 程序解析命令行参数一般都会使用 getopt/getoptlong 这两个 GNU 接口,简单的参数设计还好,稍微复杂的就会维护起来很麻烦,比如手写 usage、命令参数的分散性(参数的异常处理)等等。

argtable2 是一个底层基于 getopt 的命令行参数处理库,开源协议为 LGPL, 使用它开发复杂命令行接口简单且易于维护。

argtable2 在设计上,是面向参数本身,与该参数有关的信息都会汇聚在一起,成为一个 arg 对象。也就是说,该参数的特性,以及其 usage 都是在一起的,这对于开发和维护会特别方便(相对于基础的 getopt 而言)。

先来一个实际代码示例:

 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
52
53
54
55
56
57
58
#include <stdio.h>
#include <argtable2.h>

int main(int argc, char *argv[])
{
    int ret = 0;
    // 命令行参数处理
    struct arg_lit *arghelp         = arg_lit0("h", "help", "Show help and exit");
    struct arg_lit *argversion      = arg_lit0("v",  "version", "Show version");
    struct arg_str *argdev          = arg_str0("d",  "device", "<device>", "Input device, eg '/dev/ttyUSB0'");
    struct arg_int *argbaud         = arg_int0("b",  "baudrate", "<baudrate>", "Input device baudrate, eg '115200'");
    struct arg_file *argrecord      = arg_file0("r", "record", "<file>", "Enable record to file");
    struct arg_end *end             = arg_end(20);

    void *argtable[] = {arghelp, argversion, argdev, argbaud, argrecord, end};
    if (arg_nullcheck(argtable)) {
        fprintf(stderr, "OOM\n");
        ret = 1;
        goto CLEANUP;
    }
    // 预设值默认参数
    argdev->sval[0] = "ttyUSB0";
    argbaud->ival[0] = 115200;

    int nerr = arg_parse(argc, argv, argtable);

    if (arghelp->count > 0) {
        printf("Usage: %s", argv[0]);
        arg_print_syntaxv(stdout, argtable, "\n");
        printf("\nExample:\n");
        arg_print_glossary(stdout, argtable, " %-30s %s\n");

        ret = 0;
        goto CLEANUP;;
    }

    if (argversion->count > 0) {
        printf("Version: %s\n", "v1.0");
        ret = 0;
        goto CLEANUP;
    }

    if (nerr > 0) {
        arg_print_errors(stderr, end, argv[0]);
        printf("use '%s --help' for more info.\n", argv[0]);

        ret = 1;
        goto CLEANUP;
    }
    // 打印配置
    printf("device='%s' baudrate=%d\n", argdev->sval[0], argbaud->ival[0]);


CLEANUP:
    arg_freetable(argtable, sizeof(argtable) / sizeof(argtable[0]));

    return ret;
}

其中的核心设计就是 struct arg_xxx 系列的对象,用于存储参数的类型(literal/int/double/str/date 等)、短标识(比如 -v)、长标识(比如 --version)、参数表征类型(比如 <device>)、参数说明。因为 argtable 支持多次参数设置,所以还包含一个参数数量。

arg_lit0/arg_str0 这样的函数,用来初始化参数对象,其中 lit 代表无类型参数,比如 -v 这些,后面的数字代表参数的出现次数,0 类似正则中的 ?,即 0 或者 1次,而 1 代表必须 1 次,而 n 则代表可出现1次或多次。arg_end 是一个特殊对象,用来存储参数解析的异常情况,arg_end(X) 的参数代表可以存储最多多少个错误信息。

在解析时,则需要将各个参数对象放置到一个 argtable 中,然后利用 arg_nullcheckarg_parse 去初始化和解析参数。

对于 usage 打印,可以使用 arg_print_syntaxv 这样的接口,这个接口会打印精简的命令行帮助,而 arg_print_glossary 则可以打印表格样式的命令行帮助:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# gcc -o test 1.c -largtable2
# ./test -h
./test -h
Usage: ./test [-h|--help] [-v|--version] [-d|--device=<device>] [-b|--baudrate=<baudrate>] [-r|--record=<file>]

Example:
 -h, --help                     Show help and exit
 -v, --version                  Show version
 -d, --device=<device>          Input device, eg '/dev/ttyUSB0'
 -b, --baudrate=<baudrate>      Input device baudrate, eg '115200'
 -r, --record=<file>            Enable record to file

而对于参数的处理上,使用对应参数对象的 count 属性,来判断参数出现的次数,进而取值进行处理。需要注意的是,参数对象的值只在 arg_freetable 前生存有效,使用时需要注意生命周期管理。

不过对于程序而言,有时为了方便,可以不考虑调用 arg_freetable,毕竟程序结束时系统会进行回收,而参数处理,只有在程序运行时才执行1次而已,在程序运行期间保有这部分资源也还好。

如果参数对象需要设置默认参数值,则需要在 arg_nullcheckarg_parse 前对字段进行赋值。

argtable2 本身底层仍然是调用 getopt 接口实现的,不过这种基于参数对象的管理设计,对于维护性有很大的提升,有时候,手写复杂的 getopt 太琐碎了。而且,argtable2 还支持 date 这样类型的参数处理,以及 kb/mb/gb 这样的尾缀表达,开发起来友好不少。