25
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

bat初心者がハマった構文エラーetc & コーディングする上での教訓(初歩的)

Last updated at Posted at 2020-08-08

はじめに

最近、初めてバッチファイルでコードを書くことになったのですが、他の言語の仕様に慣れていたせいか(いま思うとくだらない)構文エラーにひどく悩まされました。自由度の低い事この上ない!

bat関連の記事ではよく「バッチはクソ」みたいに毒づいているのを見かけますが、その意味を少しだけ痛感した今日この頃です...(苦笑)

今回は、私自身が実際に悩まされた構文エラーエトセトラと、今回の経験で得た(私のようなプログラミング初心者が)バッチファイルでコーディングする上での教訓(初歩的)を書き留めたいと思います。

この記事が誰かの役に立てば幸いです<(_ _)>

==========【目次】==========

構文エラーエトセトラ
rem の使い方が誤っています。
elseは)と同じ行に書かないとエラーになる
echo の出力文字の中に半角( )を使用しない
if文の( )の中が空だとエラーになる
if文の条件式で、環境変数が空だとエラーになる
ERRORLEVELに値をsetしてはいけない
if/for文内のERRORLEVELは、!ERRORLEVEL!で参照する
for文内ではラベルを使用できない
for文にbreakcontinueの仕様はない(代替案)

コーディングする上での教訓(初歩的)
わからないコマンドはヘルプを見る([コマンド] /?
デバック時はECHO ONにすべし
if/for文のネストをうまく管理するために

構文エラーエトセトラ

rem の使い方が誤っています。

作ったバッチを実行して最初に出くわしたエラーです。

原因は**for文やif文の閉じカッコの後ろに
) rem コメント
のようにコメントアウトを付けている事**でした。このように書くと上記のエラーが出ます。
remでのコメントは、)の上下の行に書くようにしましょう。

というか)の後ろに限らず、[コマンド]の後ろでremを使うのは、意図しない挙動を招く可能性が高いので控えた方が良いです。

ちなみに補足ですが、このような書き方をするとrem以外のコマンドでも同様のエラーが出力されます。

test.bat
@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つの命令文として認識される)

test.bat
@echo off
if 1 == 1 (
    echo hoge
)
else (
    echo piyo
)
exit /b
出力結果
C:\Users\microsoft\Desktop>test.bat
hoge
'else' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
piyo

【起こっていること】

  1. まず、if ( )で1つの命令文が完結しているとみなされて、hogeが出力されます。
  2. elseというコマンドは存在しないので、5行目はエラーとして無視されます。
  3. echo piyoが実行されて、piyoが出力されます。
  4. ペアのいない)で始まる行はNOPとして扱われ、その行ごと無視されるのでエラーは起こりません。

目次に戻る

echo の出力文字の中に半角( )を使用しない

echoで出力するつもりの文字列の中でも、半角( )を使えば特殊文字としてきっちり処理されます。
開きカッコ(は、最初に現れた閉じカッコ)とペアを組みたがるので、不必要な箇所で半角( )を使用すると、予期しない(というか理解不能な)挙動を示す可能性が高くなります。

なので、echoの出力にカッコを使う場合は全角にするか^( ^)のようにエスケープして使うのが良いです。

下の例では、4行目の閉じカッコでif文が完結しているとみなされているため、おかしな挙動になっています。

test.bat
@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行書けばエラーではなくなります

if文の中を何も書かないとエラーになる(test.bat)
@echo off
if 1 == 1 (
)
echo hoge
exit /b

----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
) の使い方が誤っています。
なんでもいいので1行書けばOK
@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バッチまとめ

test.bat
@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が変化する)=====

test.bat
@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を参照できない)=====

ERRORLEVELを自分でセットすると、値が変化しなくなる(test.bat)
@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%」という思い込みがあったので気付くのに時間がかかってしまいました、、、。

%ERRORLEVEL%だとうまくいかない(test.bat)
@echo off
if 1 == 1 (
    rem 間違ったコマンド errorlevel → 0以外
	type
	if %ERRORLEVEL% neq 0 (
	    echo hoge
	)
)
exit /b

----- "hoge" が出力されるはずだったのだが、、、 -----
C:\Users\microsoft\Desktop>test.bat
コマンドの構文が誤っています。
!ERRORLEVEL!を使うとうまくいく
@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文内のラベルに飛ぶ、ということは基本的にはできないと考えた方がよさそうです。
gotoforループの外に飛んで**break処理のようにすることは可能**です。

また**callは使用できる**ので、これで関数っぽいものを作ってfor文内をいくらかコンパクトにする、といったことはできそうです。

test.bat
@echo off
for /l %%i in (1,1,3) do (
	echo %%i
)
exit /b

----- 通常の動き -----
C:\Users\microsoft\Desktop>test.bat
1
2
3
for内でラベルを使うと、挙動がおかしくなる
@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
) の使い方が誤っています。
callは使用できる(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文にbreakcontinueの仕様はない

バッチのforにはbreak(forループ自体を強制終了する)やcontinue(このループだけ終了して次のループを実行する)といった仕様はないです。
なので、そういったことをしたいのであればforではなくラベルを使ってループ処理を組むのがよさそうです。

gotoとラベルを使えばforでも**break処理のようにすることは可能**です。

また、以下のように適当な環境変数とif文を使って後続処理の続行を管理することで一応continueっぽいことはできます

バッチのfor文で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文の中でエラーが生じている場合には、その中のどこでエラーが生じているかまでは特定できないので、あくまで目安程度です。

echo_offコマンドを使用しない場合(デフォルト)
----- 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つになり得ます。
下図のように、コピー元とペースト先をそれぞれ行単位で指定することで、貼り付けたときにインデントが崩れないので、ネスト構造を崩すリスクが減ります。

地味ですが重要なテクニックです。
indent.png
目次に戻る

3. 一から1つずつ組み立てる

コード修正時にいま一度for文/if文の構造を見直したいと感じた方は、下手にサボらずに別ファイルで一からfor文/if文を作り直すことをお勧めします。

コードを書き直す人も、いまから新しく作り始める人も意識した方が良いと感じたのは、ちゃんと想定した条件の時にfor文やif文に入ってくれることを確認してから中身を埋めていくという事です。

例えば下記のようなコードを作りたい場合には、まず外側のfor文だけの状態から初めて、次に中のifelseifの中のforという風に1つずつ足しながら動作確認していく、といった感じです。

test.bat
@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

step1:まずもっとも外側のforループだけ
@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
step2:内側のifだけ加えて試す
@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
step3:elseを追加
@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
step4:if内のfor文を追加
@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
step5:本来の形にする
@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

目次に戻る

25
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?