“Exit Trap” 让你的 Bash 脚本更稳固可靠
bash 脚本
![](/data/attachment/album/201805/14/150056qniv8u75vo7qmouz.jpg)


有个简单实用的技巧可以让你的 bash 脚本更稳健 -- 确保总是执行必要的收尾工作，哪怕是在发生异常的时候。要做到这一点，秘诀就是 bash 提供的一个叫做 EXIT 的伪信号，你可以 [trap](http://www.gnu.org/software/bash/manual/bashref.html#index-trap) 它，当脚本因为任何原因退出时，相应的命令或函数就会执行。我们来看看它是如何工作的。


基本的代码结构看起来像这样：



```
#!/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` 目录里下载、生成、操作中间或临时数据了。<sup> <a href="http://redsymbol.net/articles/bash-exit-traps/#footnote-1">  注1 </a></sup>



```
# 下载所有版本的 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) 上工作，要在脚本中创建一个镜像。


（名词解释: 在亚马逊云上的运行的服务器叫“[实例](http://aws.amazon.com/ec2/)”。实例从<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 服务器）已经开始运行 <sup> <a href="http://redsymbol.net/articles/bash-exit-traps/#footnote-2">  注2 </a> 。接下来你可以做任何事情：在实例中安装软件，修改配置文件等，然后为最终版本创建一个镜像。实例会在脚本结束时被删除</sup> -- 即使脚本因错误而提前退出。（请确保实例创建成功后再运行业务代码。）


### 更多应用


这篇文章只讲了些皮毛。我已经使用这个 bash 技巧很多年了，现在还能不时发现一些有趣的用法。你也可以把这个方法应用到你自己的场景中，从而提升你的 bash 脚本的可靠性。


### 尾注


* 注1. `mktemp` 的选项 `-t` 在 Linux 上是可选的，在 OS X 上是必需的。带上此选项可以让你的脚本有更好的可移植性。
* 注2. 如果只是为了获取实例 ID ，我们不用创建文件，直接写成 `instance=$(ec2-run-instances "$ami" | grep '^INSTANCE' | cut -f 2)` 就可以。但把输出写入文件可以记录更多有用信息，便于调试 ，代码可读性也更强。


作者简介：美国加利福尼亚旧金山的作家，软件工程师，企业家。[Powerful Python](https://www.amazon.com/d/0692878971) 的作者，他的 [blog](https://powerfulpython.com/blog/)。




---


via: <http://redsymbol.net/articles/bash-exit-traps/>


作者：[aaron maxwell](http://redsymbol.net/) 译者：[Dotcra](https://github.com/Dotcra) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


OK
# How "Exit Traps" Can Make Your Bash Scripts Way More Robust And Reliable

There is a simple, useful idiom to make your bash scripts more
robust - ensuring they always perform necessary cleanup
operations, even when something unexpected goes wrong. The secret
sauce is a pseudo-signal provided by bash, called EXIT, that you
can [trap](http://www.gnu.org/software/bash/manual/bashref.html#index-trap);
commands or functions trapped on it will execute when the script
exits for any reason. Let's see how this works.

The basic code structure is like this:

- #!/bin/bash
- function finish {
- # Your cleanup code here
- }
- trap finish EXIT


You place any code that you want to be certain to run in this "finish" function. A good common example: creating a temporary scratch directory, then deleting it after.

- #!/bin/bash
- scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
- function finish {
- rm -rf "$scratch"
- }
- trap finish EXIT


You can then download, generate, slice and dice intermediate or
temporary files to the `$scratch`

directory to your heart's
content. [[1]](#footnote-1)

- # Download every linux kernel ever.... FOR SCIENCE!
- 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
- # magically merge them into some frankenstein kernel ...
- # That done, copy it to a destination
- cp "$scratch/frankenstein-linux.tar.bz2" "$1"
- # Here at script end, the scratch directory is erased automatically


Compare this to how you'd remove the scratch directory without the trap:

- #!/bin/bash
- # DON'T DO THIS!
- scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
-
- # Insert dozens or hundreds of lines of code here...
-
- # All done, now remove the directory before we exit
- rm -rf "$scratch"


What's wrong with this? Plenty:

- If some error causes the script to exit prematurely, the scratch directory and its contents don't get deleted. This is a resource leak, and may have security implications too.
- If the script is designed to exit before the end, you must manually copy 'n paste the rm command at each exit point.
- There are maintainability problems as well. If you later add a new in-script exit, it's easy to forget to include the removal - potentially creating mysterious heisenleaks.

## Keeping Services Up, No Matter What

Another scenario: Imagine you are automating some system administration task, requiring you to temporarily stop a server... and you want to be dead certain it starts again at the end, even if there is some runtime error. Then the pattern is:

- function finish {
- # re-start service
- sudo /etc/init.d/something start
- }
- trap finish EXIT
- sudo /etc/init.d/something stop
- # Do the work...
-
- # Allow the script to end and the trapped finish function to start the
- # daemon back up.


A concrete example: suppose you have MongoDB running on an Ubuntu server, and want a cronned script to temporarily stop the process for some regular maintenance task. The way to handle it is:

- function finish {
- # re-start service
- sudo service mongdb start
- }
- trap finish EXIT
- # Stop the mongod instance
- sudo service mongdb stop
- # (If mongod is configured to fork, e.g. as part of a replica set, you
- # may instead need to do "sudo killall --wait /usr/bin/mongod".)


## Capping Expensive Resources

There is another situation where the exit trap is very useful: if your script initiates an expensive resource, needed only while the script is executing, and you want to make certain it releases that resource once it's done. For example, suppose you are working with Amazon Web Services (AWS), and want a script that creates a new image.

(If you're not familar with this: Servers running on the Amazon
cloud are called "[instances](http://aws.amazon.com/ec2/)". Instances are
launched from Amazon Machine Images, a.k.a. "AMIs" or "images". AMIs
are kind of like a snapshot of a server at a specific moment
in time.)

A common pattern for creating custom AMIs looks like:

- Run an instance (i.e. start a server) from some base AMI.
- Make some modifications to it, perhaps by copying a script over and then executing it.
- Create a new image from this now-modified instance.
- Terminate the running instance, which you no longer need.

That last step is **really important**. If your script fails to
terminate the instance, it will keep running and accruing charges to
your account. (In the worst case, you won't notice until the end of
the month, when your bill is way higher than you expect. Believe
me, that's no fun!)

If our AMI-creation is encapsulated in a script, we can set an exit trap to destroy the instance. Let's rely on the EC2 command line tools:

- #!/bin/bash
- # define the base AMI ID somehow
- ami=$1
- # Store the temporary instance ID here
- instance=''
- # While we are at it, let me show you another use for a scratch directory.
- scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
- function finish {
- if [ -n "$instance" ]; then
- ec2-terminate-instances "$instance"
- fi
- rm -rf "$scratch"
- }
- trap finish EXIT
- # This line runs the instance, and stores the program output (which
- # shows the instance ID) in a file in the scratch directory.
- ec2-run-instances "$ami" > "$scratch/run-instance"
- # Now extract the instance ID.
- instance=$(grep '^INSTANCE' "$scratch/run-instance" | cut -f 2)


At this point in the script, the instance (EC2 server) is running
[[2]](#footnote-2). You can do
whatever you like: install software on the instance, modify its
configuration programatically, et cetera, finally creating an image
from the final version. The instance will be terminated for you when
the script exits - even if some uncaught error causes it to exit
early. (Just make sure to block until the image creation process finishes.)

## Plenty Of Uses

I believe what I've covered in this article only scratches the surface; having used this bash pattern for years, I still find new interesting and fun ways to apply it. You will probably discover your own situations where it will help make your bash scripts more reliable.

## Footnotes

- The -t option to mktemp is optional on Linux, but needed on OS X. Make your scripts using this idiom more portable by including this option.
-
When getting the instance ID, instead of using the scratch file, we could just say:

`instance=$(ec2-run-instances "$ami" | grep '^INSTANCE' | cut -f 2)`

. But using the scratch file makes the code a bit more readable, leaves us with better logging for debugging, and makes it easy to capture other info from ec2-run-instances's output if we wish.