Shell Scriptist的な小話 目次

序文

1.基本的なこと
1-1.どのシェルを使うべきか
1-2.シェルはプロセスを起動する

2.エラーは成長の糧
2-1.Bad Interpreter!

3. How To
3-1.テンポラリファイルの作り方
3-2.処理の途中 Ctrl-C で中断するとき

テンポラリファイルの作り方

複雑な処理を行う時には、テンポラリファイルを使うことがよくあります。
このような場合、次のことに注意しましょう。

(1)テンポラリファイルを作る場所と、ファイルの名前
(2)不要になった時点で削除する


(1)テンポラリファイルを作る場所と、ファイルの名前
単純に /tmp/temp.txt のような名前にすると、同じスクリプトを複数実行する場合に問題が発生します。
このような状況を想定して、テンポラリファイルを作成するときには、プロセスIDや時間で、テンポラリファイルの名前を修飾してあげましょう。
例)
CURRENT_DATE_SECOND=`date +%s`
TEMP_FILE_NAME1=/tmp/temp1_$$_${CURRENT_DATE_SECOND}.txt
TEMP_FILE_NAME2=/tmp/temp2_$$_${CURRENT_DATE_SECOND}.txt
:
first_process > ${TEMP_FILE_NAME1}
:

first_processは、何か適当な外部コマンドか、bash関数とします。
$$には、実行中のプロセスIDがセットされています。

また、date +%s は、1970/01/01 00:00:00 から経過した秒数を返します。

このようにすることで、テンポラリファイル名が、起動プロセスと起動した時間(秒)でユニークな名前になります。

(2)不要になった時点で削除する
処理が終わった後は、テンポラリファイルは不要になります。
残しておくとディスクスペースを圧迫しますので、忘れずに削除しましょう

処理の途中 Ctrl-C で中断するとき

実行中のスクリプトを、Ctrl-Cで終了させるとき、途中で要らなくなったテンポラリファイルを削除する等の、終了処理を実行させたいことが多々あります。このようなときは trap コマンドを使いましょう。

trapはとは、指定したシグナルを補足して、その後の処理を指定するためのコマンドです。
次の書式で使用することができます。

trap コマンド シグナルリスト

シグナルリストには、trapで補足したいシグナルの番号を書きましょう。
Ctrl-Cによって送られてくるシグナル(※)番号は2ですので、今回は、このシグナルリストには 2 を指定します。
コマンドには、実行したいコマンドやシェル関数名を書きます。

※シグナルとは何かについては、ここでは詳しく説明しませんが、とりあえず「何か実行中にCtrl-Cを押すと、実行中のプロセスに、番号2のシグナルが送られる」ということだけ覚えておいてください。

テスト用に、次のようなコードを用意しました。
このコードは、10秒間眠り、10秒後に0を返して終了する、という単純なコードです。
#!/bin/bash

# Ctrl-Cが押されたら、自動的に signalExit が呼ばれるようになる
trap signalExit 2

# signal受信時の終了処理
function signalExit() {
echo "signal recieved!"
exit 2
}

# 通常の終了処理
function normalExit() {
echo "ok."
exit 0
}

# 10秒間なにもしない
sleep 10

# 10秒後、正常終了
normalExit
このコードを trap_test.sh という名前で保存して、一度実行してみましょう。
10秒後に、ok. と表示されたと思います。

それでは、今度は実行中に、キーボードからCtrl-Cを押してみましょう。
signal recieved!
と表示されたと思います。

このようにすれば、Ctrl-Cで途中で処理を止めた時、定型処理を自動で実行させることができるようになります。

また、今回指定したtrapを解除したいときには、

trap 2

と、コマンド指定をせずにシグナル番号を指定してください。

シグナル番号9(SIGKILL)は、プロセスを強制終了させるためのシグナルですので、trapで補足することはできません。このシグナルを受信したプロセスは、直ちに終了してしまいますので、注意してください。

bad interpreter!

ある日Shell Scriptを書いて実行しようとしたとき、いきなりエラーに遭遇しました。
それは、シェルの構文が間違っていたとか、存在しない変数名を使ったとか、そういうわかりやすい間違いではありませんでした。

作成したスクリプトを、hogehoge.sh というファイル名で保存し、実行してみたのですが
-bash: ./hogehoge.sh: /bin/bash^M: bad interpreter: No such file or directory
と表示されしまい、エラーとなってしまいました。
さて、一体このファイルの内容のどこに問題があったのでしょうか。

それは、ファイルの先頭行に書かれている
#!/bin/bash
の部分に原因がありました。
この1行は、このファイルの内容が、/bin/bashのスクリプトであることを明示するための宣言文です。
ここにエラーが出るとは、夢にも思っていませんでしたので、物凄く混乱した記憶があります。

右往左往した後に、エラーメッセージをよく読んでみると、
/bin/bash^M
なにか変なことに気がつきました。
記号「^M」の存在です。

この記号が、すべてを物語っていました。
UNIXの改行コードは\n。
Windowsの改行は\r\n。
そうです、この^Mというのは、Windowsの改行コードに含まれる\rだったのです。
そういえば、このファイルは、Linuxマシンに転送する前に、Windows上のテキストエディタで書いていました!

つまり、
/bin/bash という名前でシェルのプログラムを指定しなければいけないところだったのですが、/bin/bash^M という名前を指定していました。
指定されたフォルダには、bashという名前の実行ファイルはありますが、bash^Mというファイルは存在しません。
だから No such file or directory というエラーが発生していたんですね。

解ってしまえばどうということはないのですが、最初に遭遇した謎のエラーでした。


シェルはプロセスを起動する

シェルとは、OS-ユーザー間のインタフェースです。
ユーザーは、シェルに対して「これこれこういう仕事をしてくれ」と入力します。
シェルはその入力された内容を解釈して、OSに作業内容を伝えます。
その動きを、実際にシェルを操作しながら確認してみましょう。
シェルから次のように入力してください。

ps
ps とは、実行されているプロセスの一覧を表示するためのプログラムです
(プログラムは、プロセスという処理単位で実行されます)。
すると、次のような結果が得られると思います。

PID PPID PGID WINPID TTY UID STIME COMMAND
5368 1 5368 5368 con 1006 00:48:49 /usr/bin/bash
5532 5368 5532 5512 con 1006 00:50:05 /usr/bin/ps

今 ps と入力したので、シェルはpsというファイル名を検索して、実行しました。
OSから見ると、bashもプロセスですので、ここに表示されています。
その下の行にある、psについても同様です。

よく観察してみると、PIDやPPIDというカラムに分けられていることがわかります。
PID、PPIDとはそれぞれ、プロセスID、親プロセスID(Parent Process ID)と言います。
意味はその名の通り、プロセス自身に割り当てられたID、このプロセスを起動したプロセスのIDとなっています。
プロセスIDは、実行するときにOSから自動的に割り振られます。

ここで、ps のPPIDが、bashのPIDとなっていることがわかります。
もうおわかりの方もいると思いますが、入力された ps という文字を解釈して、実行したのは bash なのです。
だから ps のPPIDには、bashのPIDがセットされているのです。

このように、シェルとはユーザーの入力内容に従って、プロセスを起動しながら仕事をしていきます。

ちなみに、ps を呼び出した bash の PPIDは1となっていますが、この1という PID は、OSが実行している init というプロセスのIDです。
すべてのプロセスの親を遡っていくと、initにたどりつきます。

どのシェルを使うべきか

sh、csh、tcsh、bash、zshなど、シェルの種類は豊富です。
案件の内容、使っているOSなどから、自由に選択できないかもしれません。
基本的な文法が変わってきますので、できるだけ仕事で使うもので慣れ親しんでおくのがよいと思います。

このコンテンツで扱っているのは、bashです。
bashは、Linuxのディストリビューションであれば、ほぼ標準で使えます。
参考程度ですが、Windows環境のCygwinインストーラに用意されているシェルを書いておきます。

ash
bash
tcsh
zsh
(2008/05/07現在)

序文

このブログには、シェルスクリプト、主にBashを中心としたTipsや小話を書いていこうと思います。

過去受託開発業務に従事していた私は、実際に2年くらいシェルを中心とした業務に携わっていたことがあり、それなりの量のコードを書きました。
顧客の名前や業務内容を明かすことはできませんが、SoralisやLinuxで、24時間、365日稼働するシステムの大部分を、シェルスクリプトで実装していました(もちろん、パフォーマンスが重要視されていた部分の開発には、C言語等も使いましたが)。
シェルで実装するという方針は、顧客の要望であり、私が決めたわけではありませんでしたが、もともとUNIXやLinuxが好きだったこともあり、個人的にはとても楽しく仕事ができました。

それから時は過ぎ、当時の受託開発業務から足を洗い、現在は自社サービス開発(主にWeb)を担当するようになりましたが、今でも(というよりもむしろ今だからこそ)シェルスクリプトが手放せなくなっています。

Webサービスというものは、明確な仕様を決め、いつまでにいくらで何人で作るという契約を結ぶことよりも、世の中の流行り廃りを見ながら、面白いサービスを迅速にリリースすることのほうが重要であると思います。

この「迅速なリリース」という命題を満足させるための1つの解は、「手作業を減らす」ことだと思います。
言いかえると、反復が必要な作業や、ルーチンワークの類を、人間の手で片づけないということです。
もう少し具体的な例を挙げると、「result.txtに保存されているユーザーリストの中から、誕生日が4月のユーザーを抽出する」、「毎月の、イベント商品マスタの更新」といった作業などです。

どのように時代が変わっても、サービスや製品の裏側には、常に解析や更新の対象となる膨大なデータが存在しています。
これらのデータいかに迅速に、正確に処理するか。
これこそWebサービスを開発して、運営してくことの真髄だと思っています。

扱う件数が10や20であれば大した問題ではないかもしれませんが、では1万件だったらどうでしょうか?
こういった作業は、人間の目と手で片づけようとすると、非常に煩雑な作業になります。目は疲れ、精神的にも疲労し、ミスを誘発しますので、プログラムに仕事をさせるべきです。

長い前置きになってしまいましたが、シェルスクリプトとは、先ほどの例であげたような面倒な処理を、手軽に迅速に片づけるための、強力なツールです。
この素晴しいツールの魅力を、実例とともに紹介しながら、少しでもシェルスクリプトの面白さをおすそ分けできたら嬉しく思います。