Tuesday, February 13, 2024

Yet another reason I learned to hate, but not fear, Windows Batch Scripting

 Consider the following batch script, and save it as "test.bat":

@ECHO OFF
SETLOCAL
:step-1
SET "TEST=0"
Echo TEST before: %TEST%
:step-2
FOR /F "tokens=* USEBACKQ" %%F IN (`ECHO 1`) DO (
  REM step-3
  SET "TEST=%%F"
  ECHO F in loop: %%F
  ECHO TEST in loop: %TEST%
)
:step-4
ECHO TEST after: %TEST%


High-level explanation of each labeled step above,


1. The script sets variable TEST to the value 0.

2. A FOR loop iterates over the value(s) output from the statement "ECHO 1", which predictably yields the single value, "1". We define the placeholder %%F, a special variable to hold each value as we iterate the loop.

3. Inside our FOR loop, we do the following:
   a. Assign the current value of %%F to TEST
   b. Output the current value of %%F
   c. Output the current value of TEST

4. After our FOR loop completes, output the current value of TEST.



Pop-quiz:
What do you expect the output of this batch script to be?


The actual output:

C:\example> test.bat
TEST before: 0
F in loop: 1
TEST in loop: 0
TEST after: 1

C:\example>

But wait, didn't we assign TEST=1 before we output it's value within the loop? What could possibly be the reason our output within the loop is "0", especially when the output of %%F is indeed "1"?

Windows batch scripts evaluate the value of variables at parsing time, not at runtime. And Windows batch scripts evaluate everything within parentheses blocks at the time the script is called, not in the linear order of execution as you get in regular execution. This is why the first output of TEST returns the value of TEST at the time the paren block was parsed, not the value of TEST if you follow the order of operations. Behind the scenes TEST does get the value of "1" assigned, but the evaluation of %TEST% within the parens block already happened before we entered the FOR loop, so our output statement spits out "0". Only after we exit the FOR loop are we able to evaluate %TEST% and get the actual current value of TEST, "1".

There are a few workarounds for this:

1. The most obvious solution is, do not try to evaluate variables that have been SET within any parens block; you won't get the values you have SET until you exit the block.

2. Check out "delayed expansion", a not-widely-known feature of Windows batch scripting. Delayed expansion will hold off on evaluating your variable expansion until the more predictable time of line execution by wrapping your variable name with bangs instead of percents, e.g. "!TEST!" instead of "%TEST%". A slightly modified version of the script that will produce the output you would expect could look like this:

@ECHO OFF
SETLOCAL enabledelayedexpansion
:step-1
SET "TEST=0"
Echo TEST before: %TEST%
:step-2
FOR /F "tokens=* USEBACKQ" %%F IN (`ECHO 1`) DO (
  REM step-3
  SET "TEST=%%F"
  ECHO F in loop: %%F
  ECHO TEST in loop: !TEST!
)
:step-4
ECHO TEST after: %TEST%

For more information about delayed expansion, check out: EnableDelayedExpansion - Windows CMD - SS64.com

No comments:

Post a Comment