連載
» 2000年09月20日 00時00分 公開

ステップ・バイ・ステップ・シェルスクリプト(4):制御文で道案内

スクリプトやプログラム言語で、基本となるのは制御文です。多くの言語では条件分岐などのためにifやwhileが使われています。制御文とはまさにプログラム中の「道先案内人」です。

[さかいひろあき,@IT]

条件分岐のバリエーション

 いろいろなコマンドを連続して実行していく場合、そのコマンドが使われる局面で結果が変わってきます。その結果により処理の仕方を変えるものです。それがバッチ系プログラムの基本ともいえるでしょう。少しでもプログラムを組んだことのある人なら、こうした動きは体得していると思います。bashの場合はどうでしょうか? 実はbashの条件分岐にはいろいろバリエーションがあるのですが、代表的なものをとりあげてみましょう。

if文とcase文

 以下の例は、すべて実際に動いたものをコピーして貼り付けていますので、そのまま実行することができるはずです。一度自分の環境でもコマンドを作成して実行してみるとよいでしょう。

 まずは制御文の基本となるif文です。条件が「真」である部分のコマンドが実行されます。ifで始まり、かならずfiで終了します。elifelse部分はもちろん省略可能です。なお、bashではシャープ(#)以降行末まではコメントとみなされます。

 最初のword=2の部分の数値をいろいろ変えて試してみてください。

word=2 #いろいろ変えてみよう
if [ $word -eq 1 ]; then
echo "if"
elif [ $word -eq 2 ]; then
echo "elif"
else
echo "else"
fi

 次は、複数の条件に対応するcase文です。

word="b" #いろいろ変えてみよう
case $word in
"a")
echo "a"
;;
"b")
echo "b"
;;
"c")
echo "c"
;;
*)
echo "d"
esac 

 case文は、必ずcaseではじまりesacで終了します。各条件文はそれぞれ2つのセミコロンで区切られます。値(word)の内容とパターン(‘a’?‘c’)が一致した部分のコマンドが実行されます。どのパターンも一致しない場合は、“*)”の部分のコマンドが実行されます。この“*)”は、ファイルを展開するワイルドカードとは別物です。

繰り返しのためのwhile文とfor文

 特定の条件が満たされるまで処理を繰り返す構文もあります。while文では、条件が「真」のあいだ、doからdoneに挟まれたコマンドが繰り返し実行されます。下記の例では、変数iが1づつ加算されていき、iが10より小さい間(“-lt”は“less than”の意)繰り返されます。

i=0
while [ $i -lt 10 ]; do
echo $i
i=`expr $i + 1`
done 

 for文では、inの後の値を順番に変数nameへ入れながら、その値の数だけ処理が繰り返されます。1回目のループでは変数nameの値は‘a’、2回目は‘b’……となります。

for name in a b c d ; do
echo $name
done 

ファイルを探すスクリプトを作ろう

 ではここまで紹介した命令を組み合わせて、実際にシェルスクリプトをつくってみましょう。題材として指定した文字列を含むファイルを、再帰的にサブディレクトリまで探すコマンドをとりあげたいと思います。このようなコマンドは、すでに既存のコマンドで実現されているかもしれません。しかし、もしそのようなコマンドの存在を知らなくとも、身近なコマンドの組み合わせで実現できるのがシェルの強みなのです。

 まず、以下のファイルを作成し、「rec_grep」(recursive grepの意)という名前で保存します。

#!/bin/bash
grep "bash" `find .`

 1行目の#!/bin/bashは、コメントではありません。Linuxのスクリプトでは1行目が#!ではじまる場合、そのうしろを実行ファイルと認識し、そのコマンドを使って以下の行のスクリプトを実行します。ここでは、このスクリプトは/bin/bashを使って実行するよう明示しているのです。この1行目は省略可能ですが、他のシェルスクリプトから実行された場合、bashで実行されないことがあります。これからは必ず書くようにしましょう。

 grepというコマンドは、指定された文字列を含む行を検索し表示するコマンドです。通常は、

$ grep [検索したい文字列] [検索したいファイル名]

 という形で実行します。「rec_grep」の例では、“ `find .` ”が[検索したいファイル名]に該当します。

 bashでは、逆シングルクオーテーション(`)で囲まれた部分は、「その中のコマンドを実行し、実行結果(つまり標準出力)をコマンドラインの一部とする」という意味をもちます。これはbashが逆シングルクオーテーションで囲まれたコマンドの標準出力を受け取り、コマンドラインに追加するという働きをしています。

 findというコマンドは指定されたディレクトリ以下のファイル名を検索するというコマンドです。findコマンドを以下のように単独で実行した結果、

$ find .
helloworld rec_grep

という結果が得られたとすると、上記のシェルスクリプトは

$ grep "bash" helloworld rec_grep

を実行したのと同じことになります。つまり、そのディレクトリ以下にあるすべてのファイルの中の“bash”という文字を探す、ということです。

 ところでこのシェルスクリプトrec_grepはそのままではあまり役に立ちませんね。上記は“bash”という文字列で検索していますが、この検索したい文字列が変わるたびにシェルスクリプトの内容を変更しなくてはなりません。そこで汎用コマンドとして、もっと使い勝手がいいように親切なコマンドにしたいと思います。順番に以下の機能を追加していくことにしましょう。

  1. 実行時の引数に「検索文字列」及び「検索開始ディレクトリ名」を指定することができるようにする。
  2. 引数が1つも指定されなかった場合には、rec_grepの使い方を表示して終了するようにする。
  3. ディレクトリが指定されなかった場合は、カレントディレクトリが指定されたものとする。

 では早速順番に作って行くことにしましょう。

引数を取り込めるようにする

 引数については、前回お話しました。では自分で作成したシェルスクリプトで、引数を理解して処理していくにはどのようにするのでしょうか?

bashでは与えられた引数の順番により、$1$2$3…….という変数に引数が自動的に格納されます。ちなみに$0には実行されたシェルスクリプト自身のファイル名が格納されます。では上記で作成したシェルスクリプトを引数をとるように変更してみます。

#!/bin/bash
grep $1 `find $2`

では、このスクリプトを早速実行してみましょう。今回のrec_grepは2つ引数を指定します。

$ rec_grep bash /etc
/etc/cron.weekly/makewhatis.cron:#!/bin/bash
/etc/crontab:SHELL=/bin/bash
grep: /etc/default: Is a directory
……以下略……

 “bash”と“/etc”という2つの引数を与えたことにより、“/etc”以下のファイルすべての中から、“bash”を含むファイルを検索し、表示しています。

エラー表示がどうも気になる

 これを実行して気になったことが2つあります。1つは/etcの下にディレクトリがあった場合、“Is a directory”と出力されてしまうこと。これはgrepコマンドがディレクトリを指定された場合に、エラーメッセージとして表示しています。もう1つは、読み込む権利のないディレクトリが存在した場合、findコマンドがエラーメッセージを出してしまうことです。

 これらは通常このスクリプトを使う範囲では問題ないように見えますが、前回お話したように、シェルスクリプトの基本は標準入出力の「すりかえ」です。もしこのようなエラーが表示されてしまっていると、このrec_grepの結果をさらにほかのコマンドに読み込ませて処理する場合に困ったことになる可能性があります。そこで、これらのエラー表示を取り除くよう、さらにシェルスクリプトを次のように改良しましょう。

#!/bin/bash
grep -s $1 `find $2 -type f 2> /dev/null`

 grepの-sオプションは、「エラー表示をしない」というオプションです。また、findの-type fというオプションは「通常ファイルしか対象にしない」つまり「ディレクトリを取り除く」というオプションです。さらに、その後ろの「2>」とはリダイレクトの一種で、「標準エラー出力のみリダイレクトする」というものです。

 前回、標準入力と標準出力についてお話しましたが、実はもうひとつ「標準エラー出力」というのがあります。これはエラー情報など、通常の出力とは別の情報を標準出力とは別の「橋」を使って出力する機能です。findコマンドは通常の出力結果以外に、上記のようにエラー情報(ディレクトリにリード権がないなどのエラー)を、この標準エラー出力に出力します。今回はこのエラー情報は出力したくないので、これを/dev/nullというファイルに出力しています。

 /dev/nullというのは通常のファイルではなくて、「ごみ捨て場」とでもいうべき特殊ファイルです。この/dev/nullにリダイレクトした場合、その情報はどこにも出力されずに捨てられます。もちろん/dev/nullの中身を後から見てもその情報が入っているわけではありません。完全に捨てられるわけです。これらの変更で、エラー表示は行われなくなるでしょう。

細かいチェックを組み込もう

 このrec_grepはとても便利なので、友人にも使ってもらうことになったとします。その友人はまだ使い方になれていないので、間違って引数なしで実行してしまうかもしれません。ところが、引数なしで実行してしまうとなにやら変な結果となってしまいます。そこで、引数のチェックを初期処理としていれてあげることにしましょう。ただしrec_grepはカレントディレクトリ以下の検索で使用することが多いので、2つめの引数(ディレクトリの指定)は省略可能とすることにします。ついでに、ディレクトリが指定された場合、そのディレクトリが本当に存在するのか、存在チェックもやってしまいましょう。例を以下に示します。

#!/bin/bash
# 引数の数チェック
if [ $# -lt 1 -o $# -gt 2 ]; then
echo "Usage: rec_grep expr [directory]"
exit 1
fi
# 第2引数セット(省略された場合カレントディレクトリ)
if [ -n "$2" ]; then
DIR="$2"
else
DIR="."
fi
# ディレクトリ存在チェック
if [ ! -d "$DIR" ]; then
echo "$DIR" " is not a directory"
echo "Usage: rec_grep expr [directory]"
exit 1
fi
# コマンド実行
grep -s $1 `find $2 -type f 2> /dev/null`
exit $?

 いかがでしょうか? 少しはスクリプトらしくなってきたでしょう。では順に解説していきたいと思います。

 まず、最初のステップでは、引数の数の判定を行っています。

if [ $# -lt 1 -o $# -gt 2 ]; then
echo "Usage: rec_grep expr [directory]"
exit 1
fi

 rec_grepは引数を1つまたは2つしか許しませんので、それ以外の場合はメッセージを表示し、exitを実行して終了しています。シェルスクリプトにかぎらず、UNIXでは正常終了した場合は0、そうでない場合は0以外を終了ステータスとして返す慣習になっています。あまり気にすることはありませんが、お行儀のよいスクリプトにするためにつねに終了ステータスは返すようにしましょう。

 if文の中で使われているドルシャープ記号($#)は、引数の数を表します。-ltは以前にもでてきましたが、「より小さい」(数学では‘<’)を表します。bashでは数学記号の‘<=’や‘>=’ではなく、このような表記で数字の大小を判定します。少しなじみにくいかもしれませんが、使っているとすぐになれると思います。一覧表をあげておきます。

  • -eq 等しい (equal)
  • -ne 等しくない (not equal)
  • -lt 小さい (less than)
  • -le 小さいまたは等しい (less than or equal)
  • -gt 大きい (greater than)
  • -ge 大きいまたは等しい (greater than or equal)

 さらにif文の中を見ていきましょう。“$# -lt 1”と“$# -gt 2”のあいだに“-o”という文字列があります。これは「または」を表します。「OR」の意味です。「AND」の意味をもたせたい場合は “-a” とします。

 次のステップでは、第2引数が指定されているかどうかチェックしています。

if [ -n "$2" ]; then
DIR="$2"
else
DIR="."
fi

 “-n”というのは、文字列の長さが0でない場合「真」となります。この場合は、第2引数が与えられていれば、それを変数DIRに格納し、そうでなければ‘.’(カレントディレクトリ)をセットします。

 その次のステップでは、引数で与えられたディレクトリの存在チェックをしています。

if [ ! -d "$DIR" ]; then
echo "$DIR" " is not a directory"
exit 1
fi

 -d はディレクトリが存在した場合「真」を返します。この場合は「存在しなかった場合」としたいので、エクスクラメーション(!)を使って真偽を反転させてやります。このようなファイル判定オプションはたくさんあるのですが、代表的なものを紹介しておきます。

  • -e ファイルが存在する(ディレクトリを含むどのようなタイプのファイルであっても)。
  • -f レギュラーファイル(ディレクトリ等を除く)が存在する。
  • -d ディレクトリが存在する。
  • -r ファイルが存在し、リード権がある。
  • -s ファイルが存在し、フィルサイズが0でない。

 上記のチェックを通過し、ようやくここまでたどりつきました。コマンドの実行です。これはすでに上記で説明していますので特に触れることはありません。

 最後は特に何も書かなくても、それ以上コマンドがなければ自動的にシェルスクリプトは終了します。ただ、上記でも触れたようにお行儀のよいスクリプトにするためにかならず終了ステータスを返すようにしましょう。

exit $?

 ここでは、「grepの終了ステータス」をそのままexit文で返しています。“$?”は、直前のコマンドの終了ステータスを表します。もし万一grepコマンドが思ったように動かずエラーを返したとすると、rec_grepも同じステータスを返すようにしています。こうすることにより、将来rec_grepを他のシェルスクリプトの組み込んだ場合に、rec_grepから返ってきたステータスを判定することによりエラーハンドリングができるわけです。

 今回のrec_grepのお話は、ここまでです。パスの通ったディレクトリに保存しておき、いつでも使用できるようにしておきましょう。


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。