New Function Template - return ANY string safely and easily
Posted: 05 Jun 2011 00:00
Note: the code in this first post is out-of-date. Refer to the top post on the 2nd page of this thread for info on updated code. The post I am referring to is dated 23 Jun 2011 18:12
Summary: By packaging jeb's safe return technique with Ed Dyreen's concept of macros with arguments, I have developed a small macro library that enables simple conversion of any function into one that supports the return of any string supported by DOS. These converted functions are reliable even when called while delayed expansion is enabled.
The Problem: The function template provided by dostips.com works great for most "normal" situations. It is able to return numbers and simple strings accross the ENDLOCAL boundry. But if fails under the following circumstances:
1) The return string contains special characters such as ^ | < > & etc. Special characters must be either enclosed in quotes or escaped (but not both) if they are to be returned properly.
2) The function was called while delayed expansion was enabled and the return string contains ^ and/or ! The exclamation point must be escaped, even if it is within quotes, and the ^ must be escaped twice.
3) The return string contains carriage return <CR> or line feed <LF>. These are not normally found in DOS strings, but it is possible, and they can be extremely useful (especially <LF>). These characters require special handling when making assignments accross the ENDLOCAL boundry.
Writing generic code that knows how to deal with all three situations above is extremely tricky. I didn't think it was even possible, but jeb contributed amazing techniques to New functions :chr :asc :str2hex :hex2str that appear to be bullet proof.
The only problem left was how to easily apply jeb's safe return technique to any functions that require it. It is a significant amount of code.
I briefly tried to encapsulate the code into a function of its own, but quickly ran into a roadblock - A function cannot perform an ENDLOCAL for a SETLOCAL that was executed before the function was called. So jeb's technique could never be successfully deployed as functions.
The Solution: After first seeing Ed Dyreen's macros in the "SET /a -- Random Number?" post (why are we limited to 2 URLs per post? ), and experimenting with the syntax in my own Batch "macros" with arguments post, I realized I finally had the tools to package jeb's safe return technique as a convenient, easy to deploy macro library.
The function upgrade only requires calling a simple initialization macro at the top of the function, and replacing the ENDLOCAL block at function end with one or two additional macro calls. The macros that return a single value only add 7 msec to the function processing time.
I plan on a more comprehensive discussion of this library in my batch thread in the near future. But I wanted to make these macros available to function developers as soon as possible.
Included in this post are 4 files - each of which is self documenting:
1) Macro_RtnLib.bat - the focus of this post
2) Macro_TimerLib.bat - an auxilliary macro library that supports timing of batch operations - used in the following two demos to show the performance impact of adding the new functionality.
3) ucase.bat - a demonstration showing how to create a function that returns a single string using the macro library. I chose to convert a useful existing function :toUpper. I renamed the modified versions to :ucase to differentiate them from the original. Sample output is included after the code.
4) rtnTwoStrings.bat - a demonstration showing how to create a function that returns multiple string values using the macro library. I didn't have a useful existing function returning multiple values. So I created a very silly function for demonstration purposes only. I did not bother to include sample output for this file because it is so similar to the output of ucase.bat
Macro_RtnLib.bat - this file contains full documentation
Macro_TimerLib.bat - documentation embedded within file.
WernerGg discovered that the :toLower, :toUpper and toCamelCase have a serious bug in that they may fail depending on the name of the string variable that is passed in. (see the toLower Name dependent? post). I based the :ucase functions on a modified version of his solution to the problem. The ucase function capabable of returning any string is significantly faster than the original bugged version of :toUpper!
ucase.bat: This demonstrates some of the potential problems returning strings, and shows how the macros solve the problems. Timings of the various functions are included at the end.
Results of ucase.bat: The timing results at the end demonstrate that there
is very little overhead in calling the safe return macros.
rtnTwoStrings.bat - Uses a silly function to demonstrate how to return multiple values across the ENDLOCAL border.
Edits:
18-June-2011 - Fixed bug in macro_Rtn1 in file Macro_RtnLib.bat: Macro was generating a syntax error when called without optional RtnVar.
28-Sep-2011 - See note at top of this thread
Dave Benham
Summary: By packaging jeb's safe return technique with Ed Dyreen's concept of macros with arguments, I have developed a small macro library that enables simple conversion of any function into one that supports the return of any string supported by DOS. These converted functions are reliable even when called while delayed expansion is enabled.
The Problem: The function template provided by dostips.com works great for most "normal" situations. It is able to return numbers and simple strings accross the ENDLOCAL boundry. But if fails under the following circumstances:
1) The return string contains special characters such as ^ | < > & etc. Special characters must be either enclosed in quotes or escaped (but not both) if they are to be returned properly.
2) The function was called while delayed expansion was enabled and the return string contains ^ and/or ! The exclamation point must be escaped, even if it is within quotes, and the ^ must be escaped twice.
3) The return string contains carriage return <CR> or line feed <LF>. These are not normally found in DOS strings, but it is possible, and they can be extremely useful (especially <LF>). These characters require special handling when making assignments accross the ENDLOCAL boundry.
Writing generic code that knows how to deal with all three situations above is extremely tricky. I didn't think it was even possible, but jeb contributed amazing techniques to New functions :chr :asc :str2hex :hex2str that appear to be bullet proof.
The only problem left was how to easily apply jeb's safe return technique to any functions that require it. It is a significant amount of code.
I briefly tried to encapsulate the code into a function of its own, but quickly ran into a roadblock - A function cannot perform an ENDLOCAL for a SETLOCAL that was executed before the function was called. So jeb's technique could never be successfully deployed as functions.
The Solution: After first seeing Ed Dyreen's macros in the "SET /a -- Random Number?" post (why are we limited to 2 URLs per post? ), and experimenting with the syntax in my own Batch "macros" with arguments post, I realized I finally had the tools to package jeb's safe return technique as a convenient, easy to deploy macro library.
The function upgrade only requires calling a simple initialization macro at the top of the function, and replacing the ENDLOCAL block at function end with one or two additional macro calls. The macros that return a single value only add 7 msec to the function processing time.
I plan on a more comprehensive discussion of this library in my batch thread in the near future. But I wanted to make these macros available to function developers as soon as possible.
Included in this post are 4 files - each of which is self documenting:
1) Macro_RtnLib.bat - the focus of this post
2) Macro_TimerLib.bat - an auxilliary macro library that supports timing of batch operations - used in the following two demos to show the performance impact of adding the new functionality.
3) ucase.bat - a demonstration showing how to create a function that returns a single string using the macro library. I chose to convert a useful existing function :toUpper. I renamed the modified versions to :ucase to differentiate them from the original. Sample output is included after the code.
4) rtnTwoStrings.bat - a demonstration showing how to create a function that returns multiple string values using the macro library. I didn't have a useful existing function returning multiple values. So I created a very silly function for demonstration purposes only. I did not bother to include sample output for this file because it is so similar to the output of ucase.bat
Macro_RtnLib.bat - this file contains full documentation
Code: Select all
@echo off
:: This batch file will fail if called while delayed expansion is enabled.
::
:: This library defines macros and variables useful for creating other macros
:: and functions. Of particular note are macros that enable the return of any
:: string value(s) across the ENDLOCAL border. Routines built with these
:: macros are safe to call even when delayed expansion is enabled.
::
:: The library is designed to be installed in a directory in your PATH.
:: Any batch file that requires it can include it by simply placing the
:: following line of code at the top before any SETLOCAL:
::
:: IF NOT DEFINED macro\load.Macro_RtnLib CALL Macro_RtnLib
::
:: In this way the library becomes resident in your command shell environment
:: where it is available to any batch file that may need it. The IF condition
:: prevents unneccessary reloads of the same library.
::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: FUNCTION TEMPLATES
::
:: Function returning a single value that may contain any characters
:: Echoes the return value if RtnVar not specified
:: -------------------------------
:: :func inputVar [RtnVar]
:: %macro_InitFcnRtn%
:: setlocal
:: {do whatever you need to do}
:: set rtnValue={your return value}
:: %macro_Call% ("%errorlevel% 1 rtnValue %~2") %macro.AnyRtn1%
:: %macro_RtnAny%
:: exit /b
::
::
:: Function returning a single value that may contain any characters
:: EXCEPT <carriage return> or <line feed>
:: Echoes the return value if RtnVar not specified
:: -------------------------------
:: :func inputVar [RtnVar]
:: %macro_InitFcnRtn%
:: setlocal
:: {do whatever you need to do}
:: set rtnValue={your return value}
:: %macro_Call% ("%errorlevel% 1 rtnValue %~2") %macro.Rtn1%
:: exit /b
::
::
:: Function returning multiple values that may contain any characters
:: -------------------------------
:: :func inputVar RtnVar1 RtnVar2
:: %macro_InitFcnRtn%
:: setlocal
:: {do whatever you need to do}
:: set rtnVal1={your first return value}
:: set rtnVal2={your second return value}
:: %macro_Call% ("%errorlevel% 1 rtnVal1:%~2,rtnVal2:%~3") %macro.AnyRtnN%
:: %macro_RtnAny%
:: exit /b
::
::
:: Function returning multiple values that may contain any characters
:: EXCEPT <carriage return> or <line feed>
:: -------------------------------
:: :func inputVar RtnVar1 RtnVar2
:: %macro_InitFcnRtn%
:: setlocal
:: {do whatever you need to do}
:: set rtnVal1={your first return value}
:: set rtnVal2={your second return value}
:: %macro_Call% ("%errorlevel% 1 rtnVal1:%~2,rtnVal2:%~3") %macro.RtnN%
:: exit /b
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::define a Carriage Return string, only useable as !CR!
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
::define a Line Feed (newline) string (normally only used as !LF!)
set LF=^
::Above 2 blank lines are required - do not remove
::define a Line Feed string that can be used as %xLF%
set ^"xLF=^^^%LF%%LF%^%LF%%LF%"
::define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
::define FOR /F options that preserve the entire line
set macro_ForEntireLine=^^^^^^^"eol^^^^=^^^^^^^%LF%%LF%^%LF%%LF%^^^%LF%%LF%^%LF%%LF%^^^^ delims^^^^=^^^^^^^"
:: A simple macro used to call macros with arguments
:: Usage:
::
:: %macro_call% ("arg1 arg2 arg3...") %macro.macroName%
::
set macro_Call=for /f "tokens=1-26" %%a in
:: A simple macro used at the top of a macro definition to prepare
:: for a return by any of the Rtn macros.
set macro_InitRtn=setlocal^^^&set "NotDelayed=!"^^^&set "macro_inFcn="
:: A simple macro used at the top of a function definition to prepare
:: for a return by any of the Rtn macros.
set macro_InitFcnRtn=setlocal^&set "NotDelayed=!"^&set "macro_inFcn=true"
:: A simple macro for Rtn macro internal use
set "macro_RtnAnyPrefix=for /f "tokens=1-3" %%1 in ("!replace!") do for %%4 in ("!LF!") do "
::macro.AnyRtn1 ErrLvl EndLocalCnt ValueVar [RtnVar]
::
:: Returns the contents of variable ValueVar across the ENDLOCAL border at the
:: end of a function or macro. The number of ENDLOCAL executions is controlled
:: by the EndLocalCnt which should match the number of times SETLOCAL was used
:: within the calling function/macro.
::
:: The return value is stored in RtnVar
:: or the value is printed if RtnVar is not specified.
:: The ERRORLEVEL is set to ErrLvl
::
:: Numeric values ErrLvl and EndLocalCnt may be passed using any expression
:: supported by SET /A.
::
:: This macro can return a string containing any combination of characters
:: supported by DOS, and the macro works regardless whether the target return
:: environment has enabled or disabled delayed expansion.
::
:: In order for a function to use AnyRtn1, the calling function must start with
:: %%macro_InitFcnRtn%% at the top, and %%macro_RtnAny%% must immediately follow
:: the call to %%macro.AnyRtn1%%.
::
:: In order for a macro to use AnyRtn1, the calling macro must start with
:: %%macro_InitRtn%% at the top. Any call to a macro that uses AnyRtn1 must be
:: followed by %%macro_RtnAny%%. However, the macro call and %%macro_RtnAny%%
:: must not share a common statement block.
::
set macro.AnyRtn1=do (%\n%
setlocal enableDelayedExpansion%\n%
set /a "macro.AnyRtn1.ErrLvl=(%%~a), macro.AnyRtn1.EndLocalCnt=(%%~b)"%\n%
set "rtn=!%%~c!"%\n%
set ^"replace=%% ^"^"^" !CR!!CR!^"%\n%
set "macro_RtnAny=!macro_RtnAnyPrefix!endlocal&endlocal"%\n%
for /l %%N in (1,1,!macro.AnyRtn1.EndLocalCnt!) do set "macro_RtnAny=!macro_RtnAny!&endlocal"%\n%
if "%%~d" equ "" (echo:!rtn!) else (%\n%
if defined rtn (%\n%
set "rtn=!rtn:%%=%%~1!"%\n%
set ^"rtn=!rtn:^"=%%~2!^"%\n%
if defined CR for %%A in ("!CR!") do set "rtn=!rtn:%%~A=%%~3!"%\n%
for %%A in ("!LF!") do set "rtn=!rtn:%%~A=%%~4!"%\n%
if not defined NotDelayed (%\n%
set "rtn=!rtn:^=^^!"%\n%
call set "rtn=%%^rtn:^!=""^!%%" ! %\n%
set "rtn=!rtn:""=^!"%\n%
)%\n%
)%\n%
set "macro_RtnAny=!macro_RtnAny!&set "%%~d=!rtn!" ^!"%\n%
)%\n%
if defined macro_inFcn (%\n%
set "macro_RtnAny=!macro_RtnAny!&exit /b !macro.AnyRtn1.ErrLvl!"%\n%
) else if "!macro.AnyRtn1.ErrLvl!" == "1" (%\n%
set "macro_RtnAny=!macro_RtnAny!&(2>nul set =)"%\n%
) else if "!macro.AnyRtn1.ErrLvl!" neq "0" (%\n%
set "macro_RtnAny=!macro_RtnAny!&cmd /c exit !macro.AnyRtn1.ErrLvl!"%\n%
)%\n%
)
::macro.AnyRtnN ErrLvl EndLocalCnt ValueVar1:RtnVar1[,ValueVar2:RtnVar2]...
::
:: Returns the contents of multiple variables across the ENDLOCAL border at
:: the end of a function or macro. The number of ENDLOCAL executions is
:: controlled by the EndLocalCnt which should match the number of times
:: SETLOCAL was used within the calling function/macro. The errorlevel is
:: set to ErrLvl.
::
:: A pair of variable names must be specified for each output value - the
:: name of the variable containing the value followed by a colon followed
:: by the name of the variable that is to receive the value. Multiple pairs
:: are delimited by commas. The list of outputs cannot contain any spaces,
:: and the variable names cannot contain asterisk (*), question mark (?),
:: colon (:) or comma (,).
::
:: Numeric values ErrLvl and EndLocalCnt may be passed using any expression
:: supported by SET /A.
::
:: This macro can return strings containing any combination of characters
:: supported by DOS, and the macro works regardless whether the target return
:: environment has enabled or disabled delayed expansion.
::
:: In order for a function to use AnyRtn1, the calling function must start with
:: %%macro_InitFcnRtn%% at the top, and %%macro_RtnAny%% must immediately follow
:: the call to %%macro.AnyRtn1%%.
::
:: In order for a macro to use AnyRtnN, the calling macro must start with
:: %%macro_InitRtn%% at the top. Any call to a macro that uses AnyRtnN must be
:: followed by %%macro_RtnAny%%. However, the macro call and %%macro_RtnAny%%
:: must not share a common statement block.
::
set macro.AnyRtnN=do (%\n%
setlocal enableDelayedExpansion%\n%
set /a "macro.AnyRtn1.ErrLvl=(%%~a), macro.AnyRtn1.EndLocalCnt=(%%~b)"%\n%
set ^"replace=%% ^"^"^" !CR!!CR!^"%\n%
set "macro_RtnAny=!macro_RtnAnyPrefix!endlocal&endlocal"%\n%
for /l %%N in (1,1,!macro.AnyRtn1.EndLocalCnt!) do set "macro_RtnAny=!macro_RtnAny!&endlocal"%\n%
for %%c in (%%~c) do for /f "tokens=1,2 eol=: delims=:" %%c in ("%%c") do (%\n%
set "rtn=!%%~c!"%\n%
if defined rtn (%\n%
set "rtn=!rtn:%%=%%~1!"%\n%
set ^"rtn=!rtn:^"=%%~2!^"%\n%
if defined CR for %%A in ("!CR!") do set "rtn=!rtn:%%~A=%%~3!"%\n%
for %%A in ("!LF!") do set "rtn=!rtn:%%~A=%%~4!"%\n%
if not defined NotDelayed (%\n%
set "rtn=!rtn:^=^^!"%\n%
call set "rtn=%%^rtn:^!=""^!%%" ! %\n%
set "rtn=!rtn:""=^!"%\n%
)%\n%
)%\n%
set "macro_RtnAny=!macro_RtnAny!&set "%%~d=!rtn!" ^!"%\n%
)%\n%
if defined macro_inFcn (%\n%
set "macro_RtnAny=!macro_RtnAny!&exit /b !macro.AnyRtn1.ErrLvl!"%\n%
) else if "!macro.AnyRtn1.ErrLvl!" == "1" (%\n%
set "macro_RtnAny=!macro_RtnAny!&(2>nul set =)"%\n%
) else if "!macro.AnyRtn1.ErrLvl!" neq "0" (%\n%
set "macro_RtnAny=!macro_RtnAny!&cmd /c exit !macro.AnyRtn1.ErrLvl!"%\n%
)%\n%
)
::macro.Rtn1 ErrLvl EndLocalCnt ValueVar [RtnVar]
::
:: Returns the contents of variable ValueVar across the ENDLOCAL border at the
:: end of a function or macro. The number of ENDLOCAL executions is controlled
:: by the EndLocalCnt which should match the number of times SETLOCAL was used
:: within the calling function/macro.
::
:: The return value is stored in RtnVar
:: or the value is printed if RtnVar is not specified.
:: The ERRORLEVEL is set to ErrLvl
::
:: Numeric values ErrLvl and EndLocalCnt may be passed using any expression
:: supported by SET /A.
::
:: This macro can return a string containing any combination of characters
:: supported by DOS, except for 0x0A <Line Feed> or 0x0C <Carriage Return>.
:: The macro works regardless whether the target return environment has
:: enabled or disabled delayed expansion.
::
:: In order for a function to use Rtn1, the calling function must start with
:: %%macro_InitFcnRtn%% at the top.
::
:: In order for a macro to use Rtn1, the calling macro must start with
:: %%macro_InitRtn%% at the top.
::
set macro.Rtn1=do (%\n%
setlocal enableDelayedExpansion%\n%
set /a "macro.Rtn1.ErrLvl=(%%~a), macro.Rtn1.EndLocalCnt=(%%~b)+2"%\n%
set "rtn=!%%~c!"%\n%
if defined macro_inFcn (%\n%
set "errCmd=exit /b !macro.Rtn1.ErrLvl!"%\n%
) else if "!macro.Rtn1.ErrLvl!" == "0" (%\n%
set "errCmd=rem"%\n%
) else if "!macro.Rtn1.ErrLvl!" == "1" (%\n%
set "errCmd=set ="%\n%
) else (%\n%
set "errCmd=cmd /c exit !macro.Rtn1.ErrLvl!"%\n%
)%\n%
if "%%~d" equ "" (echo:!rtn!) else if defined rtn (%\n%
if not defined NotDelayed (%\n%
set "rtn=!rtn:^=^^!"%\n%
set "rtn=!rtn:"=""Q!^"%\n%
call set "rtn=%%^rtn:^!=""E^!%%" ! %\n%
set "rtn=!rtn:""E=^!"%\n%
set "rtn=!rtn:""Q="!^"%\n%
)%\n%
set ^"var=!var:^"=^^^"!^"%\n%
set "var=!var:&=^&!"%\n%
set "var=!var:|=^|!"%\n%
set "var=!var:<=^<!"%\n%
set "var=!var:>=^>!"%\n%
set "var=!var:(=^(!"%\n%
set "var=!var:)=^)!"%\n%
)%\n%
for /f "delims=" %%e in ("!errCmd!") do for /f %macro_ForEntireLine% %%v in ("!rtn!") do (%\n%
for /l %%n in (1,1,!macro.Rtn1.EndLocalCnt!) do endlocal%\n%
if "%%~d" neq "" set "%%~d=%%v" !%\n%
%%e 2^>nul%\n%
)%\n%
)
::macro.RtnN ErrLvl EndLocalCnt ValueVar1:RtnVar1[,ValueVar2:RtnVar2]...
::
:: Returns the contents of multiple variables across the ENDLOCAL border at
:: the end of a function or macro. The number of ENDLOCAL executions is
:: controlled by the EndLocalCnt which should match the number of times
:: SETLOCAL was used within the calling function/macro. The errorlevel is
:: set to ErrLvl.
::
:: A pair of variable names must be specified for each output value - the
:: name of the variable containing the value followed by a colon followed
:: by the name of the variable that is to receive the value. Multiple pairs
:: are delimited by commas. The list of outputs cannot contain any spaces,
:: and the variable names cannot contain asterisk (*), question mark (?),
:: colon (:) or comma (,).
::
:: Numeric values ErrLvl and EndLocalCnt may be passed using any expression
:: supported by SET /A.
::
:: This macro can return strings containing any combination of characters
:: supported by DOS, except for 0x0A <Line Feed> or 0x0C <Carriage Return>.
:: The macro works regardless whether the target return environment has
:: enabled or disabled delayed expansion.
::
:: In order for a function to use RtnN, the calling function must start with
:: %%macro_InitFcnRtn%% at the top.
::
:: In order for a macro to use RtnN, the calling macro must start with
:: %%macro_InitRtn%% at the top.
::
set macro.RtnN=do (%\n%
setlocal enableDelayedExpansion%\n%
set /a "macro.Rtn1.ErrLvl=(%%~a), macro.Rtn1.EndLocalCnt=(%%~b)+2"%\n%
set "cmd="%\n%
for /l %%n in (1,1,!macro.Rtn1.EndLocalCnt!) do set "cmd=!cmd!endlocal!lf!"%\n%
for %%c in (%%~c) do for /f "tokens=1,2 eol=: delims=:" %%c in ("%%c") do (%\n%
set "rtn=!%%~c!"%\n%
if defined rtn (%\n%
if not defined NotDelayed (%\n%
set "rtn=!rtn:^=^^!"%\n%
set "rtn=!rtn:"=""Q!^"%\n%
call set "rtn=%%^rtn:^!=""E^!%%" ! %\n%
set "rtn=!rtn:""E=^!"%\n%
set "rtn=!rtn:""Q="!^"%\n%
)%\n%
set ^"var=!var:^"=^^^"!^"%\n%
set "var=!var:&=^&!"%\n%
set "var=!var:|=^|!"%\n%
set "var=!var:<=^<!"%\n%
set "var=!var:>=^>!"%\n%
set "var=!var:(=^(!"%\n%
set "var=!var:)=^)!"%\n%
)%\n%
set "cmd=!cmd!set "%%~d=!rtn!"^!!lf!"%\n%
)%\n%
if defined macro_inFcn (%\n%
set "cmd=!cmd!exit /b !macro.Rtn1.ErrLvl!!lf!"%\n%
) else if "!macro.Rtn1.ErrLvl!" == "0" (%\n%
rem errorlevel set to 0 by default%\n%
) else (%\n%
set "cmd=!cmd!cmd /c exit !macro.Rtn1.ErrLvl!!lf!"%\n%
)%\n%
for /f "delims=" %%v in ("!cmd!") do %%v%\n%
)
set macro\load.%~n0=1
Macro_TimerLib.bat - documentation embedded within file.
Code: Select all
@echo off
:: This batch file will fail if called while delayed expansion is enabled.
::
:: This batch file defines macros that are usefull for performing timing
:: operations within a batch file
::
:: Typical Usage:
::
:: if not defined macro\load.macro_timer call macro_timer
:: ... <do whatever>...
:: %macro_call% ("t1") %macro.getTime%
:: ... <code to be timed goes here> ...
:: %macro_call% ("t2") %macro.getTime%
:: %macro_call% ("t1 t2 tm") %macro.diffTime%
:: echo It took %tm% seconds to execute
::define a Line Feed (newline) string (normally only used as !LF!)
set LF=^
::Above 2 blank lines are required - do not remove
::define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
:: A simple macro used to call macros with arguments
:: Usage:
::
:: %macro_call% ("arg1 arg2 arg3...") %macro.macroName%
::
set macro_Call=for /f "tokens=1-26" %%a in
::set macro.args.GetTime= [RtnVar]
::
:: Computes the current time of day measured as 1/100th seconds past midnight
::
:: Sets RtnVar = result
:: or displays result if RtnVar not specified ("""")
::
::
set macro.GetTime=do (%\n%
setlocal enableDelayedExpansion%\n%
set "t=0"%\n%
for /f "tokens=1-4 delims=:." %%A in ("!time: =0!") do set /a "t=(((1%%A*60)+1%%B)*60+1%%C)*100+1%%D-36610100"%\n%
for %%v in (!t!) do endlocal^&if "%%~a" neq "" (set "%%~a=%%v") else echo:%%v%\n%
)
::macro.args.DiffTime= StartTime StopTime [RtnVar]
::
:: Computes the elapsed time between StartTime and
:: StopTime and formats the result as HH:MM:SS.DD
::
:: StartTime and StopTime must be integral values
:: representing 1/100th seconds past midnight. These
:: values are typically gotten via calls to macro.GetTime
::
:: Sets RtnVar=result
:: or displays result if RtnVar not specified
::
:: DiffTime will properly handle elapsed times that span
:: midnight. However it cannot handle times that
:: reach 24 hours or more.
::
:: Note that StartTime and StopTime may be passed using
:: any numeric expression supported by SET /A%xLF%
::
set macro.DiffTime=do (%\n%
setlocal enableDelayedExpansion%\n%
set /a "DD=(%%~b)-(%%~a)"%\n%
if !DD! lss 0 set /a "DD+=24*60*60*100"%\n%
set /a "HH=DD/360000, DD-=HH*360000, MM=DD/6000, DD-=MM*6000, SS=DD/100, DD-=SS*100"%\n%
if "!HH:~1!"=="" set "HH=0!HH!"%\n%
if "!MM:~1!"=="" set "MM=0!MM!"%\n%
if "!SS:~1!"=="" set "SS=0!SS!"%\n%
if "!DD:~1!"=="" set "DD=0!DD!"%\n%
for %%v in (!HH!:!MM!:!SS!.!DD!) do endlocal^&if "%%~c" neq "" (set "%%~c=%%v") else echo:%%v%\n%
)
set macro\load.%~n0=1
WernerGg discovered that the :toLower, :toUpper and toCamelCase have a serious bug in that they may fail depending on the name of the string variable that is passed in. (see the toLower Name dependent? post). I based the :ucase functions on a modified version of his solution to the problem. The ucase function capabable of returning any string is significantly faster than the original bugged version of :toUpper!
ucase.bat: This demonstrates some of the potential problems returning strings, and shows how the macros solve the problems. Timings of the various functions are included at the end.
Code: Select all
@echo off
cls
if not defined macro\load.macro_RtnLib call macro_RtnLib
if not defined macro\load.macro_TimerLib call macro_TimerLib
setlocal disableDelayedExpansion
set "state=DISABLE"
:top
set simpleTest=a simple test
set aTestString=variable names should not matter
set theAmpersandTest="This & that " ^& the other thing
set caretTest=Square area=width^^2 ^^^^ ^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
set exclaimTest=Hello world! No answer? What a drag!
set excitedCaretTest=Square area=width^^2 ^^^^ ^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
set lineFeedTest=line1%xLF%line2
setlocal enableDelayedExpansion
set xxxxxx_carriageReturnTest=the!CR!answer
setlocal %state%DelayedExpansion
for %%a in (
simpleTest
aTestString
theAmpersandTest
caretTest
exclaimTest
excitedCaretTest
xxxxxx_carriageReturnTest
lineFeedTest
) do (
echo ----------------------------------
echo Delayed expansion is %state%D
echo:
set %%a
echo:
echo testing :ucase1
call :ucase1 %%a result
set result
echo:
echo testing :ucase2
call :ucase2 %%a result
set result
echo:
echo testing :ucase3
call :ucase3 %%a result
set result
echo:
echo testing :toUpper
call :toUpper %%a
set %%a
echo:
)
if "%state%"=="ENABLE" goto :continue
endlocal
endlocal
set "state=ENABLE"
goto :top
:continue
echo ============================================
set string=test
for %%c in (toUpper ucase1 ucase2 ucase3) do (
%macro_call% ("t1") %macro.getTime%
for /l %%n in (1,1,100) do call :%%c string result
%macro_call% ("t2") %macro.getTime%
%macro_call% ("t1 t2 result") %macro.diffTime%
echo %%c time x 100 = !result!
)
exit /b
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: FUNCTION DEFINITIONS
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:toUpper str -- converts lowercase character to uppercase
:: -- str [in,out] - valref of string variable to be converted
:$created 20060101 :$changed 20080219 :$categories StringManipulation
:$source http://www.dostips.com
if not defined %~1 EXIT /b
for %%a in ("a=A" "b=B" "c=C" "d=D" "e=E" "f=F" "g=G" "h=H" "i=I"
"j=J" "k=K" "l=L" "m=M" "n=N" "o=O" "p=P" "q=Q" "r=R"
"s=S" "t=T" "u=U" "v=V" "w=W" "x=X" "y=Y" "z=Z" "ä=Ä"
"ö=Ö" "ü=Ü") do (
call set %~1=%%%~1:%%~a%%
)
EXIT /b
:ucase1 strVar [rtnVar]
::
:: Fixes variable name beginning with lower case "a" bug
:: Echoes result if rtnVar not specified
::
setlocal enableDelayedExpansion
set "str=!%~1!"
if defined str for %%a in (
"a=A" "b=B" "c=C" "d=D" "e=E" "f=F" "g=G" "h=H" "i=I"
"j=J" "k=K" "l=L" "m=M" "n=N" "o=O" "p=P" "q=Q" "r=R"
"s=S" "t=T" "u=U" "v=V" "w=W" "x=X" "y=Y" "z=Z" "ä=Ä"
"ö=Ö" "ü=Ü"
) do set "str=!str:%%~a!"
( endlocal
if "%~2" neq "" (set %~2=%str%) else echo %str%
)
exit /b
:ucase2 strVar [rtnVar]
::
:: Fixes variable name beginning with lower case "a" bug
:: Echoes result if rtnVar not specified
::
:: Output can now contain any combination of characters except
:: <carriage return> or <line feed>
::
:: Works the same if called while delayed expansion is enabled or disabled
::
%macro_InitFcnRtn%
setlocal enableDelayedExpansion
set "str=!%~1!"
if defined str for %%a in (
"a=A" "b=B" "c=C" "d=D" "e=E" "f=F" "g=G" "h=H" "i=I"
"j=J" "k=K" "l=L" "m=M" "n=N" "o=O" "p=P" "q=Q" "r=R"
"s=S" "t=T" "u=U" "v=V" "w=W" "x=X" "y=Y" "z=Z" "ä=Ä"
"ö=Ö" "ü=Ü"
) do set "str=!str:%%~a!"
%macro_Call% ("!errorlevel! 1 str %~2") %macro.Rtn1%
exit /b
:ucase3 strVar [rtnVar]
:
: Fixes variable name beginning with lower case "a" bug
: Echoes result if rtnVar not specified
:
: Output can now contain absolutely any combination of characters
: including <carriage return> and <line feed>
:
: Works the same if called while delayed expansion is enabled or disabled
:
%macro_InitFcnRtn%
setlocal enableDelayedExpansion
set "str=!%~1!"
if defined str for %%a in (
"a=A" "b=B" "c=C" "d=D" "e=E" "f=F" "g=G" "h=H" "i=I"
"j=J" "k=K" "l=L" "m=M" "n=N" "o=O" "p=P" "q=Q" "r=R"
"s=S" "t=T" "u=U" "v=V" "w=W" "x=X" "y=Y" "z=Z" "ä=Ä"
"ö=Ö" "ü=Ü"
) do set "str=!str:%%~a!"
%macro_Call% ("!errorlevel! 1 str %~2") %macro.AnyRtn1%
%macro_RtnAny%
exit /b
Results of ucase.bat: The timing results at the end demonstrate that there
is very little overhead in calling the safe return macros.
Code: Select all
----------------------------------
Delayed expansion is DISABLED
simpleTest=a simple test
testing :ucase1
result=A SIMPLE TEST
testing :ucase2
result=A SIMPLE TEST
testing :ucase3
result=A SIMPLE TEST
testing :toUpper
simpleTest=A SIMPLE TEST
----------------------------------
Delayed expansion is DISABLED
aTestString=variable names should not matter
testing :ucase1
result=VARIABLE NAMES SHOULD NOT MATTER
testing :ucase2
result=VARIABLE NAMES SHOULD NOT MATTER
testing :ucase3
result=VARIABLE NAMES SHOULD NOT MATTER
testing :toUpper
aTestString="ü=Ü"TestString:ü=Ü
----------------------------------
Delayed expansion is DISABLED
theAmpersandTest="This & that " & the other thing
testing :ucase1
'THE' is not recognized as an internal or external command,
operable program or batch file.
result="THIS & THAT "
testing :ucase2
result="THIS & THAT " & THE OTHER THING
testing :ucase3
result="THIS & THAT " & THE OTHER THING
testing :toUpper
theAmpersandTest="This & that " & the other thing
----------------------------------
Delayed expansion is DISABLED
caretTest=Square area=width^2 ^^ ^^^ ^^^^ ^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
testing :ucase1
result=SQUARE AREA=WIDTH2 ^ ^ ^^ ^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :ucase2
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :ucase3
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :toUpper
caretTest=SQUARE AREA=WIDTH2 "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
----------------------------------
Delayed expansion is DISABLED
exclaimTest=Hello world! No answer? What a drag!
testing :ucase1
result=HELLO WORLD! NO ANSWER? WHAT A DRAG!
testing :ucase2
result=HELLO WORLD! NO ANSWER? WHAT A DRAG!
testing :ucase3
result=HELLO WORLD! NO ANSWER? WHAT A DRAG!
testing :toUpper
exclaimTest=HELLO WORLD! NO ANSWER? WHAT A DRAG!
----------------------------------
Delayed expansion is DISABLED
excitedCaretTest=Square area=width^2 ^^ ^^^ ^^^^ ^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
testing :ucase1
result=SQUARE AREA=WIDTH2 ^ ^ ^^ ^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :ucase2
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :ucase3
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :toUpper
excitedCaretTest=SQUARE AREA=WIDTH2 "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
----------------------------------
Delayed expansion is DISABLED
xxxxxx_carriageReturnTest=the
answer
testing :ucase1
result=THEANSWER
testing :ucase2
result=THE
ANSWER
testing :ucase3
result=THE
ANSWER
testing :toUpper
xxxxxx_carriageReturnTest=THEANSWER
----------------------------------
Delayed expansion is DISABLED
lineFeedTest=line1
line2
testing :ucase1
'LINE2' is not recognized as an internal or external command,
operable program or batch file.
'LINE2' is not recognized as an internal or external command,
operable program or batch file.
result=LINE1
testing :ucase2
result=LINE1
testing :ucase3
result=LINE1
LINE2
testing :toUpper
lineFeedTest=LINE1
----------------------------------
Delayed expansion is ENABLED
simpleTest=a simple test
testing :ucase1
result=A SIMPLE TEST
testing :ucase2
result=A SIMPLE TEST
testing :ucase3
result=A SIMPLE TEST
testing :toUpper
simpleTest=A SIMPLE TEST
----------------------------------
Delayed expansion is ENABLED
aTestString=variable names should not matter
testing :ucase1
result=VARIABLE NAMES SHOULD NOT MATTER
testing :ucase2
result=VARIABLE NAMES SHOULD NOT MATTER
testing :ucase3
result=VARIABLE NAMES SHOULD NOT MATTER
testing :toUpper
aTestString="ü=Ü"TestString:ü=Ü
----------------------------------
Delayed expansion is ENABLED
theAmpersandTest="This & that " & the other thing
testing :ucase1
'THE' is not recognized as an internal or external command,
operable program or batch file.
result="THIS & THAT "
testing :ucase2
result="THIS & THAT " & THE OTHER THING
testing :ucase3
result="THIS & THAT " & THE OTHER THING
testing :toUpper
theAmpersandTest="This & that " & the other thing
----------------------------------
Delayed expansion is ENABLED
caretTest=Square area=width^2 ^^ ^^^ ^^^^ ^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
testing :ucase1
result=SQUARE AREA=WIDTH2 ^ ^^ "CUBE VOLUME=WIDTH3 ^ ^ ^^"
testing :ucase2
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :ucase3
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :toUpper
caretTest=SQUARE AREA=WIDTH2 "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
----------------------------------
Delayed expansion is ENABLED
exclaimTest=Hello world! No answer? What a drag!
testing :ucase1
result=HELLO WORLD
testing :ucase2
result=HELLO WORLD! NO ANSWER? WHAT A DRAG!
testing :ucase3
result=HELLO WORLD! NO ANSWER? WHAT A DRAG!
testing :toUpper
exclaimTest=HELLO WORLD! NO ANSWER? WHAT A DRAG!
----------------------------------
Delayed expansion is ENABLED
excitedCaretTest=Square area=width^2 ^^ ^^^ ^^^^ ^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
testing :ucase1
result=SQUARE AREA=WIDTH2 ^ ^^ "CUBE VOLUME=WIDTH3 ^ ^ ^^"
testing :ucase2
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :ucase3
result=SQUARE AREA=WIDTH^2 ^^ ^^^ ^^^^ ^^^^^^^^ "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
testing :toUpper
excitedCaretTest=SQUARE AREA=WIDTH2 "CUBE VOLUME=WIDTH^3 ^^ ^^^ ^^^^!"
----------------------------------
Delayed expansion is ENABLED
xxxxxx_carriageReturnTest=the
answer
testing :ucase1
result=THEANSWER
testing :ucase2
result=THEANSWER
testing :ucase3
result=THE
ANSWER
testing :toUpper
xxxxxx_carriageReturnTest=THEANSWER
----------------------------------
Delayed expansion is ENABLED
lineFeedTest=line1
line2
testing :ucase1
'LINE2' is not recognized as an internal or external command,
operable program or batch file.
'LINE2' is not recognized as an internal or external command,
operable program or batch file.
result=LINE1
testing :ucase2
result=LINE1
testing :ucase3
result=LINE1
LINE2
testing :toUpper
lineFeedTest=LINE1
============================================
toUpper time x 100 = 00:00:07.30
ucase1 time x 100 = 00:00:00.90
ucase2 time x 100 = 00:00:01.58
ucase3 time x 100 = 00:00:01.65
rtnTwoStrings.bat - Uses a silly function to demonstrate how to return multiple values across the ENDLOCAL border.
Code: Select all
@echo off
cls
if not defined macro\load.macro_RtnLib call macro_RtnLib
if not defined macro\load.macro_TimerLib call macro_TimerLib
setlocal disableDelayedExpansion
set "state=DISABLE"
:top
set simpleTest=a simple test
set theAmpersandTest="This & that " ^& the other thing
set caretTest=Square area=width^^2 ^^^^ ^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
set exclaimTest=Hello world! No answer? What a drag!
set excitedCaretTest=Square area=width^^2 ^^^^ ^^^^^^ ^^^^^^^^ ^^^^^^^^^^^^^^^^ "cube volume=width^3 ^^ ^^^ ^^^^!"
set lineFeedTest=line1%xLF%line2
setlocal enableDelayedExpansion
set xxxxxx_carriageReturnTest=the!CR!answer
setlocal %state%DelayedExpansion
for %%a in (
simpleTest
aTestString
theAmpersandTest
caretTest
exclaimTest
excitedCaretTest
xxxxxx_carriageReturnTest
lineFeedTest
) do (
echo ----------------------------------
echo Delayed expansion is %state%D
echo:
set %%a
echo:
echo testing :RtnTwoStrings1
call :RtnTwoStrings1 %%a rtn1 rtn2
set rtn
echo:
echo testing :RtnTwoStrings2
call :RtnTwoStrings2 %%a rtn1 rtn2
set rtn
echo:
echo testing :RtnTwoStrings3
call :RtnTwoStrings3 %%a rtn1 rtn2
set rtn
echo:
)
if "%state%"=="ENABLE" goto :continue
endlocal
endlocal
set "state=ENABLE"
goto :top
:continue
echo ============================================
set string=test
for %%c in (RtnTwoStrings1 RtnTwoStrings2 RtnTwoStrings3) do (
%macro_call% ("t1") %macro.getTime%
for /l %%n in (1,1,100) do call :%%c string rtn1 rtn2
%macro_call% ("t2") %macro.getTime%
%macro_call% ("t1 t2 result") %macro.diffTime%
echo %%c time x 100 = !result!
)
exit /b
:RtnTwoStrings1 StrVar Rtn1Var Rtn2Var
setlocal enableDelayedExpansion
set "str=!%~1!"
set "rtn1="Part1:!str!"%
set "rtn2="!str!:Part2"
set err=3
(endlocal
set "%~2=%rtn1%
set "%~3=%rtn2%
exit /b %err%
)
exit /b
:RtnTwoStrings2 StrVar Rtn1Var Rtn2Var
%macro_InitFcnRtn%
setlocal enableDelayedExpansion
set "str=!%~1!"
set "rtn1="Part1:!str!"%
set "rtn2="!str!:Part2"
%macro_call% ("err 1 rtn1:%~2,rtn2:%~3") %macro.RtnN%
exit /b
:RtnTwoStrings3 StrVar Rtn1Var Rtn2Var
%macro_InitFcnRtn%
setlocal enableDelayedExpansion
set "str=!%~1!"
set "rtn1="Part1:!str!"%
set "rtn2="!str!:Part2"
%macro_call% ("err 1 rtn1:%~2,rtn2:%~3") %macro.AnyRtnN%
%macro_RtnAny%
exit /b
Edits:
18-June-2011 - Fixed bug in macro_Rtn1 in file Macro_RtnLib.bat: Macro was generating a syntax error when called without optional RtnVar.
28-Sep-2011 - See note at top of this thread
Dave Benham