源码解读——getopt
source link: http://chuquan.me/2022/04/04/getopt/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
源码解读——getopt
作为程序员的你是否有过疑问:为什么命令行工具用法都差不多?事实上,这是因为早期基于 C/C++ 开发的命令行工具都使用了 getopt
工具来进行选项和参数的解析。
getopt
定义了命令行的两种选项:长选项、短选项,其分别以 -
和 --
作为前缀,从而使得命令行工具的使用方式基本都差不多。
为了便于认知,后期使用其他编程语言(如:ruby、python)开发的命令行工具,也都延续了这种选项和参数的风格。
本文,我们来通过阅读源码,了解一下 getopt
。相关源码详见 传送门。
getopt
主要用于 解析命令行中的选项和参数,以便于检查实际的选项和参数是否符合预期。
getopt
的参数主要分为两部分:
- 内置的选项格式,包括:长选项或短选项等
- 待解析的所有参数
getopt
内置提供了两个选项 -o
/--options
和 -l
/--longoptions
,分别用于定义待解析参数的所支持的短选项和长选项的格式。基于这两项格式,getopt
才能判断待解析的参数是否符合预期。
getopt
定义了三种类型的命令行选项,分别是:
- 标志选项:该选项仅作为一个标志位,其后面不带参数。
- 带参选项:该选项用于标识一个特定的参数,其后面必须带参数。
- 可选选项:该选项是一个可选选项,其后面可以选择不带参数或带参数(参数和选项之间没有空格)。
如下所示,为三种命令行选项的示例。
# 标志位选项
$ ls -a
# 可选选项 & 带参选项
# -D 是可选选项,TEST 是参数。对于可选选项,选项和参数之间没有空格。
# -o 是带参选项,getopt 是参数
$ gcc -DTEST getopt.c -o getopt
为了实现三种选项的格式,getopt
设计了一套 DSL,分别用于描述这三种选项类型。下面,下面来看一个例子。
# 短选项格式:--options hxab:c::
# 长选项格式:--longoptions help,debug,a-long,b-long:,c-long::
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- --a-long -carg0 --b-long arg1
对于短选项,规定只能用一个字符来表示。
- 标志选项 末尾不带冒号,上述定义了三个标志短选项:
-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
用于描述一个长选项,包括:名称,是否带参数,短选项标志等。其定义如下所示:
struct option {
const char *name; // 长选项的命名
int has_arg; // 选项是否带参数
int *flag; // 如果不为 NULL,当发现选项时,将 *flag 设置为 val
int val; // 如果 flag 不为 NULL,此为要设置 *flag 的值;否则,返回该值
};
struct getopt_control
是一个顶层数据结构,主要用于描述外部选项的格式。
struct 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
的核心流程的代码实现。
int 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
域正好对应一个短选项字符。
// 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
选择不同的处理方法。对于 o
则注册外部短选项格式,存储于 ctl.long_options
数组中;对于 l
则注册外部长选项格式,存储于 ctl.optstr
字符串中。
最后,通过 ctl
中已注册的外部选项格式,对剩余的参数进行校验,并打印最终结果。
选项/参数解析
选项/参数解析的核心方法是 getopt_long
,它是一个带副作用的函数,其内部会读写多个全局变量,包括:optind
、optarg
、optpos
等。每次调用,执行结果都不一样,从而达到依次遍历 ARGV 的目的。
getopt_long
包含两部分逻辑:长选项及其参数的识别,短选项及其参数的识别。
如果 ARGV 的一个元素以 --
作为前缀,那么它是一个长选项。如果 ARGV 的一个元素以 -
作为前缀(且不以 --
作为前缀),那么它是一个短选项。
对于带参选项的参数解析,getopt
支持两种格式,如下所示。
- 选项和参数之间使用 空格 ‘ ‘ 进行分隔
- 选项和参数之间使用 等号 ‘=’ 进行分隔
# 带参长选项,空格分隔选项和参数
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- --b-long arg1
--b-long 'arg1' --
# 带参短选项,空格分隔选项和参数
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- -b arg1
--b-long 'arg1' --
# 带参长选项,等号分隔选项和参数
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- --b-long=arg1
--b-long 'arg1' --
# 带参短选项,等号分隔选项和参数
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- -b=arg1
--b-long 'arg1' --
对于可选选项的参数解析,getopt
略有不同,如下所示。
- 对于短选项,选项和参数之间不包含任何其他字符
- 对于长选项,选项和参数之间只包含 等号 ‘=’。
# 可选短选项,选项和参数之间不包含任何其他字符
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- -carg0
-c 'arg0' --
# 可选长选项,选项和参数之间只包含等号
$ getopt --options hxab:c:: --longoptions help,debug,a-long,b-long:,c-long:: -- --c-long=arg0
--c-long 'arg0' --
值得注意的是,很多命令行工具都使用 getopt_long
方法来进行选项/参数解析,比如 util-linux
项目中各种常见的命令工具——传送门。
选项格式注册
选项格式注册的核心方法有两个,add_long_options
和 add_short_options
,分别对应长选项和短选项预。
add_long_options
的定义如下所示,其本质就是对 ctl
的 long_options
、long_options_length
等字段进行初始化。
# 注册长选项
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
字段进行初始化。
# 注册短选项
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
中。
static int generate_output(struct getopt_control *ctl, char *argv[], int argc)
{
int exit_code = EXIT_SUCCESS; /* Assume everything will be OK */
int opt;
int longindex;
const char *charptr;
if (ctl->quiet_errors)
/* No error reporting from getopt(3) */
opterr = 0;
/* Reset getopt(3) */
optind = 0;
while ((opt = (getopt_long_fp(argc, argv, ctl->optstr, (const struct option *)ctl->long_options, &longindex))) != EOF)
{
if (opt == '?' || opt == ':')
exit_code = GETOPT_EXIT_CODE;
else if (!ctl->quiet_output)
{
switch (opt)
{
case LONG_OPT:
printf(" --%s", ctl->long_options[longindex].name);
if (ctl->long_options[longindex].has_arg)
print_normalized(ctl, optarg ? optarg : "");
break;
case NON_OPT:
print_normalized(ctl, optarg ? optarg : "");
break;
default:
printf(" -%c", opt);
charptr = strchr(ctl->optstr, opt);
if (charptr != NULL && *++charptr == ':')
print_normalized(ctl, optarg ? optarg : "");
}
}
}
if (!ctl->quiet_output)
{
printf(" --");
while (optind < argc)
print_normalized(ctl, argv[optind++]);
printf("\n");
}
for (longindex = 0; longindex < ctl->long_options_nr; longindex++)
free((char *)ctl->long_options[longindex].name);
free(ctl->long_options);
free(ctl->optstr);
free(ctl->name);
return exit_code;
}
getopt
定义了一套命令行选项和参数的使用规范,这套规范一直被沿用至今。getopt
的工作原理主要包含三个步骤:
- 内置选项/参数解析
- 外部选项格式注册
- 外部选项/参数校验
其中 内置选项/参数解析 的核心实现方法 getop_long
被很多命令行工具所引用,从而使得各种命令行工具的选项和参数的使用规范基本一致,也降低了使用者的学习成本。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK