erubyをERBに置き替える その1
FreeBSD 9.2ではerubyが動いていた
うちではFreeBSDでファイルサーバを作ってますが、同じマシンでいちおうApacheも動かしてて、自分用のちょっとした便利ページを、C実装のerubyを使ってCGIで動かしてます。ページの地の文をHTMLで書いておいて、途中に<% %>で囲まれたRubyスクリプトを埋め込んで、外部ファイルからリストを読み込んで一覧表示する、みたいな感じです。
例えば、「list.db」というテキストファイルに
りんご
みかん
バナナ
と書いておいて、
<html>
<body>
<p>りんご</p>
<p>みかん</p>
<p>バナナ</p>
</body>
</html>
みたいなHTMLをブラウザに表示させたいんで、下記のような「fruits.cgi」を作ってCGIとして実行させると、上のHTMLが得られる仕組みです。
#!/usr/local/bin/eruby -C UTF-8
<%
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
%><html>
<body>
% itemList.each do |item|
<p><%print item%></p>
% end
</body>
</html>
FreeBSD 10.0にしたらerubyが使えない
ところが、サーバのOSをFreeBSD 10.0-RELEASEに上げたら、「pkg search eruby」でerubyのパッケージを探しても出てきません。「portupgrade textproc/eruby」でPortsから入れようとしても、コンパイルはうまくいくのにインストールで止まってしまいます。
===> Installing for ruby19-eruby-1.0.5_2
===> Registering installation for ruby19-eruby-1.0.5_2
pkg-static: lstat(/usr/ports/textproc/eruby/work/stage/usr/local/lib/liberuby.so.10): No such file or directory
*** Error code 74
Stop.
make[1]: stopped in /usr/ports/textproc/eruby
*** Error code 1
Stop.
make: stopped in /usr/ports/textproc/eruby
** Command failed [exit code 1]: /usr/bin/script -qa /tmp/portinstall20140222-32626-1578e7h env make BATCH=yes reinstall
** Fix the installation problem and try again.
** Listing the failed packages (-:ignored / *:skipped / !:failed)
! textproc/eruby (install error)
このインストールエラーが、パッケージが作られていない原因かもしれません。ただ、PortsMonなどを見ても、現時点では特に問題は報告されておらず、詳しいことは追求しないことにします。
代わりにERBを使う
仕方がないので、C実装のerubyは諦めて、eRubyのRuby実装であるERBを使ってみることにしました。ERBは、Ruby 1.8以降に標準添付されているようです。
スクリプト片の書き方が違う
まずは、元のままのfruits.cgiを、コマンドラインからerbコマンドに解釈させてみます。
$ erb fruits.cgi
りんごみかんバナナ#!/usr/local/bin/eruby -C UTF-8
<html>
<body>
<p></p>
<p></p>
<p></p>
</body>
</html>
あれ、<p>の中身が変なところに出てしまっています。これは、erubyとERBで<%print xxx%>の解釈に違いがあるためのようです。erubyでは標準出力も地の文と同じところに出しますが、ERBでは標準出力と地の文が分離されてしまうため、地の文と同じ場所に出するには<%= %>を使う必要があります。
fruits.cgiの<%print item%>を<%=item%>に変えて、
#!/usr/local/bin/eruby -C UTF-8
<%
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
%><html>
<body>
% itemList.each do |item|
<p><%=item%></p>
% end
</body>
</html>
再実行してみました。今度は<p>の値はちゃんと出るようになりました。
$ erb fruits.cgi
#!/usr/local/bin/eruby -C UTF-8
<html>
<body>
<p>りんご</p>
<p>みかん</p>
<p>バナナ</p>
</body>
</html>
shebangが出力されてしまう
ただ、冒頭のshebang「#!/usr/local/bin/eruby -C UTF-8」まで地の文として解釈され、出力されています。HTMLの中にこんなのが埋め込まれてしまうとまずいので、なんとか消す必要があります。
erbコマンドに-xオプションを付けると、入力をRubyスクリプトに変換した結果が出力されます。
$ erb -x fruits.cgi
#coding:ASCII-8BIT
_erbout = ''; _erbout.concat "#!/usr/local/bin/eruby -C UTF-8\n"
;
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
_erbout.concat "<html>\n"
; _erbout.concat " <body>\n"
; itemList.each do |item|
_erbout.concat " <p>"; _erbout.concat((item).to_s); _erbout.concat "</p>\n"
; end
_erbout.concat " </body>\n"
; _erbout.concat "</html>\n"
; _erbout.force_encoding(__ENCODING__)
これを見ると、_erboutというのが出力を溜め込んでいく場所のようです。ということは、スクリプトの冒頭で_erboutを空にしてやればなんとかなるんじゃないか、ということで「_erbout = ““」を追加してみました。
#!/usr/local/bin/eruby -C UTF-8
<%
_erbout = ""
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
%><html>
<body>
% itemList.each do |item|
<p><%=item%></p>
% end
</body>
</html>
この状態で再実行。むりやりだけど、とりありずshebang行は出なくなりました。
$ erb fruits.cgi
<html>
<body>
<p>りんご</p>
<p>みかん</p>
<p>バナナ</p>
</body>
</html>
shebangにERBは直接指定できない
ここで、冒頭のshebangをerbコマンドのパスに置き換えたうえで、
#!/usr/local/bin/erb
<%
_erbout = ""
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
%><html>
<body>
% itemList.each do |item|
<p><%=item%></p>
% end
</body>
</html>
実行フラグを付けて単独実行できるか確認してみます。
$ ./fruits.cgi
./fruits.cgi: 行 2: %: そのようなファイルまたはディレクトリはありません
./fruits.cgi: 行 3: _erbout: コマンドが見つかりません
./fruits.cgi: 行 4: itemList: コマンドが見つかりません
./fruits.cgi: 行 5: 予期しないトークン `"list.db"' 周辺に構文エラーがあります
./fruits.cgi: 行 5: `File.foreach("list.db") do |line|'
あれ、エラーになってしまいました。よく考えると、「/usr/local/bin/erb」自体がRubyのスクリプトなので、shebangには指定できません(shebangには実行可能バイナリを指定する必要がある)。
そこで、冒頭を「#!/usr/local/bin/ruby /usr/local/bin/erb」に変えてみました。
#!/usr/local/bin/ruby /usr/local/bin/erb
<%
_erbout = ""
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
%><html>
<body>
% itemList.each do |item|
<p><%=item%></p>
% end
</body>
</html>
これで再実行すると、
$ ./fruits.cgi
<html>
<body>
<p>りんご</p>
<p>みかん</p>
<p>バナナ</p>
</body>
</html>
とりあえずうまくいったようです。
HTTPレスポンスヘッダが出ない
さて、このファイルをCGIとしてブラウザから起動してみると、Internal Server Errorになりました。Apacheのエラーログを見てみると、
[Sun Feb 23 11:17:05.434041 2014] [cgi:error] [pid 1078] [client 12.34.56.78:12345] malformed header from script 'fruits.cgi': Bad header: <html>
あ、HTTPのレスポンスヘッダが無いんですね。erubyは自分でヘッダを出してくれましたが、ERBでは自前で書く必要があるようです。というわけで、HTMLの前にContent-Typeヘッダと改行2つを追加します。
#!/usr/local/bin/ruby /usr/local/bin/erb
<%
_erbout = ""
itemList = []
File.foreach("list.db") do |line|
line.chomp!
itemList.push(line)
end
%>Content-Type: text/html; charset=utf-8
<html>
<body>
% itemList.each do |item|
<p><%=item%></p>
% end
</body>
</html>
これでようやくブラウザに表示されるようになりました。
エンコーディングのエラー
ちなみに、このサンプルは単純化した例なので、UTF-8のlist.dbをそのままバイト列(ASCII-8BIT)として読み込み、<%=item%>でそのまま出力しているため問題になっていませんが、ファイルから読み取った文字列を操作をするときは、きちんとエンコーディングを考慮しないとエラーや文字化けの原因になります。例えば、各データの最初の1文字(このサンプルで言うと「り」「み」「バ」)だけ表示する場合、ファイルのオープン時に外部エンコーディングを指定し、書く文字列オブジェクトにエンコーディングを設定してからでないと、きちんとした部分文字列の取り出しはできません。下記はそれを考慮してみた例です(Rubyにあまり詳しくないので、これが最適なのかは分かりません)。
#!/usr/local/bin/ruby /usr/local/bin/erb
<%
_erbout = ""
itemList = []
File.open("list.db", "r:utf-8") do |file|
file.each() do |line|
line.chomp!
line = line[0, 1]
itemList.push(line)
end
end
%>Content-Type: text/html; charset=utf-8
<html>
<body>
% itemList.each do |item|
<p><%=item%></p>
% end
</body>
</html>
とりあえず動いたが……
以上の方法で、とりありずなんとか動くようにはなりましたが、_erboutを無理やり初期化するなんてのは、どう考えても正しい方法とは思えないので、本当はもっとマシな方法があるはずです。また、ApacheでRubyならmod_rubyだろう、とか、RubyでウェブならRailsだべ、とか、そもそもRuby 1.9なんて古すぎ、とかいろいろ突っ込みどころはあるんだろうと思いますが、まあそのへんは追々と……。
※バージョンメモ
- FreeBSD 10.0-RELEASE-p0 amd64
- ruby-1.9.3.484_1,1
- apache24-2.4.6_1
※更新履歴
- 2014-04-19 最後の例から「line.encode(Encoding::UTF_8)」の行を削除。