使用%~dp0引用的批处理文件路径有时会在更改目录时发生更改的原因是什么?

我有一个包含以下内容的批处理文件:

echo %~dp0 CD Arvind echo %~dp0 

即使改变%~dp0目录值也是一样的。 但是,如果我从CSharp程序运行此批处理文件,则%~dp0的值会在CD之后更改。 它现在指向新目录。 以下是我使用的代码:

 Directory.SetCurrentDirectory(//Dir where batch file resides); ProcessStartInfo ProcessInfo; Process process = new Process(); ProcessInfo = new ProcessStartInfo("mybatfile.bat"); ProcessInfo.UseShellExecute = false; ProcessInfo.RedirectStandardOutput = true; process = Process.Start(ProcessInfo); process.WaitForExit(); ExitCode = process.ExitCode; process.Close(); 

为什么以不同方式执行相同脚本的输出存在差异?

我在这里想念一下吗?

这个问题开始讨论这一点,并进行了一些测试以确定原因。 因此,在cmd.exe内部进行一些调试后…(这是针对32位Windows XP cmd.exe但由于行为在较新的系统版本上是一致的,可能使用相同或类似的代码)

在杰布的答案中说明了这一点

 It's a problem with the quotes and %~0. cmd.exe handles %~0 in a special way 

杰布是对的。

在正在运行的批处理文件的当前上下文中,有一个对当前批处理文件的引用,一个“变量”包含正在运行的批处理文件的完整路径和文件名。

当访问变量时,从可用变量列表中检索其值,但如果请求的变量是%0 ,并且已经请求了某个修饰符(使用了~ ),则使用运行批处理引用“变量”中的数据。

~的用法对变量有另一个影响。 如果引用该值,则会删除引号。 这里的代码中有一个错误。 它的编码类似于(这里简化的汇编程序为伪代码)

 value = varList[varName] if (value && value[0] == quote ){ value = unquote(value) } else if (varName == '0') { value = batchFullName } 

是的,这意味着当引用批处理文件时,执行if的第一部分并且不使用批处理文件的完整引用,而是检索的值是用于在调用批处理文件时引用批处理文件的字符串。

那么会发生什么? 如果在调用批处理文件时使用了完整路径,那么就没有问题了。 但是,如果在调用中未使用完整路径,则需要检索批处理调用中不存在的路径中的任何元素。 该检索假设相对路径。

一个简单的批处理文件( test.cmd

 @echo off echo %~f0 

当使用test (没有扩展名,没有引号)调用时,我们获得c:\somewhere\test.cmd

当使用"test" (无扩展名,引号)调用时,我们获得c:\somewhere\test

在第一种情况下,没有引号,使用正确的内部值。 在第二种情况下,当引用调用时,用于调用批处理文件( "test" )的字符串是不加引号和使用的。 当我们请求完整路径时,它被认为是对称为test东西的相对引用。

这就是原因。 怎么解决?

来自C#代码

  • 不要使用引号: cmd /c batchfile.cmd

  • 如果需要引号,请使用批处理文件调用中的完整路径。 那样%0包含所有需要的信息。

从批处理文件

可以从任何地方以任何方式调用批处理文件。 检索当前批处理文件信息的唯一可靠方法是使用子例程。 如果使用任何修饰符( ~ ), %0将使用内部“变量”来获取数据。

 @echo off setlocal enableextensions disabledelayedexpansion call :getCurrentBatch batch echo %batch% exit /b :getCurrentBatch variableName set "%~1=%~f0" goto :eof 

这将回显控制当前批处理文件的完整路径,与您调用文件的方式无关,有或没有引号。

注意 :为什么它有效? 为什么子程序中的%~f0引用返回不同的值? 从子程序内部访问的数据不一样。 执行call ,将在内存中创建新的批处理文件上下文,并使用内部“变量”初始化此上下文。

我会试着解释为什么这种行为如此奇怪。 一个相当技术性和冗长的故事,我会尽力使它凝聚。 这个问题的出发点是:

  ProcessInfo.UseShellExecute = false; 

如果省略此语句或指定true ,它将按预期工作,您将看到。

Windows提供了两种启动程序的基本方法,ShellExecuteEx()和CreateProcess()。 UseShellExecute属性在这两者之间进行选择。 前者是“智能和友好”版本,它对shell的工作方式有很多了解。 这就是为什么你可以把路径传递给像“foo.doc”这样的任意文件。 它知道如何查找.doc文件的文件关联,并找到知道如何打开foo.doc的.exe文件。

CreateProcess()是低级别的winapi函数,它与本机内核函数(NtCreateProcess)之间的粘合很少。 注意函数的前两个参数lpApplicationNamelpCommandLine ,您可以轻松地将它们与两个ProcessStartInfo属性进行匹配。

什么是不可见的,CreateProcess()提供了两种不同的方式来启动程序。 第一个是将lpApplicationName设置为空字符串并使用lpCommandLine提供整个命令行。 这使得CreateProcess变得友好,它在找到可执行文件后自动将应用程序名称扩展到完整路径。 因此,例如,“cmd.exe”扩展为“c:\ windows \ system32 \ cmd.exe”。 但是当你使用lpApplicationName参数时,它会这样做,它按原样传递字符串。

这个怪癖会影响程序,这些程序取决于指定命令行的确切方式。 特别是对于C程序,他们假设argv[0]包含其可执行文件的路径。 它对%~dp0 ,它也使用该参数。 自从它使用的路径以来,你的案例中的flounders只是“mybatfile.bat”,而不是“c:\ temp \ mybatfile.bat”。 这使得它返回当前目录而不是“c:\ temp”。

那么你该做什么,这在.NET Framework文档中完全没有记录,现在由你来决定将完整的路径名传递给文件。 所以正确的代码应如下所示:

  string path = @"c:\temp"; // Dir where batch file resides Directory.SetCurrentDirectory(path); string batfile = System.IO.Path.Combine(path, "mybatfile.bat"); ProcessStartInfo = new ProcessStartInfo(batfile); 

你会看到%~dp0现在按照你的预期扩展。 它使用path而不是当前目录。

乔伊的建议有所帮助。 只需更换

 ProcessInfo = new ProcessStartInfo("mybatfile.bat"); 

 ProcessInfo = new ProcessStartInfo("cmd", "/c " + "mybatfile.bat"); 

做了伎俩。

这是引号和%~0 〜0的问题。

cmd.exe以特殊方式处理%~0%~1除外)。
它检查%0是否是相对文件名,然后将它与start目录一起添加。

如果找到一个文件,它将使用这个组合,否则它会在实际目录前面加上它。
但是当名称以引号开头时,它似乎无法删除引号,而不是在添加目录之前。

这就是cmd /c myBatch.bat工作的原因,因为myBatch.bat在没有引号的情况下被调用。
您也可以使用完全限定路径启动批处理,然后它也可以运行。

或者在更改目录之前保存批处理中的完整路径。

一个小的test.bat可以演示cmd.exe的问题

 @echo off setlocal echo %~fx0 %~fx1 cd .. echo %~fx0 %~fx1 

通过(在C:\ temp中)调用它

 test test 

输出应该是

 C:\temp\test.bat C:\temp\test C:\temp\test.bat C:\test 

因此, cmd.exe能够找到test.bat ,但只有%~fx0才能找到开始目录。

在通过它调用它的情况下

 "test" "test" 

它失败了

 C:\temp\test C:\temp\test C:\test C:\test 

cmd.exe甚至在更改目录之前无法找到批处理文件,它无法将名称扩展为c:\temp\test.bat的全名

命令行解释器cmd.exe在获取批处理文件路径的代码中有一个错误 ,如果使用双引号调用批处理文件并且具有相对于当前工作目录的路径。

创建目录C:\ Temp \ TestDir 。 在此目录中创建一个名为PathTest.bat的文件,并将以下代码复制并粘贴到此批处理文件中:

 @echo off set "StartIn=%CD%" set "BatchPath=%~dp0" echo Batch path before changing working directory is: %~dp0 cd .. echo Batch path after changing working directory is: %~dp0 echo Saved path after changing working directory is: %BatchPath% cd "%StartIn%" echo Batch path after restoring working directory is: %~dp0 

接下来打开一个命令提示符窗口,并使用以下命令将工作目录设置为C:\ Temp \ TestDir

 cd /DC:\Temp\TestDir 

现在通过以下方式调用Test.bat

  1. PathTest
  2. PathTest.bat
  3. .\PathTest
  4. .\PathTest.bat
  5. ..\TestDir\PathTest
  6. ..\TestDir\PathTest.bat
  7. \Temp\TestDir\PathTest
  8. \Temp\TestDir\PathTest.bat
  9. C:\Temp\TestDir\PathTest
  10. C:\Temp\TestDir\PathTest.bat

对于所有10个测试用例,输出是C:\ Temp \ TestDir \的四倍

测试用例7和8使用相对于当前驱动器根目录的路径启动批处理文件。

现在让我们看一下与之前相同的结果,但是在批处理文件名周围使用双引号。

  1. "PathTest"
  2. "PathTest.bat"
  3. ".\PathTest"
  4. ".\PathTest.bat"
  5. "..\TestDir\PathTest"
  6. "..\TestDir\PathTest.bat"
  7. "\Temp\TestDir\PathTest"
  8. "\Temp\TestDir\PathTest.bat"
  9. "C:\Temp\TestDir\PathTest"
  10. "C:\Temp\TestDir\PathTest.bat"

对于测试用例5到10,输出是C:\ Temp \ TestDir \的四倍

但是对于测试用例1到4,第二个输出行只是C:\ Temp \而不是C:\ Temp \ TestDir \

现在使用cd ..将工作目录更改为C:\ Temp并运行PathTest.bat ,如下所示:

  1. "TestDir\PathTest.bat"
  2. ".\TestDir\PathTest.bat"
  3. "\Temp\TestDir\PathTest.bat"
  4. "C:\Temp\TestDir\PathTest.bat"

测试用例1和2的第二个输出结果是C:\ TestDir \ ,它根本不存在。

在没有双引号的情况下启动批处理文件会为所有4个测试用例生成正确的输出。

这使得行为很明显是由错误引起的。

每当批处理文件以双引号启动并且启动时相对于当前工作目录的路径时, %~dp0在批处理执行期间更改当前工作目录时获取批处理文件的路径是不可靠的。

根据Windows shell bug,还会向Microsoft报告此错误,以及%~dp0是如何解决的 。

通过在更改工作目录之前将上述代码演示的批处理文件的路径立即分配给环境变量,可以解决此错误。

然后在需要批处理文件路径的地方引用此变量的值,并在需要时使用双引号。 像%BatchPath%这样的东西总是更好的可读性%~dp0

另一种解决方法是使用双引号启动批处理文件,使用完整路径(和文件扩展名),如类所示。

由ProcessStart调用的批处理中的每个新行都被独立地视为新的cmd命令。

例如,如果你试试这样:

 echo %~dp0 && CD Arvind && echo %~dp0 

有用。