Variables in batch file not being set when inside IF?

I have two examples of very simple batch files:

Assigning a value to a variable:

@echo off
set FOO=1
echo FOO: %FOO%
pause
echo on

Which, as expected, results in:

FOO: 1 
Press any key to continue . . .

However, if I place the same two lines inside an IF NOT DEFINED block:

@echo off
IF NOT DEFINED BAR (
    set FOO=1
    echo FOO: %FOO%
)
pause
echo on

This unexpectedly results in:

FOO: 
Press any key to continue . . .

This shouldn't have anything to do with the IF, clearly the block is being executed. If I define BAR above the if, only the text from the PAUSE command is displayed, as expected.

What gives?


Follow up question: Is there any way to enable delayed expansion without setlocal?

If I were to call this simple example batch file from inside another, the example sets FOO, but only LOCALLY.

For example:

testcaller.bat

@call test.bat 
@echo FOO: %FOO% 
@pause 

test.bat

@setlocal EnableDelayedExpansion 
@IF NOT DEFINED BAR ( 
    @set FOO=1 
    @echo FOO: !FOO! 
) 

This displays:

FOO: 1 
FOO: 
Press any key to continue . . . 

In this case, it appears that I have to enable delayed expansion in the CALLER, which may be a hassle.


Environment variables in batch files are expanded when a line is parsed. In the case of blocks delimited by parentheses (as your if defined) the whole block counts as a "line" or command.

This means that all occurrences of %FOO% are replaces by their values before the block is run. In your case with nothing, as the variable doesn't have a value yet.

To solve this you can enable delayed expansion:

setlocal enabledelayedexpansion

Delayed expansion causes variables delimited by exclamation marks (!) to be evaluated on execution instead of parsing which will ensure the correct behavior in your case:

if not defined BAR (
    set FOO=1
    echo Foo: !FOO!
)

help set details this too:

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%

The same behavior also happens when the commands are on a single line (& is the command separator):

if not defined BAR set FOO=1& echo FOO: %FOO%

Joey's explanation is my favorite. Note however that enabledelayedexpansion does not work on Windows NT 4.0 (and I'm not sure about Windows 2000).

About your follow-up question, no, it is not possible to EnableDelayedExpansion without setlocal. However the original behavior that was going against you can be used to workaroud that second problem: the trick is to endlocal on the same line where you set again the values of the variables you need.

Here is your test.bat modified:

@echo off
setlocal EnableDelayedExpansion 
IF NOT DEFINED BAR ( 
    set FOO=1 
    echo FOO: !FOO! 
)
endlocal & set FOO=%FOO%

But here is another workaround to that problem: use a procedure in the same file instead of an inline block or an external file.

@echo off
if not defined BAR call :NotDefined
pause
goto :EOF

:NotDefined
set FOO=1
echo FOO: %FOO%
goto :EOF

If it isn't working that way, you likely have delayed environment variable expansion on. You can either turn it off with cmd /V:OFF or use exclamation marks inside your if:

@echo off
IF NOT DEFINED BAR (
    set FOO=1
    echo FOO: !FOO!
)
pause
echo on