はじめに
バッチファイルって、いくら書き慣れても毎回ハマりますよね。エラーメッセージが言葉足らずなせいで、自分が書いたコードを穴のあくほど眺めても原因が分からない(いたるところにecho文を仕込んでprintfデバッグするものの、そのecho文が実行されない時など、途方に暮れます)。原因調査だけで半日溶かすことはざらにあります。
例えば、以下のようなエラーメッセージに見覚えはありませんか?
0 の使い方が誤っています。( の使い方が誤っています。ECHO は \<OFF\> です。
特に…
- if文やfor文の中と外にechoを仕込んでいるにも関わらず、どちらのechoも実行されていない。
「えー、なんで??」となりますよね。
人生の貴重な時間を、バッチファイルのデバッグで溶かすのは本当に悲しいことです。本記事では、皆さまの貴重な時間をバッチファイルなんかで溶かしてほしくないという思いから、私が書き溜めた「バッチファイルでやりがちなミス」のうち、ありがちなミスを公開したいと思います。
- 【原因1】変数名をスペルミスしている
- 【原因2】
%変数名%が即時展開されている - 【原因3】文字列リテラルに含まれるカッコ「
()」をエスケープしていない - 【原因4】単一文字変数(
%%A)が他の変数名の先頭文字とかぶっている - 【原因5】
)の直上にラベルを置いている - 【原因6】
ifやforのブロック内を空にしている - 【原因7】
setlocal enabledelayedexpansionの罠を踏んでいる
本記事について
本記事のネタは、すべて以下の記事からの抜粋となります。以下記事も合わせてよろしくお願いします。
バッチファイルでハマった時はコレを疑え!7選
【原因1】変数名をスペルミスしている
一番多いのが単純に変数名をスペルミスしてしまい、別の変数として解釈されてしまうケースです。
以下、変数名「STATUS」を誤って「STAUTS」とタイポしてしまった例です。「STAUTS」は未初期化変数=値が設定されていないため、それをechoしようとすると引数の無いechoを実行するのと同じことになるため、「ECHO は <OFF> です。」というメッセージが出力されます。
@echo off
set STATUS=255
echo %STAUTS%
rem => 「ECHO は <OFF> です。」
以下は、equ演算子の左オペランドが無いと解釈されるため「0 の使い方が誤っています」というメッセージが出力されます。
@echo off
set STATUS=0
if %STAUTS% equ 0 (
echo ステータスは 0 です.
)
rem => 「0 の使い方が誤っています。」
対策
変数名は直打ちせずコピペしましょう。検索語がハイライトされるテキストエディタで検索してみて、同じ変数にハイライトがつくか確認するのもよいです。
【原因2】%変数名%が即時展開されている
まともなプログラム言語に親しんでいる人にとっては、以下のコードには何も問題は無いと感じることでしょう。
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演算子の左オペランドが無い)。
rem 終了ステータスを確認
if neq 0 (
echo エラーが発生しました.
)
その結果、「0 の使い方が誤っています。」という実行時エラーが出力されます(neqの左右オペランドが消えた場合は「( の使い方が誤っています。」)。なお、これはif文の条件部が偽であり、ifブロックの中に入らない場合もこのようになります(構文解析時の問題なので)。
Linuxのシェルでコマンドラインを実行する際、シェルがコマンドライン中に含まれる${変数名}を値に展開するのと同様、cmd.exeにとってはif文やfor文もコマンドに近い扱いなのかもしれません。
ちなみにデバッグ時は@echo onにしてバッチファイルを実行すると、cmd.exeがif文やfor文内の変数を展開した結果が出力されます。
対策
残念ながら、蛇蝎のごとく嫌われているgotoを使ってください。安易に遅延展開(!変数名!)を使って二次災害を出すよりも、gotoを使ったほうがシンプルです。
【原因3】文字列リテラルに含まれるカッコ「()」をエスケープしていない
以下のバッチファイルを実行すると、「**** START ****」の次の行には何が出力されるでしょうか。
@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 ****
- 「
0以上 (0 or more)」 - 「
0未満 (-1 or less)」 - 「
**** END ****」 - 何も表示されない。
【答え】
正解は**「4. 何も表示されない。」**です。
おそらく、文字列リテラルに含まれるカッコ「()」がif文のカッコと対応づけられてシンタックスが壊れたのだと思います。シンタックスが壊れた結果、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)が他の変数名の先頭文字とかぶっている
クイズです。以下のコードを実行すると、何が表示されるでしょうか。
@echo off
set "customer_name=****"
for /f "tokens=1-3" %%a in ("xxx yyy zzz") do (
call echo %%customer_name%%
)
exit /b 0
****%customer_name%zzzustomer_name- 何も表示されない
【答え】
正解は3です。
- まず、2つの連続する
%が、1つの%に置換されます(%%→%)。 - 次に、
%customer_name%は%cとustomer_name%に分離されます。` - 次に、
%cがzzzに展開されます。ustomer_name%末尾の%は消滅します(単一の%は消滅するルール)。 - 以上の結果、
zzzustomer_nameが出力されます。
ようこそ、バッチファイルの狂気の世界へ!
対策
単一文字変数は、なるべく後ろのアルファベット(%%Xなど)を使うのがよいかもしれません。
【原因5】)の直上にラベルを置いている
理由は分かりませんが、(とペアになる)の直上行にラベルを置くと、シンタックスエラーを示すメッセージ() の使い方が誤っています。)が出力されます。意味ワカンネ。
if 1 equ 1 (
echo hoge
:LABEL
)
rem => ) の使い方が誤っています。
対策
ラベル行の直下にNOPとしてremを入れておきましょう。というより、(``)ブロック内でラベルを使うのは止めたほうがいいかも。
if 1 equ 1 (
echo hoge
:LABEL
rem
)
ちなみに、ペアとなる(が存在しない)の直上にラベルを置いてもエラーとはなりませんでした。よく分からん…。
@echo off
:LABEL
)
【原因6】ifやforのブロック内を空にしている
エラー処理は後で書きたいので、ifブロック内を空にするつもりで
if not exist %filePath% (
)
rem 標準出力 ⇒ ) の使い方が誤っています。
なんて書くと、構文エラーで怒られてしまいます。
対策
remコマンドでNOPの代用をするしかないようです。無名ラベルの:でも構文エラーとなります。
if not exist %filePath% (
rem //あとでかく
)
【原因7】setlocal enabledelayedexpansionの罠を踏んでいる
以下をご参照下さい。