シェルスクリプトのクリーンアップエラー

シェルスクリプトのクリーンアップエラー

シェルスクリプトのエラークリーンアップロジックを含む最良の方法は何ですか?

具体的には、以下を実行するスクリプトがあります。

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でラップしたくありません(マウントを追加すると、すべてを再インデントする必要があるためです)。

上記の内容を次のように作成できるように最終スタックなどを生成する方法はありますか?

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_thingumount yumount xdo_somethingmount b yumount x

(失敗した場合は正確な終了コードを維持する必要はありませんが、ゼロまたはゼロ以外の値で適切に終了する必要があるかどうかです。)

終了時にコマンドを実行できる組み込み機能があることはわかっていますが、trapこれは1つのコマンドのみをサポートし、毎回それを置き換えます。これを上記のようなスタックに拡張する方法はありますか?それともこれを行う他のきれいな方法はありますか?

Ye Olde Cコードのエラー処理パターンでも同様の結果が得られますが、x || goto cleanup_before_xもちろんgotoはありません。

理想的には(da)shで動作しますが、単純化されている場合はbashも要求できます。 (たぶん配列を使用していますか?)

答え1

trap一般的な手法は、操作が完了したらすべての項目を元に戻すことです。等級でなければなりません。つまり、一部の手順を完了できない場合は、正常に失敗する必要があります。

#!/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シェルなどでは機能しません。

答え2

既存のマウントポイントを別々に処理するように修正されました。コードがこの形式であると仮定すると、問題を引き起こす可能性がある行には1行に1つのコマンドがあります。つまり、mount次のようになりますumount

mount a x
mount b y
setup_thing
mount c z
do_something
umount z
cleanup_thing
umount y
umount x

このパッチワークが機能することもあります。このコードをスクリプトの2行目にコピーします。

mount | cut -d' ' -f3 | sed 's/.*/^u\?mount [^[:space:]]\* &$/' | 
  grep -v -f - $0 | sed 2d | exec sh -s -- "$@"

仕組み(理論的には):

  1. 既存のマウントポイントのリストを作成してそのままにするには、mountおよびを使用します。cut
  2. これらのマウントポイントを使用するパターンまたはコマンドに一致するパターンのリストを作成するために使用されますsedgrepmountumount
  3. grep -v現在のスクリプトからすべての行を検索するために使用されます。いいえ前のパターンのリストと一致しますgrep。実行に適したすべてのコードをそのままにしてください。
  4. sed再帰を防ぐために2番目の行を削除するために使用されます。
  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

関連情報