返回

CMD批处理循环中感叹号(!)处理及延迟扩展详解

windows

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后的变量值,也必须输出!时, 可以使用“跳出”技巧: 用FORCALL 模拟一层子环境,跳过感叹号被错误展开的影响。

方法 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

核心思想:

  1. CALL :label ...: CALL 命令在调用子程序:label之前,会再次 对参数进行解析.

  2. %%的转义:FOR 循环中,单个%有特殊含义(用于循环变量)。如果你写%game_name%,它尝试展开成环境变量game_name的值,而不是字面量 %game_name%。 所以我们要用两个%%来表示一个%, %%game_name%% 就会被解析成 %game_name%.

  3. 子过程处理 : 将参数传给子过程, 在: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 的这个感叹号问题, 多半还是因为变量展开的机制导致。 理解了延迟变量和正常变量的区别, 问题就能迎刃而解. 遇到其他类似情况的时候,思路是一样的,关键还是控制好什么时候进行变量展开.