付録: バッチの落とし穴カタログ

症状 → 原因 → 修正 の形で14項目。動かないときに症状から引くための資料です

📅 作成: 2026-08-25 / 更新: 2026-08-25

症状から探す

#症状原因の要約
1ループ内で変数が更新されない括弧ブロックが実行前に一括展開される
2比較が成立しない / 値の末尾に空白が入るset VAR = value の空白
3日本語が化ける・実行が途中で止まるファイルが UTF-8 で保存されている
4ダブルクリックでは動くのに別の場所から呼ぶと失敗するカレントディレクトリを固定していない
5パスに空白が含まれると失敗する引用符の付け方
6エラーが出ているのに処理が続行する終了コードを確認していない
7if の中の %errorlevel% が期待と違うブロック進入時の値に固定されている
8exit したらコンソールごと閉じた/b がない
9:: コメントを入れたらループが構文エラーになった:: はラベルであり括弧内では使えない
10% を含む文字列が消えるバッチ内では %% と書く
11for /f でファイルの中身ではなく文字列そのものが処理される引用符の有無で対象の解釈が変わる
12他のバッチを呼んだらそこで処理が終わってしまったcall を付けていない
13実行後、環境変数が書き換わっているsetlocal がない
14共有フォルダ(UNC パス)でカレント移動が失敗するcd は UNC を扱えない

分類から探す

  1. 変数と展開(#1 #2 #7 #13)
  2. 構文とエスケープ(#9 #10 #11)
  3. 制御フローと終了コード(#6 #8 #12)
  4. パスと実行環境(#4 #5 #14)
  5. 文字コード(#3)

変数と展開

#1 ループ内で変数が更新されない

症状.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回だけ文字列展開されます。この時点で count0 なので、3回分すべての echoecho 現在: 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% でも正しく動くのは「ループ中に値が変わらない変数」だけで、その判断を毎回するより機械的に統一したほうが事故が減ります。

#2 比較が成立しない / 値の末尾に空白が入る

症状値は合っているはずなのに 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! と遅延展開で読んでください(展開結果は読み直されません)。

#7 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" のように比較します。

#13 実行後、環境変数が書き換わっている

症状バッチを実行した後、同じコンソールで別の作業をすると挙動がおかしい。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%"

構文とエスケープ

#9 :: コメントを入れたらループが構文エラーになった

症状コメントを追加しただけで コマンドの構文が誤っています。 と表示される。

for %%f in (*.txt) do (
    :: ファイルごとの処理
    echo %%f
)

原因:: はコメント記号ではなく、名前が空のラベルです。ラベルは括弧ブロックの中に置けないため構文エラーになります。ブロックの外なら結果的にコメントとして機能しますが、括弧内では使えません。

修正括弧の中では rem を使います。

for %%f in (*.txt) do (
    rem ファイルごとの処理
    echo %%f
)

rem はどこでも安全なので、最初から rem だけを使うと決めておけばこの問題は起きません。

#10 % を含む文字列が消える

症状進捗率を表示したいのに 50 だけが出て % が消える。あるいは意図しない文字列に化ける。

echo 進捗: 50%
echo 割引率: 10%OFF
進捗: 50
割引率: 10FF

原因% は変数展開の開始記号です。%OF のような未定義変数として解釈されて消えます(10%OFF%OF が消えて 10FF になります)。

修正バッチファイル内では %% と書きます。

echo 進捗: 50%%
echo 割引率: 10%%OFF

コマンドプロンプトに直接打つ場合は % 1つです。%% のエスケープが必要なのはバッチファイルの中だけです。プロンプトで試して動いたコードをバッチに貼ると、この点だけが崩れます。for %%ffor %f の違いも同じ理由です。

#11 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 側で書くことが多くなります。

制御フローと終了コード

#6 エラーが出ているのに処理が続行する

症状コピー元が存在せずエラーメッセージが出ているのに、その後の「完了しました」まで表示される。

@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 を付ける前に、そのコマンドの終了コードの意味を コマンド /? で確認してください。

#8 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 ncmd.exe を終了cmd /c で単発起動している場合のみ
goto :eof末尾へ飛ぶ(終了コードは変えない)サブルーチンからの復帰

#12 他のバッチを呼んだらそこで処理が終わってしまった

症状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 ラベルへ流れ込んで二重に実行されます。

パスと実行環境

#4 ダブルクリックでは動くのに別の場所から呼ぶと失敗する

症状バッチのあるフォルダで実行すると動くが、タスクスケジューラや別フォルダから実行すると 指定されたパスが見つかりません。 になる。

@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 で失敗します)。

#5 パスに空白が含まれると失敗する

症状C:\Program Files\... を渡すと コマンドの構文が誤っています。 になる。あるいは C:\Program までしか認識されない。

set SRC=C:\Program Files\MyApp\data.txt
copy %SRC% D:\backup\

原因空白は引数の区切りです。引用符で囲まないと C:\ProgramFiles\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:\..."" になり、やはり失敗します。値には引用符を含めないと決めておくのが混乱を避ける唯一の方法です。

#14 共有フォルダ(UNC パス)でカレント移動が失敗する

症状共有フォルダ上に置いたバッチをダブルクリックすると 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 を忘れると割り当てが残るため、必ず対で書きます。

文字コード

#3 日本語が化ける・実行が途中で止まる

症状echo 処理を開始します蜃ヲ逅�繧帝幕蟋� のように表示される。あるいは日本語を含む行でエラーになり、以降が実行されない。

原因バッチファイルは日本語 Windows では CP932(Shift-JIS)として読まれます。UTF-8 で保存すると、日本語部分が別のバイト列として解釈されます。

さらに厄介なのは、化けた結果に |& に相当するバイトが現れると、構文エラーや意図しないコマンド実行になる点です。表示が崩れるだけでは済みません。

化けるかどうかは「2つの設定の組み合わせ」で決まる

ファイルの文字コードだけでは決まりません。実行時のコンソールのコードページとの組み合わせで結果が変わります。

ファイルの文字コードコンソールのコードページ結果
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

UTF-8 で書きたい場合

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 として読み直して保存すると、不正バイトが ? に置換されて日本語が復元不能になります。エンコーディングを変える前に、現在何で保存されているかを必ず確認してください。