zhangguanzhang's Blog

shell脚本的选项和参数处理

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

shell 脚本选项参数解析笔记

由来

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

1
./test.sh -b 5 -i -nm1 hosts remove --file=/etc/hosts test.domain

基本所有 Linux 的命令后面的选项存在以下情况的组合:

选项情况 描述 举例
-b 5 短选项和它的值 例如 head -n 2
-i 短选项无值,实际上就是 bool 值,有选项则内部某个记录变量为true。但是有些编程语言 flag 库处理后它后面都是可以跟 =true 或者 =false 例如 rm -fgrpe -E
hosts 是 subCmd,特别是 cli 工具,总体的功能分类命令入口,后面的 remove 是 hosts 的子命令,hosts前面的是全局选项 kubectl -n kube-system delete pod
--file=/etc/hosts 长选项和它的值,长选项可能只有长选项,但是也存在对应的短选项,也可能后面无值 grpe -Vgrep --version 以及 grep 的 -A, --after-context=NUM
test.domain argument,也就是不是选项,剩下的 args 例如 kubectl -n kube-system delete pod pod_name1 pod_name2

上面只是粗糙举例,了解下概念即可。在 shell中,可以用以下三种方式来处理命令行参数,每种方式都有自己的应用场景:

  • 手工处理方式
  • getopts
  • getopt

手工处理不说了,手工处理高度依赖于你在命令行上所传参数的位置,所以一般都只用来处理较简单的参数,复杂的就很痛苦了。

getopts 与 getopt

下面我们依次讨论这后两种处理方式

getopts

getopts 是 shell 内置的,只支持短选项,也就是说 getopts 只支持带参数的和不带参数的短选项,命令格式如下所示

1
getopts optstring name [args]

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

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

下面是官方文档:

1
2
3
4
5
6
7
8
9
冒号和问号字符不能用作选项字符。每次调用时,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是短选项对应的实际值

OPTARG

上面说了一大堆理论,我们来实践看看,先一个带参数选项看看

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

加个带参数选项看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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
$ ./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
$ 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
$ ./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
$ 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
$ ./bashtest -c
./bashtest: illegal option -- c
error

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

1
2
$ ./bashtest -c
error

OPTIND

然后测试下 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
$ 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
$ ./bashtest -f 3 -h 2 -d
use -f: $OPTARG: 3 -- $OPTIND 3
use -h: $OPTARG: 2 -- $OPTIND 5
use -d: $OPTARG: -- $OPTIND 6
$ ./bashtest -f 3 -h2 -d
use -f: $OPTARG: 3 -- $OPTIND 3
use -h: $OPTARG: 2 -- $OPTIND 4
use -d: $OPTARG: -- $OPTIND 5
$ ./bashtest -f3 -h2 -d
use -f: $OPTARG: 3 -- $OPTIND 2
use -h: $OPTARG: 2 -- $OPTIND 3
use -d: $OPTARG: -- $OPTIND 4
$ ./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
$ ./bashtest -f 3 -h -d
use -f: $OPTARG: 3 -- $OPTIND 3
use -h: $OPTARG: -d -- $OPTIND 5

getopt

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 后面无 : ,表明它不需要参数
    • -t -r 后面都有 :,都有参数
    • -p 后面 :: 表明 -p 选项可有可无,无参数,例如 cat -n
  • 长选项:
    • --tlong 有参数
    • --tlong-t1 有参数
    • --long-t2 无参数,选项可有可无
  • 长选项可以用等号也可以不用(不用的时候必须空格隔开选项和参数,不能像短选项那样贴着写)

getopt 是个外部命令,所以它是返回东西需要我们去在 shell 里处理的

1
2
3
4
5
6
$ getopt -o ut:r:p:: -n "$0" -l tlong:,long-t1:,long-t2:: -- -u -r3 -p --tlong 1 
-u -r '3' -p '' --tlong '1' --
$ getopt -o ut:r:p:: -n "$0" -l tlong:,long-t1:,long-t2:: -- -u -r3 -p --tlong=1
-u -r '3' -p '' --tlong '1' --
$ 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' --

-- 是把后面的东西不当作选项,例如我们需要创建一个名为 -p 的文件夹 mkdir -p 的话会把 -p 当作选项,mkdir -- -p 就行了,这个是 shell 不把 -- 后面的内容当作选项,所有命令同理。
-- 后面就是对应的需要解析的参数了,所以在脚本里一般这部分是 -- "$@" 直接把脚本的选项部分全部传进来,然后把输出用 set -- 输出 转换成位置参数后再用循环判断,输出里结尾的--是表示结束了,给用户判断的一个标识。

-- 后面的部分如果有选项字符串里不存在的选项就会报错,例如

1
2
3
$ getopt -o ab:c::  -- -a -b3 -d
getopt: invalid option -- 'd'
-a -b '3' --

getopt 的 -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
$ 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
$ bash option.sh -t 2 -p22
-t or --tlong has be used with: '2'
-p has be used with: '22'
$ bash option.sh -t 2 -p 22
-t or --tlong has be used with: '2'
-p has be used with: ''
$ 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'
$ 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

还有测试发现可选的短参数必须要紧贴着选项字符,如上面结果所示。

CATALOG
  1. 1. 由来
  2. 2. getopts 与 getopt
    1. 2.1. getopts
      1. 2.1.1. OPTARG
      2. 2.1.2. OPTIND
    2. 2.2. getopt