Windows 7 batch file: Why are these IF blocks keeping me from assigning variables to a value?

Here's the broken batch file:

@echo off
if prod==prod (
    if xps==xps (
        set i1=prodxpsi1
        set i2=prodxpsi2
        set e1=prodxpse1
        set e2=prodxpse2
    ) else (
        set i1=prodzpsi1
        set i2=prodzpsi2
        set e1=prodzpse1
        set e2=prodzpse2
    )

    if 1==1 (
        echo %i1%, %i2%, %e1%, %e2%
    ) else (
        echo %i1%, %i2%, %e1%, %e2%
    )
)

pause

However, when I take out the outer if prod==prod block like this, it works:

@echo off
if xps==xps (
    set i1=prodxpsi1
    set i2=prodxpsi2
    set e1=prodxpse1
    set e2=prodxpse2
) else (
    set i1=prodzpsi1
    set i2=prodzpsi2
    set e1=prodzpse1
    set e2=prodzpse2
)

if 1==1 (
    echo %i1%, %i2%, %e1%, %e2%
) else (
    echo %i1%, %i2%, %e1%, %e2%
)

pause

When I run the batch file the first time, it echoes , , ,. When I run it the second time, it works fine:

lolwut


Solution 1:

This is a quirk of the command parser. Because of the brackets, it sees everything from if ... to ) as one line. When it reads this "one line", it expands all the variables to their values before processing any of it. The set commands all occcur after the variables have been expanded.

There are two solutions: branches and delayed exansion.

Branches: Ensure the set commands and echo commands are not in the same set of top-most brackets:

@echo off
if not prod==prod goto :end
if xps==xps (
    set ...
) else (
    set ...
)
if 1==1 (
    ...
)
:end
pause

Delayed Expansion: This causes variables to be expanded as needed, rather than in advance. Use the command SetLocal EnableDelayedExpansion to activate this mode, use ! marks to refer to a variable in this manner, and use the command EndLocal when you're done. Note that EndLocal will forget any variables declared after SetLocal, so you may want to move SetLocal to after the set commands.

@echo off
setlocal enabledelayedexpansion
if prod==prod (
    if xps==xps (
        set i1=prodxpsi1
        ...
    ) else (
        set i1=prodzpsi1
        ...
    )
    if 1==1 (
        echo !i1!, !i2!, !e1!, !e2!
    ) else (
        echo !i1!, !i2!, !e1!, !e2!
    )
)
endlocal
pause

Solution 2:

If you enable delayed expansion, then use !var! syntax to reference the variables, then it behaves as you expect.

@echo off

setlocal enabledelayedexpansion

if prod==prod (
    if xps==xps (
        set i1=prodxpsi1
        set i2=prodxpsi2
        set e1=prodxpse1
        set e2=prodxpse2
    ) else (
        set i1=prodzpsi1
        set i2=prodzpsi2
        set e1=prodzpse1
        set e2=prodzpse2
    )

    if 1==1 (
        echo !i1!, !i2!, !e1!, !e2!
    ) else (
        echo !i1!, !i2!, !e1!, !e2!
    )
)

The help text (cmd /?) explains:

/V:ON -- Enable delayed environment variable expansion using ! as the delimiter. For example, /V:ON would allow !var! to expand the variable var at execution time. The var syntax expands variables at input time, which is quite a different thing when inside of a FOR loop.

If delayed environment variable expansion is enabled, then the exclamation character can be used to substitute the value of an environment variable at execution time.

When you wrap the whole thing in an if statement, the whole block becomes essentially a single command. All the variables inside the command are expanded at the time of input, which is before the if xps==xps part begins. At that point in the script, the variables have not yet been defined.

By using the !var! syntax along with delayed expansion, then the value of !i1! isn't evaluated until that particular line is executed.

Thanks! Your question taught me something today!