Rails 2.3.2と$KCODE

先日のエントリで発生した文字コードのエラーだが、Rails::Initializerの初期化で発生しているらしく、Rakeタスクの実行だけでも再現することが解った。

E:\www\master>rake db:schema:dump
(in E:/www/master)
rake aborted!
E:/www/master/config/initializers/will_paginate.rb:2: Invalid char `\216' in expression
E:/www/master/config/initializers/will_paginate.rb:2: Invalid char `\237' in expression
E:/www/master/config/initializers/will_paginate.rb:2: Invalid char `\202' in expression
E:/www/master/config/initializers/will_paginate.rb:2: syntax error, unexpected tIDENTIFIER, expecting $end

こうなる可能性は3つ程ある

1. UTF-8N(BOM付き)のファイルを処理している(Rubyが処理するUNICODEはBOM無しを前提にしている)
2. 想定している改行コードで行が終了していない
3. Rubyインタプリタが想定していない文字セット(文字エンコーディング)を処理した

このエラーになっているスクリプトはRubyMineのコンテキストから新規に作成されたRubyスクリプトだが、文字セットはShift_JIS(Windows-31J)、改行コードはCR+LF(\r\n)であった。これ自体は日本語用Windows上でプログラムを編集する際の規定値であり、Javaの時も.NETの時もこれで全く問題が無かった訳だが、Rubyでは何故駄目なのだろう。

一つずつ検証していこう。
スクリプトはShift_JIS(Windows-31J)でファイルが保存されているので、割とありがちな1.は消える。次は改行コードをRailsが生成するコードと同じLF(\n)に変更して保存してみたが、結果は変わらなかった。ならば3.ということになるが、今回の場合Shift_JISを処理させるためにわざわざenvironment.rbの先頭に$KCODE='S'のセットを挿入している(ruby1.8.7)ので、Shift_JIS文字の処理に問題は無いと思うのだが、問題の箇所はenvironment.rbよりも前に実行されるようなので、わざわざrubyインタプリタ起動時の文字セットも指定するようにしてみた。

ruby -Ks 〜

しかし、これでも結果は変わらない。
変だなと思って調べて見ると、/config/initializers/will_paginateの読込前に、ランタイムでSJISにセットしたはずの$KCODEがなんらかによってUTF8に上書きされているのだ。

なんじゃこりゃ。
絶対にどこかで、それもRailsの中で$KCODEセットしているだろうとgrepした所、それは簡単に引っかかった。

  • $RAILS_HOME\vendor\rails\railties\lib\initializer.rb
398〜
    # For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the
    # multibyte safe operations. Plugin authors supporting other encodings
    # should override this behaviour and set the relevant +default_charset+
    # on ActionController::Base.
    #
    # For Ruby 1.9, this does nothing. Specify the default encoding in the Ruby
    # shebang line if you don't want UTF-8.
    def initialize_encoding
      $KCODE='u' if RUBY_VERSION < '1.9'
    end

なるほど。Ruby 1.8以前との組みあわせでは$KCODEに問答無用で'u'(UTF8)をセットするのか。 まあ、こうしないとマルチバイト文字には対応できない訳だが、これってどうなの。
initializer.rbはenvironment.rbよりも後、アプリケーションよりも前で実行されるので、避けようが無い。

案としては

    • Shift_JISを諦めて、UTF-8で統一する
    • initializer.rbよりも後方で$KCODEを改めてセットする

このどちらだろうか。理想的なのはUTF-8で統一することだが、そうするとsjisなデータベースを扱う際には必ずエンコード変換を実施する必要があり面倒だ。
次善策として、必要な箇所で$KCODEをセットする方法だが、どこが良いだろう。

モンキーパッチを作成することも考えられたが、そもそも問題になっているスクリプトはRails::Initializerによって、$RAILS_HOME\config\initializers\に配置することによってRailsの初期化コードとして動作するルールなので、今回は同様のルールで解決することにした。

具体的には、以下のような$KCODEを設定するだけのスクリプトを$RAILS_HOME\config\initializers\に配置し、他の初期化用のスクリプトに先んじて動作するようにしておけばOKだ。

  • _initialize_encoding.rb
#encoding: shift_jis
$KCODE='s'

なお、このときのスクリプトを記述したファイル名は重要だ。なぜならばRails::Initializerが実行する初期化用のスクリプトは、以下のコードで収集、ソートしてからロード(実行)されているからだ。

def load_application_initializers
  if gems_dependencies_loaded
    Dir["#{configuration.root_path}/config/initializers/**/*.rb"].sort.each do |initializer|
      load(initializer)
    end
  end
end

この設定をしておけば、environment.rbの修正は不要なのかもしれないな。

これでサーバを起動。見事に日本語のページネーション・ラベルが表示できた。長かったな。

Ruby 1.8は今回のような問題から逃れられない。(この辺、Java2以前と状況が似ているかもしれない)
Javaや.NETのように文字列エンコーディングの概念が組み込まれたRuby1.9に、早めに移行する必要があるなと思った。