Going back to
jeb's original post, I find it interesting that the worst performing return is when the return environment has delayed expansion disabled. Yet the absolute simplest solution exists for disabled delayed expansion if the return value does not contain the newline character - a FOR /F can easily return the value without worrying about any escaping.
I decided to develop different techniques for handling delayed expansion enabled vs disabled, and return value containing or not containing newline. The enabled delayed expansion code is based on jeb's code. I then experimented with ways of dynamically applying the most efficient method possible, depending on the circumstance.
I also added another option to specify the returned ERRORLEVEL. Values of 0 and 1 can be generated very efficiently. But values greater than 1 are more problematic. I started by using
cmd /c exit N, but that was unacceptably slow on my home machine. I switched to using a CALLed subroutine with
exit /b N.
I now have two optional arguments - the number of active SETLOCAL, and the returned ERRORLEVEL. I opted to make those optional arguments named arguments of the form
local=N and
err=M. I adapted jeb's original code to my style with the new optional arguments.
I also put all the required macro code in a stand-alone library script. The ugly CALLed functions are embedded within the script. It is very convenient to load the macro from any script as needed.
I wrote 3 new versions: macro.rtnDave1.bat, macro.rtnDave2.bat, and macro.rtnDave3.bat, along with my version of jeb's original code - macro.rtnJeb.bat.
I also wrote a modified test script that provides timing data. The test script expects a single argument value of dave1, dave2, dave3, or jeb. The argument controls which version of the macro gets loaded into memory.
testReturn.batCode: Select all
@echo off
cls
setlocal DisableDelayedExpansion
call macro.rtn%1
set /a test_cnt=0
set "value1=%%1^a!"^^b"
set "value2=caret^"
set "value3="
set "value4====Equals"
rem &"&^&!!!^^%^!
set ^"value5=^&"&^&!!!^^%%^!"
setlocal enabledelayedexpansion
set "value6=Hello &^^ "^^^^ ^&" world^!!CR!*!LF!X"
set "value7=Line1!LF!Line2!LF!"
set "value8=Line1!CR!!LF!Line2"
set "value9=CR!CR!"
echo(
for /L %%n in (1 1 9) do call :Test_Both value%%n
set "input="
set "singleLine=!value1!"
set "multiLine =!value6!"
for %%a in ("SingleLine" "MultiLine ") do for %%b in ("Disable" " Enable") do for %%c in (0 1 2) do (
set "beg=!time!"
setlocal %%~bDelayedExpansion
for /l %%N in (1 1 5000) do (
setlocal enableDelayedExpansion
set "input=!%%~a!"
%returnVar% result input local=1 err=%%c
)
endlocal
set "end=!time!"
set /a "t1=t2=0"
for /f "tokens=1-4 delims=:." %%a in ("!beg: =0!") do set /a "t1=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100"
for /f "tokens=1-4 delims=:." %%a in ("!end: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100"
set /a "diff=t2-t1"
if !diff! lss 0 set /a "diff+=24*60*60*100"
echo %%~a %%~b err=%%c : !diff!
)
exit /b
:Test_Both
setlocal EnableDelayedExpansion
echo ------- %1 -------------------- text='!%1!'
for %%x in (0 1 2) do (
call :Test_context Disable Disable %1 %%x
call :Test_context Disable Enable %1 %%x
call :Test_context Enable Disable %1 %%x
call :Test_context Enable Enable %1 %%x
)
echo(
exit /b
:Test_context
set /a test_cnt+=1
setlocal %1DelayedExpansion
set "result=not set by macro"
call :startTest %2 %3 %4
REM Output the result
set "error=%errorlevel%"
setlocal enabledelayedexpansion
set "input=inputNotSet"
set "format1= Test #!test_cnt! "
set "format2=%1/%2 "
set "postfix=<FAIL>"
if "!result!"=="!%~3!" if %error% equ %4 set "postfix= OK "
echo !format1:~0,10! err=%4 !postfix! !format2:~0,15! err=!error! result='!result!'
exit /b
:startTest
setlocal enableDelayedExpansion
set "input=!%2!"
setlocal %1DelayedExpansion
%ReturnVar% result input local=2 err=%3
exit /b
macro.rtnJeb.batCode: Select all
@echo off
if not defined returnVar call :loadMacro&exit /b
if "%~1" neq ":returnDisabled" exit /b %2
setlocal enableDelayedExpansion
set "rtn=!%2!"
set "rtn=!rtn:%%=%%3!"
set "rtn=!rtn:""n=%%~L!"
set "rtn=!rtn:""r=%%4!"
set "rtn=!rtn:""q=%%~5!"
for /f "tokens=1-4" %%3 in (^"%% !CR! """") do (
endlocal
set "%2=%rtn:~1%"
exit /b %%X
)
:loadMacro
if "!" equ "" (
>2 echo ERROR: Delayed expansion must be off when loading the returnVar macro.
exit /b 1
)
set LF=^
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
for /F "usebackq delims= " %%C in (`copy /z "%~f0" nul`) do set "CR=%%C"
set ^"returnVar=for %%# in (1 2) do if %%#==2 (setlocal enableDelayedExpansion%\n%
for /f "tokens=1-4" %%1 in ("!returnVar.args!") do (%\n%
set "rtn=x!%%2!"%\n%
set /a "err=!errorlevel!, local=1"%\n%
for %%A in ("%%~3" "%%~4") do if %%A neq "" set /a "%%~A"%\n%
set /a "local+=1"%\n%
for /f %%R in ("!CR!!CR!") do for %%L in ("!LF!") do for %%X in (!err!) do (%\n%
set ^"rtn=!rtn:"=""q!"%\n%
set "rtn=!rtn:%%R=""r!"%\n%
set "rtn=!rtn:%%~L=""n!"%\n%
set "rtnDis=!rtn!"%\n%
set "rtn=!rtn:^=^^!"%\n%
set "path="%\n%
set "pathExt=;"%\n%
call set "rtn=%%rtn:^!=""c^!%%"%\n%
set "rtn=!rtn:""c=^!"%\n%
for /f "delims=" %%D in (""!rtnDis!"") do for /f "delims=" %%E in (""!rtn!"") do (%\n%
for /l %%n in (1 1 !local!) do endlocal%\n%
if "!"=="" (%\n%
set "%%1=%%~E" !%\n%
set "%%1=!%%1:""n=%%~L!"%\n%
set "%%1=!%%1:""r=%%R!"%\n%
set ^"%%1=!%%1:""q="!"%\n%
set "%%1=!%%1:~1!"%\n%
if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
) else (%\n%
set "%%1=%%~D"%\n%
call "%~f0" :returnDisabled %%1%\n%
)%\n%
)%\n%
)%\n%
)) else set returnVar.args="
exit /b 0
macro.rtnDave1.bat - new code if value does not contain newline (delayed expansion enabled or disabled)
Code: Select all
@echo off
if not defined returnVar call :loadMacro&exit /b
if "%~1" neq ":returnDisabled" exit /b %2
setlocal enableDelayedExpansion
set "rtn=!%2!"
set "rtn=!rtn:%%=%%3!"
set "rtn=!rtn:""n=%%~L!"
set "rtn=!rtn:""r=%%4!"
set "rtn=!rtn:""q=%%~5!"
for /f "tokens=1-4" %%3 in (^"%% !CR! """") do (
endlocal
set "%2=%rtn%"
exit /b %%X
)
:loadMacro
if "!" equ "" (
>2 echo ERROR: Delayed expansion must be off when loading the returnVar macro.
exit /b 1
)
set LF=^
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
for /F "usebackq delims= " %%C in (`copy /z "%~f0" nul`) do set "CR=%%C"
::returnVar OutVar InVar [Local=N] [Err=N]
set ^"ReturnVar=for %%# in (1 2) do if %%#==2 (setlocal enableDelayedExpansion%\n%
for /f "tokens=1-4" %%1 in ("!returnVar.args!") do (%\n%
set "rtn="!%%2!""%\n%
set /a "err=!errorlevel!, local=1"%\n%
for %%A in ("%%~3" "%%~4") do if %%A neq "" set /a "%%~A"%\n%
set /a "local+=1"%\n%
for %%L in ("!LF!") do for /f %%R in ("!CR!!CR!") do for %%X in (!err!) do (%\n%
if "!rtn:%%~L=!" equ "!rtn!" (%\n%
for /f "delims=" %%V in ("!rtn!") do (%\n%
for /l %%N in (1 1 !local!) do endlocal%\n%
if "!!" neq "" (%\n%
set "%%1=%%~V"%\n%
) else (%\n%
setlocal disableDelayedExpansion%\n%
set "rtn=%%V"%\n%
setlocal enableDelayedExpansion%\n%
for /f %%R in ("!CR!!CR!") do (%\n%
set "rtn=!rtn:@=@A!"%\n%
set "rtn=!rtn:"=@Q!^"%\n%
set "rtn=!rtn:%%R=@R!"%\n%
set "rtn=!rtn:^=@C@C!"%\n%
setlocal disableDelayedExpansion%\n%
set "path="%\n%
set "pathext=;"%\n%
call set "rtn=%%rtn:!=@C!%%"%\n%
setlocal enableDelayedExpansion%\n%
set "rtn=!rtn:@C=^!"%\n%
set "rtn=!rtn:@R=%%R!"%\n%
set "rtn=!rtn:@Q="!^"%\n%
set "rtn=!rtn:@A=@!"%\n%
for /f "delims=" %%V in ("!rtn!") do (%\n%
endlocal^&endlocal^&endlocal^&endlocal%\n%
set "%%1=%%~V"!%\n%
)%\n%
)%\n%
)%\n%
if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
)%\n%
) else (%\n%
set "rtn=!rtn:~1,-1!"%\n%
set ^"rtn=!rtn:"=""q!"%\n%
set "rtn=!rtn:%%R=""r!"%\n%
set "rtn=!rtn:%%~L=""n!"%\n%
set "rtnDis=!rtn!"%\n%
set "rtn=!rtn:^=^^!"%\n%
set "path="%\n%
set "pathExt=;"%\n%
call set "rtn=%%rtn:^!=""c^!%%"%\n%
set "rtn=!rtn:""c=^!"%\n%
for /f "delims=" %%D in ("!rtnDis!") do for /f "delims=" %%E in ("!rtn!") do (%\n%
for /l %%n in (1 1 !local!) do endlocal%\n%
if "!"=="" (%\n%
set "%%1=%%~E" !%\n%
set "%%1=!%%1:""n=%%~L!"%\n%
set "%%1=!%%1:""r=%%R!"%\n%
set ^"%%1=!%%1:""q="!"%\n%
if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
) else (%\n%
set "%%1=%%~D"%\n%
call "%~f0" :returnDisabled %%1%\n%
)%\n%
)%\n%
)%\n%
)%\n%
)) else set returnVar.args=^"
exit /b
macro.rtnDave2.bat - new code only if value does not contain newline and delayed expansion is disabled
Code: Select all
@echo off
if not defined returnVar call :loadMacro&exit /b
if "%~1" neq ":returnDisabled" exit /b %2
setlocal enableDelayedExpansion
set "rtn=!%2!"
set "rtn=!rtn:%%=%%3!"
set "rtn=!rtn:""n=%%~L!"
set "rtn=!rtn:""r=%%4!"
set "rtn=!rtn:""q=%%~5!"
for /f "tokens=1-4" %%3 in (^"%% !CR! """") do (
endlocal
set "%2=%rtn%"
exit /b %%X
)
:loadMacro
if "!" equ "" (
>2 echo ERROR: Delayed expansion must be off when loading the returnVar macro.
exit /b 1
)
set LF=^
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
for /F "usebackq delims= " %%C in (`copy /z "%~f0" nul`) do set "CR=%%C"
::returnVar OutVar InVar [Local=N] [Err=N]
set ^"ReturnVar=for %%# in (1 2) do if %%#==2 (setlocal enableDelayedExpansion%\n%
for /f "tokens=1-4" %%1 in ("!returnVar.args!") do (%\n%
set "rtn="!%%2!""%\n%
set /a "err=!errorlevel!, local=1"%\n%
for %%A in ("%%~3" "%%~4") do if %%A neq "" set /a "%%~A"%\n%
set /a "local+=1"%\n%
for %%L in ("!LF!") do for /f %%R in ("!CR!!CR!") do for %%X in (!err!) do (%\n%
if "!rtn:%%~L=!" neq "!rtn!" (set returnVar.part2=1) else set "returnVar.part2="%\n%
if not defined returnVar.part2 for /f "delims=" %%V in ("!rtn!") do (%\n%
for /l %%N in (1 1 !local!) do endlocal%\n%
if %%V equ "" (%\n%
set "%%1=%%~V"%\n%
set "returnVar.part2="%\n%
) else if "!" neq "" (%\n%
set "%%1=%%~V"%\n%
set "returnVar.part2="%\n%
) else (%\n%
setlocal disableDelayedExpansion%\n%
set "rtn=%%V"%\n%
setlocal enableDelayedExpansion%\n%
set "returnVar.part2=1"%\n%
set local=2%\n%
)%\n%
if not defined returnVar.part2 if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
)%\n%
if defined returnVar.part2 (%\n%
set "returnVar.part2="%\n%
set "rtn=!rtn:~1,-1!"%\n%
set ^"rtn=!rtn:"=""q!"%\n%
set "rtn=!rtn:%%R=""r!"%\n%
set "rtn=!rtn:%%~L=""n!"%\n%
set "rtnDis=!rtn!"%\n%
set "rtn=!rtn:^=^^!"%\n%
set "path="%\n%
set "pathExt=;"%\n%
call set "rtn=%%rtn:^!=""c^!%%"%\n%
set "rtn=!rtn:""c=^!"%\n%
for /f "delims=" %%D in ("!rtnDis!") do for /f "delims=" %%E in ("!rtn!") do (%\n%
for /l %%n in (1 1 !local!) do endlocal%\n%
if "!"=="" (%\n%
set "%%1=%%~E" !%\n%
set "%%1=!%%1:""n=%%~L!"%\n%
set "%%1=!%%1:""r=%%R!"%\n%
set ^"%%1=!%%1:""q="!"%\n%
if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
) else (%\n%
set "%%1=%%~D"%\n%
call "%~f0" :returnDisabled %%1%\n%
)%\n%
)%\n%
)%\n%
)%\n%
)) else set returnVar.args=^"
exit /b
macro.rtnDave3.bat - new code only if value does not contain newline (or is empty) and delayed expansion is disabled. More escaping done up front before deciding on which technique to use.
Code: Select all
@echo off
if not defined returnVar call :loadMacro&exit /b
if "%~1" neq ":returnDisabled" exit /b %2
setlocal enableDelayedExpansion
set "rtn=!%2!"
set "rtn=!rtn:%%=%%3!"
set "rtn=!rtn:""n=%%~L!"
set "rtn=!rtn:""r=%%4!"
set "rtn=!rtn:""q=%%~5!"
for /f "tokens=1-4" %%3 in (^"%% !CR! """") do (
endlocal
set "%2=%rtn:~1%"
exit /b %%X
)
:loadMacro
if "!" equ "" (
>2 echo ERROR: Delayed expansion must be off when loading the returnVar macro.
exit /b 1
)
set LF=^
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
for /F "usebackq delims= " %%C in (`copy /z "%~f0" nul`) do set "CR=%%C"
::returnVar OutVar InVar [Local=N] [Err=N]
set ^"ReturnVar=for %%# in (1 2) do if %%#==2 (setlocal enableDelayedExpansion%\n%
for /f "tokens=1-4" %%1 in ("!returnVar.args!") do (%\n%
set "rtn=.!%%2!"%\n%
set /a "err=!errorlevel!, local=1"%\n%
for %%A in ("%%~3" "%%~4") do if %%A neq "" set /a "%%~A"%\n%
set /a "local+=1"%\n%
for %%L in ("!LF!") do for /f %%R in ("!CR!!CR!") do for %%X in (!err!) do (%\n%
set ^"rtnE=!rtn:"=""q!"%\n%
set "rtnE=!rtnE:%%R=""r!"%\n%
set "rtnD=!rtnE:%%~L=""n!"%\n%
if !rtnD! neq !rtnE! (%\n%
set multiLn=1%\n%
set "rtnE=!rtnD!"%\n%
) else set "multiLn=0"%\n%
set "rtnE=!rtnE:^=^^!"%\n%
set "path="%\n%
set "pathExt=;"%\n%
call set "rtnE=%%rtnE:^!=""c^!%%"%\n%
set "rtnE=!rtnE:""c=^!"%\n%
set "returnVar.continue=1"%\n%
for /f "delims=" %%D in ("!rtnD!") do for /f "delims=" %%E in ("!rtnE!") do for %%M in (!multiLn!) do for /f "delims=" %%V in (""!rtn:~1!"") do if defined returnVar.continue (%\n%
for /l %%n in (1 1 !local!) do endlocal%\n%
if "!"=="" (%\n%
set "%%1=%%~E" !%\n%
set "%%1=!%%1:""n=%%~L!"%\n%
set "%%1=!%%1:""r=%%R!"%\n%
set ^"%%1=!%%1:""q="!"%\n%
set "%%1=!%%1:~1!"%\n%
if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
) else if %%M equ 0 (%\n%
set "%%1=%%~V"%\n%
if "%%X" equ "0" (call ) else if "%%X" equ "1" (call) else call "%~f0" :exitErr %%X%\n%
) else (%\n%
set "%%1=%%~D"%\n%
call "%~f0" :returnDisabled %%1%\n%
)%\n%
)%\n%
)%\n%
)) else set returnVar.args=^"
exit /b
Here are the timing results, two runs for each test. I think I prefer the Dave2 version.Code: Select all
Timing for 5000 Iterations (centiseconds)
TEST | JEB | DAVE1 | DAVE2 | DAVE3
-------------------------+-----------+-----------+-----------+----------
SingleLine Disable err=0 | 3778 3772 | 1392 1483 | 1194 1196 | 1778 1781
SingleLine Disable err=1 | 3772 3767 | 1366 1370 | 1194 1196 | 1791 1794
SingleLine Disable err=2 | 3777 3755 | 2344 2725 | 2014 2381 | 3105 3120
-------------------------+-----------+-----------+-----------+----------
SingleLine Enable err=0 | 1236 1226 | 2504 2508 | 2015 2020 | 1873 1876
SingleLine Enable err=1 | 1246 1237 | 2503 2508 | 1931 1930 | 1876 1882
SingleLine Enable err=2 | 2381 2376 | 3503 3887 | 2732 3099 | 3257 3267
-------------------------+-----------+-----------+-----------+----------
MultiLine Disable err=0 | 3819 3800 | 4550 5365 | 3524 4291 | 5246 5270
MultiLine Disable err=1 | 3822 3814 | 4556 5377 | 3520 4278 | 5260 5298
MultiLine Disable err=2 | 3806 3804 | 4543 5364 | 3497 4286 | 5263 5300
-------------------------+-----------+-----------+-----------+----------
MultiLine Enable err=0 | 1274 1273 | 2014 2020 | 1643 1640 | 1989 1990
MultiLine Enable err=1 | 1283 1283 | 2019 2016 | 1646 1645 | 1990 1994
MultiLine Enable err=2 | 2433 2426 | 3027 3408 | 2442 2808 | 3368 3366
Dave Benham