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