zhangguanzhang's Blog

shell脚本的选项和参数处理

字数统计: 2.8k阅读时长: 12 min
2017/05/25

在写shell脚本时经常会用到命令行选项、参数处理方式,如:

1
./test.sh -f 5 -rF --host-file=/etc/hosts

其中-f和-rF都是短选项,–host-file是长选项
短选项又分为可选参数和必选参数,不需要参数的短选项可以合着写
例如cat和tail的-n有默认值10
在shell中,可以用以下三种方式来处理命令行参数,每种方式都有自己的应用场景

  • 手工处理方式
  • getopts
  • getopt

手工处理不说了,手工处理高度依赖于你在命令行上所传参数的位置,所以一般都只用来处理较简单的参数
下面我们依次讨论这后两种处理方式
getopts是shell内置的,只支持短选项,也就是说getopts只支持带参数的和不带参数的,命令格式如下所示

1
getopts optstring name [args]

man手册这样写的
              getopts被shell程序用来分析位置参数.optstring包含要识别的选项字符;如果一个字符后面跟着一个冒号,这个选项应该有一个参数,这个参数应该用空格隔开,例如下面

getopts 'f:' name 表示支持选项-f并且必须有参数

冒号和问号字符不能用作选项字符。每次调用时,getopts都会将下一个选项放在name变量名称中(如果不存在则初始化name),并将要处理的下一个参数的索引放入变量OPTIND中。每次调用shell或shell脚本时,OPTIND初始化为1。当选项需要参数时,getopts将该参数放入变量OPTARG中。shell不会自动重置OPTIND;如果要使用一组新的参数,则必须在多次调用之间的时候必须手动重置以在同一个shell调用中能多此使用getopts。
              遇到选项结束时,getopts以大于零的返回值退出。 OPTIND被设置为第一个非选项参数的索引,名称被设置为?
              getopts通常会分析位置参数,但是如果args中有很多参数,getopts会解析这些参数。
              getopts可以通过两种方式报告错误。如果optstring的第一个字符是冒号,则使用静默错误报告。在正常操作中,当遇到无效选项或缺少选项参数时,打印诊断消息。如果变量OPTERR设置为0,即使optstring的第一个字符不是冒号,也不会显示错误消息。
          如果看到一个无效的选项,getopts会把name设置成?,如果不是静音,则输出错误消息并取消OPTARG。如果getopts无声,则找到的选项字符被放置在OPTARG中,并且不打印诊断消息。

              如果没有找到必需的参数,并且getopts不是忽略报错的,则会把name设置成?,并取消设置OPTARG,并打印诊断消息。如果getopts是沉默的,那么冒号(:)放在name中,OPTARG被设置为找到的选项字符。

              如果找到指定或未指定的选项,getopts将返回true。如果遇到选项结束或发生错误,它将返回false。

说得比较理论,我实验了下结合文档来通俗易懂的解释下
opstring这部分就是你要支持的选项,也就是每个字符一个选项,要把你所有的选项字符写成一个字符串

选项字符后面有冒号的是表示必须要有参数,例如'f:h:d'表示三个选项,其中f和h后面有冒号表示必须需要参数值,d后面没有冒号表示不需要参数值,当然也可以写成'df:h:',注意:选项字符不能是冒号和问好
每次使用时,getopts都会把下一个选项字符放在上面命令格式的那个”name”这个shell变量里(如果这个变量不存在就初始化它),并把下一个
要处理的参数的下标放在变量OPTIND中。每次启动脚本时,都把OPTIND初始化为1
也就是说name是选项字符,OPTARG是选项对应的值
上面说了一大堆理论,我们来实践看看,先一个带参数选项看看

1
2
3
4
5
6
7
8
9
10
11
[root@guan temp]# cat bashtest
#!/bin/bash
while getopts 'f:' args;do
case $args in
f)
echo 'use -f :' $OPTARG
;;
esac
done
[root@guan temp]# ./bashtest -f 4
use -f : 4

加个带参数选项看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@guan temp]# cat bashtest
#!/bin/bash
while getopts 'f:h:' args;do
case $args in
f)
echo 'use -f :' $OPTARG
;;
h)
echo 'use -h :' $OPTARG
;;
esac
done
[root@guan temp]# ./bashtest -f 3 -h 4
use -f : 3
use -h : 4

加上一个不需要参数的选项试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@guan temp]# cat bashtest
#!/bin/bash
while getopts 'f:h:d' args;do
case $args in
f)
echo 'use -f :' $OPTARG
;;
h)
echo 'use -h :' $OPTARG
;;
d)
echo 'used d'
;;
esac
[root@guan temp]# ./bashtest -f 3 -h 4 -d
use -f : 3
use -h : 4
used d

加上?处理下未知选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@guan temp]# cat bashtest
#!/bin/bash
while getopts 'f:h:d' args;do
case $args in
f)
echo 'use -f :' $OPTARG
;;
h)
echo 'use -h :' $OPTARG
;;
d)
echo 'used d'
;;
?)
echo error
exit 1
;;
esac
done
[root@guan temp]# ./bashtest -c
./bashtest: illegal option -- c
error

有标准2的错误输出,我们只想自定制错误输出内容,所以按照文档在选项字符开始加冒号静默试试’:f:h:d’

1
2
[root@guan temp]# ./bashtest -c
error

然后测试下OPTIND

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
[root@guan temp]# cat bashtest
#!/bin/bash
while getopts ':f:h:d' args;do
case $args in
f)
echo 'use -f: $OPTARG:' $OPTARG -- '$OPTIND' $OPTIND
;;
h)
echo 'use -h: $OPTARG:' $OPTARG -- '$OPTIND' $OPTIND
;;
d)
echo 'use -d: $OPTARG:' $OPTARG -- '$OPTIND' $OPTIND
;;
?)
echo error
exit 1
;;
esac
done
[root@guan temp]# ./bashtest -f 3 -h 2 -d
use -f: $OPTARG: 3 -- $OPTIND 3
use -h: $OPTARG: 2 -- $OPTIND 5
use -d: $OPTARG: -- $OPTIND 6
[root@guan temp]# ./bashtest -f 3 -h2 -d
use -f: $OPTARG: 3 -- $OPTIND 3
use -h: $OPTARG: 2 -- $OPTIND 4
use -d: $OPTARG: -- $OPTIND 5
[root@guan temp]# ./bashtest -f3 -h2 -d
use -f: $OPTARG: 3 -- $OPTIND 2
use -h: $OPTARG: 2 -- $OPTIND 3
use -d: $OPTARG: -- $OPTIND 4
[root@guan temp]# ./bashtest -f3 -dh2
use -f: $OPTARG: 3 -- $OPTIND 2
use -d: $OPTARG: -- $OPTIND 2
use -h: $OPTARG: 2 -- $OPTIND 3

上面可以看出OPTIND是下一个选项的索引值,默认是被初始化成1的,你选项和选项的值没有用空格隔开连着写也会智能识别到下一个选项的索引值
在一些命令里有这样的场景,选项值多个但是空格分开写会不好识别所以类似-f a,b,c,我们可以利用$OPTIND去处理
到现在为止,getopts不支持类似cat -n那样选项的参数是可选的方式,如下图-h的值会被认为是-d

1
2
3
[root@guan temp]# ./bashtest -f 3 -h -d
use -f: $OPTARG: 3 -- $OPTIND 3
use -h: $OPTARG: -d -- $OPTIND 5
  • getopt命令是Linux下的命令行工具,并且getopt支持命令行的长选项(比如,–some-option)。

另外,在脚本中它们的调用方式也不同。
看下命令帮助

1
2
3
4
5
6
7
8
9
名称
getopt - parse command options (enhanced)
概要
       getopt optstring parameters
       getopt [options] [--] optstring parameters
       getopt [options] -o |--options optstring [options] [--] parameters
...
-o, --options shortopts
-l,--longoptions longopts

短选项在-o后面写,和getopts类似,不过getopts不支持可选选项,-l后面是长选项

1
getopt -o ut:r:p:: -n "$0" -l tlong:,long-t1:,long-t2::  -- "$@"

例如上面的u后面没有冒号和getopts一样,选项字符后面有冒号就是有参数,没冒号就没参数,俩个冒号就是可选参数,可选参数就是像cat -nhaed -n啥的,可有可无表示开启某个功能一样(至于这个选项是否需要值,例如head -n可以指定前多少行,这个后面会说到)
这个是个外部命令,所以它是返回东西需要我们去在shell里处理的

1
2
3
4
5
6
[root@guan ~]# getopt -o ut:r:p:: -n "$0" -l tlong:,long-t1:,long-t2:: -- -u -r3 -p --tlong 1 
-u -r '3' -p '' --tlong '1' --
[root@guan ~]# getopt -o ut:r:p:: -n "$0" -l tlong:,long-t1:,long-t2:: -- -u -r3 -p --tlong=1
-u -r '3' -p '' --tlong '1' --
[root@guan ~]# getopt -o ut:r:p:: -n "$0" -l tlong:,long-t1:,long-t2:: -- -u -r3 -p --tlong 1 --long-t1=233
-u -r '3' -p '' --tlong '1' --long-t1 '233' --

-o后面接短选项字符,-l后接长选项字符,长选项可以用等号也可以不用(不用的时候必须空格隔开选项和参数,不能像短选项那样贴着写),–是把后面的东西不当作选项,例如我们需要创建一个名为-p的文件夹,mkdir -p的话会把-p当作选项,mkdir – -p就行了,这个是交互式shell终端下就是不把–后面的内容当作选项,所有命令同理
–后面就是对应的需要解析的参数了,所以在脚本里一般这部分是– “$@”直接把脚本的选项部分全部传进来,然后把输出用’set – 输出’转换成位置参数后再用循环判断,输出里结尾的–是表示结束了,给用户判断的一个标识
–后面的部分如果有选项字符串里不存在的选项就会报错,例如

1
2
3
[root@guan ~]# getopt -o ab:c::  -- -a -b3 -d
getopt: invalid option -- 'd'
-a -b '3' --

-n的作用就是把那个报错程序名的getopt换成-n的参数,一般在脚本里都是写-n $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
59
60
61
62
63
64
65
66
67
[root@guan temp]# cat option.sh 
#!/bin/bash
#将规范化后的命令行参数分配至位置参数($1,$2,...)
temp=`getopt -n $0 -o ut:r:p:: -l help,tlong:,long-t1:,long-t2:: -- "$@"`

[ $? != 0 ] && {
echo 'Try '$0 '--help for more information.'
exit 1
}
set -- $temp

while true;do
case "$1" in
-u)
echo '-u has be used';
shift
;;
-r)
echo '-r has be used with:' $2;
shift 2
;;
-p)
case "$2" in
"")
echo '-p has be used without arg'
shift 2
;;
*)
echo '-p has be used with:' $2
shift 2
;;
esac
;;
-t|--tlong)
echo '-t or --tlong has be used with: ' $2
shift 2
;;
--help)
#help_function_msg
echo 'some information about how to usage'
shift 1
;;
--long-t1)
echo '--long-t1 has be used with: ' $2
shift 2
;;
--long-t2)
case "$2" in
"")
echo '--long-t2 has be used without arg'
;;
*)
echo '--long-t2 has be used with:' $2
shift 2
;;
esac
;;
--)
shift
break
;;
*)
echo "Internal error!"
exit 1
;;
esac
done
1
2
3
4
5
6
7
8
9
10
11
12
[root@guan temp]# bash option.sh -t 2 -p22
-t or --tlong has be used with: '2'
-p has be used with: '22'
[root@guan temp]# bash option.sh -t 2 -p 22
-t or --tlong has be used with: '2'
-p has be used with: ''
[root@guan temp]# bash option.sh -t2 -p22 --long-t2=testlong
-t or --tlong has be used with: '2'
-p has be used with: '22'
--long-t2 has be used with: 'testlong'
[root@guan temp]# bash option.sh --help
some information about how to usage

shift是移动位置参数,一般选项和它的参数都是成对的,所以循环+case判断–是否break退出循环,当然你也可以写到一个变量里用awk判断数量后循环里面内容
上面随便写了下,有的命令长短选项一样,就像上面的-t|--tlong,一般命令--help--version都输出,有兴趣可以自己补全,脚本下载地址 https://github.com/zhangguanzhang/bash/blob/master/option.sh
还有测试发现可选的短参数必须要紧贴着选项字符,如上面结果所示,看了下man里的ftp地址结果找不到,命令属于util-linux,都找不到地方反馈,有兴趣的c大佬可以看下对应的源码包改下

CATALOG