22

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

echo %~dp0
CD Arvind
echo %~dp0

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

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();

为什么以不同方式执行相同脚本时输出会有所不同?

我在这里想念什么吗?

4

6 回答 6

15

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

杰布的回答里面说

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

Jeb 是正确的。

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

当一个变量被访问时,它的值是从可用变量列表中检索的,但是如果请求的变量是%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,会在内存中创建一个新的批处理文件上下文,并使用内部“变量”来初始化此上下文。

于 2014-11-10T19:52:46.250 回答
5

我将尝试解释为什么它的行为如此奇怪。一个相当技术性和冗长的故事,我会尽量保持简洁。这个问题的出发点是:

   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,它也使用该论点。在你的情况下比目鱼,因为它使用的路径只是“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而不是当前目录。

于 2014-11-10T16:16:28.513 回答
4

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

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

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

成功了。

于 2012-08-27T12:36:19.420 回答
3

这是引号和%~0.

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

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

这就是为什么cmd /c myBatch.bat起作用的原因,因为 thenmyBatch.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

编辑:FixIt,即使%~0有引号也能检索全名

存在函数调用的解决方法。

@echo off
echo This can be wrong %~f0
call :getCorrectName
exit /b

:getCorrectName
echo Here the value is correct %~f0
exit /b
于 2014-11-10T14:23:16.940 回答
1

命令行解释器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 /D C:\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的路径时,在批处理执行期间更改当前工作目录时获取批处理文件的路径是不可靠的。

这个 bug 也会根据Windows shell bug with how %~dp0 is resolved报告给 Microsoft 。

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

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

另一种解决方法是在使用双引号时始终使用完整路径(和文件扩展名)启动批处理文件,就像类Process一样。

于 2014-11-10T18:43:17.600 回答
-3

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

例如,如果您尝试这样:

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

有用。

于 2012-08-27T12:28:03.407 回答