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

Le variabili nel file batch non vengono impostate quando sono dentro IF?

Ho due esempi di file batch molto semplici:

Assegnare un valore a una variabile:

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

Che, come previsto, risulta in:

FOO: 1 
Press any key to continue . . .

Tuttavia, se metto le stesse due righe all'interno di un blocco IF NOT DEFINED:

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

Questo inaspettatamente risulta in:

FOO: 
Press any key to continue . . .

Questo non dovrebbe avere niente a che fare con l'IF, chiaramente il blocco viene eseguito. Se definisco BAR sopra l'if, viene visualizzato solo il testo del comando PAUSE, come previsto.

Cosa succede?


Domanda successiva: C'è un modo per abilitare l'espansione ritardata senza setlocal?

Se dovessi chiamare questo semplice file batch di esempio da dentro un altro, l'esempio imposta FOO, ma solo LOCALMENTE.

Per esempio:

testcaller.bat

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

test.bat

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

Questo visualizza:

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

In questo caso, sembra che io debba abilitare l'espansione ritardata nel CALLER, che potrebbe essere una seccatura.

Risposte (6)

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

Le variabili d'ambiente nei file batch sono espanse quando viene analizzata una linea. Nel caso di blocchi delimitati da parentesi (come il tuo if defined) l'intero blocco conta come una “linea” o comando.

Questo significa che tutte le occorrenze di %FOO% sono sostituite dai loro valori prima che il blocco sia eseguito. Nel vostro caso con niente, poiché la variabile non ha ancora un valore.

Per risolvere questo problema potete abilitare l'espansione ritardata:

setlocal enabledelayedexpansion

L'espansione ritardata fa sì che le variabili delimitate da punti esclamativi (!) siano valutate all'esecuzione invece che al parsing, il che assicura il comportamento corretto nel vostro caso:

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

help set dettaglia anche questo:

Infine, è stato aggiunto il supporto per l'espansione ritardata delle variabili d'ambiente. Questo supporto è sempre disabilitato per default, ma può essere abilitato/disabilitato tramite lo switch da linea di comando /V a CMD.EXE. Vedi CMD /?

L'espansione ritardata delle variabili d'ambiente è utile per aggirare le limitazioni dell'attuale espansione che avviene quando una linea di testo viene letta, non quando viene eseguita. L'esempio seguente dimostra il problema con l'espansione immediata della variabile:

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

non visualizzerebbe mai il messaggio, poiché lo %VAR% in both IF statements viene sostituito quando viene letta la prima istruzione IF, poiché essa include logicamente il corpo dello IF, che è un'istruzione composta. Quindi l'IF all'interno dell'istruzione composta sta davvero confrontando “prima” con “dopo”, che non saranno mai uguali. Allo stesso modo, il seguente esempio non funzionerà come previsto:

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

in quanto non costruirà non una lista di file nella directory corrente, ma imposterà semplicemente la variabile LIST all'ultimo file trovato. Di nuovo, questo perché la variabile %LIST% viene espansa solo una volta quando l'istruzione FOR viene letta, e in quel momento la variabile LIST è vuota. Quindi l'attuale ciclo FOR che stiamo eseguendo è:

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

che continua a impostare LIST all'ultimo file trovato.

L'espansione ritardata delle variabili d'ambiente ti permette di usare un carattere diverso (il punto esclamativo) per espandere le variabili d'ambiente al momento dell'esecuzione. Se l'espansione ritardata delle variabili è abilitata, gli esempi precedenti potrebbero essere scritti come segue per funzionare come previsto:

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

Lo stesso comportamento si verifica anche quando i comandi sono su una singola linea (& è il separatore di comando):

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

La spiegazione di Joey è la mia preferita. Notate però che enabledelayedexpansion non funziona su Windows NT 4.0 (e non sono sicuro di Windows 2000).

Riguardo alla tua domanda successiva, no, non è possibile EnableDelayedExpansion senza setlocal. Tuttavia il comportamento originale che ti dava fastidio può essere usato per risolvere questo secondo problema: il trucco è quello di endlocal sulla stessa linea in cui imposti di nuovo i valori delle variabili di cui hai bisogno.

Ecco il tuo test.bat modificato:

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

Ma ecco un altro workaround a questo problema: usare una procedura nello stesso file invece di un blocco in linea o un file esterno.

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

:NotDefined
set FOO=1
echo FOO: %FOO%
goto :EOF
3
3
3
2009-12-03 21:02:01 +0000

Se non funziona in questo modo, probabilmente avete un'espansione ritardata della variabile d'ambiente. Potete disattivarla con cmd /V:OFF o usare i punti esclamativi all'interno del vostro 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

Questo succede perché la tua linea con il comando FOR si valuta solo una volta. Avete bisogno di un modo per rivalutarla. Potreste simulare un'espansione ritardata con il comando CALL:

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

A volte, come ho avuto nel mio caso, quando avete bisogno di essere compatibili con sistemi legacy come i vecchi server NT4 dove l'espansione ritardata non funziona o per qualche motivo non funziona, potreste aver bisogno di una soluzione alternativa.

Nel caso in cui userete Delayd Expansion, si raccomanda anche di includere almeno un controllo nel batch:

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

Nel caso tu voglia un approccio più semplice e leggibile, ecco delle soluzioni alternative:

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

che funziona così:

>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

e un'altra soluzione cmd pura:

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

che funziona così:

>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

e questa è la soluzione completa della sintassi 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%

funziona così:

>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 notare che funziona se è un singolo comando sulla stessa linea.

ad esempio

if not defined BAR set FOO=1

imposterà effettivamente FOO a 1. Si potrebbe quindi fare un altro controllo come:

if not defined BAR echo FOO: %FOO%

Ehi, non ho mai detto che fosse carino. Ma se non volete incasinarvi con setlocal o con il business dell'espansione ritardata, è un workaround veloce.