ズッキーニのプログラミング実験場

プログラミング + アカデミック + 何か面白いこと。 記載されているものは基本的に私が所属する団体とは関係がありません。

   Oct 19

[Shell][Bash]BashでTry Catch Finally

by zuqqhi2 at 2014年10月19日
Pocket

背景

最近、業務でのプログラミングにて、
シェルで書いた方が動くものが楽に書ける場合が多々ある。
ただシェルだとエラーハンドリング周りが若干面倒なので、
Try-Catch-Finallyできたらいいなと思って調べたら、
それができたのでメモしてみた。

環境

  • OS
    • Linux 2.6.32-279.el6.x86_64 #1 SMP Fri Jun 22 12:19:21 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux

内容

Try-Catch-Finally

では実際に
Try-Catch-Finallyをbashで実現してみる。
これには「set」コマンドと「trap」コマンドを利用する。
それぞれ、ここここを参考にした。

実際のソースコードは以下。

#!/bin/bash
function main() {
        trap catch ERR
        echo "[INFO]Main"
        return 0
}

function catch() {
        echo "[ERROR]Fail"
}
function finally() {
        echo "[INFO]Finish"
}

# Entry Point
set -eu
trap finally EXIT
main

「set -eu」と「trap finally EXIT」と「trap catch ERR」を設定することで、
コマンドがエラーするとcatch関数に、終了するとfinally関数に処理が移るようにした。
「trap catch ERR」は関数ごとに設定しないと動いてくれないので、
main関数に記述してある。

以下のソースコードを中身とするmain.shという名前のファイルを作成して実行すると次のような結果となる。

$ ./main.sh
[INFO]Main
[INFO]Finish

それでは次のように無理やり動かないコマンドをmain関数の中に入れてみる。

#!/bin/bash
function main() {
        trap catch ERR
        echo "[INFO]Main"
        diff test
        return 0
}

function catch() {
        echo "[ERROR]Fail"
}
function finally() {
        echo "[INFO]Finish"
}

# Entry Point
set -eu
trap finally EXIT
main

diffコマンドの引数が一つ足りないため、必ずエラーになる。
それでは、実行してみよう。

$ ./main.sh
[INFO]Main
diff: missing operand after `test' 
diff: Try `diff --help' for more information.
[ERROR]Fail
[INFO]Finish

catch関数の部分が実行されている。

Retry

Try-Catch-Finallyの形式にすることで、
他のシステムとの連携タイミングや通信に一時的な問題が発生して
スクリプトが動かない場合、
簡単に全体のリトライ処理が書ける。

#!/bin/bash
function main() {
        trap catch ERR
        echo "[INFO]Main"
        return 0
}

function catch() {
        echo "[ERROR]Fail"
        echo "[INFO]Retry"
        main

}
function finally() {
        echo "[INFO]Finish"
}

# Entry Point
set -eu
trap catch ERR
trap finally EXIT
main

ただ、catch関数でmain関数を呼んだだけである。

先ほどと同じように、
必ず失敗するコマンドを入れて実行してみる。

#!/bin/bash
function main() {
        trap catch ERR
        echo "[INFO]Main"
        diff test
        return 0
}

function catch() {
        echo "[ERROR]Fail"
        echo "[INFO]Retry"
        main

}
function finally() {
        echo "[INFO]Finish"
}

# Entry Point
set -eu
trap catch ERR
trap finally EXIT
main

実行結果は以下。

$ ./main.sh
[INFO]Main
diff: missing operand after `test'
diff: Try `diff --help' for more information.
[ERROR]Fail
[INFO]Retry
[INFO]Main
diff: missing operand after `test'
diff: Try `diff --help' for more information.
[INFO]Finish

ちゃんとリトライ処理が走っている。
他にも、アラートメールを飛ばしたりするのも良い。

unit test

この形でスクリプトを書くとロジックの分離がしやすい気がするので、
自分は別ファイルでロジックを書いて、
shunit2で単体テストとカバレッジを測っている。

具体的には以下のように書いている。

main.sh

#!/bin/bash
function main() {
        trap catch ERR
        echo "[INFO]Main"
        lib_main
        return 0
}
function catch() {
        echo "[ERROR]Retry"
        main
}
function finally() {
        if [ ! $? -eq 0 ]; then
                TITLE="error"
                TO=abc@example.com
                FROM=abc@example.com
                echo -e "Please check log file." | mail -s "$TITLE" -r "$FROM" "$TO"
                echo "[ERROR]Alert mail is sent to $TO ."
        fi

        echo "[INFO]Finish"
}

# Entry Point
SOURCE_PATH="$(cd $(dirname $0);pwd)"
source $SOURCE_PATH/lib.sh
set -eu
trap finally EXIT
main

lib.sh

#!/bin/bash
function lib_main() {
        echo "[INFO]Lib Main"
        touch "test.log"
        return 0
}

test.sh

#!/bin/bash
function oneTimeSetUp() {
  echo "[INFO]Setup"
  source ./lib.sh
}
function testLibMain() {
  lib_main
  test -e test.log
  ${_ASSERT_EQUALS_} "TestFileExists" $? 0
}
. "path/to/shunit2/shunit2"

main.shを実行すると以下のようになる。

$ ./main.sh
[INFO]Main
[INFO]Lib Main
[INFO]Finish

test.shを実行すると以下のようになる。

[INFO]Setup
testLibMain
[INFO]Lib Main
 
Ran 1 test.
 
 
OK

まとめ

Bashでは「set」コマンドと「trap」コマンドを使うことで、
Try-Catch-Finallyの機構を作ることができ、
エラーハンドリング・リトライ処理に利用できて便利。

デスクの横に置いておいて、困ったときにパラパラめくる辞書のように使えるので割と便利。

Related Posts

Pocket

You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.