バッチファイルでハマった時はコレを疑え！7選

#はじめに
バッチファイルって、いくら書き慣れても毎回ハマりますよね。エラーメッセージが言葉足らずなせいで、自分が書いたコードを穴のあくほど眺めても原因が分からない（いたるところにecho文を仕込んでprintfデバッグするものの、そのecho文が実行されない時など、途方に暮れます）。原因調査だけで半日溶かすことはざらにあります。

例えば、以下のようなエラーメッセージに見覚えはありませんか？

* `0 の使い方が誤っています。`
* `( の使い方が誤っています。`
* `ECHO は \<OFF\> です。`

特に…

* if文やfor文の中と外にechoを仕込んでいるにも関わらず、どちらのechoも実行されていない。

「えー、なんで？？」となりますよね。

人生の貴重な時間を、バッチファイルのデバッグで溶かすのは本当に悲しいことです。本記事では、皆さまの貴重な時間をバッチファイルなんかで溶かしてほしくないという思いから、私が書き溜めた「バッチファイルでやりがちなミス」のうち、ありがちなミスを公開したいと思います。

* 【原因1】変数名をスペルミスしている
* 【原因2】`%変数名%`が即時展開されている
* 【原因3】文字列リテラルに含まれるカッコ「`()`」をエスケープしていない
* 【原因4】単一文字変数（`%%A`）が他の変数名の先頭文字とかぶっている
* 【原因5】`)`の直上にラベルを置いている
* 【原因6】`if`や`for`のブロック内を空にしている
* 【原因7】`setlocal enabledelayedexpansion`の罠を踏んでいる

#本記事について
本記事のネタは、すべて以下の記事からの抜粋となります。以下記事も合わせてよろしくお願いします。

https://qiita.com/yz2cm/items/8058d503a1b84688af09

# バッチファイルでハマった時はコレを疑え！7選
##【原因１】変数名をスペルミスしている
一番多いのが単純に変数名をスペルミスしてしまい、別の変数として解釈されてしまうケースです。

以下、変数名「`STATUS`」を誤って「`STAUTS`」とタイポしてしまった例です。「`STAUTS`」は未初期化変数＝値が設定されていないため、それを`echo`しようとすると引数の無い`echo`を実行するのと同じことになるため、「`ECHO は <OFF> です。`」というメッセージが出力されます。

```bat:○STATUSを×STAUTSとタイポ
@echo off

set STATUS=255
echo %STAUTS%
rem => 「ECHO は <OFF> です。」
```

以下は、`equ`演算子の左オペランドが無いと解釈されるため「`0 の使い方が誤っています`」というメッセージが出力されます。

```bat:○STATUSを×STAUTSとタイポ
@echo off

set STATUS=0
if %STAUTS% equ 0 (
    echo ステータスは 0 です.
)
rem => 「0 の使い方が誤っています。」
```

### 対策
変数名は直打ちせずコピペしましょう。検索語がハイライトされるテキストエディタで検索してみて、同じ変数にハイライトがつくか確認するのもよいです。

##【原因２】`%変数名%`が即時展開されている
まともなプログラム言語に親しんでいる人にとっては、以下のコードには何も問題は無いと感じることでしょう。

```bat:
rem 変数に値を代入し、それを参照しているだけのコード。
rem どこに問題があるのか...

if defined STATUS (
    rem 変数に代入
    set /a STATUS = 0

    rem 変数を参照
    if %STATUS% neq 0 (
        echo エラーが発生しました.
    )
)
```

しかし、バッチファイル的には許されないコードなのです！実際、このコードを実行すると「`0 の使い方が誤っています。`」という実行時エラーが出力されます。

なぜなら、処理系（`cmd.exe`）がこの`if`文を解釈する際、まず（遅延展開されていない）すべての`%変数%`を値に展開し、その上で`if`文全体を構文解析します。すると、`if`文全体を構文解析するタイミングでは変数`STATUS`は初期化されていませんから、`%STATUS%`は空文字列に展開されます。

イメージとしては、処理系の視点からは以下のようなコードを実行することになります（`neq`演算子の左オペランドが無い）。

```bat:×ill-formed
    rem 終了ステータスを確認
    if neq 0 (
        echo エラーが発生しました.
    )
```

その結果、「`0 の使い方が誤っています。`」という実行時エラーが出力されます（`neq`の左右オペランドが消えた場合は「`( の使い方が誤っています。`」）。なお、これは`if`文の条件部が偽であり、`if`ブロックの中に入らない場合もこのようになります（構文解析時の問題なので）。

Linuxのシェルでコマンドラインを実行する際、シェルがコマンドライン中に含まれる`${変数名}`を値に展開するのと同様、`cmd.exe`にとっては`if`文や`for`文もコマンドに近い扱いなのかもしれません。

ちなみにデバッグ時は`@echo on`にしてバッチファイルを実行すると、`cmd.exe`が`if`文や`for`文内の変数を展開した結果が出力されます。

###対策
残念ながら、蛇蝎のごとく嫌われている`goto`を使ってください。安易に遅延展開（`!変数名!`）を使って二次災害を出すよりも、`goto`を使ったほうがシンプルです。

##【原因３】文字列リテラルに含まれるカッコ「`()`」をエスケープしていない

以下のバッチファイルを実行すると、「`**** START ****`」の次の行には何が出力されるでしょうか。

```bat:カッコのエスケープ忘れ
@echo off

echo **** START ****

set /a n=-1
if %n% geq 0 (
    echo 0以上 (0 or more)
    exit /b 0
) else (
    echo 0未満 (-1 or less)
    exit /b -1
)

echo **** END ****
```

1. 「`0以上 (0 or more)`」
1. 「`0未満 (-1 or less)`」
1. 「`**** END ****`」
1. 何も表示されない。

【答え】
正解は**「4. 何も表示されない。」**です。

おそらく、文字列リテラルに含まれるカッコ「`()`」が`if`文のカッコと対応づけられてシンタックスが壊れたのだと思います。シンタックスが壊れた結果、cmd.exeには以下のように解釈したのだと思われます。

```bat:cmd.exeにはこのように見えている
@echo off

echo **** START ****

set /a n=-1
if %n% geq 0 (
    echo 0以上 (0 or more
)
exit /b 0
rem // exitの前でif文は閉じているため、シンタックスが壊れた以下のコード片は構文解析されない。
) else (
    echo 0未満 (-1 or less)
    exit /b -1
)

echo **** END ****
```

###対策
理解不能なバグが起きたら、**カッコの対応が壊れていないか**を疑ってみることです。バッチファイルでは**文字列リテラルにカッコを使わない**というルールを設けておくのも有効だと思います。

##【原因4】単一文字変数（`%%A`）が他の変数名の先頭文字とかぶっている

クイズです。以下のコードを実行すると、何が表示されるでしょうか。

```bat:
@echo off

set "customer_name=****"

for /f "tokens=1-3" %%a in ("xxx yyy zzz") do (
  call echo %%customer_name%%
)

exit /b 0
```

1. `****`
2. `%customer_name%`
3. `zzzustomer_name`
4. 何も表示されない

【答え】
正解は**3**です。

* まず、2つの連続する`%`が、1つの`%`に置換されます（`%%`→`%`）。
* 次に、`%customer_name%`は`%c`と`ustomer_name%`に分離されます。`
* 次に、`%c`が`zzz`に展開されます。`ustomer_name%`末尾の`%`は消滅します（単一の`%`は消滅するルール）。
* 以上の結果、`zzzustomer_name`が出力されます。

ようこそ、バッチファイルの狂気の世界へ！

###対策
単一文字変数は、なるべく後ろのアルファベット（`%%X`など）を使うのがよいかもしれません。

##【原因5】`)`の直上にラベルを置いている

理由は分かりませんが、`(`とペアになる`)`の直上行にラベルを置くと、シンタックスエラーを示すメッセージ（`) の使い方が誤っています。`）が出力されます。意味ワカンネ。

```bat:×悪い例
if 1 equ 1 (
    echo hoge
:LABEL
)
rem => ) の使い方が誤っています。
````

###対策
ラベル行の直下にNOPとして`rem`を入れておきましょう。というより、`(``)`ブロック内でラベルを使うのは止めたほうがいいかも。

```bat:○良い例
if 1 equ 1 (
    echo hoge
:LABEL
rem
)
````

ちなみに、ペアとなる`(`が存在しない`)`の直上にラベルを置いてもエラーとはなりませんでした。よく分からん…。

```bat:
@echo off

:LABEL
)
```

##【原因6】`if`や`for`のブロック内を空にしている

エラー処理は後で書きたいので、`if`ブロック内を空にするつもりで

```bat:×悪い例
if not exist %filePath% (
)
rem 標準出力 ⇒ ) の使い方が誤っています。
```

なんて書くと、構文エラーで怒られてしまいます。

###対策
`rem`コマンドでNOPの代用をするしかないようです。無名ラベルの`:`でも構文エラーとなります。

```bat:○良い例
if not exist %filePath% (
rem //あとでかく
)
```

##【原因7】`setlocal enabledelayedexpansion`の罠を踏んでいる

以下をご参照下さい。

https://qiita.com/yz2cm/items/4983be006116c369d08b