Is it safe to use a loop variable within a nested script, even without SETLOCAL

Viewed 91

Suppose a CALL appears within a FOR loop. Is it safe to use the variable that controls the loop within the second script, even if the second script does not SETLOCAL? Something like this:

FOR %%A IN (A B C) DO (
ECHO %%A
CALL Second.bat
)
REM This is Second.bat
FOR %%A IN (1 2 3) DO (
ECHO %%A
...
)

In my tests, variable A seems local to each script. I would like to know whether this behavior is granted.

3 Answers

Firstly, to answer your question:
Yes, it is safe to use the same for loop meta-variable in a called script without setlocal/endlocal, because such are not related to the environment block, which merely contains environment variables (those usually assigned by set) as well as the current working directory.

When multiple nested loops use the same meta-variable, the inner-most loop defines the returned value. As soon as the inner-most loop is finished, the loop of the next higher nesting level defines the return value, and so on. So meta-variables are never overwritten, they are local to each individual loop.


The following scripts demonstrate that there is no relation between meta-variables and the environment block, and that meta-variables do even not depend on whether they appear in the main or a called sub script:

  1. This script just contains two nested for loops (the outer one iterates once only, establishing two meta-variables %%A and %%B; the inner one iterates twice, reestablishing meta-variable %%A):

     @echo off
     setlocal
     for /F "tokens=1,2" %%A in ("outer_A outer_B") do (
         echo Outer loop: "%%A" "%%B"
         endlocal
         for %%A in (inner_1 inner_2) do (
             echo Inner loop: "%%A" "%%B"
         )
         setlocal
         echo Outer loop: "%%A" "%%B"
     )
     echo End
     endlocal
     exit /B
    
  2. This time the inner for loop is moved into a called sub-routine:

     @echo off
     setlocal
     for /F "tokens=1,2" %%A in ("outer_A outer_B") do (
         echo Outer loop: "%%A" "%%B"
         endlocal
         call :SUB
         setlocal
         echo Outer loop: "%%A" "%%B"
     )
     echo End
     endlocal
     exit /B
    
     :SUB
     for %%A in (inner_1 inner_2) do (
         echo Inner loop: "%%A" "%%B"
     )
    
  3. This time the inner for loop is moved into a called sub-script:

     @echo off
     setlocal
     for /F "tokens=1,2" %%A in ("outer_A outer_B") do (
         echo Outer loop: "%%A" "%%B"
         endlocal
         call "sub.bat"
         setlocal
         echo Outer loop: "%%A" "%%B"
     )
     echo End
     endlocal
     exit /B
    

    This is the sub-script sub.bat:

     @echo off
     for %%A in (inner_1 inner_2) do (
         echo Inner loop: "%%A" "%%B"
     )
    

Each of the above scripts return exactly the same output:

Outer loop: "outer_A" "outer_B"
Inner loop: "inner_1" "outer_B"
Inner loop: "inner_2" "outer_B"
Outer loop: "outer_A" "outer_B"
End

This proves that the for meta-variables are not related to the environment block, because they are still available after endlocal when the loop was initialised after setlocal. Furthermore, it proves that meta-variables can be accessed when they are initialised in another section of a script or even in another script.

In your example, second.bat inherits the state of the parent script's environment block. Loop variables are not stored in the environment block, they are limited to loop block scope. You can even nest them:

@setlocal EnableExtensions

@for /L %%A in (1,1,3) do @(
    @echo OuterA: %%A
    @for /L %%A in (1,1,3) do @echo %%InnerA: %%A
)

Outputs:

> test
OuterA: 1
%InnerA: 1
%InnerA: 2
%InnerA: 3
OuterA: 2
%InnerA: 1
%InnerA: 2
%InnerA: 3
OuterA: 3
%InnerA: 1
%InnerA: 2
%InnerA: 3

While it's okay to use the same loop variable in sequential loops, it should be avoided in nested loops, as it can cause some confusion for maintainers/reviewers. In your case, it's perfectly safe and acceptable to use the same control variable names in your loops.

But it's still safe to use the same name in a nested or called sub FOR loop.

The innermost variable is always the one which will be used.
FOR-Loop variables can't be changed and they don't modify the outer variables.

But there exits a glitch, each FOR-loop see ALL other currently active FOR-variables.

In your example, if you change your second.bat to

REM This is Second.bat
FOR %%X IN (1 2 3) DO (
  ECHO %%X
  echo %%A will also be shown
)

You will get also the values from the caller script.

This can be a bit annoying in a deep nested or library function.

Like

:myFunc
for /F %%x in ("myfile.txt") DO (
  set "line=%%x"
  call set new=%%line:~0,2%%
)

That block works until it's called from a construct like this

FOR %%l in (DUMMY-) do call :myFunc

Because the part call set new=%%line:~0,2%% has now a different meaning.
It's parsed to call set new=DUMMY-ine:~0,2%%

Related