こんにちは、おうどんです🍜
ななみさん、バッチで注文の数を数えるループ、できたよ。かけ、きつね、たぬき、全部 count=0 だけど
3杯数えて、3回とも0杯。お店が閉まりますね
でも、ループを出たあとは count=3 って出るんだよ。ちゃんと数えてはいるみたい
(会話は説明用に作ったものです)
数えているのに、表示は0のまま。
バッチファイル(Windows のコマンドプロンプト cmd.exe で動かす、拡張子 .bat・.cmd のスクリプト)で for や if を書くと、ほぼ全員が一度はここで止まります。
犯人は cmd の読み方です。
cmd は %count% を、コマンドを実行するときではなく、行を読んだときに値へ置き換えます。しかも ( から ) までのブロックは、何行あっても「1行」として丸ごと読む。
たとえるなら、cmd は写真係です。ブロックを読んだ瞬間に「はい、チーズ!」と全部の %count% を撮ってしまい、あとは撮った写真を見せ続ける。
この記事では、写真のままにしない書き方(遅延展開)と、そのときに ! が消える落とし穴、値をブロックの外へ持ち出す方法までまとめます。
検証は、今回の検証環境(Windows の cmd.exe、バージョン 10.0.19045)で、本文のバッチをすべて実行しました。
まず覚えるのは、この3つだけです
forやifの( )の中で変えた変数は、%count%ではなく!count!で読む!count!を使うバッチは、先頭近くにsetlocal enabledelayedexpansionを書く- ファイル名やメッセージに
!が入るときは、遅延展開をオンにしたままにしない(後半で説明)
forの中の%count%が0のまま:症状を再現する
まずは冒頭のループです。count.bat として保存して、コマンドプロンプトで実行しました。
@echo off
set count=0
for %%f in (kake kitsune tanuki) do (
set /a count+=1
echo in loop: %%f count=%count%
)
echo after: count=%count%
in loop: kake count=0
in loop: kitsune count=0
in loop: tanuki count=0
after: count=3
ループの中は3回とも0、ループを出たら3。set /a count+=1 はちゃんと効いています。表示だけが、for の行を読んだ瞬間の0で止まっている。
じゃあ、echo が古いんですね。新しい echo に替えましょう
echo に新旧はありません。古いのは、echo に渡された写真のほうです。
if のブロックでも同じことが起きます。
@echo off
set menu=kake
if "%menu%"=="kake" (
set menu=kitsune
echo inside: %menu%
)
echo outside: %menu%
inside: kake
outside: kitsune
ブロックの中で「きつねに変えた」直後に表示しても、まだ「かけ」。外に出ると、きつねになっています。
set /? のヘルプ(コマンドプロンプトで set /? と打つと出る説明)には、まさにこの例が載っています。if の中に if がある例で、両方の %VAR% が「最初の IF 文を読み取ったときに展開されます」と書かれている。( ) でくくった中身は、まとめて1つの文として読まれ、そのときに % の変数が全部置き換わる、ということです。
つまり、カッコの中では時間が止まってる
時間は進んでいます。止まっているのは、写真に写った値だけ。set はちゃんと仕事をしていて、変わった値はブロックが終わってから見えるようになります。
setlocal enabledelayedexpansionと!count!で直す
直し方は、変数の両側を % ではなく ! にすることです。これを遅延展開(変数の置き換えを、行を読むときではなく、そのコマンドを実行するときまで遅らせること)といいます。
既定ではオフなので、setlocal enabledelayedexpansion でオンにします。
@echo off
setlocal enabledelayedexpansion
set count=0
for %%f in (kake kitsune tanuki) do (
set /a count+=1
echo in loop: %%f count=!count! percent=%count%
)
echo after: count=!count!
endlocal
in loop: kake count=1 percent=0
in loop: kitsune count=2 percent=0
in loop: tanuki count=3 percent=0
after: count=3
!count! は1、2、3と進み、同じ行に並べた %count% は0のまま。
Microsoft Learn の cmd のページにも、遅延展開を有効にすると ! で環境変数の値を実行時(run time)に置き換えられる、と書かれています。
写真係の cmd さんに、生中継のカメラを持たせた
じゃあ、全部の変数を ! にしておけば安心ですね
安心ではない場面があります。それが後半の「! が消える」話です。
ブロックの外では %count% でもちゃんと今の値になるので、! が必要なのは「同じブロックの中で変えて、同じブロックの中で読む」ときだけ、と覚えておけば十分です。
%%f のほうは、最初から1回ごとに違う値になっていました。for の変数(%%f)は環境変数とは別物で、for が回るたびに中身を差し替えてくれます(Microsoft Learn の for のページ)。写真ではなく、毎回お皿ごと入れ替わる係です。
お皿ごと入れ替わるなら、洗い物が大変そうですね
洗うのは for さんなので、こちらは食べるだけです。
🔰 ここまで読めば今日から困らない( ) のブロックの中で変えた変数を、同じブロックの中で読むときは、setlocal enabledelayedexpansion を書いて %count% を !count! にする。ブロックの外や、for の変数(%%f)はそのままで大丈夫です。
ここから先は中級者向け:ブロックの中で困る場面と書き方
遅延展開を知っていても、ブロックの中では細かい落とし穴がいくつもあります。
set /a は、% を付けなければ今の値で計算する
冒頭のループで、表示は0なのに最後は3になっていました。set /a count+=1 が、写真ではなく今の値を使っているからです。
@echo off
set a=0
set b=0
for %%f in (kake kitsune tanuki) do (
set /a a+=1
set /a b=%b%+1
)
echo a=%a% b=%b%
a=3 b=1
set /a は、式の中の数字でない文字列を環境変数の名前として扱い、その値を数に変えてから計算します(Microsoft Learn の set のページ)。だから a+=1 はブロックの中でも今の値を読む。
一方 b=%b%+1 は、読んだときに b=0+1 に置き換わってしまうので、3回とも「0+1」。結果は1です。
b さんは、3回とも同じ答えを書いてますね
テストで、毎回1問目の答えを書き写してる子だ
set /a の中では % を付けない。これだけで、遅延展開なしでも数え上げはできます。
終了コードは if errorlevel か !errorlevel! で見る
ブロックの中でコマンドを動かして、その終了コード(プログラムが終わるときに返す番号。0 が成功)を見るときも同じです。
@echo off
setlocal enabledelayedexpansion
for %%c in (0 3) do (
cmd /c exit %%c
echo code=%%c percent=%errorlevel% bang=!errorlevel!
if errorlevel 1 (echo if errorlevel 1: yes) else (echo if errorlevel 1: no)
)
endlocal
code=0 percent=0 bang=0
if errorlevel 1: no
code=3 percent=0 bang=3
if errorlevel 1: yes
終了コード3のときも、%errorlevel% は0のまま。これで「失敗したら止める」を書くと、失敗を見逃します。if errorlevel 1 は、直前のプログラムの終了コードが1以上なら真になる書き方で(Microsoft Learn の if のページ)、写真を使わずにその場で判定してくれます。!errorlevel! でも今の値が取れます。
失敗したのに、写真の中ではずっと笑顔
写真の笑顔を信じて出荷すると、あとで大変なことになります。終了コードの基本は終了コードの基礎講座にまとめています。
ブロックの中で文字列をつなぐ
一覧を1つの変数につないでいくときは、つなぐ側も ! にします。
@echo off
setlocal enabledelayedexpansion
set list=
for %%f in (kake kitsune tanuki) do (
if defined list (set "list=!list!,%%f") else (set "list=%%f")
)
echo list=!list!
endlocal
list=kake,kitsune,tanuki
if defined list は、変数が定義されているときに真になります(Microsoft Learn の if のページ)。最初の1つだけ , を付けない、という定番の書き方です。
遅延展開を使わない書き方もある
call で、ラベルの付いたサブルーチン(バッチの中の小さな処理のまとまり)に1回ずつ飛ばす方法です。
@echo off
set count=0
for %%f in (kake kitsune tanuki) do call :add %%f
echo after: count=%count%
goto :eof
:add
set /a count+=1
echo in sub: %1 count=%count%
goto :eof
in sub: kake count=1
in sub: kitsune count=2
in sub: tanuki count=3
after: count=3
:add の中は ( ) のブロックではないので、1行ずつ読まれます。%count% でもその都度の値になる。
処理が長くなるなら、こちらのほうが読みやすいこともあります。! の落とし穴とも無縁です。
もう1つ、call echo %%count%% という書き方もネットでよく見かけます。
@echo off
set count=0
for %%f in (kake kitsune tanuki) do (
set /a count+=1
call echo call echo: %%count%%
)
call echo: 1
call echo: 2
call echo: 3
今回の検証環境では、これでも今の値が出ました。バッチを読んだときに %%count%% が %count% になり、call が実行するときにもう一度置き換えている、と読める結果です(この仕組みは、今回確認した Microsoft の資料には書かれていません)。
動きはしますが、何をしているかが見た目で分かりにくいので、新しく書くなら遅延展開か call :ラベル をおすすめします。
%% が2つも並んでると、割引のクーポンみたいですね
100%%オフ。たぶん、お店がつぶれる
!が消える:遅延展開をオンにしたときの落とし穴
遅延展開をオンにすると、! は「変数の印」になります。すると、ふつうの文字としての ! が消えることがあります。
@echo off
setlocal enabledelayedexpansion
for %%f in ("udon!.txt" "soba.txt") do (
echo file: %%~f
)
set "msg=Thank you!"
echo msg: %msg%
echo msg2: !msg!
endlocal
file: udon.txt
file: soba.txt
msg: Thank you
msg2: Thank you
ファイル名の udon!.txt が udon.txt に、メッセージの Thank you! が Thank you になりました。msg は %msg% で読んでも ! がないので、set した時点で、もう ! が落ちています。
うどんの ! がない。うどんの元気がなくなった
ファイル名が変わったら、コピーやリネームで別のファイルを探しに行きませんか
行きます。udon!.txt を扱うつもりのコマンドに udon.txt が渡るので、見つからないか、もっと悪いと別のファイルに当たります。
ファイル名を扱うループで、遅延展開を最初から最後までオンにしておくのが危ないのはこのためです。
^ で守れるが、数え方がややこしい
^(cmd のエスケープ文字。次の1文字を特別扱いしない印)で ! を守ることもできます。ただ、今回の検証環境では、書き方によって結果がこう変わりました。
@echo off
setlocal enabledelayedexpansion
echo 1: Hello^^!
echo 2: "Hello^!"
echo 3: Hello^!
endlocal
setlocal disabledelayedexpansion
echo 4: Hello^^!
echo 5: Hello!
endlocal
1: Hello!
2: "Hello!"
3: Hello
4: Hello^!
5: Hello!
遅延展開がオンのとき、引用符の外では ^^!、引用符の中では ^! で、やっと ! が残りました。Microsoft Learn の echo のページで ! を表示する方法として紹介されているのも、この2つの書き方です。
ところが、遅延展開がオフのときに同じ ^^! を書くと、今度は ^ が1つ残る(オフなら ! はそのまま書けばよい)。
同じ文字を守るのに、場所とモードで ^ の数が変わります。
^ の数を数えるのに、もう1本バッチを書きたい
その気持ちは分かりますが、もっと素直な方法があります。
必要なところだけ、遅延展開をオンにする
ファイル名などの値は、遅延展開がオフのうちに変数へ入れ、表示や比較のときだけ短くオンにします。
@echo off
setlocal disabledelayedexpansion
set count=0
for %%f in ("udon!.txt" "soba.txt") do (
set "name=%%~f"
set /a count+=1
setlocal enabledelayedexpansion
echo !count!: !name!
endlocal
)
endlocal
1: udon!.txt
2: soba.txt
set "name=%%~f" は遅延展開がオフのときに実行されるので、! はそのまま変数に入ります。そのあと遅延展開をオンにして !name! で読むと、値の中の ! は変数の印として読み直されず、そのまま表示されました。
ループの中の setlocal と endlocal は、必ず対にします。endlocal すると、その setlocal のあとに変えた変数は元に戻るので、オンの間に set したものはループの外に残りません(Microsoft Learn の setlocal のページ)。
カメラを、撮るときだけ持たせるんですね
そうです。ずっと生中継していると、! まで中継の合図だと思われてしまうので。
値をsetlocalの外へ持ち出す
setlocal から endlocal までに変えた変数は、endlocal で元に戻ります。バッチの最後まで来たときも、閉じていない setlocal の分だけ、自動で endlocal が実行されます(setlocal /? のヘルプ)。
計算した結果を外に残したいときは、endlocal と同じ行で set します。
@echo off
set total=
setlocal enabledelayedexpansion
set sum=0
for %%n in (3 5 7) do set /a sum+=%%n
echo inside: !sum!
endlocal & set "total=%sum%"
echo outside sum=[%sum%] total=[%total%]
inside: 15
outside sum=[] total=[15]
endlocal & set "total=%sum%" の行は、読んだ時点で %sum% が15に置き換わります。そのあと endlocal で sum は消えますが、set にはもう 15 が渡っている。
ここでは、さっきまで悪者だった「読んだときに置き換える」が、味方になっています。
写真係が、閉店まぎわに最後の1枚を撮ってくれた
写真、役に立つじゃないですか
ここが、この話のいちばんおもしろいところです。% の写真は、使う場所を選べば便利な道具なんです。
今回試したのは数字の持ち出しだけです。値に ! が入るもの(ファイル名など)を持ち出すときは、前の章のように ! が落ちることがあるので、受け渡しの行も含めて実際に試してから使ってください。
じゃあ、持ち出す前に ! を全部抜いておけば安心ですね
それは、うどんからねぎを全部抜いて出前するようなものです。届きはするけれど、頼んだものと違う。
そのまま使える:CSVをコピーして件数と終了コードを返すバッチ
ここまでを1本にまとめました。フォルダの中の .csv を別のフォルダにコピーし、1件ずつ結果を表示して、最後に件数と終了コードを返します。
ファイル名に ! や & が入っていても壊れないように、遅延展開は表示の1行だけでオンにしています。
@echo off
rem 使い方: copy_csv.bat コピー元フォルダ コピー先フォルダ
setlocal disabledelayedexpansion
if "%~2"=="" (
echo 使い方: %~nx0 コピー元フォルダ コピー先フォルダ 1>&2
exit /b 2
)
set "src=%~1"
set "dst=%~2"
set /a n=0, ok=0, ng=0
for %%A in ("%src%\*.csv") do if exist "%%~fA" (
set "name=%%~nxA"
set /a n+=1
copy /y "%%~fA" "%dst%\" >nul 2>&1
if errorlevel 1 (
set /a ng+=1
set "result=NG"
) else (
set /a ok+=1
set "result=OK"
)
setlocal enabledelayedexpansion
echo [!n!] !result! !name!
endlocal
)
echo 合計 %n% 件: OK %ok% / NG %ng%
if %ng% gtr 0 exit /b 1
exit /b 0
書き方のポイントです。
if exist "%%~fA"で、CSV が1件もないときにワイルドカード文字列を1件として数えないset /aの中は%なしで数える(ブロックの中でも今の値になる)- 成功・失敗の判定は
if errorlevel 1(%errorlevel%は写真になる) - 表示の行だけ
setlocal enabledelayedexpansionで囲み、endlocalで閉じる - 合計の行はブロックの外なので、
%n%でよい exit /b 1で、呼び出し元に終了コード1を返して失敗を伝える
試すために、src に kake.csv・kitsune!.csv・tanuki&ten.csv・memo.txt を置き、dst には読み取り専用の tanuki&ten.csv を先に置いて、1件だけコピーに失敗するようにしました。
呼び出す側のバッチ(check.bat)はこうです。
@echo off
call "%~dp0copy_csv.bat" src dst
echo exit=%errorlevel%
call "%~dp0copy_csv.bat"
echo exit=%errorlevel%
標準出力(画面に出る通常の出力)はこうなりました。
[1] OK kake.csv
[2] OK kitsune!.csv
[3] NG tanuki&ten.csv
合計 3 件: OK 2 / NG 1
exit=1
exit=2
引数を付けなかった2回目は、標準エラー出力(エラー用の出力)に使い方が出ています。
使い方: copy_csv.bat コピー元フォルダ コピー先フォルダ
引数を忘れたら、使い方を言い残して終了コード2で帰る。礼儀正しい
帰り際に理由を置いていってくれるバッチは、夜中の障害対応でとても助かります。
kitsune!.csv の ! も、tanuki&ten.csv の & も、そのまま表示されました。memo.txt は *.csv に当たらないので数えていません。
読み取り専用を外して copy_csv.bat src dst を実行し直すと、全部成功しました。
[1] OK kake.csv
[2] OK kitsune!.csv
[3] OK tanuki&ten.csv
合計 3 件: OK 3 / NG 0
このときの終了コードは0でした(call で呼んで echo exit=%errorlevel% を出すだけのバッチで、exit=0 を確かめました)。
たぬきが1杯だけ失敗。天ぷらの & が悪いのかと思ったら、器が読み取り専用だった
器を疑う前に、天ぷらを疑うんですね
疑う順番は大事です。今回は & の扱いは正しく、失敗の理由は書き込み先でした。
robocopy でコピーする場合は終了コードの読み方が違う(1 も成功)ので、robocopy の終了コードの記事も見てください。
ブロックの中の変数のチェックリスト
( )の中で変えて、同じ( )の中で読む変数は!にしたset /aの式の中には%を付けていない- 終了コードは
if errorlevel 1か!errorlevel!で見ている - ファイル名など
!が入りうる値は、遅延展開がオフのときにsetしている - ループの中の
setlocalとendlocalが対になっている - 外に残したい値は
endlocal & set "x=%y%"で持ち出している
ここから先は上級者向け。読み飛ばしてもOKです:遅延展開はどこでオンになるのか
「自分のパソコンでは動くのに、ほかの人のパソコンでは !count! がそのまま出る」。こういうときは、遅延展開がどこでオンになっているかを疑います。
cmd /? のヘルプと Microsoft Learn の資料にある、遅延展開のオン・オフの決まり方をまとめます。
| 方法 | 効く範囲 | 根拠 |
|---|---|---|
| 何もしない | 既定はオフ | set /?・cmd /?、Learn の set |
cmd /v:on で起動 | その cmd の中 | cmd /?、Learn の cmd |
レジストリの DelayedExpansion(HKLM・HKCU の Software\Microsoft\Command Processor) | そのコンピューター・ユーザーで起動する cmd 全部。ユーザーの設定がコンピューターの設定より優先、コマンドラインのスイッチがレジストリより優先 | cmd /? |
バッチの中で setlocal enabledelayedexpansion | 対応する endlocal まで。cmd /v の指定より優先 | setlocal /?、Learn の setlocal |
レジストリでオンにしている人のパソコンでは、setlocal enabledelayedexpansion を書き忘れても動いてしまいます。
バッチを人に渡すなら、どこで動いても同じになるように、! を使うバッチには setlocal enabledelayedexpansion を必ず書く。逆に ! を文字として扱いたいバッチには setlocal disabledelayedexpansion を書いておくと、レジストリの設定に左右されません。
わたしのパソコンでは動きます、は、バッチでも言っちゃだめなやつ
おうどんさん、それ言ったことあります?
この記事には書きません。
setlocal はバッチの外では効かない
Microsoft Learn の setlocal のページには、スクリプトやバッチファイルの外で setlocal を使っても効果がない、とあります。cmd /c に直接書いて試しました。
cmd /d /c "set x=1&setlocal enabledelayedexpansion&echo [!x!]"
[!x!]
コマンドラインで遅延展開を使いたいときは、cmd /v:on で起動します。
setlocal は終了コードも変える
setlocal /? のヘルプによると、setlocal に引数を付けると ERRORLEVEL が設定されます(有効な引数なら0)。わざと0以外にしてから確かめました。
@echo off
verify other 2>nul
setlocal enabledelayedexpansion
echo errorlevel after setlocal with arg: %errorlevel%
endlocal
errorlevel after setlocal with arg: 0
verify other は、わざと間違った引数で ERRORLEVEL を0以外にする、ヘルプに載っている手です。そのあとの setlocal enabledelayedexpansion で0に戻りました。
直前のコマンドの終了コードを見たいときは、間に setlocal を挟まないようにします。
%VAR% と !VAR! と %%f の違い
| 書き方 | 置き換わるとき | ブロックの中で | 主な根拠 |
|---|---|---|---|
%VAR% | 行(ブロックなら全体)を読んだとき | 読んだ時点の値のまま | set /? |
!VAR! | そのコマンドを実行するとき(遅延展開がオンのときだけ) | 今の値 | Learn の cmd、cmd /? |
%%f(for の変数) | for が回るたび | 1回ごとの値 | Learn の for |
set /a の式の中の名前 | set /a を実行するとき | 今の値 | Learn の set |
for の変数は大文字と小文字を区別し、0〜9 は使えません(%0〜%9 はバッチの引数と紛らわしいため)。%%~nxA のような修飾子と見分けやすいように、大文字の変数名にすると読みやすい、と Learn の for のページにあります。完成バッチで %%A にしたのはこのためです。
%%f の f と、%%~fA の f が別物なのは、ずるいです
ずるいので、変数のほうを大文字にして逃げました。
写真係にカメラを持たせるのは、必要な1行だけ
冒頭のループに戻ります。
ななみさん、直したよ。!count! にしたら、ちゃんと1、2、3って出た。ファイル名の ! が消えるのも、表示の行だけ遅延展開にして避けた
! が消える話まで先に調べてあるのは、さすがです。これなら人に渡せますね
ほめられた。この瞬間、写真に撮っておこう
% で撮ると、ほめられる前の顔が写るかもしれません。
cmd は、ブロックを読んだ瞬間に「はい、チーズ!」と % の変数を全部撮ってしまう写真係です。
悪気はありません。そういう読み方をする係なだけで、endlocal & set のように、その写真が役に立つ場面もある。
ブロックの中で変えて読む変数だけ、!VAR! で生中継にする。
カメラをずっと回しっぱなしにすると、今度は ! まで撮り忘れるので、回すのは必要な1行だけ。
まずは手元のバッチの ( ) の中から、% で読んでいる変数を1つ探してみてください🍜
参考資料
- Microsoft Learn:set — 遅延展開は既定で無効で cmd /v で切り替える、set /a は数字でない文字列を変数名として扱う、= の後ろがすべて値になる
- Microsoft Learn:setlocal — enabledelayedexpansion・disabledelayedexpansion は対応する endlocal まで、endlocal やバッチの終わりで元に戻る、入れ子にできる、バッチの外では効果がない、ERRORLEVEL の設定
- Microsoft Learn:cmd — /v:on で遅延展開を有効にする、! で実行時に値を置き換える
- Microsoft Learn:for — バッチでは %%、for の変数は大文字小文字を区別、0〜9 は使えない、修飾子と大文字の変数名
- Microsoft Learn:if — if errorlevel は終了コードが指定の数以上で真、if defined、if exist、else は同じ行に書く
- Microsoft Learn:echo —
!を表示する書き方(引用符の中の^!、引用符なしの^^!) - cmd のヘルプ(
set /?・cmd /?・setlocal /?。今回の検証環境のコマンドプロンプトで表示したもの) — % の変数は行を読み取るときに展開される例(if の入れ子・for で一覧を作る例)、レジストリの DelayedExpansion と優先順位、setlocal の引数が cmd /v より優先、バッチの終わりの暗黙の endlocal
2026年11月時点の内容です。








コメント