事故と原因の記録

PowerShellで日本語ファイルを丸ごと壊した|Windowsで1行だけ直すつもりが全損する経路

定数をひとつ書き換えるだけのつもりで Get-Content | Set-Content を通したら、ファイル全体が文字化けしました。壊れたのは書き込みではなく読み込みの側です。Windowsで運用しているとこの手の穴がいくつかあります。

このサイトはAnthropicの公式サイトではありません。Claude Codeの機能・料金・提供範囲は変わる可能性があるため、重要な判断の前には公式のドキュメントと自分のアカウント設定を確認してください。

何が起きたか

やろうとしたのは、UTF-8で書かれた日本語混じりのソースの中の、数値をひとつ変えることでした。書いたのはこういう処理です。

(Get-Content site.js) -replace 'VERSION = 12', 'VERSION = 13' | Set-Content site.js

実行すると数値は確かに変わります。ただし日本語の部分が全部壊れます。1行だけ直すつもりが、ファイル全体が読めなくなります。

原因は Set-Content 側だと思われがちですが、実際には Get-Content の時点で壊れています。エンコーディングを指定しないと、環境によってはシステムのANSIコードページとして読み込まれ、UTF-8のバイト列が別の文字に化けます。その化けた文字列を書き戻すので、元には戻せません。

さらに Set-Content 側も、既定の出力エンコーディングが Out-File と違うことがあります。読み書きの両方に地雷があるということです。

対処:ソースの書き換えにシェルを使わない

いろいろ試しましたが、いちばん確実なのはシェルでソースを書き換えないことでした。

Claude Code には文字列を置換する専用のツールがあり、そちらはファイルのエンコーディングを保ったまま書き換えます。定数ひとつの変更でも、そちらを使うようにしています。「1文字だから」と -replace を通した回が、いちばん被害が大きくなります。

どうしてもスクリプトから書き換える必要があるときは、PowerShellではなくNode.jsで書いています。

const fs = require('fs');
const s = fs.readFileSync('site.js', 'utf8');       // 明示的に utf8
fs.writeFileSync('site.js', s.replace('VERSION = 12', 'VERSION = 13'), 'utf8');

読み書きの両方でエンコーディングを明示していれば化けません。日本語を含む検証スクリプトも .ps1 ではなく .js で書くようにしています。出力に日本語が混ざるだけで、コンソールの表示が崩れることがあるためです。

Git Bash がパスを書き換える

Windowsではもうひとつ、パスに関する落とし穴があります。Git Bash(MSYS2系のシェル)は、コマンドの引数がスラッシュで始まるUNIX風のパスに見えると、Windowsのパスに変換してから渡します

これが問題になるのは、リモートサーバー上のパスを引数に渡すときです。たとえばFTPの操作スクリプトに /domains/example.jp/public_html/ を渡したいのに、途中で C:/Program Files/Git/domains/... のような文字列に化けます。

エラーにならないのが厄介なところで、「そのパスは存在しません」という、一見もっともらしい失敗になります。対処は単純で、リモートの絶対パスを引数に渡すときはPowerShellから実行することにしました。

# これは化ける可能性がある(Git Bash)
node ftp.js /domains/example.jp/public_html/index.html

# PowerShell から実行する
node ftp.js /domains/example.jp/public_html/index.html

見た目は同じコマンドですが、通すシェルで結果が変わります。

node -e をダブルクォートで囲むと中身が展開される

関連する事故をもうひとつ。bashから node -e にコードを渡すとき、ダブルクォートで囲むとシェルが先に中身を展開します

# PHPのコードを流し込むつもりが、$data がシェルに食われて空になる
node -e "fs.writeFileSync('a.php', '<?php echo $data; ?>')"

生成されたPHPは echo ; になり、構文エラーで500を返します。シングルクォートで囲むか、そもそもコードは -e ではなくファイルに書いて実行するほうが安全です。1行で済ませようとして壊した回が何度かあります。

node の dns モジュールが通らない環境がある

DNSレコードの設定確認を自動化しようとして、Node.js の dns モジュールで引いたところ、設定済みのレコードが全て「なし」と返ってきました

原因はレコード側ではなく、環境からUDPの53番ポートが出られなかったことです。dns.resolveMx() などは黙って空を返すので、「MXが設定されていない」という誤った結論になります。これもエラーにならずに間違った答えを返すタイプの失敗です。

対処は、OSのDNSクライアントを子プロセスで呼ぶことにしました。

const { execFileSync } = require('child_process');
const out = execFileSync('nslookup', ['-type=MX', 'example.jp'], { encoding: 'utf8' });

こちらは通ります。「ライブラリが空を返したら、まず経路を疑う」というのが、この件から得た教訓です。

Windowsで運用するときのまとめ

やりたいこと避けること使うもの
日本語ソースの書き換えGet-Content|Set-Content編集用ツール、または fs で utf8 明示
リモート絶対パスを引数に渡すGit BashPowerShell
短いコードの実行node -e "..."ファイルに書いて実行
DNSレコードの確認dns モジュールnslookup を子プロセスで
日本語を出す検証スクリプト.ps1.js

共通しているのは、どれもエラーを出さずに間違った結果を返すことです。だから「動いた」だけでは確認になりません。書き換えたファイルは中身を読み直す、DNSは別の手段でも引く、といった二重の確認が要ります。

よくある質問

PowerShellでエンコーディングを指定すれば使えますか?

読み込みと書き込みの両方で明示すれば動きます。ただしバージョンによって既定が違い、環境が変わると再発します。壊れたときの被害がファイル全体なので、そもそも別の手段を使うほうを選びました。

壊れたファイルは戻せますか?

化けた文字列を書き戻しているので、元のバイト列は残っていません。バックアップか、本番サーバーからのダウンロードで戻すことになります。編集の前に退避を取る習慣があるかどうかで被害が変わります。

WSLを使えば解決しますか?

エンコーディングとパスの問題は減ります。ただしWindows側のファイルを触ると別の癖が出るので、置き換えれば済むという話でもありません。ここに書いたのはWindowsのまま運用した場合の話です。

次に読むページ