CMD批处理循环中感叹号(!)处理及延迟扩展详解
2025-03-20 13:33:15
CMD 批处理中循环与延迟扩展下感叹号 (!) 的处理
最近遇到个事儿,处理文件名的时候死活打印不出感叹号 (!),这可把我给整懵了。 原来是 CMD 批处理里,循环和延迟扩展搅和在一起,感叹号就成精了。 今天这篇儿就聊聊这茬,怎么把这个感叹号给揪出来。
一、问题出在哪儿?
批处理文件在处理包含感叹号 !
的文件名时出现问题,主要和 CMD 的变量延迟扩展(Delayed Expansion)特性有关。 看下面这段代码:
SETLOCAL EnableDelayedExpansion
FOR /f "tokens=*" %%A IN ('dir /B "%covers_dump_full_path%" ^| findstr /V /I /C:".html"') DO (
SET "game_name=%%A"
FOR %%F IN ("%%A") DO SET "game_name_no_ext=%%~NF"
ECHO !game_name!
)
原意是想用!game_name!
直接显示带感叹号的文件名, 实际却事与愿违。
核心原因是啥? 当启用延迟扩展后,CMD 会在执行整行命令之前 将!
之间的变量替换为其当前值。 但在循环体内, 如果你像例子一样用 SET 设置变量后立刻用 !var! 获取其内容, 得到的变量通常还是循环之前 的值. 这就是循环的"滞后性".
具体到感叹号,由于它成了变量引用的标识符, 批处理反倒会尝试把它作为变量名的一部分来解释, 这当然不对. 所以最后输出文件名, 要么是没感叹号, 要么是变量展开异常导致空输出或乱七八糟的内容.
"Maximum setlocal recursion level reached." 这个错误也经常会冒出来. 它常常和在循环中 Enable/Disable Delayed Expansion 有关. 如果处理不当,每次循环迭代都可能创建新的本地环境, 最终把“栈”给撑爆了.
二、解决办法
下面介绍几个法子, 让你摆脱这个困扰:
1. 不用延迟扩展 (简单粗暴)
如果可以, 尽量不用延迟扩展, 这是最省事的。 前提是你脚本的功能不需要立即获取循环内设置变量的值。
针对一开始的代码, 如果你只是想把文件名打出来, 可以把ECHO !game_name!
改成ECHO %%A
:
SETLOCAL EnableDelayedExpansion
FOR /f "tokens=*" %%A IN ('dir /B "%covers_dump_full_path%" ^| findstr /V /I /C:".html"') DO (
SET "game_name=%%A"
FOR %%F IN ("%%A") DO SET "game_name_no_ext=%%~NF"
ECHO %%A
)
ENDLOCAL
%%A
直接包含了文件名, 它没啥毛病, 就没感叹号的事儿了。 适用场景是单纯的输出文件名,或后续操作不用立即使用刚 SET 的变量值.
2. "跳出" 延迟扩展
当既需要循环, 又需要立刻用SET
后的变量值,也必须输出!
时, 可以使用“跳出”技巧: 用FOR
或CALL
模拟一层子环境,跳过感叹号被错误展开的影响。
方法 2.1: FOR 循环
把需要用到感叹号的操作,放到一个内嵌FOR
循环中,通过变量传递值:
SETLOCAL EnableDelayedExpansion
FOR /f "tokens=*" %%A IN ('dir /B "%covers_dump_full_path%" ^| findstr /V /I /C:".html"') DO (
SET "game_name=%%A"
FOR %%F IN ("%%A") DO SET "game_name_no_ext=%%~NF"
FOR %%B IN ("!game_name!") DO (
IF !current_column! == 1 (
ECHO ^<tr^>>> "%game_html%"
)
ECHO ^<td align="center"^> ^<img src="%%~B" width=300 height=250^> ^<br^> ^<b^> !game_name_no_ext! ^<br^> ^</b^> ^</td^>>> "%game_html%"
IF !current_column! == !max_columns! (
ECHO ^</tr^>>> "%game_html%"
SET /a current_column=0
)
SET /a current_column=!current_column!+1
)
)
ENDLOCAL
在这个例子里,通过FOR %%B IN ("!game_name!")
, 将!game_name!
的值传递给%%B
, %%~B
会进行正常扩展, 感叹号原样保留,而不会出幺蛾子。
后续可以用%%~B
来访问正确的文件名, 包含了!
. 注意到这里的 !game_name_no_ext!
还是可以正确展开, 因为这是另外一个独立的"环境变量"。
方法 2.2: CALL 命令 + %%
转义
这个技巧更精妙,也更常用:
@echo off
SETLOCAL EnableDelayedExpansion
FOR /f "tokens=*" %%A IN ('dir /B . ^| findstr /V /I /C:".html"') DO (
SET "game_name=%%A"
FOR %%F IN ("%%A") DO SET "game_name_no_ext=%%~NF"
CALL :display "%%game_name%%" "%%game_name_no_ext%%"
)
ENDLOCAL
goto :eof
:display
if %current_column% == 1 (
ECHO ^<tr^>>> "%game_html%"
)
ECHO ^<td align="center"^> ^<img src="%~1" width=300 height=250^> ^<br^> ^<b^> %~2 ^<br^> ^</b^> ^</td^>>> "%game_html%"
if %current_column% == %max_columns% (
ECHO ^</tr^>>> "%game_html%"
SET /a current_column=0
)
SET /a current_column=current_column+1
goto :eof
核心思想:
-
CALL :label ...
:CALL
命令在调用子程序:label
之前,会再次 对参数进行解析. -
%%
的转义: 在FOR
循环中,单个%
有特殊含义(用于循环变量)。如果你写%game_name%
,它尝试展开成环境变量game_name
的值,而不是字面量%game_name%
。 所以我们要用两个%%
来表示一个%
,%%game_name%%
就会被解析成%game_name%
. -
子过程处理 : 将参数传给子过程, 在
:display
这个“子程序”中,%~1
和%~2
获取到的参数中是含有!
的.
通过以上几步, 就避免了在FOR
循环里, 延迟变量扩展的副作用。
3. 禁用延迟扩展 + CALL
这是第二种方法的另一种变体。 相对来说更加容易控制, 也很推荐.
SETLOCAL DisableDelayedExpansion
FOR /f "tokens=*" %%A IN ('dir /B . ^| findstr /V /I /C:".html"') DO (
SET "game_name=%%A"
FOR %%F IN ("%%A") DO SET "game_name_no_ext=%%~NF"
SETLOCAL EnableDelayedExpansion
CALL :display "%%game_name%%" "!game_name_no_ext!"
ENDLOCAL
)
goto :eof
:display
ECHO ^<td align="center"^> ^<img src="%~1" width=300 height=250^> ^<br^> ^<b^> %~2 ^<br^> ^</b^> ^</td^>>> "%game_html%"
if %current_column% == %max_columns% (
ECHO ^</tr^>>> "%game_html%"
SET /a current_column=0
)
SET /a current_column=current_column+1
goto :eof
在这个代码中, 外层先关闭延迟扩展, 这样对主循环没啥影响。
当进入:display
之前的一瞬间, 用 SETLOCAL EnableDelayedExpansion
只为本次 CALL
开启, 再ENDLOCAL
关闭.
安全提示
- 当处理来自外部或用户输入的文件名时,要小心,避免路径遍历攻击. 虽然批处理的安全性问题相对较少,但多留心没坏处.
总而言之, CMD 的这个感叹号问题, 多半还是因为变量展开的机制导致。 理解了延迟变量和正常变量的区别, 问题就能迎刃而解. 遇到其他类似情况的时候,思路是一样的,关键还是控制好什么时候进行变量展开.