文章目录
前言
getopt()
,很好理解,字面意思就是get option
。虽然挺好用,但是用法,刚看到真是让人头大,有点复杂。不过结合实际,也是能拿下的。
函数原型
#include <unistd.h>
/*要配套使用的变量*/
extern int optind, opterr, optopt;
extern char *optarg;
int getopt(int argc, char *const argv[], const char *optstring);
//See main text for description of return value
背景知识
- 命令行的格式,是连字符
-
跟着一个唯一的字符用来标识该选项(比如 -a ),以及一个针对该选项的可选参数。 - 带有一个参数的选项能够以可选的方式在参数和选项之间用空格分开。
- 多个选项可以归组后共用一个连字符,而组内最后一个选项可能带有一个参数。
意思就是下面三条是等价的:
grep -l -i -f patterns *.c
grep -lif patterns *.c
grep -lifpatterns *.c
(最后一条真的离谱……)
上面三条中,-l
、-i
没有参数,-f
有参数pattern
参数说明
-
getop()
的前两个参数argc
和argv
和通常从main()
的参数列表获取。 -
最后一个参数
opstring
指定了函数getopt()
命令行选项集合。这个选项由一组字符组成,SUSv3规定至少应该接受62个字符[a-zA-Z0-9]作为选项。
也就是最低要求:0,1,2,……9,a,b,c,d……z,A,B,C……Z 都可以写进去。
除了: 、 ? -
这几个有特殊意义的,大多数实现还会允许更多的字符。每个选项字符后面可以根一个冒号字符(:
),表示这个选项有一个参数。
解析过程
(坚持住,看完别晕哦~)
- 要通过连续调用
getopt()
来解析命令行,每调用一次都会返回下一个未处理选项的信息。如果找到了选项,那么就会返回该选项的字符;如果到达了选项列表结尾,就会返回-1。 - 如果选项带有参数,那么
getopt()
会将全局变量optarg
(就是上面和它配套使用的一个变量)指向这个参数, - 如果选项没有参数,
glibc
在内的大多数实现会将optarg
设置为NULL
。(但这不是SUSv3标准规定的) optind
(也是配套的变量)包含着参数列表argv
中未处理的下一个元素的索引,初次调用时optind
会被自动设置成1。(更复杂的归组后共用一个连字符,getopt()
内部会处理好的)
需要使用optind的情况
getopt()
返回了-1 ,表示没有可供解析的选项了,且optind
的值比argc
小,此时argv[optind]
表示命令行下一个非选项单词。- 处理多个命令行向量或者重新扫描相同的命令行时,必须手动将
opeind
重新设置为1。
getopt()返回-1,表示已到选项列表结尾的情况
- 由
argc
加上argv
所代表的列表已经到达结尾,也就是argv[optind]
为NULL
argv
中下一个未处理的元素只有一个单独的连字符组成,也就是argv[optind]
为-
argv
下一个未处理的元素是--
。此时getopt()
会读取这’--
, 然后做出调整,把optind
指向下一个单字(即使下一个元素是-a
这种很像一个选项。比如grep -- -k myfile
,会在myfile
中查找-k
),也就是说下一个元素会被"加强",被当成参数(就算它长成选项的样子),而不再被当成选项看待。
getopt()错误
出现错误的情况
- 当遇到某个没有在
opstring
中指定的选项时 - 当某个选项需要一个参数,但却没有提供参数时。
处理错误的规则
- 默认情况,
getopt()
会在标准错误打印一条恰当的错误信息,并且将字符?
作为函数的返回结果。 同时,optopt
会用来返回那个出错的选项字符(也就是未被指定,或者缺少参数的选项字符)。 - 全局变量
opterr
默认设置为1,如果设置为0,可以设置禁止显示getopt()
打印的错误消息。 - 把参数
optstring
中将第一个字符设置为:
,也可以禁止显示错误消息,但是,此时缺失参数的选项会通过函数返回:
来报告(这意味着,未识别的选项会返回?
,缺失参数会返回:
,可以对二者进行区分)
报错归纳表
报错规则 | 是否自动打印错误消息 | 未识别选项返回值 | 缺少参数返回值 |
---|---|---|---|
opterr 为1(默认情况)) | Y | ? | ? |
opterr 为0 | N | ? | ? |
optstring[0] 为: | N | ? | : |
总之就是
int getopt()
,全称get option
,返回选项字符 ,结束则返回-1(所以才用int
而不用char
)。
char* optstring
,全称option string
,指定的选项字符串。
char* optarg
,全称option argument
,指向getopt()
返回的选项的参数,没参数为NULL
(非标准规定)。
int optind
, 全称option index
,表示参数列表argv
的元素的索引。
int opterr
,全称option error
,指定是否开启自动显示错误信息,1开启,0关闭。
int optopt
,全称option option
,用来返回报错的选项字符。
实例解析
自己改怕出毛病,就用书上的例子吧,虽然不够简单。
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int printable(int ch)
{
if (isprint((unsigned char)ch))
return ch;
return '#';
}
static void
usageError(char *progName, char *msg, int opt)
{
if (msg != NULL && opt != 0)
fprintf(stderr, "%s (-%c)\n", msg, printable(opt));
fprintf(stderr, "Usage: %s [-p arg] [-x]\n", progName);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
int opt, xfnd;
char *pstr;
xfnd = 0;
pstr = NULL; /*解释一下,开头的':'表示不自动打印错误消息,看上面的表,
p后的':'表示p选项带有参数,x不带参数*/
for (; (opt = getopt(argc, argv, ":p:x")) != -1;)
{
printf("opt =%3d (%c); optind = %d", opt, printable(opt), optind);
if (opt == '?' || opt == ':')
printf("; optopt =%3d (%c)", optopt, printable(optopt));
printf("\n");
switch (opt)
{
case 'p':
pstr = optarg;
break;
case 'x':
xfnd++;
break;
case ':':
usageError(argv[0], "Missing argument", optopt);
case '?':
usageError(argv[0], "Unrecognized option", optopt);
default:
{
printf("Unexpected case in switch()");
exit(EXIT_FAILURE);
}
}
}
if(xfnd != 0)
printf("-x was specified (count=%d)\n", xfnd);
if(pstr != NULL)
printf("-p was specified with the value \"%s\"\n", pstr);
if(optind < argc)
printf("first nonoption argument is \"%s\" at argv[%d]\n",argv[optind], optind);
exit(EXIT_SUCCESS);
}
(nonoption,就是none option,不是选项……)
$./getpot -x -p hello world /*初始时候optind被设置为1*/
opt =120 (x); optind = 2 /*argv[1],也就是 "-x",不带参数。optind指向下一个没处理的"-p"*/
opt =112 (p); optind = 4 /*把p(argv[2])和它的参数(argv[3])都处理了,是2个,所以optind+=2*/
-x was specified (count=1)
-p was specified with the value "hello" /*p解析匹配到了它的参数 "hello"*/
first nonoption argument is "world" at argv[4] /*"world"解析失败*/
$./getpot -p
opt = 58 (:); optind = 2; optopt =112 (p) /*getopt()返回':'是报错了,而且是参数缺失,不懂看表去*/
Missing argument (-p)
Usage: ./getopt [-p arg] [-x]
$./getopt -a
opt = 63 (?); optind = 2; optopt = 97 (a) /*返回'?',报错了,而且是因为没有指定选项a*/
Unrecognized option (-a)
Usage: ./getopt [-p arg] [-x]
$./getopt -p str -- -x
opt =112 (p); optind = 3
-p was specified with the value "str" /*p匹配到了自己的参数str*/
first nonoption argument is "-x" at argv[4] /*-x被当成参数了,不被看成选项,虽然它本来就不是选项*/
$./getopt -p -x
opt =112 (p); optind = 3
-p was specified with the value "-x" /*p匹配了参数-x,x不再单独作为选项*/
GNU扩展
这一部分看书上的意思有点偏差(就是说用了"+"还要用putenv()
和setenv()
才能避免扩展,但是似乎一种方法就足够了),不知道是我理解的问题还是翻译不准确。还是看下手册为好。
标准手册
getpot(3)(2019-03-06)
By default, getopt() permutes the contents of argv as it scans, so that eventually
all the nonoptions are at the end. Two other modes are also implemented. If the
first character of optstring is ‘+’ or the environment variable POSIXLY_CORRECT is
set, then option processing stops as soon as a nonoption argument is encountered. If
the first character of optstring is ‘-’, then each nonoption argv-element is handled
as if it were the argument of an option with character code 1. (This is used by pro‐
grams that were written to expect options and other argv-elements in any order and
that care about the ordering of the two.) The special argument “–” forces an end of
option-scanning regardless of the scanning mode.
petmute:排列
意思是说,getopt()
默认会尽量把最后的选项理解为非法选项,就是走一步看一步,这很符合递进读取的思维。
但是也有两种其他方式是实现定义的:
- 如果
opstring
的第一个字符是+
,或者环境变量POSIX_CORRECT
被设置,那么就会按默认的规则:一碰到非法选项就报错。 - 如果
opstring
的第一个字符是-
,那么非法项会被处理,会被尽量当作一个选项的参数。--
会强制结束(一次)选项匹配而不管匹配模式。
(还是不太懂那个with character code 1是什么鬼)
GNU实现
怎么理解呢?
就是在一些实现,比如glibc
版本下面两个命令都能成功:
ls -l file
ls file -l
因为GNU的实现没有严格遵守SUSv3(或者SUSv4)标准,它对argv
里的元素进行了重新排列,但标准是要求不能更改的,而且函数原型给的argv
是char * const argv[]
,是不能改变指针指向的(但是手册中好像也说这个const
不是强加的,而是一种约定)。
避免方法
为了让程序拥有更好的移植性,包括shell脚本,还是要严格遵守SUSv3(SUSv4)标准,虽然GNU也很强~。
- 在
shell
中定义条件变量POSIXLY_CORRECT
为任意值即可,
比如:
然后你再使用export POSIXLY_CORRECT=abcdefg
ls file -l
就会发现报错了。但是这样每次都要修改,还得用户来改。 - putenv(),setenv()
这两个还没学,不会用,就不献丑了。💩 - 把
opstring[0]
设置为+
(此时如果还想用开头:
关闭报错,就必须这样的顺序:+:
)
总之glibc的扩展和SUS标准比起来是会有一些bug的。
比如对于chmod
,有一个名是以连字符-
开头的,那么glibc
版的重排列就会导致这个文件名被解释成chmod
的一个选项。
对于大部分命令,如果不设定 POSIXLY_CORRECT
,要处理这种运行在Linux上的脚本,方法是在第一个非选项参数前加上--
。
因此对于chmod 644 *
可以重写为chmod -- 644 *
补充
- SUSv3标准允许指代有强制性参数的选项,在GNU版的
getopt()
中可以通过在选项字符后防止两个冒号来表示这个参数是可选的。此时,对于这样的选项,选项和参数之间不能有空格。如果参数不存在,那么getopt()
返回后optarg
被设为NULL - 许多GNU都允许长选项语法。长选项允许两个连字符开始,选项本身用一个单字来标识,
比如$gcc --version
- GNU C函数库还有更为复杂的API(但不可移植),称为
argp
,详情见glibc
手册。
结语
拿下这个函数,真的不是很轻松,希望能对你有帮助。
如果想看实例,可以看看下一篇7.利用getopt()实现tee命令
如有错误,望大佬多多指正~ 😃
参考资料:《Linux/UNIX系统编程手册》