やんごとなき理由により、Windowsバッチ(.bat)ファイルを触る必要があるとします。
それなりの拡張性を求められたため、環境変数から動作を変更できるようにしました。
頑張ってコードを読み、複雑に入り組んだbat構造の一部を変更しました。
ところが、何故か思った通りの値が環境変数に入っていません。
直前に 1 を返す bat を call しているのに、errorlevel には 0 が入ってる…
…という問題を解決できるかもしれない記事です。
この記事の対象者
- 複雑なWindowsバッチでタイトルの現象に悩まされている方
- 将来的にこんな問題にぶち当たるかもしれない…と思っている方
問題となる例
main.bat というファイルがあり、このファイルは同じディレクトリに存在する sub1.bat, sub2.bat, … を呼んでいくものとします。
あなたは拡張性を高めるため、サブバッチをいくつ配置しても大丈夫なように、以下のファイルを作成しました。
@echo off
cd %~dp0
for %%i in (sub*.bat) do (
call %%i
echo %errorlevel%
)
Windowsバッチはあまり見慣れない方も多いと思うので、軽く解説します。
1行目で余計な echo(標準出力に文字列を出力)をオフにしています。
2行目で main.bat の置かれたディレクトリをカレントディレクトリにしています。
4行目以降は for ループ文で、カレントディレクトリにある「sub*.bat」というファイルを列挙し、「%%i」 という変数にセットしています。
5行目で %%i にセットされた sub*.bat というサブバッチを呼んでいます。
6行目で「errorlevel」という環境変数を出力しています。errorlevel には直前に呼んだバッチの戻り値が入る、という仕様になっています。
サブバッチは以下の2つを用意しておきます。
echo "sub1"
exit /b 1
echo "sub2"
exit /b 2
サブバッチの1行目で呼んだバッチ名を標準出力へ出力します(分かりやすさのため直書きしています)。
2行目で、バッチ番号を戻り値として終了しています。
さて、main.bat はサブバッチの戻り値を出力するような構造なので、実行すると「1」「2」という出力が得られるはずです。
ところが、これを実行すると以下のような出力が得られると思います。
"sub1"
0
"sub2"
0
問題とはならない例
ここで「サブバッチは2つしか存在しない」ということで、全く拡張性の無い以下のようなファイルを作成しました。
@echo off
cd %~dp0
call sub1.bat
echo %errorlevel%
call sub2.bat
echo %errorlevel%
これで main.bat を実行すると、なんと想定通りの結果を得ることができます。
>main.bat
"sub1"
1
"sub2"
2
解決策(「遅延環境変数」が鍵)
この現象ですが、「%VARIABLE%」という形式を使った場合、文の実行直前に環境変数が展開されるという仕様が原因で発生しています。
if や for は括弧で囲んだ範囲が一文になるため、今回の例ではfor文の中で環境変数の値は固定されてしまいます。
…で、これの解決策ですが、遅延環境変数というものを利用します。
@echo off
cd %~dp0
setlocal enabledelayedexpansion
for %%i in (sub*.bat) do (
call %%i
echo !errorlevel!
)
endlocal
最初の例から変わった点ですが、「setlocal enabledelayedexpansion」という行と、「endlocal」という行が増えました。
そして、分かりにくいですが echo 対象が %errorlevel% から「!errorlevel!」に変わっています。
setlocal – endlocal は元々局所的に変数を宣言したい場合に利用できます。
そこで「変数の遅延展開を許可する」という命令を付加しています。
実際に遅延展開される変数はエクスクラメーションマークで囲む必要があります。
以上で、タイトルの問題は解決できます。
まとめ
- if 文・for 文の中で環境変数を展開する場合、必要に応じて enabledelayedexpansion を使う。
- 遅延展開する変数はパーセント記号ではなくエクスクラメーションマークで囲む。
たいへんつらい仕様ですが、頑張っていきましょう…!
コメント