I am trying to nest two forfiles
loops so that the command of the inner loop receives @
variables from both the outer and the inner loop iteration. For the latter the @
variable replacement needs to be escaped for the outer loop so that the inner forfiles
command receives the variable name.
I have got a code snippet that enumerates a given directory (C:\root
), and if the iterated item is a directory on its own, all the contained text files (*.txt
) are listed.
However, it does not work as expected: I tried to escape the expansion of @file
with \
, but it expands to the value of the outer loop:
2> nul forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE forfiles /P @path /M *.txt /C \"cmd /C echo @relpath -- \@file\""
Besides \@file
, also ^@file
, ^^@file
, ^^^@file
, 0x40file
(0x40
being the hexadecimal character code representation of @
) expand to the value of the outer forfiles
variable.
\\@file
and ^^^^@file
expand to the outer value too, with \
and ^
preceeded, respectively.
Even @^file
, @^^file
(see this post) does not work (the latter expands to @file
literally).
So: is there a way to escape the replacement of @
variables (like @file
,...) from the outer forfiles
loop so that the inner forfiles
iteration receives the literal variable name and expands it to its value?
I am working on Windows 7 64-bit.
Note:
The 2> nul
redirection should avoid numerous error messages ERROR: Files of type "*.txt" not found.
when no file in the currently processed directory matches the given mask (*.txt
).
Here's a workaround - forfiles is not a quick beast so any extra processing isn't going to matter much.
The trick is to use the hexadecimal character code replacement feature of
forfiles
in the format0xHH
, which can be nested on its own. In this context,00x7840
is used, hence the first (outer)forfiles
loop replaces the0x78
portion byx
, resulting in0x40
, which is in turn resolved by the second (inner)forfiles
loop by replacing it with@
.A simple
0x40
does not work asforfiles
replaces hexadecimal codes in a first pass and then it handles the@
variables in a second pass, so0x40file
will be replaced by@file
first and then expanded to the currently iterated item by the outerforfiles
loop both.The following command line walks through a given root directory and displays the relative path of each immediate sub-directory (iterated by the outer
forfiles
loop) and all text files found therein (iterated by the innerforfiles
loop):The output might look like (relative sub-directory paths left, text files right):
Explanation of Code:
00x7840file
portion hides the@file
variable name from the outerforfiles
command and transfers its replacement to the innerforfiles
command;"
andcmd /C
, quotes within the string after the/C
switch of the outerforfiles
are avoided by stating their hexadecimal code0x22
;(
forfiles
supports escaping quotes like\"
, howevercmd /C
does not care about the\
and so it detects the"
;0x22
has no special meaning tocmd
and so it is safe)if
statement checks whether the item enumerated by the outerforfiles
loop is a directory and, if not, skips the innerforfiles
loop;forfiles
returns an error message likeERROR: Files of type "*.txt" not found.
at STDERR; to avoid such messages, redirection2> nul
has been applied;Step-by-Step Replacement:
Here is the above command line again but with the redirection removed, just for demonstration:
We will now extract the nested command lines which are going to be executed one after another.
Taking the items of the first line of the above sample output (
.\data_dir -- "text_msg.txt"
), the command line executed by the outerforfiles
command is:So the inner
forfiles
command line looks like (cmd /C
removed, and theif
condition is fulfilled):Now the command line executed by the inner
forfiles
command is (notice the removed literal quotes around.\data_dir
and the instant replacement of0x40file
by the value of variable@file
):Walking though these steps from the innermost to the outermost command line like that, you could nest even more than two
forfiles
loops.Note:
All path- or file-name-related
@
-variables are replaced by quoted strings each; however, the above shown sample output does not contain any surrounding quotes for the directory paths; this is becauseforfiles
removes any literal (non-escaped) quotes"
from the string after the/C
switch; to get them back in the output here, replace@relpath
in the command line by00x7822@relpath00x7822
;\\\"@relpath\\\"
works too (but is not recommended though to not confusecmd
).Appendix:
Since
forfiles
is not an internal command, it should be possible to nest it without thecmd /C
prefix, likeforfiles /C "forfiles /M *"
, for instance (unless any additional internal or external command, command concatenation, redirection or piping is used, wherecmd /C
is mandatory).However, due to erroneous handling of command line arguments after the
/C
switch offorfiles
, you actually need to state it likeforfiles /C "forfiles forfiles /M *"
, so the innerforfiles
command doubled. Otherwise an error message (ERROR: Invalid argument/option
) is thrown.This best work-around has been found at this post: forfiles without cmd /c (scroll to the bottom).