在批处理文件中, setlocal enabledelayedexpansion 非常常见, 但是它的具体作用可能不是那么好理解. 虽然网上对它的作用的介绍很多, 但是感觉不是很准确. 所以写下这篇文章进行一下总结.


什么是 EnableDelayedExpansion?

它是 setlocal 的一个参数. setlocal 好理解, 就是设置本地环境变量, 也就是说只对本批处理脚本生效. 但是 EnableDelayedExpansion 是个难点, 直译过来应该是启用延迟展开. 注意 Expansion 这里我们翻译为展开我认为比较准确, 因为这里主要指的是对变量的展开.

关键是怎么理解这个翻译. 首先我们来看一点例子:

@echo off
set var=0
set /a var=%var% + 1 && echo %var%

这里的输出应该是多少? 1吗? 不对, 应该是0. 可能有些批处理使用经验的人都遇到过类似的问题, 变量并没有如同想象中那样获得改变, 但是应该怎么处理却并不太清楚, 或者即使知道了怎么做却也并没有搞清楚为什么是这样.

要搞清楚这个问题, 接下来需要引入两个概念, parse timeexecution time.


Parse time & Execution time

同样地, 直译过来分别是解析时执行时. 在批处理运行的过程中, 需要经历这两个阶段. 先是 parse time, 然后是 execution time. -- 参考连接

那么什么是 parse time 呢? 有过 C语言 使用经验的人应该知道 C语言 中具有一个预编译的概念. 在经过预编译以后, 代码才会被真正送入编译器翻译成汇编代码. 先来看看以下代码:

#define PI 3.14

int main() {
    int r = 3;
    printf("Perimeter is: %d", 2 * PI * r);
    return 0;
}

这段代码在预编译的时候就会发生一些变化, 其中的 PI 由于已经被 #define 定义为了3.14, 所以以上代码在经过与预编译以后, 所有的 PI 已经被直接替换为了3.14, 代码将会变成下面这个样子:

int main() {
    int r = 3;
    printf("Perimeter is: %d", 2 * 3.14 * r);
    return 0;
}

如果能理解上面的内容, 那么就能很容易理解 parse time 的作用. 与预编译类似地, 在 parse time 的时候, 批处理同样会做很多简单替换, 用当前已知的变量的值去替换这个变量, 也就是说把这个变量展开(所以知道我最开始要翻译为展开了吧).

所以来看我们的第一个例子:

@echo off
set var=0
set /a var=%var% + 1 && echo %var%

分析一下, 对于这个批处理, 我们首先会进入 parse time, 此时会对所有的变量进行简单替换, 所以我们的代码会变成如下的样子:

@echo off
set var=0
set /a var=0 + 1 && echo 0

由此可见, 输出的值必然是0而不会是1. 通过把代码直接翻译一下应该很好理解这个效果. 但是还有最后一个问题, 同样地先看一下代码:

@echo off
set var=0
set /a var=%var% + 1 && echo No.1 is: %var%
echo No.2 is: %var%

输出的结果应该是多少? 答案如下:

No.1 is: 0
No.2 is: 1

此时为什么 No.2的时候值又成功的发生了变化呢? 不是应该已经被替换了吗? 此时必须要提到一个概念: 逐行处理.


逐行处理

C语言 不同, 批处理的实质是一堆命令的批量处理, 简称批处理. 因为只是一堆命令, 命令只能按行执行, 它的处理原则是逐行处理, 以上提到的 parse timeexecution time 也是针对每一行来说的. C语言 中预编译的时候不管三七二十一, 直接对整个文件中的所有宏定义进行展开. 而批处理中只是在当前行的 parse time 的时候进行展开.

所以我们能看到, 先运行的第一行set var=0, 很好, 现在 %var% 等于0了.

然后进入第二行, set /a var=%var% + 1 && echo No.1 is: %var%, 在 "parse time" 的时候, 展开为set /a var=0 + 1 && echo No.1 is: 0, 所以能够输出值的是这句话echo No.1 is: 0, 所以输出了:

No.1 is: 0

但是同时也要注意对 %var% 的赋值是成功了的(set var=0 + 1), 所以此时 %var% 已经等于 1 了.

最后是第三行. 由于此时 %var% 已经是1, 所以展开为echo No.2 is: 1, 所以输出的结果是:

No.2 is: 1

在批处理中, 除了以上这种明显的是一行的情形外, 还有一些类似于 for 命令的特殊情况, 比如:

@echo off
set var=0
for %%i in (h,e,l,l,o) do (
    set /a var=%var% + 1
    set /a var=%var% + 1
    set /a var=%var% + 1
)
echo %var%

这个例子的输出将会是1而不会是其他值. 这里看起来 for 命令中好像分开了好几行书写, 但是由于是在括号的包裹中, 批处理只会认为这是一行, 仍然满足逐行处理的要求. 请自行分析其中输出结果是1的原因.


使用 EnableDelayedExpansion

其实看了上面这么多内容, 可能我们能发现, 这种替我们把变量展开的做法很多时候都不是我们所期望的效果. 因为一旦变量被展开后就不能发生改变了, 很多时候就不能正常起到一个变量应有的效果了. 那么怎么办呢, 使用 EnableDelayedExpansion, 延迟变量展开, 将本应在 parse time 完成的变量展开工作"延迟"到 execution time.

举几个例子, 我们想统计 word.txt 中就行有多少行内容, 我们写了如下代码:

@echo off
set var=0
for /f %%i in (word.txt) do (
    set /a var=%var% + 1
)
echo %var%

很显然, 通过之前的知识来分析, 最后只会输出一个1, 根本无法统计出正确的行数.

如果说想修改一下使之能够正确的统计, 那么需要将代码修改为如下形式:

@echo off
setlocal enabledelayedexpansion

set var=0
for /f %%i in (word.txt) do (
    set /a var=!var! + 1
)
echo %var%

这下试试, 是不是能正确统计了?

原因也很简单, 使用 setlocal enabledelayedexpansion 以后, 批处理中引用变量的方法就不再只有 %var% 一种了, 你还可以使用 !var! 的形式. 而如果你使用了 !var! 的形式, 变量的展开就被延迟到了 execution time, 也就是说在 parse time 的时候并不会被替换为一个具体的值, 而能够继续以一个变量的形式 !var! 进入到 execution time, 从而统计出每一次变化.

所以使用 setlocal enabledelayedexpansion 的效果就是让你能够使用 !var! 的形式引用变量, 而且对于这种方式引用的变量将不会在 parse time 中被展开, 而是被延迟execution time. 最后要注意的是即使你启用了 setlocal enabledelayedexpansion, 如果你使用老式的 %var% 引用方法的话, 变量依然会在 parse time 被展开.

例如, 即使你启用了 setlocal enabledelayedexpansion, 以下代码依然不能正确统计 word.txt 中的行数:

@echo off
setlocal enabledelayedexpansion

set var=0
for /f %%i in (word.txt) do (
    set /a var=%var% + 1
)
echo %var%

请自行分析原因.


写在最后

居然写了这么多字, 累死我了!

2016-08-13 15:1629