89

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.

6 Answers 6

108

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

This means that all occurrences of %FOO% are replaced 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%
1
  • 20
    And if you need to echo an !, use ^^^! (escape it twice). Otherwise the "delayed expansion" feature will eat it. Dec 4, 2009 at 13:32
5

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
1
  • "the trick is to endlocal on the same line" - THANKS for that!
    – dforce
    Apr 29, 2019 at 11:54
3

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
1
  • 3
    Enabling delayed expansion only alters the semantics of the exclamation mark. It doesn't have any effect whatsoever on normal variable expansion. Besides, accidentally enabling it is very unlikely; people who have it enabled usually do so on purpose.
    – Joey
    Dec 3, 2009 at 21:38
1

This happens because your line with FOR command evaluates only once. You need some way to reevaluate it. You could simulate a delayed expansion with CALL command:

for /l %%I in (0,1,5) do call echo %%RANDOM%%
1
  • The delayed expansion didn't work, but this /do call/ worked on win7, for my 3 line cmd script to find the real hardlink: @for /f "delims=" %%a in ('bash ~/perl/cwd2.sh %*') do call set CWD_REAL=%%a echo cd /d %CWD_REAL% cd /d %CWD_REAL%
    – mosh
    Oct 5, 2016 at 4:40
0

It's worth noting that it DOES work if it's a single command on the same line.

e.g.

   if not defined BAR set FOO=1

will actually set FOO to 1. You could then do another check such as:

   if not defined BAR echo FOO: %FOO%

Hey, I never said it was pretty. But if you don't want to mess with setlocal or the delayed expansion business, it's a quick workaround.

0

Sometimes, as I had in my case, when you need to be compatible with legacy systems like old NT4 servers where delayed expansion are not working or for some reason is not working you may need alternate solution.

In case you will use Delayd Expansion, also recommended that at least check in batch should be included:

IF NOT %Comspec%==!Comspec! ECHO Delayed Expansion is NOT enabled. 

In case you just want more readable and simple approach, here are alternate solutions:

REM Option 2: use `call` inside block statement
IF NOT DEFINED BAR2 ( 
  set FOO2=2 
  call echo FOO2: %%FOO2%%
)
echo FOO2: %FOO2%

which works like this:

>REM Option 2: use `call` inside block statement

>IF NOT DEFINED BAR2 (
 set FOO2=2
 call echo FOO2: %FOO2%
)
FOO2: 2

>echo FOO2: 2
FOO2: 2

and one more pure cmd solution:

REM Option 3: use `goto` logic blocks
IF DEFINED BAR3 goto endbar3
  set FOO3=3 
  echo FOO3: %FOO3%
:endbar3
echo FOO3: %FOO3%

it works like this:

>REM Option 3: use `goto` logic blocks

>IF DEFINED BAR3 goto endbar3

>set FOO3=3

>echo FOO3: 3
FOO3: 3

>echo FOO3: 3
FOO3: 3

and this is full IF THEN ELSE syntax solution:

REM Full IF THEN ELSE solution
IF NOT DEFINED BAR4 goto elsebar4
REM this is TRUE condition, normal batch flow
  set FOO4=6
  echo FOO4: %FOO4%
  goto endbar4
:elsebar4
  set FOO4=4
  echo FOO4: %FOO4%
  set BAR4=4
:endbar4
echo FOO4: %FOO4%
echo BAR4: %BAR4%

it works like this:

>REM Full IF THEN ELSE solution

>IF NOT DEFINED BAR4 goto elsebar4

>set FOO4=4

>echo FOO4: 4
FOO4: 4

>set BAR4=4

>echo FOO4: 4
FOO4: 4

>echo BAR4: 4
BAR4: 4

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .