getopt、getopt_long和getopt_long_only的用法

getopt、getopt_long和getopt_long_only的用法

引子

前段时间博主曾写过一个Linux下的小项目叫myls,也就是实现了ls的基本功能。myls的命令行参数解析是由博主自己编码完成的,这可把博主给坑惨了!博主不久前查资料的时候看到了getoptgetopt_longgetopt_long_only这三个函数,发现这三个函数堪称命令行参数解析的“神器”!早知道有这样的工具,博主也不至于被命令行参数解析所困扰了。这么好的东西当然要分享给大家啦!所以博主写了这篇博客,也欢迎大佬指教!

用途

getoptgetopt_longgetopt_long_only这三个函数是干啥的嘞?当然是用来解析命令行参数的!我们通过向这三个函数传入字符串或者结构体数组来指定我们所关心的命令行参数,然后就可以得到解析的结果了。

getopt函数用于解析单字符参数,比如ls的参数-l-a等。

getopt_long函数和getopt_long_only函数既可以解析单字符命令行参数,又可以解析多字符命令行参数

需要说明的是这三个函数只能在Linux/UNIX使用哦!

getopt函数的用法

函数原型

1
2
3
#include <unistd.h>

int getopt(int argc, char *const argv[], const char *optstring);

getopt的三个参数

getopt的argcargv参数就是main函数argcargv参数。optstring参数指定了我们关心的命令行选项。例如,我们写的一个程序hello支持两个选项-a-b,那么optstring就可以设置为"ab"。getopt还支持带参数的选项。例如:

1
gcc helloworld.c -o helloworld

如果读者使用过GCC编译器,应该知道-o后面的helloworld是二进制文件的路径。这里的helloworld就是-o选项的参数。如果我们写的程序hello还有一个选项-c需要一个参数,那么optstring就可以是abc:。也就是在c后面加上一个冒号:。如果我们在c的后面加上两个冒号,那么参数对于-c选项来说就是可选的。

四个全局变量

getopt函数会使用到如下4个全局变量:

  1. extern char *optarg

    optarg用来保存选项的参数。例如,optstring的值为a:,我们在终端中键入命令

    1
    hello -a hahaha

    getopt就会将-a选项的参数hahaha存储到optarg。

  2. extern int optind

    getopt会从前往后解析命令行参数,optind就是getopt已解析到的选项在argv的下标。

  3. extern int opterr

    当opterr为非零值时,getopt会将错误信息输出到stderr;当opterr为0时则不会输出。

    opterr的默认值为1

  4. extern int optopt

    optopt存储解析出错的选项。比如我们只关心-a-b,但是传入了-c,则optopt会存储选项c。还有另一种情况就-b选项需要一个参数,而用户并没有为-b参数,那么getopt也会解析出错,并把optopt设置为b

getopt的返回值

当getopt解析到一个包含于optstring的选项时,会这个选项字符对应的ASCII码。例如解析到-a会返回97。如果解析出错,getopt会返回?ASCII码。如果argv已经解析完成,返回-1

举个栗子

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
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
int c;
while ((c = getopt(argc, argv, "ab:c::")) != -1)
{
switch (c)
{
case 'a':
printf("选项a\n");
break;
case 'b':
printf("选项b\n");
printf("选项b的参数是: %s\n", optarg);
break;
case 'c':
printf("选项c\n");
if (optarg != NULL)
{
printf("选项c的参数是: %s\n", optarg);
}
break;
case '?':
printf("未知的选项: '%c'\n", c);
break;
}
}

for (int i = optind; i < argc; i++)
{
printf("%s\n", argv[i]);
}

return 0;
}

在上面的getopt_test.c中,我们在optstring设置了三个选项:不带参数a、带参数的b、以及带可选参数的c。然后用switch-case语句处理getopt的返回值。根据前面的叙述,读者应该能看懂这段代码。但是最后的一段for循环代码是干啥的呢?其实,getopt在解析命令行参数时,会调整argv中命令行参数的顺序。它会将每个选项及其参数放在argv的前面,将非选项放在最后。当所有的选项解析完后,optind就是argv中第一个非选项的下标。所以,这个for循环的用途就是输出命令行参数中所有的非选项。

下面编译一下:

1
gcc getopt_test.c -o getopt_test -std=gnu17 -Wall -Werror

在上面的shell命令中,我们C标准设置为gnu17是因为getopt属于扩展的库函数,并非标准规定的函数。

然后运行几个样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
./getopt_test -a -bhhh -chh
# 选项a
# 选项b
# 选项b的参数是: hhh
# 选项c
# 选项c的参数是: hh

./getopt_test -a aa -bhhh bb -chh ff
# 选项a
# 选项b
# 选项b的参数是: hhh
# 选项c
# 选项c的参数是: hh
# aa
# bb
# ff

对于必须有参数的选项,例如-b,它的参数既可以与选项分开写,也可以写在一起。(-bhhh-b hhh都是正确的)。对于有可选参数的选项,则必须将选项和参数写在一起。

getopt_long的用法

用途

getopt函数用来解析单字符命令行参数getopt_long函数则同时支持单字符命令行参数多字符命令行参数。例如ps --help的help参数。

函数原型

1
2
#include <getopt.h>
int getopt_long(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);

函数参数

getopt_long的前三个参数与getopt的含义相同,此处不再赘述。我们来看后两个参数。

getopt_long的第四个参数longopts是一个结构体数组。struct option的定义如下:

1
2
3
4
5
6
7
struct option
{
const char *name;
int has_arg;
int *flag;
int val;
};

每个结构体对应了一个长选项。其中,name是长选项的名称,例如--help选项的name就是helphas_arg指明这个长选项是否有参数:0表示没有参数,1表示需要参数,2表示可选参数,在getopt.h分别定义了no_argumentrequired_argumentoptional_argument三个宏来表示has_arg的三种可能的值。

flagval的作用分两种情况:

  1. 当flag为NULL时,如果getopt_long解析到了该长选项,则返回val。

前文中我曾提到,getopt返回选项字符对应的ASCII码。如果我们将长选项也对应一个val来返回,就可以将短选项和长选项联系在一起。

  1. 当flag不为NULL时,如果getopt_long解析到了该长选项,则将val存入flag指向的地址。

让flag指向一个int,就可以通过检查flag来知道有没有对应的长选项的。此时getopt_long返回0

getopt_long的最后一个参数用于返回长选项在longopts结构体数组中的索引值,用于调试,一般置为NULL。

返回值

getopt_long的返回值与getopt类似,不同的是当某个长选项的flag为NULL时,会将对应的val返回。

注意

getopt_long的longopts是一个结构体数组。在C语言中,我们传递数组的常用方法是传入一个指向数组的指针以及这个数组的长度。而在getopt_long中,并没有传入getopt_long长度,那么getopt_long如何知道这个结构体的长度呢。答案是我们传入getopt_long的结构体数组的最后一个结构体的各字段必须都为0,这样getopt_long就知道这个结构体是这个数组的结尾了。这类似于C语言中的字符串,用\0表示字符串的终止。

举个例子

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 <getopt.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
int c = 0;
int flag = 0;
struct option options[] = {{"help", required_argument, NULL, 1000},
{"version", optional_argument, &flag, 2000},
{"hello", no_argument, NULL, 3000}};
while ((c = getopt_long(argc, argv, "ab:c::", options, NULL)) != -1)
{
switch (c)
{
case 'a':
printf("选项a\n");
break;
case 'b':
printf("选项b\n");
printf("选项b的参数为:%s\n", optarg);
break;
case 'c':
printf("选项c\n");
if (optarg != NULL)
{
printf("选项c的参数为:%s\n", optarg);
}
break;
case 0:
if (flag == 2000)
{
printf("--version选项\n");
if (optarg != NULL)
{
printf("--version选项的参数为:%s\n", optarg);
}
}
break;
case 1000:
printf("--help选项\n");
printf("--help选项的参数为:%s\n", optarg);
break;
case 3000:
printf("--hello选项\n");
break;
case '?':
printf("未知的选项:%s\n", optarg);
break;
}
}

for (int i = optind; i < argc; i++)
{
printf("%s\n", argv[i]);
}

return 0;
}

这段代码不是很复杂,博主就不详细解释了。但是需要注意的一点是,向必须要有参数的长选项传入参数的方法有两种:一种是将参数放在长选项的后面,例如--help hhh;另一种是使用等号=,例如,--help=hhh。而向可选参数的长选项传参数只有一种,也就是使用等号=

getopt_long_only的用法

get_long_onlygetopt_long用法相似,不同的是get_long_only解析的长选项以一个连字符-开头,而get_long解析的长选项以两个连字符--开头。

参考文献

  1. Linux下getopt()函数的简单使用

  2. getopt(3) — Linux manual page

  3. Linux编程里getopt_long_only函数用法详解


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!