はじめに
UNIXを活用するツールの一つがシェルスクリプトです。UNIXに用意されているコマンドを組み合わせることで、必要とする機能を短時間で効率よく実現することができます。
シェルスクリプトは通常、そのファイル単体で使用します。C言語で作成されたコマンドが、実行時にほかのライブラリをリンクして使用するのと比べると対照的です。ファイル単体で動作するということは便利ですが、反面、コードの重複が起りやすいという問題も抱えています。
本稿では、シェルスクリプトでコードの重複を抑えつつも、ファイル単体で使用する利便性を損ねないための方法を紹介します。
対象読者
本稿では、ある程度シェルスクリプトを使うことができる、中級者から上級者を読者として想定します。
必要な環境
紹介するシェルスクリプトを実行するために必要となる環境は、最近のUNIX互換OSです。こちらではFreeBSD 5.3で動作を確認してあります。
シェルスクリプトの共有関数ファイル
シェルスクリプトの共有ライブラリ化に関連して、NetBSDが新しく採用した起動機構である「rc.d」の話からはじめます。
これまでNetBSDはBSDに由来するrc起動機構を改良した仕組みを採用していました。これは簡素で扱いやすい反面、アプリケーションの起動や停止を処理を追加したり削除することが難しいという問題を持っていました。問題を解決する方法としては、SYSV4に由来するinit.d起動機構を採用すれば良いのですが、こちらにも起動順序がファイル名順であったり仕組みが煩雑だという問題がありました。
もちろん、BSD rc起動機構にせよ、SYSV4 init.d起動機構にせよ、起動処理をおこなっているのはシェルスクリプトです。そのシェルスクリプトをどういった規則で起動と停止処理に使用するかということが、ここでの起動機構ということになります。
結局、長い議論の末、The NetBSD Projectは、BSD rc起動機構とSYSV4 init.d起動機構の良いところを合わせたようなNetBSD rc.d起動機構を開発します。この機構はのちにFreeBSDに移植されました。
NetBSD rc.d起動機構のベースとなるアイディアは、SYSV4 init.d起動機構からもってきています。その際、起動スクリプトで管理が困難であった原因の一つを改善する方法が提案されました。
SYSV4 init.d起動機構では、1サービスごとに起動スクリプトファイルが用意されます。それぞれのスクリプトファイルは単体で実行できるように独立しています。これは便利なことですが、管理の手間という面において問題がありました。すべての起動スクリプトが独立しているため、それぞれのスクリプトにはどれも似たようなコードが入っています。一つのスクリプトを変更したとすると、他のスクリプトに入っている同じようなコードの部分を、同様に変更をする必要があったのです。これは面倒であり、見落としもあって厄介でした。
The NetBSD Projectではこの問題を解決するために、起動スクリプトで使用される処理を共通の関数として作成し、1つのファイルにまとめておくことにしました。「/etc/rc.subr」がそれです。その結果、起動スクリプトのサイズが小さくなって見通しがよくなり、共通の機能は「/etc/rc.subr」にまとまっているため重複したコードがなくなりました。これは管理の面からいってとても有用です。
よく使う処理を関数化してまとめる
いくつもシェルスクリプトを作成していれば、かならず似たような処理をおこなう部分が現れます。たとえば、プログラムがインストールされているか調べるところ、usage
を出力するところなどです。
頻出する処理を関数として抜き出しファイルにまとめておけば、その部分の記述を省くことができます。たとえば、リスト3.1のような関数共有ファイルを作ったとします。
# usage # print usage # usage # usage_msg='usage message' # usage # usage() { echo "$usage_msg" 1>&2 }
この共有関数ファイルを使用する例を、リスト3.2に示します。このコマンドは、なにも引数が指定されていないか-h
が指定されているときは使い方を出力し、そうでない場合は引数をそのまま標準出力に出力します。コマンドの方では、メッセージを設定してusage
関数を実行しています。usage
関数自身は「.shsubr」に記述されているので、コマンド自身の方では記述されていません。
#!/bin/sh . "${0%/*}"/.shsubr usage_msg='usage: command message -h print help message' [ 0 = $# ] && { usage; exit; } echo "$@"
リスト3.2の上から3行目のところリスト3.3で、リスト3.1ファイルの読み込みをおこなっています。リスト3.3のように記述されている場合、コマンドと同じディレクトリにある「.shsubr」が読み込まれることになります。こうしておけば、コマンドと同じディレクトリに「.shsubr」をおいておくことができますし、ファイル名が「.」から始まっているので、コマンドと混乱することもありません。
. "${0%/*}"/.shsubr
このように共有関数を作成しておくと便利です。しかし、便利である反面欠点もあります。他のOSにコピーして使ったり、ネットワークで配布する際に面倒なのです。
これまで、シェルスクリプトで使用する汎用的な共有関数ファイルを作成するということは、積極的には行われてきませんでした。このため、シェルスクリプトの共有関数のファイル名や、共有関数ファイルをおいておくためのディレクトリが、規格化されていませんし、慣例もありません。このため、ファイルを複数に分けてある状態で配布すると使いにくいのです。
分割されたものを統合する
シェルスクリプトを効果的に使用するには、「/etc/rc.subr」のような共有関数ファイルを使うとよいということはわかっています。しかし、シェルスクリプトを複数のファイルに分割すると、使用にあたっての共通認識がないため、別の環境にもっていったときに使いにくくなるという問題があります。
それにシェルスクリプトは便利ですが、他人にとっては読みにくくなりがちという問題もあります。C言語であれば、ある程度関数は標準化されていますので、共通認識できるコードが多く可読性もそれなりにあります。しかし、個人が勝手に作成した関数が使われたシェルスクリプトは、本人以外には読みにくいものでしょう。シェルスクリプトの内容を変更しようとしてファイルを開いてみて、自分のまったく知らない関数が多用されていれば、その時点でうんざりするはずです。
この問題を解決するには、作成する時点では共有関数ファイルとシェルスクリプトを分割して作業しておいて、配布する段階でそれを統合して一つのファイルにしておけばいいことになります。
統合作業は手作業でおこなってもよいのですが、いちいち手で作業してはせっかくシェルスクリプトを使っている意味がありません。共有関数ファイルとシェルスクリプトを統合して1つのファイルにするシェルスクリプトを作成すればよいのです。
共有関数ファイルとシェルスクリプトをマージするshsubrmergeコマンド
ここで紹介するshsubrmerge
コマンドは、共有関数ファイルとシェルスクリプトをマージして、それ単体で動作するシェルスクリプトを生成します。
shsubrmerge
コマンドはtopless
というコマンドを配布するために作成されたのですが、当初このシェルスクリプトは、FreeBSDで共有関数ファイルを使って作成されました。
topless
は思いの他便利ということもあって、すぐに他のOSでも動作するように広まりました。しかし、配布する段階になって問題がわかりました。シェルスクリプトを2つのファイルに分けて配布することが、共通認識としては受け入れがたいということです。シェルスクリプトなら、その単体をダウンロードしてきて使えないと意味がないというか、使いにくいということです。
この問題に対処するために、shsubrmerge
コマンドが作成されました。たとえばリスト3.2を引数に指定してshsubrmerge
コマンドを実行すると、次のようなシェルスクリプトとが標準出力に出力されます。リスト3.1の関数が統合されている様子がわかります。
#!/bin/sh # usage # print usage # usage # usage_msg='usage message' # usage usage() { echo "$usage_msg" 1>&2 } usage_msg='usage: command message -h print help message' [ 0 = $# ] && { usage; exit; } echo "$@"
shsubrmerge
コマンドを使えば、配布用のシェルスクリプトを自動的に生成することができるわけです。
shsubrmergeコマンドの処理内容
shsubrmerge
コマンドの処理は大きく分けて3手順になります。
- 共有関数ファイルから関数を抜き出す
- シェルスクリプトで使用されている関数を調べる
- インクルードしている部分を使用されている関数に置き換える
シェルスクリプトでは解析プログラムを使っているわけではないので、複雑な解析をおこなうことはできません。shsubrmerge
コマンドでは、共有関数ファイルがリスト3.1のように作成されているものと仮定して処理をおこなっています。
shsubrmerge
コマンドはまず、共有関数ファイルを一行づつ読み込んで、マッチングに応じて処理をおこないます。気をつける必要があるのは、while
で一行づつ読み込むときにIFS=
を指定しておくことです。こうしないと、空白タブが区切り記号として解釈され、行の内容が変更されてしまいます。
cat "${0%/*}"/.shsubr | while IFS= read line do ....
切り出した関数はいったんファイルに出力しています。シェルスクリプトで使用されている関数は、grep
で調べています。一致した関数は変数にためていきます。
commandlist=$(ls "$tempdir") for command in $commandlist do grep -- "$command" "$1" > /dev/null && { [ -z "$functions" ] && functions="$(cat "$tempdir/$command")" || functions="$(echo "$functions"; echo; cat "$tempdir/$command")" } done
最後にマージします。「.shsubr」を読み込んでいる行番号を調べて、そこまでをhead
で出力、関数を出力、それ以降をtail
で出力しています。
includeline=$(cat -n "$1" | grep '/*}"/.shsubr' | head -1 | awk '{print $1}') head -$(($includeline - 1)) "$1" echo "$functions" tail -$(( $(wc -l "$1" | awk '{print $1}') - $includeline)) "$1"
シェルスクリプトで実現できる内容は、基本的にコマンドができること、そしてコマンドを組み合わせてできることです。ほかのプログラミング言語と同じ発想で作成しようとしてもうまくいきません。視点をコマンドを使うところに据えることが大切です。
まとめ
シェルスクリプトを効果的に活用するには、よくつかう処理を共有関数としてファイルにまとめておくとよいでしょう。
ただし、シェルスクリプトはそのファイル単体で使用するという慣例が強いため、実行するために複数のファイルがあると別の環境に持っていったときに使いにくくなります。本稿ではそれを回避するため、共有関数ファイルとシェルスクリプトを統合するコマンドshsubrmerge
を紹介しました。
なお、shsubrmerge
コマンド自身はFreeBSDで作成されました。ほかのOSでは動作確認をとっていません。それに、対象としている共有関数ファイルも、コマンドと同じディレクトリにある「.shsubr」ファイルと固定されています。
shsubrmerge
コマンドは、シェルスクリプトで多くの作業を行っているヘビーユーザには重宝するコマンドです。ダウンロードしたshsubrmerge
コマンドをベースにして、自分の環境に合うようにカスタマイズしてください。
手作業をおこなっているところを自動化することが、シェルスクリプトの使用目的の一つです。思いあたる節があるのであれば、シェルスクリプトで自動化してみましょう。