3

源码解读——getopt

 2 years ago
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.
neoserver,ios ssh client

源码解读——getopt

发表于 2022-04-04

|

更新于 2022-04-04

| 分类于 GNU

作为程序员的你是否有过疑问:为什么命令行工具用法都差不多?事实上,这是因为早期基于 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 optionstruct 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,它是一个带副作用的函数,其内部会读写多个全局变量,包括:optindoptargoptpos 等。每次调用,执行结果都不一样,从而达到依次遍历 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_optionsadd_short_options,分别对应长选项和短选项预。

add_long_options 的定义如下所示,其本质就是对 ctllong_optionslong_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 的定义如下所示,其本质就是对 ctloptstr 字段进行初始化。

# 注册短选项
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 被很多命令行工具所引用,从而使得各种命令行工具的选项和参数的使用规范基本一致,也降低了使用者的学习成本。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK