9. Subroutines with call & goto¶
What & why¶
Batch has no functions in the normal sense, but you can fake them with labels and call :label. This gives you reusable, parameterised blocks with their own arguments and a way to return a value. goto handles plain jumps and loops.
Labels and goto¶
A label is a line starting with :. goto label jumps there.
@echo off
goto start
:helper
echo (this is skipped on the way down)
:start
echo Running.
if "%1"=="" goto usage
echo Arg: %1
goto :eof
:usage
echo Usage: %~nx0 ^<name^>
exit /b 1
goto :eof is a built-in label meaning "end of file" — it ends the current script or subroutine cleanly. (The colon is required: goto :eof, not goto eof.)
call :label — a subroutine¶
@echo off
setlocal
call :greet World
call :greet "Ada Lovelace"
call :add 3 4
echo Back in main.
exit /b 0
:greet
echo Hello, %~1!
goto :eof
:add
set /a _sum = %1 + %2
echo Sum is %_sum%
goto :eof
Key points:
call :greet Worldjumps to:greet, runs untilgoto :eof(orexit /b), then returns to the line after thecall.- Inside the subroutine,
%1 %2 ...are the subroutine's own arguments, not the script's.%0is the label. %~1strips quotes, same modifiers as always.- Put
exit /b 0before your first label so main execution doesn't "fall through" into the subroutines.
Returning a value¶
Batch subroutines can't return x. Two patterns:
1. Set a variable the caller reads¶
Here the caller passes the name of the output variable (result) as %2, and the subroutine assigns to it. This works because a called subroutine shares the caller's variable scope (no automatic setlocal).
2. Return via exit code (for small integers / status)¶
call :is_even 7
if errorlevel 1 (echo odd) else (echo even)
exit /b 0
:is_even
set /a "r = %~1 %% 2"
exit /b %r%
exit /b N sets errorlevel to N and returns from the subroutine. exit /b (no number) returns without changing errorlevel. exit without /b closes the entire Command Prompt — rarely what you want in a script.
call for other things¶
call also re-runs the parser on a line, which lets you do a double expansion:
And call another batch file (returns control after it finishes, unlike a bare invocation which... also returns, but call is explicit and required if you want %errorlevel% and to come back reliably):
Without call, invoking build.bat from inside run.bat transfers control and never comes back to run.bat.
Recursion¶
A subroutine can call itself. Use setlocal/endlocal inside to keep each level's variables separate, and guard the depth:
call :countdown 5
exit /b 0
:countdown
if %~1 lss 0 goto :eof
echo %~1
set /a "next=%~1 - 1"
call :countdown %next%
goto :eof
More in Advanced Lesson 1.
A clean script skeleton¶
@echo off
setlocal EnableExtensions
if "%~1"=="" goto :usage
call :main %*
exit /b %errorlevel%
:main
echo Doing work with: %*
call :log "started"
rem ... real work ...
call :log "finished"
goto :eof
:log
echo [%date% %time%] %~1
goto :eof
:usage
echo Usage: %~nx0 ^<args^>
exit /b 1
Common mistakes¶
- Falling through into subroutines — forgot
exit /bbefore the first label. Every run "also" executes every subroutine top-to-bottom. goto eofinstead ofgoto :eof—eofis a label you probably don't have → "cannot find the batch label."- Calling a subroutine without
call—:greetalone doesn't call it;goto :greetjumps and never returns. - Invoking another
.batwithoutcall— control doesn't return. exitvsexit /b—exitkills the whole shell/window.- Expecting subroutine
%1to be the script's%1— it's the subroutine's own arg list. - Label names with spaces or after
:a space —: mylabelmay not matchgoto :mylabel. No space after the colon. gotointo aforloop body — doesn't resume the loop.
Exercises¶
- Write a script with a
:usagelabel andgoto :usagewhen%1is empty; otherwisecall :run %*. - Write
:max a bthat prints the larger of two numbers. Call it three times with different pairs. - Write
:square n outvarthat sets the named variable ton*n. Verify%result%after the call. - Write
:is_prime nthat returns0viaexit /bif prime,1if not. Test it on 2, 9, 13, 15, 17. - Write a recursive
:factorial n outvar. Compute 5! (= 120) and 7! (= 5040). Note where it overflows 32-bit. - Demonstrate the fall-through bug: put a subroutine at the bottom with an
echo, omitexit /bbefore it, run, and observe it runs unbidden. Fix it.
Recap¶
- Labels are
:name;goto :namejumps;goto :eofends the script/subroutine. call :name argsruns a subroutine and returns to the next line; inside,%1..%9are the subroutine's args.- Put
exit /bbefore the first label to avoid fall-through. - Return values by assigning to a caller-named variable, or via
exit /b N+errorlevel. call other.batreturns control; bareother.batdoes not.exit /breturns from the script;exitcloses the shell.