最近会社で聞かれて答えたことをまとめてみました。
事の始まり
ある定期的な処理が必要になって、それをバッチファイルで作っているんだという話を聞きました。定期的に状態監視を行い、その後、一定期間待機するというものでした。大まかには以下のような処理になっていました。
@ECHO OFF
SETLOCAL
:LOOP
CALL :監視処理
TIMEOUT /T 30
GOTO LOOP
:監視処理
SET MSG=%TIME% 何かを監視しました
ECHO %MSG%
ECHO %MSG% >> 監視ログ.TXT
EXIT /B 0
バッチファイルを実行している最中、「30秒間隔で実行しているのは短すぎる。1分間隔で実行するようにしよう。」と話をしながら、"TIMEOUT /T 60"に変更して上書き保存、引き続いて話をしていました。
しばらくすると、その人が首をかしげていました。「あれ?バッチファイルを変更してから再起動したかな?」「いつの間にか処理間隔が1分に伸びている」と聞かれました。
なるほど。これは説明が必要なようだとなって、説明しました。そのとき説明したことをまとめてみます。
バッチファイルの読み込みタイミングを実験する
実験1
冒頭に書いた処理を使って、同じことをやってみましょう。冒頭の処理をバッチファイルにして実行します。"監視ログ.TXT"に書き出されていることを確認します。バッチファイルを実行したままで、"TIMEOUT /T 30"と書かれた個所を"TIMEOUT /T 60"に変更して、上書き保存を行います。すると"監視ログ.TXT"はこんな感じになります。
:
12:17:58.20 何かを監視しました
12:18:28.19 何かを監視しました
12:18:58.19 何かを監視しました
12:19:28.14 何かを監視しました
12:19:58.13 何かを監視しました
12:20:58.19 何かを監視しました
12:21:58.21 何かを監視しました
12:22:58.17 何かを監視しました
:
"12:19:58"よりも上は約30秒ごとに、"12:19:58"よりも下は約60秒ごとに出力されていることがわかります。つまり、バッチファイルは実行途中でも随時読み込まれることがわかります。
実験2
時間のかかる処理を混ぜてみましょう。以下のバッチファイルを作成してみます。
@ECHO OFF
ECHO 11111
PAUSE
ECHO 22222
PAUSE
ECHO 33333
PAUSE
実行すると、画面が以下のようになった状態でバッチファイルは一時停止します。
11111
続行するには何かキーを押してください . . .
ここでバッチファイルを以下のように変更してみます。2つめの表示内容を"22222"から"XXXXX"に変更しています。
@ECHO OFF
ECHO 11111
PAUSE
ECHO XXXXX
PAUSE
ECHO 33333
PAUSE
変更後に何かキーを押すと、画面は以下のように変化します。
11111
続行するには何かキーを押してください . . .
XXXXX
続行するには何かキーを押してください . . .
"PAUSE"で一時停止している間にバッチファイルの内容を変更しました。キーを押して再開すると、変更後のバッチファイルに従って実行されました。このように、バッチファイルは書かれた内容を実行するたびに読み直されます。
別パターンで試してみます。上のファイルを、以下の内容に変更してみます。
@ECHO OFF
ECHO 11111
PAUSE
ECHO XXXXX1234YYYYYECHO ZZZZZ
PAUSE
バイナリエディタで修正前後を確認すると、以下のようになっています。確認にはStirlingを使用しました。
修正前はこうです。バッチファイルは2度目のPAUSEで一時停止しています。アドレス"00000031"から再開する予定になっています。
ADDRESS 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0123456789ABCDEF
00000000 40 45 43 48 4F 20 4F 46 46 0D 0A 45 43 48 4F 20 @ECHO OFF..ECHO
00000010 31 31 31 31 31 0D 0A 50 41 55 53 45 0D 0A 45 43 11111..PAUSE..EC
00000020 48 4F 20 58 58 58 58 58 0D 0A 50 41 55 53 45 0D HO XXXXX..PAUSE.
00000030 0A 45 43 48 4F 20 33 33 33 33 33 0D 0A 50 41 55 .ECHO 33333..PAS
00000040 53 45 0D 0A SE..
修正後はこうです。アドレス"00000031"からの内容は"ECHO ZZZZZ"に書き換えられています。但し、直前の改行コードは"YY"で塗りつぶされています。そのため、"ECHO ZZZZZ"部分は直前行の"ECHO XXXXX~"に続く引数の一部でしかありません。
ADDRESS 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0123456789ABCDEF
00000000 40 45 43 48 4F 20 4F 46 46 0D 0A 45 43 48 4F 20 @ECHO OFF..ECHO
00000010 31 31 31 31 31 0D 0A 50 41 55 53 45 0D 0A 45 43 11111..PAUSE..EC
00000020 48 4F 20 58 58 58 58 58 31 32 33 34 59 59 59 59 HO XXXXX1234YYYY
00000030 59 45 43 48 4F 20 5A 5A 5A 5A 5A 0D 0A 50 41 55 YECHO ZZZZZ..PAU
00000040 53 45 0D 0A SE..
実行を再開すると、以下のようになります。
11111
続行するには何かキーを押してください . . .
XXXXX
続行するには何かキーを押してください . . .
ZZZZZ
続行するには何かキーを押してください . . .
行位置ではなくバイト位置をベースに、続きの処理が再開されることが確認できました。
今、最後の"PAUSE"で止まっていることになります。バッチファイルを削除あるいはリネームすると、どうなるでしょうか。リネームしてファイルが存在しない状態としてから再開します。
11111
続行するには何かキーを押してください . . .
XXXXX
続行するには何かキーを押してください . . .
ZZZZZ
続行するには何かキーを押してください . . .
バッチ ファイルが見つかりません。
"PAUSE"に続くコマンドがなくとも、続きに何が書かれているか、バッチファイルに書かれた内容を参照しに行っていることがわかります。そして、バッチファイルがないと処理を継続できないこともわかります。
このように、バッチファイルに書かれたコマンドが実行されるたびに、バッチファイルが読み直されていることがわかります。
実験3
次のバッチファイルはこうです。3回"PAUSE"する処理となっています。"PAUSE"の引数"XXX"は、桁数合わせのためにつけてあるものです。実行に影響を与えるような意味はありません。
@ECHO OFF
ECHO START
FOR /L %%I IN (1,1,3) DO PAUSE XXX
ECHO END
一回目の"PAUSE"で、以下の画面になります。
START
続行するには何かキーを押してください . . .
ここで以下のようにバッチファイルを変更します。回数を5回に変更し、ループ内で実行するコマンドを"ECHO"に変更しています。変更前のコマンドと行内のバイト数を合わせるため、ECHOの引数を"XXXX"に変えてあります。
@ECHO OFF
ECHO START
FOR /L %%I IN (1,1,5) DO ECHO XXXX
ECHO END
再開すると、以下の動きになります。
START
続行するには何かキーを押してください . . .
続行するには何かキーを押してください . . .
続行するには何かキーを押してください . . .
END
回数は3回のまま、ループ内で実行されるコマンドは"PAUSE"のままでした。FORを実行している間は、バッチファイルは読み直されていません。
実験4
では、ループ内をCALLにするとどうなるでしょう。上と同じことをするなら、修正前はこうです。
@ECHO OFF
ECHO START
FOR /L %%I IN (1,1,3) DO CALL :DO
ECHO END
EXIT /B 0
:DO
PAUSE
EXIT /B 0
修正後はこうです。
@ECHO OFF
ECHO START
FOR /L %%I IN (1,1,5) DO CALL :DO
ECHO END
EXIT /B 0
:DO
ECHO XXXX
EXIT /B 0
一度目の"PAUSE"のタイミングでバッチファイルを変更すると、以下の動きとなりました。
START
続行するには何かキーを押してください . . .
XXXX
XXXX
END
ループ回数は3回のままです。しかし、ループ内で処理される内容は"ECHO"に変わりました。ラベルの呼び出し先を実行するために、バッチファイルを読み直すようです。
実験5
ループ内を()で囲んでみます。さらに遅延変数展開をかけるようにします。修正前はこうです。1から3までを加算していく処理になっています。
@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET SUM=0
FOR /L %%I IN (1,1,3) DO (
SET /A SUM=!SUM!+%%I
ECHO XXXXX !SUM! XXXXX
PAUSE
)
ENDLOCAL
修正後はこうです。
@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
SET SUM=0
FOR /L %%I IN (1,1,5) DO (
SET /A SUM=!SUM!+%%I
ECHO YYYYY !SUM! YYYYY
PAUSE
)
ENDLOCAL
ループ回数が5回に、ループ内で表示している文字列が"XXXXX"から"YYYYY"に変わっています。一度目の"PAUSE"のタイミングでバッチファイルを変更すると、以下の動きとなりました。
XXXXX 1 XXXXX
続行するには何かキーを押してください . . .
XXXXX 3 XXXXX
続行するには何かキーを押してください . . .
XXXXX 6 XXXXX
続行するには何かキーを押してください . . .
ループ回数が変わらないのは多分予想できると思います。"FOR ~ DO ( ~ )"の場合、カッコ内は途中でバッチファイルを書き換えても変わらないということになります。言い換えると、"FOR ~ DO ( ~ )"の部分はコマンド実行時点で確定しているということになります。
この話、どこかで聞いたことはないでしょうか。実験5の冒頭でヒントがあった、遅延変数展開です。
遅延変数展開の使いどころはこうです。FORが実行されるタイミングで変数展開され、ループ内で実行されるコマンドが確定してしまっている。さらにループ内でも変数展開をさせたいから遅延変数展開を行うというものです。
"遅延でない"変数展開を行うタイミングで、FOR全体の実行コマンドが確定し、ループ回数およびループ内で実行されるコマンドが確定します。FOR全体の実行コマンドが確定する、その直前でバッチファイルが読み込まれるように思います。
まとめ
WindowsのEXE、Linuxのシェルスクリプトなど、起動タイミングですべてをメモリ上に載せるというパターンをよく見る中、バッチファイルはちょっと異質な手法を用いているように思います。
なお、PowerShellは起動タイミングですべて(かどうかはともかく少なくとも行以上の範囲)をメモリに載せているようです。以下のスクリプトを実行して"ReadKey()"で一時停止している間に"BBB"を"CCC"に変更しても、動作は変わりませんでした。
"AAA"
Write-Host "続行するには何かキーを押してください..."
$host.UI.RawUI.ReadKey() > $Null
"BBB"
まとめに対する追記 (2022/01/07)
Linuxのシェルスクリプトは起動タイミングですべてをメモリ上に乗せるわけではありません。会社で話をしているときに以下のURLを紹介されました。