infinite loop with break condition

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

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

Re: infinite loop with break condition

#76 Post by jfl » 11 Feb 2024 12:25

I've been playing with these WHILE, REPEAT, and BREAK macros for a while, and came up with several significant improvements.

First, it's relatively easy to avoid the need for a WEND macro following the WHILE macro, while still preserving the ability to nest WHILE and REPEAT loops.
Unfortunately, that technique does not allow removing the REND macro after REPEAT.
Here's the code that does it:

Code: Select all

set REP16X=for /l %%- in (0,1,15) do if not defined -
set REP4GX=%REP16X% %REP16X% %REP16X% %REP16X% %REP16X% %REP16X% %REP16X% %REP16X%

set NOP=if 1==0 NOP
set BREAK=set "-=-"

set WHILE=for %%- in ^(1 2^) do set "-=" ^& if %%-==1 %REP4GX% ^(if
set DO=^(%NOP%^) else %BREAK%^) ^& if not defined -
set WEND=

set REPEAT=for %%- in ^(1 2^) do set "-=" ^& if %%-==1 %REP4GX%
set UNTIL=^& if
set REND=%BREAK%
Notes:
- I've removed the parenthesis from the previous versions of the macro names WHILE( )DO and UNTIL( )REND, as they add an unnecessary complexity.
- They're using the same for %%- variable for all 9 for loops each, to minimize the %%variable usage.
- I've left a definition clearing the WEND macro, to allow testing various versions with and without a WEND.
- The ^ ahead of each parenthesis allows including theses definitions inside parenthesized blocks.
Advantages:
- These macro definitions can be done in any delayed expansion mode.
- These macros can be embedded in other macros.
Drawbacks:
- #1 If another instruction follows on the same line, both WHILE and REPEAT loops must be enclosed in parenthesis. (Same as for a 'for' or an 'if' instruction for that matter.)
Ex:

Code: Select all

set "N=0" & (%WHILE% not "!N!"=="25" %DO% (<NUL set /p "=!N! " & set /a "N+=1") %WEND%) & echo. & echo Done
- #2 In most practical cases, these macros require enabled delayed expansion for their execution, but they don't enforce that.
- #3 They break severely if used in a block piped to another block.
- #4 The WHILE and REPEAT macros are quite long, with respectively 486 and 483 characters.

Workaround for drawback #1:
That need for parenthesis in some cases introduces a risk of bug.
It can easily be prevented by inserting a ' (' ahead of the WHILE value, and redefining WEND=) ... At the cost of reintroducing that WEND variable which I struggled to eliminate.

Workaround for drawback #2:
This one can easily be fixed by automatically enabling delayed expansion, using the techniques developed recently on this forum.
But again this comes at a cost: Once the macros themselves contain '!' characters, they cannot be reused easily inside other macros.

Workaround for drawback #3:
The root cause is that each piped block is serialized into a single command line, which is passed to a child cmd instance.
- This forces having parenthesis around WHILE and REPEAT blocks due to drawback #1, even if the initial multi-line block did not need these parenthesis.
- A known cmd bug causes `if defined -` instructions to be changed to `if defined-`, generating a syntax error.
Workaround: Change `if defined -` to `if !-!1==-1`. Which implies implementing workaround #2, with all its drawbacks.
- But this still does not work, as child cmd instances are started with expansion disabled, and `setlocal EnableDelayedExpansion` has no effect on the command-line. No solution so far.

Workaround for drawback #4:
Use jeb's technique in the REP16 definition: set REP16X=for /l %%- in (0,1,!-!15) do
Again, this forces implementing workaround #2, with all its drawbacks.

As workaround #2 was necessary in many cases, I went ahead and implemented it along with most of the above.
Then I quickly got stuck by a serious issue:
The `for /f %%! in ("! ^! ^^^!") do` prefix could not easily be used for defining the REP16X macro, as it's %expanded% THREE times before being used.
To make a long story short, I eventually defined two helper macros
- A %FOR!% macro replacing the above `for /f %%! in ("! ^! ^^^!") do` prefix for macros to be %expanded% once.
- A %FOR!!% macro replacing the above prefix for macros to be %expanded% twice.
And I rewrote my code to avoid the intermediate REP4GX macro, limiting the number of %expansion% levels support I needed.
The result is this final set of resilient macros, which can be defined AND used in any delayed expansion mode.

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 :while.init
  call :while.test
  endlocal
)
exit /b

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

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

:# Define macros used internally in WHILE and REPEAT macros
%FOR!!% set REP16X_=for /l %%- in ^(0,1,%%!-%%!15^) do

set NOP=if 1==0 NOP
set BREAK=set "-=-"

:# Define WHILE, DO, WEND macros, definable and usable in any expansion mode
%FOR!% set WHILE=^(for /f "tokens=2" %%? in ^("%%!%%! 1 0"^) do for %%- in ^(1 2^) do set "-=" ^&^
 if %%-==2 ^(if %%?==1 endlocal^) else ^(if %%?==1 setlocal EnableDelayedExpansion^) ^&^
 %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% ^(if
%FOR!% set DO=^(%NOP%^) else %BREAK%^) ^& if not %%!-%%!1==-1
set WEND=^)

:# Define REPEAT, UNTIL, REND macros, definable and usable in any expansion mode
%FOR!% set REPEAT=^(for /f "tokens=2" %%? in ^("%%!%%! 1 0"^) do for %%- in ^(1 2^) do set "-=" ^&^
 if %%-==2 ^(if %%?==1 endlocal^) else ^(if %%?==1 setlocal EnableDelayedExpansion^) ^&^
 %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% %REP16X_% if not %%!-%%!1==-1
set UNTIL=^& if
set REND=%BREAK%^)

exit /b

:while.test
set "PUT=<NUL set /p"

echo :# Test line: N=0; while (not N==25) {echo N; N++}
set "N=0" & %WHILE% not "!N!"=="25" %DO% (%PUT% "=!N! " & set /a "N+=1") %WEND% & echo. & echo :# Done

echo :# Test tree: N=0; while (not N==3) {M=0; while (not M==3) {echo NM; M++}; N++}
set "N=0"
%WHILE% not "!N!"=="3" %DO% (
  set "M=0"
  %WHILE% not "!M!"=="3" %DO% (
    %PUT% "=!N!!M! "
    set /a "M+=1"
  ) %WEND%
  set /a "N+=1"
) %WEND%
echo.
echo :# Done

echo :# Test line: N=0; repeat {echo N; N++} until (N^>=25)
set "N=0" & %REPEAT% (%PUT% "=!N! " & set /a "N+=1") %UNTIL% !N! GEQ 25 %REND% & echo. & echo :# Done

echo :# Test tree: N=0; repeat {M=0; repeat {echo NM; M++} until (M==3)}; N++} until (N==3)
set "N=0"
%REPEAT% (
  set "M=0"
  %REPEAT% (
    %PUT% "=!N!!M! "
    set /a "M+=1"
  ) %UNTIL% "!M!"=="3" %REND%
  set /a "N+=1"
) %UNTIL% "!N!"=="3" %REND%
echo.
echo :# Done

echo :# Test tree: N=0; while (not N==2) {M=0; repeat {L=0; while (not L==2) {echo NML; L++}; M++} until (M==2); N++}
set "N=0"
%WHILE% not "!N!"=="2" %DO% (
  set "M=0"
  %REPEAT% (
    set "L=0"
    %WHILE% not "!L!"=="2" %DO% (
      %PUT% "=!N!!M!!L! "
      set /a "L+=1"
    ) %WEND%
    set /a "M+=1"
  ) %UNTIL% "!M!"=="2" %REND%
  set /a "N+=1"
) %WEND%
echo.
echo :# Done

exit /b
Output:

Code: Select all

Expansion is Enabled
:# Test line: N=0; while (not N==25) {echo N; N++}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
:# Done
:# Test tree: N=0; while (not N==3) {M=0; while (not M==3) {echo NM; M++}; N++}
00 01 02 10 11 12 20 21 22
:# Done
:# Test line: N=0; repeat {echo N; N++} until (N>=25)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
:# Done
:# Test tree: N=0; repeat {M=0; repeat {echo NM; M++} until (M==3)}; N++} until (N==3)
00 01 02 10 11 12 20 21 22
:# Done
:# Test tree: N=0; while (not N==2) {M=0; repeat {L=0; while (not L==2) {echo NML; L++}; M++} until (M==2); N++}
000 001 010 011 100 101 110 111
:# Done

Expansion is Disabled
:# Test line: N=0; while (not N==25) {echo N; N++}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
:# Done
:# Test tree: N=0; while (not N==3) {M=0; while (not M==3) {echo NM; M++}; N++}
00 01 02 10 11 12 20 21 22
:# Done
:# Test line: N=0; repeat {echo N; N++} until (N>=25)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
:# Done
:# Test tree: N=0; repeat {M=0; repeat {echo NM; M++} until (M==3)}; N++} until (N==3)
00 01 02 10 11 12 20 21 22
:# Done
:# Test tree: N=0; while (not N==2) {M=0; repeat {L=0; while (not L==2) {echo NML; L++}; M++} until (M==2); N++}
000 001 010 011 100 101 110 111
:# Done
Advantages:
- Can be defined and used in any delayed expansion context.
- No risk of forgetting the outside parenthesis.
- These WHILE and REPEAT macros are significantly shorter than the previous set (376 and 388 characters resp.), despite being more powerful.
Drawbacks:
- Cannot be used inside another macro.
- Still cannot be used in a piped block.
=> I'm planning to add a third %FOR!!!% macro, for easily defining macros resisting to another expansion level, but this will be the subject of another topic.

Post Reply