日本の Windows ユーザーはローカルで使う Rails テンプレートにマジックコメントを付けるべきと悟った記録
予備知識
知ってる人は飛ばしてくれ。
Rails テンプレート
rails new hoge
したあとに毎回同じような Gemfile 修正とかファイル追加とかやるの面倒なので,そういった一連の作業をテンプレートファイルに記述しておくことができる。
ファイルの中身は Ruby スクリプト。
テンプレートファイルはファイルシステムに置いてもいいし,ウェブサーバーに載せてもいい。
使うときは -m
オプションで
$ rails new hoge -m path/to/template.rb
とか
$ rails new hoge -m https://localhost/rails/template
のように指定する。
マジックコメント
Ruby スクリプトの第一行に
# encoding: utf-8
のようなコメントの形で書き,そのファイルが何の文字コードで書かれているかを指定するもの。「マジコメ」と略す。
Ruby 1.9 で導入されたが,Ruby 2.0 でデフォルトが UTF-8 になったので,スクリプトが UTF-8 で書かれているなら不要になった。
ことの経緯
鼻歌を歌いながら Rails テンプレートを書いた。
文字列リテラルに日本語が入ってるけど気にしなかった。
Ruby 2.1 なので当然のごとくマジコメは付けなかった。つか,既にマジコメの存在自体を忘れて久しい。
よしっ,テンプレートのテストだ。
$ rails new hoge -m path/to/template.rb
(略)invalid multibyte char (Windows-31J) (SyntaxError)(略)
し,しんたっくすえらあ? ぁあん?
なんか,UTF-8 で書いたテンプレートがなぜか Windows-31J(CP932)とみなされちゃったみたいだな。
えーっと,マジコメは要らないはずだけど,試しに付けてみるか。
$ rails new hoge -m path/to/template.rb
通った! 正常に完了した! なんでやねん? マジコメ要るのかよ?
マジコレ? (ハイ,笑うとこですョ)
はっ,そうだ! さっきウトウトしてたときに時空の裂け目から Ruby 1.9 の世界に迷い込んでしまったのかも?!
$ ruby -v
ruby 2.1.7p400 (2015-08-18 revision 51632) [i386-mingw32]
そ,そういうわけでもないようだな。
原因
原因はわりとすぐに分かった。
例外を吐いたのは thor というライブラリーの ここ:
def apply(path, config = {})
verbose = config.fetch(:verbose, true)
is_uri = path =~ %r{^https?\://}
path = find_in_source_paths(path) unless is_uri
say_status :apply, path, verbose
shell.padding += 1 if verbose
if is_uri
contents = open(path, "Accept" => "application/x-thor-template") { |io| io.read }
else
contents = open(path) { |io| io.read }
end
instance_eval(contents, path)
shell.padding -= 1 if verbose
end
Rails テンプレートを IO#read
で読み込んで contents
に入れ,それを instance_eval
してた。
読み込むときに encoding
を指定してない。
Windows の日本語環境の場合,ロケール情報を元に Encoding.default_external
が Encoding::Windows_31J
にされるので,encoding
を指定しないで IO#read
すると,Windows 31J で読み込まれるのだ。
それを instance_eval
しようとするからテンプレートファイルの中身によっては例外が発生することになる。
やれやれ。あちらで作られた gem はこの手の話が多いな。
もうちょっと調べてみた
ちょっと気になったのが,マジコメを付けたらどうして問題が無くなったのか,ということ。
マジコメを付けようが付けまいが,今の場合,contents
に代入される文字列は Windows 31J である。これは間違いない。
ということは,instance_eval
がマジコメを見てるってコト?
マジコレ? (もうええっちゅうに)
早速実験だ。instance_eval
でも eval
でも同じだろうから,次のようなことをやってみる。ファイルはすべて UTF-8 で書く。
まずはマジコメ無しで:
script = IO.read("bar.rb")
p script.encoding
eval script
puts "あ"
$ ruby foo.rb
#<Encoding:Windows-31J>
foo.rb:3:in `eval': (eval):3: invalid multibyte char (Windows-31J) (SyntaxError)
(eval):3: unterminated string meets end of file
from foo.rb:3:in `<main>'
まあ,そうなるだろうな。
次は bar.rb にマジコメを付けてみる:
# encoding: utf-8
puts "あ"
$ ruby foo.rb
#<Encoding:Windows-31J>
あ
おおー,やはり eval
は文字列がマジコメで始まっていたら,その文字コードで解釈するのだ。
勘が当たった。がははは。と威張る。 (ハイ,笑うとこですョ)
いやー,知らなかった。やっぱりリファレンスマニュアルはちゃんと読まなきゃな。
か,書いてないよ。 orz
最後に
- Ruby スクリプトは
require
で読み込まれるとは限らない。IO#read
してeval
されることもある。 -
require
ならマジでマジコメ要らないのだと思う。 - Linux や Mac OS X の人には関係の無い話だと思う。
-
default_external
が UTF-8 になるような言語圏の人には関係の無い話だと思う。 - テンプレートをウェブサーバーに置いた場合はたぶん
Content-Type
で文字コードが決まるので,サーバーの設定が正しくできていれば関係の無い話だと思う。
追記:環境変数による解決(2020-01-07)
コメント欄で @topstone さんが環境変数による解決法を書いてくださったので,記事に追記します。
私は試していませんが,環境変数 RUBYOPT
に -EUTF-8
を含めておけばよいのだそうです。
-E
は ruby
コマンドに与えるオプションで,デフォルトの外部エンコーディング,内部エンコーディングを変更するものですね。
-EUTF-8
とすると,デフォルトの外部エンコーディングが UTF-8
になります。
環境変数 RUBYOPT
の値は,ruby
コマンドにオプションとして与えたのと同じように Ruby 処理系に渡されるので,この環境変数にセットしておけばいいわけですね。