バッチファイルでSET文が効かない場合の原因と対策

はじめに

Windowsのバッチファイルでは、変数に値を設定するときにSET文という命令を使う。
例えば、a という変数に100を設定する場合には以下のように記述する。

SET a=100

このように記述することで、バッチファイルのそれ以降の行で 変数a を参照すると100が取得できる。

ところが、ある条件下ではこの当たり前としか思えないことが成立しないのだ。
上記の例で言えば、 a の値が100を設定する以前の状態のままになることがある。

一見するとバグではないかと疑ってしまうのだが、もしこのような動作がIF文やFOR文の( )内で発生した場合は、バグではなくてバッチファイルの仕様通りの挙動である可能性がある。

今回は、この一見不可解なバッチファイルの一風変わった仕様について紹介する。

サンプルプログラム

まずは上記現象が発生するサンプルを提示しよう。
以下はファイルのバックアップを行うプログラムだと想定していただきたい。

所定のディレクトリーに保存された today.dat というファイルを、バックアップ用のディレクトリーにコピーするプログラムだ。

rem 今日のデータをバックアップする
set fromDir=C:\work\original
set toDir=C:\work\backup
set fromFile=today.dat
set toFile=%fromFile%

rem バックアップ済みならファイル名に2を付ける
if exist %toDir%\%toFile% (
set toFile=%fromFile%-2
rem 2回バックアップ済みならファイル名に3を付ける
if exist %toDir%\%toFile% (
set toFile=%fromFile%-3
)
)
copy %fromDir%\%fromFile% %toDir%\%toFile%

バックアップ先に既にファイルが存在する場合、すなわち既にバックアップ処理が行われていた場合は、バックアップ済みのファイルを上書きしないように別のファイル名でバックアップを行う。
具体的には、ファイル名を「today.dat」から「today.dat-2」に変更してバックアップを行う。

さらに「today.dat-2」も存在する場合、すなわち既に2回目のバックアップ処理が行われていた場合は、ファイル名を「today.dat-3」に変更してバックアップを行う。

ソースをざっと眺めた限りでは仕様通りに動作しそうに思えるこのプログラムは、実際には思惑通りには動いてくれない。

1回目のバックアップ、すなわち「C:\work\backup」ディレクトリーが空の場合は正常に動作する。

3回目のバックアップ、すなわち「C:\work\backup」ディレクトリーに「today.dat」と「today.dat-2」が存在する場合も正常に動作する。

ところが、2回目のバックアップ、すなわち「C:\work\backup」ディレクトリーに「today.dat」のみが存在する場合は挙動がおかしくなる。
実際にバッチファイルを実行した後のバックアップ用ディレクトリーの状態は以下の通りとなる。

なぜか「today.dat-2」の前に「today.dat-3」が作られてしまうのだ。
この一見奇妙としか思えない現象が仕様通りだとは理解しがたいのだが、この理由を以下で紹介しよう。

遅延環境変数という仕組み

一見奇妙としか思えないこの現象は、遅延環境変数というバッチシステム独自の仕様に起因する。

バッチファイルでは、IF文やFOR文などの( )内のブロックで環境変数が使用された場合、それらが評価されるタイミングはそれらが使われるタイミングでは「なく」、ブロックに入ったタイミングになっているのだ。
すなわち、( )内のブロックで環境変数を変更した場合、それを同じブロック内で参照しても、変更前の値が参照されてしまうのだ。

もちろん、ブロックから外へ出たタイミングでは環境変数の変更は反映される。
そのため、2つ目のバックアップファイルの名前は「today.dat-3」と最初の状態から変更されている。

一方で、IF文のブロック内で「today.dat-2」の存在チェックをしているはずの箇所では、この遅延環境変数の仕組みのため、実際には変更前の「today.dat」の存在チェックが行われてしまう。
「today.dat」は存在するので、この2つ目のIF文の判定は真となり、バックアップのファイル名が「today.dat-3」になってしまうのだ。

遅延環境変数対策

プログラムが動作しない原因は分かったので、続いてはこの問題への対策を紹介する。

まずは、以下の一文をプログラムに追加しよう。

setlocal enabledelayedexpansion

この一文を記述すると、プログラムのそれ以降の行では、環境変数の評価タイミングを使用直前のタイミングに変更することが可能となる。

評価タイミングを使用する直前に変更するには、使用する環境変数名を「%」ではなく「!」で囲むように記述を変更する必要がある。

ちなみに従来通りに「%」で囲った場合は、相変わらず評価タイミングはIF文やFOR文のブロックに入ったタイミングのままとなる。
状況に応じて「%」と「!」を使い分ければよいのだ。

上記の知識を得たところで、前述のプログラムを書き直してみよう。
その結果は以下の通りだ。

rem 今日のデータをバックアップする
set fromDir=C:\work\original
set toDir=C:\work\backup
set fromFile=today.dat
set toFile=%fromFile%
setlocal enabledelayedexpansion
rem バックアップ済みならファイル名に2を付ける
if exist %toDir%\%toFile% (
set toFile=%fromFile%-2
rem 2回バックアップ済みならファイル名に3を付ける
if exist %toDir%\!toFile! (
set toFile=%fromFile%-3
)
)
copy %fromDir%\%fromFile% %toDir%\%toFile%

変更箇所は赤字で強調した2か所だ。
このようにすることで、2つ目のIF文で使われる変数「toFile」は、その2行上で変更された内容が反映されるようになる。

変数「toDir」や変数「fromFile」はブロック内で変更されないので「%」で囲った記述を変更する必要はない。

以下は、変更後のバッチファイルを実行した後のバックアップ用ディレクトリーの状態だ。
バックアップが仕様通りに動いたことが分かるだろう。

以上で、遅延環境変数対策の紹介は終了だ。

まとめ

WindowsのバッチファイルでSET文が機能していないように見えるケースについて、その理由と対策を紹介した。

バッチファイルは古い技術だが、いまだに使われている現場が多く存在する、技術者が避けて通れない技術でもある。
今回紹介した内容を覚えておいて損することはないだろう。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です