脚本(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 | # 成对的符号,应当一次性写出来,再退格添加内容 |