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 -f
、grpe -E
hosts
是 subCmd,特别是 cli 工具,总体的功能分类命令入口,后面的 remove
是 hosts 的子命令,hosts前面的是全局选项
kubectl -n kube-system delete pod
--file=/etc/hosts
长选项和它的值,长选项可能只有长选项,但是也存在对应的短选项,也可能后面无值
grpe -V
、grep --version
以及 grep 的 -A, --after-context=NUM
test.domain
argument,也就是不是选项,剩下的 args
例如 kubectl -n kube-system delete pod pod_name1 pod_name2
上面只是粗糙举例,了解下概念即可。在 shell中,可以用以下三种方式来处理命令行参数,每种方式都有自己的应用场景:
手工处理不说了,手工处理高度依赖于你在命令行上所传参数的位置,所以一般都只用来处理较简单的参数,复杂的就很痛苦了。
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
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
还有测试发现可选的短参数必须要紧贴着选项字符,如上面结果所示。