Windows – Call subroutine where parameter contains ampersand in batch file

batch filewindows

How do I call a subroutine with a parameter being a variable containing an ampersand (&)?

There is no error, but the call never seems to execute.

example.bat

@echo off
setlocal enableDelayedExpansion

rem Doesn't work
set val=with^&ampersand
call :Output !val!

rem Works fine
set val=without_ampersand
call :Output !val!
goto End

:Output
set "line=%1"
echo Called: !line!
goto :eof

End:

Output:

Called: without_ampersand

Edit:

The usage of delayedExpansion is not required. It was just used in this example. A way to do it without using delayedExpansion is preferred.

The question is focusing on "How do I call" rather than "How do I initially set the variable". The variable may come from user input or a for /f loop (as is my case).

Best Answer

The batch escape rules are quite nasty, but the behavior is totally predictable if you know the rules.

The info you need to understand the problem is available at How does the Windows Command Interpreter (CMD.EXE) parse scripts? in phases 1, 2, 5, and 6 of the accepted answer. But good luck absorbing that info any time soon :-)

There are two fundamental design issues that lead to your problem: - Phase 6 doubles all carets, which then restarts phase 2 (actually phases 1, 1.5 and 2). - But phase 2 requires & to be escaped as ^&. Note it must be a single ^, not doubled!

The only way to get your approach to work is to introduce the ^ after the phase 6 caret doubling has occurred.

@echo off
setlocal enableDelayedExpansion
set "ESC=^"

rem Calling with delayed expansion value
set "val=with%%ESC%%&ampersand"
call :Output !val!

rem Calling with a string literal
call :Output with%%ESC%%^&ampersand

exit /b

:Output
set "line=%1"
echo Called: !line!
goto :eof

ESC is defined to hold ^.
The first round of phase 1 expands %%ESC%% to %ESC%
the second round of phase 1 (initiated by phase 6) expands %ESC% to ^

This is all totally impractical, especially if you don't know what the content is going to be.

The only sensible strategy to reliably pass any value into a CALLed routine is to pass by reference. Pass the name of a variable that contains the string value, and expand that value within your subroutine using delayed expansion.

@echo off
setlocal enableDelayedExpansion
set "val=with&ampersand"
call :Output val
exit /b

:Output
set "line=!%~1!"
echo Called: !line!
exit /b
Related Question