バッチファイルのif文やfor文の中で、変数の値がおかしい問題


Y子です。
変数に格納したはずの値が、
if文やfor文の中で取得できなかったり、
・取得できても古い値のままだったり、
という病気にかかりました。

調べたところ、「遅延環境変数の展開を有効にする」というおまじないが必要だそうです。
ナニソレー
でもどうやら、ちょっと複雑なバッチファイルを書くためには、避けて通れない技だそうで…。

いろいろ試して、ようやくわかった気になりましたので、なんとかそれをまとめてみます。

【2021/04/12 19:50 追記】
時間がない人のための簡潔スッキリ版も書きました。ご活用ください。

概要

※煩雑になるので「if文」と書きますが、すべて「if文やfor文」と読み替え可能です※

if文の中で変数に値を格納し、その値を同じif文の中で取得するには、以下の注意が必要です。
%a%では取得不可 (%a%には、if文に入る前の値が入っています)
・事前にsetlocal enabledelayedexpansionを宣言すれば、!a!で取得可

if文中の変数%a%は、if文に入った瞬間に展開されます。
これに対し、参照されるまで変数の展開を遅延させることを「遅延環境変数の展開」といいます。
これを有効にする宣言が「setlocal enabledelayedexpansion」で、取得する変数が!a!です。

コード

(1) 成功例

以下のことを実施しています。
・遅延環境変数の展開を有効にする宣言
・値の「格納と取得」を、同じif文の中で行う変数は、!a!形式で取得
・それ以外の変数は、%a%形式で取得

delayedexpansion_if1_ok.bat
@echo off

rem 遅延環境変数の展開を有効にする
setlocal enabledelayedexpansion

rem if文の外で格納
set str_a=aaa

if 1==1 (
  rem if文の中で格納
  set str_b=bbb
  echo if文の中で取得 : "%str_a%" "!str_b!"
)
echo if文の外で取得 : "%str_a%" "%str_b%"

値の取得を4回行っていますが、そのうち2回目だけが、「if文の中で格納した値を、同じif文の中で取得」したい場所なので、!str_b!を使います。

実行結果
> delayedexpansion_if1_ok.bat
if文の中で取得 : "aaa" "bbb"
if文の外で取得 : "aaa" "bbb"

すべて、格納した通りの値が取得できました。

より正確に言うと、上記の例は結果的に、すべて!a!形式で取得しても、期待した結果になります。
同じif文の中で値を格納していなければ、%a%!a!どちらで取得しても同じです。
上記の例では、!a!としなければ期待した(直前で格納した)値を取得できない場所だけ、!a!形式にした、ということです。

(2) 失敗例:if文の中で値の取得に失敗する。2回目は成功する(ように見える)

「できる場合とできない場合がある。理由はわからない」って最悪じゃないですか。
それです。

delayedexpansion_if2_ng.bat
@echo off
if 1==1 (
  set str_a=aaa
  echo %str_a%
)
実行結果
> delayedexpansion_if2_ng.bat
ECHO は <OFF> です。

なんじゃこりゃ、ってなりません?
ECHO は <OFF> です。」は、echoコマンドを引数なしで実行したときに出るやつです。
つまり、変数%str_a%が空っぽということです。直前で格納してるのに!

でも、2回実行すると、2回目はaaaになるんですよ。やんなっちゃいますね。

実行結果
> delayedexpansion_if2_ng.bat
ECHO は <OFF> です。

> delayedexpansion_if2_ng.bat
aaa

同じ問題は、for文でも発生します。

delayedexpansion_for2_ng.bat
@echo off
for /l %%a in (1,1,1) do (
  set str_a=aaa
  echo %str_a%
)
実行結果
> delayedexpansion_for2_ng.bat
ECHO は <OFF> です。

これらはいずれも概要に書いた通り、if文に入った瞬間(値を格納する前)の%str_a%の値が出力されてこうなりました。
直前に格納した値が出力されることを期待したわたしは、悲しい思いをしました。

「2回目は成功する」のは、同じ変数を使いまわした結果1回目に格納された値が取得できただけなので、厳密には成功とは言えません(値が変わる処理だったら、2回目の結果としては誤っている可能性があります)。

直前に格納した値を出力したければ、以下のようにするべきでした。

delayedexpansion_if2_ok.bat
@echo off
rem 遅延環境変数の展開を有効にする
setlocal enabledelayedexpansion

if 1==1 (
  set str_a=aaa
  echo !str_a!
)
実行結果
> delayedexpansion_if2_ok.bat
aaa

(3) 失敗例:if文の中で値を格納しても、期待した値でない

こんなこともありました。

delayedexpansion_if3_ng.bat
@echo off
set str_a=aaa
if 1==1 (
  set str_a=bbb
  echo %str_a%
)
echo %str_a%
実行結果
> delayedexpansion_if3_ng.bat
aaa
bbb

おかしいじゃないですか(半ギレ)。
直前にbbbを入れてるのに、aaaなんて言われちゃうんですから。
ん?でもif文が終わった後はbbbになってる…

もちろんこれも、if文の中では、if文に入った瞬間(値を格納する前)の%str_a%の値が出力されただけのことです。
if文が終わると、if文の中で格納された値が反映されてbbbが出力されます。

if文の中でも、直前に格納したbbbを出力したければ、以下のようにするべきでした。

delayedexpansion_if3_ok.bat
@echo off
rem 遅延環境変数の展開を有効にする
setlocal enabledelayedexpansion

set str_a=aaa
if 1==1 (
  set str_a=bbb
  echo !str_a!
)
echo !str_a!
実行結果
> delayedexpansion_if3_ok.bat
bbb
bbb

(4) 複雑な例:if文の多重化

if文に入った瞬間の値」と何回も書きましたが、if文がネストされている場合はどうなるんでしょう?
試してみました。

delayedexpansion_if4.bat
@echo off
set str_a=aaa
if 1==1 (
  set str_a=bbb
  echo %str_a%
  if 1==1 (
    set str_a=ccc
    echo %str_a%
  )
  echo %str_a%
)
echo %str_a%
実行結果
aaa
aaa
aaa
ccc

遅延展開をしないまま、if文の階層が変わるたびに、値の格納と取得をしてみました。
その結果、取得した値が変わったのは、最上位のif文が終わったときの1回だけでした。
つまり、
・ネストされている場合、親のif文に入った時点で子if文の中の変数まで展開される
・それ以降は、子if文に入ろうが出ようが、親if文が終わるまで変わらない
ってことみたいです。

おわびに

この記事がわかりやすい自信がありません。
現国の長文読解みたいです。(あははっ)

ではー。(_ _)zzZ