4 Hello, eRuby

あれ、まだHello, dRubyなの? と思われたかもしれませんが、この章で紹介するのはeRubyの実装の一つ、 ERBです。 文書にRubyスクリプトを埋め込むeRubyは、ちょっとした定型文書の生成や CGI、Webページの生成に便利です。

この章ではeRubyの紹介とERBの使用方法を説明し、 dRubyと組み合わせた簡単なサンプルを示します。

4.1 eRuby

eRubyは任意のテキストファイルにRubyスクリプトを埋め込む書式です。 JSP、ASPのRuby版をイメージするとわかりやすいでしょう。 次のマークアップを使ってRubyスクリプトを埋め込みます。

  • <% ... %> --- Rubyスクリプト片をその場で実行
  • <%= ... %> --- 式を評価した結果をその場に挿入
  • これ以外 --- 文字列をその場に挿入

変換例を示します。 次の例はRubyの式の結果を挿入するeRubyスクリプトです。

[変換前]
<% now = Time.now %>
今日は<%= now.day %>日です。

[変換後]

今日は19日です。

Rubyスクリプト片として分岐や繰り返しなどの制御構造も使用できます。 まず分岐の例です。

[変換前]
<%
  if Time.now.wday == 0
%>今日はお休み<%
  else
%>今日もお仕事<%
  end
%>

[変換後]
今日はお休み

つづいて繰り返しを埋め込んだeRubyスクリプトを示します。

[変換前]
<%
  todo = []
  todo.push("目覚ましを止める")
  todo.push("顔を洗う")
  todo.push("歯を磨く")
  todo.push("出かける")

  todo.each_with_index do |item, idx|
%>[<%= idx %>] <%= item %>
<%
  end
%>

[変換後]
[0] 目覚ましを止める
[1] 顔を洗う
[2] 歯を磨く
[3] 出かける

このようにテキストファイルやHTMLなどにRubyスクリプトを埋め込む書式がeRubyです。 eRubyはHTMLに限らず、任意のテキストファイルの出力に使用できます。 HTML以外にも使用できる反面、文法として壊れたHTMLファイルを出力する可能性もあります。

eRubyの実装にはCで書かれたerubyとRubyで書かれたERBがあります。 erubyはCGIとしてもフィルタとしても動作するコマンドです。 ERBはRubyで書かれたクラスライブラリです。ERBにはフィルタとして動作する スクリプトも付属しますが、アプリケーションがrequireして使用する方が一般的です。

4.2 ERB

ERBはeRubyのRubyによる実装で、ruby-1.8以降に標準添付されています。 ERBはかつてERb/ERbLightと呼ばれていましたが、 標準添付化の議論を重ねる中で現在の名前になりました。

ERBはRubyのライブラリとして実装されており、アプリケーションの中で eRubyスクリプトを使用することができます。 eRubyのスクリプトを実行する際に、任意のbindingを与えることができるので eRubyスクリプトの中から、eRubyスクリプトを実行するオブジェクトの インスタンス変数やインスタンスメソッドにアクセスすることができます。 これにより、eRubyスクリプトからアプリケーションに触れることができ、 アプリケーションの中にeRubyスクリプトを組み込む応用の幅が出てきます。 後で紹介する「Reminder CGIインターフェイス」でもこれを用いています。

4.2.1 ERBのインストール

ruby-1.8以降ではERBは標準添付となっているため、インストールの必要はありません。 ruby-1.6ではERBをインストールする必要があります。 ERBのインストールは簡単で、アーカイブを展開し、install.rbを実行するだけです。

% tar xzvf erb-2.0.x.tar.gz
% cd erb-2.0.x
% sudo ruby install.rb

ERBをフィルタとして使用するコマンドerbが、erb-2.0.x/bin/erb にあります。 Rubyのパスなどを編集して適切な場所にコピーしてください。

ruby-1.8ではerbが適切な場所にインストールされていると思います。

4.2.2 eRubyとの違い

ERBは一部eRubyと仕様が異なります。 この節ではeRubyとの違いについて説明します。

ERBとeRubyの仕様の違いは、埋め込みスクリプト中での標準出力への印字が その場所への文字列の挿入とはならず、そのまま標準出力へ書かれる点です。

次のように挙動が異なります。

% cat hello.erb
Hello, <% print "World" %>.

% eruby hello.erb
Hello, World.

% erb hello.erb
WorldHello, .

erbでの<% print %>は、その場所への挿入でなく標準出力への印字となってしまうため、 erubyと結果が異なってしまいます。

その場所に文字列を挿入するには <%= ... %> を使ってください。

% cat hello2.erb
Hello, <%= "World" %>.

% eruby hello2.erb 
Hello, World.

% erb hello2.erb
Hello, World.

4.2.3 ERBクラス

この節ではERBをライブラリとして使用する方法について説明します。

ERBはeRubyスクリプトを実行して印字するだけでなく、 文字列への変換やRubyスクリプトへの変換を行なえます。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2erb.jpg

図4.1 eRubyスクリプトから生成されたERBオブジェクトは、結果の印字だけでなく、result、srcを用いて変換できる。

よく使われるERBのメソッドのリファレンスを示します。

ERB.new(eruby_script, safe_level=nil, trim_mode=nil)

eruby_scriptからERBを生成する。eval時の$SAFE、trim_mode(後述)を指定できる。

run(b=TOPLEVEL_BINDING)

ERBをbのbindingで実行し、印字する。

result(b=TOPLEVEL_BINDING)

ERBをbのbindingで実行し、文字列を返す。

src

変換したRubyスクリプトを返す。

ERBを使った簡単なスクリプト(疑似コード)を示します。

001: require 'erb'
...
002: erb = ERB.new(eruby_script)
...
003: erb.run

001: ERBを使用するには'erb'をrequireします。 erbはruby-1.8系には標準で含まれていますが、 ruby-1.6系では別途インストールが必要です。

002: eRubyスクリプトからERBオブジェクトerbを生成します。 ERB.newではまだスクリプトは実行されません。

003: 次にeRubyスクリプトを実行し、結果を印字するならrunメソッドを、 文字列に変換するならresultメソッドを使います。

eRubyスクリプトの評価は通常、$SAFE=0、TOPLEVEL_BINDINGで行われます。 セーフレベルを設定するにはERB.newの第二引数で指定します。 evalのbindingを指定するにはresult・runメソッドの引数で指定します。

4.2.4 ERB使用例

次のスクリプトはCGIスクリプトでERBをライブラリとして使用する例です。 eRubyスクリプトを単に実行するのではなく、 resultのbindingによって、MyCGIオブジェクトのスコープでeRubyスクリプトを 実行しています。 eRubyスクリプトのみで処理の全てを完結させずに、 アプリケーションの中でERBを用いるこういったスタイルは、 ERBではよく使われるテクニックの一つです。

List 4.1 ERB使用例(疑似コード)

require 'erb'
require 'cgi'
class MyCGI
  def initialize
    @cgi = CGI.new
    @erb = ERB.new(eruby_script)
    @foo = 'bar'
  end

  def do_it
    # your_logic
  end

  def build_page
    begin
      return @erb.result(binding)
    rescue
      return FAILED_PAGE
    end
  end
  .....
  def eruby_script
    <<EOS
<pre>foo: <%= @foo %></pre>
.....
EOS
   end
 end
 ....
 app = MyCGI.new
 app.do_it
 page = app.build_page
 ....

このサンプルではERB#resultメソッドを使用してeRubyスクリプトを評価しています。 評価中にエラーが発生した場合、rescue句が実行され FAILED_PAGEを返します。 eRubyスクリプトで全て行なう場合に比べて、エラー処理などがシンプルになります。

begin
  return erb.result(binding)
rescue
  return エラーページ
end

このスタイルでは次のメリットがあります。

  • eRubyによるページ生成とその他のロジックが分離しCGIスクリプトがすっきりする
  • eRubyスクリプトにエラー処理を記述する必要がなくなる *1

注意

eRubyスクリプトの中でさらにeRubyスクリプトを処理させる場合には 注意が必要です。 ERBの内部では、テンポラリの変数 *2に文字列を一度格納します。 ERBの評価中(result/run)にERBをさらに評価する場合に、 この変数が同じであると文字列が初期化されてしまい、思わぬバグに遭遇します。

% irb -r erb
>> s1 = "s1: [<%= Time.now %>]"
>> ERB.new(s1).result(binding)
=> "s1: [Sat Jan 31 14:49:17 JST 2004]"

ここまでは正常です。s1をERBの内側でさらに評価してみましょう。

>> s2 = "s2: (<%= ERB.new(s1).result(binding) %>)"
>> ERB.new(s2).result(binding)
=> "s1: [Sat Jan 31 14:49:44 JST 2004])"

結果には前半部分となる"s2: ("が含まれていませんね。 これはs2のスクリプトを処理中にs1のスクリプトを処理したために 結果が初期化されてしまったのです。

スコープ(binding)が異なれば問題ありません。 (呼び出したメソッドの中でERBを評価する場合など)

この現象は、s1の評価をメソッドすることで避けることができます。

class Foo
  def do_s1
    ERB.new("s1: [<%= Time.now %>]").result(binding)
  end

  def do_s2
    ERB.new("s2: (<%= do_s1 %>)").result(binding)
  end
end

put Foo.new.do_s2 # => s2: (s1: [Sat Jan 31 14:58:02 JST 2004])

4.3 Reminder CGIインターフェイス

前章で作成したReminderにERBをつかったCGIインターフェイスを作成します。

作成するCGIは、前章のCUI版と同様に、

  • ToDoの一覧表示
  • アイテムの削除
  • アイテムの追加

が可能なものとします。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2rem0cgi.jpg

図4.2 作成するシステムの概観。ReminderサーバとCGIインターフェイスから構成され、 ERBを使ってページを生成する。

4.3.1 CGIの準備

あなたの環境(OSやhttpサーバなど)に合わせ、CGIのスクリプトをどこに 設置するかなどの設定を行い、CGIの実行方法を確認します。

たとえば、VineLinuxでは/home/httpd/cgi-bin、 MacOS Xでは/Library/WebServer/CGI-Executablesにスクリプトを置くようです。

簡単なスクリプトでCGIの準備ができているか確かめましょう。 先頭の行でrubyの実行ファイルを指定してますが、 これも各自の環境に合わせた値を書いて下さい。 なお、このスクリプトは文字のコードとしてEUCを期待しています。 ファイルもEUCで保存してください。

List 4.2 hello-cgi.rb

# hello-cgi.rb
#!/usr/local/bin/ruby
require 'cgi'
require 'erb'
require 'drb/drb'

$KCODE='euc'

cgi = CGI.new('html3')

cgi.out({"charset"=>"euc-jp"}) {
  cgi.html {
    cgi.head { 
      cgi.title { 'Hello, CGI.' }
    } + cgi.body { "<p>こんにちは、世界。<p>" }
  }
}

設置したCGIをWebブラウザで表示させてみましょう。 次のようなページが表示されるでしょうか?

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2hello-cgi.jpg

図4.3 hello-cgi.rbの実行結果

期待に反してエラーページが表示されるようなら、修正しなくてはなりません。 でも残念ながら私にはあなたの環境はわからないのです。 チェック項目をいくつか挙げますので、設定のヒントにしてください。

  • ファイルの設置場所。置き場所を間違ってませんか?
  • ファイルのパーミション(実行権など)。httpサーバのユーザから実行できますか?
  • httpサーバの設定。CGIを実行するように設定されますか?
  • rubyインタプリタの場所。文中で#!/usr/local/bin/ruby とある部分は、 rubyのインタプリタの場所を示しています。rubyの場所はどこですか?

うまく動いたでしょうか? 上のスクリプトは、標準添付のCGIクラスを使って簡単なページを出力するものです。 この後で必要なerbとdrb/drbをrequireしています。

うまく動いたら次のステップに進みましょう。

4.3.2 一覧表示

下のように<ul>を使った箇条書きで表示するERBを書くことにしましょう。

<ul>
<li>1: 13:00 ミーティング</li>
<li>3: 土曜日にDVDを返す</li>
<li>4: 15:00 進捗報告</li>
<li>5: 図書館にRHGをリクエストする</li>
</ul>

<li>...</li>の内側を変えながら繰り返し行を挿入しておけばよさそうです。 aryに項目が入っているとすると、次のように書けます。

<ul>
<% ary.each do |k, v| %>
<li><%= k %>: <%= v %></li>
<% end %>
</ul>

その前にReminderサーバの準備が必要です。 前章の最後に作ったreminder0.rbを起動します。 テスト用のデータも与えます。

[ターミナル1]
% irb --prompt simple -r reminder0.rb -r drb/drb
>> $KCODE='euc'
>> front = Reminder.new
>> front.add('13:00 ミーティング')
>> front.add('17:00 進捗報告')
>> front.add('土曜日にDVDを返す')
>> DRb.start_service('druby://localhost:12345', front)

ERBを使う

ERBを使って表示するスクリプトを書きます。

List 4.3 erb-test0.rb

# erb-test0.rb
require 'erb'
require 'drb/drb'

erb_src = <<EOS
<ul>
<% there.to_a.each do |k, v| %>
<li><%= k %>: <%= v %></li>
<% end %>
</ul>
EOS

DRb.start_service
there = DRbObject.new_with_uri('druby://localhost:12345')

ERB.new(erb_src).run

erb_srcに入っているeRubyスクリプト実行して印字するスクリプトです。 実行してみましょう。

[ターミナル2]
% ruby -Ke erb-test0.rb
<ul>

<li>1: 13:00 ミーティング</li>

<li>2: 17:00 進捗報告</li>

<li>3: 土曜日にDVDを返す</li>

</ul>

期待したように印字されていますか?

HTML生成係

処理の流れがそのまま書いてあるだけで、ちょっと座りが悪い感じがします。 HTMLを生成する係を定義して書き直してみます。 行数は増えてしまいましたが、生成係ReminderWriterの定義とmainが明らかに わけられて安心です。*3

List 4.4 erb-test1.rb

# erb-test1.rb
require 'erb'
require 'drb/drb'

class ReminderWriter
  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
  end

  def erb_src
    <<EOS
<ul>
<% @reminder.to_a.each do |k, v| %>
<li><%= k %>: <%= v %></li>
<% end %>
</ul>
EOS
  end

  def to_html
    @erb.result(binding)
  end
end

def main
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')

  writer = ReminderWriter.new(there)
  puts writer.to_html
end

main

実行してみましょう。結果は同じはずです。

[ターミナル2]
% ruby -Ke erb-test1.rb
<ul>

<li>1: 13:00 ミーティング</li>

<li>2: 17:00 進捗報告</li>

<li>3: 土曜日にDVDを返す</li>

</ul>

ReminderWriterのerb_srcとto_htmlに注目して下さい。 erb_srcで返すeRubyスクリプトで、「@reminder」と インスタンス変数を参照しています。

------
  def erb_src
    <<EOS
<ul>
<% @reminder.to_a.each do |k, v| %>
<li><%= k %>: <%= v %></li>
<% end %>
</ul>
EOS
  end

  def to_html
    @erb.result(binding)
  end
------

to_htmlで@erb.resultにbindingを与え、to_htmlのコンテキストでevalしているので eRubyスクリプトはto_htmlと同様のスコープで実行されます。 これにより、インスタンス変数@reminderにアクセスできるのです。

なお、resultやrunにbindingを与えない場合は、 TOPLEVELのbindingで評価が行なわれます。

HTMLエスケープ/サニタイジング

実は忘れている処理があります。 ToDoの項目をそのまま印字していますが、項目中に < や >、& などがあると HTMLとして正しく表示できません。 またToDo項目に<a href=".."> や JavaScript が含まれている可能性を 考えるとXSS脆弱性の原因ともなります。

動的にHTMLを生成するアプリケーションは、適切にHTMLエスケープをする必要があります。 HTMLエスケープには、標準添付のCGIクラスに用意されているCGI.escapeHTMLが使用できます。

<ul>
<% @reminder.to_a.each do |k, v| %>
<li><%= k %>: <%= CGI.escapeHTML(v) %></li>
<% end %>
</ul>

CGI#escapeHTMLでも機能としては充分ですが、 ERBには同様な機能を持ち、もうちょっとすっきり記述するモジュールERB::Utilが 用意されています。

ERB::Util.html_escape(s)
ERB::Util.h(s)

HTMLの&"<>をエスケープする

ERB::Util.url_encode(s)
ERB::Util.u(s)

文字列をURLエンコードする

h、uと短く奇妙なメソッドがありますが、これは次のように使うものです。

<ul>
<% @reminder.to_a.each do |k, v| %>
<li><%= k %>: <%=h v %></li>
<% end %>
</ul>

eRubyスクリプトで <%=h ... %> と書くことでHTMLエスケープした結果を 挿入することができます。 一見eRubyスクリプトの拡張に見えますが、実はこれは

<%= h(...) %>

で、いつものメソッド呼び出しです。

hやuを使用するには、ERB::Utilをincludeしておく必要があります。

class ReminderWriter
  include ERB::Util
  ...
end

実験の前に、ReminderのToDo項目に、そのまま印字されると困るデータを 準備しましょう。

[ターミナル1]
>> front.add('<や>をエスケープすること')
>> front.to_a
=> [[1, "13:00 ミーティング"], [2, "17:00 進捗報告"], [3, "土曜日にDVDを返す"], [4, "<や>をエスケープすること"]]

まず、先ほどのバージョンで実験してみます。4つ目の項目に注目して下さい。 <と>がそのまま表示されています。

[ターミナル2]
% ruby -Ke erb-test1.rb
<ul>

<li>1: 13:00 ミーティング</li>

<li>2: 17:00 進捗報告</li>

<li>3: 土曜日にDVDを返す</li>

<li>4: <や>をエスケープすること</li>

</ul>

続いてHTMLエスケープをするように変更したバージョンを示し、実験します。

List 4.5 erb-test2.rb

# erb-test2.rb
require 'erb'
require 'drb/drb'

class ReminderWriter
  include ERB::Util

  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
  end

  def erb_src
    <<EOS
<ul>
<% @reminder.to_a.each do |k, v| %>
<li><%= k %>: <%=h v %></li>
<% end %>
</ul>
EOS
  end

  def to_html
    @erb.result(binding)
  end
end

def main
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')

  writer = ReminderWriter.new(there)
  puts writer.to_html
end

main

ERB::Utilをincludeし、<%=h v %>に変更しました。

[ターミナル2]
% ruby -Ke erb-test2.rb
<ul>

<li>1: 13:00 ミーティング</li>

<li>2: 17:00 進捗報告</li>

<li>3: 土曜日にDVDを返す</li>

<li>4: &lt;や&gt;をエスケープすること</li>

</ul>

4つ目の項目の<と>がエスケープされていることがわかります。

HTML生成時に特殊な文字をエスケープすることで、正しい表示を得る ことができます。またこのエスケープ処理によって、ユーザからの 入力を元にページを生成する処理において、XSSなど悪意のある入力を 無害化(サニタイジング)することができます。

CGIへの組み込み

準備ができました。 erb-test2で作成したスクリプトを最初にテストしたhello-cgi.rbに 混ぜてCGIからReminderが利用できるようにします。

List 4.6 reminder0-cgi.rb

# reminder0-cgi.rb
#!/usr/local/bin/ruby
require 'cgi'
require 'erb'
require 'drb/drb'

$KCODE='euc'

class ReminderWriter
  include ERB::Util

  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
  end

  def erb_src
    <<EOS
<ul>
<% @reminder.to_a.each do |k, v| %>
<li><%= k %>: <%=h v %></li>
<% end %>
</ul>
EOS
  end

  def to_html
    @erb.result(binding)
  end
end

def main 
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')
  writer = ReminderWriter.new(there)

  cgi = CGI.new('html3')

  cgi.out({"charset"=>"euc-jp"}) {
    cgi.html {
      cgi.head { 
        cgi.title { 'Reminder' }
      } + cgi.body { writer.to_html }
    }
  }
end

main

読めますか? hello-cgi.rbを元にReminderWriterの定義をerb-test2.rbから引っ越してきたものです。

まず、このスクリプトを単体で実行してみましょう。 CGI.newは単体で起動した時にオフラインモードで動作します。

[ターミナル2]
% ruby reminder0-cgi.rb
(offline mode: enter name=value pairs on standard input)
「ここで [ctrl]-D を押し入力を終了する」
Content-Type: text/html; charset=euc-jp
Content-Length: ....
Content-Language: ja

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><HTML><HEAD><TITLE>Hello, CGI.</TITLE></HEAD><BODY> <ul>

<li>1: 13:00 ミーティング</li>

<li>2: 17:00 進捗報告</li>

<li>3: 土曜日にDVDを返す</li>

<li>4: &lt;や&gt;をエスケープすること</li>

</ul>
</BODY></HTML>

スクリプトに誤りがないことを確認できたら(実行時エラーがないことを 確認できたら)、CGIスクリプトとして設置しましょう。 hello-cgi.rbを実験した時のようにあなたの環境に合わせて設定して下さい。

ではブラウザで表示してみます。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2reminder0-cgi.jpg

図4.4 reminder0-cgi.rbの実行結果

どうですか?項目一覧が表示されたでしょうか。

外観の変更

<ul>による一覧はちょっとさみしい感じがしますね。 <table>を使った一覧に変えてみましょう。 eRubyスクリプトの部分(ReminderWriterのerb_srcメソッドの中)を次のように変えて、 もう一度CGIを表示してみましょう。

<table border='1'>
<% @reminder.to_a.each do |k, v| %>
<tr><td><%= k %></td><td><%=h v %></td></tr>
<% end %>
</table>

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2reminder0-tbl.jpg

図4.5 <table>版の実行結果

このようにちょっとした外観の変更は、eRubyスクリプトを入れ替えることで可能です。

表はなかなかよいのですが、一行ごとに行の背景色を変えておくと見やすくなりそうです。 まず、二つの背景色の属性を交互に出力するクラスを準備します。 久しぶりのRubyスクリプトです。

class BGColor
  def initialize
    @colors = ['#eeeeff', '#bbbbff']
    @count = -1
  end
  attr_accessor :colors

  def next_bgcolor
    @count += 1
    @count = 0 if @colors.size <= @count
    "bgcolor='#{@colors[@count]}'"
  end

  alias :to_s :bgcolor
end

BGColorクラスは、あらかじめ設定された背景色を順々に繰り返し出力する係です。 特別むずかしい部分はないと思います。

colors=(ary)

背景色の配列をセットします。

next_bgcolor

背景色の属性となる文字列("bgcolor='#eeeeff'"など)を返します。 呼ぶたびに異なる色を返します。

to_s

next_bgcolorの別名です。eRubyスクリプトの<%= obj %>では、 obj.to_sの結果を埋め込むので、to_sを定義しておくと簡潔に書ける 局面があります。

ReminderWriterのインスタンス変数@bgにBGColorのインスタンスをセットしておいて、 eRubyスクリプトの<tr>の部分を変更します。

def initialize(reminder)
  @reminder = reminder
  @erb = ERB.new(erb_src)
  @bg = BGColor.new       # 追加
end
[変更前]
<tr><td><%= k %></td><td><%=h v %></td></tr>

[変更後]
<tr <%= @bg %>><td><%= k %></td><td><%=h v %></td></tr>

<%= @bg %>は、@bg.to_sの結果をここに挿入します。 next_bgcolorの別名を準備したのはこのためです。 <%= @bg %>は、<%= @bg.next_bgcolor %>と同じ意味になります。

eRubyスクリプトの中でBGColorクラスを定義していない点に注目してください。 このCGIをeRubyだけで記述することも可能ですが、表示そのもののコードの中に クラス定義など前処理を記述しなければなりません。

ERBを積極的にライブラリとして使用する作戦では、 前処理やロジックをeRubyスクリプトの外、つまりRubyスクリプトに追い出します。 この例では、ReminderWriterそのものもそうですし、BGColorの定義や生成を Rubyスクリプトに置いています。 短く、表示部分に特化したeRubyスクリプトの部分と、その他部分 (リクエストの取り出し、表示の前処理など)をRubyスクリプトで CGIを構成することで、アプリケーションをわかりやすく記述できます。

List 4.7 reminder0-cgi2.rb

# reminder0-cgi2.rb
#!/usr/local/bin/ruby
require 'cgi'
require 'erb'
require 'drb/drb'

$KCODE='euc'

class BGColor
  def initialize
    @colors = ['#eeeeff', '#bbbbff']
    @count = -1
  end
  attr_accessor :colors

  def next_bgcolor
    @count += 1
    @count = 0 if @colors.size <= @count
    "bgcolor='#{@colors[@count]}'"
  end

  alias :to_s :next_bgcolor
end

class ReminderWriter
  include ERB::Util

  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
    @bg = BGColor.new
  end

  def erb_src
    <<EOS
<table border="0" cellspacing="0">
<% @reminder.to_a.each do |k, v| %>
<tr <%= @bg %>><td><%= k %></td><td><%=h v %></td></tr>
<% end %>
</table>
EOS
  end

  def to_html
    @erb.result(binding)
  end
end

def main 
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')
  writer = ReminderWriter.new(there)

  cgi = CGI.new('html3')

  cgi.out({"charset"=>"euc-jp"}) {
    cgi.html {
      cgi.head { 
        cgi.title { 'Reminder' }
      } + cgi.body { writer.to_html }
    }
  }
end

main

最後にBGColorを使ったバージョンの完全なスクリプトと実行結果を載せます。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2reminder0-tbl2.jpg

図4.6 reminder0-cgi2.rbの実行結果

項目の追加

一覧が表示できるようになりました。 この節では項目の追加ができるようにします。

まず画面を決めます。 とりあえず項目一覧の下に追加用のテキストフィールドをおきましょう。

<form method="post">
<table border="0" cellspacing="0">
<% @reminder.to_a.each do |k, v| %>
<tr <%= @bg %>><td><%= k %></td><td><%=h v %></td></tr>
<% end %>
<tr <%= @bg %>>
<td><input type="submit" value="add" /></td>
<td><input type="text" name="item" value="" size="30" /></td>
</tr>
</table>
</form>

erb_srcメソッドを修正して表示させてみましょう。 テキストフィールドが表示されていますか?

つぎにCGIのリクエストを分析して、Reminderサーバに項目を追加する部分を考えます。

  1. テキストフィールドの文字列を取り出す
  2. 文字列がなければ、一覧表示だけする
  3. 文字列があれば、文字コードを正規化してサーバに項目を追加する

これに従ってメソッドを記述します。

def kconv(str)
  NKF.nkf('-edXm0', str.to_s)             #3'
end

def do_request(cgi, reminder)
  item ,= cgi['item']                     #1
  return if item.nil? || item.empty?      #2
  reminder.add(kconv(item))               #3
end

テキストフィールドは'item'というCGIのパラメータに入っています。 なぜ'item'に入っているのかと言うと、eRubyスクリプトでそのような <form>を出力しているからです。

item ,= cgi['item'] #1

の ,= cgi[key] はCGIスクリプトでよく見られる、多重代入を利用したイディオムで、 keyと名前のついたパラメータの先頭の要素を取り出すことができます。 itemがnilあるいは空文字列の場合に何もせずreturnします(2)。 文字列があったとき、kconvメソッド(3')で文字コードを正規化した結果を reminderにaddします(3)。

簡単ですね。 リクエストから取り出した文字列は、Reminderサーバが使用する文字コードに 合わせて正規化する必要があります。 kconvメソッドに注目してみましょう。

NKF.nkf('-edXm0', str.to_s)  #3'

NKFを利用して、外部(つまりWebブラウザ)から届いた文字列を 決められた形式に正規化しています。nkfメソッドの最初の引数は、 nkfコマンドと同じオプション文字列です。 それぞれ次の意味を持ちます。

  • e - EUCに変換
  • d - 改行コードを"?n"にする
  • X - MS漢字中にX0201仮名があると仮定する。(1byteのカナに気をつけろ)
  • m0 - MIME解読をしない

完全なスクリプトと実行結果を示します。 <form>の部分にaction属性の指定が追加してあります。

List 4.8 reminder0-cgi3.rb

# reminder0-cgi3.rb
#!/usr/local/bin/ruby
require 'cgi'
require 'erb'
require 'drb/drb'
require 'nkf'

$KCODE='euc'

class BGColor
  def initialize
    @colors = ['#eeeeff', '#bbbbff']
    @count = -1
  end
  attr_accessor :colors

  def next_bgcolor
    @count += 1
    @count = 0 if @colors.size <= @count
    "bgcolor='#{@colors[@count]}'"
  end

  alias :to_s :next_bgcolor
end

class ReminderWriter
  include ERB::Util

  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
    @bg = BGColor.new
  end

  def script_name(cgi)
    cgi.script_name
  end

  def erb_src
    <<EOS
<form action="<%=script_name(cgi)%>" method="post">
<table border="0" cellspacing="0">
<% @reminder.to_a.each do |k, v| %>
<tr <%= @bg %>><td><%= k %></td><td><%=h v %></td></tr>
<% end %>
<tr <%= @bg %>>
<td><input type="submit" value="add" /></td>
<td><input type="text" name="item" value="" size="30" /></td>
</tr>
</table>
</form>
EOS
  end

  def to_html(cgi)
    @erb.result(binding)
  end
end

def kconv(str)
  NKF.nkf('-edXm0', str.to_s)
end

def do_request(cgi, reminder)
  item ,= cgi['item']
  return if item.nil? || item.empty?
  reminder.add(kconv(item))
end

def main 
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')
  writer = ReminderWriter.new(there)

  cgi = CGI.new('html3')
  do_request(cgi, there)

  cgi.out({"charset"=>"euc-jp"}) {
    cgi.html {
      cgi.head { 
        cgi.title { 'Reminder' }
      } + cgi.body { writer.to_html(cgi) }
    }
  }
end

main

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2reminder0-add.jpg

図4.7 reminder0-cgi3.rbの実行結果

うーん。なんだかおさまり悪いです。 do_request、kconvの二つのメソッドはどこに置くべきでしょうか? ReminderWriterが作るテキストフィールドの名前を外のメソッドが 知らなくてはならないし、kconvはReminderサーバの処理コードに 依存しています。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2rem0cgi1.jpg

図4.8 reminder0-cgi3.rbのクラス図。do_requestなど関数的なメソッドがある。

ReminderWriterの機能を変更し、リクエストの処理とページ生成の係に してはどうでしょうか? つまりReminderWriterをReminderのCGIインターフェイスの係とするのです。 リクエストの処理は<form>の生成と関連があるので悪くない作戦です。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2rem0cgi2.jpg

図4.9 変更後のクラス図

ReminderWriterをReminderCGIと改名しkconv, do_requestメソッドを移動してみます。 kconvはそのまま移動できますが、do_requestは引数でもらっていた reminderの代わりにインスタンス変数を利用するように変更します。

class ReminderCGI
  ...
  def kconv(str)
    NKF.nkf('-edXm0', str.to_s)
  end

  def do_request(cgi)
    item ,= cgi['item']
    return if item.nil? || item.empty?
    @reminder.add(kconv(item))
  end
end

これに伴い、mainメソッドも修正します。

def main 
  ...
  reminder = ReminderCGI.new(there)

  cgi = CGI.new('html3')
  reminder.do_request(cgi)
  ...
  ...
end

Webブラウザで変更前と同じように動作することを確認して下さい。

項目の削除

項目の一覧表示、追加ができるようになりました。 項目の削除ができたら、この章はゴールです。

表の各行に削除を行うリンクを設けて、項目の削除を可能にします。 なお、リンクで項目を削除する、というのはWebロボットなどが リンクを辿る際に実行されてしまうかもしれないので危険な仕様です。 実際に運用するシステムで採用するのは注意が必要です。

まず、CGIのパラメータを決めましょう。 これまでは操作が追加しかなかったのですが、今回から削除が増えます。 それにともない、CGIのパラメータも設計し直した方がよいでしょう。

今回は操作の種類を示す必須のパラメータと、それぞれの操作ごとに必要な パラメータで構成することにします。

  • cmd - 操作の種類。add、deleteなど。必須のパラメータ
  • item - addする項目の内容
  • key - deleteの対象を示すキー

cmdがメソッド名、その他のパラメータが引数といった風に想像すると Rubyスクリプトと対応付けて考えやすいと思います。

これに合わせてdo_requestを修正します。 これまでのdo_requestは追加操作のみを扱うようになっていましたが、 cmdに従って処理を振り分ける係にします。これまでのdo_requestは do_addというメソッドに変更し、do_requestが処理を振り分けたあとに 呼ばれるようにします。

class ReminderCGI
  ...
  def do_add(cgi)
    item ,= cgi['item']
    return if item.nil? || item.empty?
    @reminder.add(kconv(item))
  end

  def do_request(cgi)
    cmd ,= cgi['cmd']
    case cmd
    when 'add'
      do_add(cgi)
    end
  end
end

CGIのパラメータcmdが'add'であるとき、do_addに振り分けるようにしました。 これに'delete'を追加します。まずdo_requestの変更です。

class ReminderCGI
  ...
  def do_request(cgi)
    cmd ,= cgi['cmd']
    case cmd
    when 'add'
      do_add(cgi)
    when 'delete'     # 追加
      do_delete(cgi)  # 追加
    end
  end
end

そして、do_deleteを定義します。

def do_delete(cgi)
  key ,= cgi['key']
  return if key.nil? || key.empty?
  @reminder.delete(key.to_i)
end

これで操作を担当する部分は完成しました。 あとは、このパラメータに沿った形式でページを生成する必要があります。

それぞれの行の最後の列に削除のリンクを追加します。

<% @reminder.to_a.each do |k, v| %>
<tr <%= @bg %>>
 <td><%= k %></td>
 <td><%=h v %></td>
 <td>[<%=a_delete(k)%>X</a>]</td>
</tr>
<% end %>

a_deleteというメソッドは、削除の操作を作成するリンクを作成するメソッドです。 パラメータは削除対象の項目のキーです。

def make_param(hash)
  hash.collect do |k, v|
    u(k) + '=' + u(v)
  end.join(';')
end

def anchor(cgi, hash)
  %Q+<a href="#{script_name(cgi)}?#{make_param(hash)}">+
end

def a_delete(cgi, key)
  anchor(cgi, {'cmd'=>'delete', 'key'=>key})
end

追加の操作のために、パラメータcmdに'add'をセットする <input type="hidden">要素を追加します。 hiddenであるため表示されることはありません。 また、<form>の範囲を表全体を囲むのではなく、最後の行だけを囲むように 変更します。

<form action="<%=script_name(cgi)%>" method="post">
 <input type="hidden" name="cmd" value="add" />
 <tr <%= @bg %>>
  <td><input type="submit" value="add" /></td>
  <td><input type="text" name="item" value="" size="30" /></td>
  <td>&nbsp;</td>
 </tr>
</form>

完全なスクリプトと実行結果を示します。

List 4.9 reminder0-cgi4.rb

# reminder0-cgi4.rb
#!/usr/local/bin/ruby
require 'cgi'
require 'erb'
require 'drb/drb'
require 'nkf'

$KCODE='euc'

class BGColor
  def initialize
    @colors = ['#eeeeff', '#bbbbff']
    @count = -1
  end
  attr_accessor :colors

  def next_bgcolor
    @count += 1
    @count = 0 if @colors.size <= @count
    "bgcolor='#{@colors[@count]}'"
  end

  alias :to_s :next_bgcolor
end

class ReminderCGI
  include ERB::Util

  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
    @bg = BGColor.new
  end

  def script_name(cgi)
    cgi.script_name
  end

  def make_param(hash)
    hash.collect do |k, v|
      u(k) + '=' + u(v)
    end.join(';')
  end

  def anchor(cgi, hash)
    %Q+<a href="#{script_name(cgi)}?#{make_param(hash)}">+
  end
  alias :a :anchor

  def a_delete(cgi, key)
    anchor(cgi, {'cmd'=>'delete', 'key'=>key})
  end

  def erb_src
    <<EOS
<table border="0" cellspacing="0">
<% @reminder.to_a.each do |k, v| %>
<tr <%= @bg %>>
 <td><%= k %></td>
 <td><%=h v %></td>
 <td>[<%=a_delete(cgi, k)%>X</a>]</td>
</tr>
<% end %>
<form action="<%=script_name(cgi)%>" method="post">
 <input type="hidden" name="cmd" value="add" />
 <tr <%= @bg %>>
  <td><input type="submit" value="add" /></td>
  <td><input type="text" name="item" value="" size="30" /></td>
  <td>&nbsp;</td>
 </tr>
</form>
</table>
EOS
  end

  def to_html(cgi)
    @erb.result(binding)
  end

  def kconv(str)
    NKF.nkf('-edXm0', str.to_s)
  end

  def do_add(cgi)
    item ,= cgi['item']
    return if item.nil? || item.empty?
    @reminder.add(kconv(item))
  end

  def do_delete(cgi)
    key ,= cgi['key']
    return if key.nil? || key.empty?
    @reminder.delete(key.to_i)
  end

  def do_request(cgi)
    cmd ,= cgi['cmd']
    case cmd.to_s
    when 'add'
      do_add(cgi)
    when 'delete'
      do_delete(cgi)
    end
  end
end

def main 
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')
  reminder = ReminderCGI.new(there)

  cgi = CGI.new('html3')
  reminder.do_request(cgi)

  cgi.out({"charset"=>"euc-jp"}) {
    cgi.html {
      cgi.head { 
        cgi.title { 'Reminder' }
      } + cgi.body { reminder.to_html(cgi) }
    }
  }
end

main

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2reminder0-del.jpg

図4.10 reminder0-cgi4.rbの実行結果

エラーページ

これまで作成してきたスクリプトには、エラー処理が入っていません。 例えば、Reminderサーバが停止していると Internal Server Error (Apacheの場合)などのエラーになるでしょう。

この節では、エラー処理の一手法を紹介します。 実験を進める前に、まずReminderサーバを停止させて下さい。 これで実行時にエラーがでるようになるはずです。

[ターミナル1]
>> exit

まず、エラーを捉える実験をしてみます。 mainメソッドを次のように変更してみましょう。

def main 
  ...
  begin
    content = reminder.to_html(cgi)
  rescue
    content = %Q+<p>CGI実行時エラーです。</p>+
  end

  cgi.out({"charset"=>"euc-jp"}) {
    cgi.html {
      cgi.head { 
        cgi.title { 'Reminder' }
      } + cgi.body { content }
    }
  }
end

reminder.to_htmlでエラーが発生した場合に、エラーを報告するようにします。 実行すると「CGI実行時エラーです。」が表示されるはずです。 試して下さい。 ERBをライブラリとして使用するアプローチでは、例外処理をeRubyスクリプトの 外に追い出せるためeRubyスクリプトをシンプルに保つことができます。

次に、汎用のデバッグ用のエラー表示ページを作成してみましょう。 Webサーバによってはエラーログに、どのようなエラーが発生したか記録される こともあります。 これから作成するエラー表示クラスは、デバッグが効率良く行えるように、 Rubyの実行時エラーと同様な表示をWebページに出力するクラスです。

List 4.10 クラスUnknownErrorPage

class UnknownErrorPage
  include ERB::Util

  def initialize(error=$!, info=$@)
    @erb = ERB.new(erb_src)
    @error = error
    @info = info
  end

  def erb_src
    <<EOS
<p><%=h @error%> - <%=h @error.class%></p>
<ul>
<% @info.each do |line| %>
<li><%=h line%></li>
<% end %>
</ul>
EOS
  end

  def to_html
    @erb.result(binding)
  end
end

UnknownErrorPageは、Rubyのrescueによって捉えられた例外情報($!、$@)を HTMLに変換するクラスです。

次のように使用します。 mainメソッドの後半を変更し、reminder.to_htmlで例外が発生した場合に UnknownErrorPageにページの内容を作成するようにします。

begin
  content = reminder.to_html(cgi)
rescue
  content = UnknownErrorPage.new($!, $@).to_html
end

UnknownErrorPageの追加と上の変更を加え実行してみましょう。 DRb::DRbConnErrorがどこで発生したか、といった情報が表示されるはずです。

開発中は詳しい例外情報を表示し、 実運用がはじまってからは簡単なものにするのも良いかもしれません。

CGIの利用者にとってDRb::DRbConnErrorの詳しい場所がレポートされるよりも、 Reminderサーバが停止していることがわかった方が親切でしょう。 Reminderサーバが停止しているかもしれない、ということをレポートするのは mainメソッドよりもReminderCGIクラスの方が適切です。

ReminderCGIのto_htmlメソッドを次のように変更し、 DRbConnErrorの場合にのみ異なるエラーを発生させるようにします。

def to_html(cgi)
  @erb.result(binding)
rescue DRb::DRbConnError
  %Q+<p>It seems that the Reminder server is downed.</p>+
end

ReminderCGI内部のDRbConnErrorでは「Reminderサーバが停止している」ことを レポートし、それ以外の場合には詳しい例外情報をレポートするようになりました。

最後に完全なスクリプトを示します。

List 4.11 reminder0-cgi5.rb

# reminder0-cgi5.rb
#!/usr/local/bin/ruby
require 'cgi'
require 'erb'
require 'drb/drb'
require 'nkf'

$KCODE='euc'

class BGColor
  def initialize
    @colors = ['#eeeeff', '#bbbbff']
    @count = -1
  end
  attr_accessor :colors

  def next_bgcolor
    @count += 1
    @count = 0 if @colors.size <= @count
    "bgcolor='#{@colors[@count]}'"
  end

  alias :to_s :next_bgcolor
end

class ReminderCGI
  include ERB::Util

  def initialize(reminder)
    @reminder = reminder
    @erb = ERB.new(erb_src)
    @bg = BGColor.new
  end

  def script_name(cgi)
    cgi.script_name
  end

  def make_param(hash)
    hash.collect do |k, v|
      u(k) + '=' + u(v)
    end.join(';')
  end

  def anchor(cgi, hash)
    %Q+<a href="#{script_name(cgi)}?#{make_param(hash)}">+
  end
  alias :a :anchor

  def a_delete(cgi, key)
    anchor(cgi, {'cmd'=>'delete', 'key'=>key})
  end

  def erb_src
    <<EOS
<table border="0" cellspacing="0">
<% @reminder.to_a.each do |k, v| %>
<tr <%= @bg %>>
 <td><%= k %></td>
 <td><%=h v %></td>
 <td>[<%=a_delete(cgi, k)%>X</a>]</td>
</tr>
<% end %>
<form action="<%=script_name(cgi)%>" method="post">
 <input type="hidden" name="cmd" value="add" />
 <tr <%= @bg %>>
  <td><input type="submit" value="add" /></td>
  <td><input type="text" name="item" value="" size="30" /></td>
  <td>&nbsp;</td>
 </tr>
</form>
</table>
EOS
  end

  def to_html(cgi)
    @erb.result(binding)
  rescue DRb::DRbConnError
    %Q+<p>It seems that the reminder server is downed.</p>+
  end

  def kconv(str)
    NKF.nkf('-edXm0', str.to_s)
  end

  def do_add(cgi)
    item ,= cgi['item']
    return if item.nil? || item.empty?
    @reminder.add(kconv(item))
  end

  def do_delete(cgi)
    key ,= cgi['key']
    return if key.nil? || key.empty?
    @reminder.delete(key.to_i)
  end

  def do_request(cgi)
    cmd ,= cgi['cmd']
    case cmd.to_s
    when 'add'
      do_add(cgi)
    when 'delete'
      do_delete(cgi)
    end
  end
end

class UnknownErrorPage
  include ERB::Util

  def initialize(error=$!, info=$@)
    @erb = ERB.new(erb_src)
    @error = error
    @info = info
  end

  def erb_src
    <<EOS
<p><%=h @error%> - <%=h @error.class%></p>
<ul>
<% @info.each do |line| %>
<li><%=h line%></li>
<% end %>
</ul>
EOS
  end

  def to_html
    @erb.result(binding)
  end
end

def main 
  DRb.start_service
  there = DRbObject.new_with_uri('druby://localhost:12345')
  reminder = ReminderCGI.new(there)

  cgi = CGI.new('html3')
  reminder.do_request(cgi)

  begin
    content = reminder.to_html(cgi)
  rescue
    content = UnknownErrorPage.new($!, $@).to_html
  end

  cgi.out({"charset"=>"euc-jp"}) {
    cgi.html {
      cgi.head { 
        cgi.title { 'Reminder' }
      } + cgi.body { content }
    }
  }
end

main

4.4 まとめ

この章ではeRubyとその実装ERBを紹介し、dRubyと組み合わせた 簡単なCGIを作成しました。 ERBとはどのようなものか、感じることができたでしょうか?


*1eRubyだけ版のサンプルスクリプトが要る?
*2デフォルトでは_erbout
*3安心できないかな?