バッチファイルで「ループ内で変数を更新しているのに値が変わらない」「% や ! を含む文字列が壊れる」「setlocal の中で変えた値が外に出ない」——こうしたトラブルの原因はほぼ例外なく変数展開のタイミングと文脈にあります。
本記事では cmd.exe が変数をいつどのように展開するかを基礎から整理し、FORループ・特殊文字・スコープ・デバッグまで実際に動くコードとともに体系的に解説します。
- cmd.exe の変数展開タイミングを理解する
- FORループ内で値が変わらない(最も多いトラブル)
- %VAR% と !VAR! の違いを整理する
- call による二段階展開(遅延展開なしの代替手段)
- %% のエスケープルール
- ! のエスケープ(遅延展開有効時)
- 特殊文字のエスケープ一覧
- set "VAR=値" 形式が必須な理由
- setlocal/endlocal スコープと値の引き渡し
- 変数の空判定の落とし穴
- for /f と遅延展開の組み合わせ
- 遅延展開が効かないケースと対処
- デバッグ方法:展開を可視化する
- よくある変数展開トラブルのパターンまとめ
- 実践例:遅延展開を使った文字列処理スクリプト
- よくある質問(FAQ)
- まとめ
cmd.exe の変数展開タイミングを理解する
cmd.exe はコマンドを実行する前に2段階の変数展開を行います。ここを理解するだけで、大半のトラブルの原因がわかります。
| フェーズ | 展開の種類 | タイミング | 構文 |
|---|---|---|---|
| フェーズ1 | 通常展開(パーセント展開) | 行またはブロック全体を読み込んだ直後に一括置換 | %VAR% |
| フェーズ2 | 遅延展開 | 各コマンドを実行する直前に置換 | !VAR! |
ポイントはブロック(括弧で囲まれた範囲)全体が1回の読み込みとして処理される点です。for や if の本体はブロックとして一括読み込みされるため、%VAR% はブロック開始前の値しか参照できません。
FORループ内で値が変わらない(最も多いトラブル)
バッチ初心者が最初にぶつかる典型的な問題です。
@echo off
set COUNT=0
for %%A in (1 2 3) do (
set /a COUNT+=1
echo COUNT=%COUNT%
)
echo 最終値: %COUNT%
rem 出力:
rem COUNT=0
rem COUNT=0
rem COUNT=0
rem 最終値: 3 ← ループ後は正しい
なぜこうなるのか。for (...) do (...) の括弧内は1つのブロックとして一括解析されます。%COUNT% はブロック読み込み時点(COUNT=0)で展開済みのため、ループ中に COUNT を更新しても echo には反映されません。
@echo off
setlocal enabledelayedexpansion
set COUNT=0
for %%A in (1 2 3) do (
set /a COUNT+=1
echo COUNT=!COUNT!
)
echo 最終値: !COUNT!
endlocal
rem 出力:
rem COUNT=1
rem COUNT=2
rem COUNT=3
rem 最終値: 3
setlocal enabledelayedexpansion を有効にすると、!VAR! は実行時(コマンドごと)に展開されるため、ブロック内の最新値を参照できます。遅延展開の詳しい使い方はsetlocal enabledelayedexpansion 完全ガイドを参照してください。
%VAR% と !VAR! の違いを整理する
| 項目 | %VAR%(通常展開) | !VAR!(遅延展開) |
|---|---|---|
| 展開タイミング | 行/ブロック読み込み時 | コマンド実行直前 |
| 有効条件 | 常に有効 | setlocal enabledelayedexpansion 必須 |
| ループ内での動作 | ブロック開始時の値のまま | 実行時の最新値を取得 |
| 未定義変数 | %VAR% をそのまま出力 |
!VAR! をそのまま出力 |
| 使いどころ | ループ外・単純な置換 | ループ内・ブロック内で値を更新して参照 |
@echo off
setlocal enabledelayedexpansion
set X=before
if 1==1 (
set X=after
echo %%展開: %X%
echo !展開: !X!
)
rem 出力:
rem %展開: before ← ブロック読み込み時の値
rem !展開: after ← 実行時の最新値
call による二段階展開(遅延展開なしの代替手段)
setlocal enabledelayedexpansion が使えない環境や、! を含む文字列を扱う際の一時回避として、call コマンドによる二段階展開が使えます。
@echo off
set COUNT=0
for %%A in (1 2 3) do (
set /a COUNT+=1
rem call を挟むと再展開される
call echo COUNT=%%COUNT%%
)
rem %%COUNT%% は call 実行時に %COUNT% として展開され
rem さらに COUNT の値として展開される
call echo %%COUNT%% は2段階で処理されます。①ブロック読み込み時:
%%COUNT%% → %COUNT%(%% → %)②call 実行時:
%COUNT% → 実際の値
ただし call の呼び出しコストは高く、ループが多い場合は遅延展開の方がシンプルで高速です。
%% のエスケープルール
% はバッチファイル内で特殊な意味を持ちます。リテラル(文字としての)% を出力するには、文脈によってエスケープ方法が異なります。
@echo off rem バッチファイル内で % を文字として出力する echo 達成率: 100%% rem 出力: 達成率: 100% rem FORのループ変数はバッチファイル内では %%F for %%F in (*.txt) do echo %%~nxF rem コマンドラインから直接実行する場合は %F(シングル) rem for %F in (*.txt) do echo %~nxF
| 場面 | ループ変数 | 変数参照 | リテラル % |
|---|---|---|---|
| バッチファイル(.bat) | %%F |
%VAR% |
%% |
| コマンドラインから直接入力 | %F |
%VAR% |
%(不要) |
| call 内で再展開させる | %%%%F |
%%VAR%% |
%%%% |
! のエスケープ(遅延展開有効時)
setlocal enabledelayedexpansion を有効にすると、! が変数区切りとして解釈されます。パスやログ行など ! を含む文字列を扱う場合は注意が必要です。
@echo off
setlocal enabledelayedexpansion
rem 方法1: setlocal DisableDelayedExpansion で一時的に無効化
for /f "usebackq delims=" %%L in ("input.txt") do (
set "LINE=%%L"
setlocal disabledelayedexpansion
echo %LINE%
endlocal
)
rem 方法2: ^^! でエスケープ(^ を2つ重ねる)
echo This is a test^^!
rem 出力: This is a test!
遅延展開有効時、
! の前の ^ がエスケープ文字として機能します。しかし ^ 自体もエスケープが必要な特殊文字のため ^^ と書く必要があります。結果として ^^! が ! を文字として出力します。最も確実なのは「方法1」の
setlocal disabledelayedexpansion による一時無効化です。特殊文字のエスケープ一覧
バッチファイルでは & | > < ^ ( ) などがコマンドの区切りやリダイレクトとして解釈されます。文字として扱うには ^ でエスケープするか、ダブルクォートで囲みます。
| 文字 | 通常の意味 | ^ によるエスケープ | クォートで囲む |
|---|---|---|---|
& |
コマンド区切り | ^& |
"&" |
| |
パイプ | ^| |
"|" |
> |
リダイレクト(上書き) | ^> |
">" |
< |
リダイレクト(入力) | ^< |
"<" |
^ |
エスケープ文字 | ^^ |
"^" |
( ) |
ブロック区切り | ^( ^) |
"(" |
! |
遅延展開(有効時のみ) | ^^!(遅延展開有効時) |
クォートでは無効化不可 |
% |
変数区切り | 不可 | "%%" または %% |
@echo off rem set "VAR=値" 形式で特殊文字を安全に格納 set "FILE=C:\work\report(2024).txt" set "MSG=処理完了 & バックアップ完了" rem 変数展開時はクォートで囲む echo "%FILE%" echo "%MSG%" rem echo で特殊文字を含む文字列を出力する場合は ^ でエスケープ echo ^(test^) ^& done rem 出力: (test) & done
set "VAR=値" 形式が必須な理由
変数への代入は set "VAR=値" 形式を必ず使うべきです。set VAR=値 の形式では複数の問題が発生します。
@echo off rem NG1: 行末スペースが変数に含まれる set VAR=hello if "%VAR%"=="hello" echo 一致 rem → "hello " == "hello" は偽になる rem NG2: 特殊文字が変数に含まれる場合に誤解析 set MSG=処理完了 & echo NG rem → & の後が別コマンドとして実行される rem OK: set "VAR=値" 形式 set "VAR=hello" set "MSG=処理完了 & バックアップ完了" if "%VAR%"=="hello" echo 一致 ← 正しく動作
setlocal/endlocal スコープと値の引き渡し
setlocal で始まるスコープ内の変数変更は endlocal で元に戻ります。サブルーチンや一時処理の結果をスコープ外に渡すには特別なテクニックが必要です。
@echo off call :get_result RESULT echo 結果: %RESULT% exit /b :get_result setlocal set "TEMP_RESULT=計算結果ABC" rem endlocal と set を & で同一行に書く rem endlocal が実行された瞬間に %%TEMP_RESULT%% が展開される endlocal & set "%~1=%TEMP_RESULT%" exit /b rem 出力: 結果: 計算結果ABC
endlocal & set "%~1=%TEMP_RESULT%" は1行で2つのコマンドを実行します。まず行全体が読み込まれ
%TEMP_RESULT% が展開されます(この時点ではまだ setlocal 内)。その後
endlocal が実行されスコープが戻り、続いて set が実行されます。%~1 には引数 RESULT(変数名の文字列)が入るため、親スコープの RESULT 変数に値を設定できます。変数の空判定の落とし穴
変数が空かどうかを判定する際、よく使われる if "%VAR%"=="" には落とし穴があります。
@echo off rem NG: VAR が未定義の場合は "%VAR%" → "" になり空判定できるが rem VAR に特殊文字("や&など)が含まれると構文エラーになる if "%VAR%"=="" echo 空です rem OK1: [] で囲む方法(特殊文字に強い) if [%VAR%]==[] echo 空または未定義です rem OK2: not defined で未定義チェック if not defined VAR echo 未定義です rem OK3: 遅延展開で空チェック setlocal enabledelayedexpansion if "!VAR!"=="" echo 空または未定義です endlocal
| 方法 | 未定義を空として扱う | 特殊文字に安全 | 推奨度 |
|---|---|---|---|
if "%VAR%"=="" |
◎ | △(" などで壊れる) | △ |
if [%VAR%]==[] |
◎ | ◎ | ◎(シンプル) |
if not defined VAR |
◎(未定義のみ) | ◎ | ◎(意図が明確) |
if "!VAR!"=="" |
◎ | ◎ | ◎(遅延展開利用時) |
条件分岐の詳しい使い方はバッチファイルで条件分岐する方法完全ガイドを参照してください。
for /f と遅延展開の組み合わせ
for /f でファイルを読み込みながら変数処理を行う場合、遅延展開と ! を含む行の処理に注意が必要です。
@echo off
setlocal enabledelayedexpansion
set COUNT=0
set "TOTAL=0"
rem ファイルの各行を読みながらカウント
for /f "usebackq delims=" %%L in ("data.txt") do (
set /a COUNT+=1
echo 行 !COUNT!: %%L
)
echo 合計行数: !COUNT!
endlocal
@echo off
setlocal enabledelayedexpansion
for /f "usebackq delims=" %%L in ("input.txt") do (
set "LINE=%%L"
rem ! を含む行を echo する場合は DisableDelayedExpansion に切り替え
setlocal disabledelayedexpansion
echo %LINE%
endlocal
)
endlocal
遅延展開が効かないケースと対処
遅延展開を有効にしていても思い通りに動かないケースがあります。
@echo off setlocal enabledelayedexpansion set "DIR=C:\work" rem NG: バッククォート内は !VAR! が展開されない場合がある for /f "usebackq delims=" %%F in (`dir /b "!DIR!"`) do echo %%F rem OK: 事前に変数に展開してから使う set "CMD=dir /b "%DIR%"" for /f "usebackq delims=" %%F in (`!CMD!`) do echo %%F rem または %VAR% で先に展開しておく(ブロック外なら動く) for /f "usebackq delims=" %%F in (`dir /b "%DIR%"`) do echo %%F
@echo off
setlocal enabledelayedexpansion
rem set /a は !VAR! も %VAR% も両方使える(どちらでも最新値を参照)
set NUM=10
for %%i in (1 2 3) do (
set /a NUM+=%%i
echo NUM=!NUM!
)
rem 出力: NUM=11, NUM=13, NUM=16
デバッグ方法:展開を可視化する
変数展開の問題をデバッグする際は、以下の方法でコマンドの実際の状態を確認します。
@echo off rem 1. @echo on でコマンド展開後の内容を表示 @echo on set X=hello world echo %X% @echo off rem → [echo hello world] と展開後が表示される rem 2. echo でブラケット付き表示(スペースを視覚化) set "VAR= spaced " echo [%VAR%] rem → [ spaced ] ← 前後のスペースが見える rem 3. cmd /v:on でコマンドラインから遅延展開を一時テスト rem cmd /v:on /c "set X=test & echo !X!" rem 4. 遅延展開で変数の内容を確認 setlocal enabledelayedexpansion set "TARGET=" echo [!TARGET!] rem 出力: [] ← 空変数の確認 endlocal
よくある変数展開トラブルのパターンまとめ
| 症状 | 原因 | 解決策 |
|---|---|---|
| FORループ内で変数が更新されない | %VAR% がブロック開始時に一括展開 | setlocal enabledelayedexpansion + !VAR! |
| IF-ELSEブロック内で変数が変わらない | 同上(ブロック一括展開) | 同上 |
echo 100% が echo 100 になる |
%が変数区切りとして解釈される | echo 100%% に変更 |
| ! を含む文字列が欠けて出力される | 遅延展開有効時に ! が変数区切りに | setlocal disabledelayedexpansion で一時無効化 |
| 変数に余分なスペースが入る | set VAR=値 の行末スペース |
set "VAR=値" 形式に変更 |
| set で & の後が別コマンドになる | & がコマンド区切りとして解釈 | set "VAR=値" 形式に変更 |
| 空変数判定が失敗する | %VAR% に特殊文字が含まれる |
if [%VAR%]==[] または if not defined VAR |
| endlocal 後に変数が消える | setlocal スコープの変数はリセットされる | endlocal & set "RESULT=%TEMP%" テクニック |
| call echo %%VAR%% で値が出ない | VAR 名のスペルミスまたは未定義 | set コマンドで変数一覧を確認 |
実践例:遅延展開を使った文字列処理スクリプト
ファイルリストから特定の文字列を含む行を抽出・集計する実用的なスクリプトです。
@echo off
setlocal enabledelayedexpansion
if "%~1"=="" (
echo 使い方: search_count.bat "キーワード" "ファイルパス"
exit /b 1
)
set "KEYWORD=%~1"
set "FILEPATH=%~2"
set COUNT=0
if not exist "%FILEPATH%" (
echo エラー: ファイルが見つかりません: %FILEPATH%
exit /b 1
)
echo 検索キーワード: !KEYWORD!
echo 対象ファイル: !FILEPATH!
echo ---
for /f "usebackq delims=" %%L in ("!FILEPATH!") do (
echo %%L | findstr /i "!KEYWORD!" >nul
if not errorlevel 1 (
set /a COUNT+=1
echo 行 !COUNT!: %%L
)
)
echo ---
echo ヒット数: !COUNT! 件
endlocal
よくある質問(FAQ)
setlocal enabledelayedexpansion を書いた後に変数を設定し直していますか?また、endlocal より後のコードでは遅延展開は無効になります。@echo on でコマンドの展開状態を確認するのが近道です。%%F、コマンドプロンプトから直接入力する場合は %F を使います。バッチファイルでは % が変数区切りとして処理されるため、FORループ変数には %% が必要です。setlocal enabledelayedexpansion を推奨します。読みやすく、パフォーマンスも良いです。call echo は遅延展開が使えない事情がある場合(例: ! を含む文字列の処理)の代替手段です。! が変数区切りとして解釈されています。! を含む文字列を処理する箇所だけ setlocal disabledelayedexpansion で一時無効化し、ループカウンタなど遅延展開が必要な箇所は enabledelayedexpansion のスコープで処理するよう分割してください。endlocal & set "%~1=%TEMP_RESULT%" の展開順を確認してください。%TEMP_RESULT% は行全体の読み込み時(endlocal 実行前)に展開されるため、setlocal スコープ内の値が参照されます。@echo on で展開後の文字列を確認してデバッグしてください。set コマンドを引数なしで実行すると現在の全変数一覧が表示されます。特定の変数だけ確認するなら set VAR(= なし)で VAR で始まる変数を一覧できます。また echo [%VAR%] のようにブラケットで囲むと前後のスペースも視覚化できます。まとめ
バッチファイルの変数展開トラブルを解決するポイントをまとめます。
- FORやIFのブロック内で変数を更新・参照するには
setlocal enabledelayedexpansion+!VAR! - 変数への代入は常に
set "VAR=値"形式(行末スペース・特殊文字対策) - バッチファイル内の
%のリテラルは%%、FORのループ変数は%%F - 遅延展開有効時に
!を文字として使うには一時的にdisabledelayedexpansion - 特殊文字(
& | > < ^)はクォートか^でエスケープ endlocal & set "RESULT=%TEMP%"でスコープ外に値を引き渡す- デバッグは
@echo onでコマンド展開後の状態を確認する
変数展開の仕組みを理解することで、バッチファイルの「なぜか動かない」問題のほとんどに対処できます。環境変数の基礎についてはバッチファイルの環境変数完全ガイド、FORループの使い方はFOR文の使い方完全ガイドも合わせて参照してください。

