【bat】バッチファイルのサブルーチン完全ガイド|CALL・引数・戻り値・再帰・実務パターン

バッチファイルでサブルーチンを活用すると、コードの重複をなくし、保守性・可読性が劇的に向上します。この記事では 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 vs GOTO :EOF
両者ともサブルーチンから呼び出し元に戻る命令ですが、EXIT /B が推奨されます。
GOTO :EOFSETLOCAL を使った場合に変数スコープの扱いが微妙に異なるケースがあるためです。
【よくあるミス】EXIT /B を忘れるとフォールスルーが発生
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 はダブルクォートを含めた文字列(例: "田中"
%~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
  )
ERRORLEVEL の注意点
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 /RFORFILES /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)

Q. バッチファイルのサブルーチンで戻り値を返すにはどうすればよいですか?
A. バッチには関数の戻り値機能はありません。環境変数を使って値を返します:サブルーチン内でset RESULT=値し、呼び出し元でCALL後に%RESULT%を参照します。SETLOCAL/ENDLOCALを使っている場合は変数がスコープ外に出ないため、ENDLOCALの前にendlocal & set RESULT=%RESULT%のように一時的に移す技法が必要です。
Q. サブルーチンに引数を渡すにはどうすればよいですか?
A. CALL :SUB_NAME arg1 arg2で引数を渡し、サブルーチン内では%1%2で受け取ります。スペースを含む引数はダブルクォートで囲みます:CALL :SUB "value with spaces"。引数の数はSHIFTコマンドでずらすことができます。
Q. 再帰呼び出し(サブルーチンから自分を呼ぶ)はバッチで可能ですか?
A. 技術的には可能ですが、スタックの深さに制限があり深い再帰はエラーになります。再帰を使う場合はSETLOCAL/ENDLOCALを各呼び出しで使ってスコープを管理します。バッチの再帰は主に階乗計算などのデモで見られますが、実務では再帰ではなくループで実装する方が安全です。

まとめ

テクニック キーワード 用途
基本呼び出し CALL :ラベル サブルーチン実行・呼び元に戻る
引数渡し CALL :ラベル 値1 値2%~1 パラメータ付き処理
可変長引数 SHIFT + GOTO :ループラベル 引数の数が不定の処理
戻り値(変数) set RESULT=値 計算結果・文字列を返す
戻り値(成否) EXIT /B 0 / EXIT /B 1 ERRORLEVEL で成否判定
スコープ制御 setlocal / endlocal & set 変数の漏れ防止+値の受け渡し
再帰 CALL :自分自身 ディレクトリ走査・数列計算
エラー処理 CALL :OnError 一貫したエラーログ出力

サブルーチンを使いこなすと、バッチファイルが構造化・再利用可能・テスト可能なスクリプトに変わります。まず「ログ出力」「エラーハンドラ」の2つをサブルーチン化するだけでも、保守性が大幅に向上します。