我使用Linux已有8年有余,经常会编写shell脚本进行自动化处理。然而,到目前为止,我依然不能像熟练使用C语言一样编写shell脚本。确实,我的主力编程语言是C语言,仅在做自动化脚本或者编写自动化测试用例的时候才会使用shell。另外一方面,shell脚本的语法的变种太多,例如,if语句在做字符串、数值、文件比较时的判断语句都相差很大;特殊符号多,$#
、$@
、$?
等等,如果你是第一次接触shell脚本,必然会手足无措,更坑爹的是,这些特殊符号使用的场景相差很大,记忆负担真的大!!还有,shell脚本会依赖太多小程序,正如unix哲学所言:一个工具肩负单一使命,这些程序的各自用途、各自选项差异很大,你根本没有办法一下子就记住所有用法!!!
上面就是对自己写不好shell脚本的一些反思,破局还是有办法的。既然shell脚本的用法诡谲多变、充满奇淫技巧,依靠大脑的记忆肯定是不靠谱的,应该建立codebase记录那些不太好记忆的shell语法、指令的典型应用示例。当忘记了相关的shell命令时,便可翻阅codebase唤醒代码记忆。当然,那些已深刻在你记忆中的知识就冗余无需记录。本篇文章是shell codebase的第一篇文章,主要介绍shell命令行参数的处理。
命令行参数基础
向shell脚本传输数据最基本的方法是使用命令行参数。传入的命令行参数通过位置参数(positional parameter)的特殊变量进行区分:$0
表示程序名,$1
、$2
分别表示第一、第二个参数,依次类推。需要注意的是$0
表示的是命令行中执行shell脚本的路径,使用相对路径和绝对路径,$0
中的内容会相应改变。可使用basename
提取文件名。
shell将空格作为参数的分割符,若参数中需要包含空格,则需要用引号(单引号、双引号均可)完整地包含参数。
有个小聪明啊,可以编写基于脚本名来执行不同功能的脚本:
1 | # cat filename_as_func.sh |
将filename_as_func.sh
链接到不同的文件名,查看运行结果:
1 | # ln -s filename_as_func.sh add |
检查预期的命令行参数是否存在是一种良好的编码风格,让脚本直接报错不可取。如下是检查第一个参数是否存在:
1 | if [ -z "$1" ]; then |
bash shell中有些特殊变量记录命令行参数。$#
表示命令行参数的个数。很直观地,我们会考虑借助$#
表示最后一个命令行参数:${$#}
。然而这种表示并不正确,不能在花括号中使用美元符,必须将美元符替换为感叹号,即${!#}
。
1 | [root@localhost parameters]# cat last_parameter.sh |
$@
和$*
特殊变量都能表示所有参数。
$*
会将命令行提供的所有参数当作单个单词进行访问。$@
会将命令行上提供的所有参数当作同一个字符串中多个独立的单词。它允许你遍历所有值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23[root@localhost parameters]# cat all_parameter.sh
#!/bin/bash
# test $* and $@ difference, they both represent all the parameters
count=1
for param in "$*"
do
echo "\$* parameter #$count = $param"
count=$[ $count + 1 ]
done
count=1
for param in "$@"
do
echo "\$@ parameter #$count = $param"
count=$[ $count + 1 ]
done
[root@localhost parameters]# ./all_parameter.sh 1 2 3 4
$* parameter #1 = 1 2 3 4
$@ parameter #1 = 1
$@ parameter #2 = 2
$@ parameter #3 = 3
$@ parameter #4 = 4
处理命令行选项
执行脚本通常会指定命令行选项,实际上,可以像处理命令行参数一样,处理命令行选项。我们先来看看最硬核的“手撕shell命令行选项”的处理方式。
1 | [root@localhost parameters]# cat basic_options.sh |
shift
命令会将每个参数变量减一。所以变量$2
会移动到$1
,而原来的$1
会被删除,依次类推。注意:变量$0
的值是不会改变,即程序名不会改变。所以通过while
循环配合shift
命令便能够处理所有命令行参数。通过case
语句判断命令行选项,并做相应处理。
上面的脚本可以识别选项,但同时使用选项和参数,则对参数无法很好地处理。Linux中处理这个问题的标准方法是使用特殊字符(双破折线–)将选项和参数分开,特殊字符会告诉脚本,选项何时结束以及普通参数何时开始。
1 | [root@localhost parameters]# cat extract_parameters.sh |
当脚本遇到双破折线时,就停止处理选项,将剩下的参数都作为命令行参数。然而,此种方式将命令行选项和参数泾渭分明。如果命令行选项会带有额外的参数,此种方式便无法区分,需要手动处理选项参数:
1 | [root@localhost parameters]# cat basic_option_parameter.sh |
当前shell脚本已经具备处理命令行选项的基本能力,但是还有一些限制,如无法区分合并的命令行选项。另外,没有用户愿意通过输入”–”区分选项和参数,这个工作最好由shell脚本自动完成。下面介绍的getopt
命令就是为了摆脱这种最硬核的选项解析方式。
getopt命令
getopt
命令是处理命令行选项和参数的一个非常强有力的工具,在CentOS中,其由util-linux
提供。getopt
命令参数可以分为两部分:1.命令行选项[options]
2. getopt
待解析的命令行参数parameters
,parameters
是从第一个非getopt
命令行选项开始的,或者在--
之后。根据getopt
命令第一部分的状态,getopt
的调用方式可以分为三种:
- 无命令行选项,此种选项解析最为简单,也是最常用的,但是只能解析短选项:
1
getopt optstring parameters
parameters
:getopt要解析的命令行参数1
2# getopt "ab:cd" -a 1 -b 2 -cd 3 4
-a -b 2 -c -d -- 1 3 4
- 有命令行选项,但是没有
-o|--options
选项,-o
选项比较特殊,用来指定短选项(shortopts):1
getopt [options] [--] optstring parameters
- options表示所有的非
-o
选项,如果没有--
,则第一个非getopt
选项的参数就是shortopts,随后便是待解析参数。如果指定了一个不存在1
2
3
4
5
6
7
8
9
10
11[root@localhost ~]# getopt "ab:cd" -c 1 -b 2 -ade 3 4
getopt: invalid option -- 'e'
-c -b 2 -a -d -- 1 3 4
[root@localhost ~]# getopt -q "ab:cd" -c 1 -b 2 -ade 3 4
-c -b '2' -a -d -- '1' '3' '4'
[root@localhost ~]# echo $?
1
[root@localhost ~]# getopt -q -- "ab:cd" -c 1 -b 2 -ade 3 4
-c -b '2' -a -d -- '1' '3' '4'
[root@localhost ~]# echo $?
1optstring
的选项,默认情况下,getopt
会报错,通过-q
选项会忽略这条错误信息,但是getopt
还是会返回错误值。上面例子的最后两条指令表明了shortopts的位置。这种隐式指明shortopts位置的方式在有-l|--longoptions
参数的时候很容易造成误解。同shortopts指定参数的方式,唯一不同的是,longopts的每个参数通过逗号分隔。上面这个示例,我们打算只定义长命令行选项。然而第一条shell命令未解析到参数1
2
3
4[root@localhost ~]# getopt -l extract:,tar:,help,directory -- file --extract a.tar
--extract 'a.tar' --
[root@localhost ~]# getopt -l extract:,tar:,help,directory -- file --extract a.tar -f -i abc
--extract 'a.tar' -f -i -- 'abc'file
。再看看后一条命令,大概就心里有数了。如果getopt
命令带有选项,但是未带有-o
选项,getopt
会将命令第二部分的第一个参数即file
作为shortopts
。这说明getopt
默认总是会解析短选项,其必须指定shortopts
。然而,这种隐式的指定,对于命令的理解极其不友好。
- 若要指定长命令选项,最好通过
-o|--options
显式地指明短命令选项:还是上面的例子,但同时指明短命令选项:1
getopt [options] -o|--options optstrin [options] [--] parameters
可见,即便你不想指定短选项,显式地通过将1
2
3
4[root@localhost ~]# getopt -o e:t:hd -l extract:,tar:,help,directory -- file --extract a.tar
--extract 'a.tar' -- 'file'
[root@localhost ~]# getopt -o '' -l extract:,tar:,help,directory -- file --extract a.tar
--extract 'a.tar' -- 'file'-o
选项指定为空。也能够正确地解析出参数file
。
使用getopt
处理脚本的命令行参数:
1 | [root@localhost parameters]# cat basic_getopt.sh |
其中最为关键的是set
与getopt
的配合使用。前面,我们已经提到--
是选项和参数的分界符,--
之后都代表set
命令的参数,即便其中包含-
,也不会被识别为set
命令的选项。上述set命令会将当前环境变量$@
设置为getopt -q "ab:c" "$@"
的输出。那么,这条set
命令就是为了用getopt
格式化后的命令行参数来替换原始的命令行参数。后续while循环处理的便是格式化的命令行参数即:
1 | [root@localhost parameters]# getopt -q "ab:c" -c 1 -b 2 -ad 3 4 |
getopts命令
bash shell包含了getopts
命令。与getopt
将命令行上找到的选项和参数处理后只生成一个输出不同。每次调用getopts
,它只处理一个命令行上检测到的参数。处理完所有参数后,它会退出并返回一个大于零的退出状态码。因此,getopts
非常适合用于解析命令行所有参数的循环中。getopts
命令的格式如下:
1 | getopts optstring variable |
optstring
同getopt
命令的shortopts
。若getopts
要忽略错误信息,可以在optstring
之前加上冒号。variable
表示待解析的命令行参数。
getopts
会用到两个环境变量。如果选项后面跟参数,OPTARG
环境变量保存该参数值。OPTIND
环境变量保存了参数列表中getopts
正在处理的参数位置。
1 | [root@localhost parameters]# cat getopts_OPTIND.sh |
由上面的示例可知,getopts
处理每个选项时,它会将OPTIND
环境变量的值增1。在getopts
完成处理后,可以将OPTIND
和shift
命令一起使用移动参数。
与getopt
命令不同的是:getopts
解析命令行选项时,会移除开头的破折号。它能够将命令行上找到的所有未定义的选项统一输出问号。同时getopts
不支持长选项的解析。
但是getopts
命令并不灵活:
1 | [root@localhost parameters]# ./getopts_OPTIND.sh -a -btest1 -d "test2 test3" -c |
如果参数出现在选项之前,getopts
便不能够解析到选项了。
因此getopt
提供了较为强大且定制的功能,getopts
提供了快捷的命令行参数选项解析功能,但并不灵活。
解析选项冲突
同一命令的不同选项存在冲突语义的情况,例如tar
命令的-x
选项表示解压tar包,-t
选项表示打包操作。显然,这两个选项的语义互相冲突。getopt
和getopts
并未定义选项之间的关系,因此我们必须手动解析选项之间的关系,可参考下面的例子:
1 | [root@localhost parameters]# cat option-relation.sh |
这不一定是最好的方法,但是代码较为清晰。前半部分解析选项,标记相应的flag。然后根据flag情况判断相应的依赖关系,进行实际的命令行选项处理。