Rails テンプレートにはマジコメ必要

日本の Windows ユーザーはローカルで使う Rails テンプレートにマジックコメントを付けるべきと悟った記録

# 予備知識

知ってる人は飛ばしてくれ。

## Rails テンプレート

`rails new hoge` したあとに毎回同じような Gemfile 修正とかファイル追加とかやるの面倒なので，そういった一連の作業をテンプレートファイルに記述しておくことができる。

ファイルの中身は Ruby スクリプト。

テンプレートファイルはファイルシステムに置いてもいいし，ウェブサーバーに載せてもいい。
使うときは `-m` オプションで

```bash
$ rails new hoge -m path/to/template.rb
```

とか

```bash
$ rails new hoge -m https://localhost/rails/template
```

のように指定する。

## マジックコメント

Ruby スクリプトの第一行に

```rb
# encoding: utf-8
```

のようなコメントの形で書き，そのファイルが何の文字コードで書かれているかを指定するもの。「マジコメ」と略す。

Ruby 1.9 で導入されたが，Ruby 2.0 でデフォルトが UTF-8 になったので，スクリプトが UTF-8 で書かれているなら不要になった。

# ことの経緯

鼻歌を歌いながら Rails テンプレートを書いた。

文字列リテラルに日本語が入ってるけど気にしなかった。

Ruby 2.1 なので当然のごとくマジコメは付けなかった。つか，既にマジコメの存在自体を忘れて久しい。

よしっ，テンプレートのテストだ。

```bash
$ rails new hoge -m path/to/template.rb
（略）invalid multibyte char (Windows-31J) (SyntaxError)（略）
```

し，しんたっくすえらあ？　ぁあん？
なんか，UTF-8 で書いたテンプレートがなぜか Windows-31J（CP932）とみなされちゃったみたいだな。

えーっと，マジコメは要らないはずだけど，試しに付けてみるか。

```bash
$ rails new hoge -m path/to/template.rb
```

通った！　正常に完了した！　なんでやねん？　マジコメ要るのかよ？

マジコレ？　（ハイ，笑うとこですョ）

はっ，そうだ！　さっきウトウトしてたときに時空の裂け目から Ruby 1.9 の世界に迷い込んでしまったのかも？！

```bash
$ ruby -v
ruby 2.1.7p400 (2015-08-18 revision 51632) [i386-mingw32]
```

そ，そういうわけでもないようだな。

# 原因

原因はわりとすぐに分かった。

例外を吐いたのは thor というライブラリーの [ここ](https://github.com/erikhuda/thor/blob/master/lib/thor/actions.rb#L223)：

```rb
    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 で書く。

まずはマジコメ無しで：

```rb:foo.rb
script = IO.read("bar.rb")
p script.encoding
eval script
```

```rb:bar.rb
puts "あ"
```

```bash
$ 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 にマジコメを付けてみる：

```rb:bar.rb
# encoding: utf-8

puts "あ"
```

```bash
$ ruby foo.rb
#<Encoding:Windows-31J>
あ
```

おおー，やはり `eval` は文字列がマジコメで始まっていたら，その文字コードで解釈するのだ。

勘が当たった。がははは。と威張る。　（ハイ，笑うとこですョ）

いやー，知らなかった。やっぱりリファレンスマニュアルはちゃんと読まなきゃな。

http://docs.ruby-lang.org/ja/2.3.0/method/Kernel/m/eval.html

か，書いてないよ。　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 処理系に渡されるので，この環境変数にセットしておけばいいわけですね。