前回は実用的なシェルスクリプトの例として、Plamo Linux用のupdatepkgコマンドの雛形を考えてみました。今回はシェルスクリプトの重要な機能である関数(サブルーチン)を使って、このコマンドに前回宿題として残しておいたバージョンチェック機能を追加することにします。
バージョン取得用関数
オープンソースソフトウェア(OSS)の世界では、「早めの公開、しばしば公開(release early, release often)」を標語にバージョンアップが頻繁に行われ、バージョン番号はそれらの新旧を見分ける重要な指標です。
以前にも紹介したように、バージョン番号の付け方に統一されたルールはなく、ソフトウェアプロジェクトごとに独自のルールで運用しているのが実情ですが、最近のソフトウェアプロジェクトでは「 “.”(ピリオド)を区切りにした3ケタ程度の数字」で、「各ケタは左からメジャー番号、マイナー番号、リリース(パッチ)番号を示す」程度の合意はあります。
前回紹介したようにPlamo Linuxの場合、/var/log/packages/ディレクトリにパッケージのベース名のファイルが作成され、そこにパッケージに関する情報が記録されています。
バージョン番号はパッケージ名の一部として PACKAGE NAME: 行に記録されているので、インストール済みのパッケージのバージョン番号はこの行から取り出すことができます。バージョン番号を取り出す作業は他の処理とは独立しているので、関数として作成しておけば便利でしょう。
シェルスクリプトでは、関数は関数名の後ろに( )(丸カッコ)を付けて定義し、処理は { } (波カッコ)で囲んだ部分に記述します。この関数をget_version()という名前にしてみましょう。
2行目と3行目にあるlocalという宣言は、指定した変数をこの関数のみで参照できるローカル変数に定義して、外部での変数名の衝突を防いでいます。
2行目にある$1は関数に与えた引数の指定で、呼び出し側からget_version bashのように関数を呼び出すと、$1は1つ目の引数であるbashに置換されcat /var/log/package/bashが実行されます。
パイプの後半ではgrepコマンドで“PACKAGE NAME”という行を取り出して、次のcutで “:”(コロン)を区切りに2つの部分に分け、パッケージ名を含む2つめの部分を取り出します。この処理の結果を$old_nameという変数に代入しておき、3行目では、この変数からcutで“-”(ハイフン)を区切りにパッケージ名を分割し、バージョン部分である2つめの部分を $old_vers に代入し、4行目で $old_vers をechoで呼び出し元に返すようにしています。
シェルスクリプトでは、関数を実行した結果の戻り値は return で指定するか、最後に実行したコマンドの結果として返すことができます。ただし、returnで返せる値は数字に限られており、バージョン番号のような文字列を返すことはできませんので、ここではechoを実行した結果として$old_versを返すようにしました。
バージョンチェック用関数
次に、インストール済みのパッケージと新しくインストールしようとしているパッケージのバージョンを比較する処理を作成しましょう。この処理は2つのバージョン番号を受け取って比較する関数にしてみます。
この関数では5行目から11行目のループの部分で、x.y.zと表記されるバージョン番号に対し、cutコマンドを使って“.”を区切り文字(-d'.')にし、左から順に各欄を取り出して(-f$i)新旧の比較をしています。
8行目はシェルのtest 機能([ .. ])を用いて、2番目の引数に指定したバージョン名の各欄の値$t2が、最初の引数の各欄の値$t1よりも大きいかどうか(-gt)を調べています。
x.y.zと表記されるバージョン番号のうち、どこか1つでも$t2の方が$t1よりも大きければ、2番目の引数で指定した$newの方が$oldよりも新しいと判断できるので、ループ処理を打ち切って、その大きかった数字を9行目のreturn $t2で返しています。
一方、バージョン番号の各欄を全て調べても最初の引数($old)の方が大きかった場合は、12行目で0を返すようにしました。このようにしておけば、呼び出し側で戻り値を調べて0以外の数字が帰ってくれば、2番目の引数に与えたバージョンの方が新しく、0が戻れば最初の引数のバージョンの方が新しいと判断できるでしょう。
なお、先にバージョン番号は「3ケタ程度の数字」と述べましたが、Linuxカーネル(2.6.27.10)やFirefox(2.0.0.18)のように4ケタを使っているソフトウェアも存在するため、念のため5ケタ分をチェックするようにしています。
改訂版スクリプト
それでは、今回作った関数を前回のスクリプトに組み込んでみましょう。
3行目から21行目が今回作成した関数です。シェルスクリプトの関数は実行される前に定義しておかないといけないので、スクリプトの先頭部分に置いています。
33行目と34、35行目が今回作成した関数を呼び出している部分です。単純に実行するだけならば関数名に引数を付けて get_version $base などととすればいいのですが、33行目では関数を実行した結果を $old_vers という変数に代入したいので、関数を呼び出す部分を `(バッククォート)で括って、実行結果を $old_vers に代入するようにしています。
一方、34,35行目は関数が return で返す値を受けとる例です。34行目では check_vers という関数に$old_vers, $vers という引数を与えて実行します。関数からreturnで返される値は$?というシェルの特殊変数に収められるので、35行目で$?の値を$ver_testという変数に代入しています。
36行目からは$ver_testの値を調べて、0でなければ新しくインストールしようとしているパッケージの方が新しいので古いバージョンを削除します(38行)。一方、$ver_testが0ならば、すでにインストールされているパッケージの方が新しくインストールしようとしているパッケージよりも新しいので、エラーメッセージを出してインストールを中止します(39、40行)。
オプション解析機能
いくつかのパッケージで動作テストしてみると、たいていは上記のコードでうまく行くのですが、openssl-0.9.8i等、バージョン番号に数字以外のアルファベット等が付いている場合、バージョンチェックが正しく動作しないようです。
原因は-gtに代表されるシェルスクリプトの比較演算子が整数の大小を見分ける機能しか持たないことで、アルファベット等の文字列を比較しようとするとエラーになってしまいます。また、比較部分を文字列の比較ができるsortを使っても書き直したとしても、1.2rc1と1.2のどちらが新しいバージョンかを見分けることはできないでしょう。
バージョン番号に数字以外が含まれた場合の処理を考えるのは面白いテーマになりそうですが、条件が多様すぎて簡単には扱えそうにありません。そこで今回はバージョンチェックを無視して強制的にインストールするオプションを追加する機能を入れておきましょう。
この処理は、オプションとして-fと-hを受け付け、-fならば強制インストール用のフラグ($force_flag)を立て、-hならばusageという関数を実行する、というものです。
1行目の $* は引数全てを意味し、すべての引数を1つずつ$optに格納して、$optが-fならば 5行目の処理を、-hならば7行目の処理を行います。5行目でフラグを立てた後のshiftは引数の並びを一つ左にシフトして使用済みの引数(-f)を除くための処理です。
完成版スクリプト
オプション解析機能を追加し、ヘルプメッセージの表示機能も関数化した完成版のコードはこうなりました。
23から28行目が、以前はスクリプトの先頭部分に置いていたメッセージ表示機能を関数化したusageです。この関数は呼びだされると指定したメッセージを表示してスクリプトを終了(exit)させます。
30から38行目がオプションの解析部分で、オプションはパッケージ名よりも先に処理する必要があるためこの部分に置いています。
40から42行目は-fオプションを除いても引数(パッケージ名)が残っているかをチェックし、残っていなければコマンドの使い方が間違っているので、usageを呼び出してメッセージを出力、終了します。
パッケージをインストールする44行目からのループでは、50行目に$force_flagをチェックするif文を追加して、$force_flagに値が入っていれば、53行目からのバージョンチェックを行わず、古いパッケージをアンインストールするようにしました。
数字以外が含まれた際のバージョンチェックやパッケージごとの強制インストールの有無など、もう少しイジりたい気もしますが、あまり複雑にすると見直した時に自分でもわからなくなりがちなので(苦笑)、今回のupdatepkgコマンドはこれくらいで完成ということにしておきましょう。
今回はシェルスクリプトを作っていく過程を順を追って紹介しました。たいていのソースコードは、今回の例のように、中心となる機能を作った上で付加的な機能やさまざまなチェック機能を追加しながら成長していきます。そのようなソースコードを読む際には、頭から丁寧に読んでいくよりも、まず全体をざっと流し読みして中心となる部分を見つけた上で、そこからどのような処理が呼び出されていくのかを追ってゆく方がわかりやすくなります。
本連載で紹介してきた文書の読み方同様、ソースコードもメリハリを付けて読むのがコツと言えるでしょう。