如何正确嵌套两个“forfiles”循环(以便内部主体扩展两个迭代级别的变量)?

How to nest two `forfiles` loops properly (so that the inner body expands variables of both iteration levels)?

我试图嵌套两个 forfiles 循环,以便内循环的命令从外循环和内循环迭代接收 @ 变量。对于后者,需要为外部循环转义 @ 变量替换,以便内部 forfiles 命令接收变量名称。

我有一个枚举给定目录的代码片段 (C:\root),如果迭代项目本身就是一个目录,则列出所有包含的文本文件 (*.txt) .
但是,它没有按预期工作:我试图用 \ 来逃避 @file 的扩展,但它扩展到外循环的值:

2> nul forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE forfiles /P @path /M *.txt /C \"cmd /C echo @relpath -- \@file\""

除了\@file,还有^@file^^@file^^^@file0x40file0x40是[的十六进制字符编码表示=12=]) 扩展到外部 forfiles 变量的值。
\@file^^^^@file 也扩展到外部值,分别在 \^ 之前。
@^file@^^file(见)都不行(后者按字面意思展开为@file)。

那么:有没有一种方法可以避免从外部 forfiles 循环中替换 @ 变量(如 @file,...),以便内部 forfiles 迭代接收文字变量名并将其扩展为它的值?

我正在开发 Windows 7 64 位。


注:
当当前处理的目录中没有文件与给定掩码 (*.txt) 匹配时,2> nul 重定向应该避免大量错误消息 ERROR: Files of type "*.txt" not found.

这里有一个解决方法 - forfiles 不是一个快速的野兽,所以任何额外的处理都不会很重要。

@echo off
for /f "delims=" %%a in ('2^>nul forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE echo @path"') do forfiles /P %%a /M *.txt /C "cmd /C echo @relpath -- @file"
pause

技巧是在格式0xHH中使用forfiles的十六进制字符代码替换特性,可以自行嵌套。在这种情况下,使用 00x7840,因此第一个(外部)forfiles 循环将 0x78 部分替换为 x,导致 0x40,它在通过将其替换为 @.

由第二个(内部)forfiles 循环解决

一个简单的 0x40 工作,因为 forfiles 在第一遍中替换十六进制代码,然后 然后 它在第二遍中处理 @ 变量,因此 0x40file 将首先被 @file 替换,然后通过外部 forfiles 循环扩展到当前迭代的项目。


以下命令行遍历给定的根目录并显示每个直接子目录的相对路径(由外部 forfiles 循环迭代)和其中找到的所有文本文件(由内部 forfiles 循环迭代) =16=] 循环):

2> nul forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE forfiles /P @path /M *.txt /C 0x22cmd /C echo @relpath -- 00x7840file0x22"

输出可能如下所示(左侧为相对子目录路径,右侧为文本文件):

.\data_dir -- "text_msg.txt"

.\logs_dir -- "log_book.txt"
.\logs_dir -- "log_file.txt"

代码解释:

  • 如上所述,00x7840file部分隐藏了外部forfiles命令的@file变量名,并将其替换转移到内部forfiles命令;
  • 为避免引号 "cmd /C 出现任何问题,在外部 forfiles/C 开关之后的字符串中的引号通过声明其十六进制代码来避免0x22;
    forfiles 支持像 \" 这样的转义引号,但是 cmd /C 不关心 \ 所以它检测到 "0x22 没有对 cmd 有特殊意义,所以它是安全的)
  • if语句检查外forfiles循环枚举的项目是否是一个目录,如果不是,则跳过内forfiles循环;
  • 如果枚举的子目录不包含与给定模式匹配的任何项目,forfiles returns STDERR 中的 ERROR: Files of type "*.txt" not found. 错误消息;为避免此类消息,已应用重定向 2> nul

逐步替换:

这里还是上面的命令行,但是去掉了重定向,只是为了演示:

forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE forfiles /P @path /M *.txt /C 0x22cmd /C echo @relpath -- 00x7840file0x22"

我们现在将提取将要依次执行的嵌套命令行。

取上面示例输出第一行的项(.\data_dir -- "text_msg.txt"),外forfiles命令执行的命令行为:

cmd /C if TRUE==TRUE forfiles /P "C:\root" /M *.txt /C "cmd /C echo ".\data_dir" -- 0x40file"

因此内部 forfiles 命令行看起来像(cmd /C 已删除,并且满足 if 条件):

forfiles /P "C:\root" /M *.txt /C "cmd /C echo ".\data_dir" -- 0x40file"

现在由内部 forfiles 命令执行的命令行是(注意删除了 .\data_dir 周围的文字引号,并立即将 0x40file 替换为变量 @file):

cmd /C echo .\data_dir -- "text_msg.txt"

像这样从最内层到最外层的命令行遍历这些步骤,您甚至可以嵌套两个以上的 forfiles 循环。


注:

所有与路径或文件名相关的 @ 变量均由带引号的字符串替换;但是,上面显示的示例输出不包含目录路径的任何引号;这是因为 forfiles/C 开关之后的字符串中删除了任何文字(非转义)引号 ";要将它们返回到此处的输出中,请将命令行中的 @relpath 替换为 00x7822@relpath00x7822\\"@relpath\\" 也有效(但不推荐使用,以免混淆 cmd)。


附录:

因为 forfiles 不是内部命令,应该可以在没有 cmd /C 前缀的情况下嵌套它,例如 forfiles /C "forfiles /M *"(除非有任何额外的内部或外部命令,使用命令串联、重定向或管道,其中 cmd /C 是必需的)。
但是,由于在 forfiles/C 开关之后对命令行参数的错误处理,您实际上需要像 forfiles /C "forfiles forfiles /M *" 一样声明它,因此内部 forfiles 命令加倍。否则会抛出一条错误消息 (ERROR: Invalid argument/option)。
最佳解决方法已在 post 中找到:forfiles without cmd /c(滚动到底部)。