fc2ブログ

本を読む

読書やコンピュータなどに関するメモ

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巻

 「エレファント」「動機とアリバイ」の2編を収録。

 「エレファント」にはやられた。シリーズを利用した叙述トリックだな、これは。ラストでも叙述トリックをもうひと押し。そのため、これ以上の言及は自粛しておく。

 「動機とアリバイ」は、オーソドックスなアリバイトリックもの。


「かへ ―とその他の短篇」

かへ―とその他の短篇
藤井組 山本 揚志
マッグガーデン (2008/02)

 ネット中に感動と驚愕を呼んだネットマンガ「瀬をはやみ」「かへ」が、いつのまにか単行本になっていた。

 思わず買ってしまった。

 「瀬をはやみ」を最初に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巻

 「恐喝詐欺」と「任意後見人詐欺」の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人の盗賊」は「アラビアンナイト」に元からあるエピソードではなく、後世に追加されたエピソードだそうだ。同じように、「アラビアンナイト」にありそうなエピソードを、パスティーシュの名手、清水義範が書いた本。「水パイプの中の恋と冒険の物語」「タイル職人とコウノトリの物語」「坊さんも気絶するナス料理の物語」など、タイトルだけでも不思議なタイトルの物語が並んでいる。そして最後に…

 もうひとつのポイントは、著者が旅行して知ったイスラム圏の薀蓄をちりばめてあること。話の最初にエッセイ風にマクラを振って本編に移ったり、話の途中で歴史上のエピソードにつなげたりと、うまく話にとけこんでいる。「坊さんも気絶するナス料理」が実在するとは知らなかった。

 面白かった。

新アラビアンナイト (集英社文庫 し 22-14)
清水 義範
集英社 (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は、実用性を無視したパロディソフトです。誤解のないようお願いします。

 | HOME | 

Categories

Recent Entries

Recent Comments

Recent Trackbacks

Appendix

emasaka

emasaka

フリーター。
連絡先はこのへん

Monthly