0%

用`getopt`处理程序参数

Stage 1

命令行参数

用过命令行程序(特别是GNU开源程序)的人都知道程序执行时的首要任务就是解析命令行参数,然而我是在学Linux Shell的时后遇到了参数处理,命令行参数通常以字符串(项与项之间通常用空白字符隔开)的形式传入程序,而程序通过解析传入的字符串来确定选项和选项参数以及操作数,再执行相应的操作。

Stage 2

POSIX约定

POSIX表示可移植操作系统接口: Portable Operating System Interface,电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)最初开发 POSIX 标准,是为了提高 UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于 UNIX。许多其它的操作系统,例如 DEC OpenVMS 和 Microsoft Windows NT,都支持 POSIX 标准。

下面是POSIX标准中关于程序名、参数的约定:

  • 程序名不宜少于2个字符且不多于9个字符;
  • 程序名应只包含小写字母和阿拉伯数字;
  • 选项名应该是单字符活单数字,且以短横‘-‘为前綴;
  • 多个不需要选项参数的选项,可以合并。(譬如:foo -a -b -c ——>foo -abc)
  • 选项与其参数之间用空白符隔开;
  • 选项参数不可选。
  • 若选项参数有多值,要将其并为一个字串传进来。譬如:myprog -u “arnold,joe,jane”。这种情况下,需要自己解决这些参数的分离问题。
  • 选项应该在操作数出现之前出现。
  • 特殊参数‘—‘指明所有参数都结束了,其后任何参数都认为是操作数。
  • 选项如何排列没有什么关系,但对互相排斥的选项,如果一个选项的操作结果覆盖其他选项的操作结果时,最后一个选项起作用;如果选项重复,则顺序处理。
  • 允许操作数的顺序影响程序行为,但需要作文档说明。
  • 读写指定文件的程序应该将单个参数’-‘作为有意义的标准输入或输出来对待。

Stage 3

GNU长选项(本文只讨论GNU长选项,毕竟最实用不是?)

GNU鼓励程序员使用—help、—verbose等形式的长选项。这些选项不仅不与POSIX约定冲突,而且容易记忆,另外也提供了在所有GNU工具之间保持一致性的机会。GNU长选项有自己的约定:

  • 对于已经遵循POSIX约定的GNU程序,每个短选项都有一个对应的长选项。(同时使用长选项和短选项)
  • 额外针对GNU的长选项不需要对应的短选项,仅仅推荐要有。
  • 长选项可以缩写成保持惟一性的最短的字串。
  • 选项参数与长选项之间或通过空白字符或通过一个’=’来分隔。
  • 选项参数是可选的(只对短选项有效)。
  • 长选项允许以一个短横线为前缀。

Stage 4

C/C++

学C/C++学得较认真的同学对main函数的参数应该听说过了,可你知道它是用来干什么的吗?

1
2
3
4
int main(int argc, char *argv[]) {
/*code here*/
return 0;
}

或者

1
2
3
4
int main(int argc, char **argv) {
/*code here*/
return 0;
}

操作系统外壳(shell)将参数传入程序,而C运行库对命令行进行了处理后把argc和argv两个变量传给main()。argc 参数包含参数的计数值,而 argv 包含指向这些参数的指针数组。argv[0]是程序进程名。

ps:举个例子可能更能明白:myprog -u "arnold,joe,jane"其中(程序名为myprog,选项u,以及选项u对应的参数”arnold,joe,jane”):

最后到了main()函数里面:

  • argc=3
  • argv[0]=”myprog”
  • argv[1]=”-u”
  • argv[2]=”arnold,joe,jane”

ps:起初在没有getopt的时候程序员们采用手动方式来解析参数,上述的myprog可能会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
string uname;
int main(int argc, char const *argv[])
{
int i;
for (i = 0; i < argc; i++) {
if (argv[i][0] == '-' && argv[i][1] == 'u') {
uname = argv[i + 1];
i++;
}
}
if (uname.size())
cout << "Uname=" << uname << endl;
return 0;
}

上述方式手动搜索一遍参数然后获取选项以及选项的参数,可能你也发现了,若用户执行myprog -u(忘掉了选项的参数),数组argv将会发生访问越界出现Segmentation fault (core dumped)
所以手动处理参数在debug方面会增加程序员的工作量,有没有更优雅的解决办法呢?

答案就是使用GNU getopt库#include <getopt.h>

1
2
3
4
5
6
7
8
9
10
#include <getopt.h>

int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex);

struct option {
const char *name;
int has_arg;
int *flag;
int val;
};
  • optstring 选项字符串,是选项字母组成的字串,例如”ab:cd::e”对应命令行选项”-a -b -c -f -e”,字母后面有一冒号表示该选项需要带参数,两个冒号表示该选项的参数可选。

对于结构体中各元素解释如下:

  • const char *name 这是选项名,前面没有短横线。譬如”help”、”verbose”之类。
  • int has_arg 描述了选项是否有选项参数。如果有,是哪种类型的参数,此时,它的值一定是下表中的一个。
符号常量 数值 含义
no_argument 0 选项没有参数
required_argument 1 选项需要参数
optional_argument 2 选项参数可选
  • int *flag 如果这个指针为NULL,那么 getopt_long()返回该结构val字段中的数值。如果该指针不为NULL,getopt_long()会使得它所指向的变量中填入val字段中的数值,并且getopt_long()返回0。如果flag不是NULL,但未发现长选项,那么它所指向的变量的数值不变。
  • int val 这个值是发现了长选项时的返回值,或者flag不是NULL时载入*flag中的值。典型情况下,若flag不是NULL,那么val是个真/假值,譬如1或0;另一方面,如果flag是NULL,那么 val通常是字符常量,若长选项与短选项一致,那么该字符常量应该与optstring中出现的这个选项的参数相同。

看下面一个例子

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
/*
**Writed to learn getopt_long()
*/
#include <stdio.h>
#include <getopt.h>

void help();

int main(int argc, char *argv[]) {
int ag;
struct option longopts[] = { //定义存放长选项的结构体,*flag设为NULL,val存放短选项,这样getopt_long()返回长选项对应的短选项。
{"name", no_argument, NULL, 'n'},
{"gf_name", no_argument, NULL, 'g'},
{"like", required_argument, NULL, 'l'},
{"help", no_argument, NULL, 'h'}
};
if (argc == 1) {
help();
}
while ((ag = getopt_long(argc, argv, "ngl:", longopts, NULL)) != -1) { //用getopt_long()函数来处理参数
switch (ag) {
case 'n':
printf("My name is Edward.\n");
break;
case 'g':
printf("Her name is Monika.\n");
break;
case 'l':
printf("We like %s.\n", optarg);
break;
case 'h':
help();
break;
default: //未知参数
printf("Use --help for help.\n");
break;
}
}
return 0;
}

void help() { //用于--help选项显示帮助
printf("Usage:name [options] (argument)\n");
printf("Options:\n");
printf(" --help\t\tPrint this help.\n");
printf(" -n --name\t\tPrint my name.\n");
printf(" -g --gf_name\t\tPrint my girlfriend's name.\n");
printf(" -l --like\t\t(Followed by one argument) Print what we like.\n");
}

Stage 5

Linux Shell

只给清单:

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
#!/bin/bash
function help #用于显示帮助的函数
{
echo -e "Usage:name [options] (argument)"
echo -e "Options:\n"
echo -e " --help\t\tPrint this help."
echo -e " -n, --name\t\tPrint my name."
echo -e " -g, --gf_name\t\tPrint my girlfriend's name."
echo -e " -l, --like\t\t(Followed by one argument) Print what we like."
}

if [ $# -eq 0 ] #无参数输入直接退出
then
help
exit 1
fi

temp=`getopt -a -o ngl: --long name,gf_name,like:,help -n "$0" -- "[email protected]"` # man getopt 自行查看用法,大致原理就是对参数进行排序整理方便下面的处理

if [ $? != 0 ]; then
echo "Use --help for help."
exit 1
fi

eval set -- "${temp}" #重新设置参数为${temp}

while true
do
case "$1" in
-n | --name) echo "My name is Edward."; shift;;
-g | --gf_name) echo "Her name is Monika."; shift;;
-l | --like) echo "We like $2."; shift 2;;
--help) help; exit 0;;
--) shift; break;;
*) echo "Internal error! Use --help for help."; exit 1;; #默认情况
esac
done

参考

原来命令行参数处理可以这么写-getopt?

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