views:

1273

answers:

3

I'm writing a simple .bat file and I've run into some weird behavior. There are a couple places where I have to do a simple if/else, but the code inside the blocks don't seem to be working correctly.

Here's a simple case that demonstrates the error:

@echo off

set MODE=FOOBAR

if "%~1"=="" (
  set MODE=all
  echo mode: %MODE%
) else (
  set MODE=%~1
  echo mode: %MODE%
)
echo mode: %MODE%

The output I'm getting is:

C:\>test.bat test
mode: FOOBAR
mode: test

Why is the echo inside the code block not getting the new value of the variable? In the actual code I'm writing I need to build a few variables and reference them within the scope of the if/else. I could switch this to use labels and gotos instead of an if/else, but that doesn't seem nearly as clean.

What causes this behavior? Is there some kind of limit on variables within code blocks?

+8  A: 

You are running into the problem of cmd's static variable expansion. The MODE variable is only evaluated once. You can see this if you omit the @echo off line.

From the set /? documentation:

Finally, support for delayed environment variable expansion has been added. This support is always disabled by default, but may be enabled/disabled via the /V command line switch to CMD.EXE. See CMD /?

Delayed environment variable expansion is useful for getting around the limitations of the current expansion which happens when a line of text is read, not when it is executed. The following example demonstrates the problem with immediate variable expansion:

 set VAR=before
 if "%VAR%" == "before" (
     set VAR=after
     if "%VAR%" == "after" @echo If you see this, it worked
 )

would never display the message, since the %VAR% in BOTH IF statements is substituted when the first IF statement is read, since it logically includes the body of the IF, which is a compound statement. So the IF inside the compound statement is really comparing "before" with "after" which will never be equal. Similarly, the following example will not work as expected:

set LIST=
for %i in (*) do set LIST=%LIST% %i
echo %LIST%

in that it will NOT build up a list of files in the current directory, but instead will just set the LIST variable to the last file found. Again, this is because the %LIST% is expanded just once when the FOR statement is read, and at that time the LIST variable is empty. So the actual FOR loop we are executing is:

for %i in (*) do set LIST= %i

which just keeps setting LIST to the last file found.

Delayed environment variable expansion allows you to use a different character (the exclamation mark) to expand environment variables at execution time. If delayed variable expansion is enabled, the above examples could be written as follows to work as intended:

set VAR=before
if "%VAR%" == "before" (
    set VAR=after
    if "!VAR!" == "after" @echo If you see this, it worked
)

set LIST=
for %i in (*) do set LIST=!LIST! %i
echo %LIST%
Is there a way to set that flag programatically at the top of a bat file? Most of the time this will be run either by another bat file, and the environment it's run in isn't fixed (might be double-clicked, might be run from a cygwin shell, might be from cmd, etc).
Herms
@Herms, "setlocal enabledelayedexpansion" at the start, "endlocal" at the end.
paxdiablo
Thanks. This saved my butt paxdiablo.
Chris Benard
A: 

Looks like the read and write use different scoping rules.

If you eliminate this line

set MODE=FOOBAR

it will work as expected. So you'll probably need to have a complex series if if/elses to get the variables populated as you'd like.

Harry Lime
Actually no, it doesn't. That set at the top wasn't originally there. If I omit that then the first time I run the bat file the first echo is empty. The second time I run it (from the same cmd instance) the first echo shows the last used value.
Herms
You're right, I'd run the script a second time.. well spotted!
Harry Lime
+2  A: 

setlocal EnableDelayedExpansion

will enable the /v flag