1. 概要

スクリプトを実行するとき、エラーなしでスクリプトが終了することは期待できません。 私たちはこれを予測し、これらの予期しないシナリオに対応するためのコードを記述します。 また、エラーが発生した場合は、ユーザーにメッセージを表示してスクリプトを終了することをお勧めします。 それに加えて、後で問題をデバッグするのに十分なログを印刷します。

このチュートリアルでは、エラーを発生させてスクリプトを終了するさまざまな方法を確認します。

2. set-eオプションの使用

Bashには、シェルにさまざまなオプションを設定するためのこの組み込みのsetコマンドがあります。 多くのオプションの1つは、errexitです。 このオプションを設定するだけで、いずれかのコマンドがゼロ以外のステータスを返したときにスクリプトを終了するのに役立ちます。

例を見てみましょう:

$ cat set.sh 
#!/bin/bash
set -e
ls
ls -l nofolder
ls -l
$ ./set.sh 
set.sh
ls: cannot access 'nofolder': No such file or directory
$

上記のスクリプトでは、set-eコマンドを使用してerrexitオプションを有効にしました。 結果から、最初のlsコマンドが実行されたことがわかります。 2つ目は、 nofolder が存在しないため、フォルダにアクセスできないというエラーが表示されます。 そして、最後の ls コマンドについては、結果が表示されません。 これは、前のコマンドでエラーが発生したときにスクリプトが終了したことを意味します。

ご覧のとおり、1行のコードを使用して、必要なことを達成することができました。

これは実装が簡単ですが、注意すべき点がいくつかあります。 これらのケースのいくつかを見てみましょう。

2.1. パイプラインの操作

まず、パイプラインの戻りステータスは、最後のコマンドの終了ステータスです。 したがって、パイプラインに複数のコマンドがあり、最後のコマンド以外はすべて失敗した場合、成功として扱われます。したがって、スクリプトは実行を停止しません。

例を挙げてこれを見てみましょう。

$ cat set.sh 
#!/bin/bash
set -e
ls
ls | ls -l nofolder | ls
ls -l
$ ./set.sh
set.sh
ls: cannot access 'nofolder': No such file or directory
-rwxrwxr-x 1 bluelake bluelake 306 Mar 28 13:00 set.sh
$

上で見たように、パイプラインの途中で失敗したlsコマンドがあります。 エラーを出力しても、スクリプトはそこで停止しません。結果から、最後のlsコマンドまで実行されたことがわかります。

幸いなことに、この問題には簡単な解決策があります。

次に、これを解決する方法を見てみましょう。

$ cat set.sh 
#!/bin/bash
set -e
set -o pipefail
ls
ls | ls -l nofolder | ls
ls -l
$ ./set.sh
set.sh
ls: cannot access 'nofolder': No such file or directory
$

上記のように、 setコマンドを使用して別のオプションpipefailを追加しました。したがって、パイプラインの終了ステータスは、ゼロ以外のステータスで終了する最後のコマンドの終了ステータスになります。 その結果、パイプラインでエラーが発生したときにスクリプトの実行が停止しました。

2.2. その他の問題

これに加えて、このsetコマンドを使用する際に注意すべきシナリオがいくつかあります。

まず、 ifまたはwhileステートメントでラップされたコマンドは、終了条件をトリガーしません。

第二に、 &&または||のコマンドオペレーターはこのerrexitオプションの影響を受けません

これらの場合の例を見てみましょう。

$ cat set.sh 
#!/bin/bash
set -e
echo "And and Or operator:"
ls && ls -l nofolder || ls
echo "While loop:"
while ls -l nofolder; do
    echo "Folder exists"
    break
done
echo "If condition:"
if ls -l nofolder; then
    echo "Folder exists"
fi
ls -l
$ ./set.sh 
And and Or operator:
set.sh
ls: cannot access 'nofolder': No such file or directory
set.sh
While loop:
ls: cannot access 'nofolder': No such file or directory
If condition:
ls: cannot access 'nofolder': No such file or directory
total 24
-rwxrwxr-x 1 bluelake bluelake 192 Mar 28 17:34 set.sh
$

上から、どのエラーもスクリプトを終了させなかったことがわかります。 フローを理解するために、スクリプト全体にechoコマンドを配置しました。

最後に、このエラー状態を不要にトリガーする場合があります。

以下のこのスクリプトを見てみましょう。

$ cat t.sh 
#!/bin/bash
set -e
i=0
echo "Incrementing"
let i++
ls
$ ./t.sh 
Incrementing
$

上記のスクリプトに示されているように、最後のlsコマンドが実行されなかったことがわかります。 letコマンドは、変数iの値を1にインクリメントし、それを返しました。 次に、最後のコマンドの終了コードとして解釈され、errexit条件がトリガーされます。 したがって、スクリプトは実行を停止しました。

同様に、注意すべきシナリオが他にもあります。 完全なリストについては、Bashのmanページを確認してください。

3. if-elseブロックの使用

これは、コマンドの結果を確認して例外を発生させる簡単な方法です。

例を確認してみましょう。

$ cat if.sh 
#!/bin/bash

if ! ls -l nofolder; then
    echo "Folder doesn't exist"
    exit 1
fi
ls -l
$ ./if.sh 
ls: cannot access 'nofolder': No such file or directory
Folder doesn't exist
$

上記のように、if句でエラーを引き起こす可能性のあるコマンドをラップしました。次に、そのコマンドの終了コードを確認し、エラーをログに記録して、exitから脚本。

これを改善するために、処理部分を別の関数に移動する場合があります。 次に、エラーが発生したときにエラー文字列を渡してその関数を呼び出すことができます。

逆に、小さなスクリプトがあり、上記のような再利用可能なシナリオがない場合は、これを以下のように少し簡略化できます。

$ cat if.sh 
#!/bin/bash
ls -l nofolder || exit 1
ls -l
$ ./if.sh 
ls: cannot access 'nofolder': No such file or directory

ここでは、 OR演算子を使用してエラーを識別し、スクリプトを終了しました。

3.1. PIPESTATUSを使用する

if ステートメントを使用して例外を発生させているときに、パイプラインにコマンドがある場合に問題が発生します。 パイプラインのコマンドが失敗したときを知る必要があります。 このために、PIPESTATUS配列を使用できます。

PIPESTATUS 配列には、パイプライン内の各コマンドの終了コードが特定のインデックスにあります。 次に、各コマンドの終了コードを確認し、必要に応じてエラーを発生させることができます。

例でこれを確認しましょう:

$ cat pipe.sh 
#!/bin/bash
ls | grep nofolder | wc -l
result=`echo ${PIPESTATUS[@]} | grep -E '^[0 ]+$'`
if [ "$result" = "" ]; then
    echo "Command failed"
    exit 1
fi
ls
$ ./pipe.sh 
0
Command failed

上記のスクリプトでは、パイプラインのgrepコマンドが失敗します。 次に、PIPESTATUS変数の終了コードを取得します。 次に、この変数の内容を別のgrepコマンドで実行します。 そこで、ゼロまたはスペース以外の文字が含まれているかどうかを確認します。 エラーの場合、PIPESTATUS変数の終了コードはゼロ以外になります。したがって、result変数は空になります。 最後に、この状態を確認し、エラーを出力して、スクリプトを終了します。

4. trapコマンドの使用

trap はシェル組み込みコマンドであり、特定の信号を受信したときにコマンドを実行するために使用されます。 さまざまな信号の中で、ERRは私たちが関心を持っている信号です。 これは、ゼロ以外のステータスコードでコマンドが終了したときに発行されます。

trapコマンドの使用方法を見てみましょう。

$ cat trap.sh 
#!/bin/bash
function handler() {
    echo "Error"
    exit 1
}
trap handler ERR
ls -l nofolder
ls
$ ./trap.sh
ls: cannot access 'nofolder': No such file or directory
Error
$

上記のスニペットでは、exitコマンドを使用して関数handlerを宣言しています。 次に、 trap コマンドを使用して、handler関数をERRシグナルにバインドしました。 その後、lsコマンドを実行しましたが失敗しました。 次に、 ERR シグナルが発生し、handler関数が呼び出されます。 最後に、 handler 関数から、エラーを出力してスクリプトを終了します。

これは、前に見た set-eコマンドに似ています。 ここでも同じ注意事項が当てはまります。 完全なリストについては、manページを確認できます。 ただし、ここでの利点の1つは、スクリプトを終了する前に、ハンドラーを介してハウスキーピングを実行できることです。

ただし、 set コマンドを使用すると、終了する前にハンドラーを実行することもできます。 次にそれを見てみましょう。

4.1. セットトラップを組み合わせる

setコマンドとtrapコマンドを組み合わせて、ハンドラーを実行できます。

例を見てみましょう:

$ cat combine.sh
#!/bin/bash
set -e
trap "echo 'Trapped'" ERR
ls -l nofolder
ls -l
$ ./combine.sh
ls: cannot access 'nofolder': No such file or directory
Trapped
$

ここでは、trapハンドラーを使用してエラーが発生したときにメッセージをログに記録します。 errexit オプションを設定しているため、エラーが発生すると自動的にスクリプトを終了します。 トラップハンドラーにexitコマンドを記述していないことがわかります。

5. 結論

このチュートリアルでは、エラーを発生させてスクリプトを終了するさまざまな方法を見てきました。 示されているように、すべてのオプションがそれ自体で完全であるとは限りませんが、ユースケースの必要に応じてこれらを組み合わせる必要がある場合があります。