PowerShell の Set-Content で日本語 UTF-8 ファイルが壊れる話
Get-Content … | Set-Content で日本語ファイルが文字化けして焦っている人へ

先に結論です。
- Windows PowerShell 5.1 の
Get-Content/Set-Content/Out-Fileの既定エンコーディングは環境依存(日本語環境では cp932 / Shift-JIS)です。UTF-8 の日本語ファイルをこれで読み書きすると、多バイト文字が不可逆に壊れます。 Get-Content file | ... | Set-Content fileのようなUTF-8 ファイルの書き戻し(ラウンドトリップ)は厳禁。読んだ時点で文字化けし、書いた時点で確定します。- 回避は3つ。(A) Get/Set 両方に
-Encoding utf8を付ける、(B) PowerShell 7(pwsh)を使う(既定が UTF-8)、(C) そもそも PowerShell でテキスト変換しない(エディタや node/python でやる)。本命は C です。
私は実際に、作業中の index.html(日本語まみれ)を Set-Content で書き戻して一度壊しました。その時の記録と、二度とやらないための具体策を残します。
何が起きたか — 置換するだけのつもりが全部文字化け
やりたかったのは単純なことでした。「HTML の中の特定の文字列を置換する」。それでこう書きました。
(Get-Content index.html) -replace '古い文字列','新しい文字列' | Set-Content index.html
実行した瞬間、ファイル内の日本語が全部 蛟倶ココ髢狗匱 のような化け方をして、見るも無残になりました。しかも元ファイルを上書きしてしまっているので戻せない。バックアップを取っていなかったので、その部分は作り直しになりました。
なぜ壊れるのか — 既定エンコーディングが UTF-8 ではない
Windows PowerShell 5.1(OS に最初から入っている powershell.exe)の文字コードの扱いは直感に反します。
Get-Contentは-Encodingを省略すると、ファイルをシステムの ANSI コードページ(日本語環境では cp932)として読みます。UTF-8 ファイルを cp932 として読むので、この時点でメモリ上の文字列がすでに化けています。Set-Content/Add-Contentも既定がDefault(= cp932)。化けた文字列を cp932 で書き出すので、ディスク上のファイルが確定的に壊れます。- さらに紛らわしいことに、
Out-Fileや>の既定は UTF-16 LE(BOM 付き)で、cmdlet ごとに既定が違います。
つまり Get-Content | Set-Content という何気ないパイプは、UTF-8 → cp932 解釈 → cp932 書き込みという二段階の変換を黙って行い、多バイト文字を破壊します。英数字だけのファイルなら気づかないのも罠で、日本語(や絵文字)が入った瞬間に牙をむきます。
回避A — Get/Set の両方に -Encoding utf8 を付ける
どうしても PowerShell でやるなら、読み・書きの両方でエンコーディングを明示します。片方だけだと直りません。
(Get-Content index.html -Encoding utf8) -replace 'A','B' |
Set-Content index.html -Encoding utf8
ただし PowerShell 5.1 の -Encoding utf8 はBOM 付き UTF-8を書きます。BOM があると、シェルスクリプト・一部の JSON パーサ・古いツールが先頭にゴミがあると誤認して壊れることがあります。BOM なし UTF-8 が必要なら、cmdlet ではなく .NET を直接呼ぶ必要があります。
$text = [System.IO.File]::ReadAllText('index.html', [System.Text.Encoding]::UTF8)
$text = $text.Replace('A','B')
[System.IO.File]::WriteAllText('index.html', $text, (New-Object System.Text.UTF8Encoding $false))
# 第3引数の $false が「BOM なし」
回避B — PowerShell 7(pwsh)を使う
PowerShell 7 系(pwsh.exe)では、全 cmdlet の既定エンコーディングが BOM なし UTF-8 に統一されています。5.1 のような cmdlet ごとのバラつきがなく、普通に Get-Content | Set-Content しても日本語が壊れません。Windows でテキスト処理を PowerShell でやるなら、5.1 ではなく 7 を入れて使うだけで地雷の大半を踏まなくなります。自分の powershell.exe が 5.1 か 7 かは $PSVersionTable.PSVersion で確認できます。
回避C(本命)— そもそも PowerShell で変換しない
身も蓋もない結論ですが、UTF-8 の多バイトファイルを PowerShell のテキスト cmdlet で変換するのはやめるのが一番安全です。理由は単純で、エンコーディングを一箇所でも間違えると上書きで即・不可逆だからです。代わりに、
- エディタ(VS Code 等)の検索置換でやる。差分が見えるしアンドゥが効く。
- スクリプトでやるなら node や python を使う。これらは既定が UTF-8 で、ファイルを開いて読んで書くだけで化けない。
- どうしても PowerShell なら、実行前に必ずバックアップを取る(
Copy-Item index.html index.html.bak)。私が壊した時の最大の反省点はこれを怠ったことです。
「ちょっとした一括置換」ほど雑に Get-Content | Set-Content を打ちがちで、そこに日本語ファイルが当たると一発で壊れます。多バイト文字を含むファイルは、PowerShell のテキスト cmdlet の射程外だと割り切るのが、結局いちばん事故りません。
まとめ
PowerShell 5.1 で日本語 UTF-8 ファイルが文字化けするのは、既定エンコーディングが cp932 で、UTF-8 を読み書きするだけで多バイト文字が壊れるからです。やむを得ず使うなら Get/Set 両方に -Encoding utf8、できれば pwsh(7)、本当はエディタか node で変換するのが安全です。そして何より、上書き前にバックアップ。私はこれを忘れて index.html を一度作り直す羽目になりました。
よくある質問
Get-Content に -Encoding utf8 を付けたのにまだ文字化けします。
書き込み側にも付いているか確認してください。Get-Content だけ UTF-8 にしても、Set-Content / Out-File 側が既定(cp932)のままだと、書き出し時に再び壊れます。読み・書きの両方で -Encoding utf8 を明示する必要があります。それでも BOM が問題になる場合は、.NET の File.WriteAllText に UTF8Encoding($false) を渡して BOM なしで書いてください。
壊れてしまったファイルは元に戻せますか?
cp932 として書き戻して上書き保存してしまった場合、多バイト文字の情報は失われており、基本的に元には戻せません。これがこの事故の怖さです。バックアップ(.bak)か Git のコミット履歴があればそこから復元するのが唯一の道です。上書き前に必ずバックアップを取る習慣をおすすめします。
PowerShell 5.1 と 7 のどちらを使うべきですか?
テキスト処理、特に日本語や絵文字を含むファイルを扱うなら PowerShell 7(pwsh)を強く推奨します。7 では全 cmdlet の既定エンコーディングが BOM なし UTF-8 に統一されており、5.1 のような cmdlet ごとの既定の食い違いによる事故が起きにくくなっています。$PSVersionTable.PSVersion で現在のバージョンを確認できます。
なぜ英数字だけのファイルでは気づかなかったのですか?
cp932 と UTF-8 は ASCII 範囲(半角英数記号)では同じバイト表現なので、英数字だけのファイルはラウンドトリップしても壊れません。日本語・絵文字などの多バイト文字が含まれた瞬間に初めて破壊が表面化します。そのため「今まで動いていたのに、日本語の入ったファイルで突然壊れた」という形で踏みやすいです。