0%

编写高质量的bash脚本

Linux bash 是提高效率的重要方法。本文介绍几个在编写bash脚本中有用的小技巧,能够有效提升脚本运行的健壮性和可维护性。

日志和输出

日志是debug的重要手段。丰富的日志能够帮助我们快速定位问题所在,从而高效解决问题。

打印命令

set -x指令能够让脚本向标准输出打印每一个执行的命令,这样一旦出错,可以通过打印的输出,找到错误执行的命令。

例子:

执行脚本:

print_trace.sh
1
2
set -x
echo `expr 10 + 20 `

输出为:

print_trace.out
1
2
3
+ expr 10 + 20
+ echo 30
30

若想关闭这个功能,只需使用set +x即可:

执行脚本:

disable_trace.sh
1
2
3
4
set -x
echo `expr 10 + 20 `
set +x
echo `expr 10 + 20 `

输出为:

print_trace.out
1
2
3
4
+ expr 10 + 20
+ echo 30
30
30

以上两个例子来自https://stackoverflow.com/a/36277661/5634636。

重定向set -x的输出

我们希望将打印出来的命令重定向到日志文件中,以便留存和之后查看。此时可以使用exec命令,重定向脚本的输出:

redirect_output.sh bash
1
2
3

exec &> >(tee -a "${terminal_output_dir}/terminal.out")

tee -a的作用是同时输出到文件和标准输出,这样我们可以在terminal看到输出,同时输出也会被保存到文件中。

本方法来自https://unix.stackexchange.com/a/145654/283657。

有颜色的输出

在执行危险操作时,我们希望有更醒目的警示来提示用户。例如在删除文件时,我们希望用红色的字体来提示用户。在执行成功时,我们通常使用绿色的字体。如何打印带颜色的字体?

本方法来自https://stackoverflow.com/a/20983251/5634636。

首先预先定义三个变量:

1
2
3
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
RESET=$(tput sgr0)

然后就可以自由地在字符串中使用了:

1
echo "${RED}ERROR! SOMETHING WRONG!${RESET}"

实践:在删除危险文件前警示用户:

remove_dangerous_files.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

# set color
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
RESET=$(tput sgr0)

read -p "Are you sure to remove files?${RESET}" -n 1 -r
echo # move to a new line
if [[ $REPLY =~ ^[Yy]$ ]]
then
# do dangerous stuff
rm -rf ./output_files
fi

更健壮的脚本

bash有的行为和可能会带来意想不到的结果,甚至带来灾难性的后果。我们可以用一些方法来预先阻止这样令人意外的行为。

出错即停止

和大多数程序语言报错即退出不同,在执行bash脚本时,如果某行命令报错,bash会接着执行之后的指令。如果没有考虑到这一点,可能会带来非预期的效果。例如我们有一个简单的删除日志文件的脚本:

clear_log.sh
1
2
cd ./logs
rm -rf *

这段代码中,如果./logs目录因为某种原因不存在,cd ./logs就会报错,然后继续执行rm -rf *命令,这是十分危险的,有可能将整个路径都删除。

为了预防这样的错误,我们令脚本在任意一行命令出错后即停止,不再接着执行接下来的命令。只需要加一行set -e即可:

save_clear_log.sh
1
2
3
4
set -e

cd ./logs
rm -rf *

save_clear_log.sh中,如果cd ./logs报错,该程序会退出,删除的命令将不会被执行。

禁止使用未定义变量

在Linux中,使用未定义的变量是被允许的,如果一个变量未被定义,则自动替换为空字符串。这在有时候是危险的:

clear_log.sh
1
2
3
4
set -e

cd ${log_path}
rm -rf *

此时如果${log_path}变量未定义,就会执行cd,当前目录就会跳转至用户home目录(即等价于cd ~)。这样这个脚本会将用户home目录中所有文件全部删除。

为了预防这样的错误,我们可以禁止将未定义变量展开为空字符串:

1
set -u

脚本可以写为

clear_log.sh
1
2
3
4
set -eu

cd ${log_path}
rm -rf *

此时若${log_path}变量未定义,cd ${log_path}就会报错,同时因为set -e的缘故,整个脚本将会退出,不再执行删除命令。

set -eu 等价于

1
2
set -e
set -u

确保本地git仓库和远端保持一致

在运行代码时,特别是在本地编写代码在服务器运行的情况,如果在本地修改好,却忘记在服务器端pull下来,就会运行非预期的代码。我们可以在脚本运行时首先检查本地代码是否与远端一致:

check_git_status.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# check git status
# see https://stackoverflow.com/questions/3258243/check-if-pull-needed-in-git
# check if the dir is git repo
# see https://stackoverflow.com/a/38088814/5634636
if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]; then
echo 'Checking git status...'
git fetch
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse "@{u}")

if [ $LOCAL = $REMOTE ]; then
echo "${GREEN}Git is up-to-date${RESET}"
else
echo "${GREEN}Git is NOT up-to-date! Pull from remote first!${RESET}"
exit 1
fi
else
echo "Not in git repo."
fi

Further Reading