源码解读——getopt
作为程序员的你是否有过疑问:为什么命令行工具用法都差不多?事实上,这是因为早期基于
C/C++ 开发的命令行工具都使用了 getopt
工具来进行选项和参数的解析。
getopt
定义了命令行的两种选项:长选项、短选项,其分别以
-
和 --
作为前缀,从而使得命令行工具的使用方式基本都差不多。
为了便于认知,后期使用其他编程语言(如:ruby、python)开发的命令行工具,也都延续了这种选项和参数的风格。
本文,我们来通过阅读源码,了解一下 getopt
。相关源码详见
传送门。
基本功能
getopt
主要用于
解析命令行中的选项和参数,以便于检查实际的选项和参数是否符合预期。
getopt
的参数主要分为两部分:
- 内置的选项格式,包括:长选项或短选项等
- 待解析的所有参数
getopt
内置提供了两个选项
-o
/--options
和
-l
/--longoptions
,分别用于定义待解析参数的所支持的短选项和长选项的格式。基于这两项格式,getopt
才能判断待解析的参数是否符合预期。
选项类型
getopt
定义了三种类型的命令行选项,分别是:
- 标志选项:该选项仅作为一个标志位,其后面不带参数。
- 带参选项:该选项用于标识一个特定的参数,其后面必须带参数。
- 可选选项:该选项是一个可选选项,其后面可以选择不带参数或带参数(参数和选项之间没有空格)。
如下所示,为三种命令行选项的示例。 1
2
3
4
5
6
7# 标志位选项
$ ls -a
# 可选选项 & 带参选项
# -D 是可选选项,TEST 是参数。对于可选选项,选项和参数之间没有空格。
# -o 是带参选项,getopt 是参数
$ gcc -DTEST getopt.c -o getopt
为了实现三种选项的格式,getopt
设计了一套
DSL,分别用于描述这三种选项类型。下面,下面来看一个例子。
1 | # 短选项格式:--options hxab:c:: |
对于短选项,规定只能用一个字符来表示。
- 标志选项 末尾不带冒号,上述定义了三个标志短选项:
-h
,-x
,-a
。 - 带参选项
末尾带一个冒号,上述定义了一个带参短选项:
-b
。 - 可选选项
末尾带两个冒号。上述定义了一个可选短选项:
-c
。
对于长选项,规定可以用多个字符来表示,使用逗号或空格进行分隔。
- 标志选项 末尾不带冒号,上述定义了三个标志长选项:
--help
,--debug
,--a-long
。 - 带参选项
末尾带一个冒号,上述定义了一个带参长选项:
--b-long
。 - 可选选项
末尾带两个冒号。上述定义了一个可选长选项:
--c-long
。
注意,上述例子中 --
右边的部分就是待解析参数,即
--a-long -carg0 --b-long arg1
。
实现原理
数据结构
getopt
内部定义了两个关键的数据结构:struct option
和
struct getopt_control
。
struct option
用于描述一个长选项,包括:名称,是否带参数,短选项标志等。其定义如下所示:
1
2
3
4
5
6struct option {
const char *name; // 长选项的命名
int has_arg; // 选项是否带参数
int *flag; // 如果不为 NULL,当发现选项时,将 *flag 设置为 val
int val; // 如果 flag 不为 NULL,此为要设置 *flag 的值;否则,返回该值
};
struct getopt_control
是一个顶层数据结构,主要用于描述外部选项的格式。 1
2
3
4
5
6
7
8
9
10
11
12
13struct getopt_control {
shell_t shell; /* the shell we generate output for */
char *optstr; /* getopt(3) optstring */
char *name;
struct option *long_options; /* long options */
int long_options_length; /* length of options array */
int long_options_nr; /* number of used elements in array */
unsigned int
compatible:1, /* compatibility mode for 'difficult' programs */
quiet_errors:1, /* print errors */
quiet_output:1, /* print output */
quote:1; /* quote output */
};
核心流程
getopt
的核心流程分为三部分:
- 内置选项/参数解析:主要针对
getopt
自身所支持的选项进行解析。 - 外部选项格式注册:主要基于
内置选项/参数解析
的解析结果,比如:基于-o/--options
和-l/--longoptions
选项的 DSL 参数,注册外部选项格式。 - 外部选项/参数校验:基于外部选项格式,对剩余参数进行解析校验,判断外部选项是否符合预期。
如下所示,为 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86int main(int argc, char *argv[])
{
struct getopt_control ctl = {
.shell = BASH,
.quote = 1};
int opt;
// 内置的长选项和短选项
static const char *shortopts = "+ao:l:n:qQs:TuhV";
static const struct option longopts[] = {
{"options", required_argument, NULL, 'o'},
{"longoptions", required_argument, NULL, 'l'},
{"quiet", no_argument, NULL, 'q'},
{"quiet-output", no_argument, NULL, 'Q'},
{"shell", required_argument, NULL, 's'},
{"test", no_argument, NULL, 'T'},
{"unquoted", no_argument, NULL, 'u'},
{"help", no_argument, NULL, 'h'},
{"alternative", no_argument, NULL, 'a'},
{"name", required_argument, NULL, 'n'},
{"version", no_argument, NULL, 'V'},
{NULL, 0, NULL, 0}};
// 本地化及其他设置
// ...
// 初始化 ctl 的部分字段
add_longopt(&ctl, NULL, 0); /* init */
// 设置函数指针
getopt_long_fp = getopt_long;
// ...
// 内置选项/参数解析
while ((opt =
getopt_long(argc, argv, shortopts, longopts, NULL)) != EOF)
switch (opt)
{
case 'a':
getopt_long_fp = getopt_long_only;
break;
case 'o':
// 设置外部短选项格式
add_short_options(&ctl, optarg);
break;
case 'l':
// 设置外部长选项格式
add_long_options(&ctl, optarg);
break;
case 'n':
free(ctl.name);
// 设置外部程序的命名
ctl.name = xstrdup(optarg);
break;
case 'q':
ctl.quiet_errors = 1;
break;
case 'Q':
ctl.quiet_output = 1;
break;
case 's':
ctl.shell = shell_type(optarg);
break;
case 'T':
free(ctl.long_options);
return TEST_EXIT_CODE;
case 'u':
ctl.quote = 0;
break;
case 'V':
print_version(EXIT_SUCCESS);
case '?':
case ':':
parse_error(NULL);
case 'h':
usage();
default:
parse_error(_("internal error, contact the author."));
}
// ...
// 外部选项/参数校验
return generate_output(&ctl, argv + optind - 1, argc - optind + 1);
}
首先,进行内置选项/参数解析,可以看到 getopt
自身长选项和短选项格式如下所示。由于长选项和短选项直接存在映射关系,所以可以看到长选项初始化时的
val
域正好对应一个短选项字符。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// getopt 自身短选项格式
static const char *shortopts = "+ao:l:n:qQs:TuhV";
// getopt 自身长选项格式
static const struct option longopts[] = {
{"options", required_argument, NULL, 'o'},
{"longoptions", required_argument, NULL, 'l'},
{"quiet", no_argument, NULL, 'q'},
{"quiet-output", no_argument, NULL, 'Q'},
{"shell", required_argument, NULL, 's'},
{"test", no_argument, NULL, 'T'},
{"unquoted", no_argument, NULL, 'u'},
{"help", no_argument, NULL, 'h'},
{"alternative", no_argument, NULL, 'a'},
{"name", required_argument, NULL, 'n'},
{"version", no_argument, NULL, 'V'},
{NULL, 0, NULL, 0}};
内置选项/参数解析的核心是通过一个 while
循环,依次解析每一个选项和参数。这里通过调用 getopt_long
方法来进行解析。
getopt_long
会返回一个 ASCII 码值,通过
switch case
选择不同的处理方法。对于 l
则注册外部长选项格式,存储于 ctl.long_options
数组中;对于
o
则注册外部短选项格式,存储于 ctl.optstr
字符串中。
最后,通过 ctl
中已注册的外部选项格式,对剩余的参数进行校验,并打印最终结果。
选项/参数解析
选项/参数解析的核心方法是
getopt_long
,它是一个带副作用的函数,其内部会读写多个全局变量,包括:optind
、optarg
、optpos
等。每次调用,执行结果都不一样,从而达到依次遍历 ARGV 的目的。
getopt_long
包含两部分逻辑:长选项及其参数的识别,短选项及其参数的识别。
如果 ARGV 的一个元素以 --
作为前缀,那么它是一个长选项。如果 ARGV 的一个元素以 -
作为前缀(且不以 --
作为前缀),那么它是一个短选项。
对于带参选项的参数解析,getopt
支持两种格式,如下所示。
- 选项和参数之间使用 空格 ' ' 进行分隔
- 选项和参数之间使用 等号 '=' 进行分隔
1 | # 带参长选项,空格分隔选项和参数 |
对于可选选项的参数解析,getopt
略有不同,如下所示。
- 对于短选项,选项和参数之间不包含任何其他字符
- 对于长选项,选项和参数之间只包含 等号 '='。
1 | # 可选短选项,选项和参数之间不包含任何其他字符 |
值得注意的是,很多命令行工具都使用 getopt_long
方法来进行选项/参数解析,比如 util-linux
项目中各种常见的命令工具——传送门。
选项格式注册
选项格式注册的核心方法有两个,add_long_options
和
add_short_options
,分别对应长选项和短选项预。
add_long_options
的定义如下所示,其本质就是对
ctl
的
long_options
、long_options_length
等字段进行初始化。 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# 注册长选项
static void add_longopt(struct getopt_control *ctl, const char *name, int has_arg)
{
static int flag;
int nr = ctl->long_options_nr;
// 对 ctl->long_options_length 进行修改,并为 ctl->long_options 增加内存
if (ctl->long_options_nr == ctl->long_options_length)
{
ctl->long_options_length += REALLOC_INCREMENT;
ctl->long_options = xrealloc(ctl->long_options,
sizeof(struct option) *
ctl->long_options_length);
}
// 如果选项有名称,则进行存储
if (name)
{
/* Not for init! */
ctl->long_options[nr].has_arg = has_arg;
ctl->long_options[nr].flag = &flag;
ctl->long_options[nr].val = ctl->long_options_nr;
ctl->long_options[nr].name = xstrdup(name);
}
// 否则,置空
else
{
/* lets use add_longopt(ct, NULL, 0) to terminate the array */
ctl->long_options[nr].name = NULL;
ctl->long_options[nr].has_arg = 0;
ctl->long_options[nr].flag = NULL;
ctl->long_options[nr].val = 0;
}
}
add_short_options
的定义如下所示,其本质就是对
ctl
的 optstr
字段进行初始化。
1
2
3
4
5
6
7
8
9
10
11
12# 注册短选项
static void add_short_options(struct getopt_control *ctl, char *options)
{
// 将短选项存入 ctl
free(ctl->optstr);
if (*options != '+' && getenv("POSIXLY_CORRECT"))
ctl->optstr = strconcat("+", options);
else
ctl->optstr = xstrdup(options);
if (!ctl->optstr)
err_oom();
}
选项/参数校验
外部选项/参数校验的核心是调用了 generate_output
方法。该方法的实现如下所示,其本质上还是基于选项/参数解析方法
getopt_long
对参数进行解析,判断是否符合预期。和内置选项/参数解析的区别在于其校验的标准存储在了
ctl
中。
1 | static int generate_output(struct getopt_control *ctl, char *argv[], int argc) |
总结
getopt
定义了一套命令行选项和参数的使用规范,这套规范一直被沿用至今。getopt
的工作原理主要包含三个步骤:
- 内置选项/参数解析
- 外部选项格式注册
- 外部选项/参数校验
其中 内置选项/参数解析 的核心实现方法
getop_long
被很多命令行工具所引用,从而使得各种命令行工具的选项和参数的使用规范基本一致,也降低了使用者的学习成本。
关于 getopt
在实际中的用途,如果希望开发基于 C/C++
的命令行工具,可以像其他 Linux 内置的工具一样,使用 getopt
提供的 API
来进行选项校验;如果希望开发基于其他语言的命令行工具,可以调用
getopt
命令行工具来进行选项/参数校验。通过
getopt
,我们可以极大地减少选项/参数的定义与校验相关的开发工作量。