条件分岐と繰り返し
前提:6-2が完了し、~/scriptsにhello.sh・greet.shがあり、ホームディレクトリにいる状態から始めます。6-2まではコマンドを上から順に流すだけのスクリプトでした。このページでは、状況によって処理を分けるifと、同じ処理を何度も繰り返すfor・whileを使い、「賢く動く」スクリプトに一歩近づきます。数値を判定するjudge.shと、繰り返し処理を行うloop.shを作りながら、実際に動かして結果を目で確認していきましょう。
このページではSET 1〜3、合計30行のコマンドを上から順に叩きます。手打ち推奨(コピーは確認用)です。
SET 1 ― if と test の正体
- $cd ~/scripts
- $which test
- /usr/bin/test
- $which [
- /usr/bin/[
- $test 5 -gt 3
- $echo $?
- 0
- $test 5 -lt 3
- $echo $?
- 1
- $nano judge.sh
- (#!/bin/bash から始まる下のスクリプトを入力し保存)
- $cat judge.sh
- #!/bin/bash
- SCORE=80
- if [ $SCORE -ge 60 ]; then
- echo "合格です(${SCORE}点)"
- else
- echo "不合格です(${SCORE}点)"
- fi
- $chmod +x judge.sh
2行目・3行目のwhichで確認しているのは、条件分岐の心臓部であるtestと[の正体です。出力例のとおり、どちらも/usr/binの下にある独立した実行可能ファイルであることがわかります。つまりtest※1と[は、シェル専用の特殊な構文に見えて、実際はlsやcatと同じ「ただのコマンド」なのです。[ 条件式 ]という書き方は、実は[というコマンドに条件式と]という引数を渡しているだけで、]の前に必ずスペースが必要なのもこのためです。
4行目のtest 5 -gt 3を実行しても画面には何も表示されませんが、5行目のecho $?(直前のコマンドの終了コードを表示する書き方)を打つと、出力例のとおり0が返ってきます。testコマンドは「真か偽か」を、私たちが読む文字ではなく終了コード※3という数字で表現しており、0は真(条件を満たす)を意味します。6〜7行目のように偽の条件を試すと、echo $?の結果が1になり、偽であることがわかります。if文は、この裏側にある終了コードの0か0以外かを見て、処理を分けているのです。
8行目で書くjudge.shが、点数によって合否を判定するスクリプトです。3行目のif [ $SCORE -ge 60 ]; thenが条件分岐の骨組みで、「もし$SCOREが60以上ならば」という意味です。-geは数値比較演算子※2のひとつで、greater than or equalの略、つまり「以上」を表します。thenのあとに条件が真のときの処理、elseのあとに偽のときの処理を書き、最後はifを逆から読んだfiで締めくくります。
インデント(行頭の空白)は見やすさのためのもので、bashの動作には影響しませんが、then・elseの中身を2文字下げて書く習慣をつけておくと、あとで読み返したときに構造が一目でわかります。10行目でchmod +xを忘れずに実行し、次のSETで実際に動かします。
数値比較でよく使う演算子は-eq(等しい)・-ne(等しくない)・-gt(より大きい)・-ge(以上)・-lt(より小さい)・-le(以下)の6つです。「イーキュー・エヌイー・ジーティー・ジーイー・エルティー・エルイー」と音で覚えると定着しやすいです。

数字の比較なのに>や<じゃなくて-gtとか使うの、最初は「なんで!?」ってなるよね。実はbashの[ ]の中で>を使うと別の意味(ファイルへの出力)になっちゃうから、区別するために-gtみたいな書き方になってるんだよ。ここは理屈より先に「数値比較は文字で書く」って覚えちゃうのが早い!
SET 2 ― judge.shを動かし、文字列とファイルの判定を試す
- $./judge.sh
- 合格です(80点)
- $NAME="yumi"
- $if [ "$NAME" = "yumi" ]; then echo "yumiさんです"; fi
- yumiさんです
- $if [ -f hello.sh ]; then echo "存在します"; fi
- 存在します
- $if [ -f nothing.sh ]; then echo "存在します"; else echo "ありません"; fi
- ありません
- $if [ "$NAME" != "devteam" ]; then echo "devteamさんではありません"; fi
- devteamさんではありません
- $if [ -d ~/practice ]; then echo "practiceディレクトリがあります"; fi
- practiceディレクトリがあります
- $if [ -d nowhere ]; then echo "あります"; else echo "ディレクトリはありません"; fi
- ディレクトリはありません
- $unset NAME
- $ls
1行目で./judge.shを実行すると、出力例のとおりSCORE=80が60以上なので「合格です」と表示されます。試しにnanoでSCOREの値を書き換えて再実行すれば、else側の分岐に切り替わる様子も確認できるので、余裕があれば試してみてください。
3行目はifを1行にまとめて書く方法です。[ "$NAME" = "yumi" ]のように、文字列比較には=を使います(数値比較の-eqとは書き方が違う点に注意)。ここで$NAMEをダブルクォートで囲んでいるのは、値が空だったりスペースを含んだりしたときにエラーになるのを防ぐための定番の書き方で、変数を[ ]の中で使うときは基本的にクォートで囲む癖をつけておくと安全です。
4行目の[ -f hello.sh ]はファイル存在チェック※4です。-fは「指定した名前が、通常のファイルとして存在するかどうか」を判定するオプションで、6-2で作ったhello.shが~/scriptsにあるため真になります。5行目では存在しないnothing.shを指定しているため、出力例のとおりelse側の「ありません」が表示されます。ファイルの有無を確認してから処理を分けるこの形は、次のページで作る実用スクリプトでもそのまま使います。
6行目の[ "$NAME" != "devteam" ]の!=は「等しくない」を表す文字列比較演算子です。$NAMEにはSET 2の2行目で入れたyumiが入ったままなので、devteamとは一致せず真になります。7行目の[ -d ~/practice ]の-dは-fのディレクトリ版で、指定した名前がディレクトリとして存在するかを判定します。第1章から使い続けている~/practiceが今もあることを、条件分岐を使って改めて確認できました。
8行目の[ -d nowhere ]では、存在しない名前を指定しているためelse側の「ディレクトリはありません」が表示されます。-fと-dを使い分けることで、「ファイルとして存在するか」「ディレクトリとして存在するか」を明確に区別できます。最後に9行目で練習用のNAMEを片付け、10行目のlsで~/scriptsの中身に変化がないことを確認します。
SET 3 ― forとwhileで繰り返す
- $nano loop.sh
- (#!/bin/bash から始まる下のスクリプトを入力し保存)
- $cat loop.sh
- #!/bin/bash
- for i in 1 2 3; do
- echo "${i}回目の練習です"
- done
- for f in *.sh; do
- echo "スクリプト発見: $f"
- done
- ls *.sh | while read LINE; do
- echo "一覧から読んだ行: $LINE"
- done
- $chmod +x loop.sh
- $./loop.sh
- 1回目の練習です
- 2回目の練習です
- 3回目の練習です
- $./loop.sh | wc -l
- $for i in 1 2 3 4 5; do echo -n "$i "; done
- 1 2 3 4 5
- $echo
- $ls *.sh | wc -l
- $for n in $(seq 1 3); do echo "seqで$n回目"; done
- seqで1回目
- seqで2回目
- seqで3回目
- $cd ~
1行目で書くloop.shには3種類の繰り返しを入れます。最初のfor i in 1 2 3; do 〜 doneはforループ※5のもっとも基本的な形で、1 2 3という並びを順番に変数iへ入れながら、doからdoneまでの処理を3回繰り返します。
2つ目のfor f in *.sh; do 〜 doneは、1 2 3のような固定の並びの代わりに*.shというワイルドカード※6を使っています。これは「今のディレクトリにある.shで終わる名前のファイルすべて」に展開されるため、~/scriptsにあるhello.sh・greet.sh・judge.sh・loop.shを順番に変数fに入れながら処理できます。決まった回数ではなく「ある条件に合うファイルすべて」を処理したいときによく使う書き方です。
3つ目のls *.sh | while read LINE; do 〜 doneはwhileループ※7とread、そして第3章3-3で学んだパイプ|を組み合わせた形です。ls *.shの出力を1行ずつread LINEが受け取り、受け取れる行がある間はずっとループを続けます。for f in *.shと似た結果になりますが、こちらは「コマンドの出力を1行ずつ処理する」という、ログファイルの解析などでも使う非常に汎用的なパターンです。while自体は「条件が真である間はずっと繰り返す」構文で、ここでは「読み込める行がある間」が条件になっています。行が読めなくなった時点で自動的にループを抜けるため、無限ループの心配はありません。
3行目のchmod +xのあと4行目で実行すると、出力例のとおり1つ目のforによる3行がまず流れ、そのあとに2つ目のfor・3つ目のwhileによる出力が続きます。5行目の./loop.sh | wc -lのようにパイプでwc -lに渡せば、スクリプト全体の出力行数を数値でも確認できます。
6行目のfor i in 1 2 3 4 5; do echo -n "$i "; doneは、echoに第1章では触れなかった-n(改行しないオプション)を組み合わせ、1行の中に数字を横並びで表示する例です。7行目のechoだけを打って改行を1つ足し、プロンプトの表示を整えています。8行目のls *.sh | wc -lで、~/scriptsに今何本の.shファイルがあるかを数えます。
9行目のfor n in $(seq 1 3); do 〜 doneは、6-1で学んだコマンド置換$( )とforを組み合わせた応用例です。seq 1 3は「1から3までの数字」を1行ずつ出力するコマンドで、その結果をそのままforの並びとして使うことで、1 2 3と直接書かなくても同じ結果を得られます。数が多くなったときや、変数で範囲を指定したいときに役立つ書き方です。最後に10行目のcd ~でホームディレクトリへ戻り、このページを終えます。

whileループでカウンターを増やす行を書き忘れて、画面が止まらなくなったことあたしもあるよ! そういうときは慌てずCtrl+C。焦って強制終了ボタンとか探さなくても、この2つのキーだけで止まってくれるから覚えておいてね。
まとめ
6-3では、ifによる条件分岐とfor・whileによる繰り返しを使い、judge.shとloop.shという2本のスクリプトを作りました。このページで叩けるようになったコマンド・構文を一覧にまとめます。
| コマンド / 構文 | 何をするか | 覚え方 |
|---|---|---|
if [ 条件 ]; then 〜 fi | 条件が真のときだけ処理を実行する | ifを逆から読むとfi |
if 〜 else 〜 fi | 条件が偽のときの処理も指定する | それ以外は全部こっち |
-eq -ne -gt -ge -lt -le | 数値を比較する演算子 | equal / not equal / greater / less |
[ "$A" = "$B" ] / [ "$A" != "$B" ] | 文字列が等しいか・等しくないかを比較する | 数値比較と書き方が違うので注意 |
[ -f ファイル名 ] / [ -d ディレクトリ名 ] | ファイル/ディレクトリとして存在するか判定する | f = file, d = directory |
echo $? | 直前のコマンドの終了コードを表示する | 0なら成功、それ以外は失敗 |
for 変数 in 並び; do 〜 done | 並びの数だけ処理を繰り返す | 並びを1つずつ変数に入れて回す |
while [ 条件 ]; do 〜 done | 条件が真である間ずっと繰り返す | 止める条件を書き忘れると無限ループ |
$((計算式)) | 数値の計算をする | 二重かっこで算数 |
次のページ「6-4. 引数と関数、実用スクリプト」では、スクリプトの外から値を渡す引数と、処理をひとまとまりにできる関数を学び、実際に自分の仕事を自動化するスクリプトを完成させます。
- test … 条件式が真か偽かを判定する独立したコマンド。
[ 条件式 ]という書き方はtestコマンドの別名にあたる。↩ - 数値比較演算子 …
-eq・-gtなど、2つの数値の大小や一致を判定するための記号のこと。↩ - 終了コード … コマンドの実行結果を表す数値。
0は正常終了(真)、0以外は異常終了(偽)を意味し、echo $?で確認できる。↩ - ファイル存在チェック …
[ -f ファイル名 ]のように、指定した名前のファイルが実際に存在するかどうかを確認する判定。↩ - forループ … あらかじめ決まった並び(数値・ファイル名など)を1つずつ変数に入れながら、その個数分だけ処理を繰り返す構文。↩
- ワイルドカード …
*のように「任意の文字列」を表す特殊記号。*.shは「拡張子が.shのすべてのファイル」に展開される。↩ - whileループ … 指定した条件が真である間、処理を繰り返し続ける構文。条件を偽に変える処理を忘れると無限ループになる。↩