为 shell 脚本包含错误清除逻辑的最佳方法是什么?
具体来说,我有一个脚本可以执行以下操作:
mount a x
mount b y
setup_thing
mount c z
do_something
umount z
cleanup_thing
umount y
umount x
任何安装座及其do_something
本身都可能会失败。例如,如果mount c z
失败,我希望脚本在退出之前卸载已成功安装的安装。
我不想多次重复清理代码,也不想将所有内容包装到 if-nest 中(因为如果添加额外的安装,则必须重新缩进所有内容)。
有没有办法创建一个finally-stack或其他东西,所以上面的内容可以写成:
set -e
mount a x && finally umount x
mount b y && finally umount y
setup_thing && finally cleanup_thing
mount c z && finally umount z
do_something
这个想法是,一旦注册了“finally”命令,它将在退出、通过或失败时执行(以相反的顺序)该命令 - 但未成功设置的内容不会被清理(因为如果安装失败,清理可能无法安全执行)。
或者换句话说,如果一切成功,那么它将按照umount z
、cleanup_thing
、umount y
、的顺序执行——如果仅失败umount x
则相同。do_something
但如果mount b y
失败则只会执行umount x
.
(无论它需要以 0 或非 0 适当退出,尽管我不需要在失败时保留确切的退出代码。)
我知道有一个trap
内置命令可以让您在退出时运行命令,但它只支持一个命令并每次都会替换它。有没有办法将其扩展到像上面这样的堆栈中?或者其他一些干净的方法来做到这一点?
在 Ye Olde C 代码的错误处理模式中,您可能会实现类似的效果,x || goto cleanup_before_x
但当然没有 goto。
理想情况下,它可以在 (da)sh 中工作,尽管如果这可以简化事情,我也可以要求 bash 。 (也许使用数组?)
答案1
trap
一种常见的技术是在完成后使用 a撤消所有内容。它需要是幂等的,即如果某些步骤无法完成,它应该优雅地失败。
#!/bin/bash
clean_up=false
errorhandler () {
umount z || true
$clean_up && cleanup_thing
umount y || true
umount x
}
trap errorhandler ERR EXIT
mount a x
mount b y
setup_thing
clean_up=true
mount c z
do_something
请注意,陷阱也会被触发,EXIT
因此它也会在脚本正常终止之前执行;所以你根本不需要明确地清理。
我相信伪ERR
信号是 Bash 扩展。所以这在 Ash/Dash/legacy Bourne shell 等中不起作用。
答案2
修改为单独处理预先存在的挂载点。假设代码采用这种格式,其中可能导致问题的行每行有一个命令,即mount
或umount
:
mount a x
mount b y
setup_thing
mount c z
do_something
umount z
cleanup_thing
umount y
umount x
这个拼凑可能会起作用...将此代码复制到脚本的第二行:
mount | cut -d' ' -f3 | sed 's/.*/^u\?mount [^[:space:]]\* &$/' |
grep -v -f - $0 | sed 2d | exec sh -s -- "$@"
它(理论上)是如何工作的:
- 使用
mount
和cut
来创建预先存在的挂载点的列表,不要管它。 - 用于创建与使用任何这些安装点的模式或命令相匹配的模式
sed
列表。grep
mount
umount
- 用于
grep -v
搜索当前脚本中的任何行不匹配前面的模式列表grep
。留下所有适合运行的代码。 - 用于
sed
删除第二行,以防止递归。 - 用于
exec sh -s -- "$@"
仅运行该代码以及任何命令行参数。超过该exec
线的任何内容都不会运行。
exec
通过更改cat #
并检查输出来预先测试。如果看起来不错,就放exec
回去。
答案3
就我个人而言,我会选择嵌套if then
,因为它更容易阅读和维护。但是,如果您有大量的嵌套级别,您可以尝试类似的方法(我已使用前缀echo
进行测试):
#!/bin/bash
run1(){ echo mount a x;}
run2(){ echo mount b y;}
run3(){ echo setup_thing;}
run4(){ echo mount c z;}
run5(){ echo do_something;}
undo5(){ :;}
undo4(){ echo umount z;}
undo3(){ echo cleanup_thing;}
undo2(){ echo umount y;}
undo1(){ echo umount x;}
for i in {1..5}
do run$i
code=$?
[ $code != 0 ] && break
done
let i=i-1
while [ $i -gt 0 ]
do undo$i
let i=i-1
done
exit $code
我按照示例的顺序保留了运行和撤消函数,但是您可以通过将它们彼此靠近来获得好处:
run1(){ mount a x;}
undo1(){ umount x;}
run2(){ mount b y;}
undo2(){ umount y;}
run3(){ setup_thing;}
undo3(){ cleanup_thing;}
...
您可以将运行函数命名为任何名称,然后按照您想要的顺序列出名称,而不是将函数编号为 1,2,3...。为撤消函数添加一致的前缀以使其更容易:
mnta(){ ... }
undomnta(){ ... }
mntb(){ ... }
undomntb(){ ... }
order='mnta mntb ...' toundo=
for i in $order
do $i
code=$?
[ $code != 0 ] && break
toundo="undo$i $toundo"
done
for i in $toundo
do $i
done
或者您确实可以使用trap
,但只需使用 进行一次设置trap mytrap exit
,并使用全局变量来保存要清理的内容并在每个步骤中添加到其中:clean="1 $clean"
。然后该函数mytrap
将仅遍历 中的值$clean
。