前回はソフトウェア・ツールズの考え方と基本的な使い方を紹介しました。その際にも説明しましたが、ソフトウェア・ツールズでは、最近のGUIベースのソフトウェアのように1つのソフトウェアに多くの機能を詰め込むのではなく、1つの機能に特化した小さなコマンドをさまざまに組み合わせて、複雑な処理を実現しようとします。
これらのコマンドは前の結果を確認しながら対話的に実行していくこともできますが、あらかじめ実行したいコマンドを並べたファイルを用意して一気に実行することもできます。このファイル(シェルスクリプト)を保存しておけば、別の機会に同じ操作を再び行うことも可能ですし、先頭行にシェバン(#!/bin/bash等)を付けて実行パーミッションを与えておけば、新しいコマンドとして直接実行することも可能です。
ユーザが作った新しいコマンドも、PATH環境変数で指定するコマンドのサーチパスに保存しておけば、システムにあらかじめインストールされているコマンドと同様に利用できるようになります。このように自分で作ったコマンドでシステムを拡張していけるのも、Linux/UNIXの魅力のひとつです。
今までにも何度かシェルスクリプトの読み方を紹介しましたが、今回はシェルスクリプトを作っていく例を紹介しましょう。
Plamo Linuxのパッケージ管理システム
筆者がまとめ役をやっているPlamo Linuxでは元々のベースとしていたSlackwareのパッケージ管理システムを改良して利用しています。そのためパッケージをインストールするinstallpkgとインストール済みのパッケージを削除するremovepkgコマンドはあるものの、削除とインストールを自動的に行うようなコマンドはなく、パッケージのアップデートが必要な際はremovepkgコマンドでインストール済みのパッケージを削除してからinstallpkgコマンドを実行していました。
少数のパッケージを更新するだけならばこれでも別に困らないのですが、前回紹介したようにPlamo-4.5を出してからアップデートされたパッケージをまとめて更新しようとすると、400近いパッケージをアップデートする必要があります。これだけ多数のパッケージを一々手動でアップデートするのは大変なので、updatepkgコマンドを作ることにしました。
updatepkgスクリプトを考える前に、Plamo Linuxで採用しているパッケージ管理システムの概要を紹介しておく方がいいでしょう。Plamo Linuxでは、パッケージ名はパッケージのベース名とバージョン番号、対象アーキテクチャ、ビルド番号の4つの部分を'-'(ハイフン)でつないだ形になっています。たとえば、今回のアップデートで更新するPython 2.6のパッケージ名はpython-2.6-i586-P1.tgzです。このパッケージ名の場合、ベース名がpython、バージョンが2.6、対象アーキテクチャがi586、ビルド番号がP1となります。
installpkgコマンドでパッケージ名を指定してパッケージをインストールすると、そのパッケージのベース名のファイルが/var/log/packages/ディレクトリに作成され、このファイルにはパッケージ名や説明、インストールしたファイルやディレクトリの一覧が記録されます。
% cat /var/log/packages/python
PACKAGE NAME: python-2.6-i586-P1
COMPRESSED PACKAGE SIZE: 17855 K
UNCOMPRESSED PACKAGE SIZE: 61720 K
PACKAGE LOCATION: ./Python/python-2.6-i586-P1.tgz
PACKAGE DESCRIPTION:
FILE LIST:
install/
install/doinst.sh
usr/
usr/include/
usr/include/python2.6/
usr/include/python2.6/cellobject.h
usr/include/python2.6/bytearrayobject.h
...
一方、インストールしたパッケージを削除したい場合は、removepkgコマンドにパッケージのベース名を指定して実行します。
# removepkg rhythmbox
Removing package rhythmbox...
Removing files:
--> Deleting symlink usr/lib/librhythmbox-core.so
--> Deleting symlink usr/lib/librhythmbox-core.so.0
--> Deleting etc/gconf/schemas/rhythmbox.schemas
--> Deleting usr/bin/rhythmbox
...
removepkgコマンドは/var/log/packages/以下にあるパッケージのベース名のファイルからそのパッケージでインストールされたファイルやディレクトリを調べ、それらを削除していきます。
updatepkgコマンドでは、指定されたパッケージをインストールする前にそのパッケージと同じベース名のファイルが/var/log/packages/ディレクトリ以下にあるかどうかを調べ、ファイルがあればremovepkgでアンインストールしてからinstallpkgでインストールするようにすればいいでしょう。
updatepkgコマンドの作成
前述の通り、パッケージ名は'-'(ハイフン)で区切られた4つの部分から構成されており、パッケージのベース名はその最初の部分になるので、パッケージ名からベース名を取り出すにはcutコマンドを使えばいいでしょう。ベース名のファイルが/var/log/packages/ディレクトリにあるかどうかはfindコマンドを使ってもいいですが、再帰的に調べる必要があるわけでもないので、lsコマンドとgrepコマンドを組み合わせる程度でいいでしょう。
まず最初に作ってみたのはパッケージ名からベース名を取り出し、そのベース名が/var/log/packages/以下にあるかどうかを調べるスクリプトです。
1 #!/bin/sh
2 pkg=$1
3 base=`echo $pkg | cut -f1 -d'-'`
4 chk=`ls /var/log/packages/* | grep $base`
5 if [ "$chk.x" != ".x" ]; then
6 echo "removepkg $base"
7 fi
8 echo "installpkg $pkg"
このスクリプトでは、
2行目で$pkgという変数に1つめの引数($1)で指定したパッケージ名を入れ、
3行目で$pkgからcutを使ってパッケージのベース名を取り出して$base変数に代入し、
4行目で/var/log/packages/ディレクトリの全ファイルから$baseをgrepした結果を$chkという変数に代入して、
5行目で$chk変数に何か文字列が入っているかを調べ、(何か文字列が入っていればダブルクォートで展開した結果は.xとは異なる)入っていればgrep $baseで文字列が検出されている(=そのパッケージはインストールされている)から、まずそのパッケージを
6行目のremovepkgでアンインストールしてから、
8行目のinstallpkgで指定されたパッケージをインストールする、
という流れになっています。
なお、このスクリプトでは動作テストのために6行目と8行目はremovepkgやinstallpkgコマンドを実際には実行せずechoで実行すべきコマンドラインを出力させています。
アップデートするパッケージが1つだけならこのスクリプトでも動きそうですが、updatepkgコマンドを使いたいのは複数のパッケージをまとめてアップデートする際なので、複数のパッケージを指定できるようにした方がいいでしょう。多数の引数を指定できるようにfor文でループを回すようにしてみます。
1 #!/bin/sh
2 for pkg in $* ; do
3 base=`echo $pkg | cut -f1 -d'-'`
4 chk=`ls /var/log/packages/* | grep $base`
5 if [ "$chk.x" != ".x" ]; then
6 echo "removepkg $base"
7 fi
8 echo "installpkg $pkg"
9 done
この例では2行目の$*で引数として指定した全てのパッケージを対象にforループで処理を回すようにしています。
このスクリプトを使えば、あるディレクトリの全てのパッケージを対象にupdatepkg *.tgzのような操作が可能になりますが、カレントディレクトリにあるパッケージ以外のパッケージを対象にしようとすると、パス名を含んだパッケージ名の分割がうまく行かず、インストール済みか否かのチェックが機能しません。そこでパッケージ名をパス名付きで指定した場合にも対応するようにしました。
1 #!/bin/sh
2 for tmppkg in $* ; do
3 pkg=`basename $tmppkg`
4 base=`echo $pkg | cut -f1 -d'-'`
5 chk=`ls /var/log/packages/* | grep $base`
6 if [ "$chk.x" != ".x" ]; then
7 echo "removepkg $base"
8 fi
9 echo "installpkg $tmppkg"
10 done
以前は直接$pkgに引数で指定したパッケージ名を代入していましたが、このスクリプトではいったん$tmppkgという名前に受けて、basenameコマンドで$tmppkgのファイル名の部分のみを$pkgに代入して以後の処理を行っています。ただし、installpkgの際には元のパス名を含んだパッケージ名を使う必要があるので、9行目のinstallpkgでは$tmppkgを対象にしています。
このスクリプトでだいたい思ったような動作になりましたが、いろいろ試しているとパッケージのベース名の一部が重なる際(xvはインストールされておらず、xvinfoがインストールされている環境で、xvパッケージをインストールしようとすると、5行目のgrepでxvinfoが引っかかってxvはインストール済みとされremovepkgしようとしてエラーになる)、正しく処理ができないようです。grepのチェックをもう少し厳しくしてみましょう。
1 #!/bin/sh
2 for tmppkg in $* ; do
3 pkg=`basename $tmppkg`
4 base=`echo $pkg | cut -f1 -d'-'`
5 chk=`ls /var/log/packages/* | grep "^$base$" `
6 if [ "$chk.x" != ".x" ]; then
7 echo "removepkg $base"
8 fi
9 echo "installpkg $tmppkg"
10 done
5行目のgrepの変更した引数"^$base$"は正規表現を使った指定で、「行頭(^)から$baseが出現し、$baseの後には何も無くて行末($)が来る」というパターンにマッチさせています。この指定を使えばxvとrxvtやxvinfo、xvidtuneなどを区別できるでしょう。
とりあえずこの程度で必要な処理は行えそうですが、備忘録代わりにヘルプメッセージを出力する機能を追加しておきましょう。
1 #!/bin/sh
2 if [ $# = 0 ]; then
3 echo "usage: $0 package(s)"
4 exit
5 fi
6 for tmppkg in $* ; do
7 pkg=`basename $tmppkg`
8 base=`echo $pkg | cut -f1 -d'-'`
9 chk=`ls /var/log/packages/* | grep "^$base$" `
10 if [ "$chk.x" != ".x" ]; then
11 echo "removepkg $base"
12 fi
13 echo "installpkg $tmppkg"
14 done
追加した2行目から4行目の処理は、引数の個数($#)を調べて、引数が指定されていなければusage(使い方)メッセージを表示して終了する、というものです。ここで$0はこのシェルスクリプトの名前に置換されるので、このスクリプトをupdatepkgというコマンドで保存しておけば、その名前に置換されてメッセージが表示されます。
% ./updatepkg
usage: ./updatepkg package(s)
今回はこのあたりで止めておきますが、このコマンドをテストしていると「指定されたパッケージのバージョンを調べて、インストール済みの方が新しいバージョンならばエラーメッセージを出してインストールを中止する」とか、「そのような場合でも-fオプションを指定すれば強制的にインストールできる」といった機能があればより便利そうだ、という気になりました。これらの機能をどう実現するかは宿題ということにしておきましょう。
このようにテストしながら問題点をさっと修正したり、あったら便利と思った機能を簡単に追加したりできるのが、シェルスクリプトの大きな魅力です。また、こうして作った新しいコマンドをパスの通った場所に置けば、システムに用意されているコマンドと同様に利用できるようになり、そのコマンドを下請けに使う新しいコマンド(たとえば、指定したディレクトリ以下のパッケージを全てアップデートするupdatedirコマンド)を作ることも可能です。
筆者の場合、Linux上でソフトウェア・ツールズをあれこれ組み合わせて使ったり、このような自作コマンドを使って作業している時は新しいアイデアがいろいろと湧いてきて「PCを使っている」という気になりますが、Windowsでアプリの指示に従ってあちらこちらをクリッククリックさせられる作業では「PCに使われている」という気になってイライラしてきます。この「自分の意のままにPCを使いこなしている」という麻薬的な感覚が、Linuxから離れられない理由のひとつでしょう。