【Bash】シェルスクリプトにて exit 前にコマンドを実行する

September 16, 2020

シェルスクリプト内で異常終了時のハンドリングでハマったので。 だいぶ基礎的な気もしますがメモしときます。

あと記事のタイトルが微妙な気がしてます。いい案あったら教えてほしいです。

結論

trap コマンドで終了時の処理を設定する

#!/bin/bash -e

# 終了前に実行したい処理を定義する(関数名は何でも良い)
function finally () {
  echo 'nyao'
}

# EXITシグナルをtrapして中断前に実行したい処理を登録する
trap finally EXIT

# 以降に失敗するかもしれない処理を書く
echo "hoge"
false # ←ここでコマンドが失敗する
echo "huga"
$ ./main.sh       
hoge
nyao

メモ

本題に入る前に set の話をします。 シェルスクリプトは set -e を実行して以降では「失敗時に中断(exit)する」といった挙動にすることができます。 false は必ず失敗する処理です。そこで中断するので echo "huga" は実行されていません。

#!/bin/bash

set -e

echo "hoge"
false # ←ここでコマンドが失敗する
echo "huga"
$ ./main.sh
hoge

試しに set -e を外すとこうなります。 失敗後も実行が継続していることがわかります。

#!/bin/bash

# set -e # ←コメントアウトしてみる

echo "hoge"
false # ←ここでコマンドが失敗する
echo "huga"
$ ./main.sh
hoge
huga

ちなみに set -e はシェルスクリプト戦闘の Shebang (#!/bin/bash の部分)に -e を指定することでも設定できます。 以下のスクリプトでも set -e したことになります

#!/bin/bash -e

# 以降処理を記述

この set -e ですが、 set +e で解除することが出来たり、他のオプションを指定できたりします。 しますが、全部話すとキリがないのでこのぐらいで。 これだけでも記事書けそうだな…。


コマンドが失敗したときのハンドリングは以下のような記述で定義できます。 以下は「 cd コマンドが失敗したら “hogeディレクトリは無いみたいです” と表示する」という例です。

$ cd hoge || echo "hogeディレクトリは無いみたいです"

これはもちろんシェルスクリプトでもできます。 上記のような記述をすると右側のコマンドが評価されて正常に実行できたことになってしまうので、更に || exit 1 みたいに書いて異常終了させる必要があります。 そうすればシェルスクリプトでも書くことができますが、シェルスクリプト内の全部の「失敗するかもしれない処理」に対してそれを書いていくわけにはいかないです。 以下みたいに書くと面倒ですよね。

#!/bin/bash -e

# hoge ディレクトリが無いことを考慮して記述
cd hoge || echo "終了しました" || exit 1

# huga.txt が無いことを考慮して記述
cat huga.txt || echo "終了しました" || exit 1

echo "終了しました

で、この記述を楽にできるのが trap です。 上記の例だとこう書くことが出来ます。

#!/bin/bash -e

function finally(){
  echo "終了しました"
}
trap finally EXIT

cd hoge
cat huga.txt

重要なところはこの trap finally EXIT というやつです。 これは「 EXIT シグナルを trap して中断前にfinallyを実行する」という記述になります。 シグナルとは、実行中のプロセスにさまざまなイベントを通知するために送出されるものです。

追記ここから ----------------

フォロワーから「EXIT はシグナルじゃなくて SIGKILL (9) を除くどんな場合でもトラップするという特別な値」と教えてもらいました。

POSIXを確認したところ、 EXIT は正確にはシグナルと呼べるものではないみたいです。 trap の引数に指定できるのは「 EXIT あるいはシグナル」ということらしいです。 https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#trap

また、Bashの仕様には明確な挙動が書かれています。

If a sigspec is 0 or EXIT, arg is executed when the shell exits.

trap [-lp] [arg] [sigspec …] で言うところの sigspacEXIT なら、 arg がシェルの exit 前に実行されるということです。 が、シェルによって EXIT の挙動が異なるようなので、bash 以外で利用するには注意が必要みたいです。

https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#trap

と、これを誤解なく記述しつつ文章をまとめる方法が思いつかないので、とりあえず追記というフォーマットで補足しておきます。

追記ここまで ----------------

シェルスクリプト内において、trapは本来用意されていない EXIT というシグナル(のようなもの)を使うことが出来ます。 シェルスクリプト内のとあるコマンドが失敗する際には「シェルは自分自身のプロセスに EXIT というシグナル(のようなもの)を送出したあとに中断が行われる」ということが行われます。 EXIT は正常終了時でも送出されるので、 try-catch-finally 構文で言うところの finally のような挙動を再現するのに適切です。

ちなみに他の、例えば ERR シグナルを用いることで try-catch-finally で言うところの catch も再現することが出来ます。

#!/bin/bash -e

function catch(){
  echo "失敗しました"
}
trap catch ERR

function finally(){
  echo "終了しました"
}
trap finally EXIT

cd hoge
echo "hogeに移動しました"
cat huga.txt
echo "huga.txtを表示しました"
# hoge ディレクトリがない状態で実行
$ ./main.sh
./main.sh: line 13: cd: hoge: No such file or directory
失敗しました
終了しました

# hoge ディレクトリを作成
$ mkdir hoge

# hoge ディレクトリはあるが hoge/huga.txt がない状態で実行
$ ./main.sh
hogeに移動しました
cat: huga.txt: No such file or directory
失敗しました
終了しました

# hoge/huga.txt を作成
$ echo "ニャオニャオ" > hoge/huga.txt

# hoge ディレクトリも hoge/huga.txt もある状態で実行
$ ./main.sh                          
hogeに移動しました
ニャオニャオ
huga.txtを表示しました
終了しました

なんでこんなこと調べたかという話ですが、 git の操作をするシェルスクリプトを書いていたからです。 例えば以下のような「指定されたコミットハッシュに存在する hoge.txthuga.txt を表示する」みたいなシェルスクリプトを組んでいたとしましょう。

#!/bin/bash -e

# ORIGINAL_BRANCH に現在のブランチ名を格納
ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)

# シェルスクリプトの引数で指定されたコミットハッシュに移動する
git checkout $1

# hoge.txt を表示する
cat hoge.txt

# huga.txt を表示する
cat huga.txt

# もとのブランチに戻る
git checkout -f "$ORIGINAL_BRANCH"

ここで気になるのは「指定されたハッシュにファイルが存在しなくて cat hoge.txtcat huga.txt が失敗したら、もとのブランチに戻る処理が実行されない」という点です。 こういうときに困っていたときにたどり着いた情報が trap を使うことでした。

trap を使うことによって以下のように書けました。良かったですね。

#!/bin/bash -e

# ORIGINAL_BRANCH に現在のブランチ名を格納
ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD)

# EXITシグナルが送出されたらもとのブランチに戻る
function finally(){
  git checkout -f "$ORIGINAL_BRANCH"
}
trap finally EXIT

# シェルスクリプトの引数で指定されたコミットハッシュに移動する
git checkout $1

# hoge.txt を表示する(失敗するかも)
cat hoge.txt

# huga.txt を表示する
cat huga.txt

参考


© takanakahiko 2024, Built with Gatsby
This site uses Google Analytics.