脚本(script)就是包含一系列命令的一个文本文件。Shell 读取这个文件,依次执行里面的所有命令,就好像这些命令直接输入到命令行一样。所有能够在命令行完成的任务,都能够用脚本完成。
脚本的好处是可以重复使用,也可以指定在特定场合自动调用,比如系统启动或关闭时自动执行脚本。
脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。
这一行以#!字符开头,#!后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh或/bin/bash。所以这一行就叫做 Shebang 行。
1 | # 为了保险,也可以写成下面这样,返回`bash`可执行文件的位置。(防止如果 Bash 解释器不放在目录`/bin`中) |
#!/usr/bin/env NAME这个语法的意思是,让 Shell 查找$PATH环境变量里面第一个匹配的NAME。如果不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。
如果没有 Shebang 行,就只能手动将脚本传给解释器来执行。
1 | $ /bin/sh ./hello.sh |
脚本执行的前提条件,就是脚本需要有执行权限。脚本的权限通常设为755(拥有者有所有权限,其他人有读和执行权限)或者700。
除了执行权限,脚本调用时,一般需要指定脚本的路径(比如path/script.sh)。如果将脚本放在环境变量$PATH指定的目录中,就不需要指定路径了。因为 Bash 会自动到这些目录中,寻找是否存在同名的可执行文件。
建议在主目录新建一个~/scripts子目录,专门存放可执行脚本,然后把~/scripts加入$PATH。
1 | $ export PATH=$PATH:~/scripts # 添加环境变量 |
Bash 脚本中,#表示注释,可以放在行首,也可以放在行尾。
1 | # 本行是注释 |
建议在脚本开头,使用注释说明当前脚本的作用,这样有利于日后的维护。
source命令也可以用于执行脚本,通常用于重新加载一个配置文件。**source命令最大的特点是在当前 Shell 执行脚本**,不像直接执行脚本时,会新建一个子 Shell。
所以source命令的另一个用途,是在脚本内部加载外部库。
1 |
|
source有一个简写形式,可以使用一个点(.)来表示
调用脚本的时候,脚本文件名后面可以带有参数。
1 | $ script.sh word1 word2 word3 |
上面例子中,script.sh是一个脚本文件,word1、word2和word3是三个参数。
脚本文件内部,可以使用特殊变量,引用这些参数。
$0:脚本文件名,即script.sh。$1~`$9:对应脚本的第一个参数到第九个参数。如果脚本的参数多于9个,那么第10个参数可以用${10}`的形式引用,以此类推。$#:参数的总数。$@:全部的参数,参数之间使用空格分隔。可以理解为是一个包含全部参数的列表。$*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。用户可以输入任意数量的参数,利用for循环,可以读取每一个参数。
1 | for i in "$@"; do |
如果多个参数放在双引号里面,视为一个参数。
1 | $ ./script.sh "a b" |
上面例子中,Bash 会认为"a b"是一个参数,$1会返回a b。注意,返回时不包括双引号。
shift命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1),使得后面的参数向前一位,即$2变成$1、$3变成$2、$4变成$3,以此类推。
while循环结合shift命令,也可以读取每一个参数。
1 |
|
上面例子中,shift命令每次移除当前第一个参数,从而通过while循环遍历所有参数。
shift命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1。
1 | shift 3 |
上面的命令移除前三个参数,原来的$4变成$1。
脚本中经常要处理命令行参数,对于比较复杂的脚本,有些命令行参数是作为以-开头的选项(以下称为opt),有些命令行参数是作为opt的参数(以下称为arg),如果手工处理是很复杂的。这时候可以使用getopts命令。调用格式:
1 | getopts options variable |
getopts 的设计目标是在循环中运行,每次执行循环,getopts就检查下一个命令行参数,并判断它是否为合法opt(即检查参数是否以-开头,且属于 options 中的字母)。如果是,就把opt字母存在指定的变量 variable 中,若在其后面还发现了:,会从该opt后读取其arg,将其保存在特殊的变量OPTARG中,并返回退出状态0;如果 - 后面的字母没有包含在 options 中,就在 variable 中存入一个 ?,并返回退出状态0;如果命令行中已经没有参数,或者下一个参数不以 - 开头,就返回不为0的退出状态。
OPTIND 表示命令行下一个选项或参数的索引,变量OPTIND在getopts开始执行前是1,然后每次执行就会加1。等到退出while循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1就是已经处理的连词线参数个数,使用shift命令将这些参数移除,保证后面的代码可以用$1、$2等处理命令的主参数。
下面是一个例子:
1 | while getopts 'lha:' OPTION; do |
---和--开头的参数,会被 Bash 当作配置项解释。但是,有时它们不是配置项,而是实体参数的一部分,比如文件名叫做-f或--file。
1 | $ cat -f |
上面命令的原意是输出文件-f和--file的内容,但是会被 Bash 当作配置项解释。这时就可以使用配置项参数终止符--,它的作用是告诉 Bash,在它后面的参数开头的-和--不是配置项,只能当作实体参数解释。
1 | $ cat -- -f |
上面命令可以正确展示文件-f和--file的内容,因为它们放在--的后面,开头的-和--就不再当作配置项解释了。
下面是另一个实际的例子,如果想在文件里面搜索--hello,这时也要使用参数终止符--。
1 | $ grep -- "--hello" example.txt |
上面命令在example.txt文件里面,搜索字符串--hello。这个字符串是--开头,如果不用参数终止符,grep命令就会把--hello当作配置项参数,从而报错。
命令执行结束后,会有一个返回值。0表示执行成功,非0(通常是1)表示执行失败。环境变量$?可以读取前一个命令的返回值。
利用这一点,可以在脚本中对命令执行结果进行判断。
1 | cd /path/to/somewhere |
由于if可以直接判断命令的执行结果,执行相应的操作,上面的脚本可以改写成下面的样子。
1 | if cd /path/to/somewhere; then |
更简洁的写法是利用两个逻辑运算符&&(且)和||(或)。
1 | # 第一步执行成功,才会执行第二步 |
exit命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。
1 | $ exit # 中止当前脚本,将最后一条命令的退出状态,作为整个脚本的退出状态。 |
exit命令后面可以跟参数,该参数就是退出状态。
1 | # 退出值为0(成功) |
退出时,脚本会返回一个退出值。脚本的退出值,0表示正常,1表示发生错误,2表示用法不对,126表示不是可执行脚本,127表示命令没有发现。如果脚本被信号N终止,则退出值为128 + N。简单来说,只要退出值非0,就认为执行出错。
Shell脚本第一行指定脚本解释器
1 |
|
Shell脚本的开头添加作者、日期、用途等信息
1 | # Date: xxxx |
尽量使用英文注释
1 | 如果一定要添加中文,可以对系统和脚本都设置字符集 |
Shell脚本应放在固定的路径中,并且以点 .sh 结尾
1 | cd /server/scripts |
保持良好的代码编写习惯
1 | # 成对的符号,应当一次性写出来,再退格添加内容 |