前言

当我们通过 Bash 执行 Script时,会创建一个子Shell进程进行执行。而且子Shell中的执行环境是一个独立的属于自己的环境,Bash 会给这个环境一些默认参数。

我们可以通过一些命令进行配置,改变执行环境中的一些参数,从而改变执行环境。即自定义执行环境。自定义执行环境与外层的父Shell的执行环境是两个互不影响的执行环境。也就说不必担心在子Shell中变更了执行环境会影响到父Shell的执行化境。

如何去改变呢,可以通过set、shopt命令进行改变。shopt命令在前面(Shell 学习(3)Bash 的模式扩展)已经详细介绍过了,那么与set命令的区别在哪呢?

  1. set 命令从 Ksh 继承的,属于 POSIX 规范的一部分。
  2. shopt是 Bash 特有的。

这两个命令都可以去改变当前 Shell 的执行环境的。下面就主要介绍一下set命令的参数。

set 命令

官方手册

我们介绍最常用的几个。

1、set -u:读取不存在的变量时抛出错误信息,终止执行。而不是默认的进行忽略。

等价于:set -o nounset

#!/bin/bash
set -u

echo $a
echo "end line"

改配置前输出:

$ ./test.sh 

end line

改配置后输出:

$ ./test.sh 
./test.sh: line 4: a: unbound variable
2、set -x:在命令的运行结果前,输出执行的命令。

关闭参数:set +x

等价于:set -o xtrace

#!/bin/bash
set -x

number=1
if [ $number = 1 ]; then
  echo "Number is equal to 1."
else
  echo "Number is not equal to 1."
fi

输出:

$ ./test.sh 
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.

不仅仅是命令,赋值运算操作也会进行输出。

但是感觉每行输出前的+不能直观的定位到行代码,该怎么改变呢?

在 Scirpt 顶部修改一下PS4环境变量,更改为取LINENO行号环境变量:

export PS4='${LINENO}: '

输出:

$ ./test.sh 
+ PS4='${LINENO}: '
6: number=1
7: '[' 1 = 1 ']'
8: echo 'Number is equal to 1.'
Number is equal to 1.

这样输出信息就更加直观了。但是千万注意:PS4='${LINENO}: ',取行号时是用的单引号。如果改为双引号,那么PS4环境变量会被赋值为赋值行的行号。那么在后面输出的行号会都是一个值,即赋值行行号。

3、set -e:产生错误时就终止执行(Script 的异常处理)。

关闭参数:set +e

等价于:set -o errexit

执行 Script 时,如果执行了一些退出码为非0的运行失败命令,Bash 默认会继续执行后面的内容,而不是终止 Script 的执行。

开发时我们期待的总是发生执行异常时退出,而不是继续执行。那么可能会通过下面的方式进行处理:

command || exit 1

通过逻辑或运算符去执行exit命令,使程序终止。

如果我们还期望在终止执行前,在做一些其他的操作:

# 写法一
command || { echo "command failed"; exit 1; }

# 写法二
if ! command; then echo "command failed"; exit 1; fi

# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi

这些写法,多少有些麻烦。可以通过set -e,交给 Bash 检查并抛出异常信息后中断执行:

#!/bin/bash

set -e

nonexistentCommand

echo "end line"

改配置前输出:

$ ./test.sh 
./test.sh: line 4: nonexistentCommand: command not found
end line

改配置后输出:

$ ./test.sh
./test.sh: line 5: nonexistentCommand: command not found

但是如果当我们即修改了配置,又做了容错处理,会导致无法中断 Script:

#!/bin/bash

set -e

nonexistentCommand || echo "error"

echo "end line"

输出:

$ ./test.sh 
./test.sh: line 5: nonexistentCommand: command not found
error
end line

还有一种情况,希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e,该命令执行结束后,再重新打开set -e

set +e
command1
command2
set -e
4、set -o pipefail:解决管道命令中的特殊情况。

set -e 有一个例外情况:不适用管道命令。

#!/bin/bash
set -e

nonexistentCommand | echo "after error"
echo "end line"

输出:

$ ./test.sh 
./test.sh: line 4: nonexistentCommand: command not found
after error
end line

依旧打印了最后一行的输出,这是因为,只要管道符的最后一个命令不执行失败,返回的结果总是成功。

解决方式:set -eo

#!/bin/bash
set -e
set -o pipefail

nonexistentCommand | echo "after error"
echo "end line"

输出:

$ ./test.sh 
after error
./test.sh: line 5: nonexistentCommand: command not found
5、set -E:修复set -e引起的函数内的异常信息不会被trap命令捕获的问题。

trap 命令先理解为捕获 Script 发生的错误即可,后面会对该命令进行说明。

#!/bin/bash
set -e

trap "echo ERR trap fired!" ERR

func()
{
  # 执行一个不存在的命令
  nonexistentCommand
}

func

输出:

$ ./test.sh 
./test.sh: line 9: nonexistentCommand: command not found

非函数内引发的异常是可以被正常捕获的:

#!/bin/bash
set -e

trap "echo ERR trap fired!" ERR

nonexistentCommand

func

输出:

$ ./test.sh 
./test.sh: line 6: nonexistentCommand: command not found
ERR trap fired!
6、其他参数
set命令还有一些其他参数。

set -n:等同于set -o noexec,不运行命令,只检查语法是否正确。
set -f:等同于set -o noglob,表示不对通配符进行文件名扩展。
set -v:等同于set -o verbose,表示打印 Shell 接收到的每一行输入。
set -o noclobber:防止使用重定向运算符>覆盖已经存在的文件。

上面的-f和-v参数,可以分别使用set +f、set +v关闭。

set 命令总结

上面重点介绍的set命令的几个参数,一般都放在一起使用:

# 写法一
set -Eeuxo pipefail

# 写法二
set -Eeux
set -o pipefail

这两种写法建议放在所有 Bash 脚本的头部。

另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数:

$ bash -euxo pipefail script.sh

根据个人习惯,也可以修改一下PS4环境变量:export PS4='${LINENO}: '