【bat】Unicode ファイルを扱えないときの原因と解決策

バッチ(.bat)で「UTF‑8 のテキストが化ける」「UTF‑16 の内容を for /f で読めない」「日本語が『???』になる」といった不具合は、コンソールのコードページとファイルのエンコーディングの不一致が主因です。cmd.exe は歴史的に Shift‑JIS(CP932)前提で、UTF 系の読み書きやパイプ処理に制限があります。ここでは原因の切り分けと、安全に Unicode を扱うための現実解を順にまとめます。

現状把握:コードページとファイルのエンコーディングを確認する

まずコンソールのコードページを chcp で確認します。932 は Shift‑JIS、65001 は UTF‑8 を意味します。次に対象ファイルの保存形式をエディタで確認します。UTF‑8(BOM 有無)や UTF‑16LE(Unicode)かどうかを把握し、コンソール側の前提と一致しているかを見ます。バッチファイルそのものを UTF‑8 BOM 付きで保存すると、先頭の BOM がコマンドとして解釈される事故が起きるため、.bat 本文は基本的に ASCII もしくは ANSI(SJIS)で保存し、非 ASCII は外部化する方針が安全です。

UTF‑8 テキストを「表示」したいだけなら一時的に 65001 へ切り替える

表示中心の用途では、処理の前後でコードページを 65001 に切り替え、最後に元へ戻します。古いツールは 65001 で不安定なことがあるため、切り替えの範囲を最小にします。

@echo off
setlocal
for /f "tokens=2 delims=: " %%A in ('chcp ^| find ":"') do set "_orig=%%A"
chcp 65001 >nul
type utf8.txt
chcp %_orig% >nul
endlocal

UTF‑8 や UTF‑16 の「行処理」は cmd だけで完結させない

for /f は内部的に ANSI を前提としており、UTF‑8/UTF‑16 の行を正しく読み取れないことが多いです。確実に処理したい場合は PowerShell に委譲してエンコーディングを明示します。UTF‑8 を読むなら -Encoding UTF8、UTF‑16LE なら -Encoding Unicode を指定します。

@echo off
for /f "usebackq delims=" %%L in (`
  powershell -NoProfile -Command "Get-Content -Encoding UTF8 -Path 'C:\data\utf8.txt'"
`) do (
  echo %%L
)
@echo off
for /f "usebackq delims=" %%L in (`
  powershell -NoProfile -Command "Get-Content -Encoding Unicode -Path 'C:\data\utf16.txt'"
`) do (
  echo %%L
)

「書き出し」を UTF‑8 や UTF‑16 で固定したいときの安全策

リダイレクト(>)で作られるファイルの文字コードは現在のコードページに依存します。生成ファイルを UTF 系に固定したいなら PowerShell の出力系を使います。Windows PowerShell 5.x の既定は BOM 付き UTF‑16LE、PowerShell 7 の既定は BOM なし UTF‑8 です。必要な形式を明示します。

@echo off
set "MSG=こんにちは"
powershell -NoProfile -Command "Set-Content -Path 'out-utf8.txt' -Value $env:MSG -Encoding UTF8"
powershell -NoProfile -Command "Set-Content -Path 'out-utf16.txt' -Value $env:MSG -Encoding Unicode"

パイプやリダイレクトで Unicode を通したいときの cmd /U

cmd /U を使うと、パイプやリダイレクトのストリームが UTF‑16LE になります。下流が UTF‑16LE を受け取れる前提なら安定しますが、他の工程が ANSI と思い込んでいると別の文字化けを招きます。流れ全体でエンコーディングを統一できる場合に限定して使います。

cmd /U /C "type C:\data\utf16.txt | findstr /R . > C:\work\pass_through.txt"

ファイル名が Unicode(日本語・絵文字等)の場合の扱い

ファイル内容とは別に、名称が非 ASCII の場合も配慮が必要です。基本は必ず二重引用符で囲み、cmd の制約が強いコピーや移動は PowerShell もしくは robocopy に任せます。PowerShell では -LiteralPath を使うとワイルドカード展開の影響を受けません。

@echo off
powershell -NoProfile -Command ^
  "Copy-Item -LiteralPath 'C:\data\日本語\資料📄.txt' -Destination 'D:\bk\資料📄.txt' -Force"

BOM と .bat 本文の関係を誤解しない

.bat 自体を UTF‑8 BOM 付きで保存すると、先頭 3 バイトがコマンドとして解釈され、不可解なエラーや先頭行欠落の原因になります。バッチ本文は可能な限り ASCII で記述し、日本語文字列は外部ファイルや PowerShell から読み込んで表示する設計にすると、編集者やツールが変わっても安定します。

@echo off
powershell -NoProfile -Command "Get-Content -Encoding UTF8 -Path 'msg_ja.txt' | Write-Host"

それでも化けるときの切り分け手順

まず echo( で変数の実体を出し、表示の問題か中身の問題かを分けます。次に typemore を使って元ファイルを直に表示し、コンソール側のコードページとフォント(Consolas や Lucida Console 推奨)を確認します。最後に PowerShell 経由で同じファイルを読み、正しく読めるなら cmd の限界が原因です。以降は PowerShell へ委譲する方針に切り替えます。

まとめ:運用方針を固めて迷わない

表示だけなら一時的に 65001 へ切り替える方法が簡便です。行処理や変換を伴う場合は、Get‑Content/Set‑Content によるエンコーディング明示を基本にし、バッチ本文は ASCII に徹して日本語は外部化します。パイプ全体を UTF‑16LE で流す必要があるときは cmd /U を使いますが、工程全体のエンコーディングを統一できる場合に限定します。ファイル名の Unicode 取り扱いは PowerShell の -LiteralPath を使い、二重引用符を徹底します。これらを守れば、cmd の歴史的制約に振り回されず、Unicode ファイルを安定して扱えるようになります。