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)」の行を削除。