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).