このエントリーをはてなブックマークに追加
はてなブックマーク - [ruby] いろいろ難点のあるERBよりもBabyErubisを使ってみよう、という話
Bookmark this on Digg

(これは Ruby Advent Calendar 2014 参加エントリです。)

BabyErubis ver 2.0.0 をリリースしました。 BabyErubis は、2007年 RubyKaigi での発表で使ったサンプルコードをもとにした、とてもコンパクトな eRuby 処理系です。

次のような特徴があります。

  • 小さい (全体で約230行、コア部分だけなら100行未満)
  • 速い (Erubisと同じ速度)
  • 安全 (デフォルトでエスケープを行う)
  • きれい (出力から余分な空行を除いてくれる)
  • Ruby on Rails のテンプレートに対応 (since ver 2.0.0)

詳しくは以前の記事『Erubisの弟分「BabyErubis」1.0.0 リリース』を読んでください。

さて、Ruby ではすでに標準で ERB がついてきます。 なのになぜ新しい eRuby 処理系を作ったのでしょうか? それは、ERB にはいくつか難点があるからです。

ここではすでに ERB を知っている人を対象として、ERB にどんなまずいがあるのか、そして BabyErubis ではどう解決しているのかを説明します。

なお ERB の使い方は、「ERB 使い方」でぐぐって最初に出てきたページを参考にしています。

その 1:ローカル変数が意図せず変更されることがある

ERB では、テンプレートをレンダリングすると、ローカル変数が意図せず変更されてしまうことがあります。

## ローカル変数 i は、最初は 100 である
n = 3
i = 100

## しかし ERB を使うと…
require 'erb'
erb = ERB.new <<'END'
<% for i in (1..n) %>
i = <%= i %>
<% end %>
END
print erb.result(binding)

## なんと 3 になってる!
puts i    #=> 3

これは、ERB が Kernel#binding() を使っているためです。 本来、テンプレートに渡した変数だけが使われるべきですが、ERB では Kernel#binding() のせいで、渡すつもりのない変数まで渡されたのと同じ状態になります。

BabyErubis なら、テンプレートに渡した変数 (データ) だけが使われるため、ほかのローカル変数が勝手に変更されることはありません。

## ローカル変数 i は、最初は 100 である
n = 3
i = 100

## BabyErubis なら、テンプレートをレンダリングしても
require 'baby_erubis'
erb = BabyErubis::Html.new.from_str <<'END'
<% for i in (1..@n) %>
i = <%= i %>
<% end %>
END
print erb.render({:n=>n})

## i は 100 のまま (渡していない変数は変更されない)
puts i    #=> 100

なお render() の引数には、Hash ではないオブジェクトを渡せます。 その場合は、そのオブジェクトのインスタンス変数やインスタンスメソッドに、テンプレートからアクセスできます。

require 'baby_erubis'

## たとえばこのようなクラスがあったとして、
class Hello
  include BabyErubis::HtmlEscaper  # necessary to define escape()
  def initialize(name)
    @name = name
  end
  def greeting
    "Hello #{@name}!"
  end
end

## そのインスタンスオブジェクトを作って、
obj = Hello.new("world")

## render() に渡すと、そのインスタンス変数やインスタンスメソッドに
## テンプレートからアクセスできる
erb = BabyErubis::Html.new.from_str <<'END'
## Instance variables
@name = '<%= @name %>'

## Instande methods
self.greeting = '<%= self.greeting %>'
END
print erb.render(obj)

その 2:エラーがあったときに間違った行番号が表示される

規模の小さなスクリプトの場合、テンプレートを別ファイルにせず、スクリプト中に埋め込むことがよくあります。

そのような場合、テンプレート中でエラーが発生すると、ERB では正確な行番号がわかりません。 なぜなら、ERB ではファイル名の指定はできても行番号の指定ができないからです。

## ERBでは行番号が指定できない → エラー時に正確な行番号がわからない
require 'erb'
erb = ERB.new <<'END'
<p>
name is <%= name %>   # ここで NameError
</p>
END
erb.filename = __FILE__   ## ファイル名を指定
print erb.result

これの実行結果は次のようになります。 エラー箇所が example1.rb:1 であると最初の行で表示されていますが、本当ならこれは example1.rb:5 と表示されて欲しいところです。

exmample.rb:1:in `<main>': undefined local variable or method `name' for main:Object (NameError)
    from /opt/vs/ruby/2.1.3/lib/ruby/2.1.0/erb.rb:850:in `eval'
    from /opt/vs/ruby/2.1.3/lib/ruby/2.1.0/erb.rb:850:in `result'
    from example.rb:9:in `<main>'

BabyErubis なら、ファイル名だけでなく行番号も指定できます。 そのため、エラー時に正確な行番号がわかるため、とてもデバッグしやすいです。

## BabyErubisではファイル名も行番号も指定できる
require 'baby_erubis'
erb = BabyErubis::Html.new.from_str <<'END', __FILE__, __LINE__+1
<p>
name is <%= name %>   # ここで NameError
</p>
END
print erb.render({})

これがエラーの例です。 最初の行で example.rb:5 と、正確な行番号つきで表示されていることがわかります。

example.rb:5:in `block in empty_binding': undefined local variable or method `name' for #<BabyErubis::HtmlTemplateContext:0x007f82aa920bb0> (NameError)
        from /opt/vs/ruby/2.1.3/lib/ruby/gems/baby_erubis-2.0.0/lib/baby_erubis.rb:102:in `instance_eval'
        from /opt/vs/ruby/2.1.3/lib/ruby/gems/baby_erubis-2.0.0/lib/baby_erubis.rb:102:in `render'
        from example.rb:8:in `<main>'

なおテンプレートを別ファイルにしていれば、ERB でも行番号が問題になることはありません。

その 3:Rails のテンプレートがコンパイルできない

Ruby on Rails では、埋め込み式の中にブロック引数を使う場合があります。 いちばん有名なのは、form_form() でしょう。

<%= form_for :article do |f| %>
  ...
<% end %>

このテンプレートを ERB でコンパイルすると、次のようになります。

_erbout = ''; _erbout.concat(( form_for(:article) do |f| ).to_s); _erbout.concat "\n"
; _erbout.concat "  ...\n"
;  end ; _erbout.concat "\n"

ここで _erbout.concat(( form_for :article do |f| ).to_s) が Ruby の文法としては正しくありません。 そのため、ERB では Rails 用のテンプレートファイルをコンパイルできません (正確に言うと、コンパイルはできるけど使い物にならない)。 これは Rails が eRuby の仕様を逸脱しているのが悪いのであって、ERB のせいというわけではありませんが、不便であるのは確かです。

これに対し、BabyErubis では ver 2.0.0 から Rails 専用のモードを用意し、ブロック引数にも対応しました。 たとえば先のテンプレートも問題なくコンパイルできます。

require 'baby_erubis'
require 'baby_erubis/rails'

erb = BabyErubis::RailsTemplate.new.from_str <<'END'
<%= form_for :article do |f| %>
  ...
<% end %>
END
print erb.src

実行結果:

@output_buffer = output_buffer || ActionView::OutputBuffer.new;@output_buffer.append= form_for :article do |f| ;@output_buffer.safe_append='
  ...
'.freeze; end;
@output_buffer.to_s

とはいえ、コンパイル結果を見て分かるように、これは Rails べったりのコードを生成します。 そのため、BabyErubis でコンパイルはできても (Rails なしだと) 実行はできないことに注意してください。

なお実行はできなくてもコンパイルはできるため、テンプレートの文法エラーを見つける場合は多いに役立ちます。

## Rails テンプレートをコンパイルし、Ruby の文法エラーがないか調べる
$ baby_erubis -xR example.html.erb | ruby -wc

## <% %> と <%= %> で埋め込まれたコードだけを抜き出し、行番号つきで表示
$ baby_erubis -XRUN example.html.erb | less

余談:eRuby におけるブロック引数のサポートについて

最近、こういう資料を見かけました。

この資料の 108 ページ目で、以下のようなことが述べられています。

  • Rails ではブロック引数の有無を正規表現で判定しているが、そこにバグがある
    • たとえば <%= @todo %> がブロック引数つきだと誤判定されてしまう (らしい。本当?)
  • そもそも Ruby の文法は複雑であり、正規表現で判定しようとするのが間違い
  • だから eRuby やめて Haml や Slim を使いましょう

自分の意見としては、これは誤解ではないかと思います。

  • ブロック引数の有無を判定する正規表現にバグがあるなら、直せばよい
    • この場合なら \b を使って /\s*(\bdo|\{)(\s*\|[^|]*\|)?\s*\Z/ にするだけ
  • eRuby では Ruby の文法を正確にパースする必要はない。ブロック引数の有無を判定する程度なら、正規表現で十分
  • eRuby でのブロック引数サポートについては、判定用の正規表現ではなく、他の部分に問題がある (が、Rails はウルトラ C の方法で解決してた!)

おそらく、あの資料では Haml や Slim への乗り換えを勧めるためにああいう説明になったのではないかと推測しますが、ちょっと無理があるかなーと思いました。

まとめ

あまり知られてない、ERB の難点を 3 つ説明しました。 3 番目はしょうがないにしても、1 番目と 2 番目は多くの人が困るのではないでしょうか。

  • その 1:ローカル変数が意図せず変更されることがある
  • その 2:エラーがあったときに間違った行番号が表示される
  • その 3:Railsのテンプレートがコンパイルできない

そして、BabyErubis ではこれらが解消されていることを説明しました。 今まで「標準添付だから」という理由だけで ERB を使っていた人も、ぜひ BabyErubis を試してみてください。

なお BabyErubis のソースコードを読むなら、ver 1.0 のほうがわかりやすいです。 まず ver 1.0 のソースコードを理解してから、ver 2.0 のほうを読むといいでしょう。

あわせて読みたい: