†DOSとバッチ†

遅延環境変数展開

作成開始日 2018.03.04
最終更新日 2018.03.10

バッチにおける環境変数は、他のプログラミング言語の《定数》に近い存在で、バッチ中で自由に書き換えることができない(正確には、書き換えは可能だが参照できない)。環境変数を自由に読み書き可能にするには、遅延間変数展開を有効にする必要がある。なお、遅延展開する変数を参照する時は「%〜%」ではなく「!〜!」で括る。このため、文字列中の「!」が正常に扱えなくなるという欠点もある(別項参照)。
@echo off
setlocal enabledelayedexpansion		←遅延環境変数展開を有効に

set st=ABC
set st=!st!X
echo !st!				←ABCXと表示される

endlocal
ただし、遅延展開を有効にしなくても、環境変数の書き換えは可能なことがある。実は、上記のサンプルも、遅延展開が無効でも同じように機能する。しかし、ループ処理などが入ると、両者ははっきりと違った振る舞いをする。たとえば、遅延展開が有効になっていないと;
@echo off
set st=ABC
for /L %%n in (1,1,10) do (
  set st=%st%X
  echo %st%
)
ABCX
ABCX
……
ABCX
と、同じ文字列が10回表示されるだけだが、遅延展開を有効にすると;
@echo off
setlocal enabledelayedexpansion
set st=ABC
for /L %%n in (1,1,10) do (
  set st=!st!X
  echo !st!
)
endlocal
ABCX
ABCXX
ABCXXX
……
ABCXXXXXXXXX
のように累積的に変化する。通常、このアルゴリズムであれば、こちらの動作を期待するだろう。
なお、用途は限定的だが、遅延展開を有効にしないでも、ループ処理中の環境変数の値を変化させる裏技もある(別項参照)。

●遅延展開なしの場合の環境変数の振る舞い

ここでは、通常の環境変数にどんな問題があるか確認しておく。 例として、拡張子「.txt」のファイルを「.doc」にリネームする処理を考える(「ren *.txt *.doc」に相当)。
for %%f in (*.txt) do (
  set orig=%%f
  set newf=%orig:.txt=.doc%	←文字列置換処理
  ren "%orig%" "%newf%"
)
このアルゴリズム自体は至って単純なもので、拡張子が「.txt」ファイルを見つけ、そこから拡張子を「.doc」に変更したファイル名を生成し、「…….txt」→「…….doc」とリネームしている。が、このバッチを実行しても、期待したような結果は得られない。以下、実際にバッチを実行して結果を確認してみる。ただし、リネーム処理は少々厄介なので、単にorigとnewfの中身を表示するだけのスクリプトに書き換えた。
@echo off			←結果を見易くするため
for %%f in (*.txt) do (
  set orig=%%f
  set newf=%orig:.txt=.doc%
  echo "%orig%" "%newf%"	←renをechoに置き換えた
)
で、これを実行すると、初回は;
"" ""
"" ""
………
"" ""
つまり、%orig%も%newf%も空白とみなされてしまっている。
じゃあ、本当に空白なのかと、コマンドラインで確認すると;
D:\work>@echo %orig%	←D:\workはカレントディレクトリ
file3.txt		←該当する最後のファイル名

D:\work>@echo %newf%
.txt=.doc
のように、全然空白ではないのである。%newf%の値がおかしいような気もするが、ともかく空白ではない。
では、同じコマンド窓で、もう一度同じバッチを再実行すると;
"file3.txt" ".txt=.doc"
"file3.txt" ".txt=.doc"
……
"file3.txt" ".txt=.doc"
と、先ほどコマンドラインで確認した値が、まったく変化せずに該当するファイルの数だけ出力される。
さらに、もう一度実行すると;
"file3.txt" "file3.doc"
"file3.txt" "file3.doc"
……
"file3.txt" "file3.doc"
今度は置換処理の行われたファイル名が出力されるが、ファイル名はすべて同じ(最後のファイル名)である。
以後、何度繰り返しても、同じ結果になる。何が起きているのだろうか?

まずは、初回実行時を考えみよう。この場合、環境変数orig、newfは未定義状態=ブランクである。したがって、このバッチは以下のように展開されて実行される。

@echo off
for %%f in (*.txt) do (
  set orig=%%f
  set newf=.txt=.doc		←%orig%は空白に展開、「:」は機能文字で、ここでは無効
  echo "" ""			←%orig%も%newf%も空白に展開
)
ポイントは、forループ内のset命令(set orig=…/set newf=…)自体は実行されているのだが、echo出力が空白に固定されてしまっているため、origやnewfの値がどう変化しようと、echo出力には何の関係もなくなってしまっている点。すなわち、このバッチが終了した時点では、やはりorigには最後のファイル名(ここでは「file3.txt」)が入り、newfには「.txt=.doc」という文字列が入っているのである。

したがって、2回目に実行したときには、この値が引き継がれ;

@echo off
for %%f in (*.txt) do (
  set orig=%%f
  set newf=file3.txt:.txt=.doc		←この表記では文法的には機能しない
  echo "file3.txt" ".txt=.doc"
)
となる。今回はecho出力が「file3.txt」「.txt=.doc」に固定されてしまう。
一方、「set newf=%orig:.txt=.doc%」の「%orig」が「file3.txt」に展開されるので、newfの値は「file3.doc」となる。
3回目以降は、この「file3.txt」と「file3.doc」が初期値となり、echo出力で固定されるので、ずっとこの値が表示される。
@echo off
for %%f in (*.txt) do (
  set orig=%%f
  set newf=file3.txt:.txt=.doc		←この表記では文法的には機能しない
  echo "file3.txt" "file3.doc"
)
もちろん、この処理を遅延環境変数展開を用いて書き直せば、期待通りに機能する。
@echo off
setlocal enabledelayedexpansion
for %%f in (*.txt) do (
  set orig=%%f
  set newf=!orig:.txt=.doc!
  echo "!orig!" "!newf!"
)
endlocal


【DOSとバッチ目次】 【ホーム】