CMD: CALL 引用批处理文件名时 %~d0 失败

Posted

技术标签:

【中文标题】CMD: CALL 引用批处理文件名时 %~d0 失败【英文标题】:CMD: failure of %~d0 when CALL quotes the name of the batch file 【发布时间】:2013-11-15 21:20:10 【问题描述】:

为什么%~d0返回批处理文件的驱动器号S失败:当CALL引用批处理文件的名称时?

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:\>

编辑以下来自 Jerry 和 MC 的回复:这是一个显示相同内容的非 CALL 示例:

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:\>

【问题讨论】:

这不仅仅是 CALL - 即使您直接执行批处理文件也会发生这种情况。如果你把echo %0 %~d0 %~f0放在那里,当你引用名字时,你会得到"test.bat" S: S:\!DJ DAP\test.bat 一个疯狂的猜测。引号被作为文件名的一部分。这总是返回当前驱动器:C:\>for /f "usebackq" %a in ('^"^"^"') do echo %~dpfnxa 我希望没有投票的最爱来自 OP。这个问题绝对值得我在书中投赞成票。 在Quotes when starting a batch file引用更多效果 dbenham 写道:“我希望没有投赞成票的最爱来自 OP。”请钙化。最喜欢什么没有投票?? 【参考方案1】:

编辑 - npocmaka,你是对的。奇怪的。

原始答案已删除-我错了。

但问题不在于call 命令。问题在于引号和 cmd。

经过测试,文件名的处理方式以及 cmd 如何处理 api 调用中的一些错误似乎更像是一个错误/功能。

用下面的批处理文件(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

放在d:\temp\testCMD,c盘当前目录为C:\Users,执行结果为:

1.- 不带引号调用来自 cmd 目录: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
---------------------------------------------------------

结果:一切正常。

2.- 使用引号调用 从 cmd 目录 "test.cmd"(不,不需要 call 命令)

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
---------------------------------------------------------

结果:仅当直接从 cmd 的主执行行获取时,才能获取正确的 %~d0 值。与子程序调用相同,按预期工作。

所有不带引号测试的场景都可以正常工作。使用引号,如果调用行包含驱动器(ej:"d:.\test.cmd"),则所有值都会正确检索。如果批处理调用中不包含驱动器,(ej:"test.cmd" 带有批处理目录在路径中,或"\temp\testCMD\test.cmd" 来自 D: 的根目录),则检索到不正确的值,但仅来自批处理文件中的主执行行。子程序总是得到正确的值。

为什么?不知道。但是当使用 procmon 跟踪 cmd 执行时,在失败的情况下,当 cmd.exe 尝试检索文件的信息时,会为 C:\Users\test.cmd 进行 QueryDirectory API 调用,并以NO SUCH FILE 回答,但 cmd 会忽略它并继续执行,显示错误的值

所以,没有答案,对不起。但我必须“记录”这一点。房间里有什么大师?

更新:更多信息here

【讨论】:

自己测试一下。真的很奇怪:)。它返回不同的结果,具体取决于是否使用引号调用。希望批处理大师对此有答案... 感谢 MC 的透彻分析。我也有兴趣等待大师:)【参考方案2】:

令人着迷的发现。

DosTips 上的某处有一篇 jeb 帖子描述了 %0 和类似 %~f0 之类的变体如何在主脚本中与在调用的子例程中工作:%0 从子例程中给出子例程标签,但添加一个修饰符就像 %~f0 一样,它与运行脚本路径一起工作,即使已经使用了 SHIFT。

但我不记得 jeb 的帖子描述了主例程(无子例程)中引用与未引用 %0 之间的区别。

我在下面扩展了 MC ND 的测试。我的脚本是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

这是使用test 作为命令,test 作为第一个参数的结果:

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>

以下是使用引用值的结果:

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>

我从 XP 和 Win 7 得到相同的结果。

在子例程中一切都按预期工作。

但我无法从主要层面解释这种行为。在 SHIFT 之前,未加引号的命令使用执行脚本的真实路径。但是引用的命令使用命令行中的字符串代替,并使用当前工作驱动器和目录填充缺失值。然而,在 SHIFT 之后,不带引号和带引号的值的行为相同,它只是与实际传递的参数和当前工作驱动器和目录一起工作。

因此,在脚本中的任何位置获取执行脚本的路径信息的唯一可靠方法是使用子例程。如果当前驱动器和/或目录自启动以来已更改,或者存在%0 的 SHIFT,则主级别的值将不正确。很奇怪。充其量,我会将其归类为设计缺陷。最坏的情况是一个彻头彻尾的错误。


更新

实际上,修复代码的最简单方法是简单地使用 PUSHD 和 POPD,但我认为这不是您真正想要的 :-)

pushd R:
popd

我曾经认为您可以通过在更改工作目录之前捕获环境变量中的值来解决%~0 问题。但是,如果使用封闭引号调用您的脚本,但没有 .bat 扩展名,这可能会失败。如果您只查找驱动器,它可以工作,但路径、基本名称、扩展名、大小和时间戳等其他内容可能会失败。

事实证明,获得正确值的唯一方法是使用 CALLed 子例程。

请注意,在不明确的情况下可能会出现另一个潜在问题。 ^! 都可以用于文件和文件夹名称。如果在启用延迟扩展的情况下捕获具有这些值的名称,它们可能会损坏。批处理文件启动时通常会禁用延迟扩展,但它可能会在启用延迟扩展的情况下启动。您可以在捕获值之前显式禁用延迟扩展,但还有另一个使用函数调用的选项。

下面的脚本定义了一个:currentScript函数,可以在任何情况下使用,并且保证给出正确的值。您传入一个变量的名称来接收该值,并可选择传入一个修饰符字符串(不带波浪号)。默认选项为F(完整路径,相当于DPNX

:currentScript 函数位于底部。脚本的其余部分是用于演示和测试功能的测试工具。它对比了使用函数与直接使用%0 的结果。

@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

当我给脚本起一个疯狂的名字test^it!.bat 时,这是一些测试结果。我用不带引号和带引号的值进行了测试。可以看到:CurrentScript 函数始终有效,但直接扩展%~tzf0 经常失败。

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>

我还用test^it.battest!.battest.bat 的名称进行了测试,并且都正常工作(未显示)。

【讨论】:

【参考方案3】:

像德本汉姆:令人着迷!

我想这是 cmd.exe 的一个特性。

不会从主批处理上下文中的 %0 中删除引号。 但是它们都被一个子程序的调用剥离了。 这可以在使用两个以上的引号时实现,当 %0 展开时,每侧只会删除一个引号。

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

输出:""""PARAM"test.BAT" ""paramTEST.bAt""

主要 %0: """PARAM"test.BAT, D:\"""PARAM"test.BAT 主 %1:“paramTEST.bAt”,D:\“paramTEST.bAt” 函数 %0: :func, C:\temp\ParamTest.bat 函数 %1: "paramTEST.bAt", D:\"paramTEST.bAt"

并且%0 似乎保存相关目录,因为即使内容似乎相同,%~f0%~f1 也会得到不同的结果。 但也许路径只是以%0 为前缀。

输出:PARAMtest.BAT paramTEST.bAt

主要 %0: PARAMtest.BAT, C:\temp\ParamTest.bat 主要 %1: paramTEST.bAt, D:\paramTEST.bAt 函数 %0: :func, C:\temp\ParamTest.bat 功能 %1: paramTEST.bAt, D:\paramTEST.bAt

【讨论】:

【参考方案4】:

"%~dpi" 在您列出文件但工作目录是不同的文件夹或驱动器时也会失败。它显示的是工作目录,而不是文件的路径。

我认为这里的解决方案可能是在更换驱动器之前获取%~d0

【讨论】:

以上是关于CMD: CALL 引用批处理文件名时 %~d0 失败的主要内容,如果未能解决你的问题,请参考以下文章

CMD获取当前目录的绝对路径

强制 bat 文件使用非默认的 cmd.exe

三菱PLC call指令理解

CMD 在参数中嵌套双引号

在 CMD 批处理脚本中调用标签时如何使用超过 9 个参数?

Windows 批处理文件中的 %~d0 是啥意思?