batファイルでfor文内に変数を利用する場合の罠

プログラミング
スポンサーリンク

はじめに

今回は、batファイルを書いていてハマった問題を紹介したいと思います。

やりたかったことは、batファイルから外部のテキストファイルを読み込み、そのテキストファイルの各行に記載されている文字を、別のバッチファイルへの引数に渡して処理をさせたいと思っていました。

しかし、なぜか別のバッチファイルの引数に文字列が渡らないという問題が発生したため、原因を突き止めていきました。

ファイルをロードして処理するスクリプト

調査

再現するスクリプトを調べた結果、以下のことが分かりました。

読み込みたいテキストファイル(Text.txt)を用意します。

1
2
3
4

上記のファイルを各行ごとに表示するスクリプトを用意。

@echo off

for /f %%a in (./Text.txt) do (
	set line=%%a
	echo %line%
)

pause

上記の処理でechoにより、1,2,3,4が表示されることを期待してたのですが、実行すると以下のようになります。

ECHO は <OFF> です。
ECHO は <OFF> です。
ECHO は <OFF> です。
ECHO は <OFF> です。
続行するには何かキーを押してください . . .

これはおかしいですね。

ECHO は <OFF> です。

なぜか、ECHO は <OFF> です。と表示されています。このような記載はしていないはずなのに不思議ですね。「ECHO は <OFF> です。」という単語を調査した結果、変数自体の初期化ができていないときのエラーのようです。

正確には、変数の初期化ができていなかったことで、echoの引数無しだけが実行されてしまい、@echo offの状態を知らせる「ECHO は <OFF> です。」というメッセージが表示されたようです。(cinabar様、コメントにてご指摘ありがとうございます。)

つまり、set line=%%a の行自体が実行されず、初期化されていないことが引き金で変な処理になっていたのが原因です。

ブロック文の環境変数の展開タイミング

さらに調査を進めた結果、Windowsのバッチファイルの仕様であることが分かりました。具体的には、括弧()をつかったブロック文を作った場合に、このブロックに入った瞬間に、ブロック内の内部の環境変数が全て展開されるようです。

つまり、ブロック内の echo %line% は、その上の行の set line=%%a を実行される前に展開されてしまい、%line%が未初期化のため、エラーになるのです。

修正方法

以下、修正方法として以下の手順の説明をします。

  • そもそもブロック内で環境変数への代入を避ける
  • 遅延環境変数」を利用する

ブロック内で環境変数に代入しない

ブロック内で環境変数に代入し、代入結果を利用することが問題なので、例えば、代入結果を利用しないように以下のようにするだけで修正できます。

@echo off

for /f %%a in (./Text.txt) do (
	echo %%a
)

pause

遅延環境変数を使用する

展開自体を遅らせる方法があります。それは、遅延環境変数を利用するというもの。

使用方法は、変数を使うときに%line%ではなく、!line! のようにエクスクラメーションマークで囲います。さらに遅延環境変数を使用する前に、setlocal EnableDelayedExpansionを実行して、遅延環境変数が利用できるように設定しておく必要があります。

上記をふまえて以下のように書き換えましょう。

@echo off
setlocal EnableDelayedExpansion

for /f %%a in (./Text.txt) do (
	set line=%%a
	echo !line!
)

endlocal
pause

環境変数の展開タイミングによる他の問題例

今回、for文を使用したことで問題が発生しましたが、ブロック文自体が問題なので if文でも同様の問題が発生します。

@echo off

set num=1

if %num% equ 1 (
	rem 1 と表示する
	echo %num% 
	set num=2
	rem 2 と表示する
	echo %num%
)

rem 1 と表示する
echo %num%

pause

「1,2,2」と出ることを期待して上記を実行すると

1
1
2
続行するには何かキーを押してください . . .

と出てしまいます。

これもブロック内を展開するタイミングで、echo %num%が全て、echo 1 に展開されてしまうためです。

遅延環境変数で解決できます。

@echo off
setlocal EnableDelayedExpansion

set num=1

if !num! equ 1 (
	rem 1 と表示する
	echo !num!
	set num=2
	rem 2 と表示する
	echo !num!
)

rem 1 と表示する
echo !num!

endlocal
pause

以下は修正後の実行結果となります。

1
2
2
続行するには何かキーを押してください . . .

おわりに

展開するタイミングを考えるのは大変なので、そもそも普通の環境変数は、遅延環境変数と扱ってくれればいいのですけどもね・・・。とりあえずbatファイルなどのスクリプトは、結構知らない仕様がまだまだあって怖いなと思いました。

最後まで読んでいただきありがとうございました。

コメント

  1. cinabar より:

    echo を引数無しで実行すると、現在 echo on か、echo off かを表示します。
    バッチ冒頭部で echo off に設定しているので

    変数が展開されないと、echo が引数無しで実行されて、結果 ECHO は です。
    と表示されるのであって、変数自体の初期化ができていないときのエラーではありません。

    • なたで より:

      cinabar 様、ご指摘ありがとうございました。
      なるほど、初期化されていないことでECHOだけが引数無しで実行されてしまい、
      メッセージが表示されてしまっていたのですね。

      記事の方を修正いたしました。ありがとうございました。

  2. 通りすがり より:

    > つまり、set line=%%a の行自体が実行されず、初期化されていないことが引き金で変な処理になっていたのが原因です。
    間違ってます
    “set line=%%a” 文は実行されています
    %展開はコマンドの構文解析時に展開されます
    for文の解析時には 環境変数”line” は存在していないので空文字が展開され
    do (
    set line=%%a
    echo
    )
    となって実行され、『ECHO は です。』と表示されています
    以下のように
    for /f %%a in (./Text.txt) do (
    set line=%%a
    echo %line%
    )
    echo %line%

    としてみれば”set line=%%a”が実行されていることが分かるかと。更に
    set line=0
    for /f %%a in (./Text.txt) do (
    set line=%%a
    echo %line%
    )
    echo %line%
    を実行してみればfor実行前に%展開されている事が理解出来ると思います

タイトルとURLをコピーしました