I'm using the following commands:
C:\>for %I in (a: b: c: ">:" "&:") do @rem %~fI
C:\>pushd c:
C:\>set "
and the output:
=&:=&:\
=>:=>:\
=A:=A:\
=B:=B:\
=C:=C:\
....
As the =Drive:
variables are storing the last accessed path the corresponding drive , it looks like the %~fI
expansion somehow accessed not existing drive (which is not possible) . (all parameter expansions create such variables)
When a modifier is used in the for
replaceable parameter to request a path element, the for
command (well, a function that retrieves the contents of the variables being read) uses the GetFullPathName
function to adapt the input string to something that could be handled. This API function (well, some of the OS base functions called by this API) generates the indicated behaviour when a relative path is requested. You can test this c code (sorry, just a quick code test), calling the executable with the ex. ;:
as the first argument.
#define _WIN32_WINNT 0x0500
#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 4096
int main(int argc, char **argv){
char buffer[BUFFER_SIZE];
DWORD ret;
LPTSTR lpszVariable;
LPTCH lpvEnv;
if (argc != 2) return 1;
if (0 == GetFullPathName( argv[1], BUFFER_SIZE, buffer, NULL )){
printf ("GetFullPathName failed (%d)\n", GetLastError());
return 2;
}
printf("current active directory: %s\r\n", buffer );
if (NULL == (lpvEnv = GetEnvironmentStrings())) {
printf("GetEnvironmentStrings failed (%d)\n", GetLastError());
return 3;
}
lpszVariable = (LPTSTR) lpvEnv;
while (*lpszVariable) {
if (lpszVariable[0]== '=') printf("%s\n", lpszVariable);
lpszVariable += lstrlen(lpszVariable) + 1;
}
FreeEnvironmentStrings(lpvEnv);
return 0;
}
to get something like
D:\>test ;:
current active directory: ;:\
=;:=;:\
=C:=C:\Windows\System32
=D:=D:\
=ExitCode=00000000
EDITED 2016/12/23
This is for windows 10, but as windows 7 behaves the same it should share the same or similar code.
The output of environment strings to console is handled by DisplayEnvVariable
function. In older windows versions (checked and XP did it this way) this function calls GetEnvironmentStrings
to retrive the values, but now (checked and in Vista it was changed) a pointer to a memory area is used. Somehow (sorry, at this moment I can not give this problem more time), it points to a non updated copy of the environment (in this case the updated was not generated by cmd
command, but from a base Rtl
function called when resolving the current drive path), generating the observed behaviour.
It is not necessary to execute a pushd
or cd
command, any change to the environment or any process creation will result in an update of the pointer.
@echo off
setlocal enableextensions disabledelayedexpansion
echo = before ------------------------------
set "
for %%a in ( ";:" ) do rem %%~fa
echo = after -------------------------------
set "
<nul >nul more
echo = after more --------------------------
set "
You can replace the more
line with a simple set "thisIsNotSet="
to get the same result
I think there are two things contributing:
A drive letter can actually be every character other than white-spaces, /
and \
. Check out the subst
command, which accepts also an &
, for example (although it is not listed by subst
):
C:\>subst X: C:\
C:\>subst ^&: C:\
C:\>subst
X:\: => C:\
C:\>X:
X:\>^&:
&:\>
for
does not access the file system unless it really needs to, which is the case when:
So the ~d
modifier, and also the corresponding part of ~f
, is handled by pure string manipulation as long as the file system is not accessed according to the aforementioned conditions.
Simply try your original code but with absolute paths, like for %I in (a:\ b:\ c:\ ">:\" "&:\") do @rem %~fI
, etc., and you will find that there are no corresponding =Drive:
variables.
Summary
Drive letters can literally be almost any characters (see subst
).
As soon as for
accesses the file system to search for drives and paths, the accessed drives are recorded in the =Drive:
variables.
Windows command interpreter tries to get real name of a file or directory on storage media on using an extension like %~fI
by processing a QueryDirectory
as it can be seen on using Sysinternals Process Monitor. But the loop variable I
just holds a string value as defined in set.
From a programmers point of view what should the command interpreter do for example on following code?
@echo off
pushd "%SystemRoot%\Temp"
del #abcdefghi.tmp 2>nul
for %%I in (#abcdefghi.tmp) do echo %%~fI
popd
There is most likely no file with name #abcdefghi.tmp
in directory for temporary files for system processes. But output is nevertheless
C:\Windows\Temp\#abcdefghi.tmp
The Windows command interpreter must built a string as the batch code expects a string. It can't replace %%~fI
with nothing or with an error message text as this would definitely result in an undefined behavior on further processing of the command lines in the batch file.
Exiting batch processing completely is also no option for Windows command interpreter because the FOR loop could be used to check for existence of files.
So the Windows command interpreter makes its best to build from current directory and current string of loop variable a valid file name with path, or just file path, or just file name, ... independent on existence of file/directory or validity of created string.
The command line user respectively writer of batch code has to check for validity or existence and not Windows command interpreter on expanding loop variable reference.
IMO the drives aren't really accessed but parsed at an early stage.
A simple for loop can be used to parse nonexistent drives\pathes\files
> cd
C:\Users\LotPings
> for %A in (\nonexistent) do @echo %~pnxA
\nonexistent
> for %A in (\nonexistent\a.b) do @echo %~pnxA
\nonexistent\a.b
> for %A in (\nonexistent\a.b) do @echo %~nxA
a.b
> for %A in (\nonexistent\a.b) do @echo %~fA
C:\nonexistent\a.b
> for %A in (^>\nonexistent\a.b) do @echo %~zA
ECHO ist eingeschaltet (ON).
> for %A in (^>\nonexistent\a.b) do @echo %~aA
ECHO ist eingeschaltet (ON).
> for %A in (^>\nonexistent\a.b) do @echo %~fA
C:\Users\LotPings\.\nonexistent\a.b
The very last one is quite interesting