来源:公众号『很酷的程序员』
ID:RealCoolEngineer
正确安全地编写shell脚本,避免脚本导致出乎意料的结果,并且在出现的问题的时候及时报错退出。
本文基于笔者以往的shell脚本编程经验,给出一些基础的编程技巧,以便提高shell脚本的安全性和健壮性。
一 一定要使用shell吗?
shell脚本其实很多地方可能都会导致问题,尤其在执行删除(rm -rf xxx)操作的时候,更是要仔细。因此如果使用shell脚本不是必要的,那就尽量避免使用,毕竟只要不用就不会犯错(^_^)。
在实践的过程中,往往也会有额外的字符串处理、目录遍历等等一些操作,所以笔者实际工作中更多会采用Python + Shell的方式开发,使用Python的subprocess
模块执行一些需要在shell环境中执行的命令。但是在使用的时候要进行避免指定shell=True
参数,否则还是可能遇到纯shell编程中的一些问题。比如:
import subprocess as sp
# 推荐使用
sp.run(('ls', '-l'))
# 而不是
sp.run('ls -l', shell=True)
二 shell脚本推荐设置
shell本身是有一些配置选项的,通过内置命令set
进行设置和查看,细节可以查看官方文档:The Set Builtin。
通过命令set -o
可以查看shell的当前选项开关情况如:
➜ ~ # set -o | grep pipe
pipefail off
通过set -o <option-name>
可以开启某个选项:
➜ ~ # set -o pipefail
➜ ~ # set -o | grep pipe
pipefail on
使用set +o <option-name>
则是关闭指定选项:
➜ ~ # set +o pipefail
➜ ~ # set -o | grep pipe
pipefail off
下面重点来了,对于安全的shell脚本,建议在脚本开头添加以下设置:
set -euf -o pipefial
1 -e参数
在执行到某个命令返回非0时立即结束脚本。
这是很有用的,在已经发生错误的时候及时结束脚本的执行,否则脚本即使有错误信息也还是会继续执行下去,只要最后一个命令正确执行,那么整个脚本的返回值将会是正常。如下脚本:
#!/bin/bash
set -e
echo "Begin to run"
some-command-not-exists # Will quit script here
echo "Done"
上面的脚本将会在第4行处出现错误,并直接退出脚本。
如果某些命令可能执行错误是在预期内的,那么可以通过在命令后面添加|| true
来避免脚本直接退出:
some-comman-may-fail || true
如果是多行命令,则建议使用set +e
暂时关闭发生错误则立即退出的功能:
set +e
some-comman-may-fail-1
some-comman-may-fail-2
...
set -e
2 -u参数
将未设置的参数或者变量视为错误并退出脚本。
比如以下脚本:
#!/bin/bash
#set -u
echo "Begin to run"
some-command "p1" $var "p3" # Will quit script here
echo "Done"
因为var
变量没有定义,那么在第4行就根本不会执行命令,而是直接报错退出。
这里还有另外一个问题,如果没有使用
-u
,var
没有定义,传递给命令的第二个参数就会变成"p3"
,显然这不是编程者的本意。该问题的解决方法后面会提及。
3 -f参数
禁用文件名通配符。
比如脚本:
#!/bin/bash
set -f
echo "Begin to run"
rm -rf *.md
echo "Done"
此时*.md
并不会匹配所有文件名后缀为.md
的文件,而是文件名只能是"*.md"。这样的好处在于避免错误操作一些后缀名相同的文件,此时可以通过执行需要操作的文件列表来替代。
此功能也可以通过内置命令
shopt
设置shopt -s failglob
来实现,同样的是不对通配符*
做任何处理。
当然如果必须要使用通配符,那么就不能使用-f
参数了。
4 -o pipefail选项
针对管道,在管道中某个命令出错时立即报错返回。结合-e
就可以实现在管道报错时也直接退出脚本。
比如以下log分析命令:
set -e -o pipefail
find . -name "logcat*" | xargs grep -a -h "" | grep -a --color=auto -v "\-* switch to"
如果当前目录没有任何log文件,那就没有必要继续往下执行了。
类似地,如果多个命令之间存在顺序依赖关系,那么可以使用&&
将多个命令连接起来,这样在某个命令执行失败时也可以提前返回。比如:
make && make test && make install
5 -x参数
显示脚本的执行过程。
此参数并不是安全编写shell脚本的推荐参数,但是对于新手而言,使用此参数可以看到脚本的执行过程。而且不一定需要写在脚本内部,也可以通过bash -x xxx.sh
查看执行过程。
比如脚本:
#!/bin/bash
echo "Begin to run"
some-command "p1" $var "p3"
echo "Done"
执行时可以看到其流程:
+ echo 'Begin to run'
Begin to run
+ some-command p1 p3
./demo.sh: line 3: some-command: command not found
+ echo Done
Done
每个+
后面就是执行的命令,紧接着的就是执行命令的输出。
二 安全使用变量
1 任何时候都应该使用引号
在使用变量时,总应该使用双引号。因为shell脚本会执行Word Splitting,可能导致一些意想不到的情况。
和前面示范demo的一样,对于some-command "p1" $var "p3"
,因为var
没有定义,所以在执行时实际执行的是some-command p1 p3
,因此就会发生意料之外的问题;而如果使用了双引号,第二个参数就会是一个空字符串,这才是更加符合预期的结果。
2 设置参数默认值
可以给变量设置默认值以避免一些异常的发生。
针对未定义字符串:
- ${var-defaultValue}:如果var未定义,那么表达式为默认值;
- ${var=defaultValue}:如果var没有定义,那么表达式为默认值,并且设置var为默认值。
针对未定义或者空字符串:
- ${var:-defaultValue}:如果未定义或者为空,那么表达式为默认值;
- ${var:=defaultValue}:如果var没有定义或者为空,那么表达式为默认值,并且设置var为默认值。
三 借助工具进行检查
写完脚本可以使用外部工具做一次检查,比如开源的shellcheck工具,可以在本地安装,也可以使用网页版:https://www.shellcheck.net/。
其实很多IDE(Clion、vscode)都有相关的插件支持shell脚本的检查,安装shellcheck插件即可。