Page 1 of 2

Macros with parameters appended

Posted: 21 Nov 2011 05:35
by jeb
Hi all, (especially dbenham and tooComplex/Ed) :)

sometimes someone asks for a better solution for macros with parameters.

Currently the standard technic (from Ed) is to invoke a batch macro with parameters seems to be

Code: Select all

%_forU% ( 'params' ) %@macro%

So you need a construct with two expansions, and the parameters are in the middle.

I would prefere something like (even I didn't use macros at all)

Code: Select all

%myMacro% param1,param2
OR
%myMacro% (param1,param2)

How to solve this might not be obviously, but it can be done. :wink:

And the result isn't very complex, it's only a bit slower as it uses one extra FOR/L-Loop.
The main idea is this:

Code: Select all

FOR/L %%n in (1,1,2) DO (
  if %%n==2 ( // Now we can access the parameters
     // get the parameters
     // run the macro
  )
) & set parameters=


And here is a working sample with strlen
Calling it via
%$strlen% resultVar,stringVar

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 argv=Empty
set $strLen=for /L %%n in (1 1 2) do ( %\n%
   if %%n==2 (%\n%
       setlocal enableDelayedExpansion%\n%
      for /F "tokens=1,2 delims=, " %%1 in ("!argv!") do (%\n%
         echo par1=%%1 %\n%
         echo par2=%%2 %\n%
         set "str=A!%%~2!"%\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 "%%~b" neq "" (set "%%~1=%%v") else echo %%v%\n%
      ) %\n%
   ) %\n%
) ^& set argv=,

set "testString=this has a length of 23"
%$strlen% result,testString
echo Length of testString ("%testString%") is %result%


jeb

Re: Macros with parameters appended

Posted: 21 Nov 2011 07:19
by dbenham
:shock: Awesome and Revolutionary :!:

Two minor inconveniences:

1) The parameter value of argV persists after the macro completes. I don't think there is anything we can do about that.

2) Any command added on the same line after the macro call will be executed twice

This code with your strlen macro

Code: Select all

set "testString=this has a length of 23"
%$strlen% result,testString & echo Hello World %%n
echo Length of testString ("%testString%") is %result%
set argv

produces:

Code: Select all

Hello world 1
par1=!argv!
par2=
Hello world 2
Length of testString ("this has a length of 23") is
argv=, result,testString

Great work jeb :!:
I think I will be adopting this macro style.


Dave Benham

Re: Macros with parameters appended

Posted: 21 Nov 2011 07:42
by Ed Dyreen
'
My god, this may prove to work pretty well actually ( you definately made dave happy ).

:x
How am I ever going to get my library stable if things keep changing at this rate ? impressive !
You know jeb, you are a freaking clever guy, luckily for clever people, there is always someone more clever than you.

And while you're at it, how about:

Code: Select all

set /a $var=%StrLen% ( "I wish this worked" ) + %random%
Now that would be really impressive. :lol:

I have a feeling this topic is going to grow...

Re: Macros with parameters appended

Posted: 21 Nov 2011 08:13
by jeb
dbenham wrote: :shock: Awesome and Revolutionary :!:

Thanks :D


dbenham wrote:Two minor inconveniences:

1) The parameter value of argV persists after the macro completes. I don't think there is anything we can do about that.

2) Any command added on the same line after the macro call will be executed twice


The solution for these two points is obviously :wink:

- Use setlocal direct before set argv
- Do the set argv and the rest only if %%n==1

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 argv=original
set $strLen=for /L %%n in (1 1 2) do if %%n==2 (%\n%
      for /F "tokens=1,2 delims=, " %%1 in ("!argv!") do (%\n%
         echo par1=%%1 %\n%
         echo par2=%%2 %\n%
         set "str=A!%%~2!"%\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 "%%~b" neq "" (set "%%~1=%%v") else echo %%v%\n%
      ) %\n%
) ELSE setlocal enableDelayedExpansion ^& set argv=,

set "testString=this has a length of 23"
%$strlen% result,testString & echo hello
set argv


Output
hello
par1=result
par2=testString
Length of testString ("this has a length of 23") is 23
argv=original


jeb

Re: Macros with parameters appended

Posted: 21 Nov 2011 09:09
by dbenham
jeb wrote:The solution for these two points is obviously :wink:

- Use setlocal direct before set argv
- Do the set argv and the rest only if %%n==1

Ding Ding Ding Ding Ding Ding Ding
WoooHooooo - Jackpot :!:

I am definitely adopting jeb's new macro style now :D

I will probably define a few helper macros:

macro_begin to encapsulate the 1st two lines

macro_end to define the final ELSE construct under normal circumstances

macro_endSafe to define the final ELSE construct with some additional stuff to prepare for the safe return technique

Thanks again jeb

Addendum - I just realized the solution is not quite perfect :|

Any command appended to the end of the line actually executes before the macro. This might throw off some people . . .
. . . but the technique is still awesome :D

Dave Benham

Re: Macros with parameters appended

Posted: 21 Nov 2011 09:31
by Ed Dyreen
'
dbenham wrote:Any command appended to the end of the line actually executes before the macro. This might throw off some people . . .
but Dave, doesn't this solves that :?

Code: Select all

set $strLen=for /L %%n in (1 1 2) do if %%n==2 (%\n%
      endlocal ^&cmd /c exit 0 %\n%
) ELSE setlocal enableDelayedExpansion ^& set argv=,

Code: Select all

( %$strlen% result,testString ) &&echo.succes ||echo.failed
Ok guys, I'm in jeb rocks 8)

Re: Macros with parameters appended

Posted: 21 Nov 2011 10:01
by dbenham
Ed Dyreen wrote:
dbenham wrote:Any command appended to the end of the line actually executes before the macro. This might throw off some people . . .
but Dave, doesn't this solves that :?

Code: Select all

set $strLen=for /L %%n in (1 1 2) do if %%n==2 (%\n%
      endlocal ^&cmd /c exit 0 %\n%
) ELSE setlocal enableDelayedExpansion ^& set argv=,

I don't understand your logic, or where you are going with this.

Please provide a complete but small working example that demonstrates that when executing

Code: Select all

%strlen% "my string" & echo hello
ECHO HELLO executes after the strlen is computed. I don't see how this can be done.

Dave Benham

Re: Macros with parameters appended

Posted: 21 Nov 2011 10:10
by Ed Dyreen
'

Code: Select all

@echo off

set $strLen=for /L %%n in (1 1 2) do if %%n==2 ( echo.instrlen ^!argv^! ^&endlocal ^&cmd /c exit 0 ) ELSE setlocal enableDelayedExpansion ^& set argv=,

( %$strlen% result,testString ) &&echo.succes ||echo.failed

pause
exit

Code: Select all

instrlen , result,testString
succes
Druk op een toets om door te gaan. . .
You'll have to enclose in brackets :|

Re: Macros with parameters appended

Posted: 21 Nov 2011 10:23
by dbenham
Of course - I see how it works now. Thanks.

But some one could still do it without brackets and be confused with the results. It's not a big deal, just something to look out for.

Dave Benham

Re: Macros with parameters appended

Posted: 21 Nov 2011 16:03
by aGerman
That's amazing!
I don't understand the behaviour that ^& set argv=, is able to catch the argument but, however, it works just fine.
That's indeed a trigger to commence working with macros. Thanks to each of you three.

Regards
aGerman (still impressed ...)

Re: Macros with parameters appended

Posted: 21 Nov 2011 16:25
by dbenham
aGerman wrote:I don't understand the behaviour that ^& set argv=, is able to catch the argument

At first I was confused as well. But it is really quite simple, once you see it. It helps to see everything on one line:

Code: Select all

set macro=for /l %%n in (1 1 2) do if %%n==2 (MACRO BUSINESS) else setlocal enableDelayedExpansion ^& set argv=,

%macro% arg1 arg2

:: the call expands to
:: for /l %%n in (1 1 2) do if %%n==2 (MACRO BUSINESS) else setlocal enableDelayedExpansion & set argv=, arg1 arg2

Really simple, yet clever :D


Dave Benham

Re: Macros with parameters appended

Posted: 21 Nov 2011 16:37
by aGerman
Now it dawns me :idea:
Thanks Dave :D

Regards
aGerman

Re: Macros with parameters appended

Posted: 12 Dec 2011 05:19
by Aacini
jeb: First of all, I want to offer you a VERY BIG CONGRATULATION :!: for all of your ideas, specially this one. :D :P

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:

Code: Select all

%completeLet%


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.

Re: Macros with parameters appended

Posted: 12 Dec 2011 06:26
by jeb
Welcome Aacini to the expert discussions :)

Aacini wrote:jeb: First of all, I want to offer you a VERY BIG CONGRATULATION :!: for all of your ideas, specially this one. :D :P
Thanks :D :)

Aacini wrote:WHY YOU DIDN'T EXPLAIN YOUR METHODS?
They are obvious and self-explanatory :!:

Ok, perhaps not complete obvious :wink: , but I wrote this especially for Dave and Ed, as only expert should/could build these types of macros.

Aacini wrote: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.
I love good questions, and thanks for your explanation, so I don't have to do it.

Aacini wrote:Because this trick use textual substitution of whichever text placed after the macro, if another & COMMAND is placed after macro params, that command will also be carried into the for body and hence also executed 2 times:
[...]
The only way to avoid this effect is isolating the macro and parameters from the rest of the line by enclosing they in parentheses, as Ed indicated.
Did I said that I love to disprove such declarations :?: 8)

I simply forgot to post the solution for this

Code: Select all

set $SomeMacro=for /L %%n in (1 1 2) do if %%n==2 (%\n%
      rem *** MACRO Content
      for /f "delims=" %%r in ("!result!") do endlocal^& set "%%~3=%%~r" %\n%
   ) %\n%
) ELSE setlocal enableDelayedExpansion ^& echo ProofOfconcept is showed only once %%n ^& set argv=,

In this case it is really obvious, isn't it?

jeb

Re: Macros with parameters appended

Posted: 12 Dec 2011 07:37
by Ed Dyreen
'
Very obvious as usual :) , I see , :) .

Code: Select all

@echo off

set $lf=^


::

set ^"\n=^^^%$lf%%$lf%^%$lf%%$lf%^^"

set $SomeMacro=for /L %%n in (1 1 2) do if %%n==2 (%\n%
      set argv%\n%
      for /f "delims=" %%r in ("!argv!") do endlocal^& echo.set "%%~3=%%~r" %\n%
   ) %\n%
) ELSE setlocal enableDelayedExpansion ^& echo ProofOfconcept is showed only once %%n ^& set argv=,


%$SomeMacro% yesyes

pause
exit

Code: Select all

Omgevingsvariabele argv is niet gedefinieerd
set "%~3=!argv!"
Druk op een toets om door te gaan. . .