The trick is to use the hexadecimal character code replacement feature of forfiles
in the format 0xHH
, which can be nested on its own. In this context, 00x7840
is used, hence the first (outer) forfiles
loop replaces the 0x78
portion by x
, resulting in 0x40
, which is in turn resolved by the second (inner) forfiles
loop by replacing it with @
.
A simple 0x40
does not work as forfiles
replaces hexadecimal codes in a first pass and then it handles the @
variables in a second pass, so 0x40file
will be replaced by @file
first and then expanded to the currently iterated item by the outer forfiles
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 inner forfiles
loop):
2> nul forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE forfiles /P @path /M *.txt /C 0x22cmd /C echo @relpath -- 00x7840file0x22"
The output might look like (relative sub-directory paths left, text files right):
.\data_dir -- "text_msg.txt"
.\logs_dir -- "log_book.txt"
.\logs_dir -- "log_file.txt"
Explanation of Code:
- as described above, the
00x7840file
portion hides the @file
variable name from the outer forfiles
command and transfers its replacement to the inner forfiles
command;
- to avoid any trouble with quotes
"
and cmd /C
, quotes within the string after the /C
switch of the outer forfiles
are avoided by stating their hexadecimal code 0x22
;
(forfiles
supports escaping quotes like \"
, however cmd /C
does not care about the \
and so it detects the "
; 0x22
has no special meaning to cmd
and so it is safe)
- the
if
statement checks whether the item enumerated by the outer forfiles
loop is a directory and, if not, skips the inner forfiles
loop;
- in case the enumerated sub-directory does not contain any items that match the given pattern,
forfiles
returns an error message like ERROR: Files of type "*.txt" not found.
at STDERR; to avoid such messages, redirection 2> nul
has been applied;
Step-by-Step Replacement:
Here is the above command line again but with the redirection removed, just for demonstration:
forfiles /P "C:\root" /M "*" /C "cmd /C if @isdir==TRUE forfiles /P @path /M *.txt /C 0x22cmd /C echo @relpath -- 00x7840file0x22"
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 outer forfiles
command is:
cmd /C if TRUE==TRUE forfiles /P "C:\root" /M *.txt /C "cmd /C echo ".\data_dir" -- 0x40file"
So the inner forfiles
command line looks like (cmd /C
removed, and the if
condition is fulfilled):
forfiles /P "C:\root" /M *.txt /C "cmd /C echo ".\data_dir" -- 0x40file"
Now the command line executed by the inner forfiles
command is (notice the removed literal quotes around .\data_dir
and the instant replacement of 0x40file
by the value of variable @file
):
cmd /C echo .\data_dir -- "text_msg.txt"
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 because forfiles
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 by 00x7822@relpath00x7822
; \\\"@relpath\\\"
works too (but is not recommended though to not confuse cmd
).
Appendix:
Since forfiles
is not an internal command, it should be possible to nest it without the cmd /C
prefix, like forfiles /C "forfiles /M *"
, for instance (unless any additional internal or external command, command concatenation, redirection or piping is used, where cmd /C
is mandatory).
However, due to erroneous handling of command line arguments after the /C
switch of forfiles
, you actually need to state it like forfiles /C "forfiles forfiles /M *"
, so the inner forfiles
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).