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

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

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

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

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

###### ==========【目次】==========
**構文エラーエトセトラ**
・[rem の使い方が誤っています。](#rem-の使い方が誤っています)
・[elseは`)`と同じ行に書かないとエラーになる](#elseはと同じ行に書かないとエラーになる)
・[echo の出力文字の中に半角`(` `)`を使用しない](#echo-の出力文字の中に半角-を使用しない)
・[if文の`(` `)`の中が空だとエラーになる](#if文の-の中が空だとエラーになる)
・[`if`文の条件式で、環境変数が空だとエラーになる](#if文の条件式で環境変数が空だとエラーになる)
・[`ERRORLEVEL`に値をsetしてはいけない](#errorlevelに値をsetしてはいけない)
・[`if`/`for`文内のERRORLEVELは、`!ERRORLEVEL!`で参照する](#iffor文内のerrorlevelはerrorlevelで参照する)
・[`for`文内ではラベルを使用できない](#for文内ではラベルを使用できない)
・[`for`文に`break`や`continue`の仕様はない](#for文にbreakやcontinueの仕様はない)（代替案）

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

# 構文エラーエトセトラ
## rem の使い方が誤っています。
作ったバッチを実行して最初に出くわしたエラーです。

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

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


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

```bat:test.bat
@echo off
rem verコマンドは、Windowsのバージョン情報を出力する
ver
if 1 == 1 (
    echo hoge
) ver

exit /b
```
```bat:出力結果
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つの命令文として認識される）

```bat: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`文が完結しているとみなされているため、おかしな挙動になっています。

```bat: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
```

```bat:実際には、こんな感じの読み取られ方をしています
@echo off
if 1 == 2 (
    echo hoge (hoge
)
echo hogehoge
rem ) else (   ## NOP(行ごと無視) 
echo piyo
rem )          ## NOP(行ごと無視)
exit /b
```
```bat:エスケープか全角カッコを使えば想定通りの挙動になる
@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行書けばエラーではなくなります**。

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

----- 出力結果 -----
C:\Users\microsoft\Desktop>test.bat
) の使い方が誤っています。
```
```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バッチまとめ](https://qiita.com/tomotagwork/items/5b9e08f28d5925d96b5f)）



```bat: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**という環境変数が作られてしまうことが原因のようです。

これを解決するには、
<font color=blue>**`set ERRORLEVEL=`（右辺に何も書かない）**</font>
を実行して、新しく作られた**ERRORLEVEL**をリセットすればOKです。

`ERRORLEVEL`を変化させたい場合には、`set`するのではなく意図的にコマンドエラーを起こすようにしましょう。

=====  〇通常の場合（`ERRORLEVEL`が変化する）=====

```bat: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`を参照できない）=====

```bat: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%`**」という思い込みがあったので気付くのに時間がかかってしまいました、、、。

```bat:%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
コマンドの構文が誤っています。
```

```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`文内のラベルに飛ぶ、ということは基本的にはできないと考えた方がよさそうです。
`goto`で`for`ループの外に飛んで**`break`処理のようにすることは可能**です。

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

```bat: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
```
```bat: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
```
```bat:ラベルの直後に")"があるとエラーになる
@echo off
for /l %%i in (1,1,3) do (
	echo %%i
	:hoge	
)
exit /b

C:\Users\microsoft\Desktop>test.bat
) の使い方が誤っています。
```
```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`文に`break`や`continue`の仕様はない
バッチの`for`には`break`（forループ自体を強制終了する）や`continue`（このループだけ終了して次のループを実行する）といった仕様はないです。
なので、そういったことをしたいのであれば`for`ではなくラベルを使ってループ処理を組むのがよさそうです。

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

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

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

```bat: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. インデントをきちんと整える
バッチではこれまで書いてきた`(` `)`の<font color=silver>~~クソ~~</font>仕様と相まって、他の言語以上に`for`文/`if`文をデリケートに管理しないと痛い目を見ます（見た）。

なので、インデント調整もかなり重要なファクターだと感じました。
今までインデント調整をサボってきた方は、ぜひこの機会にしっかり意識することをお勧めします。

【[目次に戻る](#目次)】

### 2. 行ごとコピペ
他の箇所に書いたコードを別の箇所にコピペする場合、ペースト先をカーソルで指定するとどうしても貼り付けたときにインデントが崩れるので、それがネスト構造が崩れる原因の1つになり得ます。
下図のように、コピー元とペースト先をそれぞれ行単位で指定することで、貼り付けたときにインデントが崩れないので、ネスト構造を崩すリスクが減ります。

地味ですが重要なテクニックです。
![indent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/650537/9503f51c-5f99-5d24-9056-d1bbdd16030a.png)
【[目次に戻る](#目次)】

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

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

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

```bat: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
```
---

```bat: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
```

```bat: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
```
```bat: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
```
```bat: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
```
```bat: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
```

【[目次に戻る](#目次)】