bashシェルに新機能を追加するBashDiff

 本稿で紹介する BashDiff は、bashシェルに適用するパッチの1つであるが、これを用いることで実に多彩な機能追加が可能となる。具体的には、awkの一部機能をシェル本体に組み込む、C言語の一部機能をbashシェルプログラミングで使用可能とする、例外の処理機構を追加する、関数型プログラミングに特有なリスト内包表記やmap関数などの機能を提供する、GTK+2やデータベースとの通信機能を設けるといった、通常のbashには用意されていない機能を利用可能にするのだが、それだけに止まらず、Webサーバ機能を標準のbashシェルに組み込むことすらできるのだ。

 残念ながら、openSUSE、Fedora、Ubuntuのリポジトリに、BashDiffパッケージは収録されていない。私の場合も、BashDiff 1.45を用いたソースからのビルドを、bash 3.0の動作するx86 Fedora 9マシンにて行った。bashについてはバージョン3.1および3.2も利用可能となっているが、バージョン1.45のBashDiffの場合、これらに対するクリーンなパッチ処理ができないのである。

 BashDiffシェルをビルドする際には、共有オブジェクトをランタイム時に読み込ませるか、あるいは、これらのオブジェクトをシェル本体に直接リンクさせるかを選択できる。RAMに余裕がある場合は、BashDiffの包括インストールをしておくと、使用前に機能拡張群を読み込む負担から解放されるはずだ。これから行う解説では、2種類のアプローチを試すため、bashおよびbash+williamという2つのバイナリをコンパイルしてみることにする。

 各自のLinuxディストリビューションに付属するオリジナルのbashバイナリをそのまま残しておきたければ、BashDiffの設定時に、プライベートプレフィックスを付けておけばいいだろう。これにより、/usr/local/binのバイナリ群に対するリンクを維持しつつ、BashDiffのインストレーションについては別の場所に格納しておくことができる。

 BashDiffによるbashへのパッチ適用およびコンパイル処理は、下記のコマンド群にて行える。ここでは最初の作業として、オリジナルとなるbash 3.0のtarボールの展開および、BashDiffのtarボールからパッチの取得をしておかなくてはならない。そしてパッチの適用後は、通常行うconfigureとmakeの処理が必要となる。ここで注目して頂きたいのは、BashDiffシェル用の全ファイル群を1つのprefixで取り込んでいる点だ。そしてbash+williamバイナリに対するバイナリファイルのインストールは、install-binターゲットを用いて実行させている。最後の4つのコマンドでインストールしているのは、非williamバージョンにて動的な機能読み込みに使用する、william.so共有オブジェクトである。

$ mkdir bashdiff-build
$ cd bashdiff-build
$ tar xzf ../bash-3.0.tar.gz
$ cd ./bash-*
$ tar xzvf .../bashdiff-1.45.tar.gz
$ patch -p2 < bashdiff-1.45.diff
$ autoconf
$ ./configure --prefix=/usr/local/bashdiff
$ make
$ make bash+william
$ sudo make install
$ sudo make install-bin

$ cd examples/loadables/william
$ make
$ sudo make install
$ sudo ldconfig

 BashDiffのコンパイルに関する最後の注意点だが、SQLite(2.x)、MySQL、PostgreSQL、GTK+2、GDBM、Expatの開発パッケージをインストールしておかないと、これらの機能をサポートするようBashDiffビルドをコンパイルすることはできない。

 BashDiffによる機能拡張の1つは、forwhileuntilコマンドにて、メインのループブロックに続けてthenおよびelseオプションを使用可能にすることである。新規のオプションとして追加されるthen/elseブロックの役割は、通常の繰り返しブロック(thenブロック)で行うコマンド群に対して、breakコマンドが指定されていた場合に特定のアクション(elseブロック)を実行させるというものである。こうしたelseブロックの具体的な用途としては、例えば、それ以前の処理としてbreakコマンドによる中断がされていたかの判定に用いると便利だろう。

 bash 3は、{1..100}などの連続値を示す正規表現にも対応しており、この例の場合は、1から100までの数値を半角スペース区切りで並べたものを意味する。ところが通常のbashの場合、こうした最大値と最小値による数値範囲の指定をシェル変数を介しては行えないため、結局はseqコマンドを使用する以外ないのだが、BashDiffを用いると下記のような変数の使い方が可能となるのである。

$ max=15
$ echo {1..max}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

 正規表現については、caseコマンドでのパターンマッチングにおける通常のglob表示との併用もできるようになる。個々の部分一致の結果は、SUBMATCH配列を介してアクセスすればいい。更にBashDiffは、KshおよびZshの機能も取り込んでおり、通常の;;の代わりに;&を使用することで、caseステートメントを中断させて、残りの処理にフローを渡すという操作が行えるようになるのだ。

 BashDiffの追加する例外処理機能は、tryraiseコマンドとして実装されるようになっている。tryブロックの中断はdoneにて行われるが、ここではcaseブロックを用いたエラーの検出と処理が行える。なお、doneブロックで処理されなかったエラーは1階層上のtryブロックに渡されるが、これはC++やJavaにおける例外処理と同様の仕様だ。

 Cプログラマにとって助かるのは、C言語ではお馴染みな文字列関数の一部がBashDiffによって使用可能となることだろう。特にstrcpy、strcat、strcmp、strlenを使い慣れているプログラマであれば、これらを用いることで、メンタルなコンテクストスイッチが1つ分削減できることになる。また、strstr、str[c]spn、strtokなどの関数も使用可能となるが、これらの名称については若干の変更が施されており、例えばstrspnはBashDiffにてaccept関数とされている。

 C言語に関連した話題を続けると、読み込んだデータを分解するsscanf関数も、BashDiffによって使用可能となる。本来bashでは文字列と数値の区別がされないので、scan系の処理は文字列(あるいは単一キャラクタ)を対象としたものに限られているが、BashDiffの提供するsscanf関数では、パーセント記号で示される2つの特殊な演算子を介すことにより、指定のキャラクタクラスに一致ないし一致しないすべてのキャラクタを一括して取得できるのだ。この関数が便利なのは、半角スペースで区切られた入力文字列を分解する場合であるが(演算子は%sを使用)、その他にも特定の方式で区切られた、数値、アルファベット、英数字データに対しても適用できる。例えば下記のサンプルコードでは、sscanfコマンドを用いて簡易的なURLパーサを構築している。

$ url=https://www.linux.com/foo
$ sscanf $url '%[a-zA-Z]://%[^\/]/%[a-zA-Z1-9]' a b c
$ declare -p a b c
declare -- a="http"
declare -- b="www.linux.com"
declare -- c="foo"

 sscanf関数では対応不可能な入力データについては、matchコマンドで正規表現を用いた構文解析を行えばよく、個々の部分一致結果は適当な配列に格納しておけばいい。例えば下記のサンプルコードでは、先のURLの下層にあるディレクトリを含めた文字列を解析させているが、この正規表現を用いたパーサでは、そうしたデータも正しく処理できるようになっている。ここでの正規表現による判定で行うのは、最初にURLの種類がhttpかftpかを判定してから、その次にドメイン名と1番目のディレクトリコンポーネントを取得させ、そのマッチングの結果をaという配列に格納させるという処理である。このうち特別な意味を持つのは、1番目と2番目および最後の配列要素に格納される情報で、これらにはそれぞれ、正規表現に一致しなかった文字列の先頭部、正規表現に一致した文字列の全該当部、正規表現に一致しなかった文字列の末尾部が、個別に格納される。つまりこれらの要素を取得すれば、最初にスキップされた部分、実際のマッチ部、マッチせずに残された部分を確認できるのだ。なお、これら以外の配列要素に格納されるのは、正規表現の構成要素に対する個々のマッチング結果である。

$ url=https://www.linux.com/foo/bar
$ match $url '((ht|f)tp):[/]*([^\/]+)/([^\/]+)' a
$ declare -p a
declare -a a='([0]="" [1]="https://www.linux.com/foo"
  [2]="http" [3]="ht" [4]="www.linux.com" [5]="foo" [6]="/bar")'

 関数型プログラミングという観点から見た場合に有用な、1つのコマンドに対していくつかの配列を組み合わせたものを引数として受け取ることができるarraymapコマンドも、BashDiffの適用によって利用可能となる。ここで指定したコマンドは、引数として渡された配列の各要素について実行されるが、2つ以上の配列を渡した場合、各配列における要素の1つが位置パラメータとして関数に渡される。例えば下記のサンプルコードにおけるadder関数では、最初に呼び出された際には配列aおよび配列bの1番目の要素が渡され、その次に呼び出された際には両配列の2番目の要素が渡される、という処理が行われていく。

$ a=(1 2 3)  b=(4 5 6)
$ adder() { echo $(($1 + $2)); }
$ arraymap adder a b
5
7
9

 同じく関数型プログラミングに関係する機能としては、リスト内包表記を行う${var|command}も追加される。このパラメータ展開時には、指定されたvarに対してcommandの適用結果を用いた置換が行われることになる。実際にcommand部に記述する指示としては、各種の用途に適したプレフィックス群がいくつか事前定義されているので、それを使用すればいいだろう。例えば下記のサンプルコードでは、command部にて-プレフィックスと正規表現を用いた文字列分割を行うことで、この判定にマッチしないブロックを配列要素に返すようにしている。この例で用いたvar部への入力データは、1番目の単語“and”の後には連続した半角スペースが、2つ目の単語“there”の後には1つのタブ記号が挿入されたという状態の文字列だ。こうした正規表現およびglobによる分割方法、大文字変換、引用符や半角スペースの省略表記などの詳細については、BashDiffのドキュメントを参照して頂きたい。

$ in="and    there	was singing"
$ for z in ${in|-[ \\t]+}; do echo $z; done
and
here
was
singing

 ここでは、マイナス記号、プラス記号、プラスとマイナス記号といった短縮演算子を用いることで、正規変換による各種の分割処理を、var部の指定文字列に対して実行できるようになっている。このうちマイナス記号バージョンは、正規表現のマッチ部を文字区切りと見なさせるもので、この場合の実行結果としては、これらマッチ部の間にある文字列が返される。これに対してプラス記号バージョンでは、正規表現のマッチ部が結果として返される。最後に残されたプラスとマイナス記号バージョンは±記号を用いるもので、この場合は正規表現のマッチ部と非マッチ部の双方が返される。

$ echo $url
https://www.linux.com/foo/bar
$ echo   ${url|-/}
http: www.linux.com foo bar

 次回の解説では、位置パラメータの操作法、XMLの構文解析、ISAMやリレーショナルデータベースとの通信、GTK+2 GUIの作成、その他若干のテクニックや注意事項を紹介することにしよう。

パート2に続く…

Ben Martinは10年以上にわたってファイルシステムに取り組んでおり、博士課程の修了後、現在はlibferris、ファイルシステム、検索ソリューションを中心としたコンサルティング業に従事している。

Linux.com 原文(2008年10月27日)