終了ステータス

終了ステータスとは?

コマンド終了時には「終了ステータス (exit-status)」と呼ばれるコマンドの成否を表す数値が特殊変数 $? に自動で設定される

各コマンドにより異なるが、一般的には、

  • コマンド成功時には「0」
  • 失敗時には「1」(コマンドやエラーの種類によっては 0 以外)

が設定される。

command
echo $?

→ 直前に実行したコマンドの成否は、特殊変数 $? に設定されている値で確認する。

終了ステータスを設定

シェルスクリプトでは exit コマンドに指定したパラメータ (0 もしくは 1 ~ 255 の正の整数値のみ可) が、そのシェルの終了ステータスとなる。シェルスクリプトにおいても正常終了時は exit 0 で、異常終了時には exit 1 で終了するようにするのが慣例である。

関数も同様に return コマンドに指定したパラメータが終了ステータスとなる。

exit status
return status

→ シェルスクリプトの終了ステータスを設定するには、exit コマンドに 0 もしくは 1 ~ 255 の正の整数値を指定する。

exit コマンド、関数ならば return コマンドに終了ステータスとなる値を指定することで、任意の終了ステータスでシェルスクリプトおよび関数を終了することができる。

exit コマンドは省略可能であるが、省略された場合はシェルスクリプト内で最後に実行されたコマンドの終了ステータスがそのシェルスクリプトの終了ステータスとなる。関数においても、exit コマンドの代わりに return コマンドを使用すること以外は同様である。

$ cat hogehoge
cat: hogehoge: そのようなファイルやディレクトリはありません
$ echo $?
1
#↑ファイルが存在せず、cat コマンドが失敗したため、終了ステータスは 1 になった。

$ touch hogehoge
$ cat hogehoge
$ echo $?
0
#↑touch コマンドで空のファイルを作成したので、今度は cat コマンドが成功し、終了ステータスが 0 になった。

シェルスクリプトに exit コマンドを使用して、終了ステータスを設定するシェルスクリプト (exit_status.sh) を作成してみる。

#!/bin/bash

# カレントディレクトリ内に foo ディレクトリが存在するか?
if [ -d ./foo ]; then
  echo "ディレクトリが存在します。"
  exit 0
else
  echo "ディレクトリが存在しません。"
  exit 1
fi

このシェルスクリプト exit_status.sh の実行結果は、以下のとおりとなる。

$ ls -d ./foo
ls: ./foo: そのようなファイルやディレクトリはありません
$ ./exit_status.sh
ディレクトリが存在しません。
$ echo $?
1
#↑foo ディレクトリが存在しないので、シェルスクリプトは 1 で終了した。

$ mkdir ./foo
$ ls -d ./foo
./foo
#↑foo ディレクトリを作成する。

$ ./exit_status.sh
ディレクトリが存在します。
$ echo $?
0
#↑ディレクトリを作成したので、今度は終了ステータスが 0 になった。

終了ステータスを判定してコマンドを実行する

コマンドが成功した場合のみ次のコマンドを実行する

command1 && command2
command1 && command2 && command3

&& でコマンドをつなげると、直前のコマンドが成功した場合のみ次のコマンドが実行される。

コマンドが && で結合されていると、シェルは command1 の終了ステータスを判断し、「0」(成功)であった場合のみ command2 を実行する。

さらに複数のコマンドが結合されている場合も、直前のコマンドが成功した場合のみ、次のコマンドが実行される。

直前のコマンドが失敗していると困る処理 (例えば、1 つ目のコマンドが失敗すると、2 つ目のコマンドが確実に失敗するような処理) には、この && を使用すると直前のコマンドが成功していることが保証されるので、単純に連続して 2 つのコマンドを実行するよりも確実性の高い処理になる。

$ ls hogehoge
ls: hogehoge: そのようなファイルやディレクトリはありません
#↑hogehoge ファイルは存在しない。

$ ls hogehoge && echo "exists."
ls: hogehoge: そのようなファイルやディレクトリはありません
#↑存在しないので ls コマンドは失敗し、&& で結合した echo コマンドは実行されない。

$ touch hogehoge
#↑hogehoge ファイルを作成する。

$ ls hogehoge && echo "exists."
hogehoge
exists.
#↑今度は ls コマンドが成功するので、echo コマンドが実行される。

コマンドが失敗した場合のみ次のコマンドを実行する

command1 || command2
command1 || command2 || command3

|| でコマンドをつなげると、直前のコマンドが失敗した場合のみ次のコマンドが実行される。

コマンドが || で結合されていると、シェルは command1 の終了ステータスを判断し、「0」以外(失敗)であった場合のみ command2 を実行する。

さらに複数のコマンドが結合されている場合も、直前のコマンドが失敗した場合のみ、次のコマンドが実行される。

もし command1 が失敗したら command2 を試したい、といったような処理を行う場合は、この || を使用する。

$ ls fugafuga
ls: fugafuga: そのようなファイルやディレクトリはありません
$ ls fugafuga 2>/dev/null || echo "not exists."
not exists.
#↑ファイルは存在しないので ls コマンドは失敗し、それにより echo コマンドが実行される。

$ touch fugafuga
$ ls fugafuga 2>/dev/null || echo "not exists."
fugafuga
#↑今度はファイルが存在するので ls コマンドは成功し、echo コマンドは実行されない。

「 rm -f 」の終了ステータス

rm -f (強制削除) の終了ステータスは**常に「0」**となるため、&&|| を使用してコマンドを連結しても狙い通りの結果にはならない。

例えば「ファイルを削除できなかった場合のみ、あるコマンドを実行する」といった処理は、終了ステータスの判定ではできない。

$ ls foo
ls: foo: そのようなファイルやディレクトリはありません
$ rm foo
rm: cannot remove `foo': そのようなファイルやディレクトリはありません
$ echo $?
1
#↑存在しないファイルを削除しようとしたので、rm コマンドは失敗。

$ rm -f foo
$ echo $?
0
#↑-f オプション付きで実行すると、ファイルが存在しなくとも rm コマンドは成功。

ただし rm -f を使用した場合でも /tmp などスティッキービットが設定されているディレクトリで、他のユーザのファイルを削除しようとした場合には、終了ステータスが「1」になる。

このように rm -f の終了ステータスは当てにならないので、強制削除の終了ステータス判定処理は避けること。

終了ステータスの応用

入力値が数値か文字列かを判別する

キーボードから入力された値が、数値かそれとも数字以外の文字列かを判別したい場合は、expr コマンドとその終了ステータスを使用する。

expr コマンド (四則演算などに使用するコマンド) は、

  • 数値同士で演算を行った場合は「0」
  • 数値同士でも計算結果が 0 になる場合は「1」
  • 数値以外で演算を行った場合 (計算できない場合) は「2 以上」

が終了ステータスとなる。

つまり適当な数値とキーボードからの入力値とを expr コマンドで演算を行い、

  • 終了ステータスが「0」か「1」ならば入力値は数値、
  • 「2」以上ならば入力値は数値以外の文字列、

と判断することができる。

この expr コマンドと終了ステータスを使用した、入力値の判定を行うシェルスクリプト (input_check.sh) を作成してみる。

#!/bin/bash

# キーボードから入力(-n を付けると改行なしで出力する)
echo -n "数字を入力してください >"
read KEY

# 入力値に1をプラスし、その終了ステータスを判定する。
expr 1 + $KEY >/dev/null 2>&1
case $? in
  0 | 1 ) echo "[\$?=$?] 数字が入力されました >$KEY" ;;
      * ) echo "[\$?=$?] 数字以外が入力されました >$KEY" ;;
esac

exit 0

このシェルスクリプト input_check.sh の実行結果は、以下のとおりとなる。

$ ./input_check.sh
数字を入力してください >1
[$?=0] 数字が入力されました >1
#↑expr コマンドが成功するので、終了ステータスは「0」になる。

$ ./input_check.sh
数字を入力してください >-1
[$?=1] 数字が入力されました >-1
#↑expr コマンドは成功するが計算結果が0となるので、終了ステータスは「1」になる。

$ ./input_check.sh
数字を入力してください >hoge
[$?=3] 数字以外が入力されました >hoge
#↑計算できないため expr コマンドが失敗し、終了ステータスは「3」になる。

パイプ処理の終了ステータスを取得する

パイプ処理を行った場合、特殊変数 $? に設定される値は当然ながら、パイプ処理の一番最後に実行されたコマンドの終了ステータスとなる。

例えば次のような場合を考える。

$ exit 0 | exit 1 | exit 2
$ echo $?
2
#↑$? には最後のコマンドの終了ステータスが設定されている。

このようなパイプ処理において、最後のコマンドの終了ステータスではなく、パイプ処理の先頭、もしくは途中で実行しているコマンドの終了ステータスを取得したい場面が出てくると思う。

そのような場合には、特殊変数 $PIPESTATUS (配列)を参照することで、パイプ処理にて実行された各コマンドの終了ステータスを取得することができる (ただし bash 限定で ksh は不可)。

$ exit 0 | exit 1 | exit 2
$ echo ${PIPESTATUS[@]}
0 1 2
#↑$PIPESTATUS は配列なので、「@」を指定して全要素を出力している。

ここでは全要素 (全終了ステータス) を参照しているが、もちろん個別に参照することも可能である。詳しくは配列の使用方法の項目を参照してほしい。