`forfiles` 在什么时候枚举一个目录(树)?
At which point does `forfiles` enumerate a directory (tree)?
命令 forfiles
用于枚举目录并对每个项目应用 (a) 某些命令。使用 /S
可以对完整的目录树完成相同的操作。
当 forfiles
命令正文中的命令更改枚举目录(树)的内容时会发生什么?
假设我们有包含以下内容的目录 D:\data
:
file1.txt
file2.txt
file3.txt
在上述目录中执行forfiles /P "D:\data" /M "*.txt" /C "cmd /C echo @file"
的输出将明显反映上述列表。
但是,当正文中的命令修改目录内容时,forfiles
的输出是什么?例如,列表中的一个文件在实际迭代之前被删除,比方说 file3.txt
?或者如果在循环完成之前创建了一个新文件,例如 file4.txt
?
forfiles /S
在这种情况下如何表现?假设有几个子目录sub1
、sub2
、sub3
,每个子目录都包含上面的文件列表; forfiles /S
当前正在遍历 sub2
,sub1
已经处理,但 sub3
尚未处理; sub1
和 sub3
的内容在该点发生了变化(如前所述,当前遍历 sub2
时);那么会列举什么呢?我猜,sub1
的内容变化不会被识别,但是sub3
呢?
我主要对 forfiles
自 Windows Vista 以来的行为感兴趣。
注:
我已经 post 编辑了 关于 for
命令的内容。但是,由于 forfiles
不是内置命令并且具有完全不同的语法,我决定 post 一个单独的问题而不是扩展另一个问题的范围。
一旦您尝试使用解析为不存在文件的@变量,forfiles
将无法继续枚举带有 ERROR: The system cannot find the file specified.
的重命名文件夹。删除的文件不会有错误,如果它的名称遵循当前使用的枚举顺序的当前处理的文件,它将看到一个新添加的文件(我已经使用默认字母排序按升序对其进行了测试)。所以很明显它不会在执行命令之前构建整个文件列表,而是在自定义命令完成后一个一个地枚举它们。
根据您需要对 forfiles
执行的操作,可靠的解决方案是在仅列表模式下解析 dir /s /b
或 robocopy
的输出。因此,您可以确保在任何更改之前生成列表。
for /f "delims=" %%a in ('dir "d:\data\*.txt" /s /b') do .......
适用于简单枚举
for /f "tokens=*" %%a in ('robocopy /L /njh /njs /ndl ........') do ...
适用于更复杂的场景,如限制日期跨度,可能需要使用额外的解析和 /v
在非简单的情况下。
我用 forfiles
做了一些测试——这是结果...
目的和范围
此处的测试用例旨在证明 forfiles
是否在迭代所有(子)项之前完成给定目录(树)的枚举。
下面的列表显示了此处测试涵盖的模式:
- 文件模式 (
/M
) 总是 *.txt
;
- 文件模式 (
/M
) 只匹配文件,不匹配目录;
- 总有一个根搜索路径给定 (
/P
);
- 在正文 (
/C
) 中仅使用内部 cmd.exe
命令(前缀为 cmd /C
);
- 非递归操作迭代几个和一百个文件;
- 递归操作(
/S
)只迭代几个目录;
- 递归操作(
/S
)遍历一层深度的目录层次结构;
- 文件年龄过滤器 (
/D
) 根本没有使用;
- 目录(树)内容仅在特定迭代期间修改一次;
- 文件(内容)未被修改,因此未检测到大小和 date/time 更改;
测试设置
所有测试均在NTFS格式磁盘上进行。 (这可能是所有文件都按字母顺序 forfiles
枚举的原因。)
操作系统为 Microsoft Windows 7 64 位(版本 6.1.7601)。
先决条件
在执行各个命令行之前,需要提前促进各个测试步骤中描述的所需目录树。
使用的根目录 D:\Data
.
中不得存在任何其他文件或目录
forfiles /S
,递归
这里的测试用例需要建立如下目录树:
D:\Data\
+---sub1\
| file1.txt
| file2.txt
| file3.txt
+---sub2\
| file1.txt
| file2.txt
| file3.txt
+---sub3\
| file1.txt
| file2.txt
| file3.txt
+---sub4\
| file1.txt
| file2.txt
| file3.txt
+---sub5\
file1.txt
file2.txt
file3.txt
我使用了以下几行代码来设置它:
@(pushd D:\Data
md sub1 & pushd sub1 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub2 & pushd sub2 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub3 & pushd sub3 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub4 & pushd sub4 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub5 & pushd sub5 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
rd /S /Q sub6
popd) > nul 2>&1
我的意图是等到文件夹sub3
中的项目file2.txt
被迭代,然后完成以下任务:
- in
sub3
,当前正在迭代,
- 删除
file1.txt
(已迭代);
- 删除
file3.txt
(尚未迭代);
- 创建
file4.txt
(新项目,因此尚未迭代);
- 删除容器
sub1
(已迭代);
- 删除容器
sub4
(尚未迭代);
- 通过将
file2.txt
重命名为 file4.txt
来更改 sub2
的内容(已迭代);
- 通过将
file2.txt
重命名为 file4.txt
来更改 sub5
的内容(尚未迭代);
- 创建容器
sub6
(新项目,因此尚未迭代),在里面创建file4.txt
;
对于所有迭代项,完整路径会回显到命令提示符。
如果在遍历所有项目之前完成枚举,则应输出原始目录树,因此应显示 none 的修改。
现在让我们看看会发生什么;这是要执行的命令行:
forfiles /S /P "D:\Data" /M "*.txt" /C "cmd /C (if @relpath==\".\sub3\file2.txt\" (del file1.txt & del file3.txt & rem.> file4.txt & rd /S /Q ..\sub1 & rd /S /Q ..\sub4 & ren ..\sub2\file2.txt file4.txt & ren ..\sub5\file2.txt file4.txt & md ..\sub6 & rem.> ..\sub6\file4.txt)) & echo @path"
输出如下:
"D:\Data\sub1\file1.txt"
"D:\Data\sub1\file2.txt"
"D:\Data\sub1\file3.txt"
"D:\Data\sub2\file1.txt"
"D:\Data\sub2\file2.txt"
"D:\Data\sub2\file3.txt"
"D:\Data\sub3\file1.txt"
"D:\Data\sub3\file2.txt"
"D:\Data\sub3\file3.txt"
ERROR: The system cannot find the file specified.
"D:\Data\sub5\file1.txt"
"D:\Data\sub5\file3.txt"
"D:\Data\sub5\file4.txt"
我们可以清楚地看到,这显然不是原来的目录树。
似乎树中的目录是在迭代之前枚举的,但是迭代到达那里后会立即枚举每个目录内容。 (至少对于手头的小树来说是这样;但是,在迭代之前,可能无法完全枚举具有高层次结构深度的大树的目录。)
sub1
的删除和sub2
内容的修改没有注意到。一旦达到 sub4
,就会返回一个错误,因为在遍历 sub3
期间,sub4
已被删除。检测到sub5
的内容被修改。 sub6
是在遍历 sub3
期间创建的,根本无法识别。
forfiles
,非递归
对于没有/S
选项的forfiles
,使用平面目录树:
D:\Data\
file1.txt
file2.txt
file3.txt
这是使用以下代码片段创建的:
@(pushd D:\Data
rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt
popd) > nul 2>&1
为了测试,forfiles
体中的命令行检查当前文件是否为file2.txt
;如果是,则删除 file1.txt
和 file3.txt
,并创建新的 file4.txt
。当前文件回显到命令提示符。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file2\" (del file1.txt & del file3.txt & rem.> file4.txt)) & echo @file"
输出为:
"file1.txt"
"file2.txt"
"file3.txt"
这表示在遍历文件之前已经枚举了整个目录内容。
但是,为了证明上述假设,让我们进行一些更深入的测试。
这次我们使用一百个文件:
D:\Data\
file0.txt
file1.txt
file2.txt
...
file99.txt
这些是使用以下代码创建的:
@(pushd D:\Data
del file100.txt & del file999.txt
for /L %%N in (0,1,99) do (echo.%%N> file%%N.txt)
popd) > nul 2>&1
在这个实验中,我们在 file1.txt
迭代后立即将 file99.txt
重命名为 file999.txt
。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file1\" (ren file99.txt file999.txt)) & echo @file"
输出为:
"file0.txt"
"file1.txt"
"file10.txt"
"file11.txt"
...
"file98.txt"
"file999.txt"
我们收到反映重命名的 100 个文件的列表,这意味着我们不读取原始文件列表。因此枚举在迭代开始之前没有完成。
这里我们再次使用上面的100个文件
在这个实验中,我们在 file1.txt
迭代后立即将 file99.txt
重命名为 file100.txt
。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file1\" (ren file99.txt file100.txt)) & echo @file"
输出为:
"file0.txt"
"file1.txt"
"file10.txt"
"file11.txt"
...
"file98.txt"
所以现在我们收到一个只有 99 个文件的列表,没有 file99.txt
和 file100.txt
。似乎最后一个文件的枚举是在文件重命名后完成的,但是 file100.txt
没有显示,因为它会违反字母顺序(它应该出现在 file10.txt
之后,但那个地方附近的文件似乎已经列举过了)。
我们再次使用上面的 100 个文件。
在这个实验中,我们在 file1.txt
迭代后立即将 file0.txt
重命名为 file999.txt
。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file1\" (ren file0.txt file999.txt)) & echo @file"
输出为:
"file0.txt"
"file1.txt"
"file10.txt"
"file11.txt"
...
"file98.txt"
"file99.txt"
"file999.txt"
所以现在我们收到一个包含 101 个文件的列表,其中包含 file0.txt
和 file999.txt
。似乎 file0.txt
在重命名之前已经被枚举,但最后的文件还没有,所以 file999.txt
也出现在列表中。
结论
显然,forfiles
不会 在遍历所有(匹配的)项目之前枚举整个目录(树)。
似乎有一种缓冲区,其中枚举了一些项目,一旦迭代需要更多数据,枚举就会继续下一部分,依此类推,直到结束。
命令 forfiles
用于枚举目录并对每个项目应用 (a) 某些命令。使用 /S
可以对完整的目录树完成相同的操作。
当 forfiles
命令正文中的命令更改枚举目录(树)的内容时会发生什么?
假设我们有包含以下内容的目录 D:\data
:
file1.txt
file2.txt
file3.txt
在上述目录中执行forfiles /P "D:\data" /M "*.txt" /C "cmd /C echo @file"
的输出将明显反映上述列表。
但是,当正文中的命令修改目录内容时,forfiles
的输出是什么?例如,列表中的一个文件在实际迭代之前被删除,比方说 file3.txt
?或者如果在循环完成之前创建了一个新文件,例如 file4.txt
?
forfiles /S
在这种情况下如何表现?假设有几个子目录sub1
、sub2
、sub3
,每个子目录都包含上面的文件列表; forfiles /S
当前正在遍历 sub2
,sub1
已经处理,但 sub3
尚未处理; sub1
和 sub3
的内容在该点发生了变化(如前所述,当前遍历 sub2
时);那么会列举什么呢?我猜,sub1
的内容变化不会被识别,但是sub3
呢?
我主要对 forfiles
自 Windows Vista 以来的行为感兴趣。
注:
我已经 post 编辑了 for
命令的内容。但是,由于 forfiles
不是内置命令并且具有完全不同的语法,我决定 post 一个单独的问题而不是扩展另一个问题的范围。
forfiles
将无法继续枚举带有 ERROR: The system cannot find the file specified.
的重命名文件夹。删除的文件不会有错误,如果它的名称遵循当前使用的枚举顺序的当前处理的文件,它将看到一个新添加的文件(我已经使用默认字母排序按升序对其进行了测试)。所以很明显它不会在执行命令之前构建整个文件列表,而是在自定义命令完成后一个一个地枚举它们。
根据您需要对 forfiles
执行的操作,可靠的解决方案是在仅列表模式下解析 dir /s /b
或 robocopy
的输出。因此,您可以确保在任何更改之前生成列表。
for /f "delims=" %%a in ('dir "d:\data\*.txt" /s /b') do .......
适用于简单枚举for /f "tokens=*" %%a in ('robocopy /L /njh /njs /ndl ........') do ...
适用于更复杂的场景,如限制日期跨度,可能需要使用额外的解析和/v
在非简单的情况下。
我用 forfiles
做了一些测试——这是结果...
目的和范围
此处的测试用例旨在证明 forfiles
是否在迭代所有(子)项之前完成给定目录(树)的枚举。
下面的列表显示了此处测试涵盖的模式:
- 文件模式 (
/M
) 总是*.txt
; - 文件模式 (
/M
) 只匹配文件,不匹配目录; - 总有一个根搜索路径给定 (
/P
); - 在正文 (
/C
) 中仅使用内部cmd.exe
命令(前缀为cmd /C
); - 非递归操作迭代几个和一百个文件;
- 递归操作(
/S
)只迭代几个目录; - 递归操作(
/S
)遍历一层深度的目录层次结构; - 文件年龄过滤器 (
/D
) 根本没有使用; - 目录(树)内容仅在特定迭代期间修改一次;
- 文件(内容)未被修改,因此未检测到大小和 date/time 更改;
测试设置
所有测试均在NTFS格式磁盘上进行。 (这可能是所有文件都按字母顺序 forfiles
枚举的原因。)
操作系统为 Microsoft Windows 7 64 位(版本 6.1.7601)。
先决条件
在执行各个命令行之前,需要提前促进各个测试步骤中描述的所需目录树。
使用的根目录 D:\Data
.
forfiles /S
,递归
这里的测试用例需要建立如下目录树:
D:\Data\
+---sub1\
| file1.txt
| file2.txt
| file3.txt
+---sub2\
| file1.txt
| file2.txt
| file3.txt
+---sub3\
| file1.txt
| file2.txt
| file3.txt
+---sub4\
| file1.txt
| file2.txt
| file3.txt
+---sub5\
file1.txt
file2.txt
file3.txt
我使用了以下几行代码来设置它:
@(pushd D:\Data
md sub1 & pushd sub1 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub2 & pushd sub2 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub3 & pushd sub3 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub4 & pushd sub4 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
md sub5 & pushd sub5 & rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt & popd
rd /S /Q sub6
popd) > nul 2>&1
我的意图是等到文件夹sub3
中的项目file2.txt
被迭代,然后完成以下任务:
- in
sub3
,当前正在迭代,- 删除
file1.txt
(已迭代); - 删除
file3.txt
(尚未迭代); - 创建
file4.txt
(新项目,因此尚未迭代);
- 删除
- 删除容器
sub1
(已迭代); - 删除容器
sub4
(尚未迭代); - 通过将
file2.txt
重命名为file4.txt
来更改sub2
的内容(已迭代); - 通过将
file2.txt
重命名为file4.txt
来更改sub5
的内容(尚未迭代); - 创建容器
sub6
(新项目,因此尚未迭代),在里面创建file4.txt
;
对于所有迭代项,完整路径会回显到命令提示符。
如果在遍历所有项目之前完成枚举,则应输出原始目录树,因此应显示 none 的修改。
现在让我们看看会发生什么;这是要执行的命令行:
forfiles /S /P "D:\Data" /M "*.txt" /C "cmd /C (if @relpath==\".\sub3\file2.txt\" (del file1.txt & del file3.txt & rem.> file4.txt & rd /S /Q ..\sub1 & rd /S /Q ..\sub4 & ren ..\sub2\file2.txt file4.txt & ren ..\sub5\file2.txt file4.txt & md ..\sub6 & rem.> ..\sub6\file4.txt)) & echo @path"
输出如下:
"D:\Data\sub1\file1.txt"
"D:\Data\sub1\file2.txt"
"D:\Data\sub1\file3.txt"
"D:\Data\sub2\file1.txt"
"D:\Data\sub2\file2.txt"
"D:\Data\sub2\file3.txt"
"D:\Data\sub3\file1.txt"
"D:\Data\sub3\file2.txt"
"D:\Data\sub3\file3.txt"
ERROR: The system cannot find the file specified.
"D:\Data\sub5\file1.txt"
"D:\Data\sub5\file3.txt"
"D:\Data\sub5\file4.txt"
我们可以清楚地看到,这显然不是原来的目录树。
似乎树中的目录是在迭代之前枚举的,但是迭代到达那里后会立即枚举每个目录内容。 (至少对于手头的小树来说是这样;但是,在迭代之前,可能无法完全枚举具有高层次结构深度的大树的目录。)
sub1
的删除和sub2
内容的修改没有注意到。一旦达到 sub4
,就会返回一个错误,因为在遍历 sub3
期间,sub4
已被删除。检测到sub5
的内容被修改。 sub6
是在遍历 sub3
期间创建的,根本无法识别。
forfiles
,非递归
对于没有/S
选项的forfiles
,使用平面目录树:
D:\Data\
file1.txt
file2.txt
file3.txt
这是使用以下代码片段创建的:
@(pushd D:\Data
rem.> file1.txt & rem.> file2.txt & rem.> file3.txt & del file4.txt
popd) > nul 2>&1
为了测试,forfiles
体中的命令行检查当前文件是否为file2.txt
;如果是,则删除 file1.txt
和 file3.txt
,并创建新的 file4.txt
。当前文件回显到命令提示符。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file2\" (del file1.txt & del file3.txt & rem.> file4.txt)) & echo @file"
输出为:
"file1.txt"
"file2.txt"
"file3.txt"
这表示在遍历文件之前已经枚举了整个目录内容。
但是,为了证明上述假设,让我们进行一些更深入的测试。
这次我们使用一百个文件:
D:\Data\
file0.txt
file1.txt
file2.txt
...
file99.txt
这些是使用以下代码创建的:
@(pushd D:\Data
del file100.txt & del file999.txt
for /L %%N in (0,1,99) do (echo.%%N> file%%N.txt)
popd) > nul 2>&1
在这个实验中,我们在 file1.txt
迭代后立即将 file99.txt
重命名为 file999.txt
。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file1\" (ren file99.txt file999.txt)) & echo @file"
输出为:
"file0.txt"
"file1.txt"
"file10.txt"
"file11.txt"
...
"file98.txt"
"file999.txt"
我们收到反映重命名的 100 个文件的列表,这意味着我们不读取原始文件列表。因此枚举在迭代开始之前没有完成。
这里我们再次使用上面的100个文件
在这个实验中,我们在 file1.txt
迭代后立即将 file99.txt
重命名为 file100.txt
。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file1\" (ren file99.txt file100.txt)) & echo @file"
输出为:
"file0.txt"
"file1.txt"
"file10.txt"
"file11.txt"
...
"file98.txt"
所以现在我们收到一个只有 99 个文件的列表,没有 file99.txt
和 file100.txt
。似乎最后一个文件的枚举是在文件重命名后完成的,但是 file100.txt
没有显示,因为它会违反字母顺序(它应该出现在 file10.txt
之后,但那个地方附近的文件似乎已经列举过了)。
我们再次使用上面的 100 个文件。
在这个实验中,我们在 file1.txt
迭代后立即将 file0.txt
重命名为 file999.txt
。
要执行的命令行是:
forfiles /P "D:\Data" /M "*.txt" /C "cmd /C (if @fname==\"file1\" (ren file0.txt file999.txt)) & echo @file"
输出为:
"file0.txt"
"file1.txt"
"file10.txt"
"file11.txt"
...
"file98.txt"
"file99.txt"
"file999.txt"
所以现在我们收到一个包含 101 个文件的列表,其中包含 file0.txt
和 file999.txt
。似乎 file0.txt
在重命名之前已经被枚举,但最后的文件还没有,所以 file999.txt
也出现在列表中。
结论
显然,forfiles
不会 在遍历所有(匹配的)项目之前枚举整个目录(树)。
似乎有一种缓冲区,其中枚举了一些项目,一旦迭代需要更多数据,枚举就会继续下一部分,依此类推,直到结束。