Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CMD: failure of %~d0 when CALL quotes the name of the batch file

Why the following failure of %~d0 to return the batch file's drive letter S: when CALL quotes the name of the batch file?

S:\!DJ DAP>type test.bat
R:
%~d0

S:\!DJ DAP>call test.bat

S:\!DJ DAP>R:

R:\>S:

S:\!DJ DAP>call "test.bat"

S:\!DJ DAP>R:

R:\>R:

R:\>

EDIT following responses from Jerry and MC: Here's a non-CALL example showing the same:

R:\>s:

S:\!DJ DAP>type test.bat
R:
%~d0

S:\!DJ DAP>test.bat

S:\!DJ DAP>R:

R:\>S:

S:\!DJ DAP>"test.bat"

S:\!DJ DAP>R:

R:\>R:

R:\>
like image 582
ChrisJJ Avatar asked Nov 05 '13 03:11

ChrisJJ


2 Answers

EDIT - npocmaka, you are right. Strange.

Original answer removed - I was wrong.

But problem is not the call command. The problem are the quotes and cmd.

After testing, it seems more a bug/feature in how filenames are processed and how cmd are handling some errors in api calls.

With the following batch file (test.cmd)

@echo off
    setlocal enableextensions

    echo Calling subroutine from drive d:
    call :getInfo
    echo.

    c:
    echo Calling subroutine from drive c:
    call :getInfo
    echo.

    echo Getting data directly without subroutine

:getInfo
    echo ---------------------------------------------------------
    echo cd    : %cd%
    echo d0    : %~d0
    echo dp0   : %~dp0
    echo f0    : %~f0
    echo ---------------------------------------------------------
    echo.
    goto :EOF

placed in d:\temp\testCMD and current directory in drive c: is C:\Users, the results on execution are:

1.- Calling without quotes from cmd directory: test.cmd

Calling subroutine from drive d:
---------------------------------------------------------
cd    : D:\temp\testCMD
d0    : D:
dp0   : D:\temp\testCMD\
f0    : D:\temp\testCMD\test.cmd
---------------------------------------------------------


Calling subroutine from drive c:
---------------------------------------------------------
cd    : C:\Users
d0    : D:
dp0   : D:\temp\testCMD\
f0    : D:\temp\testCMD\test.cmd
---------------------------------------------------------


Getting data directly without subroutine
---------------------------------------------------------
cd    : C:\Users
d0    : D:
dp0   : D:\temp\testCMD\
f0    : D:\temp\testCMD\test.cmd
---------------------------------------------------------

Result: everything ok.

2.- Calling with quotes from cmd directory "test.cmd" (no, no need for call command)

Calling subroutine from drive d:
---------------------------------------------------------
cd    : D:\temp\testCMD
d0    : D:
dp0   : D:\temp\testCMD\
f0    : D:\temp\testCMD\test.cmd
---------------------------------------------------------


Calling subroutine from drive c:
---------------------------------------------------------
cd    : C:\Users
d0    : D:
dp0   : D:\temp\testCMD\
f0    : D:\temp\testCMD\test.cmd
---------------------------------------------------------


Getting data directly without subroutine
---------------------------------------------------------
cd    : C:\Users
d0    : C:
dp0   : C:\Users\
f0    : C:\Users\test.cmd
---------------------------------------------------------

Result: Failure to get correct value of %~d0 ONLY if directly get from main execution line of cmd. The same with subroutine call works as expected.

All scenarios tested without quotes work without failure. With quotes, if the calling line include the drive (ej: "d:.\test.cmd") all values are correctly retrieved. If not drive included in batch call, (ej: "test.cmd" with batch directory in path, or "\temp\testCMD\test.cmd" from root of D:), incorrect values retrieved, but only from main line of execution in batch file. Subroutines always get correct values.

Why? No idea. But when tracing cmd execution with procmon, in failure cases, when cmd.exe try to retrieve the information for the file, a QueryDirectory API call is made for C:\Users\test.cmd which is answered with NO SUCH FILE, but cmd ignores it and continues execution, showing the wrong values.

So, no answer, sorry. But i had to "document" this. Some guru in the room?

update: More information here

like image 199
MC ND Avatar answered Oct 06 '22 07:10

MC ND


Fascinating discovery.

Somewhere on DosTips there is a jeb post describing how %0 and variants like %~f0 work from the main script vs. within a CALLed subroutine: %0 from within a subroutine gives the subroutine label, but adding a modifier like %~f0 works with the running scripts path, even if SHIFT has been used.

But I don't remember jeb's post describing a difference between a quoted vs. unquoted %0 from the main routine (no subroutine).

I extended MC ND's tests below. My script is c:\test\test.bat.

@echo off
setlocal
echo(
echo Upon entry:
echo ---------------------------------------------------------
echo   %%shift%% : %shift%
echo      %%cd%% : %cd%
echo        %%0 : %0
echo        %%1 : %1
echo      %%~d0 : %~d0
echo      %%~p0 : %~p0
echo      %%~n0 : %~n0
echo      %%~x0 : %~x0
echo      %%~f0 : %~f0
call echo call %%%%~f0 : %%~f0
echo ---------------------------------------------------------

set "shift=FALSE"
d:
echo(
echo Current directory set to D:\

:top
call :getInfo

:getInfo
echo(
if "%0" equ ":getInfo" (
  <nul set /p "=From subroutine "
) else (
  <nul set /p "=From main "
)
if "%shift%" equ "TRUE" (echo after SHIFT) else (echo before SHIFT)
echo ---------------------------------------------------------
echo   %%shift%% : %shift%
echo      %%cd%% : %cd%
echo        %%0 : %0
echo        %%1 : %1
echo      %%~d0 : %~d0
echo      %%~p0 : %~p0
echo      %%~n0 : %~n0
echo      %%~x0 : %~x0
echo      %%~f0 : %~f0
call echo call %%%%~f0 : %%~f0
echo ---------------------------------------------------------
if "%0" equ ":getInfo" exit /b
if "%shift%" equ "TRUE" exit /b
shift
set "shift=TRUE"
goto top

Here is the result using test as the command, and test as the first argument:

C:\test>test test

Upon entry:
---------------------------------------------------------
  %shift% :
     %cd% : C:\test
       %0 : test
       %1 : test
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 : .bat
     %~f0 : C:\test\test.bat
call %~f0 : C:\test\test.bat
---------------------------------------------------------

Current directory set to D:\

From subroutine before SHIFT
---------------------------------------------------------
  %shift% : FALSE
     %cd% : D:\
       %0 : :getInfo
       %1 :
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 : .bat
     %~f0 : C:\test\test.bat
call %~f0 : C:\test\test.bat
---------------------------------------------------------

From main before SHIFT
---------------------------------------------------------
  %shift% : FALSE
     %cd% : D:\
       %0 : test
       %1 : test
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 : .bat
     %~f0 : C:\test\test.bat
call %~f0 : C:\test\test.bat
---------------------------------------------------------

From subroutine after SHIFT
---------------------------------------------------------
  %shift% : TRUE
     %cd% : D:\
       %0 : :getInfo
       %1 :
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 : .bat
     %~f0 : C:\test\test.bat
call %~f0 : C:\test\test.bat
---------------------------------------------------------

From main after SHIFT
---------------------------------------------------------
  %shift% : TRUE
     %cd% : D:\
       %0 : test
       %1 :
     %~d0 : D:
     %~p0 : \
     %~n0 : test
     %~x0 :
     %~f0 : D:\test
call %~f0 : D:\test
---------------------------------------------------------

C:\test>

And here are the results using quoted values:

C:\test>"test" "test"

Upon entry:
---------------------------------------------------------
  %shift% :
     %cd% : C:\test
       %0 : "test"
       %1 : "test"
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 :
     %~f0 : C:\test\test
call %~f0 : C:\test\test
---------------------------------------------------------

Current directory set to D:\

From subroutine before SHIFT
---------------------------------------------------------
  %shift% : FALSE
     %cd% : D:\
       %0 : :getInfo
       %1 :
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 : .bat
     %~f0 : C:\test\test.bat
call %~f0 : C:\test\test.bat
---------------------------------------------------------

From main before SHIFT
---------------------------------------------------------
  %shift% : FALSE
     %cd% : D:\
       %0 : "test"
       %1 : "test"
     %~d0 : D:
     %~p0 : \
     %~n0 : test
     %~x0 :
     %~f0 : D:\test
call %~f0 : D:\test
---------------------------------------------------------

From subroutine after SHIFT
---------------------------------------------------------
  %shift% : TRUE
     %cd% : D:\
       %0 : :getInfo
       %1 :
     %~d0 : C:
     %~p0 : \test\
     %~n0 : test
     %~x0 : .bat
     %~f0 : C:\test\test.bat
call %~f0 : C:\test\test.bat
---------------------------------------------------------

From main after SHIFT
---------------------------------------------------------
  %shift% : TRUE
     %cd% : D:\
       %0 : "test"
       %1 :
     %~d0 : D:
     %~p0 : \
     %~n0 : test
     %~x0 :
     %~f0 : D:\test
call %~f0 : D:\test
---------------------------------------------------------

C:\test>

I get identical results from XP and Win 7.

Everything works as expected when within a subroutine.

But I cannot explain the behavior from the main level. Before the SHIFT, the unquoted command works with the true path to the executing script. But the quoted command works with the string from the command line instead and fills in missing values using the current working drive and directory. Yet after the SHIFT, both the unquoted and quoted values behave the same, it simply works with the actual passed parameter and current working drive and directory.

So the only reliable way to get the path info for the executing script at any point within a script is to use a subroutine. The values will be incorrect from the main level if the current drive and/or directory have changed since launch, or if there has been a SHIFT of %0. Very bizarre. At best, I would classify this as a design flaw. At worst, a downright bug.


Update

Actually, the easiest way to fix your code is to simply use PUSHD and POPD, but I don't think that is what you are really looking for :-)

pushd R:
popd

I used to think you could solve the %~0 problem by capturing the value in an environment variable prior to changing your working directory. But that can fail if your script is called using enclosing quotes, but without the .bat extension. It can work if you are only looking for the drive, but other things like path, base name, extension, size, and timestamp can fail.

It turns out the only way to positively get the correct value is to use a CALLed subroutine.

Note that there is another potential problem that can crop up under obscure circumstances. Both ^ and ! can be used in file and folder names. Names with those values can be corrupted if you capture them while delayed expansion is enabled. Delayed expansion is normally disabled when a batch file starts, but it is possible that it could launch with delayed expansion enabled. You could explicitly disable delayed expansion before you capture the value(s), but there is another option using a function call.

The script below defines a :currentScript function that can be used under any circumstances, and it is guaranteed to give the correct value. You pass in the name of a variable to receive the value, and optionally pass in a string of modifiers (without the tilde). The default option is F (full path, equivalent to DPNX)

The :currentScript function is at the bottom. The rest of the script is a test harness to demonstrate and test the functionality. It contrasts the results using the function vs. using %0 directly.

@echo off
setlocal disableDelayedExpansion
set arg0=%0
if "%arg0:~0,1%%arg0:~0,1%" equ """" (set "arg0=Quoted") else set "arg0=Unquoted"

call :header
echo                %%~tzf0 = "%~tzf0"
call :currentScript rtn tzf
echo :currentScript result = "%rtn%"

setlocal enableDelayedExpansion
call :header
echo                %%~tzf0 = "%~tzf0"
call :currentScript rtn tzf
echo :currentScript result = "!rtn!"

endlocal
d:
call :header
echo                %%~tzf0 = "%~tzf0"
call :currentScript rtn tzf
echo :currentScript result = "%rtn%"

setlocal enableDelayedExpansion
call :header
echo                %%~tzf0 = "%~tzf0"
call :currentScript rtn tzf
echo :currentScript result = "!rtn!"

exit /b

:header
set "rtn="
setlocal
echo(
echo(
if "!" equ "" (set "delayed=ON") else set "delayed=OFF"
if "%cd%\" equ "%~dp0" (set "cwd=Original") else set "cwd=Modified"
echo %arg0%: %cwd% working directory, Delayed expansion = %delayed%
echo ---------------------------------------------------------------------------
exit /b


:currentScript  rtnVar  [options]
setlocal
set "notDelayed=!"
setlocal disableDelayedExpansion
set "options=%~2"
if not defined options set "options=f"
call set "rtn=%%~%options%0"
if not defined notDelayed set "rtn=%rtn:^=^^%"
if not defined notDelayed set "rtn=%rtn:!=^!%"
endlocal & endlocal & set "%~1=%rtn%" !
exit /b

Here are some test results when I give the script a crazy name of test^it!.bat. I tested with both unquoted and quoted values. You can see that the :CurrentScript function always works, but a direct expansion of %~tzf0 often fails.

C:\test>TEST^^IT!.BAT


Unquoted: Original working directory, Delayed expansion = OFF
---------------------------------------------------------------------------
               %~tzf0 = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"


Unquoted: Original working directory, Delayed expansion = ON
---------------------------------------------------------------------------
               %~tzf0 = "11/07/2013 08:17 PM 1400 C:\test\testit.bat"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"


Unquoted: Modified working directory, Delayed expansion = OFF
---------------------------------------------------------------------------
               %~tzf0 = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"


Unquoted: Modified working directory, Delayed expansion = ON
---------------------------------------------------------------------------
               %~tzf0 = "11/07/2013 08:17 PM 1400 C:\test\testit.bat"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"

C:\test>"TEST^IT!.BAT"


Quoted: Original working directory, Delayed expansion = OFF
---------------------------------------------------------------------------
               %~tzf0 = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"


Quoted: Original working directory, Delayed expansion = ON
---------------------------------------------------------------------------
               %~tzf0 = "11/07/2013 08:17 PM 1400 C:\test\testit.bat"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"


Quoted: Modified working directory, Delayed expansion = OFF
---------------------------------------------------------------------------
               %~tzf0 = "D:\TEST^IT!.BAT"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"


Quoted: Modified working directory, Delayed expansion = ON
---------------------------------------------------------------------------
               %~tzf0 = "D:\TESTIT.BAT"
:currentScript result = "11/07/2013 08:17 PM 1400 C:\test\test^it!.bat"

C:\test>"TEST^IT!"


Quoted: Original working directory, Delayed expansion = OFF
---------------------------------------------------------------------------
               %~tzf0 = "C:\test\TEST^IT!"
:currentScript result = "11/07/2013 10:43 PM 1397 C:\test\test^it!.bat"


Quoted: Original working directory, Delayed expansion = ON
---------------------------------------------------------------------------
               %~tzf0 = "C:\test\TESTIT"
:currentScript result = "11/07/2013 10:43 PM 1397 C:\test\test^it!.bat"


Quoted: Modified working directory, Delayed expansion = OFF
---------------------------------------------------------------------------
               %~tzf0 = "D:\TEST^IT!"
:currentScript result = "11/07/2013 10:43 PM 1397 C:\test\test^it!.bat"


Quoted: Modified working directory, Delayed expansion = ON
---------------------------------------------------------------------------
               %~tzf0 = "D:\TESTIT"
:currentScript result = "11/07/2013 10:43 PM 1397 C:\test\test^it!.bat"

C:\test>

I also tested with names of test^it.bat, test!.bat, and test.bat, and all worked properly (not shown).

like image 45
dbenham Avatar answered Oct 06 '22 07:10

dbenham