Faça uma variável de ambiente sobreviver ao ENDLOCAL

Eu tenho um arquivo em lotes que calcula uma variável através de uma série de variables ​​intermediárias:

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

O script na última linha não é chamado, porque vem depois de endlocal, o que atrapalha a variável de ambiente de scripts , mas ele precisa vir após endlocal porque seu propósito é definir um monte de outras variables ​​de ambiente para uso pelo usuário.

Como faço para chamar um script cuja finalidade é definir variables ​​de ambiente permanentes, mas a localização de quem é determinada por uma variável de ambiente temporária?

Eu sei que posso criar um arquivo de lote temporário antes do endlocal e chamá-lo depois de endlocal, o que farei se nada mais vier à tona, mas gostaria de saber se existe uma solução menos digna de constrangimento.

    O ENDLOCAL & SET VAR=%TEMPVAR% é clássico. Mas há situações em que isso não é ideal.

    Se você não conhece o conteúdo do TEMPVAR, poderá encontrar problemas se o valor contiver caracteres especiais como < > & ou | . Geralmente você pode proteger contra isso usando citações como SET "VAR=%TEMPVAR%" , mas isso pode causar problemas se houver caracteres especiais e o valor já estiver citado.

    Uma expressão FOR é uma excelente opção para transportar um valor através da barreira ENDLOCAL se você estiver preocupado com caracteres especiais. A expansão atrasada deve ser ativada antes do ENDLOCAL e desativada após o ENDLOCAL.

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

    Limitações:

    • Se a expansão atrasada for ativada após o ENDLOCAL, o valor final será corrompido se o TEMPVAR contiver ! .

    • valores contendo um caractere lineFeed não podem ser transportados

    Se você precisar retornar vários valores e souber de um caractere que não pode aparecer em nenhum valor, simplesmente use as opções FOR / F apropriadas. Por exemplo, se eu souber que os valores não podem conter | :

     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" ) 

    Se você precisar retornar vários valores e o conjunto de caracteres não for restrito, use loops 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, confira a resposta de jeb para uma técnica segura, à prova de balas, que funciona para todos os valores possíveis em todas as situações.

    2017-08-21 - Nova function RETURN.BAT
    Eu trabalhei com o usuário DosTips jeb para desenvolver um utilitário em lote chamado RETURN.BAT que pode ser usado para sair de um script ou chamar uma rotina e retornar uma ou mais variables ​​através da barreira ENDLOCAL. Muito legal 🙂

    Abaixo está a versão 3.0 do código. Eu provavelmente não manterei este código atualizado. É melhor seguir o link para ter certeza de obter a versão mais recente e ver alguns exemplos 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% 

    A resposta do dbenham é uma boa solução para strings “normais” , mas falha com pontos de exclamação ! se a expansão atrasada for ativada após ENDLOCAL (dbenham também disse isso).

    Mas sempre falhará com alguns conteúdos complicados, como alimentações de linha incorporadas,
    como o FOR / F irá dividir o conteúdo em várias linhas.
    Isso resultará em um comportamento estranho, o endlocal será executado várias vezes (para cada alimentação de linha), portanto, o código não é à prova de balas.

    Existem soluções à prova de balas, mas são um pouco confusas 🙂
    Existe uma versão macro SO: Preservando exclamação … , usar é fácil, mas ler é …

    Ou você pode usar um bloco de código , você pode colá-lo em suas funções.
    Dbenham e eu desenvolvemos essa técnica no thread Re: novas funções:: chr,: asc,: asciiMap ,
    há também as explicações para essa 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 o seguinte (eu não testei):

     @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 o acima não funciona (veja o comentário de Marcelo), eu provavelmente faria isso da seguinte maneira:

     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= 

    onde uniquePrefix_ é escolhido para ser “quase certamente” exclusivo em seu ambiente.

    Você também pode testar na input do arquivo bat que as variables ​​de ambiente “uniquePrefix _…” são indefinidas na input como esperado – se não, você pode sair com um erro.

    Eu não gosto de copiar o BAT para o diretório TEMP como uma solução geral por causa de (a) o potencial para uma condição de corrida com> 1 chamador, e (b) no caso geral um arquivo BAT pode estar acessando outros arquivos usando um caminho em relação à sua localização (por exemplo,% ~ dp0 .. \ somedir \ somefile.dat).

    A seguinte solução feia 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 minha própria pergunta (caso nenhuma outra resposta venha à luz, e para evitar repetições do que eu já conheço) …

    Crie um arquivo de lote temporário antes de chamar endlocal que contenha o comando para chamar o arquivo de lote de destino e, em seguida, chame e exclua-o após endlocal :

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

    Isso é tão feio que quero envergonhar a cabeça. Melhores respostas são bem-vindas.

    Eu quero contribuir para isso também e dizer-lhe como você pode passar por cima de um conjunto de variables ​​como uma matriz:

     @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 

    O código funciona, porque o primeiro elemento da matriz é consultado por if defined dentro do bloco setlocal onde ele é realmente definido, portanto, o endlocal é executado apenas uma vez. Para todas as iterações de loop sucessivas, o bloco setlocal já está finalizado e, portanto, if defined avaliado como FALSE .

    Isso depende do fato de que pelo menos um elemento de matriz é atribuído ou, na verdade, que há pelo menos uma variável definida cujo nome começa com ARRAY , dentro do bloco setlocal / endlocal . Se não houver nenhum, o endlocal não será executado. Fora do bloco setlocal , nenhuma variável deve ser definida, porque, if defined contrário, if defined avaliada como TRUE mais de uma vez e, portanto, o endlocal é executado várias vezes.

    Para superar essas restrições, você pode usar uma variável semelhante a um sinalizador, de acordo com isso:

    • limpe a variável flag, diga ARR_FLAG , antes do comando setlocal : set "ARR_FLAG=" ;
    • defina a variável flag dentro do bloco setlocal / endlocal , ou seja, atribua um valor não vazio a ele (imediatamente antes do loop for /F preferencialmente): set "ARR_FLAG=###" ;
    • altere a linha de comando if defined para: if defined ARR_FLAG ( ;
    • então você também pode fazer opcionalmente:
      • altere a string da opção for /F para "delims=" ;
      • mude a linha de comando set no loop for /F para: set "%%V" ;

    Para sobreviver múltiplas variables: Se você optar por ir com o “clássico”
    ENDLOCAL & SET VAR =% TEMPVAR%
    mencionado às vezes em outras respostas aqui (e estão satisfeitos que as desvantagens mostradas em algumas das respostas são abordadas ou não são um problema), note que você pode fazer várias variables, a la
    ENDLOCAL & SET var1 =% local1% e SET var2 =% local2%
    Compartilho isso porque, além do site vinculado abaixo, só vi o “truque” ilustrado com uma única variável e, como eu, alguns podem ter incorretamente assumido que ele “só funciona” para uma única variável. https://ss64.com/nt/endlocal.html

    Que tal agora.

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