バッチ(.bat)で、パラメータの引き渡し、if文／for文などするテンプレつくった

# はじめに

.batをどうしても書かなきゃいけない場面に遭遇したとき、
今まで勉強してた言語っぽく書いても上手く動いてくれず苦戦することがあったので
.bat書く際のテンプレつくりました。

他の何らかの言語がそこそこに書けること前提で、
- **パラメータを引き渡す**
- **とりあえずifとfor**
- **処理途中でエラー終了させる**
- **別のbatchを呼ぶ**
この4つの入ったテンプレです。

細部を掘り下げるってよりかは、大体こんな感じのテンプレ作っておけば、色々とアレンジ利くであろうって感じです。

- 初めから見たい人 ... 『(1) -> 2 -> 3 -> 4 -> 5 -> 6』
- テンプレを見たい人 ... 『6のみ』


## １．パラメータ宣言

下記のような形で、実行引数を渡してバッチの中で使いたい。

```
test_batch.bat -log test_batch.log -workdir 【WORK_DIR】 -l 【USER/PASS】 -j 【JDBC_URL】
```

上記のコマンドを実行したとき、
%0=test_batch.bat
%1=-log
%2=test_batch.log
... みたいな感じで実行時に引数が入ってくる。
これを何とかして、batchの変数に詰める。

```
rem /* START PARAM SETTING */
rem /* デフォルトのパラメータ設定する場合は予め変数入れる */
:START_PARAM_LOOP
IF "%1"=="" GOTO END_PARAM_LOOP
  if not "%1"=="-log" goto PARAM_ELSE1
     set OUTPUT_LOG=%2
     echo TEST_BATCH is started. >> %OUTPUT_LOG%
     goto PARAM_END_IF
:PARAM_ELSE1
  if not "%1"=="-target" goto PARAM_ELSE2
     set TARGET_DIR=%2
     goto PARAM_END_IF
:PARAM_ELSE2
  IF NOT "%1"=="-l" GOTO PARAM_ELSE3
      SET USERPASS=%2
      GOTO PARAM_END_IF
:PARAM_ELSE3
  IF NOT "%1"=="-j" GOTO PARAM_ERR
      SET ORACLE_JDBC=%2
      GOTO PARAM_END_IF
:PARAM_ERR
    echo.
    echo "%1 " >> %OUTPUT_LOG% >> %OUTPUT_LOG%
    echo "%0 PARAMETER ERROR" >> %OUTPUT_LOG%
    echo "%1 " >> %OUTPUT_LOG%
    echo "%0 PARAMETER ERROR"
    echo     -log         test_batch.batのログファイル名
    echo     -l           sqlplus接続USER/PASSパス(ex.【USER】/【PASS】)
    echo     -j           sqlplus接続JDBC_URL(ex.jdbc:oracle:thin:@【HOST】:【PORT】:【SID】)
    echo     -target      バージョンごとのフォルダを格納しているディレクトリ
    echo.
    goto PARAM_ERROR_END
:PARAM_END_IF
    shift /1
    shift /1
    goto START_PARAM_LOOP
:END_PARAM_LOOP
rem /* END PARAM SETTING */

:PARAM_ERROR_END
      rem /* 本バッチの実行パラメータが正しくない時 */
      echo PARAMETER ERROR OF test_batch.bat
      echo PARAMETER ERROR OF test_batch.bat >> %OUTPUT_LOG%
      exit /b 1
```


#### 上記でやっていることの解説
2個ずつ(%1と%2)取って2個ずつshiftしていって、

- 引数なくなったら実処理スタートする
- 引数ある場合はオプション：オプション値のセットを取ってきて変数に詰める
- 引数あるけど、定義していないオプションがあればエラーに飛ばす

の繰り返し。

## ２．if文
#### 文字列
`"`で囲む。

```
set VAR=HOGE
if "%VAR"="%STRING" (

) else (

)
```

#### 数値
#### 数値比較する際の演算子
- EQU(==)
- NEQ(!=)
- LSS(<)
- GTR(>)
- LEQ(<=)
- GEQ(>=)

数値型の変数を入れる場合は、/a で数値として扱うことを明記する。

```
set /a VAR=1
if %VAR% EQU %NUMERIC% (

) else (

)
```


## ３．for文
我々が普段使ってるプログラミング言語(java, c++, pythonなど)っぽくfor文を書きたいときは、
`setlocal enabledelayedexpansion`で始めて、
`endlocal`で終える。

また、変数を使う時は `%【変数名】%`じゃなくて、
`!【変数名】!`使う。

for文の書き方については長くなりそうなので、下記の役立つQuita記事を紹介します。
・for文の書き方いろいろ
[ここが詳しい] https://qiita.com/plcherrim/items/67be34bab1fdf3fb87f9
・`%【変数名】%`を使った場合に何が起こるのか
※forどの段階においても、forの最終実行時のものを参照してしまう。
[ここが詳しい] https://qiita.com/mitsuru793/items/eff207178546d6d29237

#### 【FOLDER-ROOT】の中に入っている、ファイルとディレクトリを名前順で取ってきてechoする

```
setlocal enabledelayedexpansion
for /f "usebackq tokens=1" %%i in (`dir /b /O:N 【FOLDER-ROOT】`) do (
    set FOLDER=%%i
    echo TARGET FOLDER: !FOLDER! >> %OUTPUT_LOG%
)
endlocal
```

## ４．エラー終了する
各処理中にエラー終了させたいときは、
gotoで、処理中に飛ばしてEXITさせる。

gotoしないで、if文の中で`exit /b 1`みたいな形でエラー終了させてもいいが、
共通のエラー終了ロジックを使いたい場合は、
下記のようにバッチファイルの最後に書いておくと使いやすい。

```
rem /* ERROR FUNCTION START */
:PARAM_ERROR_END
      rem /* 本バッチの実行パラメータが正しくない時 */
      echo PARAMETER ERROR OF test_batch.bat
      echo PARAMETER ERROR OF test_batch.bat >> %OUTPUT_LOG%
      exit /b 1
:NOT_FOUND_VERSION
      rem /* Section 1でVERSIONが取得できなかった時 */
      exit /b 1
rem /* ERROR FUNCTION END */
```

## ５．別のbatchを呼ぶ
■前提：カレントディレクトリにある`get_oracle_version.bat`を実行する。
メインのbatchからbatchファイルを分割するのであれば、基本callだと思う。

#### callコマンド
呼び元のバッチは、callでキックされたバッチが**終わるまで待つ**。

```
call get_oracle_version.bat
```

#### startコマンド
呼び元のバッチは、startでキックされたバッチが**終わるのを待たない**。

```
start get_oracle_version.bat
```

## ６．全部ぶっこんでみた

テンプレの共有なので、敢えて中身の解説はしないです。

```
@echo off
rem @@   [編集履歴]
rem @@   日付            VERSION           開発者      修正内容
rem @@--------------------------------------------------------------------------------------
rem @@   YYYY/MM/DD    【VERSION】         にらんけん   test_batch.bat
rem @@--------------------------------------------------------------------------------------
echo *****************************************************************
rem /* START PARAM SETTING */
rem /* デフォルトのパラメータ設定する場合は予め変数入れる */
:START_PARAM_LOOP
IF "%1"=="" GOTO END_PARAM_LOOP
  if not "%1"=="-log" goto PARAM_ELSE1
     set OUTPUT_LOG=%2
     echo TEST_BATCH is started. >> %OUTPUT_LOG%
     goto PARAM_END_IF
:PARAM_ELSE1
  if not "%1"=="-target" goto PARAM_ELSE2
     set TARGET_DIR=%2
     goto PARAM_END_IF
:PARAM_ELSE2
  IF NOT "%1"=="-l" GOTO PARAM_ELSE3
      SET USERPASS=%2
      GOTO PARAM_END_IF
:PARAM_ELSE3
  IF NOT "%1"=="-j" GOTO PARAM_ELSE4
      SET ORACLE_JDBC=%2
      GOTO PARAM_END_IF
:PARAM_ELSE4
  IF NOT "%1"=="-dest" GOTO PARAM_ERR
      SET DEST_DIR=%2
      GOTO PARAM_END_IF
:PARAM_ERR
    echo.
    echo "%1 " >> %OUTPUT_LOG% >> %OUTPUT_LOG%
    echo "%0 PARAMETER ERROR" >> %OUTPUT_LOG%
    echo "%1 " >> %OUTPUT_LOG%
    echo "%0 PARAMETER ERROR"
    echo     -log         test_batch.batのログファイル名
    echo     -l           sqlplus接続USER/PASSパス(ex.【USER】/【PASS】)
    echo     -j           sqlplus接続JDBC_URL(ex.jdbc:oracle:thin:@【HOST】:【PORT】:【SID】)
    echo     -target      バージョンごとのコピー対象フォルダを格納しているディレクトリ
    echo     -dest        フォルダコピー先
    echo.
    goto PARAM_ERROR_END
:PARAM_END_IF
    shift /1
    shift /1
    goto START_PARAM_LOOP
:END_PARAM_LOOP
rem /* END PARAM SETTING */

rem /* START BATCH MAIN */
echo ************************** >> %OUTPUT_LOG%
echo **** 1. VERSION-CHECK **** >> %OUTPUT_LOG%
echo ************************** >> %OUTPUT_LOG%
rem /* ここにORACLE-DBから、sqlplusを使ってVERSIONを取得するロジックを記載する(ORACLE_JDBC, USERPASSを使う) */
call get_oracle_version.bat -output latest_ver6.txt -j %ORACLE_JDBC% -l %USERPASS%
for /f %%i in (latest_ver6.txt) do (
    set /a VERSION6=%%i
)
if [%VERSION6%]==[] (
    GOTO NOT_FOUND_VERSION
) else (
    echo VERSION6: %VERSION6% >> %ORACLE_LOG%
)

echo **************************** >> %OUTPUT_LOG%
echo ** 2. COPY-FOLDER-VERSION ** >> %OUTPUT_LOG%
echo **************************** >> %OUTPUT_LOG%
rem /* ORACLE-DBから取得したアプリのバージョンから、コピーするフォルダを選択してコピーする */
set CHECKED_FOLDER=FALSE

rem /* .%TARGET_DIR%の中にあるフォルダ名を1個ずつ取得して、バージョンにマッチするフォルダをコピーする */
setlocal enabledelayedexpansion
for /f "usebackq tokens=1" %%i in (`dir /b /O:N %TARGET_DIR%`) do (
    set FOLDER=%%i
    echo TARGET FOLDER: !FOLDER! >> %OUTPUT_LOG%
    rem /* "test_【開始VER】-【終了VER】"のうち、_の後ろ(【開始VER】-【終了VER】)を取得する */
    for /f "tokens=2 delims=_" %%A in ("!FOLDER!") do (
        set FOLDER_VERSION=%%A
        echo FOLDER_VERSION: !FOLDER_VERSION! >> %OUTPUT_LOG%
    )
    rem /* "【開始VER】-【終了VER】"のうち、-の前(【開始VER】=%%A)と後ろ(【終了VER】=%%B)を取得する */
    for /f "tokens=1,2 delims=-" %%A in ("!FOLDER_VERSION!") do (
        if [%%A]==[] (
            rem /* 下限なし */
            set /a FROM_FOLDER_VER6=
        ) else (
            set /a FROM_FOLDER_VER6=%%A
        )
        if [%%B]==[] (
            rem /* 上限なし */
            set /a TO_FOLDER_VER6=
        ) else (
            set /a TO_FOLDER_VER6=%%B
        )
    )
    
    echo FROM_FOLDER_VER6: !FROM_FOLDER_VER6!
    echo TO_FOLDER_VER6: !TO_FOLDER_VER6!

    rem /* ここに、!FROM_FOLDER_VER6! <= %VERSION6% <= !TO_FOLDER_VER6! を満たすときに、 */
    rem /* !FOLDER!を%DEST_DIR%にコピーするロジックを記載する。DEST_DIRはパラメータで渡す。 */
    rem /* ただし、値が無いとき=下限なしor上限なしに注意する */
)
endlocal

if "%CHECKED_FOLDER%"=="TRUE" (
    echo %VERSION6% の条件にマッチするフォルダのコピーが完了しました。
    echo %VERSION6% の条件にマッチするフォルダのコピーが完了しました。 >> %OUTPUT_LOG%
    exit /b 0
) else (
　　goto NOT_FOUND_FOLDER
)
rem /* END BATCH MAIN */

rem /* ERROR FUNCTION START */
:PARAM_ERROR_END
      rem /* 本バッチの実行パラメータが正しくない時 */
      echo PARAMETER ERROR OF test_batch.bat
      echo PARAMETER ERROR OF test_batch.bat >> %OUTPUT_LOG%
      exit /b 1
:NOT_FOUND_VERSION
      rem /* Section 1でVERSIONが取得できなかった時 */
   　 echo CANNOT GET VERSION FROM ORACLE DB... (-j:%ORACLE_JDBC%, -l:%USERPASS%)
   　 echo CANNOT GET VERSION FROM ORACLE DB... (-j:%ORACLE_JDBC%, -l:%USERPASS%) >> %OUTPUT_LOG%
      exit /b 1
:NOT_FOUND_FOLDER
      rem /* Section 2でVERSIONに該当するフォルダが取得できなかった時 */
   　 echo TARGET VERSION FOLDER IS NOT FOUND FOR %VERSION6%...
   　 echo TARGET VERSION FOLDER IS NOT FOUND FOR %VERSION6%... >> %OUTPUT_LOG%
      exit /b 1
rem /* ERROR FUNCTION END */
```

■前提：-targetで与えた、`%TARGET_DIR%`には下記のようなフォルダが入っている。
① "~ バージョン:123456"の場合にコピーするフォルダ
→ `ver_-123456`
② "バージョン:123457 ~ バージョン:222222"の場合にコピーするフォルダ
→ `ver_123457-222222`
③ "バージョン:222223 ~"の場合にコピーするフォルダ
→ `ver_222223-`


# おわりに
Windows Serverを運用していて、batの保守で書かざるを得ないときあると思います。
僕は今の仕事で山のようなバッチファイルを読んだり修正せねばならないことがあり、
結構苦戦しました。（.batやめたいが本音ですが）

何かしらお役に立てる幸いです。