【问题标题】:Make an environment variable survive ENDLOCAL使环境变量在 ENDLOCAL 中存在
【发布时间】:2011-03-16 19:01:12
【问题描述】:

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

@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 之后调用它,如果没有其他问题我会这样做,但我想知道是否有一个不那么令人畏惧的解决方案。

【问题讨论】:

  • 我喜欢下面的所有答案,这些答案展示了如何处理各种与您的特定问题无关的边缘情况。 (您的最终结果必须是合法目录,因此不太可能有 !; 或其他元字符怪异。)它表明我们都更关心教学,而不仅仅是快速通过答案。我的朋友们,这就是 StackOverflow。
  • 我在这里使用 reg.exe 和 volatile 注册表回答了这个问题:stackoverflow.com/questions/26246151/…
  • 有趣的是,我怀疑被调用的脚本只需要引用它自己的路径,IE,“Activate.bat”需要知道它是从哪里调用的,并将该变量分配为“脚本”以备将来使用在自身内部调用。
  • 因此,对于可能需要在其目录中调用其他脚本的独立脚本,您可以轻松使用 %0 参数,它是任何被调用脚本的隐式定义参数。因此,要获取运行脚本的路径将使用 %~p0。
  • 由于我们知道脚本“Activate.bat”在您的其他变量中定义的路径中运行,因此可以安全地得出结论,它可以将脚本变量设置为自己的路径。 (在 Activate.bat 中的 IE,您将在顶部运行 [code]SET "scripts=%~p0"[/code])

标签: batch-file environment-variables


【解决方案1】:

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's answer,了解适用于所有情况下所有可能值的安全、防弹技术。

2017-08-21 - 新功能 RETURN.BAT
我与 DosTips 用户 jeb 合作开发了a batch utility called 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

【讨论】:

  • 顺便说一句。奇怪的语法是多余的,它会因为一个空的 tempvar 而失败。更好的是FOR /F "delims=" %%A in (""!tempvar!"") do endlocal & set "var=%%~A"
  • @jeb - 多么多余?如果值以; 开头,则将被跳过。带引号的好主意。另一种选择是在发出 SETLOCAL 之前将值初始化为空,除非通过多个 FOR /F 循环返回多个值会失败。
  • 事实上,它不是多余的,而是与双引号版本相比,因为 eol 字符和空值都没有问题。
【解决方案2】:
@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%

【讨论】:

  • +1。就是那个。它之所以起作用,是因为在解析一行时会扩展环境变量,这意味着一旦运行此行,首先会执行endlocal,然后set中的变量已经被扩展。
  • 但是解决方案可能会失败,这取决于 %FOO% 中的内容,请参阅 dbenham 的答案
【解决方案3】:

dbenham 的answer“正常” 字符串的一个很好的解决方案,但是如果在ENDLOCAL 之后启用延迟扩展(dbenham 也这么说),它会失败并带有感叹号!

但它总是会因为一些棘手的内容而失败,例如嵌入式换行符,
因为 FOR/F 会将内容分成多行。
这将导致奇怪的行为,endlocal 将执行多次(对于每个换行符),因此代码不是防弹的。

存在防弹解决方案,但它们有点混乱 :-)
有一个宏版存在SO:Preserving exclamation ...,使用起来很简单,但是阅读起来就...

或者您可以使用代码块,您可以将其粘贴到您的函数中。
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

【讨论】:

    【解决方案4】:

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

    @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

    这依赖于这样一个事实,即至少分配了一个数组元素,或者实际上,在setlocal/endlocal 块中定义了至少一个名称以ARRAY 开头的变量。如果其中不存在,endlocal 将不会被执行。在 setlocal 块之外,不必定义这样的变量,因为否则,if defined 不止一次计算为 TRUE,因此,endlocal 被执行多次。

    为了克服这个限制,你可以使用一个类似标志的变量,根据这个:

    • 清除标志变量,例如ARR_FLAG,在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"

    【讨论】:

      【解决方案5】:

      类似下面的东西(我没有测试过):

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

      您还可以在输入 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"
      

      【讨论】:

      • 一个灵感的想法!我喜欢它。不幸的是,它不起作用。我试了一下,发现在setlocal是本地之前endlocal也会恢复到工作目录。
      • 请参阅Anonymous' answer,了解一个不错的方法。它会因更多变量而变得混乱,但无需创建任何文件即可正常工作。
      【解决方案6】:

      对于幸存的多个变量:如果您选择使用“经典”
      ENDLOCAL &amp; SET VAR=%TEMPVAR% 有时在此处的其他回复中提到(并且满意某些回复中显示的缺点已得到解决或不是问题),注意你可以做多个变量,比如ENDLOCAL &amp; SET var1=%local1% &amp; SET var2=%local2%

      我之所以分享这个,是因为除了下面的链接网站之外,我只看到了用单个变量说明的“技巧”,并且像我自己一样,有些人可能错误地认为它只对单个变量“有效”。

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

      【讨论】:

        【解决方案7】:

        回答我自己的问题(以防没有其他答案出现,并避免重复我已经知道的问题)...

        在调用endlocal之前创建一个临时批处理文件,其中包含调用目标批处理文件的命令,然后在endlocal之后调用并删除:

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

        这太丑了,我想羞愧地低下头。欢迎提供更好的答案。

        【讨论】:

          【解决方案8】:

          这个怎么样。

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

          【讨论】:

          • 为什么在发帖前没有测试?它根本不起作用
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-09-13
          • 1970-01-01
          • 2023-01-22
          • 2020-03-02
          • 2021-03-04
          相关资源
          最近更新 更多