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

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

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

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

【2021/04/12 19:50 追記】
時間がない人のための[簡潔スッキリ版](https://qiita.com/koiwaiwaiko/items/b9dd568da53e7264e79f "バッチファイルの遅延環境変数（簡単な例）")も書きました。ご活用ください。

https://qiita.com/koiwaiwaiko/items/b9dd568da53e7264e79f

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

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

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

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

```bat: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回目は成功する（ように見える）
「できる場合とできない場合がある。理由はわからない」って最悪じゃないですか。
それです。

```bat: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`文でも発生します。

```bat: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回目の結果としては誤っている可能性があります）。

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

```bat: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文の中で値を格納しても、期待した値でない
こんなこともありました。

```bat: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`を出力したければ、以下のようにするべきでした。

```bat: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`文がネストされている場合はどうなるんでしょう？
試してみました。

```bat: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