Page 1 of 1

How batch macros work

Posted: 12 Jun 2011 20:00
by Ed Dyreen

It is currently 05 Sep 2011 08:26

The project was moved to new location :arrow:
http://www.mediafire.com/myfiles.php#3p17oxw36b33s,1

This page is outdated !

Creating a macro is suprisingly easy, a macro is just another name for a variable with some special characteristics.

We simply create a variable by giving it a value:

Code: Select all

set "$variable=value"

the important thing are the quotes, as this won't work:

Code: Select all

set variable=value&value

it is interpreted as:

Code: Select all

set variable=value
value

not recognized as a valid command.

Then there are escapes ^, as the name suggests they are used to escape charcters:

Code: Select all

set variable=value^&echo.value

it is interpreted as:

Code: Select all

set variable=value &echo.value

a valid command.
Wait a minute ! We created our first macro :)

With the escape character ^ we can do line continuations:

Code: Select all

set var=^
hello

it is interpreted as:

Code: Select all

set var=hello

It's called a macro because unlike a variable that has just some text in it, the text is a valid command, for as far as dos is concerned.

It will just expand a variable into memory when it's used:

Code: Select all

set "@\necho=echo. &echo."

it is interpreted as:

Code: Select all

echo.
echo.

Let's direct expand it:

Code: Select all

%@\necho% Hello There

it is interpreted as:

Code: Select all

echo.
echo. Hello There

we define a linefeed variable like this, we must first define it in order to use it:

Code: Select all

set \LF=^


:: Two empty lines are neccessary

But we need to recreate the linefeed at least once to get a newline:

Code: Select all

set ^"\n=^%\LF%%\LF%"

Now we can use it :

Code: Select all

echo.hello!\n!There

Code: Select all

echo.hello
echo.There

It is the same as & in a way, because we can get the same results with it.

Code: Select all

"set variable=value &echo.value"
"set variable=value !\n!echo.value"

these commands do exactly the same. they set variable to value & echo value.

this character is the delayed expansion character !, you could also use %, the direct expansion character.
but they both invoke their own set of limitations & possibilities.

I normally default to setlocal EnableDelayedExpansion, this enables me to evaluate inside for:

Code: Select all

for %%! in ( something else ) set "var=%%!" &echo. !var!

it is interpreted as:

Code: Select all

set "var=something" &echo.something
set "var=else" &echo.else

We can simply use a macro within a macro:

Code: Select all

set "@test=set /a $testcount += 1 &%@\necho% test : '^^^!$testcount^^^!' &%@Pause%"

But to use the newline in a macro, we have to recreate it another time:

Code: Select all

set ^"\n2=^^^%\LF%%\LF%^%\LF%%\LF%^^^"

Our macro needs input if we want to do something usefull with it. We simply use for:

Code: Select all

for /l %%! in

And while were at it, why not create a macro out of this :

Code: Select all

set "@forC=for /l %%^^^! in"

It works :D

Code: Select all

set /a Available_errorlevels = 99
::
echo.   @Available_errorlevels = '%Available_errorlevels%'
::
%@forC% (

   2, 1, %Available_errorlevels%

) do  for %%! in (

   "$%%~!.error"

) do echo.set ^"%%~!=$%%~!.error unset ^^^!"
::
set "Available_errorlevels="

Let's define something usefull now:

Code: Select all

set ^"@GetRandom=do ( %\n2%
          call set /a $StdOut = %%Random%% %%%% ^^^^^^^( $Maximum - $Minimum + 1 ^^^^^^^) + $Minimum %\n2%
)"

Now we just expand if needed:

Code: Select all

set "@forA=for /f "usebackq tokens=1-26 delims=¦" %%a in"

%@forA% ( '"$StoreVAR"¦"$Mininmum"¦"$Maximum"' ) %@GetRandom%

The sky is the limit, or is it your imagination :geek:

Code: Select all

    ::--------------------------------------------------------------------------------------------------------------------------
    %@Pre% @GetRandom
    ::
    :: %@forA% ( '"$Store"¦"0"¦"1000"' ) %@GetRandom%
    ::
    ::(
       set "$Defines=@GetRandom"
       ::
       set ^"$Usage.%$Defines%=^
    %   % Usage : %%@forA%% !\n!^
    %   %  ^( '"$StoreVAR"¦"$Mininmum"¦"$Maximum"' ^) !\n!^
    %   %  %%%$Defines%%% !\n!^
    %   % input: !\n!^
    %   %  $StoreVAR: required : String  : name of return variable !\n!^
    %   %  $Mininmum: required : StrVar  : Lower boundary !\n!^
    %   %  $Maximum : required : StrVar  : Upper boundary !\n!^
    %   % ToConsole: !\n!^
    %   %  Contents of $StoreVAR !\n!^
    %   %  The output can be redirected. !\n!^
    %   % Return: !\n!^
    %   %  $error: 0 for succes, panic otherwise "
       ::
       set ^"%$Defines%=do ( %\n2%

          !@forTS! ( "%$Defines%"¦"req:str:$StoreVAR=%%~a"¦"req:SVr:$Mininmum=%%~b"¦"req:SVr:$Maximum=%%~c" ) !@LeadIn! %\n2%
          rem !@forTS! ( "@DebugIn" ) !@CallMacro! %\n2%

          Setlocal EnableDelayedExpansion %\n2%

          call set /a $StdOut = %%Random%% %%%% ^^^^^^^( $Maximum - $Minimum + 1 ^^^^^^^) + $Minimum %\n2%

          !@forTS! ( %\n2%

             "^!$StoreVAR^!=^!$StdOut^!" %\n2%

          )    !@PassVarOverEndlocal! %\n2%

       ) ^&( %\n2%

          !@\necho! %\n2%
          call set /p "?= %%~a : ^!%%~a^! [^!$0.error^!]" ^<nul %\n2%

          rem !@forTS! ( "@DebugOut" ) !@CallMacro! %\n2%
          !@LeadOut! %\n2%
       )"
    ::)
    %@Post% @GetRandom [OK]
    ::--------------------------------------------------------------------------------------------------------------------------

The amount of characters a variable can contain, is limited !
( The "normal" maximum line length is ~8192, the maximum line length for drag&drop operations is ~2048. And even the 8192 can be exceeded with an FOR /F loop. jeb )
Assigning any more will result in a syntax error at defenition phase !
This is the only problem I cannot overcome.
And it's a very nasty problem, because it prohibits me from writing complex macro's.

But we can try to work around this problem:
Complex macro's often refer internally to other macro's. Remember we can do that !

Code: Select all

    ::--------------------------------------------------------------------------------------------------------------------------
    %@\necho% ^>^> Pre Define @Pre
    ::(
       set "@Pre=%@\necho% ^>^> Pre Define"
    ::)
    echo. ^<^< Post Define @Pre [OK]
    ::--------------------------------------------------------------------------------------------------------------------------

But they are expanded at definition phase. That's why a macro that is overloaded fails at this very moment.

So what makes our macro big ? Lets zoom into something expert:

Code: Select all

    expand %@getmacro% >> if error = then else if then else for then...

But wait !, an error event shouldn't occur ! That's like aborting the hole batch script ! But why is this spacetaking code inside my macro ? Workaround:

Code: Select all

    ::--------------------------------------------------------------------------------------------------------------------------
    %@Pre% @IfErrorBoolExit
    ::(
       set ^"@IfErrorBoolExit=( %\n2%

          if /i ["^!$error.bool^!"] neq ["0"] !@IfErrorExit! %\n2%
       )"
    ::)
    %@Post% @IfErrorBoolExit [OK]
    ::--------------------------------------------------------------------------------------------------------------------------

And this is the workaround, Remember our functions ?:

Code: Select all

 
    ::--------------------------------------------------------------------------------------------------------------------------
    %@Pre% @IfErrorExit
    ::(
       set ^"@IfErrorExit=( %\n2%

          if ^^^!$error^^^! gtr 0 !@forTS! ( "@PANIC" ) !@CallMacro! %\n2%
       )"
    ::)
    %@Post% @IfErrorExit [OK]
    ::--------------------------------------------------------------------------------------------------------------------------

We are doing a function call, exactly what we wanted to avoid in the first place, our macro just lost a couple of hundreds of millisecs !

Code: Select all

    !@forTS! ( "@PANIC" ) !@CallMacro! %\n2%
    for %%! in ( "@PANIC" ) do ( call :§CallMacro %%~! )

But now we pushed the limits a little higher and are allowing our macro to grow in complexity.
Besides fatal errors should never occur, that's why we don't loose any speed !
As if ^^^!$error^^^! gtr 0 should always be false !

Code: Select all

    ::--------------------------------------------------------------------------------------------------------------------------
    %@Pre% @PANIC
    ::(
       set ^"$PANIC=^
    %   % !\n!^
    %   % 00000      0    00    0 0000000    0000 !\n!^
    %   % 0    0    0 0   00    0    0      0     !\n!^
    %   % 0     0   0 0   0 0   0    0     0      !\n!^
    %   % 0     0   0 0   0 0   0    0     0      !\n!^
    %   % 0    0   0   0  0  0  0    0    0       !\n!^
    %   % 00000    00000  0  0  0    0    0       !\n!^
    %   % 0        0   0  0  0  0    0    0       !\n!^
    %   % 0        0   0  0   0 0    0     0      !\n!^
    %   % 0       0     0 0   0 0    0     0      !\n!^
    %   % 0       0     0 0    00    0      0     !\n!^
    %   % 0       0     0 0    00 0000000    0000 "
       ::
       set ^"@PANIC=( %\n2%

          !@Get_Error_from_ErrorLevel_NotZero! %\n2%

          !@\necho! ^&set /p "?= Function: " ^<nul %\n2%

          SetLocal EnableExtensions EnableDelayedExpansion %\n2%

          set /a $error = 0 %\n2%

          if not defined $Debug.Array (             %$6.error=attempt to use $Debug.Array while undefined% %\n2%

             if defined $Usage.@DeclareArray ( %\n2%

                set /p "?=@PANIC on $Debug.Array" ^<nul %\n2%
                !@forTS! ^( "$Usage.@DeclareArray" ^) !@SeeVarsNoErr! %\n2%
                echo.       ^&set /p "?= Fatality: " ^<nul %\n2%

                set /a $error = 6 %\n2%

             ) else (                %$7.error=attempt to use an array undefined% %\n2%

                set /p "?=@PANIC on $Usage.@DeclareArray" ^<nul %\n2%
                echo.       ^&set /p "?= Fatality: " ^<nul %\n2%

                set /a $error = 7 %\n2%

             ) %\n2%

          ) %\n2%

          if ^^^!$error^^^! equ 0 ( %\n2%

             set /a $count = 0 %\n2%
             set "$Debug.Array=^!$Debug.Array:¦= ^!" %\n2%
             for %%^^^? in ( %\n2%

                ^^^!$Debug.Array^^^! %\n2%

             ) do ( %\n2%

                set /a $count += 1 %\n2%
                set "$Debug.^!$count^!=%%~^?" %\n2%

             ) %\n2%
             set "$Debug.Array=^!$Debug.Array: =¦^!" %\n2%
             set /a $Debug.0 = ^^^!$count^^^! %\n2%

             if not defined $Usage.^^^!$Debug.1^^^! (    %$7.error=attempt to use an array undefined% %\n2%

                set /p "?=@PANIC on $Usage.^!$Debug.1^!" ^<nul %\n2%
                echo.       ^&set /p "?= Fatality: " ^<nul %\n2%

                set /a $error = 7 %\n2%

             ) else ( %\n2%

                set /p "?='^!$Debug.1^!'" ^<nul %\n2%
                echo.       ^&!@forTS! ( "$Usage.^!$Debug.1^!" ) !@SeeVarsNoErr! %\n2%
                !@\necho!    ^&set /p "?= Fatality: " ^<nul %\n2%
                echo.      ^&set /p "?=  Traces: '^!$Debug.Array^!'" ^<nul %\n2%
                echo.      ^&set /p "?=  Caller: '^!$Debug.2^!'" ^<nul %\n2%
                echo.      ^&set /p "?=  Called: '^!$Debug.1^!'" ^<nul %\n2%
                echo.      ^&set /p "?=  LInput: '^!$LInput^!'" ^<nul %\n2%

             ) %\n2%

          ) %\n2%

          if ^^^!$error^^^! neq 0 !@ExitFATAL! %\n2%

          EndLocal %\n2%

          !@ExitFATAL! %\n2%
       )"
    ::)
    %@Post% @PANIC [OK]
    ::--------------------------------------------------------------------------------------------------------------------------

Code: Select all

    ::--------------------------------------------------------------------------------------------------------------------------
    :§ExitFATAL "()"
    ::(
       "!FullPathFile.CSOUND!" 220^(2^) 294^(4^)
       ::
       for /l %%! in (

          1, 1, 10

       ) do (
          color 0C &>nul "!FullPathFile.CHOICE!" /c:ny /t:n,2 /n &if Errorlevel 2 exit /b
          color C0 &>nul "!FullPathFile.CHOICE!" /c:ny /t:n,1 /n &if Errorlevel 2 exit /b
       )
       ::
       goto :§ExitFATAL "()"
    ::
    exit /b
    ::)
    ::--------------------------------------------------------------------------------------------------------------------------