Bash 6 脚本技巧
Table of Contents
1 一定要检查文件夹是否存在
给一个例子:
#! /bin/bash
dir_name=/path/not/exist
cd $dir_name
rm *
目录 $dir_name
不存在, cd $dir_name
命令就会执行失败。脚本就会在当前目录下继续执行,然后 rm *
命令就会删除当前目录的文件。
cd $dir_name && rm *
也不行,如果 $dir_name
为空,命令就变成了 cd && rm *
,这次是删除用户主目录的文件。
[[ -d $dir_name ]] && cd $dir_name && rm *
这样才对。
[[ -d $dir_name ]] && cd $dir_name && echo rm *
如果不放心删除什么文件,可以先打印出来看一下。 echo rm *
不会删除文件,只会打印出来要删除的文件。
2 用 bash -x 查 bug
bash 的 -x
参数可以在执行每一行命令之前,打印该命令。一旦出错,这样就比较容易追查。
$ cat>test.sh<<'EOF' #!/bin/bash echo "hello world1" echo "hello world2" echo "hello world3" EOF $ bash -x test.sh + echo 'hello world1' hello world1 + echo 'hello world2' hello world2 + echo 'hello world3' hello world3
3 用环境变量查 bug
3.1 $LINENO 保存行号
环境变量 $LINENO
保存了这个变量出现的行号。
$ cat>test.sh<<'EOF' #!/bin/bash echo "This is line $LINENO" echo "This is line $LINENO" EOF $ bash test.sh This is line 3 This is line 4
3.2 $FUNCNAME 保存函数调用栈
环境变量 $FUNCNAME
返回一个数组,内容是当前的函数调用堆栈。数组的 0 号成员是当前被调用的函数,1 号成员是调用了当前函数的函数,以此类推。
$ cat>test.sh<<'EOF' #!/bin/bash function func1() { echo "func1: FUNCNAME0 is ${FUNCNAME[0]}" echo "func1: FUNCNAME1 is ${FUNCNAME[1]}" echo "func1: FUNCNAME2 is ${FUNCNAME[2]}" func2 } function func2() { echo "func2: FUNCNAME0 is ${FUNCNAME[0]}" echo "func2: FUNCNAME1 is ${FUNCNAME[1]}" echo "func2: FUNCNAME2 is ${FUNCNAME[2]}" } func1 EOF $ bash test.sh func1: FUNCNAME0 is func1 func1: FUNCNAME1 is main func1: FUNCNAME2 is func2: FUNCNAME0 is func2 func2: FUNCNAME1 is func1 func2: FUNCNAME2 is main
3.3 $BASH_SOURCE 保存脚本调用栈
环境变量 $BASH_SOURCE
返回一个数组,内容是当前的脚本调用堆栈。数组的 0 号成员是当前执行的脚本,1 号成员是调用了当前脚本的脚本,以此类推。
$ cat>lib1.sh<<'EOF' # lib1.sh function func1() { echo "func1: BASH_SOURCE0 is ${BASH_SOURCE[0]}" echo "func1: BASH_SOURCE1 is ${BASH_SOURCE[1]}" echo "func1: BASH_SOURCE2 is ${BASH_SOURCE[2]}" func2 } EOF $ cat>lib2.sh<<'EOF' # lib2.sh function func2() { echo "func2: BASH_SOURCE0 is ${BASH_SOURCE[0]}" echo "func2: BASH_SOURCE1 is ${BASH_SOURCE[1]}" echo "func2: BASH_SOURCE2 is ${BASH_SOURCE[2]}" } EOF $ cat>main.sh<<'EOF' #!/bin/bash # main.sh source lib1.sh source lib2.sh func1 EOF $ bash main.sh func1: BASH_SOURCE0 is lib1.sh func1: BASH_SOURCE1 is ./main.sh func1: BASH_SOURCE2 is func2: BASH_SOURCE0 is lib2.sh func2: BASH_SOURCE1 is lib1.sh func2: BASH_SOURCE2 is ./main.sh
3.4 $BASH_LINENO 保存调用命令所在的行号
环境变量 $BASH_LINENO
返回一个数组,内容是每一轮调用对应的行号。 ${BASH_LINENO[$i]}
和 ${FUNCNAME[$i]}
是对应关系,表示 ${FUNCNAME[$i]}
在调用它的脚本文件 ${BASH_SOURCE[$i+1]}
里面的行号。
$ cat>lib1.sh<<'EOF' # lib1.sh function func1() { echo "func1: BASH_LINENO is ${BASH_LINENO[0]}" echo "func1: FUNCNAME is ${FUNCNAME[0]}" echo "func1: BASH_SOURCE is ${BASH_SOURCE[1]}" func2 } EOF $ cat>lib2.sh<<'EOF' # lib2.sh function func2() { echo "func2: BASH_LINENO is ${BASH_LINENO[0]}" echo "func2: FUNCNAME is ${FUNCNAME[0]}" echo "func2: BASH_SOURCE is ${BASH_SOURCE[1]}" } EOF $ cat>main.sh<<'EOF' #!/bin/bash # main.sh source lib1.sh source lib2.sh func1 EOF $ bash main.sh func1: BASH_LINENO is 7 func1: FUNCNAME is func1 func1: BASH_SOURCE is main.sh func2: BASH_LINENO is 8 func2: FUNCNAME is func2 func2: BASH_SOURCE is lib1.sh
4 修改 shell 运行参数查 bug
4.1 POSIX 规范中的 set 命令
set -u
(等同于 set -o nounset
) 脚本执行过程中遇到不存在的变量,报错。默认情况下,不存在的变量按空处理。
set -e
只要执行过程中发生错误,终止执行。但它不适用于管道命令。由于 Bash 总会把通过管道运算符 |
组合起来的,多个子命令的,最后一个子命令的返回值作为整个命令的返回值,这导致了只要最后一个子命令不失败,管道命令总是会执行成功。那么管道命令后面的命令依然会被执行, set -e
就失效了。
$ cat>test.sh<<'EOF' #!/bin/bash set -e foo | echo a echo bar EOF $ bash test.sh a script.sh:行4: foo: 未找到命令 bar
set -e -o pipefail
可以用来解决这种情况。只要管道命令的某个子命令失败,整个管道命令就失败,脚本就会终止执行。
set -E
纠正设置了 set -e
之后 trap 命令无法捕捉信号的问题。
set -x
和 bash -x
效果相同。但 set -x
可以做到只对某段代码打开执行过程输出:
#!/bin/bash
number=1
set -x
if [ $number = "1" ]; then
echo "Number equals 1"
else
echo "Number does not equal 1"
fi
set +x
echo "set -x closed"
set 命令还有一些其他参数。
set -n
: 等同于set -o noexec
,不运行命令,只检查语法是否正确。set -f
: 等同于set -o noglob
,不对通配符进行文件名扩展。使用set +f
关闭。set -v
: 等同于set -o verbose
,表示打印 Shell 接收到的每一行输入。使用set +v
关闭。set -o noclobber
: 防止使用重定向运算符>
覆盖已经存在的文件。
4.2 Bash 特有的 shopt 命令
shopt
和 set
的区别在于,set 是从 Ksh 继承的,属于 POSIX 规范的一部分,而 shopt是 Bash 特有的。
直接输入 shopt
可以查看所有参数,以及它们各自打开和关闭的状态。
shopt 命令后面跟着参数名,可以查询该参数是否打开。
$ shopt globstar globstar off
shopt -s
用来打开某个参数。
shopt -u
用来关闭某个参数。
shopt -q
也可以查询某个参数是否打开,但它不直接输出查询结果,而是通过命令的执行状态 $?
表示查询结果。如果状态为 0,表示该参数打开;如果为 1,表示该参数关闭。
$ shopt -q globstar $ echo $? 1
这个用法主要用于脚本,供 if 条件结构使用:
# 如果打开了这个参数,就执行 if 结构内部的语句
if !(shopt -q globstar); then
...
fi