Bash on Railsを作る(10) bashOOをbashOOで作る
bashの内蔵コマンドだけでいかにRuby on Railsっぽいことをやるかというパロディ企画です。実用性は求められても私が困るので、期待は勘弁してください。でもいちおう、すでに動いてます。
今回は、「Bash on Railsを作る(8) bashでオブジェクト指向」で予告したように、bashのコマンド実行をオブジェクト指向ふうにするしくみ「bashOO」(ばしょー)の実装を解説します。「bashOOも、bashOO自身で実装しています」と予告したとおりの内容です。
bashOOは、Bash on RailsのMVCそれぞれの根幹で使われています。
基本的なタネあかし
賢明な方は気づいていると思いますが、「ore.hello」というメソッド呼び出しは、本当に「ore.hello」という(ドットを含む)bashのコマンド(関数)名の呼び出しです。newとかextendとかは、そういうコマンド(関数)を動的に生成するメソッドです。
ObjectクラスをbashOOで作る:クラスメソッドnew
最も基本的なクラスであるObjectクラスを、bashOOで作ります。
まず、インスタンスを作るクラスメソッドnewを定義します。第8回で解説したように、bashOOのクラスメソッドは「(クラス名)::class.(メソッド名)」という関数として定義します。
ざっくりと、こんな感じの定義になります。
function Object::class.new() { local class=$1 local self=$2 shift 2 # 「(クラス名)::instance.(メソッド名)」から # 「(インスタンス名).(メソッド名)」を定義 }
コメントになってる部分の仕様をコードにすると、こんな感じでしょうか。
local m for m in $(search_functions "${class}::instance.*"); do m=${m#${class}::instance.} eval "function ${self}.${m}() { ${class}::instance.${m} \"$self\" \"\$@\" }" done
仕様のとおり、「(クラス名)::instance.*」に該当する関数を探して、「(インスタンス名).(メソッド名)」の関数から呼ぶようにするわけです。
ここで、search_functionは、引数に文字列パターン(ワイルドカード形式)を受け取り、該当する関数を標準出力にリストアップするコマンド(関数)です。定義は以下のとおり。
function search_functions() { local ptn=$1 local func line typeset -F | while read line; do func=${line##* } [[ "$func" == ${ptn} ]] && echo $func done }
ようは、「typeset -F | grep パターン」みたいなののpure bash版です。
ここでついでに、「(インスタンス名).class」というインスタンスメソッドの関数を特例で定義してみます。
eval "function ${self}.class() { echo ${class} }"
最初は「(インスタンス名)_class」という変数に入れようとしたのですが、インスタンス名に「:」などが含まれると変数名にできないので、特例としました。
あと、コンストラクタが定義されていれば、ここで呼び出します。
if type -t ${self}.initialize > /dev/null; then "${self}.initialize" "$@" fi
ObjectクラスをbashOOで作る:インスタンスメソッドdelete
クラスメソッドの次はインスタンスメソッドです。なんでもいいのですが、ここでは自分自身を消滅させるdeleteメソッドを定義してみます。
上にも書いたように、インスタンスメソッドは「(クラス名)::instance.(メソッド名)」という関数として定義します。
function Object::instance.delete() { local self=$1 local fun unset "$self" eval "unset \${!${self}_*}" for fun in $(search_functions "${self}.*"); do unset -f "$fun" done }
search_functionsは上で説明したとおりです。
なお、${!文字列パターン}は、変数名をワイルドカード展開するbashの記法です。たとえば、hoge_a、hoge_b、hoge_cという変数があるとき、「${!hoge_*}」は「hoge_a hoge_b hoge_c」という文字列に展開されます。変数の間接参照とまぎらわしいので、詳しくはman bashを。
親クラスからObjectクラスを作る…どこから?
第8回で解説したように、クラスメソッドとインスタンスメソッドを定義したら、「(親クラス名).extend」というクラスメソッドを実行すれば、新しいクラスが定義されます。
では、最も基本的なクラスであるObjectクラスは、どこからextendするのでしょうか。
bashOOでは、存在しない空のクラスからextendするものと考えます。ただし、まだbashOOが存在しないので、「(クラス名)::class.extend」という、クラスメソッド定義を直接呼び出します。
::class.extend '' Object
bashOOをブートストラップする仕組み
というわけで、「::class.extend」というコマンド(関数)を定義しましょう。構造としては、以下のとおりです。
function ::class.extend() { local super=$1 local class=$2 # 親クラスのクラスメソッドとインスタンスメソッドを継承 # 「(クラス名)::class.(メソッド名)」から # 「(クラス名).(メソッド名)」を定義
まずクラスメソッドの定義。これは、newでインスタンスメソッドを定義するときと同じように書けます。
for m in $(search_functions "${class}::class.*"); do m=${m#${class}::class.} eval "function ${class}.${m}() { ${class}::class.${m} \"$class\" \"\$@\" }" done
その前に、親クラスからの継承を処理します。前もって定義しておくことによって、子クラス(定義するクラス)で定義したときに、親クラスの定義を後から上書きしてくれます。
local t m for t in class instance; do for m in $(search_functions "${super}::${t}.*"); do m=${m#${super}::${t}.} if ! type -t "${class}::${t}.${m}" > /dev/null; then eval "function ${class}::${t}.${m}() { ${super}::${t}.${m} \"\$@\" }" fi done done
ここで注目してほしいのは、Objectクラスを作ったときには、第1引数superが''として渡されていたことです。これにより、この関数自身である「::class.extend」が、親クラスのクラスメソッドとしてObjectクラスに継承され、以後のクラスで「extend」メソッドが使えるというわけです。
まとめ
bashの解釈系(シンタックス)の中で、オブジェクト指向っぽい書き方(セマンティックス)を実現するために、実装方法を解説しました。
できてみると単純なしくみですが、作っているときは論理のつじつまをあわせるのに丸1日ぐらい悩んでました。いや、文系なんで。
次回は、また軽めに、pure bashでの文字列処理について書こうかと思います。
「Q.E.D.」29巻
「エレファント」にはやられた。シリーズを利用した叙述トリックだな、これは。ラストでも叙述トリックをもうひと押し。そのため、これ以上の言及は自粛しておく。
「動機とアリバイ」は、オーソドックスなアリバイトリックもの。
講談社 (2008/02/15)
「かへ ―とその他の短篇」
ネット中に感動と驚愕を呼んだネットマンガ「瀬をはやみ」「かへ」が、いつのまにか単行本になっていた。
思わず買ってしまった。
「瀬をはやみ」を最初にWebで読んだときは、たまげた。広告マンガというのはネタバレ済みとしても、ああ展開するとは…
Web版と内容は同じ。ただし「かへ」の続編がオリジナルで載っている。改めて読むと、紙だと読むテンポが速くなるせいか、よくも悪くも古風な印象を受けた。
まだ読んだことのない人は、ネット版(↓)を見て、展開に驚いてみるのもよいかと。
/etc/apt/preferenceのPackage:行
DebianやUbuntuの/etc/apt/preferenceでは、Package:行に1つのパッケージ名か「*」しか書けないようだ。「,」で並べたりワイルドカードを使ったりできればと思ったのだけど。
apt-pkg/policy.ccより:
string Name = Tags.FindS("Package"); if (Name.empty() == true) return _error->Error(_("Invalid record in the preferences file, no Pack age header")); if (Name == "*") Name = string();
XRandRでUbuntuの外部モニタ出力を切り替える
なんてことを以前書いたのだけど、最近のX.orgではとっくにXRandRというしくみを採用していて、出力デバイスや解像度、向きなどを動的に変更できるらしい。なぁんだ。
おくればせながら、ThinkPad X60s+Ubuntu 7.10で、外部モニタ出力とデュアルモニタを試してみる。
まず、/etc/X11/xorg.confで、「Section "Screen"」の「SubSection "Display"」に「Virtual 2048 768」という設定を入れておく。
xrandrコマンドを引数なしで実行すると、出力デバイスの情報が出力される。
% xrandr Screen 0: minimum 320 x 200, current 1024 x 768, maximum 2048 x 768 VGA connected (normal left inverted right) 1024x768 75.1 70.1 60.0 800x600 72.2 75.0 60.3 640x480 75.0 72.8 60.0 720x400 70.1 LVDS connected 1024x768+0+0 (normal left inverted right) 246mm x 185mm 1024x768 50.0*+ 60.0 59.9 40.0 800x600 60.3 640x480 60.0 59.9 TV disconnected (normal left inverted right)
VGA(外部モニタ)をオンにする。
% xrandr --output VGA --mode 1024x768
LVDS(液晶モニタ)と同じ画面がVGA(外部モニタ)に出た。次に、デュアルモニタにしてみる。まず、VGAが左、LVDSが右。
% xrandr --output VGA --left-of LVDS
おお、デュアルモニタになった。続いて、VGAが右、LVDSが左。
% xrandr --output VGA --right-of LVDS
どちらも、GNOMEパネル(上下のバー)はVGA側に出た。
同じ画面に戻してみる。
% xrandr --output VGA --same-as LVDS
LVDSのみに戻す。
% xrandrm--output VGA --off
戻った。あれ、GNOMEパネルの表示が更新されない。時計やウィンドウ一覧が古いままだ。
参考にしたサイト: 「KeN's GNU/Linux Diary | Xrandrエクステンションを使う」、「What happens today?: xrandr」
Bash on Railsを作る(9) CGI.unescape
bashの内蔵コマンドだけでいかにRuby on Railsっぽいことをやるかというパロディ企画です。
今回はごく小ネタ。pure bashでURLエンコーディングをデコードする方法です。
function CGI.unescape() { local str=$1 str=${str//+/ } str=${str//%/\\x} echo -e "$str" }
…見たまんまですね、サーセン。ちなみに、エンコードする方法はまだ考えてません。
「クロサギ」17巻
小学館 (2008/02/05)
「恐喝詐欺」と「任意後見人詐欺」の2編を収録。
今回の手口メモは、ネタバレなので「続きを読む」で。
Bash on Railsを作る(8) bashでオブジェクト指向
bashの内蔵コマンドだけでいかにRuby on Railsっぽいことをやるかというパロディ企画です。1000speakers:2デビューを狙ってましたが、発表者枠が分速で埋まったようなので、のんびりとブログで解説します。
今回は、予告どおり、bashでオブジェクト指向(もどき)を実現するしくみについて、使う側から解説します。bashでOO、略してbashOO(ばしょー)というところでしょうか。O/Rマッパー(もどき)など、Bash on Railsの多くの部分で利用しています。
前提:あくまでもbash
「bashでオブジェクト指向言語を作る」のではなく、「bashの解釈系のままで、オブジェクト指向っぽい書き方をできるようにする」のを目指しました。bashOOの呼び出しは、すべてbashのコマンドとして実行されています。
基本はObjectクラス
一般的な仕様どおり、bashOOでも、Objectクラスが最も基本的なクラスです。
インスタンスを作るのは、クラスメソッドnewです。
Object.new objectA
これで、インスタンスobjectAが作られます。
インスタンスメソッドを呼ぶ
作られたインスタンスのメソッドを実行してみます。ここではclassメソッドを呼んでみます。
objectA.class
クラスメソッドclassが呼ばれ、インスタンスが属するクラスの名前である「Object」が標準出力に出力されます。
注意:オブジェクトになるのは名前
ここで注意したいのは、オブジェクトになるのは、変数の内容ではなくて、変数の名前だということです。たとえば、以下のように、ほかの変数に代入してメソッドを実行しようとすると、エラーになります。
objectB=objectA objectB.class
継承クラスを定義:まずインスタンスメソッドを定義
言語を作るわけではないので、いまのところObjectクラスには機能はほとんどありません。そこで、Objectクラスを継承して新しいクラスを作ってみましょう。ここでは、Humanクラスを作ってみます。
まず、インスタンスメソッドhelloを定義します。インスタンスメソッドは、「(クラス名)::instance.(メソッド名)」という名前の関数として定義します。
function Human::instance.hello() { echo 'Hello!' }
引数のあるメソッドも定義できます。
function Human::instance.speak() { local self=$1 local message=$2 echo "$message" }
第1引数がselfなんて、PerlかPythonみたいですね。いや、それを狙ったんですが。
クラスメソッドを定義
なんとなく気づいた方もいるかもしれませんが、クラスメソッドは、「(クラス名)::class.(メソッド名)」という名前の関数として定義します。
function Human::class.amounts() { local class=$1 echo "$class lives: 6,400million" }
クラスメソッドの第1引数はclassです。Perlのnewみたいですね。いや、それを狙ったんですが。
継承クラスを定義:実行
あとは、実際に継承を実行します。
Object.extend Human
クラスメソッドを試してみましょう。
Human.amounts
標準出力に「Human lives: 6,400million」と出力されます。
インスタンスを作ってみます。
Human.new ore
インスタンスメソッドを呼び出してみます。
ore.hello ore.speak 'Hello!'
どちらも、標準出力に「Hello!」と出力されます。
さらに継承:コンストラクタとインスタンス変数
もちろん、Humanから継承クラスを作ることもできます。ここでは、Bikerクラスを作ってみます。
ついでに、コンストラクタも解説します。コンストラクタは、そのクラスのinitializeというインスタンスメソッドです。Rubyっぽいですね。いや、それを狙ったんですが。
function Biker::instance.initialize() { local self=$1 local name=$2 eval "${self}_name='${name}'" }
インスタンス変数は、「(インスタンス名)_(メンバー名)」という変数とします。代入のためにevalを使っているのはご愛嬌ということで。
インスタンス変数を参照するメソッドも作ってみましょう。
function Biker::instance.name() { local self=$1 local name_var="${self}_name" echo "${!name_var}" }
「${!hoge}」は、変数の間接参照です。詳しくはman bashをご覧ください。
メソッドの継承
親クラスのメソッドは上書きできます。
function Biker::instance.hello() { echo "Hey!" }
では継承クラスを作ります。
Human.extend Biker
インスタンスを作ってみましょう。
Biker.new wyatt 'Captain America'
新しく定義したインスタンス変数を呼んでみます。
wyatt.name wyatt.hello
それぞれ「Captain America」「Hey!」と出力されます。
親クラスのメソッドは、そのまま使えます。
wyatt.speak 'Easy come, easy go.'
まとめ
bashの解釈系(シンタックス)の中で、オブジェクト指向っぽい書き方(セマンティックス)を実現してみました。
bashOOも、bashOO自身で実装しています。そのへんのブートストラップの仕組みは、そのうち。
「新アラビアンナイト」
本書の解説によると、「アリババと40人の盗賊」は「アラビアンナイト」に元からあるエピソードではなく、後世に追加されたエピソードだそうだ。同じように、「アラビアンナイト」にありそうなエピソードを、パスティーシュの名手、清水義範が書いた本。「水パイプの中の恋と冒険の物語」「タイル職人とコウノトリの物語」「坊さんも気絶するナス料理の物語」など、タイトルだけでも不思議なタイトルの物語が並んでいる。そして最後に…
もうひとつのポイントは、著者が旅行して知ったイスラム圏の薀蓄をちりばめてあること。話の最初にエッセイ風にマクラを振って本編に移ったり、話の途中で歴史上のエピソードにつなげたりと、うまく話にとけこんでいる。「坊さんも気絶するナス料理」が実在するとは知らなかった。
面白かった。
集英社 (2007/08)
売り上げランキング: 59402
Bash on Railsを作る(7) db/schema.sh
bashの内蔵コマンドだけでいかにRuby on Railsっぽいことをやるかというパロディ企画です。「Webアプリケーションをなめるな」とDISってもらえる日を夢みています。特にそのための行動をする気はありませんが。
今回は、前回の予告どおり、モデルのスキーマ定義であるdb/schema.shファイルについて、メタプログラミングでDSLする方法を解説します。
db/schema.shの中身
第1回で紹介したように、db/schema.shファイルの中身は以下のようになっています。
create_table members t t.column name string t.column mailaddress string t.column comment string elbat_etaerc
これをシェルスクリプトとして解釈します。
なお、いまのところdb/schema.shを手書きしていますが、そのうちscript/generateなどの生成スクリプトを用意しようと思ってます。
基本的なアイデア
前回にも書いたように、bashはオンメモリーのデータ構造を操作するのは苦手です。そこで、db/schema.shをシェルスクリプトとして読み込んだときに、できるだけその場で処理を実行することにします。
db/schema.shは、make db-schema-loadでテーブルを作るときと、ActiveRecordもどきのO/Rマッパーを起動するときの2箇所で使います。そこで、同じ名前のコマンド(関数)を定義したスクリプトファイルを、2通り用意します。用途に応じて違うスクリプトファイルを読み込み(sourceし)、そのあとにdb/schema.shを読み込めば、設定が反映されます。
テーブルを作る場合
まず、前回のとおりconfig/database.shを反映しておきます。そして、connection_adapters/${dbconf_adapter}_adapter.shを読み込んで、RDBMS固有の処理をラップした共通関数を定義します。
create_table関数は、以下のように実装します。
function create_table() { table=$1 prefix=$2 columns_sql="id INTEGER PRIMARY KEY AUTOINCREMENT" eval "function ${prefix}.column() { local column=\$1 local datatype=\$(type2dbtype \$2) string_push columns_sql \", \$column \$datatype\" }" }
bashの関数は「.」を含んでいいというのがポイントです。第2引数で渡された文字列を見て、${文字列}.columnという関数を定義しています。これで、Ruby on Railsのモデル定義にブロック変数を使うのを真似てみました。定義するコマンドは、column_sqlという変数に引数のテキストを追加するものです。
コード中、type2dbtypeは、Bash on Railsのデータ型を引数にとり、対応するRDBMSのデータ型名を返すユーティリティ関数です(connection_adapters/*.shで定義)。また、string_pushは第1引数に変数名を、第2引数に文字列をとり、変数に入っている文字列の末尾に新しく文字列を追加するユーティリティ関数です(ほかのシェルスクリプトで定義)。
elbat_etaerc(create_tableの逆綴り)のほうは、こんな実装です。
function elbat_etaerc() { echo "-- create_table($table)" do_create_table "$dbconf_database" "$table" "$columns_sql" }
一連の${文字列}.column関数で追加してきたcolumns_sqlを元にSQL文を作り、実行する処理です。do_create_tableも、connection_adapters/*.shで定義しています。
これで単にdb/schema.shをsourceしてもいいのですが、変数をグローバルにしないために関数に封じ込めます。
function load_schema() { local table prefix columns_sql . db/schema.sh unset -f ${prefix}.column }
db/schema.shを読んだ後は、${文字列}.columnという関数も未定義に戻します。
O/Rマッパーの定義として読み込む場合
次に、O/Rマッパー(もどき)の定義としてdb/schema.shを読み込む場合です。
読み込んだ結果、table_listという変数にテーブルの一覧が、${テーブル名}_columnsという変数にカラム名の一覧が、${テーブル名}_columns_typeという変数に各カラムのデータ型が入る仕様とします。
最初に、テーブルを作るときと同じく、config/database.shとconnection_adapters/*.shを読み込みます。
create_table関数は、以下のように実装します。
function create_table() { table=$1 prefix=$2 string_push table_list " $table" unset ${table}_columns ${table}_columns_type eval "function ${prefix}.column() { local column=\$1 local datatype=\$2 string_push \${table}_columns \" \$column\" string_push \${table}_columns_type \" \$datatype\" }" }
elbat_etaerc関数のほうは、何もしません。
function elbat_etaerc() { true # dummy }
そして、db/schema.shをsourceします。
function load_schema() { local table prefix unset table_list . db/schema.sh unset -f "${prefix}.column" }
まとめ
スキーマ定義をシェルスクリプトとして読み込む方法を解説しました。ブロック引数風の文字列から関数名を作るのは、単に見た目をRuby on Railsっぽくしたかっただけです。
データベース固有の処理を抽象化している関数は、まだきれいに抽象化できてはいないので、気が向いたらいろいろ変えるかもしれません。
次回は、bashでOO風のプログラミングを実現するしくみを紹介しようかと思います。
注意:Bash on Railsは、実用性を無視したパロディソフトです。誤解のないようお願いします。