Ruby で UTF-8 なのにマジックコメントが必要なケース

# 何の話？

Ruby のスクリプトは UTF-8 のほか CP932[^cp932] など複数の文字コードで書くことができる。
スクリプトを記述している文字コードを「スクリプトエンコーディング」[^encoding]という。

[^cp932]: CP932（Codepage 932）は Windows 31J ともいう。Shift JIS（シフト符号化方式の JIS 系文字コード）の一種であり，JIS コード（JIS X 0208）の文字集合に漢字・記号などを追加したもの。

[^encoding]: この記事では「文字コード」と「エンコーディング」は同じと思ってもらって大丈夫。

Ruby はバージョン 1.9 以降，マジックコメント（俗な略称：マジコメ）というものによってスクリプトエンコーディングをあらわに表現できるようになった。

具体的にはスクリプトの第一行に

```rb
# encoding: UTF-8
```

とか

```rb
# encoding: CP932
```

などと書く。

しかし，スクリプトエンコーディングは UTF-8 であることが多いので，Ruby 2.0 以降，デフォルトが UTF-8 になった。
つまり，マジックコメントを略した場合はスクリプトエンコーディングが UTF-8 であると解釈されることになった。
これにより，UTF-8 で書く限りはマジックコメントは不要になったはずである。
ところがそうとも言い切れないよ，というのが本記事の主旨。

## 追記（2022-08-07）　その他のマジコメ

確か本記事執筆時点ではマジコメはスクリプトエンコーディングを示すものしか無かったと思う。その後追加され，現時点では以下の 3 種となっている。

* encoding（スクリプトエンコーディングを示す）
* frozen_string_literal（文字列リテラルを凍結するかどうかを指定する）
* warn_indent（インデント不整合の警告を出すかどうかを指定する）

本記事でいう「マジックコメント」は encoding 限定である。

# 開発環境が引き起こす問題

イマドキのテキストエディターや統合開発環境（IDE）では，書いているスクリプトを色分け表示してくれたり，構文上おかしいところなどに下線を引いてエラーメッセージや警告を表示してくれたりする。

ところが，マジックコメント無しだと，正しく書いているのに間違っているとツッコミを入れてくる場合がある。

私が見つけたのは以下のケース。
いずれも Atom 最新版（1.37.0）に linter-ruby パッケージ最新版（1.3.1）を入れてチェックをかけた。（linter-ruby パッケージを使うためには linter パッケージも必要）
スクリプトはすべて UTF-8 で書き，マジックコメントは入れない。

実験は Windows と macOS で行った。
前者 は RubyInstaller for Windows で，後者は rbenv でインストールした，いずれも Ruby 2.6.3 を使用。


## ヒアドキュメント

再現コードは以下のとおり。

```rb
p <<EOT
あ
EOT
```

（べつに `p` は要らないんだけど，入れないと「リテラル使ってないやん」とツッコミが入る）
第 1 行について

> unexpected end-of-input, expecting tSTRING_CONTENT or tSTRING_DBEG or tSTRING_DVAR or tSTRING_END

というエラーが出る。
どうやらヒアドキュメント中に非 ASCII 文字があるとダメなようだ。
式展開をしない`'EOT'` のタイプのヒアドキュメントだとエラーは出ない。

Windows だけかと思ったら macOS でも同じだった。

マジックコメントを付けるとエラーは出なくなる。

## 正規表現1

再現コードは以下のとおり。

```rb
p(/[あ]/)
```

これは Windows と macOS とで違うエラーが出た。

エラーメッセージをコピペし忘れたが，Windows の場合，「`]` が無いやんけ」というエラーだったと思う。

これはおそらく以下のような原因だ。

まず　`あ` は UTF-8 のバイト列として `E3 81 82` である。
Windows ではこれを CP932 として解釈しようとしたようだ。
まず，`E3 81` は CP932 で `縺` という文字になる。
そして次の `82` と `]`（`5D`）とで一つの文字となる。
（0x825D は CP932 の文字として定義されていないがコードポイントとしては存在する）
その結果，正規表現の閉じの `]` が無いことになる。

一方，macOS のほうでは，エラーとして `/[]/` と表示された。
んあ？　よく分からない。文字クラスの中身が空っぽ？　もしかしたら ASCII-8BIT として解釈しようとしたのかもしれないが，確かな証拠はない。

Windows でも macOS でも，マジックコメントを付けるとエラーは出なくなる。

## 正規表現2

再現コードは以下のとおり。

```rb
p(/[あかさ]/)
```

Windows でも macOS でも，前の節の例と同じエラーが出る。

Windows の場合，それに加えて

> character class has duplicated range: /[あかさ]/

という警告も出る。

これは以下のような原因であるらしい。

まず，linter-ruby パッケージは，`/[abca]/` のような重複のある文字クラスについて，上記のような警告を出す。
`あかさ` という UTF-8 文字列（3 バイト × 3 文字）は以下のようなバイト列である。

```
E3 81 82 E3 81 8B E3 81 95
```

これを CP932 として解釈すると，2 バイト × 3 文字 + 1 バイト × 1 文字となるが，この中に 0xE381 の文字が 2 回出てくるのだ。

## VSCode ではどうか

Visual Studio Code ではどうなのか。
Windows で実験してみたが，linter-ruby を使ったのに同じ現象は起きないようだった。
ただ，VSCode は今日初めて使ったので，使い方というか設定の仕方が正しいかどうかよく分からない。

追試頼む。

# 実行に支障が起きるケース

ここまでに述べたのは，開発環境でのスクリプトのチェックでエラーや警告が出る話だった。
いずれもストレスフルだが，スクリプトの実行自体には差し支えなかった。

しかし，実行自体に問題が起きる場合もある。見つけた例は以下の二つ。

## Rails テンプレート

これについてはだいぶ前に書いた記事があるので，そちらを参照してほしい。
[Rails テンプレートにはマジコメ必要 - Qiita](https://qiita.com/scivola/items/95f257510ac82542dbf5)

直接の題材は Ruby on Rails であり，しかも「Rails テンプレート」というマイナーなものに関する報告だった。
しかし，原理的には Rails と無関係に起きうる問題である。

詳細はリンク先を見ていただくとして，手短に書くと，スクリプトを `require` するのではなく文字列として読み込んで `eval` する場合に起こりうるという話。

## Thor（2022-08-07 追記）

Thor は Rake と同じ目的で使うもの。
`.thor` という拡張子のファイルにタスクを定義しておき，[thor](https://rubygems.org/gems/thor) gem がインストールされた環境で `thor` コマンドに続けてタスク名を書くと，そのタスクを実行してくれる。

例えば，

```rb:tasks.thor
class Greeting < Thor
  desc "morning", "Morning greetings."
  def morning
    puts "Good morning!"
  end
end
```

というファイルを用意し，

```terminal
thor greeting:morning
```

とすれば，

```
Good morning!
```

と表示される。

この thor ファイルだが，どうやらマジコメを書かないとスクリプトエンコーディングが `ASCII-8BIT` になってしまうようだ。

以下の thor ファイルを用意しよう。もちろんこのファイルは UTF-8 で記述する。

```rb:test.thor
class Test < Thor
  desc "encoding", "Test about encoding"
  def encoding
    str = "日本語"
    puts "Encoding: #{ str.encoding }"
    puts "Length: #{ str.length }"
  end
end
```

そして，

```terminal 
thor test:encoding
```

とすると

```
Encoding: ASCII-8BIT
Length: 9
```

が表示される。
`"日本語"` が 9 文字になってしまった。ASCII-8BIT というのは要するにバイト列なので，9 bytes の文字列の長さは「9」なのだ。

thor ファイルの先頭に

```rb
# encoding: UTF-8
```

を追加すると，実行結果は

```
Encoding: UTF-8
Length: 3
```

となる。
thor ファイルにもマジコメが必要だったのだ。UTF-8 のはずの文字列が ASCII-8BIT だった，というのは非常に分かりにくいバグの原因になるので，注意したい。

なお，バージョンによるかもしれないので，実験に使った環境を掲げておく：

* ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin20]
* Thor 1.2.1