ワンランク上のbatchを書いてみた その1

# バッチファイルで関数化してみる
バッチファイルでのログ出力とエラーハンドリングの実装例
本記事では、Windows環境で動作するバッチファイル `main.bat` と、そのログ出力ライブラリ `loglib.bat` を用いたサンプルコードを解析しながら、ログ出力やエラーハンドリングの実装方法について解説します。
外部ファイルを参照できるようにして、コード管理や使いまわしなども考慮してみました。

## サンプルコードの概要

今回説明するサンプルコードは以下の2つのファイルで構成されています。
なお、`Command002` では、意図的にエラーが発生するように書いてあります。

- **main.bat**  
  メインの処理を記述したバッチファイル。ログの出力やエラーハンドリング、各種コマンドの呼び出しなどを行っています。

- **loglib.bat**  
  ログ出力専用のライブラリ。指定されたメッセージをコンソールとログファイルの両方に出力します。

## main.bat の詳細解説

以下に `main.bat` の内容とその解説を示します。

```bat
@echo off
setlocal enabledelayedexpansion

REM メイン処理開始のログ出力
call :LogMessage スクリプト開始


set COUNT=0
for /L %%i in (1,1,3) do (
    set /a COUNT+=1
    call :Command00!COUNT!
    call :LogMessage 3second wait time.
    timeout /t 3 >nul
)

REM --- 正常終了 ---
call :LogMessage スクリプト正常終了
goto end

REM --- コマンド1の実行 ---
:Command001
call :LogMessage Command00!COUNT!の実行開始

dir >>nul 2>&1

if errorlevel 1 (
    call :LogMessage ERROR: Command00!COUNT!に失敗。エラーコード: %errorlevel%
    goto errorHandler
)

call :LogMessage Command00!COUNT!の実行成功
exit /b

:Command002
REM --- コマンド2の実行（意図的にエラーを発生） ---
call :LogMessage Command00!COUNT!の実行開始 (意図的にエラー発生)

cmd /c exit 1 >>nul 2>&1

if errorlevel 1 (
    call :LogMessage ERROR: Command00!COUNT!に失敗。エラーコード: %errorlevel%
    goto errorHandler
)
call :LogMessage コマンド2の実行成功
exit /b

REM --- コマンド3の実行 ---
:Command003
call :LogMessage Command00!COUNT!の実行開始

dir >>nul 2>&1

if errorlevel 1 (
    call :LogMessage ERROR: Command00!COUNT!に失敗。エラーコード: %errorlevel%
    goto errorHandler
)

call :LogMessage Command00!COUNT!の実行成功
exit /b


:errorHandler
call :LogMessage スクリプトはエラーにより終了しました
goto end

REM --- ログ出力関数 ---
:LogMessage
REM 引数 %~1 を受け取ってログ出力する（%~1：ダブルクオート除去済みの第1引数）
call loglib.bat %*
exit /b


:end
endlocal
exit /b


```

### 主なポイント

- **ログ出力の統一管理**  
  `:LogMessage` というラベルを用意し、ログ出力処理を共通化しています。  
  ※内部では `loglib.bat` を呼び出しており、出力先はコンソールとログファイルです。

- **ループ処理とコマンド実行**  
  `for %%i in (a b)` により、2回のループを実行。ループ内では `COUNT` をインクリメントし、動的に `:Command001` や `:Command002` を呼び出しています（`call :Command00!COUNT!`）。

- **エラーチェック**  
  各コマンド実行後に `if errorlevel 1` を使用してエラー判定を行い、エラー発生時には `:errorHandler` ラベルにジャンプしてエラーログを出力しています。

- **待機処理**  
  `timeout /t 3` により、各コマンド実行後に3秒の待機を入れることで、処理の間隔を調整しています。

## loglib.bat の詳細解説

次に、ログ出力ライブラリ `loglib.bat` の内容を見てみます。

```bat
@echo off
setlocal

REM ログフォルダとログファイルの設定
set "LOGDIR=%~dp0logs"
if not exist "%LOGDIR%" mkdir "%LOGDIR%"
set "LOGFILE=%LOGDIR%\execution.log"

REM 引数（すべての引数を連結）をログに出力
echo [%date% %time%] %*
echo [%date% %time%] %* >> "%LOGFILE%"

endlocal
exit /b
```

### 主なポイント

- **ログディレクトリの作成**  
  `%~dp0` を用いて、実行ファイルと同じディレクトリに `logs` フォルダを作成しています。  
  ※ログディレクトリが存在しない場合は自動的に作成します。

- **ログファイルへの出力**  
  `execution.log` というファイルに、日付と時刻を先頭に付加した形でメッセージを追記しています。  
  ※コンソール出力も同時に行っています。

## まとめ

今回のサンプルコードでは、以下のような実装テクニックが確認できます。

- **モジュール化**  
  ログ出力処理を `loglib.bat` にまとめることで、メインスクリプトからの呼び出しが容易になっています。

- **エラーハンドリング**  
  各コマンド実行後のエラーチェックとエラー発生時の一元管理が実現されています。

- **遅延展開の利用**  
  `setlocal enabledelayedexpansion` を利用することで、ループ内で変数の更新を正しく反映させています。

これらのテクニックは、バッチファイルを用いた自動化スクリプト作成時に非常に有用です。特にログ管理とエラー検出の実装は、スクリプトの信頼性向上に大きく寄与します。

---

## 補足
`setlocal enabledelayedexpansion` は、バッチファイル内で「遅延環境変数展開」を有効にするためのコマンドです。

### 詳細な説明

- **setlocal**  
  バッチファイルの実行環境を一時的にローカルなものに切り替えます。`endlocal` コマンドが実行されるか、バッチファイルの実行が終了するまで、環境変数の変更はその範囲内でのみ有効になります。

- **enabledelayedexpansion**  
  通常、バッチファイルでは変数は行が実行される前に展開（置換）されます。そのため、ループや条件分岐内で変数の値を変更しても、同じコードブロック内ではその変更が反映されないことがあります。  
  「遅延環境変数展開」を有効にすると、変数を `%VAR%` ではなく `!VAR!` のように記述することで、変数の値が実際に実行される時点で展開され、更新後の最新の値を取得することができます。

### 利用例

```bat
@echo off
setlocal enabledelayedexpansion

set COUNT=0
for %%i in (a b c) do (
    set /a COUNT+=1
    echo 現在のカウント: !COUNT!
)

endlocal
```

上記の例では、`!COUNT!` を使うことでループ内で更新された最新の `COUNT` の値を表示できます。

この機能は、特にループや条件分岐で変数の値を動的に更新・参照する場合に非常に便利です。

## おわりに

本記事では、`main.bat` と `loglib.bat` のサンプルコードを通して、バッチファイルでのログ出力およびエラーハンドリングの実装例を紹介しました。実際の運用では、さらに複雑な処理やエラーハンドリングが必要となる場合もありますが、本サンプルがその一助となれば幸いです。

皆さんの環境での活用例や改善アイデアなど、ぜひコメントで教えていただけると嬉しいです！

## おまけ
`loglib.bat`を別の角度から活用した例となります。
`batchFiles`というフォルダに、複数のバッチファイルが存在していると仮定して、
それらを一括実行した際に、実行結果を受け取れるという内容になっている。


```bat:batchRun.bat
@echo off
setlocal enabledelayedexpansion
REM --- 実行対象のバッチファイルが格納されているフォルダのパス設定 ---
set "batchFolder=%~dp0batchFiles"

REM メイン処理開始のログ出力
call :LogMessage スクリプト開始

call :Init
call :Main


REM --- 正常終了 ---
call :LogMessage スクリプト正常終了
goto end

REM --- 実行対象のバッチファイルが格納されているフォルダのパス設定 ---
:Init
set "batchFolder=%~dp0batchFiles"
call :LogMessage 初期化します

REM --- フォルダの存在確認 ---
if not exist "%batchFolder%" (
    call :LogMessage %batchFolder%が見つかりません。
    goto errorHandler
)
call :LogMessage 初期化、完了
exit /b

REM --- フォルダ内のすべてのバッチファイル (*.bat) を順次実行 ---
:Main
call :LogMessage バッチファイル実行
for %%F in ("%batchFolder%\*.bat") do (
    call :LogMessage ======================================
    call :LogMessage 実行中: %%~nxF
    call "%%F"
    if errorlevel 1 (
        call :LogMessage ERROR: %%~nxF の実行に失敗しました。エラーコード: %errorlevel%
    )
    call :LogMessage ======================================
    timeout /t 3 >nul
)
if errorlevel 1 (
    call :LogMessage ERROR: バッチの実行に失敗しました。エラーコード: %errorlevel%
    goto errorHandler
)

goto end
exit /b

:errorHandler
call :LogMessage スクリプトはエラーにより終了しました
goto end

REM --- ログ出力関数 ---
:LogMessage
call loglib.bat echo %*
exit /b


:end
endlocal
exit /b

```

---