PowerShell 5.1がBOMなしUTF-8の.ps1を読み違えるとき、消えるのは引用符と改行だった
タスクスケジューラから毎朝動かしているPowerShellスクリプトが、ある日いきなり構文エラーで止まった。前日に直したのは、日本語のメッセージを1行足しただけだ。エディタで開くと文字化けもなく、どこにも問題は見えない。原因は文字コードで、Windows PowerShell 5.1が、BOM(バイト順マーク)の付いていないUTF-8のファイルをUTF-8として読んでくれないことにあった。 同じことで二度つまずかないように、仕組みと対策をまとめておく。手元のWindows 11に入っているWindows PowerShell 5.1(5.1.26100)で症状を再現し、どの文字列で壊れ、どの文字列なら通るのかも確かめた。対策は3つある。ファイルにBOMを付ける、スクリプトをASCII文字だけで書く、PowerShell 7へ移る、の3つで、それぞれの手順と向き不向きを最後に表にしている。 何が起きているのか、5.1はBOMが無ければShift_JISで読む 文字コードを取り違えると、日本語は意味のない記号の列になる。これはUTF-8を西欧向けの文字コードで読んだ例Photo: My own work as derivative of text available under CC 3.0 attrib / CC BY 3.0 テキストファイルには、自分がどの文字コードで書かれているかを示す欄がない。読む側が推測するしかなく、その手がかりになるのがファイル先頭のBOMだ。UTF-8なら先頭3バイトがEF BB BF、UTF-16LEならFF FEになっていて、これがあればどの文字コードかは迷わずに決まる。 問題はBOMが無いときの扱いだ。Windows PowerShell 5.1は、BOMの無いスクリプトをシステムの既定の文字コード(ANSIコードページ)で読む。日本語版Windowsでは、これがコードページ932、いわゆるShift_JISになる。手元の環境で[Text.Encoding]::Defaultを確かめると、shift_jis、コードページ932と返ってきた。 一方、いまのエディタの多くは、新しく作ったファイルをBOMなしのUTF-8で保存する。Visual Studio Codeの既定もそうだし、GitやLinux由来の道具もBOMなしを前提にしている。英数字だけのスクリプトなら、UTF-8とShift_JISはどちらも同じバイトになるので何も起きない。日本語のコメントや文字列を1つ入れた瞬間に、書いた側はUTF-8、読む側はShift_JISというすれ違いが表に出てくる。エディタ上では正しく見えるのに実行すると壊れる、という分かりにくさは、ここから来ている。 閉じ引用符が消える仕組みを、バイト単位で追う 文字化けだけなら、表示がおかしくなるだけで済みそうに思える。実際には構文エラーになるのは、Shift_JISとして読み直す途中で、日本語の直後にある半角の記号まで巻き込まれることがあるからだ。 UTF-8の日本語は、ひらがな・カタカナ・漢字のほとんどが1文字3バイトになる。Shift_JISは日本語を2バイトで表し、1バイト目(先行バイト)に使う範囲が81〜9FとE0〜FCと決まっている。さらにA1〜DFは1バイトの半角カナとして読まれる。UTF-8の3バイトの並びを、この規則で頭から2バイトずつ、ときどき1バイトずつ切っていくと、文字の切れ目がずれていく。そして日本語の最後のバイトが「先行バイト」として余ったとき、その次にある半角の"が2バイト文字の後半として飲み込まれてしまう。 手元でいくつかの言葉を、UTF-8のバイト列のままコードページ932で文字列に戻し、直後に置いた"が残るかどうかを見た。 文字列 UTF-8のバイト数 末尾の数バイト 直後の"は ログを保存しました 27 E3 81 9F 残る 表示 6 E7 A4 BA 残る 次の行 9 E8 A1 8C 消える 完了 6 E4 BA 86 消える 「次の行」の場合、E6 ACで1文字、A1が半角カナで1文字、E3 81で1文字、AEが半角カナ、E8 A1で1文字と切れていき、最後の8Cが余る。8Cは先行バイトの範囲に入るので、次の22(つまり")と組にされ、どちらも正しい文字にならないまま「・」のような代わりの文字に化ける。文字列を閉じるはずの引用符がここで消えるので、PowerShellは文字列が最後まで閉じていないと判断する。 ...