遅延展開される CMD バッチスクリプトで ! を記述する方法

Windowsコマンドプロンプト CMD において、環境変数に遅延展開というものがあることを知ってる人も多いと思う。
CMD は、コマンドを1ステートメントづつ実行するため、IF 文や FOR 文の中で環境変数を扱うと、嬉しくないことが起こる。たとえば、

SET a=1
SET b=1
IF "%a%"=="1" (
    SET b=2
    ECHO a=%a%, b=%b%     ……(x)
) ELSE (
    ECHO a=%a%, b=%b%
)
ECHO a=%a%, b=%b%

このようなバッチスクリプトを実行すると、

1: SET a=1
2: SET b=1
3: IF "1"=="1" (
 :     SET b=2
 :     ECHO a=1, b=1     ……(x)
 : ) ELSE (
 :     ECHO a=1, b=1
 : )
4: ECHO a=1, b=2

というステートメントに展開される。このことを知らない人は (x) の部分で b=2 となることを期待するかもしれないが、環境変数ステートメント単位で展開されてから実行される都合で、上記のような状態になる。SET b=2 が実行されないわけではないので、次のステートメントである4行目では、%b% は 2 に展開される。
一般的なプログラム言語の変数のように、環境変数を展開するためには遅延展開を用いる。

SETLOCAL ENABLEDELAYEDEXPANSION
SET a=1
SET b=1
IF "%a%"=="1" (
    SET b=2
    ECHO a=!a!, b=!b!     ……(x)
) ELSE (
    ECHO a=!a!, b=!b!
)
ECHO a=%a%, b=%b%

遅延展開は、% のかわりに ! で囲まれた環境変数名を、実行単位で評価してくれる。さて、バッチスクリプト中で % を表記するためには、%% とかけばよく、| や > のような入出力に関する制御文字は ^ を添えて ^> などと記載することができる。では、遅延展開が有効な場合に ! を記載するにはどうすればいいだろうか?

SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
ECHO !! → ECHO は <ON> です
ECHO ^! → ECHO は <ON> です

何が起こっているのだろうか?

SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
ECHO !!foo!!  → ECHO は <ON> です
ECHO ^!bar^!  → ECHO は <ON> です

もう少し試してみる。

SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET foo=test
ECHO !!foo!!     → test 
ECHO ^!foo^!     → test
ECHO foo!!bar    → foobar
ECHO foo^!bar    → foobar
ECHO !!foo!!bar  → testbar
ECHO foo^!bar^!  → foo

環境変数の遅延展開では、余った ! は捨てられるということがわかる。google で検索すると英語圏の MVP の方が ^! で回避できるようなことを書いているが、実際は上記の通りである。しかし、例示されているサンプルや文章を見ると「文字列中では」というような記載がある。

SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
SET foo=test
ECHO "foo^!bar"     → "foo!bar"
ECHO "^!foo^!bar^!" → "!foo!bar!"

見事な結果。用途としては、7-Zip で差分圧縮を実行するためであったので、クオートが ! の外側につくのは使いにくい。というわけで、拡張機能 (ENABLEEXTENSIONS のほうで有効になる)を利用して、

SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
FOR /F %%F IN ( backup.cfg ) DO (
    SET BASE_FILE=%%F_full.7z
    SET DIFF_FILE="^!%%F_diff.7z"
    7z u !BASE_FILE! -u- -up0q3x2z0!DIFF_FILE:~! %%F 1>NUL 2>>err.log

みたいなかんじに ~ を使ってクオートを削除しました。その後で気がついたのですが、

ECHO ^^!

と、^ をエスケープして遅延展開前のステートメント解析のときに ^ が消えないようにする方法でも対応できました。このことから、コマンドラインの処理では、遅延展開が有効なときの ! は ^! と記載すればエスケープできることと、

という順序で実行されることがわかりました。文字列中で ^! を記載するというのは、^ によるエスケープが文字列中では必要ない(文字列中でリダイレクトを行ったりすることができない)ことを利用していたのであって、実際は c-1. の処理時点で ^! という表現が残っていればよかったということですね。