Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why "%~fI" parameter expansion is able to "access" not existing drives?

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)

like image 357
npocmaka Avatar asked Dec 21 '16 15:12

npocmaka


2 Answers

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

like image 103
MC ND Avatar answered Nov 16 '22 22:11

MC ND


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.

like image 38
aschipfl Avatar answered Nov 16 '22 22:11

aschipfl