Why “%~fI” parameter expansion is able to “access”

2019-03-20 01:07发布

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)

4条回答
三岁会撩人
2楼-- · 2019-03-20 01:30

I think there are two things contributing:

  1. 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:\>^&:
    
    &:\>        
    
  2. for does not access the file system unless it really needs to, which is the case when:

    • wildcards (like ?, *) are used in the set (where the file system needs to be accessed by the for command immediately);
    • the for reference (%I) expansion requires information from the file system:

      • for the modifiers ~s, ~a, ~t, ~z and ~$ENV:, information from the file system is required, of course;
      • for the modifiers ~n, ~x and ~p, and also the corresponding parts of ~f, which is nothing but ~dpnx, the file system is accessed for case preservation (if the path does not exist, the original case is maintained);
      • for the modifiers ~n, ~x, ~p and ~d, and also ~f, the file system needs to be accessed in case a relative path is provided, with or without a dedicated drive specified (for instance, abc\def, D:data, P:), because the current working directory path (of the given drive in case) needs to be determined;


    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.

查看更多
做自己的国王
3楼-- · 2019-03-20 01:30

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

查看更多
男人必须洒脱
4楼-- · 2019-03-20 01:43

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.

查看更多
Root(大扎)
5楼-- · 2019-03-20 01:44

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

查看更多
登录 后发表回答