SETLOCAL和ENABLEDELAYEDEXPANSION是如何工作的?

75
我注意到大多数脚本中,它们通常在同一行中,如下所示:
SETLOCAL ENABLEDELAYEDEXPANSION

这两个命令实际上是分开的,可以分别写在不同的行吗?

如果在脚本的第一行设置了ENABLEDELAYEDEXPANSION并且直到脚本结束都没有禁用它,这会对脚本产生不良影响吗?


通常我会把那一行放在几乎所有的脚本顶部,它很少会引起问题,而且我喜欢它更好地处理转义字符的方式,尤其是在解析 XML 或 HTML 时。 - djangofan
@djangofan 你有考虑过使用AutoIt来完成这些任务吗?我强烈推荐它。我已经放弃了批处理/PowerShell/VBScript,转而使用AutoIt。 - Anthony Miller
2
是的,我听说过它。也许我会仔细看看。虽然我不喜欢需要第三方工具...但如果那是情况的话,似乎我最好使用PowerShell。 - djangofan
还有一个“SETLOCAL ENABLEEXTENSIONS”选项,但我不记得它有什么影响。 - djangofan
4个回答

137

我认为你应该了解延迟扩展是什么。现有的答案在我看来没有(充分)解释清楚。

键入SET /?可以很好地解释这个东西:

延迟环境变量扩展对于解决当前扩展的限制非常有用,当前扩展是在读取文本行时发生,而不是在执行它时发生。以下示例演示了立即变量扩展的问题:

set VAR=before
if "%VAR%" == "before" (
    set VAR=after
    if "%VAR%" == "after" @echo If you see this, it worked
)

由于两个IF语句中的%VAR%在第一个IF语句读取时就已经被替换,所以永远不会显示该消息,因为逻辑上包括IF体的复合语句。因此,复合语句内部的IF实际上是将“before”与“after”进行比较,这始终不会相等。同样,下面的示例也无法按预期工作:

set LIST=
for %i in (*) do set LIST=%LIST% %i
echo %LIST%

由于它不会在当前目录中建立文件列表,而是只将LIST变量设置为找到的最后一个文件。 这是因为%LIST%仅在读取FOR语句时扩展一次,并且此时LIST变量为空。因此我们执行的实际FOR循环是:

for %i in (*) do set LIST= %i

它只是不断将LIST设置为找到的最后一个文件。

延迟环境变量扩展允许您在执行时使用不同的字符(感叹号)来扩展环境变量。如果启用了延迟变量扩展,则可以按照以下方式编写上述示例以按预期工作:

set VAR=before
if "%VAR%" == "before" (
    set VAR=after
    if "!VAR!" == "after" @echo If you see this, it worked
)

set LIST=
for %i in (*) do set LIST=!LIST! %i
echo %LIST%
另一个例子是这个批处理文件:
@echo off
setlocal enabledelayedexpansion
set b=z1
for %%a in (x1 y1) do (
 set b=%%a
 echo !b:1=2!
)

这将打印x2y2:每个1都被替换为2。

如果没有setlocal enabledelayedexpansion,感叹号就只是它本身,因此它将两次回显 !b:1=2!

由于普通环境变量在读取(块)语句时会被展开,所以展开%b:1=2%使用循环之前b的值:z2(但未设置时为y2)。


2
明确一点,如果您的命令中没有感叹号,延迟扩展是无效的,对吗? - Kyle Delaney
3
好的!感叹号确实还有其他微妙的效果。 - Michel de Ruiter
在for循环中检查从另一个批处理文件返回的%errorlevel%时遇到了这个问题。在for循环之前添加了setlocal enabledelayedexpansion,并将检查从%errorlevel%更改为!errorlevel!,它就像魔法般地工作了。再次感谢您详细的解释! - LeoVannini

32

ENABLEDELAYEDEXPANSION是传递给SETLOCAL命令的一个参数(查看setlocal /?)。

它的作用在脚本的整个执行期间都有效,或者在执行ENDLOCAL命令时失效:

当批处理脚本到达结尾时,会为该批处理脚本发出的任何未完成的SETLOCAL命令执行隐含的ENDLOCAL

特别地,这意味着如果你在脚本中使用SETLOCAL ENABLEDELAYEDEXPANSION,除非你采取特殊措施,否则任何环境变量的更改都会在其结束时丢失


9
第二部分的问题怎么样? - Anthony Miller
只有在你启用它后,编写了一些行为不同的代码时,它才会产生负面影响。 - Alex K.
看起来不幸的是,似乎没有办法启用延迟扩展 而不 调用 setlocal,尽管它们控制着本应该是独立功能的内容。:-P - yoyo
我认为Alex K已经回答了这部分:ENDLOCAL被省略是因为当我们到达结尾时,SETLOCAL停止运行。我们程序员喜欢保持简短...但是不太冗长。 - WesternGun

11

在某些使用延迟扩展的程序中,ENABLEDELAYEDEXPANSION部分是必需的。这些程序会通过用感叹号将变量名括起来来获取在IF或FOR命令内修改过的变量值。

如果你在不需要延迟扩展的脚本中启用了此扩展,只有当脚本包含用感叹号括起来的名称,如!LIKE! !THESE!时,脚本行为会发生变化。通常名称会被删除,但如果恰好存在同名变量,则结果是不可预测的,取决于该变量的值和出现位置。

SETLOCAL部分在很少数专业(递归)程序中是必须的,但通常在想要确保不会偶然修改任何已存在变量的名称或者想要自动删除程序中使用的所有变量时使用。然而,因为没有单独的命令来启用延迟扩展,所以需要此功能的程序也必须包含SETLOCAL部分。


1
使用EnableDelayedExpansion时,即使您的文本仅包含单个“!”(如echo Hello!),您也会遇到问题。如果一行中有感叹号和插入符,例如echo "Caret^" is gone!,您也会遇到问题。 - jeb
@jeb 是的,我也曾经为此苦恼。我的解决方法是在脚本顶部添加 [setlocal enableextensions enabledelayedexpansion],并在进入一个将查看文件的 for 循环之前添加 [setlocal disabledelayedexpansion],例如 [for /r %%a in (.rar,.zip) do]。在 for 循环内部,我首先将 %%a 存储到一个变量中,例如 [set archive_path=%%a],如果需要进行任何目录修剪,则会在之前添加 [setlocal enabledelayedexpansion]。唯一值得称赞的是它能够正常工作,但在我看来,这是语言的失败。 - FocusedWolf
@FocusedWolf:你说得对,这个有点问题。你的解决方案听起来像是如何读取文件? - jeb
@jeb 是的,这是相同的方法。早些时候我注意到需要在循环内部使用endlocal,就像你的示例所示。这很奇怪,因为在某些测试中没有它不会引起任何问题,或者使其表现出奇怪的行为(具体影响%%~dpa字符串操作的原因),或者导致“达到最大setlocal递归级别”错误。 - FocusedWolf

0
一个真正的问题通常存在,因为任何在其中设置的变量在批处理文件结束时不会被导出。因此无法导出,这给我们造成了问题。因此,我只需将注册表设置为始终使用延迟扩展(我不知道为什么它不是默认值,可能是速度或遗留兼容性问题)。

5
多年使用批处理文件,我已经自动地将该指令添加到每一个批处理文件中,并且从未关闭过它。我不会将其默认启用于系统,因为你永远不知道系统运行的批处理脚本或其他公司的批处理脚本可能会与其冲突。我们现在说的是 Windows 嘛 ;) - Anthony Miller
@Mechaflash 说得好,但 @Rob 提到的问题是 setlocalenabledelayedexpansion 之间的紧密耦合。如果你想要一个影响环境的脚本,但又需要 enabledelayedexpansion,那么你需要采取一些 hack 的方法(比如临时脚本文件)来让它工作。 - Steve Hollasch

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