Заставьте переменную окружения выжить ENDLOCAL - PullRequest
24 голосов
/ 16 июля 2010

У меня есть пакетный файл, который вычисляет переменную с помощью ряда промежуточных переменных:

@echo off
setlocal

set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts

endlocal

%scripts%\activate.bat

Сценарий в последней строке не вызывается, потому что он идет после endlocal, который закрывает scripts переменная окружения, но она должна идти после endlocal, потому что ее цель состоит в том, чтобы установить кучу других переменных окружения для использования пользователем.

Как мне вызвать скрипт, целью которого является установка постоянногоПеременные окружения, но чье местоположение определяется временной переменной окружения?

Я знаю, что могу создать временный пакетный файл до endlocal и вызвать его после endlocal, что я сделаю, если больше ничего не выяснится, но я хотел бы знать, есть ли менее достойное решения.

Ответы [ 8 ]

23 голосов
/ 24 ноября 2011

Шаблон ENDLOCAL & SET VAR=%TEMPVAR% классический.Но бывают ситуации, когда он не идеален.

Если вы не знаете содержимого TEMPVAR, вы можете столкнуться с проблемами, если значение содержит специальные символы, такие как < > & или |.Как правило, вы можете защититься от этого с помощью кавычек, таких как SET "VAR=%TEMPVAR%", но это может вызвать проблемы, если есть специальные символы, а значение уже заключено в кавычки.

Выражение FOR является отличным выбором для передачи значения черезENDLOCAL барьер, если вас беспокоят специальные символы.Задержанное расширение должно быть включено до ENDLOCAL, и отключено после ENDLOCAL.

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

Ограничения:

  • Если отложенное расширение включено после ENDLOCAL, то окончательноезначение будет повреждено, если TEMPVAR содержал !.

  • значения, содержащие символ lineFeed, не могут быть переданы

Если необходимо вернуть несколько значенийи вы знаете символ, который не может появляться ни в одном из значений, а затем просто используйте соответствующие параметры FOR / F.Например, если я знаю, что значения не могут содержать |:

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

Если необходимо вернуть несколько значений, а набор символов неограничен, используйте вложенные циклы FOR / F:

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

Обязательно проверьте ответ Джеба , чтобы узнать о безопасной, пуленепробиваемой технике, которая работает для всех возможных значений в любых ситуациях.

2017-08-21- Новая функция RETURN.BAT
Я работал с пользователем jeb DosTips для разработки пакетной утилиты с именем RETURN.BAT , которую можно использовать для выхода из скрипта или вызываемой подпрограммы.и вернуть одну или несколько переменных через барьер ENDLOCAL.Очень круто: -)

Ниже приведена версия 3.0 кода.Скорее всего, я не буду обновлять этот код.Лучше всего перейти по ссылке, чтобы убедиться, что вы получите последнюю версию, и посмотреть пример использования.

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
13 голосов
/ 16 июля 2010
@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%
8 голосов
/ 24 ноября 2011

Ответ из dbenham является хорошим решением для "обычных" строк, но он не работает с восклицательными знаками !, если после ENDLOCAL включено отложенное расширение (dbenham сказал это тоже).

Но это всегда будет неудачно с некоторыми хитрыми содержимым, такими как встроенные переводы строк,
, поскольку FOR / F будет разбивать содержимое на несколько строк.
Это приведет к странному поведению, endlocal будет выполняться несколькораз (для каждого перевода строки), поэтому код не является пуленепробиваемым.

Существуют пуленепробиваемые решения, но они немного беспорядочные: -)
A версия макроса существует SO: сохранение восклицательного знака ... , для использованияэто легко, но читать это ...

Или вы можете использовать кодовый блок , вы можете вставить его в свои функции.
Дбенхем и я разработали эту технику впоток Re: новые функции:: chr,: asc,: asciiMap ,
также есть объяснения для этой техники

@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
2 голосов
/ 23 декабря 2015

Я тоже хочу внести свой вклад в это и рассказать, как вы можете передать массив переменных, подобный массиву:

@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,* 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

Код работает, потому что самый первый элемент массива запрашивается if defined в блоке setlocal, где он фактически определен, поэтому endlocal выполняется только один раз.Для всех последовательных итераций цикла блок setlocal уже завершен, и поэтому if defined оценивается как FALSE .

Это зависит от того факта, что назначен хотя бы один элемент массива,или на самом деле, существует хотя бы одна переменная, имя которой начинается с ARRAY в блоке setlocal / endlocal.Если там ничего не существует, endlocal не будет выполняться.За пределами блока setlocal такая переменная не должна определяться, так как в противном случае if defined оценивается как TRUE более одного раза и, следовательно, endlocal выполняется несколько раз.

Чтобы преодолеть эти ограничения, вы можете использовать флагоподобную переменную в соответствии с этим:

  • очистите переменную флага, скажем ARR_FLAG, перед командой setlocal: set "ARR_FLAG=";
  • определяет переменную flag внутри блока setlocal / endlocal, то есть присваивает ему непустое значение (предпочтительно непосредственно перед циклом for /F): set "ARR_FLAG=###";
  • измените if defined командную строку на: if defined ARR_FLAG (;
  • , затем вы также можете сделать необязательно:
    • изменить строку опции for /F на "delims=";
    • изменить командную строку set в цикле for /F на: set "%%V";
1 голос
/ 21 декабря 2017

Для выживания нескольких переменных: если вы решите пойти с «классическим»
ENDLOCAL & SET VAR=%TEMPVAR% упоминается иногда в других ответах здесь (и удовлетворены тем, что недостатки, показанные в некоторых ответах, устранены или не являются проблемой), обратите внимание, что вы можете сделать несколько переменных, например ENDLOCAL & SET var1=%local1% & SET var2=%local2%.

Я делюсь этим, потому что, кроме сайта, на который мы ссылаемся ниже, я видел только «трюк», проиллюстрированный одной переменной, и, как и я, некоторые могли ошибочно предположить, что он «работает» только для одной переменной.

Документы: https://ss64.com/nt/endlocal.html

1 голос
/ 16 июля 2010

Примерно так (я не проверял):

@echo off 
setlocal 

set base=compute directory 
set pkg=compute sub-directory 
set scripts=%base%\%pkg%\Scripts 

pushd %scripts%

endlocal 

call .\activate.bat 
popd

Поскольку вышеприведенное не работает (см. Комментарий Марсело), ​​я, вероятно, сделал бы это следующим образом:

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=

, где uniquePrefix_ выбран «почти наверняка» уникальным в вашей среде.

Вы также можете проверить при входе в файл bat, что переменные среды «uniquePrefix _...» не определены при входе, как и ожидалось - если нет, вы можете выйти с ошибкой.

Мне не нравится копировать BAT в каталог TEMP в качестве общего решения из-за (a) возможности возникновения состояния гонки с> 1 вызывающим абонентом и (b) в общем случае файл BAT может обращаться к другим файлы, использующие путь относительно его местоположения (например,% ~ dp0 .. \ somedir \ somefile.dat).

Следующее уродливое решение решит (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"
0 голосов
/ 30 сентября 2016

Как насчет этого?

@echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
(
  endlocal
  "%scripts%\activate.bat"
)
0 голосов
/ 16 июля 2010

Чтобы ответить на мой собственный вопрос (в случае, если другой ответ не обнаруживается, и чтобы избежать повторений того, о котором я уже знаю) ...

Создайте временный пакетный файл перед вызовом endlocal, который содержит команду для вызова целевого пакетного файла, затем вызовите и удалите его после endlocal:

echo %scripts%\activate.bat > %TEMP%\activate.bat

endlocal

call %TEMP%\activate.bat
del %TEMP%\activate.bat

Это так ужасно, я хочу опустить голову от стыда. Лучшие ответы приветствуются.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...