できない.dev

PowerShell で出力したファイルが文字化けする(UTF-8 で保存できない)

Windows PowerShell 5.1 は cmdlet ごとに既定エンコーディングが違う。Out-File> は UTF-16LE、Set-Content は ANSI になるため、-Encoding の明示か $PSDefaultParameterValues での既定変更が必要である。

公開:

要約

PowerShell で書き出した JSON や CSV を他のツールに渡すと文字化けする、あるいは先頭に見えない文字が混じる。
原因は Windows PowerShell 5.1 の既定エンコーディングが cmdlet ごとに違うことである。

"あいうえお" | Out-File data.txt
Get-Content data.txt -Encoding Byte -TotalCount 4
255
254
66
48

先頭の 255 254 は UTF-16LE の BOM である。
UTF-8 のつもりで書いたテキストが実際には UTF-16LE になっている。

公式ドキュメント(新しいタブで開く)が明記しているとおり、5.1 では Out-File> >> が UTF-16LE、Set-ContentAdd-Content が ANSI という別々の既定を持つ。
エンコーディングを常に明示するのが唯一確実な対処である。

よくある原因

  1. Out-File> は UTF-16LE: 「> でリダイレクトしただけ」でも UTF-16LE になる。
    Git の diff がバイナリ扱いになる、grep が引っかからない、といった形で表面化する。
  2. Set-Content は ANSI: 対象ファイルが空か存在しないとき、システムのアクティブなコードページ(日本語環境なら CP932)で書かれる。
    同じスクリプト内で Out-FileSet-Content を混ぜると、ファイルごとに違うエンコーディングになる。
  3. 5.1 の UTF8 は BOM 付き: 5.1 では UTF7 を除くすべての Unicode エンコーディングが BOM を付ける。-Encoding UTF8 を指定しても BOM が入るため、BOM を解釈しない Unix 系ツールが先頭を不正な文字として読む。
  4. 読み込み側も既定が ANSI: Get-Content は BOM の無いファイルを ANSI として読む。
    UTF-8(BOM 無し)で保存されたファイルを読んだ時点で内部表現が壊れており、書き戻すと化けが確定する。
  5. 追記でエンコーディングが混ざる: Out-File -Append>> は既存ファイルのエンコーディングに合わせず既定を使う。
    1 つのファイルの途中からエンコーディングが変わる、という壊れ方をする。

解決策

1. 常に -Encoding を明示する

"あいうえお" | Out-File data.txt -Encoding utf8
$obj | ConvertTo-Json | Set-Content data.json -Encoding utf8

読み込み側も同様に明示する。

Get-Content data.json -Encoding utf8 | ConvertFrom-Json

2. セッションの既定をまとめて変える

エンコーディング パラメーターを持つすべての cmdlet の既定を一括で変えられる。
5.1 以降は >>> も内部で Out-File を呼ぶため、これでリダイレクトにも効く。

$PSDefaultParameterValues['*:Encoding'] = 'utf8'

スクリプトの先頭かプロファイルに置く。
ただしこれはセッション全体に効く設定なので、他人の環境や別バージョンでも同じ挙動にしたいなら、スクリプト側にも同じ行を入れておく。

3. 5.1 で BOM 無し UTF-8 を書く

5.1 の -Encoding UTF8 では BOM を外せない。
BOM 無しが必要なら .NET のメソッドを直接呼ぶ。

$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText("$PWD\data.json", $json, $utf8NoBom)

WriteAllText は相対パスをカレントディレクトリではなくプロセスの作業ディレクトリで解決するため、$PWD を付けた絶対パスを渡す。

4. PowerShell 7 に移行する

7 系はすべての出力の既定が utf8NoBOM に統一されている。
cmdlet ごとの差もなくなるので、混在環境を抱えずに済むならこれが根本解決になる。
移行後もスクリプトを 5.1 と共用するなら、-Encoding の明示は残しておくとよい。

この記事は役立ちましたか?