使环境变量在ENDLOCAL后仍然存在

29

我有一个批处理文件,它通过一系列中间变量计算出一个变量:

@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之后调用它,如果没有更好的方法,我将这样做,但我想知道是否有更简洁的解决方案。


3
我喜欢下面所有回答的方式,展示了如何处理各种边缘情况,这些情况在你特定的问题中可能不会出现。(你最终需要一个合法的目录,因此不太可能存在 !; 等特殊字符。)这表明我们所有人更关心教学而不仅仅是快速地回答问题。那就是 StackOverflow。 - Jesse Chisholm
有趣的是,我怀疑被调用的脚本只需要引用自己的路径,也就是说,“Activate.bat”需要知道它被从哪里调用,并将该变量分配为“Scripts”,以便在其内部进行将来的调用。 - Ben Personick
对于可能需要调用其目录内其他脚本的独立脚本,您可以轻松使用%0参数,它是任何被调用的脚本的隐式定义参数。因此,要获取正在运行的脚本所在的路径,可以使用%~p0。 - Ben Personick
由于我们知道脚本“Activate.bat”正在您的其他变量定义的路径中运行,因此可以安全地得出结论,它可以将脚本变量设置为其自己的路径。(例如,在Activate.bat中,您可以在顶部运行[code]SET "scripts=%~p0"[/code]) - Ben Personick
此外,如果脚本本身需要多次重新引用“scripts”路径,则有许多选择可以在不经过很多麻烦的情况下完成。您提出的问题正在回答时,很少考虑您所述的目标及其相关的简化和不太晦涩的最佳实践。并非这些解决方案对您的查询是错误的,它们只是通常并不是最好的解决方案,甚至很少是最好的解决方案。通常,在endlocal之外设置变量是为了从子函数或脚本中将它们返回到调用环境。 - Ben Personick
显示剩余2条评论
8个回答

28

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包含!,最终值将被破坏。

  • 包含换行符的值无法传输。

如果您必须返回多个值,并且您知道一个字符不能出现在任一值中,则只需使用适当的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"
  )
)

一定要查看jeb的答案,这是一种安全、防弹的技术,适用于所有可能的值和情况。

2017-08-21 - 新功能RETURN.BAT
我与DosTips用户jeb合作开发了一个批处理实用程序,名为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

3
顺便说一句,奇怪的语法是多余的,并且在tempvar为空时会失败。更好的方式是 FOR /F "delims=" %%A in (""!tempvar!"") do endlocal & set "var=%%~A" - jeb
1
@jeb - 这有多余吗?如果值以 ; 开头,则会被跳过。用引号括起来是个好主意。另一个选项是在发出 SETLOCAL 命令之前将值初始化为空,但如果通过多个 FOR /F 循环返回多个值,则可能会失败。 - dbenham
1
现在这个版本并不多余,但与双引号版本相比,它没有换行符或空值的问题。 - jeb

14
@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%

2
+1。就是这个。它能够正常工作是因为环境变量在解析一行时被扩展,这意味着一旦运行了这行代码,首先会执行 endlocal,然后 set 中的变量已经被扩展了。 - Joey
3
解决方案可能会失败,这取决于%FOO%中的内容,请参考dbenham的答案。 - jeb

9
dbenham先生的回答对于“正常”的字符串是一个很好的解决方案,但是如果延迟扩展在ENDLOCAL之后启用,则会在感叹号!处失败(dbenham也说过这一点)。
但是它将始终无法处理某些棘手的内容,例如嵌入式换行符,因为FOR/F将内容拆分成多个行。这将导致奇怪的行为,endlocal将被执行多次(每个换行符一次),因此代码不是防弹的。
存在防弹方案,但有点凌乱。存在一个宏版本SO:保留叹号...,使用它很容易,但阅读起来有点麻烦。
或者您可以使用代码块,您可以将其粘贴到函数中。Dbenham和我在主题Re:new functions::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

3

我也想为此做出贡献,告诉您如何传递类似数组的变量集合:

@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

该代码的作用在于,由于第一个数组元素在setlocal块内被定义并且通过if defined查询,因此只会执行一次endlocal。对于所有后续循环迭代,setlocal块已经结束,因此if defined的值为FALSE

这依赖于至少分配一个数组元素或者实际上在setlocal/endlocal块中至少定义一个以ARRAY开头的变量。如果在其中不存在任何变量,则不会执行endlocal。在setlocal块外面,不能定义任何此类变量,否则,if defined将会多次评估并且endlocal将会被多次执行。
要克服这些限制,可以使用一个像标志一样的变量,具体如下:
  • setlocal命令之前清空标志变量,例如:set "ARR_FLAG="
  • setlocal/endlocal块内定义标志变量,即向其分配一个非空值(最好是在for /F循环之前):set "ARR_FLAG=###"
  • if defined命令行更改为:if defined ARR_FLAG (
  • 您也可以选择执行以下操作:
    • for /F选项字符串更改为"delims="
    • for /F循环中的set命令行更改为:set "%%V"

1

类似以下代码(我还没有测试):

@echo off 
setlocal 

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

pushd %scripts%

endlocal 

call .\activate.bat 
popd

由于上述方法无法正常工作(请参考Marcelo的评论),我建议按照以下方式进行:

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_被选择为在您的环境中“几乎肯定”是唯一的。

您还可以在进入批处理文件时测试“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"

1
一个灵感十足的想法!我喜欢它。不幸的是,它不起作用。我尝试了一下,发现endlocal也会回到setlocal本地化之前的工作目录。 - Marcelo Cantos
参见匿名用户的回答,这是一种不错的方法。当涉及到更多变量时可能会有些混乱,但是它可以很好地工作,而且不需要创建任何文件。 - Joey

1

如果您选择使用其他响应中有时提到的“经典”ENDLOCAL & SET VAR =%TEMPVAR%(并且满意某些响应中显示的缺点已经解决或不是问题),请注意您可以进行多个变量,即 ENDLOCAL &SET var1 =%local1%&SET var2 =%local2%

我分享这个是因为除了下面链接的网站,我只看到过用于单个变量的“技巧”,像我一样的一些人可能会错误地认为它仅适用于单个变量。

文档:https://ss64.com/nt/endlocal.html


0
为了回答我自己的问题(以防没有其他答案浮现并且避免重复已知的答案)……在调用 endlocal 之前创建一个包含调用目标批处理文件命令的临时批处理文件,然后在 endlocal 之后调用并删除它。
echo %scripts%\activate.bat > %TEMP%\activate.bat

endlocal

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

这太丑了,我想低头羞耻。欢迎更好的答案。


-3

这样怎么样?

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

1
你为什么在发布之前不测试一下呢?它根本就不起作用。 - jeb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接