| # | 症状 | 原因の要約 |
|---|---|---|
| 1 | ループ内で変数が更新されない | 括弧ブロックが実行前に一括展開される |
| 2 | 比較が成立しない / 値の末尾に空白が入る | set VAR = value の空白 |
| 3 | 日本語が化ける・実行が途中で止まる | ファイルが UTF-8 で保存されている |
| 4 | ダブルクリックでは動くのに別の場所から呼ぶと失敗する | カレントディレクトリを固定していない |
| 5 | パスに空白が含まれると失敗する | 引用符の付け方 |
| 6 | エラーが出ているのに処理が続行する | 終了コードを確認していない |
| 7 | if の中の %errorlevel% が期待と違う | ブロック進入時の値に固定されている |
| 8 | exit したらコンソールごと閉じた | /b がない |
| 9 | :: コメントを入れたらループが構文エラーになった | :: はラベルであり括弧内では使えない |
| 10 | % を含む文字列が消える | バッチ内では %% と書く |
| 11 | for /f でファイルの中身ではなく文字列そのものが処理される | 引用符の有無で対象の解釈が変わる |
| 12 | 他のバッチを呼んだらそこで処理が終わってしまった | call を付けていない |
| 13 | 実行後、環境変数が書き換わっている | setlocal がない |
| 14 | 共有フォルダ(UNC パス)でカレント移動が失敗する | cd は UNC を扱えない |
症状.txt ファイルが3つあるフォルダで実行しても、表示はすべて 現在: 0 になる。
@echo off
setlocal
set count=0
for %%f in (*.txt) do (
set /a count=count+1
echo 現在: %count%
)
echo 合計: %count%
現在: 0
現在: 0
現在: 0
合計: 3
原因括弧 ( 〜 ) の中身は、ループに入る前に丸ごと1回だけ文字列展開されます。この時点で count は 0 なので、3回分すべての echo が echo 現在: 0 という文字列に確定します。最後の 合計 は括弧の外なので、実行時の値 3 が出ます。
修正setlocal enabledelayedexpansion を宣言し、括弧の中では !VAR! で読みます。
@echo off
setlocal enabledelayedexpansion
set count=0
for %%f in (*.txt) do (
set /a count=count+1
echo 現在: !count!
)
echo 合計: !count!
括弧の中は常に !VAR! と決めてしまうのが安全です。%VAR% でも正しく動くのは「ループ中に値が変わらない変数」だけで、その判断を毎回するより機械的に統一したほうが事故が減ります。
症状値は合っているはずなのに if が成立しない。echo の出力を見ても違いが分からない。
set MODE = test
if "%MODE%"=="test" echo テストモード
原因set MODE = test は「MODE (末尾に空白)」という名前の変数に「 test(先頭に空白)」を代入します。%MODE% は定義されていないため、if の左辺は %MODE% という文字列そのものになります。
修正= の前後に空白を入れず、変数名と値をまとめて " で囲みます。
set "MODE=test"
if "%MODE%"=="test" echo テストモード
この set "名前=値" の形は、値の末尾に空白が付かないだけでなく、値に & や | を含められるという利点もあります。常にこの形で書くのが定型です。
ただし守られるのは代入だけです。set "MSG=Hello & World" で値は正しく入りますが、echo %MSG% は展開後の echo Hello & World という行がもう一度コマンドとして読み直され、& がコマンド区切りとして働いて壊れます。記号を含む値は echo !MSG! と遅延展開で読んでください(展開結果は読み直されません)。
if の中の %errorlevel% が期待と違う症状コピーが失敗しているのに 成功 と表示される。
if exist "%SRC%" (
copy "%SRC%" "%DST%" >nul
if "%errorlevel%"=="0" (echo 成功) else (echo 失敗)
)
原因#1 と同じ理由です。外側の括弧に入る時点で %errorlevel% が展開されて固定されるため、copy の結果は反映されません。
修正方法は3つあります。いずれも「実行時の値を見る」ための手段です。
rem (1) 遅延展開で読む
if exist "%SRC%" (
copy "%SRC%" "%DST%" >nul
if "!errorlevel!"=="0" (echo 成功) else (echo 失敗)
)
rem (2) if errorlevel を使う(この構文は実行時に評価される)
if exist "%SRC%" (
copy "%SRC%" "%DST%" >nul
if errorlevel 1 (echo 失敗) else (echo 成功)
)
rem (3) || で失敗時の処理をつなぐ(最も簡潔)
copy "%SRC%" "%DST%" >nul || (echo 失敗 & exit /b 1)
echo 成功
if errorlevel 1 は「1以上」を意味します。「1と等しい」ではありません。終了コードが 2 でも 5 でも真になります。特定の値だけを見たいときは if "!errorlevel!"=="2" のように比較します。
症状バッチを実行した後、同じコンソールで別の作業をすると挙動がおかしい。PATH が変わっている、TEMP が消えているなど。
@echo off
rem setlocal がない
set PATH=C:\mytools
set TEMP=D:\tmp
mytool.exe
原因set はプロセスの環境変数を書き換えます。バッチにはブロックスコープが存在せず、setlocal を書かない限り変更は呼び出し元のコンソールに残り続けます。
修正先頭で setlocal を宣言します。スクリプト終了時に自動的に元へ戻ります。
@echo off
setlocal enabledelayedexpansion
set "PATH=C:\mytools;%PATH%"
set "TEMP=D:\tmp"
mytool.exe
意図的に呼び出し元へ値を返したい場合は、endlocal と同じ行で持ち出します。
rem RESULT だけを呼び出し元に残す
endlocal & set "RESULT=%RESULT%"
:: コメントを入れたらループが構文エラーになった症状コメントを追加しただけで コマンドの構文が誤っています。 と表示される。
for %%f in (*.txt) do (
:: ファイルごとの処理
echo %%f
)
原因:: はコメント記号ではなく、名前が空のラベルです。ラベルは括弧ブロックの中に置けないため構文エラーになります。ブロックの外なら結果的にコメントとして機能しますが、括弧内では使えません。
修正括弧の中では rem を使います。
for %%f in (*.txt) do (
rem ファイルごとの処理
echo %%f
)
rem はどこでも安全なので、最初から rem だけを使うと決めておけばこの問題は起きません。
% を含む文字列が消える症状進捗率を表示したいのに 50 だけが出て % が消える。あるいは意図しない文字列に化ける。
echo 進捗: 50%
echo 割引率: 10%OFF
進捗: 50
割引率: 10FF
原因% は変数展開の開始記号です。%OF のような未定義変数として解釈されて消えます(10%OFF は %OF が消えて 10FF になります)。
修正バッチファイル内では %% と書きます。
echo 進捗: 50%%
echo 割引率: 10%%OFF
コマンドプロンプトに直接打つ場合は % 1つです。%% のエスケープが必要なのはバッチファイルの中だけです。プロンプトで試して動いたコードをバッチに貼ると、この点だけが崩れます。for %%f と for %f の違いも同じ理由です。
for /f でファイルの中身ではなく文字列そのものが処理される症状ファイルの各行を読みたいのに、ファイル名がそのまま1行として表示される。
rem data.txt の中身を読みたい
for /f "delims=" %%l in ("data.txt") do echo %%l
data.txt
原因for /f は括弧内の書き方で対象の解釈を変えます。
| 書き方 | 対象 |
|---|---|
in (data.txt) | ファイル data.txt の各行 |
in ("data.txt") | data.txt という文字列そのもの |
in ('dir /b') | コマンドの実行結果 |
修正引用符なしで書きます。ただしパスに空白が含まれる場合は引用符が必要になるため、そのときは usebackq を付けて意味を切り替えます。
rem 空白を含まないパス
for /f "delims=" %%l in (data.txt) do echo %%l
rem 空白を含むパス(usebackq を付けると "..." がファイル名の意味になる)
for /f "usebackq delims=" %%l in ("%~dp0data file.txt") do echo %%l
rem usebackq のとき、コマンド実行はバッククォートになる
for /f "usebackq delims=" %%l in (`dir /b *.txt`) do echo %%l
usebackq を付けると3つの記号の意味がまとめて入れ替わります。付けない場合は '…' がコマンド、"…" が文字列。付けた場合は `…` がコマンド、"…" がファイル名、'…' が文字列です。%~dp0 を使うパスは空白を含みうるので、実務では usebackq 側で書くことが多くなります。
症状コピー元が存在せずエラーメッセージが出ているのに、その後の「完了しました」まで表示される。
@echo off
copy "C:\notexist\a.txt" "D:\backup\"
del "C:\notexist\a.txt"
echo 完了しました
原因バッチに例外はありません。コマンドが失敗しても終了コードが 0 以外になるだけで、次の行はそのまま実行されます。1行ごとに自分で確認しない限り、処理は最後まで走ります。
修正失敗したら止めたい箇所に || exit /b を付けます。
@echo off
setlocal
copy "C:\notexist\a.txt" "D:\backup\" || (
echo コピーに失敗しました
exit /b 1
)
del "C:\notexist\a.txt" || exit /b 1
echo 完了しました
終了コードを返さないコマンドもあります。robocopy は成功時にも 1〜7 を返し、8 以上が失敗です。find は該当なしで 1 を返します。|| exit /b を付ける前に、そのコマンドの終了コードの意味を コマンド /? で確認してください。
exit したらコンソールごと閉じた症状エラー時に exit するバッチを別のバッチから呼んだら、呼び出し元まで終了してしまった。手で開いていたコマンドプロンプトも閉じた。
if not exist "%TARGET%" (
echo 対象がありません
exit 1
)
原因exit は cmd.exe そのものを終了させます。バッチだけを終わらせるには /b(batch)が必要です。
修正
if not exist "%TARGET%" (
echo 対象がありません
exit /b 1
)
| 書き方 | 効果 | 使う場面 |
|---|---|---|
exit /b n | このバッチだけ終了 | ほぼ常にこちら |
exit n | cmd.exe を終了 | cmd /c で単発起動している場合のみ |
goto :eof | 末尾へ飛ぶ(終了コードは変えない) | サブルーチンからの復帰 |
症状sub.cmd を呼んだ後の行が実行されない。
@echo off
echo 前処理
sub.cmd
echo 後処理 <- 実行されない
原因バッチから別のバッチを call なしで実行すると、制御が移ったまま戻ってきません。同じことがサブルーチンのラベルにも当てはまります。
修正
@echo off
echo 前処理
call sub.cmd
echo 後処理
サブルーチンも同様に call :label で呼び、末尾を goto :eof で締めます。
call :log "開始"
echo 本処理
call :log "終了"
goto :eof
:log
echo [%DATE% %TIME%] %~1
goto :eof
この goto :eof(メイン処理の末尾)を忘れると、処理がそのまま :log ラベルへ流れ込んで二重に実行されます。
症状バッチのあるフォルダで実行すると動くが、タスクスケジューラや別フォルダから実行すると 指定されたパスが見つかりません。 になる。
@echo off
rem config.ini はバッチと同じフォルダにある
for /f "delims=" %%l in (config.ini) do echo %%l
原因相対パスの起点はバッチの場所ではなく実行時のカレントディレクトリです。呼び出し方によってカレントは変わります(タスクスケジューラなら C:\Windows\System32 になることもあります)。
修正先頭でカレントをバッチの場所に固定するか、パスを %~dp0 で明示します。
@echo off
rem 方法1: カレントを移動する(最後に popd で戻す)
pushd "%~dp0" || exit /b 1
for /f "delims=" %%l in (config.ini) do echo %%l
popd
rem 方法2: パスを明示する(カレントを変えたくない場合)
for /f "delims=" %%l in ("%~dp0config.ini") do echo %%l
%~dp0 の末尾には \ が付いています。連結するときは "%~dp0config.ini" です。"%~dp0\config.ini" と書くと \ が二重になります(多くの場合は動いてしまいますが、UNC パスや一部の API で失敗します)。
症状C:\Program Files\... を渡すと コマンドの構文が誤っています。 になる。あるいは C:\Program までしか認識されない。
set SRC=C:\Program Files\MyApp\data.txt
copy %SRC% D:\backup\
原因空白は引数の区切りです。引用符で囲まないと C:\Program と Files\MyApp\data.txt の2引数として渡されます。
修正変数に入れるときは値を囲まず、使うときに囲みます。
set "SRC=C:\Program Files\MyApp\data.txt"
copy "%SRC%" "D:\backup\"
引数として受け取る場合は %~1 で既存の引用符を外し、使うときに改めて囲みます。こうすれば、呼び出し側が囲んでいても囲んでいなくても同じ形で扱えます。
set "TARGET=%~1"
if not exist "%TARGET%" (
echo 見つかりません: %TARGET%
exit /b 1
)
引用符を二重に付けると別の失敗になります。set SRC="C:\Program Files\a.txt" のように値ごと囲むと、%SRC% の中に引用符が含まれます。これを "%SRC%" と囲めば ""C:\..."" になり、やはり失敗します。値には引用符を含めないと決めておくのが混乱を避ける唯一の方法です。
症状共有フォルダ上に置いたバッチをダブルクリックすると UNC パスはサポートされません。 と表示され、カレントが C:\Windows になる。
@echo off
cd /d "%~dp0"
rem \\server\share\tools\ では失敗する
原因cd は UNC パス(\\server\share\...)をカレントディレクトリにできません。これは cmd.exe の仕様です。
修正pushd を使います。pushd は UNC パスに一時的なドライブレターを割り当てるため、そのまま移動できます。
@echo off
pushd "%~dp0" || (
echo 作業フォルダへ移動できません
exit /b 1
)
rem …処理…
popd
pushd が割り当てたドライブは popd で解放されます。popd を忘れると割り当てが残るため、必ず対で書きます。
症状echo 処理を開始します が 蜃ヲ逅�繧帝幕蟋� のように表示される。あるいは日本語を含む行でエラーになり、以降が実行されない。
原因バッチファイルは日本語 Windows では CP932(Shift-JIS)として読まれます。UTF-8 で保存すると、日本語部分が別のバイト列として解釈されます。
さらに厄介なのは、化けた結果に | や & に相当するバイトが現れると、構文エラーや意図しないコマンド実行になる点です。表示が崩れるだけでは済みません。
ファイルの文字コードだけでは決まりません。実行時のコンソールのコードページとの組み合わせで結果が変わります。
| ファイルの文字コード | コンソールのコードページ | 結果 |
|---|---|---|
| SJIS (CP932) | 932 | 正常これが日本語 Windows の既定の組み合わせ |
| SJIS (CP932) | 65001 (UTF-8) | 化けるファイルは正しいのに化ける。呼び出し元が UTF-8 のコンソールだと起こる |
| UTF-8 (BOMなし) | 932 | 化ける最も多いパターン。エディタの既定が UTF-8 のまま保存した場合 |
| UTF-8 (BOMなし) | 65001 (UTF-8) | 条件付き表示は通るが、CP932 で出力する外部コマンドの結果が化ける |
| UTF-8 (BOM付き) | どちらでも | エラー先頭の BOM が1行目のコマンドの一部として解釈される |
コードページは 親プロセスから継承されます。エクスプローラからダブルクリックした場合はシステムの OEM コードページ(日本語 Windows なら 932)になりますが、UTF-8 に設定されたターミナルや他のツールから呼ばれると 65001 のまま実行されます。同じバッチが「自分の環境では動くのに CI やタスクランナー経由だと化ける」のはこれが原因です。
現在のコードページは chcp で確認できます(引数なしで実行すると現在値の表示のみ)。
C:\>chcp
現在のコード ページ: 932
修正バッチファイルは SJIS(CP932)+ CRLF で保存します。上の表のうち安定して動く組み合わせはこれだけです。
| エディタ | 設定箇所 |
|---|---|
| VS Code | 右下のエンコーディング表示 → 「エンコード付きで保存」→ Japanese (Shift JIS)。改行も右下で CRLF にする |
| メモ帳 | 「名前を付けて保存」→ エンコード ANSI |
| サクラエディタ等 | 文字コード SJIS、改行 CRLF |
chcp 65001 でコードページを UTF-8 に変更する方法がありますが、副作用があるため推奨しません。
| 副作用 | 内容 |
|---|---|
| 変更が残る | chcp はコンソール全体の設定を変える。バッチ終了後も戻らない(setlocal の対象外) |
| 外部コマンドの出力 | CP932 で出力するコマンドの結果が化ける。for /f で読むと壊れる |
| BOM の扱い | BOM 付き UTF-8 だと1行目の @echo off の前に不正な文字が入り、エラーメッセージが出る |
どうしても UTF-8 で扱う必要がある場合は、バッチ側は SJIS のまま保ち、UTF-8 が必要な処理だけを PowerShell スクリプトに切り出して powershell -File で呼ぶほうが安全です。
SJIS で保存しても、65001 のコンソールから呼ばれれば化けます。呼び出し元を制御できないバッチでは、次の2つの方針があります。
| 方針 | 内容 | 評価 |
|---|---|---|
| メッセージを英数字だけにする | 画面出力・ログを ASCII のみで書く。日本語はコメント(rem)だけに使う |
確実コードページに一切依存しない。ログを他のツールで処理する場合にも有利 |
冒頭で chcp 932 する |
自分が想定するコードページに揃えてから処理する | 条件付き下記の制約がある |
chcp で揃える場合、終了時に元の値へ戻したくなりますが、chcp の出力書式(現在のコード ページ: 932 / Active code page: 932.)は OS の表示言語によって変わるため、元の値を取り出す処理はロケール依存になります。setlocal の対象外なので自動で戻ることもありません。無人実行の専用バッチのように「このコンソールは自分専用」と言える場合に限って使うのが安全です。
変換は1回だけ行ってください。SJIS のファイルを UTF-8 として読み直して保存すると、不正バイトが ? に置換されて日本語が復元不能になります。エンコーディングを変える前に、現在何で保存されているかを必ず確認してください。