SetLocal EnableDelayedExpansionの罠とその回避方法


以下は、カレントディレクトリにあるtxtファイルについて、その内容を表示するというバッチファイルです(エラー判定でERRORLEVELを遅延展開させたいので、setlocal enabledelayedexpansionを呼出しています)。

簡単なバッチファイル
setlocal enabledelayedexpansion

for %%a in (*.txt) do (

    type "%%a" >nul 2>&1

    if !ERRORLEVEL! neq 0 (
        echo "[%%a] Error has occurred."
    ) else (
        echo "[%%a] is OK."
    )
)
endlocal

SetLocal EnableDelayedExpansionの罠

上記のバッチファイルは作り的に考慮漏れです。なぜなら、カレントディレクトリに「!xxx!.txt」のような名前のファイルが存在するとエラーが発生するからです。

実行結果
>check.bat
"[.txt] Error has occurred."

エラー発生時に%%aに代入されていたのは!xxx!.txtという文字列ですが、!xxx!の部分が遅延展開と認識されてしまうようです。

つまり、%%a!xxx!.txtに展開され、さらに!xxx!を展開しようとします。しかしxxxという変数は未定義であるため、!xxx!の展開結果はブランクとなります。そのなれの果てが.txtという文字列です。

パスワードやファイル名など、含まれる文字をコントロールできない変数を扱う場面では、事実上setlocal enabledelayedexpansionが使えなくなってしまいます。

では、どんな回避方法があるでしょうか。

回避方法

以下のように、SetLocal~EnableDelayedExpansionのスコープを最小限にしたり

回避方法1:スコープを最小限にする
for %%a in (*.txt) do (

    type "%%a" >nul 2>&1

setlocal enabledelayedexpansion
    if !ERRORLEVEL! neq 0 (
endlocal
        echo "[%%a] Error has occurred."
    ) else (
endlocal
        echo "[%%a] is OK."
    )
)

forブロック内の処理を関数に逃すというテもあります。こうすればSetLocal EnableDelayedExpansionを使う必要がなくなります。

回避方法2:関数に逃がす
for %%a in (*.txt) do (
    call :func "%%a"
)
exit /b 0

:func
    type "%~1" >nul 2>&1
    if %ERRORLEVEL% neq 0 (
        echo "[%~1] Error has occurred."
    ) else (
        echo "[%~1] is OK."
    )
exit /b 0

if ERRORLEVEL Nを使うのもよいです。

回避方法3:if_ERRORLEVEL_N
for %%a in (*.txt) do (

    type "%%a" >nul 2>&1

    if ERRORLEVEL 1 (
        echo "[%%a] Error has occurred."
    ) else if ERRORLEVEL 0 (
        echo "[%%a] is OK."
    ) else (
        echo "[%%a] Error has occurred."
    )
)

というわけですので、バッチファイル全体をSetLocal EnableDelayedExpansionで囲むなどという恐ろしいことをしてはなりません…。

余談

なお、値を関数の引数に渡すときに以下のように値をダブルクォートしているのは「ファイル名に空白文字が含まれたファイル」対策です。ダブルクォテーションが無いと、空白文字で区切られた複数の引数として認識されてしまうためです。

スペースをエスケープ
call :func "%%a"