Заставьте переменную окружения выжить ENDLOCAL
У меня есть пакетный файл, который вычисляет переменную через ряд промежуточных переменных:
@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 ответов
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
@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%
Ответ dbenham - хорошее решение для "нормальных" строк, но оно не с восклицательными знаками !
если отложенное расширение включено после ENDLOCAL (dbenham сказал это тоже).
Но он всегда будет терпеть неудачу с некоторыми хитрыми материалами, такими как встроенные переводы строк,
поскольку FOR/F будет разбивать содержимое на несколько строк.
Это приведет к странному поведению, endlocal будет выполняться несколько раз (для каждого перевода строки), поэтому код не является пуленепробиваемым.
Существуют пуленепробиваемые решения, но они немного грязные:-)
Макро версия существует ТАК: Сохранение восклицательного знака..., использовать его легко, но читать это...
Или вы можете использовать блок кода, вы можете вставить его в свои функции.
Dbenham и я разработали эту технику в потоке 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
Я тоже хочу внести свой вклад в это и рассказать вам, как вы можете передать массив переменных, подобный массиву:
@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
Код работает, потому что самый первый элемент массива запрашивается if defined
в пределах setlocal
блок, где это на самом деле определяется, так endlocal
выполняется только один раз. Для всех последовательных итераций цикла setlocal
блок уже закончен и поэтому if defined
оценивает в ЛОЖЬ.
Это основывается на том факте, что назначен хотя бы один элемент массива, или на самом деле определена хотя бы одна переменная, имя которой начинается с ARRAY
в пределах setlocal
/endlocal
блок. Если их там нет, endlocal
не будет казнен. Вне setlocal
блок, такая переменная не должна быть определена, потому что в противном случае, if defined
оценивается как ИСТИНА более одного раза и, следовательно, endlocal
выполняется несколько раз.
Чтобы преодолеть эти ограничения, вы можете использовать флагоподобную переменную, в соответствии с этим:
- очистить флаг переменную, скажем
ARR_FLAG
, передsetlocal
команда:set "ARR_FLAG="
; - определить переменную флага внутри
setlocal
/endlocal
блок, то есть присвоить ему непустое значение (непосредственно передfor /F
кругом желательно):set "ARR_FLAG=###"
; - изменить
if defined
командная строка для:if defined ARR_FLAG (
; - тогда вы также можете сделать дополнительно:
- изменить
for /F
строка параметра для"delims="
; - изменить
set
командная строка вfor /F
цикл до:set "%%V"
;
- изменить
Что-то вроде следующего (я не проверял это):
@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 в качестве общего решения, поскольку (а) существует вероятность возникновения состояния гонки с>1 вызывающим абонентом, и (b) в общем случае файл BAT может обращаться к другим файлам, используя путь относительно его местоположения (например,%~dp0..\somedir\somefile.dat).
Следующее уродливое решение решит (б):
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"
Для выживания нескольких переменных: если вы решили пойти с "классическим"
ENDLOCAL & SET VAR =% TEMPVAR%
упоминается иногда в других ответах здесь (и удовлетворены тем, что недостатки, показанные в некоторых ответах, устранены или не являются проблемой), обратите внимание, что вы можете сделать несколько переменных, а
ENDLOCAL & SET var1=%local1% & SET var2=%local2%
Я делюсь этим, потому что, кроме ссылочного сайта, приведенного ниже, я видел только "уловку", проиллюстрированную одной переменной, и, как и я, некоторые могли ошибочно предположить, что она "работает" только для одной переменной. https://ss64.com/nt/endlocal.html
Чтобы ответить на мой собственный вопрос (в случае, если другой ответ не обнаруживается, и чтобы избежать повторений того, о котором я уже знаю)...
Создайте временный пакетный файл перед вызовом endlocal
который содержит команду для вызова целевого пакетного файла, затем вызывает и удаляет его после endlocal
:
echo %scripts%\activate.bat > %TEMP%\activate.bat
endlocal
call %TEMP%\activate.bat
del %TEMP%\activate.bat
Это так безобразно, что я хочу опустить голову от стыда. Лучшие ответы приветствуются.
Как насчет этого.
@echo off
setlocal
set base=compute directory
set pkg=compute sub-directory
set scripts=%base%\%pkg%\Scripts
(
endlocal
"%scripts%\activate.bat"
)