Macro for Dummies

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
T3RRY
Posts: 250
Joined: 06 May 2020 10:14

Macro for Dummies

#1 Post by T3RRY » 10 Feb 2024 00:27

As requested by @miskox at viewtopic.php?f=3&t=10983#p69429

As remarked within the code, this template is intended to make it easy to create macro's that take arguments

The template does nothing with the arguments other than splitting them and returning them in an array.
Features:
- Basic error handling
- Flag if expanded in envirronment without EDE
- Output Usage if expanded without Arguments
- Support for a large number of arguments - 750 innately, but the true determining factor will be the input length of the argument string
- Custom Variable name for the $Return Array via sting substitution when expanded.
- Template specifies:
- where you need to add in your custom proccessing of the arg
- where to insert help specific to your macro

CTRL + H may be used to replace $Macro.Template with your own macro name.

Code: Select all

@Echo off & Setlocal EnableExtensions & CLS

REM Author: T3RR0R, aka T3RRY. 10/02/2024
REM Discuss with author at https://discord.gg/HCSEC829F9
REM 
REM - A template for the creation of macros with arguments
REM   Macros may be defined in either Delayed enabled or disabled environments, but will require
REM   Delayed expansion to be enabled in order for them to be expanded.
REM   
REM   EXPLAINER
REM   - Macro's offer superior performance to CALLed :functions, however involve an
REM     increased degree of complexity in their definition and use.
REM     This template aims to reduce the learning curve required and make macro's more accessible
REM     As such, no processing of the Arguments provided to the Macro is performed
REM     
REM     Any Macro specific processing is to be defined by You, the user, where the template notes
REM     to insert such processing.
REM     
REM     The DEFAULT behavior is to split the arguments into an Array defined to $Return[#], where
REM     # is the current index of the Array (1 indexed).
REM     The size of the Array is returned in $Return[i]
REM     $Return may be substituted using Substring Modification with a custom variable name at the
REM     time of expansion.
REM     
REM    Use the search and replace tool of your script editor to replace $Macro.Template with the 
REM    intended name of your Macro

REM Recommended Learning resources: 
	REM [1] https://www.dostips.com/forum/viewtopic.php?t=9265#p60294
	REM [2] https://www.dostips.com/forum/viewtopic.php?f=3&t=10983&sid=f6937e02068d93bc5a97ef63d4e5319e
	REM  - Macros with arguments:
	REM [3] https://www.dostips.com/forum/viewtopic.php?f=3&t=1827
	REM  - CMD parsing behaviour:
	REM [4] https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts

	REM String length method is a modified version originally produced by JEB
	REM See link at resource [2] above.

(Set $\n=^^^

%= DO NOT MODIFY $\n newline variable for multi-line macro definition =%)

REM - Each internal line of multiline macro's must be terminated with the escaped linefeed variable:
REM   %$\n%

REM - EnableDelayedExpansion Aka EDE required to %expand% macro/s
REM   May be placed before or after Macro definiton.
Setlocal EnableDelayedExpansion

For /f %%! in ("! ^! ^^^!") Do %= This outer loop allows DDE or EDE environment independant definition =%^
%= IMPORTANT - No whitespace permitted here =%Set $Macro.Template=For %%n in (1 2)Do if %%n == 2 (%$\n%
	If "%%!%%!" == "" (%$\n: Error Check - DDE or EDE state. If Delayed expansion [EDE] is enabled; enact macro =%
		If not "%%!$Macro.Template.Args%%!" == "" (%$\n: Arg Validation; If Macro expanded with args enact arg processing =%
			If "%%!$Macro.Template.Args:~0,1%%!" == " " Set "$Macro.Template.Args=%%!$Macro.Template.Args:~1%%!"%$\n%
			Set "$Macro.Template.Args=%%!$Macro.Template.Args:" "=","%%!"%$\n%
			For /l %%i in (1 1 750)Do If defined $Macro.Template.Args For /f "tokens=1 Delims=," %%? in ("%%!$Macro.Template.Args%%!")Do If not %%? == "" (%$\n: Split Args =%
				Set "$Macro.Template.ThisArg=%%~?"%$\n%
				(Set^^ "$_=%%~?")%$\n%
				If Defined $_ (%$\n: Get Arg Len to Shift Arg =%
					Set "$Macro.Template.ThisArg.Len=1"%$\n%
					For %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1)Do (%$\n%
						If "%%!$_:~%%P,1%%!" NEQ "" (%$\n%
							Set /a "$Macro.Template.ThisArg.Len+=%%P"%$\n%
							Set "$_=%%!$_:~%%P%%!"%$\n%
						)%$\n%
					  )%$\n%
				)Else set "$Macro.Template.ThisArg.Len=0"%$\n%
				Set /a "$Macro.Template.ThisArg.Len+=3"%$\n%
				For /F "delims=" %%i in ("%%!$Macro.Template.ThisArg.Len%%!")Do Set "$Macro.Template.Args=%%!$Macro.Template.Args:~%%i%%!"%$\n: Shift Arg =%
%$\n: INSERT CUSTMIZED PROCESSING OF ARGUMENT HERE; TERMINATE EACH LINE WITH $\n VARIABLE =%
				Set "$Return[%%i]=%%!$Macro.Template.ThisArg%%!"%$\n%
				Set "$Return[i]=%%i"%$\n%
			)%$\n%
	)Else (%$\n: ARS INVALIDATED; OUTPUT USAGE =%
			Echo(%$\n%
			Echo(Usage:%$\n%
			Echo( %%$Macro.Template%% "arg string" "arg string" "arg String"%$\n%
			Echo( - Or -%$\n%
			Echo( %%$Macro.Template%% "arg string","arg string","arg String"%$\n%
			Echo(%$\n%
			Echo( %%$Macro.Template:$Return=ReturnName%% "arg string","arg string","arg String"%$\n%
			Echo( Use Substitution to provide a custom return variable name by replacing $Return%$\n%
			Echo(%$\n%
			Echo( - Arguments must be double quoted: "argument"%$\n%
			Echo( - Arguments must not contain ',' characters.%$\n%
			Echo(   Comma: ',' is used internally to seperate arguments.%$\n%
			Echo( - Arguments may be seperated by either whitespace: " " or Comma: ","%$\n%
			Echo(%$\n%
%$\n: INSERT CUSTOMIZED MACRO HELP HERE; TERMINATE EACH LINE WITH $\n VARIABLE =%
		)%$\n%
	)Else (%$\n: NO EDE - Notify Environment requirement =%
		Echo(%$\n%
		Echo( %%!	Delayed Expansion is currently Disabled.%$\n%
		Echo(	To successfully expand %%$Macro.Template%%, the command:%$\n%
		Echo(%$\n%
		Echo(Setlocal EnableDelayedExpansion%$\n%
		Echo(%$\n%
		Echo(	is required *before* this macro command.%$\n%
		Echo(%$\n%
	)%$\n%
)Else Set $Macro.Template.Args=%= Capture macro Args =%

Echo(Example:
Echo(%%$Macro.Template:$Return=My Array%% "Arg one" "Arg two" "<a=b>" "this=?" "and*" "Hello World^^^!"
%$Macro.Template:$Return=My Array% "Arg one" "Arg two" "<a=b>" "this=?" "and*" "Hello World^^^!"
Set "My Array"

REM Usage displayed if expanded with no args
%$Macro.Template%

Setlocal DisableDelayedExpansion
%$Macro.Template% "Demo Macro Error output in Environment Without EnableDelayedExpansion"


(
	Endlocal & Endlocal & Endlocal
	Pause
)
Edits: Fixed Typo's

miskox
Posts: 630
Joined: 28 Jun 2010 03:46

Re: Macro for Dummies

#2 Post by miskox » 10 Feb 2024 14:13

Thanks a lot T3rry. But I think you should go back some more steps and start with the basics.

I tried to understand this batch/macro you posted - too complicated. No luck. We would need something like 'hello world' and then go from there.

Something like this:

Code: Select all

@echo off
set helloworld=echo Hello World
%helloworld%

set forr=for /L %%f in (1,1,2) do

%forr% echo TEST_1_%%f

%forr% (
	%forr% echo TEST_2_%%f
)

And here it ends. You experts use so many extra characters that make everything so unreadable.

Thanks.

Saso

aGerman
Expert
Posts: 4678
Joined: 22 Jan 2010 18:01
Location: Germany

Re: Macro for Dummies

#3 Post by aGerman » 10 Feb 2024 17:01

Perhaps this is going to help you understanding the concept, Saso.

1. What is a Macro?
The term "macro" is borrowed from C preprocessing. Values and code can be assigned to a character string there. The C preprocessor then performs a simple text replacement before compilation. This means that wherever this character string appears in the code, it will be replaced by the previously defined value.
You can do something similar in batch using normal environment variables. The command processor performs the expansion of the variable to the value at a very early stage. So, here too we have nothing other than a text replacement even before the command line is executed.

2. What are macros for?
Actually, everyone who works with batch does already use it. If a value has to be used several times in the code (e.g. the path to a file) and it remains constant, you assign it to a variable at the beginning of the code and, thus, you have "defined a macro". This simplifies code maintenance. If the value needs to be changed, you have to do it only at one point for the entire code. It also improves the readability of the code.
However, this is not what people call a macro in this forum. The point is that you can as well put batch code in a variable that should be executed several times. You may also achieve the same advantages of maintainability and readability.

3. How do you work with macros?
Basically the same as with any other variable. You assign a value to a variable using SET. In this case, however, the value is batch code.
Simple example:

Code: Select all

@echo off &setlocal

REM macro definition:
set "alert=echo An error occurred!"  

REM macro execution:
%alert%

pause
Whenever an error is caught in a code and the user should be notified about it, place the variable at this point. An intuitive variable name tells the reader what the code behind is doing.

Of course, you can also put several commands. For example, after the notifying wait for user interaction and then abort the code:

Code: Select all

set "alert2=echo An error occurred! & pause & goto :eof"  

%alert2%
4. Multi-line macros
The last example works with "&" for command concatenation. If you want to put several command lines in a macro, the line breaks must be included in the variable value. An escaped linefeed character is sufficient for this purpose which needs to be defined beforehand

Code: Select all

(set \n=^^^
%= these lines create an escaped line feed, do not alter =%
)
We now have the variable %\n% (borrowed from C again), which when used with the SET command ensures that the variable expands to a value containing line breaks. (Edit: As T3RRY pointed out below, this works only if expanded in a parenthesized part of the macro code. In the alert3 example they are explicitely added around the whole code, while in the alert4 example the parentheses around the IF branch enclose all occurrences of %\n%.)
Of course, this definition needs to be carried out only once and can then be used in several multiline macros.
What would the alert2 macro look like as a multiline?

Code: Select all

(set \n=^^^
%= these lines create an escaped line feed, do not alter =%
)

set alert3=(%\n%
  echo An error occurred!%\n%
  pause%\n%
  goto :eof%\n%
)

%alert3%
Of course, this becomes more interesting with large blocks of code.

5. How to pass values to a macro?
This is actually a little tricky. In the macro you have to define a helper variable and the associated SET command must be at the end of the macro. The problem is that the processing of this variable has to happen in the code that comes before. How is that possible? You combine a 2-iteration FOR loop with an IF ELSE statement. The ELSE branch in which the variable assignment occurs must be processed first.
In the following example, the message to be output is passed to the alert4 macro:

Code: Select all

(set \n=^^^
%= these lines create an escaped line feed, do not alter =%
)

set alert4=for /l %%i in (1 1 2) do if %%i==2 (%\n%
  setlocal EnableDelayedExpansion%\n%
  if defined var echo !var:~1!%\n%
  endlocal%\n%
  pause%\n%
  goto :eof%\n%
) else set var=

%alert4% An error occurred!
In the first place, this looks more complicated than it actually is. It becomes understandable if you expand the variable in the last line. The code executed there is equivalent to the following block:

Code: Select all

for /l %%i in (1 1 2) do if %%i==2 (
  setlocal EnableDelayedExpansion
  if defined var echo !var:~1!
  endlocal
  pause
  goto :eof
) else set var= An error occurred!

Steffen

Aacini
Expert
Posts: 1914
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Macro for Dummies

#4 Post by Aacini » 10 Feb 2024 20:05

Some time ago I wrote my own explanation about how a macro with parameters works. You can read it here.

Later, I wrote a large macro called SET/S that can replace a substring in a variable, and then the huge SET/A macro that expand function calls in a "set /A" expression. Both macros are at this thread.

Antonio

T3RRY
Posts: 250
Joined: 06 May 2020 10:14

Re: Macro for Dummies

#5 Post by T3RRY » 10 Feb 2024 23:12

Steffen has answered well here the basics, with one minor ommission
The newline variable used for multiline definitions only works within parenthesised codeblocks

A simple example

Code: Select all

@Echo off

(Set $\n=^^^

%= DO NOT MODIFY $\n Newline variable definition =%)

Set example1=Echo Hello World 1	%$\n%
	Pause

Set example2=(			%$\n%
	Echo Hello World 2	%$\n%
	Pause			%$\n%
)

%example1%

%example2%
While the template definitely looks daunting, it notes where and how to add in you own lines of code to customize it's functionality. The idea being, the template handles the splitting of arguments, you simply insert any commands you want acted upon the values of those arguments.
The main thing you need to be aware of is that each line you add within it must end with the defined newline variable:

Code: Select all

     Some command%$\n%
Obviously there is no funtional difference between \n and $\n beyond preference. I use it as a way os segregating variable naming comventions. Anything Macro related I prefix with $

On to the nitty gritty of breaking down the template

** Note: Henceforth, DDE refers to DisableDelayedExpansion and EDE to EnableDelayedExpansion

The simple part of capturing the macro input steven has already covered, so I'll begin with the why's of given features

Code: Select all

For /f %%! in ("! ^! ^^^!") Do %= This outer loop allows DDE or EDE environment independant definition =%^
As the comment notes, the purpose of the outermost loop is to facilitate the correct definition of the macro, regardless of whether the scripter uses Setlocal EnableDeleyadExpansion before or after the macro definition. The escaped form of ! captured by the for loop will be the correct one for whichever enviroment type is active

This is not something all macro's need, but is handy for making macro's more accessible / user friendly.
Alternatives to using this method:
- force definiton in a DDE state and do not escape ! characters
- force definition in a EDE state and escape ! characters.
- as above, then test environment state and remove escaping if environment is DDE

Code: Select all

	If "%%!%%!" == "" (%$\n: Error Check - DDE or EDE state. If Delayed expansion [EDE] is enabled; enact macro =%
The above should be fairly straight forward. As we allow the macro to be defined in any environment type, we want to be sure the environment state is EDE prior to executing it.
Advanced macro's use environment toggling to force the state to EDE, however, as this is a GENERIC template designed to process unknown numbers of args [arbitrarily up to 750], using ENDLOCAL with For loop tokens becomes impractical. For this reason, macro execution is aborted in the event the environment is not in an EDE state and the user notified:

Code: Select all

	)Else (					    %$\n: NO EDE - Notify Environment requirement =%
		Echo(													%$\n%
		Echo( %%!	Delayed Expansion is currently Disabled.					%$\n%
		Echo(	To successfully expand %%$Macro.Template%%, the command:	%$\n%
		Echo(													%$\n%
		Echo(Setlocal EnableDelayedExpansion							%$\n%
		Echo(													%$\n%
		Echo(	is required *before* this macro command.					%$\n%
		Echo(													%$\n%
	)															%$\n%
(I've reformatted the above for better readability, and will do so henceforth)

The next part of the macro is another validation step. this time it's a test to see if args were provided or not.

Code: Select all

		If not "%%!$Macro.Template.Args%%!" == "" (%$\n: Arg Validation; If Macro expanded with args enact arg processing =%
If so, argument processing is done, else Macro usage is shown.

Code: Select all

	)Else (											 %$\n: ARS INVALIDATED; OUTPUT USAGE =%
			Echo(																%$\n%
			Echo(Usage:															%$\n%
			Echo( %%$Macro.Template%% "arg string" "arg string" "arg String"						%$\n%
			Echo( - Or -															%$\n%
			Echo( %%$Macro.Template%% "arg string","arg string","arg String"						%$\n%
			Echo(																%$\n%
			Echo( %%$Macro.Template:$Return=ReturnName%% "arg string","arg string","arg String"	%$\n%
			Echo( Use Substitution to provide a custom return variable name by replacing $Return	%$\n%
			Echo(																%$\n%
			Echo( - Arguments must be double quoted: "argument"							%$\n%
			Echo( - Arguments must not contain ',' characters.								%$\n%
			Echo(   Comma: ',' is used internally to seperate arguments.						%$\n%
			Echo( - Arguments may be seperated by either whitespace: " " or Comma: ","			%$\n%
			Echo(																%$\n%
				%$\n: INSERT CUSTOMIZED MACRO HELP HERE; TERMINATE EACH LINE WITH $\n VARIABLE =%
		)																		%$\n%
Prior to argument splitting, a small amount of restructuring is done on the input string to prep it for processing,
Namely trimming a sinle leading whitespace if present, and substituting the arg seperator " " with ",":

Code: Select all

	If "%%!$Macro.Template.Args:~0,1%%!" == " " Set "$Macro.Template.Args=%%!$Macro.Template.Args:~1%%!"	%$\n%
	Set "$Macro.Template.Args=%%!$Macro.Template.Args:" "=","%%!"								%$\n%
The process of argument splitting Is the only truly complex part of the macro, and not something the user should have a need to modify. Any conditional argument handling should be done using the value of the arg itself.

The purpose of this approach is to allow arguments with minimal restrictions given the generic nature of templating.
This achieves that using a psuedo-shift, by trimming the argument input variable by the length of of the current argument + 3 (to account for the "," seperating each argument) with each iteration of the outer for /l %%i loop. Once the Argument input variable has been trimmed beyond length, the for /f processing loop is disabled using an if defined test.

Code: Select all

			For /l %%i in (1 1 750)Do If defined $Macro.Template.Args For /f "tokens=1 Delims=," %%? in ("%%!$Macro.Template.Args%%!")Do If not %%? == "" (%$\n: Split Args =%
				Set "$Macro.Template.ThisArg=%%~?"							%$\n%
				(Set^^ "$_=%%~?")										%$\n%
				If Defined $_ (						%$\n: Get Arg Len to Shift Arg =%
					Set "$Macro.Template.ThisArg.Len=1"					%$\n%
					For %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1)Do (	%$\n%
						If "%%!$_:~%%P,1%%!" NEQ "" (						%$\n%
							Set /a "$Macro.Template.ThisArg.Len+=%%P"		%$\n%
							Set "$_=%%!$_:~%%P%%!"						%$\n%
						)											%$\n%
					  )												%$\n%
				)Else set "$Macro.Template.ThisArg.Len=0"						%$\n%
				Set /a "$Macro.Template.ThisArg.Len+=3"						%$\n%
				For /F "delims=" %%i in ("%%!$Macro.Template.ThisArg.Len%%!")Do Set "$Macro.Template.Args=%%!$Macro.Template.Args:~%%i%%!"%$\n: Shift Arg =%
%$\n: INSERT CUSTMIZED PROCESSING OF ARGUMENT HERE; TERMINATE EACH LINE WITH $\n VARIABLE =%
				Set "$Return[%%i]=%%!$Macro.Template.ThisArg%%!"				%$\n%
				Set "$Return[i]=%%i"										%$\n%
			)														%$\n%
Why not just use a plain For you ask?
Because plain for does not treat strings as literal strings even when they are doublequoted. Plain for has wildcard characters such as * and ? that will result in parsing not intended for this use case, and would preclude those characters from being recieved as part of an argument

The method of "shifting" arguments has an added benefit that is utilized here. as the for /l loop already contains the argument number, there is no need to use a variable to count arguments and therefor no need of a for loop to expand said variable

Some work is still needed to be done with this to easily facilitate the use of switches for the end user.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: Macro for Dummies

#6 Post by jfl » 11 Feb 2024 07:06

Code: Select all

for /f %%! in ("! ^! ^^^!") 
Why is there a '^' before the second '!' in the %%! definition above?

I've run lots of tests without that '^', and never had any problem with that:

Code: Select all

for /f %%! in ("! ! ^^^!") 

aGerman
Expert
Posts: 4678
Joined: 22 Jan 2010 18:01
Location: Germany

Re: Macro for Dummies

#7 Post by aGerman » 11 Feb 2024 08:19

Doubling the length of the variable name reduces the risk of a predefined variable by half. If the risk of having a variable name " " was 0 before, it's only half 0 using " ^". Umm ... wait ... :lol:
idk ¯\_(ツ)_/¯

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: Macro for Dummies

#8 Post by jfl » 11 Feb 2024 12:51

It's not possible to create a variable called " =".

Code: Select all

C:\JFL\Temp>set " ==1"
La syntaxe de la commande n’est pas correcte.

C:\JFL\Temp>set  ==1
La syntaxe de la commande n’est pas correcte.

C:\JFL\Temp>
What about doing this for really reducing the risk to 0?

Code: Select all

for /f %%! in ("! =! ^^^!") do

T3RRY
Posts: 250
Joined: 06 May 2020 10:14

Re: Macro for Dummies

#9 Post by T3RRY » 11 Feb 2024 13:44

jfl wrote:
11 Feb 2024 07:06

Code: Select all

for /f %%! in ("! ^! ^^^!") 
Why is there a '^' before the second '!' in the %%! definition above?

I've run lots of tests without that '^', and never had any problem with that:

Code: Select all

for /f %%! in ("! ! ^^^!") 
because the behavior differs marginally. ultimately though, ts a preference to convery the purpose

Code: Select all

@Echo off

@Echo off

Set a1 =-here-

Setlocal EnableDelayedExpansion
for /f %%! in ("!a1 ^! ^^^!") Do Echo(%%!

ENDLOCAL
for /f %%! in ("!a1 ^! ^^^!") Do Echo(%%!

Setlocal EnableDelayedExpansion
for /f %%! in ("!a1 ! ^^^!") Do Echo(%%!

ENDLOCAL
for /f %%! in ("!a1 ! ^^^!") Do Echo(%%!


jeb
Expert
Posts: 1055
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Macro for Dummies

#10 Post by jeb » 11 Feb 2024 15:11

T3RRY wrote:
11 Feb 2024 13:44
because the behavior differs marginally. ultimately though, ts a preference to convery the purpose
Your example only shows the fact that a variable can be expanded, when the name is complete

Code: Select all

@echo off
Set a1 =-ONE-
Set "a1 ^=-TWO-"

Setlocal EnableDelayedExpansion
for /f %%! in ("!a1 ! ^^^!") Do Echo(#1 %%!
for /f %%! in ("!a1 ^! ^^^!") Do Echo(#2 %%!
ENDLOCAL
Spaces can be part of a variable name, but it's not possible to create a variable name beginning with a space.

But independent of the usage of the %%! you need to handle also carets.

Code: Select all

Setlocal DisableDelayedExpansion
for /f %%! in ("!a1 ! ^^^!") Do Echo "Test#1 %%! one caret^"
Setlocal EnableDelayedExpansion
for /f %%! in ("!a1 ! ^^^!") Do Echo "Test#2 %%! one caret^"
It can be solved in the same way with a %%^

Code: Select all

Setlocal DisableDelayedExpansion
for /f %%! in ("! ! ^^^!") do FOR /F %%^^ in ("^ ^^^^%%!=%%!")  DO (
    echo "Test#1 %%! one caret%%^"
)
Setlocal EnableDelayedExpansion
for /f %%! in ("! ! ^^^!") do FOR /F %%^^ in ("^ ^^^^%%!=%%!")  DO (
    echo "Test#2 %%! one caret%%^"
)

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: Macro for Dummies

#11 Post by jfl » 12 Feb 2024 05:39

jeb wrote:
11 Feb 2024 15:11
But independent of the usage of the %%! you need to handle also carets.
This is precisely why this weekend I created the FOR! and FOR!! macros, used for building my WHILE and REPEAT macros.
Having those exclamation and caret %%variables themselves defined in a macro makes it easier to reuse them all over your code.

I like your idea of using %%^^ for defining carets. This is better than the %%h (h for hat) that I used.
Here's an updated version of these FOR! and FOR!! macros using it:

Code: Select all

:# Define a %FOR!% macro, itself defining %%^^=^ and %%!=!, after one %expansion%
for /f %%^^ in ("^ ^^^^ !!") do for /f %%! in ("! =! ^^^!") do ^
set FOR%%!=for /f %%%%^^%%^^ in ^(^"%%^^ %%^^%%^^%%^^%%^^ %%!%%!^"^) do for /f %%%%! in ^(^"%%! =%%! %%^^%%^^%%^^%%!^"^) do

:# Define a %FOR!!% macro, itself defining %%^^=^ and %%!=!, after two %expansions%
%FOR!% for /f "tokens=2" %%H in ("!! ^^^^ ^^^^^^^^^^^^^^^^") do ^
set FOR%%!%%!=for /f "tokens=2" %%%%^^%%^^ in ^(^"%%!%%! %%^^%%^^ %%H%%H%%H%%H^"^) do for /f %%%%! in ^(^"%%! =%%! %%H%%H%%H%%^^%%^^%%^^%%!^"^) do
A nice side benefit of using this %%^^ name is that I don't need the %%p anymore, suppressing the risk of conflict with any user defined %%variable using a letter.

Here's a complete script with a test:

Code: Select all

@echo off
for %%x in (Enable Disable) do (
  echo.
  setlocal %%xDelayedExpansion
  for /f "tokens=2" %%e in ("!! Dis En") do echo Expansion is %%eabled
  call :exclMacro.init
  call :exclMacro.test
  endlocal
)
exit /b

:exclMacro.init
:# Define a %FOR!% macro, itself defining %%^^=^ and %%!=!, after one %expansion%
for /f %%^^ in ("^ ^^^^ !!") do for /f %%! in ("! =! ^^^!") do ^
set FOR%%!=for /f %%%%^^%%^^ in ^(^"%%^^ %%^^%%^^%%^^%%^^ %%!%%!^"^) do for /f %%%%! in ^(^"%%! =%%! %%^^%%^^%%^^%%!^"^) do

:# Define a %FOR!!% macro, itself defining %%^^=^ and %%!=!, after two %expansions%
%FOR!% for /f "tokens=2" %%H in ("!! ^^^^ ^^^^^^^^^^^^^^^^") do ^
set FOR%%!%%!=for /f "tokens=2" %%%%^^%%^^ in ^(^"%%!%%! %%^^%%^^ %%H%%H%%H%%H^"^) do for /f %%%%! in ^(^"%%! =%%! %%H%%H%%H%%^^%%^^%%^^%%!^"^) do

exit /b

:exclMacro.test
set FOR!

:# Test the FOR! macro
%FOR!% (
  echo # Using FOR%%!
  echo All=%%^^ %%!
  echo Hat=%%^^
  echo Excl=%%!
)

:# Test the FOR!! macro
%FOR!% echo # Using FOR%%!%%!
%FOR!!% set CMD=echo All=%%^^ %%!
set CMD
%CMD%
%FOR!!% set ^"CMD=echo Hat=%%^^^" !
set CMD
%CMD%
%FOR!!% set CMD=echo Excl=%%!
set CMD
%CMD%

exit /b
and its result:

Code: Select all

Expansion is Enabled
FOR!=for /f %^^ in ("^ ^^^^ !!") do for /f %! in ("! =! ^^^!") do
FOR!!=for /f "tokens=2" %^^ in ("!! ^^ ^^^^^^^^^^^^^^^^") do for /f %! in ("! =! ^^^^^^^^^^^^^^^!") do
# Using FOR!
All=^ !
Hat=^^
Excl=!
# Using FOR!!
CMD=echo All=^^^^ ^^^!
All=^ !
CMD=echo Hat=^^^^
Hat=^^
CMD=echo Excl=^^^!
Excl=!

Expansion is Disabled
FOR!=for /f %^^ in ("^ ^^^^ !!") do for /f %! in ("! =! ^^^!") do
FOR!!=for /f "tokens=2" %^^ in ("!! ^^ ^^^^^^^^^^^^^^^^") do for /f %! in ("! =! ^^^^^^^^^^^^^^^!") do
# Using FOR!
All=^ !
Hat=^
Excl=!
# Using FOR!!
CMD=echo All=^^ !
All=^ !
CMD=echo Hat=^^
Hat=^
CMD=echo Excl=!
Excl=!
Note that with expansion enabled, and in the absence of a ! in the command, both the FOR! and FOR!! macros generate two ^^ for each %%^^. This is unavoidable.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: Macro for Dummies

#12 Post by jfl » 12 Feb 2024 09:45

The rule for adding one more %expansion% depth level support is:
  • 2x the 1st set of hats in %%^^ definition
  • 4x the 2nd set of hats in %%^^ definition
  • 4x+3 the set of hats in the %%! definition
Which allows to simplify the second macro, and generate two more:

Code: Select all

:# Define a %FOR!% macro, itself defining %%^^=^ and %%!=!, after one %expansion%
for /f %%^^ in ("^ ^^^^ !!") do for /f %%! in ("! =! ^^^!") do ^
set FOR%%!=for /f %%%%^^%%^^ in ^(^"%%^^ %%^^%%^^%%^^%%^^ %%!%%!^"^) do for /f %%%%! in ^(^"%%! =%%! %%^^%%^^%%^^%%!^"^) do

:# Define a %FOR!2% macro, itself defining %%^^=^ and %%!=!, after two %expansions%
%FOR!% for %%2 in (%%^^%%^^%%^^%%^^) do for %%+ in (%%2%%^^) do ^
set FOR%%!2=for /f "tokens=2" %%%%^^%%^^ in ^(^"%%!%%! %%^^%%^^ %%2%%2%%2%%2^"^) do for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do

:# Define a %FOR!3% macro, itself defining %%^^=^ and %%!=!, after three %expansions%
%FOR!% for %%2 in (%%^^%%^^%%^^%%^^) do for %%3 in (%%2%%2%%2%%2) do for %%+ in (%%3%%2%%^^) do ^
set FOR%%!3=for /f "tokens=2" %%%%^^%%^^ in ^(^"%%!%%! %%2 %%3%%3%%3%%3^"^) do for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do

:# Define a %FOR!4% macro, itself defining %%^^=^ and %%!=!, after four %expansions%
%FOR!% for %%2 in (%%^^%%^^%%^^%%^^) do for %%3 in (%%2%%2%%2%%2) do for %%4 in (%%3%%3%%3%%3) do for %%+ in (%%4%%3%%2%%^^) do ^
set FOR%%!4=for /f "tokens=2" %%%%^^%%^^ in ^(^"%%!%%! %%2%%2 %%4%%4%%4%%4^"^) do for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do
These four macros should make it easier to generate various versions of a given macro, for use directly, or indirectly inside another macro.
Ex:

Code: Select all

%FOR!%  set ABORT= (echo Error%%!) &:# Macro for direct use
%FOR!2% set ABORT2=(echo Error%%!) &:# Macro for indirect use within another macro
%FOR!% set MY_MACRO_REQUIRING_DELAYED_EXPANSION=^(if not ^"%%!%%!^"=="" ^(%ABORT2% ^& exit /b 1^) else echo OK^)
Last edited by jfl on 12 Feb 2024 10:33, edited 1 time in total.

jeb
Expert
Posts: 1055
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Macro for Dummies

#13 Post by jeb » 12 Feb 2024 10:29

@jfl Nice efforts :D
jfl wrote:
12 Feb 2024 05:39
Note that with expansion enabled, and in the absence of a ! in the command, both the FOR! and FOR!! macros generate two ^^ for each %%^^. This is unavoidable.
But the solution for this problem should be obvious 8)

From the Batch Library/libBase.cmd

Code: Select all

REM *** Creating %%! and %%^ for defining the $lib.macrodefine.free in a safe way
REM *** The definition is independent of the current delayed expansion mode
REM *** This macro is used later for defining delayed-independent macros
REM *** When using the macro %$lib.macrodefine.free%  ...
REM *** then %%! contains a single !
REM *** %%^ creates a single ^, but it contains "^" in DDE or "^^!=!" in EDE mode
FOR /F "tokens=1 delims== " %%! in ("!=! ^^^!") DO ^
FOR /F %%^^ in ("^ ^^^^%%!=%%!") DO ^
set ^"$lib.macrodefine.free=@FOR /F "tokens=1 delims== " %%%%! in ("%%!=%%! %%^%%^%%^%%!") DO ^
@FOR /F %%%%^^%%^^ in ("%%^ %%^%%^%%^%%^%%^%%!=%%^%%!") DO @"

You said it already, you need a ! somewhere in the line, when %%^ expands to ^^
I simply put them just behind, so %%^ expands to ^^!=! it automatically reduces to a single ^

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: Macro for Dummies

#14 Post by jfl » 13 Feb 2024 03:53

jeb wrote:
12 Feb 2024 10:29
I simply put them just behind, so %%^ expands to ^^!=! it automatically reduces to a single ^
Thanks for the tip, I should have tought of that :oops:

I've easily fixed the FOR! macro using that technique, but I'm struggling to fix the other three.
And (maybe related to my difficulties with the other three?) I still dont understand why the = sign is necessary:
I tried having %%^ expand to ^^!!, but this does not work. :shock:

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: Macro for Dummies

#15 Post by jfl » 13 Feb 2024 05:48

OK, I got it at last:

Code: Select all

:# Define a %FOR!% macro, itself defining %%^^=^ and %%!=!, after one %expansion%
for /f %%! in ("! =! ^^^!") do for /f %%1 in ("^ ^^^^ !!") do for %%~ in ("") do for %%+ in (%%1%%~~) do ^
set  FOR%%!=for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do for /f %%%%1%%1 in ^(^"%%1 %%1%%1%%1%%1%%~~%%~~%%~~%%!=%%~~%%~~%%~~%%!^"^) do

:# Define a %FOR!2% macro, itself defining %%^^=^ and %%!=!, after two %expansions%
for /f %%! in ("! =! ^^^!") do for /f %%1 in ("^ ^^^^ !!") do for %%2 in (%%1%%1%%1%%1) do for %%~ in (%%1) do for %%+ in (%%2%%~~) do ^
set FOR%%!2=for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do for /f "tokens=2" %%%%1%%1 in ^(^"%%!=%%! %%1%%1 %%2%%2%%2%%2%%~~%%~~%%~~%%!=%%~~%%~~%%~~%%!^"^) do

:# Define a %FOR!3% macro, itself defining %%^^=^ and %%!=!, after three %expansions%
for /f %%! in ("! =! ^^^!") do for /f %%1 in ("^ ^^^^ !!") do for %%2 in (%%1%%1%%1%%1) do for %%3 in (%%2%%2%%2%%2) do for %%~ in (%%2%%1) do for %%+ in (%%3%%~~) do ^
set FOR%%!3=for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do for /f "tokens=2" %%%%1%%1 in ^(^"%%!=%%! %%2 %%3%%3%%3%%3%%~~%%~~%%~~%%!=%%~~%%~~%%~~%%!^"^) do

:# Define a %FOR!4% macro, itself defining %%^^=^ and %%!=!, after four %expansions%
for /f %%! in ("! =! ^^^!") do for /f %%1 in ("^ ^^^^ !!") do for %%2 in (%%1%%1%%1%%1) do for %%3 in (%%2%%2%%2%%2) do for %%4 in (%%3%%3%%3%%3) do for %%~ in (%%3%%2%%1) do for %%+ in (%%4%%~~) do ^
set FOR%%!4=for /f %%%%! in ^(^"%%! =%%! %%+%%+%%+%%!^"^) do for /f "tokens=2" %%%%1%%1 in ^(^"%%!=%%! %%2%%2 %%4%%4%%4%%4%%~~%%~~%%~~%%!=%%~~%%~~%%~~%%!^"^) do
In any expansion mode, this generates:

Code: Select all

FOR!=for /f %! in ("! =! ^^^!") do for /f %^^ in ("^ ^^^^!=!") do
FOR!2=for /f %! in ("! =! ^^^^^^^^^^^^^^^!") do for /f "tokens=2" %^^ in ("!=! ^^ ^^^^^^^^^^^^^^^^^^^!=^^^!") do
FOR!3=for /f %! in ("! =! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^!") do for /f "tokens=2" %^^ in ("!=! ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^!=^^^^^^^^^^^^^^^!") do
FOR!4=for /f %! in ("! =! ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^!") do for /f "tokens=2" %^^ in ("!=! ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^!=^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^!") do
And in turn these macros generate code where %%^^ generates a single ^ in all cases :D

The critical change was to change "%%!%%!" to "%%!=%%!" at the beginning of the for /f %%^^ definitions.
Else that 'for /f' loop aborts with no 2nd token found in the string.
I still don't understand why appending !=! to the 3rd token changes the expansion of !! in the first token, which did work fine as intended in the previous version of the macro.

Post Reply