在批处理文件中, setlocal enabledelayedexpansion
非常常见, 但是它的具体作用可能不是那么好理解. 虽然网上对它的作用的介绍很多, 但是感觉不是很准确. 所以写下这篇文章进行一下总结.
什么是 EnableDelayedExpansion?
它是 setlocal
的一个参数. setlocal
好理解, 就是设置本地环境变量, 也就是说只对本批处理脚本生效. 但是 EnableDelayedExpansion
是个难点, 直译过来应该是启用延迟展开
. 注意 Expansion
这里我们翻译为展开
我认为比较准确, 因为这里主要指的是对变量的展开
.
关键是怎么理解这个翻译. 首先我们来看一点例子:
@echo off
set var=0
set /a var=%var% + 1 && echo %var%
这里的输出应该是多少? 1
吗? 不对, 应该是0
. 可能有些批处理使用经验的人都遇到过类似的问题, 变量并没有如同想象中那样获得改变, 但是应该怎么处理却并不太清楚, 或者即使知道了怎么做却也并没有搞清楚为什么是这样.
要搞清楚这个问题, 接下来需要引入两个概念, parse time
和 execution 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 time
和 execution 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%
请自行分析原因.
写在最后
居然写了这么多字, 累死我了!