jeb: First of all, I want to offer you a
VERY BIG CONGRATULATION for all of your ideas, specially this one.
That said, this is a small observation:
WHY YOU DIDN'T EXPLAIN YOUR METHODS? I worked for over 25 years explaining computer themes to other people. I used to write larger and clearer explanations as the themes becomes harder. I hate the
"use this as it is, but don't try to understand it" approach in new methods, because it prevents the new method be understood by other people and hence be modified and used for other purposes.
Below is my own explanation on this topic and a small additional contribution.
Suppose you have a macro-function that is usually executed this way:
Code: Select all
for /F "tokens=1-26" %%a in ("param1 param2 resultvar") do %macroFunc%
How we may execute the macro in the following way?
Code: Select all
%macroFuncWithParams% param1 param2 resultvar
Parameters placed after the macro needs to be taken and moved inside the FOR parentheses; this may be achieved by a two-step process:
1- Get the parameters
2- Execute the macro with them
That is, the macro must be executed two times: in the first step the parameters will be stored in an (argv) variable, and in the second step the macro will be really executed:
Code: Select all
for %%s in (get_params exec_macro) do (
if %%s == get_params (
set argv=<rest of the line placed after the macro>
) else ( rem %%s == exec_macro
for /F "tokens=1-26" %%a in (!argv!) do %macroFunc%
)
)
However, the <rest of the line> is
physically placed after the macro. This means that
set argv= command must be the
last part of the macro definition:
Code: Select all
for %%s in (1 2) do (
if %%s == 2 (
for /f "tokens=1-26" %%a in (!argv!) do %macroFunc%
) else (
set argv=<rest of the line...>
)
)
There are still two parentheses after
set argv=, but we can simply eliminate they because the FOR and ELSE part parentheses are placed here just for legibility:
Code: Select all
for %%n in (1 2) do if %%n == 2 (
for /f "tokens=1-26" %%a in (!argv!) do %macroFunc%
) else set argv=
This way, although the parts seems to be upside down, they execute in the right order. Note that we could place another command after ELSE separated by & and it also would belong to ELSE part, although there are not parentheses here. This point makes possible to include a SETLOCAL before the
set argv= to avoid undesired modification of a variable with same name in the caller program, and to assure that delayed expansion is enabled when !argv! is processed:
Code: Select all
for %%n in (1 2) do if %%n == 2 (
for /f "tokens=1-26" %%a in (!argv!) do %macroFunc%
endlocal
) else setlocal EnableDelayedExpansion & set argv=
However, because this method use textual substitution of whichever text placed after the macro, if another & COMMAND is placed after macro params, then that command also belongs to ELSE part and will also be executed
before the macro, that is an unexpected behavior. The only way to avoid this effect is isolating the macro and parameters from the rest of the line by enclosing they in parentheses.
Second part: An additional contribution.
When we define a macroFuncWithParams variable with this format, we may duplicate the original macroFunc code in the macro execution part, taking the parameters from argv variable (as in jeb example above); or we may use the original macroFunc code over argv variable in a nested macro invocation:
Code: Select all
for /F "tokens=1-26" %%a in ("!argv!") do %nestedMacroFunc%
This last case would be useful if we want to define a
generic macro that could call any other macro-function in a standard way. For example, suppose now that we want to execute our original macro this way:
Code: Select all
%let% resultVar=macroFunc(param1,param2)
How we can do that? Via a slightly different rearrangment of the macro parameters:
Code: Select all
for %%n in (1 2) do if %%n == 2 (
for /F "tokens=1-4 delims==(,) " %%1 in ("!argv!") do (
set macroFunc=%%1
set param1=%%2
set param2=%%3
set resultVar=%%4
for /F "tokens=1-26" %%a in ("!param1! !param2! resultValue") do %%macroFunc%%
for %%v in (!resultValue!) do endlocal & set "!resultVar!=%%v"
) else setlocal EnableDelayedExpansion & set argv=
We have, however, a big problem at this point.
There is no way to directly execute a nested macro inside another one. We may left this problem apart for a while and test now that the method above is correct by executing a subroutine instead of a macro:
Code: Select all
@echo off
cls
setlocal DisableDelayedExpansion
set LF=^
::Above 2 blank lines are required - do not remove
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
set strLen=(%\n%
setlocal enableDelayedExpansion%\n%
set "str=A!%%~b!"%\n%
set "len=0"%\n%
for /l %%A in (12,-1,0) do (%\n%
set /a "len|=1<<%%A"%\n%
for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"%\n%
)%\n%
for %%v in (!len!) do endlocal^&if "%%~a" neq "" (set "%%~a=%%v") else echo %%v%\n%
)
set let=for %%n in (1 2) do if %%n==2 (%\n%
for /F "tokens=1-4 delims==(,) " %%1 in ("!argv!") do (%\n%
echo -------------------------- %\n%
set macroFunc=%%1%\n%
echo macroFunc=!macroFunc! %\n%
set param1=%%2%\n%
echo param1=!param1! %\n%
REM set param2=%%3%\n%
REM echo param2=!param2! %\n%
set resultVar=%%3%\n%
echo resultVar=!resultvar! %\n%
call !macroFunc! !param1! !param2! resultValue%\n%
REM for /F "tokens=1-26" %%a in ("!param1! !param2! resultValue") do (%\n%
REM nested call %%%%macroFunc%%%% %\n%
REM )%\n%
for %%v in (!resultValue!) do endlocal^&set "%%1=%%v"%\n%
echo -------------------------- %\n%
)%\n%
) else setlocal enableDelayedExpansion ^& set argv=
set "testString=this has a length of 23"
echo Test string is ("%testString%")
echo/
echo Original (FOR /F ...) strLen macro invocation:
for /F "tokens=1-26" %%a in ("testString forResult") do %strLen%
echo Original FOR macro invocation result is %forResult%
echo/
echo Original "CALL :strLen testString subResult" subroutine invocation:
call :strLen testString subResult=
echo Original CALL subroutine invocation result is %subResult%
echo/
echo %%Let%% letResult=:strLen(testString) invocation:
%let% letResult=:strLen(testString)
echo LET letResult=strLen(testString) result is %letResult%
goto :EOF
:strLen string resultVar=
setlocal enableDelayedExpansion
set "str=0!%~1!"
set "len=0"
for /l %%A in (12,-1,0) do (
set /a "len|=1<<%%A"
for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"
)
for %%v in (!len!) do endlocal&if "%~2" neq "" (set "%~2=%%v") else echo %%v
exit /B
The result:
Code: Select all
Test string is ("this has a length of 23")
Original (FOR /F ...) strLen macro invocation:
Original FOR macro invocation result is 23
Original "CALL :strLen testString subResult" subroutine invocation:
Original CALL subroutine invocation result is 23
%Let% letResult=:strLen(testString) invocation:
--------------------------
macroFunc=:strLen
param1=testString
resultVar=letResult
--------------------------
LET letResult=strLen(testString) result is 23
In previous example we achieved to get a macro that allows to execute any function in a very standard way at expense of a somewhat slower execution because the additional CALL. But we will not stop at this point, right? So we try to solve now the nested macro problem. The only way to execute a nested macro is by embeding it inside the caller one so when the original macro is expanded for execution, it already contains the code of the nested one. This may be achieved by splitting the original macro in two parts: a Head, from the beginning up to the invocation of the nested macro, and a Tail, from the return of the nested invocation up to the end. This way, we can assemble the final macro by joining the Head, the nested macro, and the Tail this way:
Code: Select all
set completeLet=!letHead!!macroFunc!!letTail!
After that, the execution of both the LET macro and the nested macro-function is immediate:
EDIT: Continue at this matter.However, there is no way to include/embed a macro into another one
at execution time, so previous method imply to create a different macro for each combination of LET plus the particular macro-function, that is not the desired solution. This method may still be used to execute any subroutine-function in a clearer way and there is another instance where this solution may be useful: to get the value of an array element when the subscript vary inside a FOR loop (and it is not the FOR variable). This task may be achieved via a macro called ASET (array set) with this format:
%aset% element=array[subscript]Code: Select all
set aset=for %%n in (1 2) do if %%n==2 (%\n%
for /F "tokens=1-3 delims==[] " %%1 in ("!argv!") do (%\n%
endlocal^&set "%%1=!%%2[%%3]!"%\n%
)%\n%
) else setlocal enableDelayedExpansion^&set argv=
For example, in
this SO question I posted this program segment:
Code: Select all
set i=0
set j=1
for %%e in (%line%) do (
set /A i+=1
for %%j in (!j!) do set target=!target[%%j]!
if !i! == !target! (
for %%i in (!i!) do set heading=!heading[%%i]!
echo !heading!%%~e
set /A j+=1
)
)
Using ASET macro, the segment may be rewritten in this clearer way:
Code: Select all
set i=0
set j=1
for %%e in (%line%) do (
set /A i+=1
%aset% target=target[!j!]
if !i! == !target! (
%aset% heading=heading[!i!]
echo !heading!%%~e
set /A j+=1
)
)
Antonio
PS - I modified previous description to include a missing detail that jeb noted in a posterior post.
Note:
for %%n in (1 2) execute faster than
for /L %%n in (1,1,2).
Note: In my opinion, there must be a standard in macro names that allows to know if a macro is used in the conventional
for /F ... way instead of with parameters (to avoid confusions).
Note: I strongly encourage all of you, people, to adopt the standard of using %%1 %%2...%%9 to take macro parameters in macro definitions (and FOR /F "tokens=1-9 delims=,;= " %%1 invocation) instead of using %%a %%b...%%z (and FOR /F "tokens=1-26" %%a invocation) for several practical reasons, some of which are explained in my second post at
this topic.