TranslateProject/published/201805/20180101 How Exit Traps Can Make Your Bash Scripts Way More Robust And Reliable.md
2018-06-03 22:19:23 +08:00

165 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

“Exit Trap” 让你的 Bash 脚本更稳固可靠
============================================================
有个简单实用的技巧可以让你的 bash 脚本更稳健 -- 确保总是执行必要的收尾工作,哪怕是在发生异常的时候。要做到这一点,秘诀就是 bash 提供的一个叫做 EXIT 的伪信号,你可以 [trap][1] 它,当脚本因为任何原因退出时,相应的命令或函数就会执行。我们来看看它是如何工作的。
基本的代码结构看起来像这样:
```
#!/bin/bash
function finish {
# 你的收尾代码
}
trap finish EXIT
```
你可以把任何你觉得务必要运行的代码放在这个 `finish` 函数里。一个很好的例子是:创建一个临时目录,事后再删除它。
```
#!/bin/bash
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
rm -rf "$scratch"
}
trap finish EXIT
```
这样,在你的核心代码中,你就可以在这个 `$scratch` 目录里下载、生成、操作中间或临时数据了。^[注1][2]
```
# 下载所有版本的 linux 内核…… 为了科学研究!
for major in {1..4}; do
for minor in {0..99}; do
for patchlevel in {0..99}; do
tarball="linux-${major}-${minor}-${patchlevel}.tar.bz2"
curl -q "http://kernel.org/path/to/$tarball" -o "$scratch/$tarball" || true
if [ -f "$scratch/$tarball" ]; then
tar jxf "$scratch/$tarball"
fi
done
done
done
# 整合成单个文件
# 复制到目标位置
cp "$scratch/frankenstein-linux.tar.bz2" "$1"
# 脚本结束, scratch 目录自动被删除
```
比较一下如果不用 `trap` ,你是怎么删除 `scratch` 目录的:
```
#!/bin/bash
# 别这样做!
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
# 在这里插入你的几十上百行代码
# 都搞定了,退出之前把目录删除
rm -rf "$scratch"
```
这有什么问题么?很多:
* 如果运行出错导致脚本提前退出, `scratch` 目录及里面的内容不会被删除。这会导致资料泄漏,可能引发安全问题。
* 如果这个脚本的设计初衷就是在脚本末尾以前退出,那么你必须手动复制粘贴 `rm` 命令到每一个出口。
* 这也给维护带来了麻烦。如果今后在脚本某处添加了一个 `exit` ,你很可能就忘了加上删除操作 -- 从而制造潜在的安全漏洞。
### 无论如何,服务要在线
另外一个场景: 想象一下你正在运行一些自动化系统运维任务,要临时关闭一项服务,最后这项服务需要重启,而且要万无一失,即使脚本运行出错。那么你可以这样做:
```
function finish {
# 重启服务
sudo /etc/init.d/something start
}
trap finish EXIT
sudo /etc/init.d/something stop
# 主要任务代码
# 脚本结束,执行 finish 函数重启服务
```
一个具体的实例:比如 Ubuntu 服务器上运行着 MongoDB ,你要为 crond 写一个脚本来临时关闭服务并做一些日常维护工作。你应该这样写:
```
function finish {
# 重启服务
sudo service mongdb start
}
trap finish EXIT
# 关闭 mongod 服务
sudo service mongdb stop
# (如果 mongod 配置了 fork ,比如 replica set ,你可能需要执行 “sudo killall --wait /usr/bin/mongod”
```
### 控制开销
有一种情况特别能体现 EXIT `trap` 的价值:如果你的脚本运行过程中需要初始化一下成本高昂的资源,结束时要确保把它们释放掉。比如你在 AWS (Amazon Web Services) 上工作,要在脚本中创建一个镜像。
(名词解释: 在亚马逊云上的运行的服务器叫“[实例][3]”。实例从<ruby>亚马逊机器镜像<rt>Amazon Machine Image</rt></ruby>创建而来,通常被称为 “AMI” 或 “镜像” 。AMI 相当于某个特殊时间点的服务器快照。)
我们可以这样创建一个自定义的 AMI
1. 基于一个基准 AMI 运行一个实例(例如,启动一个服务器)。
2. 在实例中手动或运行脚本来做一些修改。
3. 用修改后的实例创建一个镜像。
4. 如果不再需要这个实例,可以将其删除。
最后一步**相当重要**。如果你的脚本没有把实例删除掉,它会一直运行并计费。(到月底你的账单让你大跌眼镜时,恐怕哭都来不及了!)
如果把 AMI 的创建封装在脚本里,我们就可以利用 `trap` EXIT 来删除实例了。我们还可以用上 EC2 的命令行工具:
```
#!/bin/bash
# 定义基准 AMI 的 ID
ami=$1
# 保存临时实例的 ID
instance=''
# 作为 IT 人,让我们看看 scratch 目录的另类用法
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
if [ -n "$instance" ]; then
ec2-terminate-instances "$instance"
fi
rm -rf "$scratch"
}
trap finish EXIT
# 创建实例,将输出(包含实例 ID )保存到 scratch 目录下的文件里
ec2-run-instances "$ami" > "$scratch/run-instance"
# 提取实例 ID
instance=$(grep '^INSTANCE' "$scratch/run-instance" | cut -f 2)
```
脚本执行到这里实例EC2 服务器)已经开始运行 ^[注2][4]。接下来你可以做任何事情:在实例中安装软件,修改配置文件等,然后为最终版本创建一个镜像。实例会在脚本结束时被删除 -- 即使脚本因错误而提前退出。(请确保实例创建成功后再运行业务代码。)
### 更多应用
这篇文章只讲了些皮毛。我已经使用这个 bash 技巧很多年了,现在还能不时发现一些有趣的用法。你也可以把这个方法应用到你自己的场景中,从而提升你的 bash 脚本的可靠性。
### 尾注
- 注1. `mktemp` 的选项 `-t` 在 Linux 上是可选的,在 OS X 上是必需的。带上此选项可以让你的脚本有更好的可移植性。
- 注2. 如果只是为了获取实例 ID ,我们不用创建文件,直接写成 `instance=$(ec2-run-instances "$ami" | grep '^INSTANCE' | cut -f 2)` 就可以。但把输出写入文件可以记录更多有用信息,便于调试 ,代码可读性也更强。
作者简介:美国加利福尼亚旧金山的作家,软件工程师,企业家。[Powerful Python][5] 的作者,他的 [blog][6]。
--------------------------------------------------------------------------------
via: http://redsymbol.net/articles/bash-exit-traps/
作者:[aaron maxwell][a]
译者:[Dotcra](https://github.com/Dotcra)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:http://redsymbol.net/
[1]:http://www.gnu.org/software/bash/manual/bashref.html#index-trap
[2]:http://redsymbol.net/articles/bash-exit-traps/#footnote-1
[3]:http://aws.amazon.com/ec2/
[4]:http://redsymbol.net/articles/bash-exit-traps/#footnote-2
[5]:https://www.amazon.com/d/0692878971
[6]:https://powerfulpython.com/blog/