shell 脚本的错误清理

shell 脚本的错误清理

为 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 zcleanup_thingumount 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

修改为单独处理预先存在的挂载点。假设代码采用这种格式,其中可能导致问题的行每行有一个命令,即mountumount

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 -- "$@"

它(理论上)是如何工作的:

  1. 使用mountcut来创建预先存在的挂载点的列表,不要管它。
  2. 用于创建与使用任何这些安装点的模式或命令相匹配的模式sed列表。grepmountumount
  3. 用于grep -v搜索当前脚本中的任何行匹配前面的模式列表grep。留下所有适合运行的代码。
  4. 用于sed删除第二行,以防止递归。
  5. 用于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

相关内容