Haz que una variable de entorno sobreviva ENDLOCAL

Tengo un archivo por lotes que calcula una variable a través de una serie de variables intermedias:

@echo off setlocal set base=compute directory set pkg=compute sub-directory set scripts=%base%\%pkg%\Scripts endlocal %scripts%\activate.bat 

No se llama al script en la última línea, porque viene después de endlocal, lo que anula la variable de entorno de scripts , pero tiene que venir después de endlocal porque su propósito es establecer un grupo de otras variables de entorno para que las use el usuario.

¿Cómo llamo a un script cuyo propósito es establecer variables de entorno permanentes, pero quién está determinado por una variable de entorno temporal?

Sé que puedo crear un archivo de lotes temporal antes de endlocal y llamarlo después de endlocal, lo cual haré si nada más sale a la luz, pero me gustaría saber si hay una solución menos arriesgada.

    El ENDLOCAL & SET VAR=%TEMPVAR% es clásico. Pero hay situaciones en las que no es ideal.

    Si no conoce el contenido de TEMPVAR, puede tener problemas si el valor contiene caracteres especiales como < > & | . En general, puede protegerse contra eso mediante el uso de comillas como SET "VAR=%TEMPVAR%" , pero eso puede causar problemas si hay caracteres especiales y el valor ya está citado.

    Una expresión FOR es una excelente opción para transportar un valor a través de la barrera ENDLOCAL si le preocupan los caracteres especiales. La expansión retrasada debería estar habilitada antes de la ENDLOCAL y deshabilitada después de la ENDLOCAL.

     setlocal enableDelayedExpansion set "TEMPVAR=This & "that ^& the other thing" for /f "delims=" %%A in (""!TEMPVAR!"") do endlocal & set "VAR=%%~A" 

    Limitaciones

    • Si la expansión retardada se habilita después de la ENDLOCAL, entonces el valor final se dañará si contiene TEMPVAR ! .

    • los valores que contienen un carácter lineFeed no se pueden transportar

    Si debe devolver valores múltiples, y sabe de un personaje que no puede aparecer en ninguno de los valores, simplemente use las opciones FOR / F correspondientes. Por ejemplo, si sé que los valores no pueden contener | :

     setlocal enableDelayedExpansion set "temp1=val1" set "temp2=val2" for /f "tokens=1,2 delims=|" %%A in (""!temp1!"|"!temp2!"") do ( endLocal set "var1=%%~A" set "var2=%%~B" ) 

    Si debe devolver múltiples valores, y el juego de caracteres no está restringido, entonces use bucles FOR / F nesteds:

     setlocal enableDelayedExpansion set "temp1=val1" set "temp2=val2" for /f "delims=" %%A in (""!temp1!"") do ( for /f "delims=" %%B in (""!temp2!"") do ( endlocal set "var1=%%~A" set "var2=%%~B" ) ) 

    Definitivamente, mira la respuesta de jeb para una técnica segura, a prueba de balas, que funciona para todos los valores posibles en todas las situaciones.

    2017-08-21 - Nueva función RETURN.BAT
    He trabajado con DosTips user jeb para desarrollar una utilidad por lotes llamada RETURN.BAT que se puede usar para salir de un script o rutina llamada y devolver una o más variables a través de la barrera ENDLOCAL. Muy genial 🙂

    A continuación se encuentra la versión 3.0 del código. Lo más probable es que no mantenga este código actualizado. Lo mejor es seguir el enlace para asegurarse de obtener la última versión y ver ejemplos de uso.

    RETURN.BAT

     ::RETURN.BAT Version 3.0 @if "%~2" equ "" (goto :return.special) else goto :return ::: :::call RETURN ValueVar ReturnVar [ErrorCode] ::: Used by batch functions to EXIT /B and safely return any value across the ::: ENDLOCAL barrier. ::: ValueVar = The name of the local variable containing the return value. ::: ReturnVar = The name of the variable to receive the return value. ::: ErrorCode = The returned ERRORLEVEL, defaults to 0 if not specified. ::: :::call RETURN "ValueVar1 ValueVar2 ..." "ReturnVar1 ReturnVar2 ..." [ErrorCode] ::: Same as before, except the first and second arugments are quoted and space ::: delimited lists of variable names. ::: ::: Note that the total length of all assignments (variable names and values) ::: must be less then 3.8k bytes. No checks are performed to verify that all ::: assignments fit within the limit. Variable names must not contain space, ::: tab, comma, semicolon, caret, asterisk, question mark, or exclamation point. ::: :::call RETURN init ::: Defines return.LF and return.CR variables. Not required, but should be ::: called once at the top of your script to improve performance of RETURN. ::: :::return /? ::: Displays this help ::: :::return /V ::: Displays the version of RETURN.BAT ::: ::: :::RETURN.BAT was written by Dave Benham and DosTips user jeb, and was originally :::posted within the folloing DosTips thread: ::: http://www.dostips.com/forum/viewtopic.php?f=3&t=6496 ::: ::============================================================================== :: If the code below is copied within a script, then the :return.special code :: can be removed, and your script can use the following calls: :: :: call :return ValueVar ReturnVar [ErrorCode] :: :: call :return.init :: :return ValueVar ReturnVar [ErrorCode] :: Safely returns any value(s) across the ENDLOCAL barrier. Default ErrorCode is 0 setlocal enableDelayedExpansion if not defined return.LF call :return.init if not defined return.CR call :return.init set "return.normalCmd=" set "return.delayedCmd=" set "return.vars=%~2" for %%a in (%~1) do for /f "tokens=1*" %%b in ("!return.vars!") do ( set "return.normal=!%%a!" if defined return.normal ( set "return.normal=!return.normal:%%=%%3!" set "return.normal=!return.normal:"=%%4!" for %%C in ("!return.LF!") do set "return.normal=!return.normal:%%~C=%%~1!" for %%C in ("!return.CR!") do set "return.normal=!return.normal:%%~C=%%2!" set "return.delayed=!return.normal:^=^^^^!" ) else set "return.delayed=" if defined return.delayed call :return.setDelayed set "return.normalCmd=!return.normalCmd!&set "%%b=!return.normal!"^!" set "return.delayedCmd=!return.delayedCmd!&set "%%b=!return.delayed!"^!" set "return.vars=%%c" ) set "err=%~3" if not defined err set "err=0" for %%1 in ("!return.LF!") do for /f "tokens=1-3" %%2 in (^"!return.CR! %% "") do ( (goto) 2>nul (goto) 2>nul if "^!^" equ "^!" (%return.delayedCmd:~1%) else %return.normalCmd:~1% if %err% equ 0 (call ) else if %err% equ 1 (call) else cmd /c exit %err% ) :return.setDelayed set "return.delayed=%return.delayed:!=^^^!%" ! exit /b :return.special @if /i "%~1" equ "init" goto return.init @if "%~1" equ "/?" ( for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do @echo(%%A exit /b 0 ) @if /i "%~1" equ "/V" ( for /f "tokens=* delims=:" %%A in ('findstr /rc:"^::RETURN.BAT Version" "%~f0"') do @echo %%A exit /b 0 ) @>&2 echo ERROR: Invalid call to RETURN.BAT @exit /b 1 :return.init - Initializes the return.LF and return.CR variables set ^"return.LF=^ ^" The empty line above is critical - DO NOT REMOVE for /f %%C in ('copy /z "%~f0" nul') do set "return.CR=%%C" exit /b 0 
     @ECHO OFF SETLOCAL REM Keep in mind that BAR in the next statement could be anything, including %1, etc. SET FOO=BAR ENDLOCAL && SET FOO=%FOO% 

    La respuesta de dbenham es una buena solución para cadenas “normales” , ¡pero falla con exclamaciones ! si la expansión retardada está habilitada después de ENDLOCAL (dbenham también dijo esto).

    Pero siempre fallará con algunos contenidos complicados como avances de línea incrustados,
    como FOR / F dividirá el contenido en múltiples líneas.
    Esto dará como resultado un comportamiento extraño, el endlocal se ejecutará varias veces (para cada alimentación de línea), por lo que el código no es a prueba de balas.

    Existen soluciones a prueba de balas, pero son un poco desordenadas 🙂
    Existe una versión de macro ASÍ: Preservar la exclamación … , usarla es fácil, pero leerla es …

    O puede usar un bloque de código , puede pegarlo en sus funciones.
    Dbenham y yo desarrollamos esta técnica en el hilo Re: nuevas funciones:: chr,: asc,: asciiMap ,
    también hay explicaciones para esta técnica

     @echo off setlocal EnableDelayedExpansion cls for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a" set LF=^ rem TWO Empty lines are neccessary set "original=zero*? %%~A%%~B%%~C%%~L!LF!one&line!LF!two with exclam^! !LF!three with "quotes^&"&"!LF!four with ^^^^ ^| ^< ^> ( ) ^& ^^^! ^"!LF!xxxxxwith CR!CR!five !LF!six with ^"^"Q ^"^"L still six " setlocal DisableDelayedExpansion call :lfTest result original setlocal EnableDelayedExpansion echo The result with disabled delayed expansion is: if !original! == !result! (echo OK) ELSE echo !result! call :lfTest result original echo The result with enabled delayed expansion is: if !original! == !result! (echo OK) ELSE echo !result! echo ------------------ echo !original! goto :eof :::::::::::::::::::: :lfTest setlocal set "NotDelayedFlag=!" echo( if defined NotDelayedFlag (echo lfTest was called with Delayed Expansion DISABLED) else echo lfTest was called with Delayed Expansion ENABLED setlocal EnableDelayedExpansion set "var=!%~2!" rem echo the input is: rem echo !var! echo( rem ** Prepare for return set "var=!var:%%=%%~1!" set "var=!var:"=%%~2!" for %%a in ("!LF!") do set "var=!var:%%~a=%%~L!" for %%a in ("!CR!") do set "var=!var:%%~a=%%~3!" rem ** It is neccessary to use two IF's else the %var% expansion doesn't work as expected if not defined NotDelayedFlag set "var=!var:^=^^^^!" if not defined NotDelayedFlag set "var=%var:!=^^^!%" ! set "replace=%% """ !CR!!CR!" for %%L in ("!LF!") do ( for /F "tokens=1,2,3" %%1 in ("!replace!") DO ( ENDLOCAL ENDLOCAL set "%~1=%var%" ! @echo off goto :eof ) ) exit /b 

    Algo como lo siguiente (no lo he probado):

     @echo off setlocal set base=compute directory set pkg=compute sub-directory set scripts=%base%\%pkg%\Scripts pushd %scripts% endlocal call .\activate.bat popd 

    Como lo anterior no funciona (ver el comentario de Marcelo), probablemente haría esto de la siguiente manera:

     set uniquePrefix_base=compute directory set uniquePrefix_pkg=compute sub-directory set uniquePrefix_scripts=%uniquePrefix_base%\%uniquePrefix_pkg%\Scripts set uniquePrefix_base= set uniquePrefix_pkg= call %uniquePrefix_scripts%\activate.bat set uniquePrefix_scripts= 

    donde uniquePrefix_ se elige para ser “casi seguro” único en su entorno.

    También puede probar al ingresar al archivo bat que las variables de entorno “uniquePrefix _…” no están definidas en la entrada como se esperaba, de lo contrario, puede salir con un error.

    No me gusta copiar el BAT en el directorio TEMP como una solución general debido a (a) la posibilidad de una condición de carrera con> 1 llamante, y (b) en el caso general, un archivo BAT podría estar accediendo a otros archivos usando un ruta relativa a su ubicación (por ejemplo,% ~ dp0 .. \ somedir \ somefile.dat).

    La siguiente solución fea resolverá (b):

     setlocal set scripts=...whatever... echo %scripts%>"%TEMP%\%~n0.dat" endlocal for /f "tokens=*" %%i in ('type "%TEMP%\%~n0.dat"') do call %%i\activate.bat del "%TEMP%\%~n0.dat" 

    Para responder a mi propia pregunta (en caso de que ninguna otra respuesta salga a la luz, y para evitar repeticiones de la que ya conozco) …

    Cree un archivo por lotes temporal antes de llamar a endlocal que contiene el comando para llamar al archivo por lotes de destino, luego, endlocal y elimínelo después de endlocal :

     echo %scripts%\activate.bat > %TEMP%\activate.bat endlocal call %TEMP%\activate.bat del %TEMP%\activate.bat 

    Esto es tan feo, quiero ahorcarme de vergüenza. Mejores respuestas son bienvenidas.

    Quiero contribuir a esto también y decirte cómo puedes pasar un conjunto de variables tipo array:

     @echo off rem clean up array in current environment: set "ARRAY[0]=" & set "ARRAY[1]=" & set "ARRAY[2]=" & set "ARRAY[3]=" rem begin environment localisation block here: setlocal EnableExtensions rem define an array: set "ARRAY[0]=1" & set "ARRAY[1]=2" & set "ARRAY[2]=4" & set "ARRAY[3]=8" rem `set ARRAY` returns all variables starting with `ARRAY`: for /F "tokens=1,2 delims==" %%V in ('set ARRAY') do ( if defined %%V ( rem end environment localisation block once only: endlocal ) rem re-assign the array, `for` variables transport it: set "%%V=%%W" ) rem this is just for prove: for /L %%I in (0,1,3) do ( call echo %%ARRAY[%%I]%% ) exit /B 

    El código funciona, porque el primer elemento de la matriz es consultado por if defined dentro del bloque setlocal donde está realmente definido, por lo que endlocal se ejecuta solo una vez. Para todas las iteraciones sucesivas de bucle, el bloque setlocal ya ha finalizado y, por if defined tanto, if defined evalúa como FALSE .

    Esto se basa en el hecho de que al menos se asigna un elemento de matriz, o en realidad, que hay al menos una variable definida cuyo nombre comienza con ARRAY , dentro del bloque setlocal / endlocal . Si no existe ninguno, endlocal no se ejecutará. Fuera del bloque setlocal , no se debe definir dicha variable, porque de lo contrario, if defined evalúa como TRUE más de una vez y, por lo tanto, endlocal se ejecuta varias veces.

    Para superar estas restricciones, puede usar una variable tipo bandera, de acuerdo con esto:

    • borre la variable del indicador, digamos ARR_FLAG , antes del comando setlocal : set "ARR_FLAG=" ;
    • defina la variable de indicador dentro del bloque setlocal / endlocal , es decir, asigne un valor no vacío (inmediatamente antes del bucle for /F preferiblemente): set "ARR_FLAG=###" ;
    • cambie la línea de comando if defined a: if defined ARR_FLAG ( ;
    • entonces también puedes hacer opcionalmente:
      • cambie la cadena de opciones for /F a "delims=" ;
      • cambie la línea de comando set en el ciclo for /F por: set "%%V" ;

    Para sobrevivir a múltiples variables: si elige ir con el “clásico”
    ENDLOCAL & SET VAR =% TEMPVAR%
    mencionado a veces en otras respuestas aquí (y están satisfechos de que los inconvenientes que se muestran en algunas de las respuestas se abordan o no son un problema), tenga en cuenta que puede hacer múltiples variables, al
    ENDLOCAL & SET var1 =% local1% & SET var2 =% local2%
    Comparto esto porque aparte del sitio vinculado a continuación, solo he visto el “truco” ilustrado con una sola variable, y como yo, algunos pueden haber asumido incorrectamente que solo “funciona” para una sola variable. https://ss64.com/nt/endlocal.html

    Qué tal esto.

     @echo off setlocal set base=compute directory set pkg=compute sub-directory set scripts=%base%\%pkg%\Scripts ( endlocal "%scripts%\activate.bat" )