2009-12-03 20:51:48 +0000 2009-12-03 20:51:48 +0000
72
72

¿Las variables en el archivo por lotes no se establecen cuando están dentro de IF?

Tengo dos ejemplos de archivos batch muy sencillos:

Asignando un valor a una variable:

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

Que, como es de esperar, da como resultado:

FOO: 1 
Press any key to continue . . .

Sin embargo, si coloco las mismas dos líneas dentro de un bloque IF NOT DEFINED:

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

Esto inesperadamente resulta en:

FOO: 
Press any key to continue . . .

Esto no debería_ tener nada que ver con el IF, claramente el bloque se está ejecutando. Si defino BAR por encima del if, sólo se muestra el texto del comando PAUSE, como es de esperar.

¿Qué pasa?

  • *

Pregunta de seguimiento: ¿Hay alguna forma de activar la expansión retardada sin setlocal?

Si llamara a este sencillo archivo batch de ejemplo desde dentro de otro, el ejemplo establece FOO, pero sólo LOCALMENTE.

Por ejemplo:

testcaller.bat

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

test.bat

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

Esto muestra:

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

En este caso, parece que tengo que habilitar la expansión retardada en el CALLER, lo que puede ser una molestia.

Respuestas (6)

86
86
86
2009-12-03 21:35:26 +0000

Las variables de entorno en los archivos por lotes se expanden cuando se analiza una línea. En el caso de bloques delimitados por paréntesis (como su if defined) el bloque entero cuenta como una “línea” o comando.

Esto significa que todas las apariciones de %FOO% se sustituyen por sus valores antes de que se ejecute el bloque. En tu caso con nada, ya que la variable no tiene valor todavía.

Para solucionar esto puede activar la expansión retardada:

setlocal enabledelayedexpansion

La expansión retardada hace que las variables delimitadas por signos de exclamación (!) se evalúen en la ejecución en lugar de analizarse, lo que asegurará el comportamiento correcto en su caso:

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

help set detalla esto también:

Finalmente, se ha añadido soporte para la expansión retardada de variables de entorno. Este soporte está siempre desactivado por defecto, pero puede ser activado/desactivado a través del interruptor de línea de comandos /V a CMD.EXE. Ver CMD /?

La expansión retardada de variables de entorno es útil para sortear las limitaciones de la expansión actual, que ocurre cuando se lee una línea de texto, no cuando se ejecuta. El siguiente ejemplo demuestra el problema con la expansión inmediata de variables:

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

nunca mostraría el mensaje, ya que el %VAR% en ambas sentencias IF se sustituye cuando se lee la primera sentencia IF, ya que lógicamente incluye el cuerpo del IF, que es una sentencia compuesta. Así que el IF dentro de la sentencia compuesta está realmente comparando el “antes” con el “después”, que nunca serán iguales. Del mismo modo, el siguiente ejemplo no funcionará como se espera:

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

en el sentido de que no construirá una lista de archivos en el directorio actual, sino que simplemente establecerá la variable LIST al último archivo encontrado. De nuevo, esto se debe a que el %LIST% se expande sólo una vez cuando se lee la sentencia FOR, y en ese momento la variable LIST está vacía. Así que el bucle FOR que estamos ejecutando es:

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

que sigue poniendo LIST en el último fichero encontrado.

La expansión retardada de variables de entorno permite utilizar un carácter diferente (el signo de exclamación) para expandir las variables de entorno en tiempo de ejecución. Si la expansión retardada de variables está habilitada, los ejemplos anteriores podrían escribirse de la siguiente manera para que funcionen como se pretende:

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%
4
4
4
2011-03-26 16:26:14 +0000
3
3
3
2009-12-03 21:02:01 +0000

Si no funciona así, es probable que tengas activada la expansión de la variable de entorno retrasada. Puede desactivarla con cmd /V:OFF o utilizar signos de exclamación dentro de su if:

@echo off
IF NOT DEFINED BAR (
    set FOO=1
    echo FOO: !FOO!
)
pause
echo on
1
1
1
2013-11-27 11:52:14 +0000

Esto sucede porque su línea con el comando FOR se evalúa sólo una vez. Necesitas alguna forma de reevaluarla. Podrías simular una expansión retardada con el comando CALL:

for /l %%I in (0,1,5) do call echo %%RANDOM%%
0
0
0
2016-12-28 21:46:34 +0000

A veces, como en mi caso, cuando se necesita ser compatible con sistemas heredados como los viejos servidores NT4 donde la expansión retardada no funciona o por alguna razón no funciona puede necesitar una solución alternativa.

En el caso de que vaya a utilizar la expansión retardada, también se recomienda que al menos se incluya la comprobación en el lote:

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

En caso de que sólo quiera un enfoque más legible y simple, aquí hay soluciones alternativas:

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

que funciona así

>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

y una solución más pura de cmd:

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

que funciona así:

>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

y esta es la solución completa de sintaxis IF THEN ELSE:

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%

funciona así:

>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
0
0
0
2012-06-26 15:36:32 +0000

Vale la pena señalar que SÍ funciona si se trata de un solo comando en la misma línea.

por ejemplo,

if not defined BAR set FOO=1

en realidad pondrá FOO a 1. Podrías entonces hacer otra comprobación como:

if not defined BAR echo FOO: %FOO%

Oye, nunca dije que fuera bonito. Pero si no quieres meterte con setlocal o el asunto de la expansión retardada, es una solución rápida.