if 文と test コマンド

if 文とは?

if 文とは、与えられた条件式が真のときのみ処理を行い、それ以外の場合は処理をスキップする、などといった場合に使用される条件分岐処理である。

条件式には一般的に test コマンドを用いるが、ls コマンドや grep コマンド等の他のコマンドを用いても構わない。

if 文は条件式に指定されたコマンドの終了ステータスを判定し分岐を行う。終了ステータスが「0」の場合は真、その他の場合は偽となる。

if 文 タイプ 1

if 条件式 ; then
  処理
fi

→ 条件式が真の場合のみ処理を行い、それ以外の場合は処理をスキップする。

if 文 タイプ 2

if 条件式 ; then
  処理1
else
  処理2
fi

→ 条件式が真の場合は処理1を、それ以外の場合は処理2を行う。

if 文 タイプ 3

if 条件式1 ; then
  処理1
elif 条件式2 ; then
  処理2
else
  処理3
fi

→ 条件式1が真の場合は処理1を、条件式1が偽でかつ条件式2が真の場合は処理2を、それ以外の場合は処理3を行う。

elif ブロックを複数追加することにより、さらに多くの分岐を作ることも可能である。

if 文のコーディングスタイルに関する補足

if 条件式
then
  処理
fi

if 文は上記のように if 条件式then をそれぞれ1行で記述する必要がある。だが、これだと then の記述だけで1行を消費しまうことになり、複数の条件分岐がある場合はスクリプトの行数が増え、結果的に可読性が低下する。

したがって、if 文を記述するときは以下のように ; (セミコロン) を使用し、1行で if 条件式then を記述するようにした方がよい。

if 条件式 ; then
  処理
fi

参考 ; (セミコロン) に関して

通常、コマンドは1行につき1つのみ記述できるが、コマンドを1行に複数個記述したい場合は ; (セミコロン) を使用する。

また、コマンドラインから複数のコマンドを連続実行したい場合にも、; (セミコロン) で区切って複数のコマンドを記述することでコマンドを連続実行することができる。

test コマンド

if 文などで条件式を評価する場合には test コマンドを使用する。どのような評価を行うかはオプションにより細かく指定することが可能である。

test コマンドは評価結果に従い、真(0) か偽(1) かの終了ステータスを返すのみで、画面上へのメッセージ出力等は一切行わない条件評価に特化したコマンドである。

数値の比較

test コマンドは与えられた2つの数値を比較し、それらの等価・大小を評価することができる。比較条件は実行時に指定されたオプションにより決定される。

数値1と数値2が等しいか?(数値1=数値2 ?)

test 数値1 -eq 数値2

test コマンドの終了ステータスを echo コマンドで確認してみる。なお、echo コマンドは ; (セミコロン) を使用し、test コマンド実行後に連続実行している。

$ test 1 -eq 1 ; echo $?
0
$ test 1 -eq 2 ; echo $?
1

2つの数値がイコールである場合のみ、真 (終了ステータスが 0) となる。

数値1と数値2が等しくないか?(数値1≠数値2 ?)

test 数値1 -ne 数値2
$ test 1 -ne 1 ; echo $?
1
$ test 1 -ne 2 ; echo $?
0

イコールになる場合のみ、偽 (終了ステータスが 1) となる。

数値1は数値2より小さいか?(数値1<数値2 ?)

test 数値1 -lt 数値2
$ test 1 -lt 1 ; echo $?
1
$ test 1 -lt 2 ; echo $?
0

「左辺<右辺」である場合のみ、真 (終了ステータスが 0) となる。

数値1は数値2より大きいか?(数値1>数値2 ?)

test 数値1 -gt 数値2
$ test 1 -gt 2 ; echo $?
1
$ test 2 -gt 1 ; echo $?
0

「左辺>右辺」である場合のみ、真 (終了ステータスが 0) となる。

数値1は数値2以下か?(数値1≦数値2 ?)

test 数値1 -le 数値2
$ test 1 -le 1 ; echo $?
0
$ test 0 -le 1 ; echo $?
0
$ test 2 -le 1 ; echo $?
1

「左辺≦右辺」である場合のみ、真 (終了ステータスが 0) となる。

数値1は数値2以上か?(数値1≧数値2 ?)

test 数値1 -ge 数値2
$ test 1 -ge 1 ; echo $?
0
$ test 2 -ge 1 ; echo $?
0
$ test 1 -ge 2 ; echo $?
1

「左辺≧右辺」である場合のみ、真 (終了ステータスが 0) となる。

test コマンドによる数値比較のまとめ

前述のように test コマンドを使用することにより、2つの数値を比較することができる。

使用可能な比較条件を以下の表にまとめる。

test コマンドの数値比較条件一覧表
オプション 使用例 オプションの意味 数式で表すと…
-eq test num1 -eq num2 num1 と num2 が等しければ真となる。 num1=num2
-ne test num1 -ne num2 num1 と num2 が等しくなければ真となる。 num1≠num2
-lt test num1 -lt num2 num1 が num2 より小ならば真となる。 num1<num2
-le test num1 -le num2 num1 が num2 以下ならば真となる。 num1≦num2
-gt test num1 -gt num2 num1 が num2 より大ならば真となる。 num1>num2
-ge test num1 -ge num2 num1 が num2 以上ならば真となる。 num1≧num2
参考 数値比較条件に関して

比較条件に使用する -lt-ge などは、一般的な数学記号の「<」や「≧」などに比べて覚えにくい印象を持つかもしれなが、それぞれが何の略語になっているかを考えると比較的簡単に覚えられる。

-eqequal
-nenot equal
-ltless than
-leless than or equal
-gtgreater than
-gegreater than or equal

文字列の比較

test コマンドは、与えられた2つの文字列を比較し評価することができる。

比較条件は =!= の2種類があり、= を使用した場合は比較する 2つの文字列が一致するときに真 (終了ステータスが 0) となる。

!= を使用した場合は、逆に比較する 2つの文字列が一致しないときに真となる。

文字列1と文字列2は等しいか?

test 文字列1 = 文字列2
$ test "abc" = "abc" ; echo $?
0
$ test "abc" = "def" ; echo $?
1

比較する 2つの文字列が同一である場合のみ真 (終了ステータスが 0) となっている。

文字列1と文字列2は等しくないか?

test 文字列1 != 文字列2
$ test "abc" != "abc" ; echo $?
1
$ test "abc" != "def" ; echo $?
0

比較する2つの文字列が非同一である場合のみ真 (終了ステータスが 0) となっている。

ファイルの比較

test コマンドは2つのファイルのタイムスタンプを比較し評価することができる。

比較方法 (オプション) は2種類あり、

  • -nt は「ファイル1 is newer than ファイル2」を意味し、
  • -ot は「ファイル1 is older than ファイル2」を意味する。

それぞれ成立した場合は終了ステータスが真 (0)、成立しなかった場合は偽 (1) となる。

file1 の方が新しいか?

test ファイル1 -nt ファイル2

file1 の方が古いか?

test ファイル1 -ot ファイル2

ファイル比較を実際に実行してみる。

$ touch file1
$ touch file2
$ ls -l
total 0
-rw-r--r-- 1 SUNONE なし 0 Aug 11 04:26 file1
-rw-r--r-- 1 SUNONE なし 0 Aug 11 04:27 file2
#↑タイムスタンプの異なる2つのファイルを作成。

$ test file1 -nt file2 ; echo $?
1
#↑「-nt」だと file1 の方が古いので終了ステータスは「偽」となる。

$ test file1 -ot file2 ; echo $?
0
#↑逆に「-ot」を使用すると、file1 の方が古いので終了ステータスは「真」となる。

$ echo "test" >>file1
$ ls -l
total 1
-rw-r--r-- 1 SUNONE なし 5 Aug 11 04:49 file1
-rw-r--r-- 1 SUNONE なし 0 Aug 11 04:27 file2
#↑今度は file1 に変更を加え、タイムスタンプをfile2より新しくしてみる。

$ test file1 -nt file2 ; echo $?
0
$ test file1 -ot file2 ; echo $?
1
#↑今度は結果が全く逆になっているのが分かる。

その他の評価条件

test オプション 対象

→ test コマンドにオプションを指定することで、さまざまな評価を行うことが可能になる。

test コマンドでは数値や文字列の比較といった評価の他にも、“ファイルが存在するか” などの様々な評価が可能である。

test コマンドの数値・文字列比較以外の評価条件一覧表

オプション 使用例 オプションの意味
-z test -z string string の文字列長が 0 ならば真となる。
-n test -n string string の文字列長が 0 より大ならば真となる。
-d test -d file file がディレクトリならば真となる。
-f test -f file file が普通のファイルならば真となる。
-s test -s file file が 0 より大きいサイズならば真となる。
-e test -e file file が存在するならば真となる。
-r test -r file file が読み取り可能ならば真となる。
-w test -w file file が書き込み可能ならば真となる。
-x test -x file file が実行可能ならば真となる。

略式 test コマンド

[ 文字列1 = 文字列2 ]
[ 数値1 オプション 数値2 ]
[ オプション 評価対象 ]

→ testコマンドは略式の [] を使用して記述することができる。

if 文など test コマンドを記述する場合は [] で代用する場合が多い。記述方法は test コマンドの引数をそのまま [] の中に記述する形になる。

ただし、[ の直後と ] の直前には必ず半角スペースが必要となる (無いと正常に動作しない)。その理由は後述。

$ ["A" = "A"]
bash: [A: command not found
#↑半角スペースが無いのでエラーとなる。

$ [ "A" = "A" ]; echo $?
0
$ [ 1 -ne 1 ]; echo $?
1
$ [ -f file1 ]; echo $?
0
$ [ -f file1 -a "A" = "A" ]; echo $?
0

[] を使用した test コマンドがうまく動作しないときは、半角スペースを付け忘れていることが多い。エラーが発生した際のシェルスクリプトのデバッグでは、まず半角スペースの付け忘れを疑ってみるとよい

AND 条件と OR 条件 その1

AND 条件

if test 条件式1 -a 条件式2; then
  ...
fi

OR 条件

if test 条件式1 -o 条件式2; then
  ...
fi

AND 条件と OR 条件

if test 条件式1 -a 条件式2 -o 条件式3; then
  ...
fi

→ AND (-a) と OR (-o) を使用して複数の条件式を指定することができる。

test コマンドは -a もしくは -o を指定することにより、複数の条件式の終了ステータスから論理演算を行うことができる。

つまり

  • 「条件式1が成り立ちかつ条件式2が成り立つ場合は真」
  • 「条件式1もしくは条件式2のどちらかが成り立つ場合は真」

といった評価を行うことが可能である。

  • **-a は「AND(かつ)」を意味し、前後の条件式が両方成り立つ場合のみ「真」**となる。
  • -o は「OR(または)」を意味し、前後の条件式のどちから一方でも成り立てば「真」となる

また、-a-o よりも評価の優先順位が高いので、両方指定した場合は -a の評価が先にに行われる。この優先順位は、後述する () によるグルーピングで変更可能である。

AND 条件の実行結果

$ test 1 = 1 -a 1 -eq 1 ; echo $?
0
$ test 1 = 1 -a 1 -ne 1 ; echo $?
1

AND 条件は両方成り立つ場合のみ「真 (終了ステータスが 0)」となる。

$ test 1 != 1 -a 1 -ne 1 ; echo $?
1

両方成り立たない場合は当然ながら「偽 (終了ステータスが 1)」となる。

OR 条件の実行結果

$ test 1 = 1 -o 1 -eq 1 ; echo $?
0
$ test 1 = 1 -o 1 -ne 1 ; echo $?
0

OR 条件だとどちらか一方でも成り立っていれば「真 (終了ステータスが 0)」となる。

$ test 1 != 1 -o 1 -ne 1 ; echo $?
1

両方成り立たない場合は AND 条件と同様に「偽 (終了ステータスが 1)」となる。

AND 条件と OR 条件の優先順位

次は AND 条件と OR 条件の優先順位について見てみる。

$ test 1 = 1 -o 1 -ne 1 -a 2 != 2 ; echo $?
0

上記の test コマンドを分かりやすく書くと次のようになる。

test 真 -o 偽 -a 偽

左から順に見ていくと、-o の前後が真と偽なのでここは「真」となる。それにより次の -a の前後が真と偽になり結果は「偽」となるはずである。

だが、上記の使用例にあるとおり、結果は「真」となっている。これは -a の優先順位が -o より高いため、-a が先に評価されることによる結果である

つまり先に -a の前後が評価され、偽と偽なので結果は「偽」となり、最終的に test 真 -o 偽 が評価され、結果は「真」となったのである。

これで -a の方が -o よりも評価の優先順位が高いということが分かったが、この優先順位は () で囲むことにより変更することが可能となる。

なお、この ()\( \) のように、必ずエスケープが必要なので注意すること。

$ test \( 1 = 1 -o 1 -ne 1 \) -a 2 != 2; echo $?
1

先ほどと全く同じ条件式ではあるが、今度のは () で囲んでいることにより結果が異なっていることが分かるだろう。

上記の場合は、

  1. 先に -o が評価され、test 真 -o 偽 で「真」となり、
  2. 次に -a が評価され、test 真 -a 偽

で、最終的に「偽」となったのである。

AND 条件と OR 条件 その2

-a-o オプションでの表記がわかりにくければ、&&|| で test コマンドを連結してもよい

  • && が AND 条件
  • || が OR 条件

となる。

&& および || の詳細に関しては、終了ステータスのページを参照のこと。

AND (&&) 条件

if test 条件式1 && test 条件式2; then
  ...
fi

OR (||) 条件

if test 条件式1 || test 条件式2; then
  ...
fi

AND (&&) 条件と OR (||) 条件

if test 条件式1 && test 条件式2 || test 条件式3; then
  ...
fi

&&|| の場合も、() により優先順位を変更することが可能である。

test 条件式1 && ( test 条件式2 || test 条件式3 )
( test 条件式1 && test 条件式2 ) || test 条件式3

-a-o オプションを使用した場合と異なり、test コマンドのパラメータとして () を指定するわけではなく、コマンドのグルーピングとして () を指定するので、エスケープは不要となる。

$ [ a = aa ] && [ b = bb ] || [ c = c ]; echo $?
0

↑「偽かつ偽もしくは真」なので、結果は「真」となる。

$ ( [ a = aa ] && [ b = bb ] ) || [ c = c ]; echo $?
0

↑グルーピングした場合も同様に「(偽かつ偽)もしくは真」となるので、結果は「真」となる。

$ [ a = aa ] && ( [ b = bb ]  || [ c = c ] ); echo $?
1

↑グルーピングにより優先順位が変わると「偽かつ(偽もしくは真)」となるので、結果は「偽」となる。

() はグルーピングを行いつつサブシェルでコマンドを実行するが、グルーピングを行いつつカレントシェルでコマンドを実行する {} でも同様の結果を得られる。

ただし、{} の場合はカッコを閉じる直前のコマンドの直後に ; が必要になるので、特別な理由がなければ () を使用する方がよいだろう。

$ { [ a = aa ] && [ b = bb ]; }  || [ c = c ]; echo $?
0
$ [ a = aa ] && { [ b = bb ]  || [ c = c ]; }; echo $?
1

NOT 条件

コマンド実行結果の NOT 条件

条件式の終了ステータスが 0以外の場合、つまりコマンドが失敗した場合に真とするには ! を使用する。! は後に続くコマンド (パイプラインの場合は最後のコマンド) の終了ステータスを反転する

つまり、コマンドの実際の終了ステータスが、

  • 「0」だった場合は「1」に
  • 「1 (0以外)」だった場合は「0」

になる。

if ! echo "$hoge" | grep -sqE "^fuga$"; then
  echo "\$hoge is not fuga."
fi

! で終了ステータスを反転する。

! の動作を実際に検証してみる。検証には、

  • 何もせずに終了ステータスが「0」で終わるコマンドの true
  • 何もせずに終了ステータスが「1」で終わるコマンドの false

を使用する。

$ true; echo $?
0
#↑true コマンドの終了ステータスは常に「0」となる。

$ ! true; echo $?
1
#↑「!」を指定することで、終了ステータスを反転する(「1」にする)ことができる。

$ false; echo $?
1
#↑false コマンドの終了ステータスは常に「1」となる。

$ ! false; echo $?
0
#↑「!」を指定することで、終了ステータスを反転する(「0」にする)ことができる。

$ true | false; echo $?
1
$ ! true | false; echo $?
0
#↑パイプラインの場合は最後のコマンドの終了ステータスを反転する。

文字列比較の NOT 条件

ただし、条件式が文字列の比較 (両辺が等しくない) の場合は、コマンドに ! を指定するのではなく != を使用する

if test "$hoge" != "fuga"; then
  echo "\$hoge is not fuga."
if
if [ "$hoge" != "fuga" ]; then
  echo "\$hoge is not fuga."
fi

→文字列の比較 (両辺が等しくない) には、他言語でも一般的な != を使用する。

もちろん、test コマンドに ! を指定して、! [ "$hoge" = "fuga" ] もしくは [ ! "$hoge" = "fuga" ] とすることも可能だが、可読性が下がるだけでメリットもないので、素直に != を使用すること。

数値比較の NOT 条件

同様に数値の比較 (両辺が等しくない) の場合にも、コマンドに ! を指定するのではなく、可読性の面からあらかじめ用意されている -ne オプションを使用するのが望ましい

if test 1 -ne 2; then
  ...
fi
if [ 1 -ne 2 ]; then
  ...
fi

→数値の比較 (両辺が等しくない) には -ne を使用する。

補足 - 「 [ ] 」と「 [[ ]] 」の違いは?

if 文には [] 以外にも [[ ]] が使用されることがある。両者の違いは次の結果を見ればよく分かる。

$ which [
/usr/bin/[
$ which [[
$

[] は /usr/bin 配下に存在する通常のコマンドである。正確には [ がコマンド本体で ] はパラメータということになる。[ の直後にスペースがないとエラーになるのはそのためでもある (ちなみに if 文自体も正確には文ではなくコマンドである)。

一方、[[ ]] は通常のコマンドではなく bash の組み込みコマンドになる。したがって which コマンドの実行結果には何も表示されない。

さらに両者の機能的な違いは次のとおりだ。

$ unset hoge
$ [ $hoge = "HOGE" ]
-bash: [: =: unary operator expected
$ [[ $hoge = "HOGE" ]]
$

未定義の変数 hoge と文字列 “HOGE” の比較であるが、変数を「""」で囲んでいないため、変数展開後の条件式が「= “HOGE”」となる。そのため [] ではエラーメッセージが出力されている。一方で [[ ]] ではエラーメッセージは表示されない。

加えて両者の終了ステータスにも次のとおり違いがある。

$ [ $hoge = "HOGE" ]
-bash: [: =: unary operator expected
$ echo $?
2
$ [[ $hoge = "HOGE" ]]
$ echo $?
1

余計なエラーメッセージを表示させたくない場合は、[[ ]] を使用した方がよいが、筆者は見た目の美しさとシンプルさを重視して [] を使用することにしている。

if 文の使用例

if 文を使用したシェルスクリプト 1

実際に if 文を使用してみる (if_test1.sh)。if 文の条件式には略式 test コマンドを使用することが多いが、if 文は終了ステータスを判定するのみなので、ls 等の一般的なコマンドを指定しても問題はない。

#!/bin/bash

if ls file1 file2 >/dev/null 2>&1; then
  # 古いほうを削除する
  if [ file1 -ot file2 ]; then
    echo "remove file1."
    rm -f file1
  else
    echo "remove file2."
    rm -f file2
  fi
else
  echo "file not found..."
  exit 1
fi

exit 0

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

$ ls -l
total 1
-rw-r--r-- 1 SUNONE なし 0 Aug 11 07:05 file1
-rw-r--r-- 1 SUNONE なし 0 Aug 11 07:06 file2
-rwxr-xr-x 1 SUNONE なし 231 Aug 11 07:04 if_test.sh*
$ ./if_test.sh
remove file1.
#↑古い方の file1 が削除される。

$ touch file1
$ ./if_test.sh
remove file2.
#↑新しく file1 を作成してから実行すると、今度は file2 の方が古いので削除される。

$ rm -f file*
$ ls -l
total 1
-rwxr-xr-x 1 SUNONE なし 231 Aug 11 07:04 if_test.sh*
$ ./if_test.sh
file not found...
#↑file1、file2 が無い状態で実行すると12行目の else ルートへ。

if 文を使用したシェルスクリプト 2

次は elif を使用した複数分岐の例 (if_test2.sh)。

#!/bin/bash

echo -n 'Input "a" or "b": '
read KEY

if [ "$KEY" = "" ]; then
  echo "何も入力されませんでした。"
elif [ "$KEY" = "a" ]; then
  echo '"a"が入力されました。'
elif [ "$KEY" = "b" ]; then
  echo '"b"が入力されました。'
else
  echo "不正な値: $KEY"
fi

exit 0

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

$ ./if_test2.sh
Input "a" or "b":
何も入力されませんでした。
#↑何も入力せずにそのまま Enter。

$ ./if_test2.sh
Input "a" or "b": a
"a"が入力されました。

$ ./if_test2.sh
Input "a" or "b": b
"b"が入力されました。

$ ./if_test2.sh
Input "a" or "b": z
不正な値: z

if 文の応用

条件式に任意のコマンドを使用する

前述のとおり、if 文は条件式に指定したコマンドの終了ステータスを判定し、条件分岐を行う制御文である。したがって、条件式には test コマンド以外にも、ls や grep などの一般的なコマンドを用いることも可能である

if echo "$var" | grep -sq "hoge"; then
  echo "hoge が見つかりました。"
fi

→ 条件式には test コマンド以外の任意のコマンドも指定可能。

多くの場合、条件式に指定するコマンドで実行結果の出力を行う必要はないので、出力を抑制するオプションを指定するか、もしくは >/dev/null 2>&1 を指定し、全ての出力を捨てるようにするとよい。

grep コマンドを条件式に指定する場合は、-s オプションと -q オプションを同時に指定することで、エラー出力と標準出力を抑制することができる。

もちろんオプションを使用せずに >/dev/null 2>&1 を指定してもかまわない。

#「>/dev/null 2>&1」で出力を捨てる
if echo "$var" | grep "hoge" >/dev/null 2>&1; then
  echo "hoge が見つかりました。"
fi

条件式にコマンドを直接指定したくない場合は、直前のコマンドの終了ステータスを表す特殊変数 $? を test コマンドで判定すればよい。

#コマンド実行して $? を判定する
echo "$var" | grep -sq "hoge"
if [ $? -eq 0 ]; then
  echo "hoge が見つかりました。"
fi

ただし、この上記の場合は、grep コマンドと条件式の間で別のコマンドを実行しないこと (終了ステータスがそのコマンドの実行結果で上書きされるため)

終了ステータスを使いまわしたい場合、もしくはコマンドと条件式の間で別のコマンドを実行したい場合は、いったん変数に格納しておく。

echo "$var" | grep -sq "hoge"; result=$?

echo "終了ステータスを変数に退避したので、間でコマンドを実行しても OK です。" >/dev/null

if [ $result -eq 0 ]; then
  echo "hoge が見つかりました。"
  echo "終了ステータスは $result です。"
fi

任意の変数に終了ステータスを退避しておくことで、$? が他のコマンドによって上書きされても影響を受けないようにすることができる。