You are not logged in.

#1 14 Nov 2020 11:48

Rekrul
Member
Registered: 17 Apr 2016
Posts: 98

Can't use EndLocal & Set inside a loop?

This works;

@echo off
set counter=0
setlocal enabledelayedexpansion
for %%f in (*.txt) do (
set filename=%%f
echo !filename!
set /a counter=counter+1
)
endlocal & set counter=%counter%
echo %counter%

This does not;

@echo off
set counter=0
for %%f in (*.txt) do (
set filename=%%f
setlocal enabledelayedexpansion
echo !filename!
set /a counter=counter+1
endlocal & set counter=%counter%
)
echo %counter%

If the "endlocal & set counter=%counter%" is inside the loop, the Set command is ignored and the value of %counter% is cleared.

The first one works, but will remove any "!" in the filenames, which creates a problem if I want to include any rename operations in the loop. The second will preserve the filenames exactly as they are, but the counter is useless since it will get cleared at the end of each loop.

I know this probably has something to do with the idiotic delayed expansion (which serves no valid purpose and only exists to make life difficult), but how do you get around this?

How do you enable delayed expansion after a loop has started (to preserve any filenames that contain "!"), end it before the loop repeats, and preserve any variables that are changed? I mean other than writing them to a file before the loop ends and then reading them from the file at the start of the next loop?

Is there a way to alter my second example to preserve the value of %counter% each time it loops while still keeping the SetLocal and EndLocal commands where they are?

Offline

#2 14 Nov 2020 18:29

OJBakker
Member
Registered: 05 Aug 2011
Posts: 6

Re: Can't use EndLocal & Set inside a loop?

You do not want to preserve the value of %counter% within the for-loop, but the up-to-date value in !counter!
Unfortunately this format can not be used after the endlocal command.
You can preserve the value before the endlocal and use it after the endlocal by using an additional for.

for %%G in (!counter!) do endlocal&set counter=%%G

Offline

#3 15 Nov 2020 01:03

Rekrul
Member
Registered: 17 Apr 2016
Posts: 98

Re: Can't use EndLocal & Set inside a loop?

OJBakker wrote:

You do not want to preserve the value of %counter% within the for-loop, but the up-to-date value in !counter!
Unfortunately this format can not be used after the endlocal command.
You can preserve the value before the endlocal and use it after the endlocal by using an additional for.

for %%G in (!counter!) do endlocal&set counter=%%G

Thank you. to be honest, I have no idea why this works and the other example doesn't, but I'll make a note of this.

Offline

#4 15 Nov 2020 09:24

Shadow Thief
Member
Registered: 12 Jul 2012
Posts: 205

Re: Can't use EndLocal & Set inside a loop?

Why are you using endlocal at all? Just move setlocal to the second line and keep it enabled for the duration of the entire script. endlocal will run implicitly when the script ends, just like how you don't need to give the script an exit /b to end it.

Offline

#5 16 Nov 2020 10:34

T3RRY
Member
Registered: 15 Oct 2020
Posts: 16

Re: Can't use EndLocal & Set inside a loop?

Rekrul wrote:

This works;


How do you enable delayed expansion after a loop has started (to preserve any filenames that contain "!"), end it before the loop repeats, and preserve any variables that are changed? I mean other than writing them to a file before the loop ends and then reading them from the file at the start of the next loop?

If filenames containing ! are a concern, you either Enable Delayed expansion within the loop after the value is assigned to the variable to expand it, then endlocal once the value is used prior to the next iteration of the loop, Use the value directly via the For metavariable without assignining it to a temp var, Call a function outside of the loop and increment the count in the function, or alternately use call with set as follows:

@Echo On & CD "%~dp0"
Setlocal
For /F "Tokens=1,2 Delims==" %%v in ('Set "Filename{"')Do Set "%%v="
For %%G in (*.txt)Do (
 Set /A i+=1
 Call Set "Filename{%%i%%}=%%~fG" 
)
Set Filename{

Offline

#6 16 Nov 2020 11:59

bluesxman
Member
From: UK
Registered: 29 Dec 2006
Posts: 1,129

Re: Can't use EndLocal & Set inside a loop?

You could try this:

@echo off
set counter=0
for %%f in (*.txt) do (
set filename=%%f
setlocal enabledelayedexpansion
echo !filename!
call :increment_counter
)
echo %counter%
goto :EOF

:increment_counter
set /a counter+=1
endlocal && set counter=%counter%
goto :EOF

Or perhaps this as a simpler option:

@echo off
set counter=0

for %%f in (*.txt) do (
set filename=%%f
setlocal enabledelayedexpansion
echo !filename!
endlocal
set /a counter+=1
)
echo %counter%

** Both Untested **

Last edited by bluesxman (16 Nov 2020 12:03)


cmd | *sh | ruby | chef

Offline

#7 21 Nov 2020 10:23

Rekrul
Member
Registered: 17 Apr 2016
Posts: 98

Re: Can't use EndLocal & Set inside a loop?

Shadow Thief wrote:

Why are you using endlocal at all? Just move setlocal to the second line and keep it enabled for the duration of the entire script.

Because doing that will leave delayed expansion enabled on all but the first repetition of the loop, mangling any filenames that contain !. Delayed expansion has to be enabled after the value in the token has been safely transferred to a variable, but then it has to be turned off before the loop repeats and fetches the next filename.

Shadow Thief wrote:

endlocal will run implicitly when the script ends, just like how you don't need to give the script an exit /b to end it.

I'm only using endlocal as a way to disable delayed expansion. Frankly, I'd rather not have to use setlocal at all, but you can't enable delayed expansion without it. And while there IS a command to disable delayed expansion, it also requires a setlocal command, which not only carries all the same problems as using the endlocal command, it will cause an error if the loop repeats too many times because it keeps opening new local "bubbles" without closing any of them. There should have been a way to toggle delayed expansion on/off without having to resort to using the local commands.

T3RRY wrote:

If filenames containing ! are a concern, you either Enable Delayed expansion within the loop after the value is assigned to the variable to expand it, then endlocal once the value is used prior to the next iteration of the loop, Use the value directly via the For metavariable without assignining it to a temp var, Call a function outside of the loop and increment the count in the function, or alternately use call with set as follows:

@Echo On & CD "%~dp0"
Setlocal
For /F "Tokens=1,2 Delims==" %%v in ('Set "Filename{"')Do Set "%%v="
For %%G in (*.txt)Do (
 Set /A i+=1
 Call Set "Filename{%%i%%}=%%~fG" 
)
Set Filename{

Thanks, but I understand none of that. I've tried to use Call Set before and never managed to figure out the magical formula to making it work. No matter what I do, it never works inside a loop to expand variables.

I've been quite vocal in my opinion that rather than this ass-backwards "solution" that requires users to jump through all kinds of hoops, they should have simply included an option to just toggle on delayed expansion. No special characters, no tricks, you just enable it and variables work like common sense says they should. Problem solved. Instead, the Microsoft programmers came up with the most convoluted "solution" imaginable, which goes against all logic and command sense.

Offline

#8 22 Nov 2020 11:45

T3RRY
Member
Registered: 15 Oct 2020
Posts: 16

Re: Can't use EndLocal & Set inside a loop?

Rekrul wrote:
T3RRY wrote:

If filenames containing ! are a concern, you either Enable Delayed expansion within the loop after the value is assigned to the variable to expand it, then endlocal once the value is used prior to the next iteration of the loop, Use the value directly via the For metavariable without assignining it to a temp var, Call a function outside of the loop and increment the count in the function, or alternately use call with set as follows:

@Echo On & CD "%~dp0"
Setlocal
For /F "Tokens=1,2 Delims==" %%v in ('Set "Filename{"')Do Set "%%v="
For %%G in (*.txt)Do (
 Set /A i+=1
 Call Set "Filename{%%i%%}=%%~fG" 
)
Set Filename{

Thanks, but I understand none of that. I've tried to use Call Set before and never managed to figure out the magical formula to making it work. No matter what I do, it never works inside a loop to expand variables.

I've been quite vocal in my opinion that rather than this ass-backwards "solution" that requires users to jump through all kinds of hoops, they should have simply included an option to just toggle on delayed expansion. No special characters, no tricks, you just enable it and variables work like common sense says they should. Problem solved. Instead, the Microsoft programmers came up with the most convoluted "solution" imaginable, which goes against all logic and command sense.

To output a variable using Call, the following syntax is typically used when an indexed variable is assigned:
Call Echo %%PrimaryVarname[%IndexVarname%]%%

CMD syntax was never designed to be a general use scripting language - It's failings as such are well known and widely documented, and it's acknowledged that alot of it's charactistics are quite illogical - however as a scripting language thats been expanded upon over the course of generations with one hand tied behind their backs to ensure backwards compatability wherever possible, expectations shouldn't be set that high. All said though, there are rules to how cmd parses scripts, and some of those rules do involve logic.

I wouldn't advocate the use of the method below, as using Call to manipulate expansion of variables is not only MUCH slower, it can also have unintended consequences when dealing with poison characters and carets.

There is a mathematical basis for expansion which governs how many times Call must be used to reset the parser to expand a variable to a given nested depth.
The basis of the principle is that with the use of Call, A variable can be expanded at increasing levels of depth, until the variable is found to be empty. The limitation is in the line length limit, as the number of % expansions that need to be used to expand the variable increases proportionally to the number of Calls used to Parse over the variable for each level of expansion to be achieved.

The following rules apply to expansion depth using Call:

E (% Expansion) begins with a value of 1
The value of C (Calls required to parse the Variable) commences at 0, increments by 1 for the 2nd expansion, Then doubles for Each subsequent Expansion
The value of E increases in proportion to C like so:

E = ( C * 4 ) -1
The following Table expresses the values required to parse the line in terms of number of Calls and % expansions to be appended to the string.

Parsing Rules

A short example script:

@echo off

Set Zero=One
Set One=Two
Set Two=Three
set Three=Four
set Four=Five
set Five=Six
set Six=Seven
set Seven=Eight
set Eight=Nine
Set Nine=Ten
Set Ten=Eleven
Set Eleven=Twelve
Set Twelve=Thirteen. Last variable. Cannot Be reached from depth 0

Goto :main

REM Subroutine to Build Expansion String based on depth:

:extract <Starting Variable> <Call Depth Range 1 - 11> <Variable to Assign>
    Setlocal EnableDelayedExpansion
    Set "Assign=%~1"
    Set EXPcount=1
    Set "CALLcount=%~2"
    IF Not "!CALLcount!"=="0" Set /A EXPcount=( !CALLcount! * 4 ) - 1
    For /L %%A in (1,1,!EXPcount!) do (Set "Assign=%%!Assign!%%")

    Setlocal DisableDelayedExpansion
    Set "Assign=Set Assign=!Assign!"
    (
    Endlocal
        Set "Assign=%Assign%"
    )

    For /L %%B in (0,1,!CALLcount!) do (Set "Assign=CALL !Assign!")
    !Assign! || (Echo String exceeds line length, No Modification. & Exit /B)
    IF "!Assign!"==" " (Echo Variable Not Defined, No Modification. & Exit /B)
    (
    Endlocal
        Set "%~3=%Assign%"
        Exit /B
    )


REM Expand variable using relationship between Number of Calls and % Expansion Required to Parse variable
:main
Setlocal EnableDelayedExpansion

REM Define variable 'Depth Shortcut' to target specific depths
For %%A in (0 1 2 4 8 16 32 64 128 256 512 1024) Do (
    Set /A #+=1
    Set Depth[!#!]=%%A
)

REM iterate over all depths
For /L %%E in (1,1,!#!) do (
    Call :Extract Zero !Depth[%%E]! New
    Echo(Start: Zero Depth: %%E, Extracted Value: !New!
)
ECHO.
REM extract from selected depth levels
For %%E in (5 8 11) do (
    Call :Extract Zero !Depth[%%E]! New
    Echo(Start: Zero Depth: %%E, Extracted Value: !New!
)
ECHO.
REM extract from selected depth levels, From a different starting Variable
For %%E in (5 6 7 8 9) do (
    Call :Extract five !Depth[%%E]! New
    Echo(Start: Five Depth: %%E, Extracted Value: !New!
)
Endlocal
Echo(Example Complete
Pause >nul

From the 12th expansion onwards, the number of Calls and % expansions appended to the string exceed the line length limit, resulting in failure to build a string of the length needed to expand at this point.

Use of the double pipe after the Call string can test for when this failure occurs, allowing intervention:

!Assign! || (Echo String exceeds line length, No Modification. & Exit /B)

To offset the string length limitation limitation, the variable used as the starting point can be changed in combination with the depth level to target.

Offline

Board footer

Powered by