バッチファイルでサブルーチンを活用すると、コードの重複をなくし、保守性・可読性が劇的に向上します。この記事では CALL :ラベル・引数の渡し方・戻り値・スコープ管理・再帰処理・エラーハンドリング まで、実務で使えるサブルーチンのすべてを解説します。
サブルーチンとは? バッチファイルにおける役割
サブルーチンとは、繰り返し呼び出せるコードブロックのことです。バッチファイルでは :ラベル で定義し、CALL :ラベル で呼び出します。
- 同じ処理を1か所にまとめて重複コードを排除
- バグ修正が1か所で済む(保守性UP)
- 処理の意図が名前で分かる(可読性UP)
- テスト・デバッグが単位ごとに可能
サブルーチンの基本構造
最小限の構造は以下の3要素です。
| 要素 | 記述 | 説明 |
|---|---|---|
| 定義 | :ラベル名 |
サブルーチンの開始点 |
| 呼び出し | CALL :ラベル名 |
サブルーチンを実行 |
| 終了 | EXIT /B または GOTO :EOF |
呼び出し元に制御を返す |
@echo off setlocal :: ---- メイン処理 ---- CALL :HelloWorld CALL :HelloWorld echo 2回呼び出しました GOTO :EOF :: ---- サブルーチン定義 ---- :HelloWorld echo Hello, World! EXIT /B
両者ともサブルーチンから呼び出し元に戻る命令ですが、
EXIT /B が推奨されます。GOTO :EOF は SETLOCAL を使った場合に変数スコープの扱いが微妙に異なるケースがあるためです。
EXIT /B を書き忘れると、そのサブルーチンの処理が終わった後、次のラベルに流れ込む(フォールスルー)バグが発生します。
:SubA echo SubA実行 :: EXIT /B を忘れた! :SubB echo SubBも勝手に実行される! ← フォールスルー EXIT /B
必ず各サブルーチンの末尾に EXIT /B を付けてください。
引数の渡し方
サブルーチン呼び出し時に値を渡すには、CALL :ラベル 引数1 引数2 のように後ろに続けます。受け取り側では %1 %2 で参照します。
@echo off setlocal :: サブルーチンに2つの引数を渡す CALL :Greet "田中" "おはようございます" CALL :Greet "佐藤" "こんにちは" GOTO :EOF :Greet set name=%~1 set msg=%~2 echo %name%さん、%msg% EXIT /B
%1 はダブルクォートを含めた文字列(例: "田中")%~1 はダブルクォートを除去した文字列(例: 田中)文字列を扱う場合は %~1 を使うのが安全です。
引数が多い場合・可変長引数:SHIFT を使う
引数が9個を超える場合や可変長引数には SHIFT を使います。SHIFT は %1 を捨てて %2 を %1 にずらします。
@echo off
setlocal
CALL :PrintAll "apple" "banana" "cherry" "date"
GOTO :EOF
:PrintAll
:PrintAllLoop
if "%~1"=="" EXIT /B
echo - %~1
SHIFT
GOTO :PrintAllLoop
実行結果:
- apple - banana - cherry - date
戻り値の返し方
バッチファイルには関数の戻り値構文がないため、変数に書き込む・ERRORLEVELを使うの2パターンで対応します。
パターン1:変数に結果を格納(推奨)
@echo off setlocal CALL :Add 10 25 echo 合計: %RESULT% GOTO :EOF :Add set /A RESULT=%~1 + %~2 EXIT /B
パターン2:ERRORLEVELで成否を返す
@echo off
setlocal
CALL :FileCheck "C: arget.txt"
if %ERRORLEVEL% EQU 0 (
echo ファイルが存在します
) else (
echo ファイルが見つかりません
)
GOTO :EOF
:FileCheck
if exist %~1 (
EXIT /B 0
) else (
EXIT /B 1
)
EXIT /B 0 で成功(0)、EXIT /B 1 以上で失敗を示すのが慣習です。サブルーチン内で外部コマンドを実行すると ERRORLEVEL が上書きされるため、必要なら即座に変数に退避してください。
変数のスコープ管理(SETLOCAL/ENDLOCAL)
サブルーチン内の変数がメインに漏れると予期しないバグの原因になります。SETLOCAL/ENDLOCAL で変数の有効範囲を制御しましょう。
@echo off setlocal set MSG=メイン処理中 CALL :Sub echo メインのMSG: %MSG% GOTO :EOF :Sub setlocal set MSG=サブルーチン内 echo サブのMSG: %MSG% endlocal :: ここで MSG はメインの値に戻る EXIT /B
実行結果:
サブのMSG: サブルーチン内 メインのMSG: メイン処理中
サブルーチンから呼び出し元に値を返すテクニック
ENDLOCAL と同時に変数をセットすることで、ローカルスコープを閉じながら値を外に渡せます。
:CalcDouble setlocal set /A LOCAL_RESULT=%~1 * 2 endlocal & set RESULT=%LOCAL_RESULT% EXIT /B
endlocal & set RESULT=%LOCAL_RESULT% は1行で実行されるため、endlocal 直前の LOCAL_RESULT の値が RESULT に渡されます。これはバッチファイルの定番テクニックとして広く使われています。
複数サブルーチンを組み合わせる
実際の業務バッチでは複数のサブルーチンを順番に呼び出して処理を構成します。
@echo off
setlocal
:: ---- メイン ----
CALL :Init
CALL :Process
CALL :Cleanup
GOTO :EOF
:: ---- 初期化 ----
:Init
echo [Init] 開始
set LOG_FILE=process.log
set ERROR_COUNT=0
EXIT /B
:: ---- メイン処理 ----
:Process
echo [Process] データ処理中...
for %%F in (data*.csv) do (
CALL :ProcessFile "%%F"
)
EXIT /B
:ProcessFile
echo 処理中: %~1
:: ファイル処理のロジック
EXIT /B
:: ---- 終了処理 ----
:Cleanup
echo [Cleanup] 完了。エラー数: %ERROR_COUNT%
EXIT /B
再帰処理(自分自身を呼び出す)
サブルーチンから自分自身を CALL することで再帰処理が実現できます。ただしバッチファイルのスタックは有限なので深い再帰には向きません(目安:数十回まで)。
例:フォルダを再帰的に処理する
@echo off
setlocal
CALL :ScanDir "C: arget"
GOTO :EOF
:ScanDir
echo [DIR] %~1
for /D %%D in ("%~1*") do (
CALL :ScanDir "%%D"
)
for %%F in ("%~1*.*") do (
echo [FILE] %%~nxF
)
EXIT /B
例:階乗計算(数値再帰)
@echo off
setlocal
set N=5
CALL :Factorial %N%
echo %N%! = %FACT%
GOTO :EOF
:Factorial
if %~1 LEQ 1 (
set FACT=1
EXIT /B
)
set /A PREV=%~1 - 1
CALL :Factorial %PREV%
set /A FACT=%FACT% * %~1
EXIT /B
再帰の深さが増えるほど処理が遅くなります。また
SETLOCAL を再帰内で使う場合は ENDLOCAL を必ず対応させてください。再帰が深くなりそうな場合は for /R や FORFILES /S などの組み込み再帰オプションを優先検討してください。
エラーハンドリングをサブルーチン化する
エラー処理を1つのサブルーチンにまとめると、一貫したエラーログ出力が実現できます。
@echo off setlocal set LOG_FILE=error.log :: コマンド実行後にエラーチェック xcopy /Y "source*" "dest" >nul 2>&1 if %ERRORLEVEL% NEQ 0 CALL :OnError "xcopy失敗" %ERRORLEVEL% exit /B :: ---- エラーハンドラ ---- :OnError set ERR_MSG=%~1 set ERR_CODE=%~2 echo [ERROR] %ERR_MSG% (ERRORLEVEL=%ERR_CODE%) echo %DATE% %TIME% [ERROR] %ERR_MSG% (code=%ERR_CODE%) >> "%LOG_FILE%" EXIT /B
実務パターン3選
パターン1:ログ出力サブルーチン(タイムスタンプ付き)
@echo off setlocal CALL :Log "INFO" "バッチ開始" CALL :Log "INFO" "処理A 実行" CALL :Log "WARN" "対象ファイルが0件" CALL :Log "INFO" "バッチ終了" GOTO :EOF :Log set LEVEL=%~1 set MESSAGE=%~2 set TIMESTAMP=%DATE% %TIME:~0,8% echo [%TIMESTAMP%] [%LEVEL%] %MESSAGE% echo [%TIMESTAMP%] [%LEVEL%] %MESSAGE% >> batch.log EXIT /B
パターン2:入力バリデーション
@echo off setlocal set TARGET_DIR=C:dataackup CALL :ValidateDir "%TARGET_DIR%" if %ERRORLEVEL% NEQ 0 ( echo エラー: ディレクトリが無効です EXIT /B 1 ) echo ディレクトリOK GOTO :EOF :ValidateDir if "%~1"=="" EXIT /B 1 if not exist %~1\ EXIT /B 1 EXIT /B 0
パターン3:リトライ付きコマンド実行
サブルーチン内にループ用の内部ラベルを置く場合、スクリプト全体でユニークな名前を付けてください(名前衝突を防ぐため、接頭辞をつけるのがおすすめです)。
@echo off
setlocal
CALL :Retry_Ping 3
if %ERRORLEVEL% NEQ 0 echo ping失敗
GOTO :EOF
:: リトライ付きping(最大MAX回)
:Retry_Ping
set /A RETRY_MAX=%~1
set /A RETRY_CNT=0
:Retry_Ping_Loop
set /A RETRY_CNT+=1
ping -n 1 192.168.1.1 >nul 2>&1
if %ERRORLEVEL% EQU 0 EXIT /B 0
if %RETRY_CNT% GEQ %RETRY_MAX% EXIT /B 1
echo 失敗(%RETRY_CNT%/%RETRY_MAX%)。リトライ中...
timeout /T 2 /NOBREAK >nul
GOTO :Retry_Ping_Loop
サブルーチン設計の注意点まとめ
| 注意点 | 悪い例 | 良い例 |
|---|---|---|
| スコープ汚染 | サブルーチン内でグローバル変数を直接書き換える | setlocal/endlocal でスコープを閉じる |
| 終了命令の漏れ | サブルーチンの後に EXIT /B がない(次のラベルに流れ込む) |
必ず EXIT /B で終わらせる |
| 引数のクォート | %1(スペース含む文字列で分割される) |
%~1 または "%~1" |
| ERRORLEVEL上書き | 外部コマンド後にERRORLEVELを確認しないまま処理継続 | 重要なERRORLEVELは即変数に退避 |
| ラベル名衝突 | サブルーチン内部ラベルに汎用名(:Loop 等)を使う |
サブルーチン名を接頭辞にした固有名(:Sub_Loop) |
| 深い再帰 | 再帰呼び出しを100回以上繰り返す | for /R 等で代替するか上限を設ける |
よくある質問(FAQ)
❓ CALL :ラベル と GOTO :ラベル の違いは? (クリックで開閉)
CALL :ラベル はサブルーチン呼び出し。処理後に 呼び出し元の次の行に戻ってきます。
GOTO :ラベル は単純なジャンプ。戻る機能はなく、呼び出し元には戻りません。
ループや条件分岐の中断には GOTO、再利用可能な処理には CALL :ラベル を使い分けてください。
❓ サブルーチンから文字列(複数単語)を返すには? (クリックで開閉)
文字列をそのまま変数に代入してください。RESULT変数に格納するのが最も簡単です。
:GetMessage set RESULT=Hello World EXIT /B
呼び出し元では echo %RESULT% でそのまま使えます。スペースを含む場合は echo "%RESULT%" のようにクォートしてください。
❓ サブルーチンは何個まで作れますか? (クリックで開閉)
明確な上限はありません。実際には数十〜数百のサブルーチンを持つバッチも存在します。ただしバッチファイル全体の行数・サイズが増えると見通しが悪くなるため、処理が大規模になる場合は PowerShell や Python への移行を検討してください。
❓ サブルーチン内から別のサブルーチンを呼べますか? (クリックで開閉)
はい、呼べます。CALL :SubA の中から CALL :SubB を呼ぶネスト呼び出しは問題なく動作します。ただし、呼び出しのネストが深くなるとデバッグが難しくなるため、1〜2段程度に抑えるのが実務上の推奨です。
よくある質問(FAQ)
set RESULT=値し、呼び出し元でCALL後に%RESULT%を参照します。SETLOCAL/ENDLOCALを使っている場合は変数がスコープ外に出ないため、ENDLOCALの前にendlocal & set RESULT=%RESULT%のように一時的に移す技法が必要です。CALL :SUB_NAME arg1 arg2で引数を渡し、サブルーチン内では%1・%2で受け取ります。スペースを含む引数はダブルクォートで囲みます:CALL :SUB "value with spaces"。引数の数はSHIFTコマンドでずらすことができます。まとめ
| テクニック | キーワード | 用途 |
|---|---|---|
| 基本呼び出し | CALL :ラベル |
サブルーチン実行・呼び元に戻る |
| 引数渡し | CALL :ラベル 値1 値2 → %~1 |
パラメータ付き処理 |
| 可変長引数 | SHIFT + GOTO :ループラベル |
引数の数が不定の処理 |
| 戻り値(変数) | set RESULT=値 |
計算結果・文字列を返す |
| 戻り値(成否) | EXIT /B 0 / EXIT /B 1 |
ERRORLEVEL で成否判定 |
| スコープ制御 | setlocal / endlocal & set |
変数の漏れ防止+値の受け渡し |
| 再帰 | CALL :自分自身 |
ディレクトリ走査・数列計算 |
| エラー処理 | CALL :OnError |
一貫したエラーログ出力 |
サブルーチンを使いこなすと、バッチファイルが構造化・再利用可能・テスト可能なスクリプトに変わります。まず「ログ出力」「エラーハンドラ」の2つをサブルーチン化するだけでも、保守性が大幅に向上します。