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.b
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
"%~dpi"
also fails when you are listing files but the working directory is a different folder, or drive. It shows the working directory and not the path of the files.
I think the solution here might be to get the %~d0
before changing drive.
Like dbenham: Fascinating!
I suppose it's a feature of cmd.exe.
Quotes aren't stripped from %0
in the main batch context.
But they are all stripped by a call to a subroutine.
This can be realized when more than two quotes are used, only one quote from each side will be removed when the %0
is expanded.
ParamTest.bat
@echo off
cls
setlocal
d:
echo Main %%0: %~0, %~f0
echo Main %%1: %~1, %~f1
call :func %1
exit /b
:func
echo Func %%0: %~0, %~f0
echo Func %%1: %~1, %~f1
exit /b
Output for: """"PARAM"test.BAT" ""paramTEST.bAt""
Main %0: """PARAM"test.BAT, D:\"""PARAM"test.BAT Main %1: "paramTEST.bAt", D:\"paramTEST.bAt" Func %0: :func, C:\temp\ParamTest.bat Func %1: "paramTEST.bAt", D:\"paramTEST.bAt"
And %0
seems to save the related directory, as you get different results for %~f0
and %~f1
even when the content seems to be equal.
But perhaps the path is just prefixed to %0
.
Output for: PARAMtest.BAT paramTEST.bAt
Main %0: PARAMtest.BAT, C:\temp\ParamTest.bat Main %1: paramTEST.bAt, D:\paramTEST.bAt Func %0: :func, C:\temp\ParamTest.bat Func %1: paramTEST.bAt, D:\paramTEST.bAt
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).