ソースコード・リテラシーのススメ

第11回シェルスクリプトを読む

過去2回に渡ってメールサーバのログ分析を紹介してきました。この分析はすべて筆者の手もとのPlamo Linux環境上で行いましたが、その際に活躍したのがSoftware Toolsと呼ばれるUNIX/Linuxの基本コマンドと、それらを組み合わせて処理を自動化するシェルスクリプトです。

デスクトップ環境が充実した最近のPCからコンピュータに触れた人には、Software Toolsやシェルスクリプトのようなコマンドラインツールは馴染みの薄いものかも知れません。しかしながら、これらのツールはUNIXの初期から磨きあげられ、現在でもUNIX/Linuxの基盤を支えている技術になっています。

シェルスクリプトとは

UNIX/Linux の世界では、CやC++といった本格的な開発言語以外にも、スクリプト言語と呼ばれる簡単に使える言語群があります。CやC++ではソースコードを実行ファイル(バイナリファイル)に変換(コンパイル)しなければ実行できないのに対して、スクリプト言語では変換作業の手間が不要で、通常のテキストファイルとして書いたソースコードをそのまま実行することができるため、日常作業の中で気軽に作成、使用することができます。

スクリプト言語にはさまざまな種類がありますが、UNIX/Linuxの世界で最も古くから存在し、最も広く使われているのはシェルスクリプトでしょう。シェルスクリプトは言語としての機能はかなり貧弱ですが、あらゆるUNIX互換OSに存在しているシェル(sh)を用いて実現しているため互換性が高く、CPUやベンダの壁を越えて、システムの起動や各種設定処理などに広く用いられています。

旧来のUNIX/Linux環境では、起動時の振舞いや各種設定等を変更するには、起動時に実行されるシェルスクリプトや各種サービスを開始するシェルスクリプトをroot権限で直接修正する必要がありました。最近のLinuxではGNOMEやKDEといったデスクトップ環境が整備され、システムに関する設定もGUIツールを用いて行えるようになっていますが、それらツールの多くは背後で動いているシェルスクリプトの設定を変更するためのラッパーで、実際の処理は現在もシェルスクリプトが担っています。

幸いなことに、システムの起動/終了に密接に関係する重要なコマンドであっても、シェルスクリプトの実体は普通のテキストファイルなので、less等のページャで開いたり、エディタで修正することが可能です。

シェルスクリプトの例【その1】

簡単なシェルスクリプトの例として、メールサーバのログを日付ごとのファイルに分解するために書いた拙作のシェルスクリプトを紹介します。

 1	#!/bin/sh
 2
 3	file01=maillog_20080106-20080113
 4	file02=maillog_20080113-20080120
 5	file03=maillog_20080120-20080127
 6	file04=maillog_20080127-20080202
 7	
 8	for i in 6 7 8 9 ; do
 9	   ./mygrep.pl "^Jan  $i" $file01 >> Daily/2008010$i
10	done
11
12	for i in  10 11 12 13  ; do
13	   ./mygrep.pl "^Jan $i" $file01 >> Daily/200801$i
14	done
15	
    ....

まず、1行目の "#!/bin/sh" ですが、これはシェバン(shebang)と呼ばれる指定で、このスクリプトを実行するためのコマンドの絶対パスを指定します。このスクリプトはシェルスクリプトなので、実行するためには /bin/shを起動しています。この部分は、Perlのスクリプトでは "#!/usr/bin/perl"、Ruby のスクリプトでは "#!/usr/bin/ruby" 等に変わります。

3行目から6行目は、メールログのファイル名が maillog_20080106-20080113等と長いので、一々入力するのが面倒だから短い名前で使うためにfile01という変数に代入しています。この代入の結果、$file01という変数名でmaillog_20080106-20080113というファイルが参照されることになります。

8行目から9行目は繰り返しで、i という変数に 6 から 9 までを代入しながら、doneまでの部分が実行されます。また、$file01は先に代入したファイル名が展開されます。その結果、

./mygrep.pl "^Jan  6" maillog_20080106-20080113 >> Daily/20080106
./mygrep.pl "^Jan  7" maillog_20080106-20080113 >> Daily/20080107
./mygrep.pl "^Jan  8" maillog_20080106-20080113 >> Daily/20080108
./mygrep.pl "^Jan  9" maillog_20080106-20080113 >> Daily/20080109

というコマンドが実行されることになります。なお、ここで実行している./mygrep.pl は以下のようなPerlで書いたごく簡単なgrep(というよりは単なるパターンマッチ)コマンドです。

 1	#!/usr/bin/perl
 2	
 3	$key = $ARGV[0];
 4	$file = $ARGV[1];
 5	
 6	open(IN, $file) || die "cannot open $file";
 7	while(<IN>) {
 8	    if ($_ =~ /$key/) {
 9		print $_;
10	    }
11	}

本来のgrep(/usr/bin/grep)は、正規表現等の高度な機能が使える分、今回の作業のように数百Mバイトのファイルを読み込んで処理するには時間がかかりすぎるようなので、必要な機能のみを持ったPerlのスクリプトを書きました。個人的にも意外だったのですが、両者の差は以下に示すくらい(4.3秒対986秒)あるようです。

% time ./mygrep.pl "Jan  5" maillog_20071230-20080106 > /dev/null
./mygrep.pl "Jan  5" maillog_20071230-20080106 > /dev/null  4.35s user 0.62s system 82% cpu 5.996 total
% time grep "Jan  5" maillog_20071230-20080106 > /dev/null   
grep "Jan  5" maillog_20071230-20080106 > /dev/null  986.13s user 1.06s system 99% cpu 16:27.98 total

シェルスクリプトの場合、2008010$iのように、文字列の一部として変数を利用しても、変数名が一意に区別できる場合はそのまま 20080106 等に展開してくれます。ただし、変数名の後ろに文字列が続けたい場合のように、変数名を一意に区別できない場合は{ }(中かっこ)で変数名を括るようにします(たとえば200801006logに展開したい場合は2008010${i}logとする⁠⁠。

このスクリプトでは、ログファイルの各行の先頭にある"Jan  6"や"Jan 10"といった日付を元に、各行を日付ごとのファイルに分割しています。シェルスクリプトは、このような「きちんとしたプログラムを書くほどではないけど、繰り返しが多くて手動では面倒な作業」を処理するのにきわめて便利な道具です。

シェルスクリプトの例【その2】

もう一つ、簡単なシェルスクリプトの例として、筆者がまとめ役をしているPlamo Linuxで使っている、ntpdと呼ばれる時刻同期デーモンを起動するスクリプトを見てみましょう。

 1	#!/bin/sh
 2	
 3	killproc() {	# kill named processes
 4		pid=`/bin/ps -e ¦ /usr/bin/grep $1 ¦ /usr/bin/sed -e 's/^  *//' -e 's/ .*//'`
 5		[ "$pid" != "" ] && kill $pid
 6	}
 7	
 8	case "$1" in
 9	'start')
10		ps -e | grep ntpd > /dev/null 2>&1
11		if [ $? -eq 0 ]
12		then
13			echo "ntp daemon already running. ntp start aborted"
14			exit 0
15		fi
16		if [ -f /etc/ntp.conf -a -x /usr/bin/ntpd ]
17		then
18			/usr/bin/ntpd -c /etc/ntp.conf
19		fi
20		;;
21	'stop')
22		killproc ntpd
23		;;
24	*)
25		echo "Usage: /etc/rc.d/init.d/ntp { start ¦| stop }"
26		;;
27	esac

Plamo Linuxでは、このシェルスクリプトを/etc/rc.d/init.d/ntpという名前で用意して、システムの起動時にntpdを起動するようにしています。

1行目はシェルスクリプトのお約束であるシェバンの指定です。

3行目から6行目はkillprocという関数(サブルーチン)を定義しています。シェルスクリプトでは "関数名(){...}" という形で一連の処理を関数として定義することができます。関数には引数を与えることができ、与えられた引数は関数の中では $1, $2 として参照することができます。

この関数はその名前が示すように、指定されたプロセスをkillするためのもので、引数としてプロセス名を渡すと、4行目の処理でそのプロセスに割り当てられたプロセス番号を調べ、プロセス番号が存在する場合は5行目でそれをkillします。

4行目の処理はps, grep、sedという 3 つのコマンドをパイプ¦で連結して、その結果をpidという変数に代入しています。まず、ps -e ですべてのプロセスを出力し、その中からgrepで($1に展開される)ntpdという文字列を調べ、最後にsedでプロセス番号以外の部分を除去する、という処理になっています。

5行目の角かっこ[ .. ]は条件式の評価で、この例では "$pid" が ""(空の文字列)と等しくない!=かどうかをテストしています。この条件が成立する(すなわち$pidに何か文字列が入っている)場合は、&& で結ばれた右辺が実行され、指定したプロセスのID番号になっている$pidを元にそのプロセスをkill します。

残りの8行目から27行目がこのシェルスクリプトの本体部分です。この部分では、スクリプトに与えた引数によって起動、終了という2つの動作を切り替えるようになっています。このスクリプトは start か stop という引数を取り、startが与えられた場合は9行目から20行目の処理が実行され、stopが与えられた場合は21行目から 23 行目の処理が実行されます。

シェルスクリプトのcase .. esac文は条件分岐で、case に続く文字列がstart だったら 'start') から20行目の ;; の部分が、stop だったら'stop') から 23行目の ;; の部分が実行されます。start や stop 以外の文字列が与えられた場合は 24行目の*) の条件がマッチし、echo 文によってusage(使い方)が表示されるようになっています。

8行目の case "$1" in の $1 はこのスクリプトに与えた引数に置換されます。すなわち、/etc/rc.d/init.d/ntp start とした場合は $1 は start になり、/etc/rc.d/init.d/ntp stop とした場合は$1 は stop になります。

9行目からの start時の処理では、まず10行目でntpdがすでにプロセスとして動いているかを調べ、もし動いていた場合は12行目から15行目で、すでにntpが動いている旨のメッセージを出力して終了、動いていなかった場合は16行目で設定ファイル(/etc/ntp.conf)と実行ファイル(/usr/bin/ntpd)の存在を確かめてから、ntpdを起動します。

11行目の if [ $? -eq 0 ] の $? は直前に実行したコマンド(この例ではgrep ntpd)の返り値になり、grepがパターンマッチに成功した場合(ps -e の表示の中に ntpd という文字列が含まれる場合)は 0、そうでない場合は 1 になります。-eq は左辺と右辺が数値として等しいかを調べる演算子です。

16行目の if [ -f /etc/ntp.conf -a -x /usr/bin/ntpd ]「/etc/ntp.confというファイルが存在するか(-f⁠⁠」というテストと「/usr/bin/ntpd というファイルが実行可能か(-x⁠⁠」というテストを「and ⁠-a⁠⁠」で結んだ条件のテストです。両者の条件が満たされる場合に18行目のコマンドが実行され、ntpd が起動されます。

今回は2つの簡単なシェルスクリプトを紹介しましたが、シェルスクリプトの雰囲気は感じてもらえたでしょうか?今回の例でも示したように、シェルスクリプトでは、ファイル操作や文字列置換等、実際の処理を行うのは外部コマンドで、シェルスクリプト自身には、外部コマンドをどのような順番で実行するか、あるいはどのような条件の際に実行するかを判断するといった程度の機能しかありません。

PerlやPython, Rubyといった新しいスクリプト言語では、言語自身に十二分な機能が用意されているので、その言語の知識だけでコードを書くことができるのに対し、シェルスクリプトではシェルスクリプトの知識だけではなく、UNIXの基本コマンド、いわゆるSoftware toolsの知識が必要となる上、今回も紹介したような表記上のクセも強いため、シェルスクリプトはとっつきにくいと感じる人が多いようです。しかしながら、最初にも述べたように、シェルスクリプトはUNIX/Linux環境を支える基盤技術ですし、Software Toolsと組み合わせたシェルスクリプトは一々手動でやるには面倒だけど、本格的なコードを書くほどでもない小さな仕事を効率よくこなすきわめて便利なツールになります。本連載では、これからしばらくSoftware Toolsの解説等も交えながら、さまざまなシェルスクリプトを読んでいく予定なので、これを機会にシェルスクリプトに関心を持っていただければ幸いです。

おすすめ記事

記事・ニュース一覧