编写安全的shell脚本

来源:公众号『很酷的程序员』
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行就根本不会执行命令,而是直接报错退出。

这里还有另外一个问题,如果没有使用-uvar没有定义,传递给命令的第二个参数就会变成"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 设置参数默认值

可以给变量设置默认值以避免一些异常的发生。

针对未定义字符串:

  1. ${var-defaultValue}:如果var未定义,那么表达式为默认值;
  2. ${var=defaultValue}:如果var没有定义,那么表达式为默认值,并且设置var为默认值。

针对未定义或者空字符串

  1. ${var:-defaultValue}:如果未定义或者为空,那么表达式为默认值;
  2. ${var:=defaultValue}:如果var没有定义或者为空,那么表达式为默认值,并且设置var为默认值。

三 借助工具进行检查

写完脚本可以使用外部工具做一次检查,比如开源的shellcheck工具,可以在本地安装,也可以使用网页版:https://www.shellcheck.net/

其实很多IDE(Clion、vscode)都有相关的插件支持shell脚本的检查,安装shellcheck插件即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章