はじめに
最近、初めてバッチファイルでコードを書くことになったのですが、他の言語の仕様に慣れていたせいか(いま思うとくだらない)構文エラーにひどく悩まされました。自由度の低い事この上ない!
bat関連の記事ではよく「バッチはクソ」みたいに毒づいているのを見かけますが、その意味を少しだけ痛感した今日この頃です...(苦笑)
今回は、私自身が実際に悩まされた構文エラーエトセトラと、今回の経験で得た(私のようなプログラミング初心者が)バッチファイルでコーディングする上での教訓(初歩的)を書き留めたいと思います。
この記事が誰かの役に立てば幸いです<(_ _)>
==========【目次】==========
構文エラーエトセトラ
・rem の使い方が誤っています。
・elseは)
と同じ行に書かないとエラーになる
・echo の出力文字の中に半角(
)
を使用しない
・if文の(
)
の中が空だとエラーになる
・if
文の条件式で、環境変数が空だとエラーになる
・ERRORLEVEL
に値をsetしてはいけない
・if
/for
文内のERRORLEVELは、!ERRORLEVEL!
で参照する
・for
文内ではラベルを使用できない
・for
文にbreak
やcontinue
の仕様はない(代替案)
コーディングする上での教訓(初歩的)
・わからないコマンドはヘルプを見る([コマンド] /?
)
・デバック時はECHO ON
にすべし
・if
/for
文のネストをうまく管理するために
構文エラーエトセトラ
rem の使い方が誤っています。
作ったバッチを実行して最初に出くわしたエラーです。
原因は**for
文やif
文の閉じカッコの後ろに
) rem コメント
のようにコメントアウトを付けている事**でした。このように書くと上記のエラーが出ます。
rem
でのコメントは、)
の上下の行に書くようにしましょう。
というか)
の後ろに限らず、[コマンド]の後ろでrem
を使うのは、意図しない挙動を招く可能性が高いので控えた方が良いです。
ちなみに補足ですが、このような書き方をするとrem
以外のコマンドでも同様のエラーが出力されます。
@echo off
rem verコマンドは、Windowsのバージョン情報を出力する
ver
if 1 == 1 (
echo hoge
) ver
exit /b
C:\Users\microsoft\Desktop>test.bat
Microsoft Windows [Version 10.0.18362.959]
ver の使い方が誤っています。
これはバッチの命令文の文法として
[コマンド]
if ( )
if ( ) else ( )
などはOKですが、
if ( ) [コマンド]
という文法は存在しないためだと考えられます。(rem
も例外ではない)
(if ( )
やif ( ) else ( )
でひとかたまりの命令文、というのは遅延環境変数あたりを学ぶとイメージ付きやすいかもです)
【目次に戻る】
elseは)
と同じ行に書かないとエラーになる
if
/else
文を書くときに、
) else
のように)
とelse
を改行して書くとエラーになります。
これも改行を挟むことでif ( )
else以降
、という2つの命令文に分けて認識されることによるエラーだと考えられます。(改行していなければif ( ) else ( )
という1つの命令文として認識される)
@echo off
if 1 == 1 (
echo hoge
)
else (
echo piyo
)
exit /b
C:\Users\microsoft\Desktop>test.bat
hoge
'else' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
piyo
【起こっていること】
- まず、
if ( )
で1つの命令文が完結しているとみなされて、hogeが出力されます。 -
else
というコマンドは存在しないので、5行目はエラーとして無視されます。 -
echo piyo
が実行されて、piyoが出力されます。 - ペアのいない
)
で始まる行はNOP
として扱われ、その行ごと無視されるのでエラーは起こりません。
【目次に戻る】
echo の出力文字の中に半角(
)
を使用しない
echo
で出力するつもりの文字列の中でも、半角(
)
を使えば特殊文字としてきっちり処理されます。
開きカッコ(
は、最初に現れた閉じカッコ)
とペアを組みたがるので、不必要な箇所で半角(
)
を使用すると、予期しない(というか理解不能な)挙動を示す可能性が高くなります。
なので、echo
の出力にカッコを使う場合は全角にするか^(
^)
のようにエスケープして使うのが良いです。
下の例では、4行目の閉じカッコでif
文が完結しているとみなされているため、おかしな挙動になっています。
@echo off
rem "piyo" が出力されるはず、、が、
if 1 == 2 (
echo hoge (hoge)
echo hogehoge
) else (
echo piyo
)
exit /b
----- 結果、何やら挙動がおかしい、、、-----
C:\Users\microsoft\Desktop>test.bat
hogehoge
piyo
@echo off
if 1 == 2 (
echo hoge (hoge
)
echo hogehoge
rem ) else ( ## NOP(行ごと無視)
echo piyo
rem ) ## NOP(行ごと無視)
exit /b
@echo off
if 1 == 2 (
echo hoge ^(hoge^)
echo hogehoge
) else (
echo piyo
)
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
piyo
【目次に戻る】
if文の(
)
の中が空だとエラーになる
if
文やfor
文は(
)
の中身を何も書かないと構文エラーになります。
rem
でもいいので、何か最低1行書けばエラーではなくなります。
@echo off
if 1 == 1 (
)
echo hoge
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
) の使い方が誤っています。
@echo off
if 1 == 1 (
rem 何か1行書く
)
echo hoge
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
hoge
【目次に戻る】
if
文の条件式で、環境変数が空だとエラーになる
if
文の条件式内に環境変数を使用している場合、環境変数が空の状態だと構文エラーになるという点に注意が必要です。これは環境変数が空の状態だとif == 1 (
という形になってしまうためです。(下の例の場合)
両辺にダブルクォートを使用して、if "%num%" == "1" (
のようにすればこの問題は解消しますが、必ずしも万能ではないようです。(参照:Windowsバッチまとめ)
@echo off
rem 環境変数 num がセットされていない
set num=
if %num% == 1 (
echo hoge
)
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
( の使い方が誤っています。
【目次に戻る】
ERRORLEVEL
に値をsetしてはいけない
ERRORLEVEL
とは、直前のバッチコマンドの実行結果に応じて値が変化する特別な変数で、基本的には直前のコマンドが正常に動いた場合には0、それ以外の挙動(エラーなど)の場合には0以外の様々な値をとります。
直前のコマンドのエラーの有無を判定したい場合などによく用いられます。
このERRORLEVEL
の動作確認をするときの注意点として、
ERRORLEVEL
の値をset
で代入してしまうと、ERRORLEVEL
がsetした値から変化しなくなります。
どうやらset
をすることで、元々あったERRORLEVEL
とは別に、新しく同名のERRORLEVELという環境変数が作られてしまうことが原因のようです。
これを解決するには、
set ERRORLEVEL=
(右辺に何も書かない)
を実行して、新しく作られたERRORLEVELをリセットすればOKです。
ERRORLEVEL
を変化させたい場合には、set
するのではなく意図的にコマンドエラーを起こすようにしましょう。
===== 〇通常の場合(ERRORLEVEL
が変化する)=====
@echo off
echo errorlevel1 : %ERRORLEVEL%
rem 間違ったコマンド errorlevel → 0以外
type
echo errorlevel2 : %ERRORLEVEL%
rem 正しいコマンド errorlevel → 0
cd .
echo errorlevel3 : %ERRORLEVEL%
if %ERRORLEVEL% == 0 (
echo hoge
)
exit /b
C:\Users\microsoft\Desktop>test.bat
errorlevel1 : 0
コマンドの構文が誤っています。
errorlevel2 : 1
errorlevel3 : 0
hoge
===== × ERRORLEVELをsetした場合(元のERRORLEVEL
を参照できない)=====
@echo off
rem ERRORLEVEL = 5 にセット
set ERRORLEVEL=5
echo errorlevel1 : %ERRORLEVEL%
rem 間違ったコマンド errorlevel → 0以外
type
echo errorlevel2 : %ERRORLEVEL%
rem 正しいコマンド errorlevel → 0
cd .
echo errorlevel3 : %ERRORLEVEL%
if %ERRORLEVEL% == 0 (
echo hoge
)
exit /b
C:\Users\microsoft\Desktop>test.bat
errorlevel1 : 5
コマンドの構文が誤っています。
errorlevel2 : 5
errorlevel3 : 5
【目次に戻る】
if
/for
文内のERRORLEVELは、!ERRORLEVEL!
で参照する
if
文やfor
文内でERRORLEVEL
の値を参照する場合には、遅延環境変数を使って!ERRORLEVEL!
とします。
これも「ERRORLEVELといえば、%ERRORLEVEL%
」という思い込みがあったので気付くのに時間がかかってしまいました、、、。
@echo off
if 1 == 1 (
rem 間違ったコマンド errorlevel → 0以外
type
if %ERRORLEVEL% neq 0 (
echo hoge
)
)
exit /b
----- "hoge" が出力されるはずだったのだが、、、 -----
C:\Users\microsoft\Desktop>test.bat
コマンドの構文が誤っています。
@echo off
setlocal enabledelayedexpansion
if 1 == 1 (
rem 間違ったコマンド errorlevel → 0以外
type
if !ERRORLEVEL! neq 0 (
echo hoge
)
)
exit /b
----- ちゃんと "hoge" が出力された -----
C:\Users\microsoft\Desktop>test.bat
コマンドの構文が誤っています。
hoge
【目次に戻る】
for
文内ではラベルを使用できない
for
文の中でラベルを使うと、ラベルよりも上の行が認識されないせいか、思うような挙動を示してくれません。
また、ラベルの直後の行が)
の場合はエラーになります。
なので、goto
を使ってfor
文内のラベルに飛ぶ、ということは基本的にはできないと考えた方がよさそうです。
goto
でfor
ループの外に飛んで**break
処理のようにすることは可能**です。
また**call
は使用できる**ので、これで関数っぽいものを作ってfor
文内をいくらかコンパクトにする、といったことはできそうです。
@echo off
for /l %%i in (1,1,3) do (
echo %%i
)
exit /b
----- 通常の動き -----
C:\Users\microsoft\Desktop>test.bat
1
2
3
@echo off
for /l %%i in (1,1,3) do (
goto :hoge
:hoge
echo %%i
)
exit /b
----- "echo %%i" が1回だけ実行されて終了 -----
C:\Users\microsoft\Desktop>test.bat
%i
@echo off
for /l %%i in (1,1,3) do (
echo %%i
:hoge
)
exit /b
C:\Users\microsoft\Desktop>test.bat
) の使い方が誤っています。
@echo off
for /l %%i in (1,1,3) do (
echo %%i
call :hoge
)
exit /b
:hoge
echo hogehoge
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
1
hogehoge
2
hogehoge
3
hogehoge
【目次に戻る】
for
文にbreak
やcontinue
の仕様はない
バッチのfor
にはbreak
(forループ自体を強制終了する)やcontinue
(このループだけ終了して次のループを実行する)といった仕様はないです。
なので、そういったことをしたいのであればfor
ではなくラベルを使ってループ処理を組むのがよさそうです。
goto
とラベルを使えばfor
でも**break
処理のようにすることは可能**です。
また、以下のように適当な環境変数とif文を使って後続処理の続行を管理することで一応continue
っぽいことはできます。
setlocal enabledelayedexpansion
rem 変数continue と if で後続処理の実行不実行を管理
for /l %%i in (1,1,10) do (
set continue=true
処理1
rem 処理を中断する場合は set continue=false
if !continue! == true (
処理2
rem 処理を中断する場合は set continue=false
)
if !continue! == true (
処理3
rem 処理を中断する場合は set continue=false
)
if !continue! == true (
処理4
)
)
【目次に戻る】
コーディングする上での教訓(初歩的)
わからないコマンドはヘルプを見る([コマンド] /?
)
コマンドプロンプト上で、コマンド名 /?
を実行すると、そのコマンドのヘルプを見ることができます。
割と詳しく説明してくれているので、そのコマンドにどんな機能があるのか知りたいときにはぜひ閲覧する事をおススメします。
実際、今回書いた内容の中には普通にヘルプに書いてあった内容もありました。(最初から見ろって話ですね. . . )
【目次に戻る】
デバック時はECHO ON
にすべし
バッチファイルの先頭に書かれている(であろう)@echo off
は、バッチファイルを実行したときに実行結果だけが出力されるようにするコマンドです。
ですが作成したソースコードの動作確認時は、下記のように@echo off
を行わないデフォルトの状態の方が、プログラムのどこに問題があるかをはるかに見つけやすくなります。
ただし、大きいfor
文やif
文の中でエラーが生じている場合には、その中のどこでエラーが生じているかまでは特定できないので、あくまで目安程度です。
----- test.bat -----
rem 先頭行に @echo off をつけない
echo hoge1
echo hoge2
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
C:\Users\microsoft\Desktop>rem 先頭行に @echo off をつけない
C:\Users\microsoft\Desktop>echo hoge1
hoge1
C:\Users\microsoft\Desktop>echo hoge2
hoge2
C:\Users\microsoft\Desktop>exit /b
【目次に戻る】
if
/for
文のネストをうまく管理するために
バッチの場合、インデントの自動補正もなければ、カッコにカーソルを合わせたら対応するカッコがハイライトされる、といった機能もないので、コード修正時にfor
文/if
文のネスト構造が崩れていることに気付かないと、修正作業が一気に迷宮入りします。
私の場合、今回のコーディングで苦しめられた大きな原因は結局そこかなと感じています。
ここでは初歩的ですが、今回の経験で得た気付きを3つ書き留めたいと思います。
1. インデントをきちんと整える
バッチではこれまで書いてきた(
)
のクソ仕様と相まって、他の言語以上にfor
文/if
文をデリケートに管理しないと痛い目を見ます(見た)。
なので、インデント調整もかなり重要なファクターだと感じました。
今までインデント調整をサボってきた方は、ぜひこの機会にしっかり意識することをお勧めします。
【目次に戻る】
2. 行ごとコピペ
他の箇所に書いたコードを別の箇所にコピペする場合、ペースト先をカーソルで指定するとどうしても貼り付けたときにインデントが崩れるので、それがネスト構造が崩れる原因の1つになり得ます。
下図のように、コピー元とペースト先をそれぞれ行単位で指定することで、貼り付けたときにインデントが崩れないので、ネスト構造を崩すリスクが減ります。
地味ですが重要なテクニックです。
【目次に戻る】
3. 一から1つずつ組み立てる
コード修正時にいま一度for
文/if
文の構造を見直したいと感じた方は、下手にサボらずに別ファイルで一からfor
文/if
文を作り直すことをお勧めします。
コードを書き直す人も、いまから新しく作り始める人も意識した方が良いと感じたのは、ちゃんと想定した条件の時にfor
文やif
文に入ってくれることを確認してから中身を埋めていくという事です。
例えば下記のようなコードを作りたい場合には、まず外側のfor
文だけの状態から初めて、次に中のif
、else
、ifの中のfor
という風に1つずつ足しながら動作確認していく、といった感じです。
@echo off
for /l %%i in (1,1,5) do (
if %%i geq 3 (
for %%a in (hoge) do (
echo %%i %%a
)
) else (
echo %%i piyo
)
)
exit /b
----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
1 piyo
2 piyo
3 hoge
4 hoge
5 hoge
@echo off
for /l %%i in (1,1,5) do (
echo %%i
)
exit /b
----- 出力結果; OK -----
C:\Users\microsoft\Desktop>test2.bat
1
2
3
4
5
@echo off
for /l %%i in (1,1,5) do (
echo %%i
if %%i geq 3 (
echo in if_block
)
)
exit /b
----- 出力結果; OK -----
C:\Users\microsoft\Desktop>test2.bat
1
2
3
in if_block
4
in if_block
5
in if_block
@echo off
for /l %%i in (1,1,5) do (
echo %%i
if %%i geq 3 (
echo in if_block
) else (
echo in else_block
)
)
exit /b
----- 出力結果; OK -----
C:\Users\microsoft\Desktop>test2.bat
1
in else_block
2
in else_block
3
in if_block
4
in if_block
5
in if_block
@echo off
for /l %%i in (1,1,5) do (
echo %%i
if %%i geq 3 (
echo in if_block
for %%a in (hoge) do (
echo in for_loop %%a
)
) else (
echo in else_block
)
)
exit /b
----- 出力結果; OK -----
C:\Users\microsoft\Desktop>test2.bat
1
in else_block
2
in else_block
3
in if_block
in for_loop hoge
4
in if_block
in for_loop hoge
5
in if_block
in for_loop hoge
@echo off
for /l %%i in (1,1,5) do (
if %%i geq 3 (
for %%a in (hoge) do (
echo %%i %%a
)
) else (
echo %%i piyo
)
)
exit /b
----- 出力結果; OK -----
C:\Users\microsoft\Desktop>test2.bat
1 piyo
2 piyo
3 hoge
4 hoge
5 hoge
【目次に戻る】