はじめに
シェルスクリプトの if
コマンドの使い方は覚えるのがとても簡単です。
関連記事 いい加減シェルスクリプトで [ $? -eq 0 ] や [ $? -ne 0 ] なんて エラー処理を書くのはやめよう!
if の一番単純な構文
if
の一番単純な書き方(elif
や else
がない場合)は次のとおりです。
if 任意のコマンド
then
任意のコマンド
fi
fi
は if
を逆に並べたものです。この文法の発想は歴史をたどると C 言語を含む多くの言語に影響を与えた ALGOL から来ており、Bourne シェルの開発者が ALGOL 68 コンパイラの開発に関わっていた影響です。Bourne シェルとは ash や bash や zsh の祖先のシェルで、今はほとんど使われることのない古いシェルの名前です(2025年1月1日でサポート終了する Solaris 10 を除く)。昔は sh = Bourne シェルという時代がありましたが、今は sh = さまざまな POSIX シェルのどれかです。POSIX シェルとは POSIX で標準化されているシェルの仕様に準拠しているシェルのことです。POSIX シェルは Bourne シェルの後継シェルの ksh88 のサブセットがベースになっています。
シェル言語(シェルスクリプトの言語)は複数のコマンドを ;
でつなげて書くことができるので、次のように書くこともできます。
if 任意のコマンド; then
任意のコマンド
fi
コマンドの後に別のコマンドをつなげるときに ;
を入れると理解しておくと ;
を入れる場所もすぐに分かると思います。if
や then
の後ろに ;
がいらないのは、それらが続けて別のコマンドを書くと決まってる特殊なシェル言語のキーワードだからです。
if
~ then
の間と、then
~ fi
の間は同じものを書くことができて、複数のコマンドを書くこともできるので(if
の中に複数のコマンドを書くことはあまりありませんが)次のように書くこともできます。
if
任意のコマンド
任意のコマンド
then
任意のコマンド
任意のコマンド
fi
なんなら関数定義コマンドを書くことだってできます。(関数定義もコマンドの一種です)
if
foo() {
echo foo
}
then
foo
fi
つまり if
~ then
の間のコマンドが最終的に正常終了であれば then
~ fi
の間のコマンドを実行するというのが if
コマンドの機能の本質です。
if の基本的な使い方
if
コマンドの最も一般的な使い方は、if
に続いて実行するコマンドを書くことです。次のコードはファイルを削除するコマンド(rm
コマンド)を実行し、ファイルの削除に成功したらメッセージを表示する例です。このように任意のコマンドを実行し、その成否で処理を分岐するのが if
コマンドです。
if rm /tmp/dummy; then
echo "/tmp/dummy の削除に成功しました"
fi
シェル言語はコマンドを実行するための言語です。したがってコマンドを実行してその成否で処理を分岐したいということがよくあります。if
コマンドはそれを行うために作られています。他の言語の場合、変数の値または関数からの戻り値を比較するというイメージですが、シェル言語の場合は「コマンドを実行する→実行したコマンドが正常終了したか異常終了したか→その結果で分岐する」という考え方の違いがあります。なので if
で変数の値を比較するということは実は少ないんですね。
コマンドの出力を変数に代入し、コマンドの実行が成功したときだけ処理を行うこともできます。この使い方はときどき便利です。こんな書き方ができるんだ?と思う方もいるかも知れませんが、if
と then
の間には何でも書くことができるので、このような書き方だってできます。
if out=$(cmd); then
echo "コマンドの実行に成功しました。出力は $out です"
fi
if の少し複雑な構文
else
を使うと、コマンドが失敗したときに実行するコマンドを書くことができます。
if 任意のコマンド; then
成功時のコマンド
else
失敗時のコマンド
fi
elif
を使うと最初のコマンドが失敗したときに、次のコマンドを実行し、その実行したコマンドで更に分岐することができます。elif
は else if を縮めたものです。
if 任意のコマンド1; then
任意のコマンド1の成功時のコマンド
elif 任意のコマンド2; then
任意のコマンド2の成功時のコマンド
else
失敗時のコマンド
fi
C 言語や JavaScript では {
... }
中が一文の場合に {
... }
を省略することができます。括弧の省略はバグを誘発することがあり使用を禁止されることもありますが、C 言語や JavaScript の else if
はこの括弧の省略を利用した書き方です。
// C 言語や JavaScript の場合
if (条件式1) {
...
} else { // ← ここの { と
if (条件式2) {
...
}
} // ← ここの } を省略すると
// 擬似的に else if を作ることができる
if (条件式1) {
...
} else if (条件式2) {
...
}
シェル言語では then
... if
を省略することはできないので専用の elif
が用意されているというわけです。
終了ステータスの取得
コマンドの成否で分岐するだけなら終了ステータスを参照する必要はありません。もし終了ステータスを知りたい場合は終了ステータスは次のようにして取得することができます。
if 任意のコマンド; then
echo $? # 成功時の終了ステータス(0)
else
echo $? # 失敗時の終了ステータス(0 以外)
fi
もし終了ステータスに応じてなにか別の処理をしたい場合は case
コマンドを併用すると良いでしょう。
if 任意のコマンド; then
echo $? # 成功時の終了ステータス(0)
else
case $? in
1) echo "終了ステータスは1です" ;;
2) echo "終了ステータスは2です" ;;
*) echo "その他の終了ステータス ($?)" ;;
esac
fi
余談ですが case
の終わりも逆に並べた esac
です。しかし do
の終わり done
です。これは od
という名前のコマンドが Bourne シェルが誕生するよりも前から使われていたので使えなかったからです。
もし成功時に何もしたくないのであれば、:
コマンド(何もしないコマンド)を書きます。ごく一部のシェル(zsh と FreeBSD sh と yash だけ)は :
を省略できますが、何もしないときには :
を書くのが Bourne シェル時代から続く POSIX シェルの流儀です。
if 任意のコマンド; then
:
else
case $? in
1) echo "終了ステータスは1です" ;;
2) echo "終了ステータスは2です" ;;
*) echo "その他の終了ステータス ($?)" ;;
esac
fi
!
を使うと成否を逆にすることができますが、終了ステータスを反転するため終了ステータスは必ず 0 か 1 になってしまいます。終了ステータスが必要ない場合には !
は便利です。
if ! 任意のコマンド; then
echo $? # 失敗(0以外)の反転は常に成功(0)
fi
# ! 使うと終了ステータスは取れない
if ! 任意のコマンド; then
echo $? # 終了ステータスは必ず 0
else
echo $? # 終了ステータスは必ず 1
fi
シェル言語の真は0ではない
シェル言語では真が0だという勘違いを多く見かけますが、シェル言語でも真は0以外です。この勘違いが起きる原因の一つは、if
を他の言語と同じように真偽値で分岐するものだと考えているからです。シェル言語の if
は成否で処理を分岐するものです。
すべての言語でコマンド(プログラム)の正常終了時のステータスコードは 0 です。
シェル言語はコマンドを実行する言語なので、if
は(真偽値)ではなくコマンドの実行が正常終了したか異常終了したかで分岐するものになっています。
if 任意のコマンド; then
echo $? # 成功時の終了ステータス(0)
else
echo $? # 失敗時の終了ステータス(0 以外)
fi
ちなみにシェル言語でも真が0以外である証拠は次のようにして証明できます。(このコードは bash などの拡張シェルで動作します。dash などでは動作しません。)
if ((0)); then
echo $? # ここには来ない
else
echo $? # 0は偽なのでこちらに来る
fi
if ((1)); then
echo $? # 0以外は真なのでこちらに来る
else
echo $? # ここには来ない
fi
さいごに
以上で if
コマンドの解説は終わりです。
以上です。
3分もかからなかったと思います。
え? [ ... ]
や [[ ... ]]
の話はどうしたんだって? それは if
コマンドと関係ありません。全く別のコマンドです。「if
任意のコマンド」の任意のコマンドにはどんなコマンドだって実行することができます。rm
コマンドだって cp
コマンドだってどんなコマンドだって実行することができます。そこに test
コマンドや [
コマンドだって実行できるというだけのことです。
if
rm コマンドでもいいし
cp コマンドでもいいし
test コマンドでもいいし
[ コマンドでもいい
then
任意のコマンド
fi
[
コマンドは test
コマンドとほぼ同じ機能を持ち、唯一の違いは [
は最後の引数に ]
を指定しなければいけないコマンドです。test
は次のようにして [
に書き換えることができます。
test 10 -gt 3
↓
1. 最初の test を [ に変更する
2. 最初の 引数に ] を付け加える
↓
[ 10 -gt 3 ]
2000 年頃までの古いシェルでは [
や test
は、/bin/[
や /bin/test
という実行ファイルが存在する外部コマンドとして実装されていました。sh や bash など、現在実用されている POSIX シェルのすべてで [
や test
はシェルに内蔵されており、/bin/[
や /bin/test
を呼び出すことはありません。昔は外部コマンドだったという歴史的事実は [
や test
が if
コマンドの一部ではないということの理解の助けになります。
外部コマンドは呼び出しが遅いです。次のように、わざわざ終了ステータスを見て条件分岐しているコードを見かけますが、昔の [
は外部コマンドだったので無駄に遅くなる良くない書き方でした。
rm /tmp/dummy
if [ $? -eq 0 ]; then
echo "/tmp/dummy の削除に成功しました"
fi
今は速度は気にする必要はありませんが、この方法だと else
で rm
コマンドの終了ステータスが取れない([
コマンドの実行結果に置き換わってしまう)ので、わざわざ変数に終了ステータスを代入しなければならなくなります。シンプルイズベストです。わざわざ [ $? -eq 0 ]
や [ $? -ne 0 ]
と書くのはやめましょう。せめて case
を使いましょう。
if rm /tmp/dummy; then
echo "/tmp/dummy の削除に成功しました"
else
echo "/tmp/dummy の削除に失敗しました ($?)"
fi
rm /tmp/dummy
case $? in
0) echo "/tmp/dummy の削除に成功しました" ;;
*) echo "/tmp/dummy の削除に失敗しました ($?)" ;;
esac
[
コマンドの話は if
コマンドとは別の話なのでこの記事の対象外です。[
(test
)コマンドの使い方を調べてください。[[ ... ]]
や (( ... ))
はコマンドではなくシェルの文法という違いがありますが同様に if
コマンドの一部ではありません。つまり文字列の比較とか数値の比較とか、そんなのは if
コマンドの対象外だということです。if
コマンドで覚えるべきことは任意のコマンドを実行してその成否で分岐するだけです。
C や JavaScript などでは if
文はこのように説明しますが
// C 言語や JavaScript の場合
if (条件式) {
...
}
シェル言語では(「任意のコマンド」を「条件コマンド」に置き換えた場場合は)このように書きます。シェル言語はコマンドを実行するための言語なので、if
コマンドはコマンドを実行し、実行したコマンドの成否で分岐するコマンドとして設計されています。
if 条件コマンド; then
...
fi
これを次のように説明している場合は、if
コマンドの文法を理解してない証拠です。[ ... ]
は if
の一部ではないからです。C や JavaScript の (...)
は if
の一部ですが、シェル言語はそうではありません。
if [ 条件式 ]; then
...
fi
異なる概念を同時に学ぼうとするから混乱します。一度に複数のことを学ぼうとせずに一歩ずつ学びましょう。[
は if
の一部ではないということは、[
は if
と組み合わせなくても使うことができます。
[ 10 -gt 3 ] && echo ok
set -e
をしているとエラーになったコマンドで終了するので次のような書き方もできます。
set -e
[ 10 -gt 3 ]
echo ok # ここには来る
[ 3 -gt 10 ]
echo ng # ここには来ない
これは次のコードと同じ意味です。
set -e
test 10 -gt 3
echo ok # ここには来る
test 3 -gt 10
echo ng # ここには来ない
以上、余談でした。