Подтвердить что ты не робот

В чем причина того, что путь пакетного файла, на который ссылается% ~ 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();

Почему существует разница в производительности при выполнении одного и того же сценария по-разному?

Мне что-то не хватает?

4b9b3361

Ответ 1

Этот вопрос начал дискуссию по этому вопросу, и некоторые испытания были проведены, чтобы определить, почему. Итак, после некоторой отладки внутри cmd.exe... (это для 32-разрядного Windows XP cmd.exe, но поскольку поведение совместимо с новыми версиями системы, возможно, используется тот же или похожий код)

Внутри ответа Jeb указано

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

Вот почему. Как решить?

Из кода С#

  • Не используйте кавычки: 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

Это будет эхо, чтобы полностью исключить полный путь к текущему пакетному файлу независимо от того, как вы вызываете файл, с кавычками или без них.

note: Почему это работает? Почему ссылка %~f0 внутри подпрограммы возвращает другое значение? Данные, доступные изнутри подпрограммы, не совпадают. Когда выполняется call, в памяти создается новый контекст командного файла, а внутренняя "переменная" используется для инициализации этого контекста.

Ответ 2

Я попытаюсь объяснить, почему это так странно. Довольно техничная и длинная история, я постараюсь, чтобы она конденсировалась. Отправной точкой для этой проблемы является:

   ProcessInfo.UseShellExecute = false;

Вы увидите, что если вы опускаете этот оператор или присваиваете true, он работает так, как вы ожидали.

Windows предоставляет два основных способа запуска программ: ShellExecuteEx() и CreateProcess(). Свойство UseShellExecute выбирает между этими двумя. Первая - это "умная и дружественная" версия, она многое знает о том, как работает оболочка, например. Вот почему вы можете, скажем, передать путь к произвольному файлу, например "foo.doc". Он знает, как искать ассоциацию файлов для .doc файлов и находить файл .exe, который знает, как открыть foo.doc.

CreateProcess() - это низкоуровневая функция winapi, там очень мало клея между ней и встроенной функцией ядра (NtCreateProcess). Обратите внимание на первые два аргумента функции lpApplicationName и lpCommandLine, вы можете легко сопоставить их с двумя свойствами ProcessStartInfo.

Что не так заметно, что CreateProcess() предоставляет два разных способа запуска программы. Первый - это то, где вы оставляете lpApplicationName установленным в пустую строку и используете lpCommandLine для предоставления всей командной строки. Это делает CreateProcess дружественным, он автоматически расширяет имя приложения до полного пути после того, как он нашел исполняемый файл. Так, например, "cmd.exe" расширяется до "c:\windows\system32\cmd.exe". Но он не делает это, когда вы используете аргумент lpApplicationName, он передает строку as-is.

Эта причуда влияет на программы, которые зависят от точного способа указания командной строки. В частности, для программ на 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.

Ответ 3

Предложение Джои помогло. Просто заменив

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

с

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

сделал трюк.

Ответ 4

Это проблема с кавычками и %~0.

cmd.exe обрабатывает %~0 особым образом (кроме %~1).
Он проверяет, является ли %0 относительным именем файла, затем он добавляет его в стартовый каталог.

Если файл можно найти, он будет использовать эту комбинацию, иначе он добавит его в фактический каталог.
Но когда имя начинается с цитаты, кажется, что не удалось удалить кавычки, прежде чем добавить каталог.

Причина, почему работает 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

Ответ 5

У интерпретатора командной строки 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 следующими способами:

  • PathTest
  • PathTest.bat
  • .\PathTest
  • .\PathTest.bat
  • ..\TestDir\PathTest
  • ..\TestDir\PathTest.bat
  • \Temp\TestDir\PathTest
  • \Temp\TestDir\PathTest.bat
  • C:\Temp\TestDir\PathTest
  • C:\Temp\TestDir\PathTest.bat

Вывод в четыре раза C:\Temp\TestDir\, как и ожидалось, для всех 10 тестовых случаев.

В тестовых случаях 7 и 8 запускается командный файл с указанием пути к корневому каталогу текущего диска.

Теперь давайте посмотрим на результаты, сделав то же, что и раньше, но с использованием двойных кавычек в имени командного файла.

  • "PathTest"
  • "PathTest.bat"
  • ".\PathTest"
  • ".\PathTest.bat"
  • "..\TestDir\PathTest"
  • "..\TestDir\PathTest.bat"
  • "\Temp\TestDir\PathTest"
  • "\Temp\TestDir\PathTest.bat"
  • "C:\Temp\TestDir\PathTest"
  • "C:\Temp\TestDir\PathTest.bat"

Вывод выполняется в четыре раза C:\Temp\TestDir\, как и ожидалось для тестовых примеров с 5 по 10.

Но для тестовых примеров с 1 по 4 вторая выходная строка просто C:\Temp\ вместо C:\Temp\TestDir\.

Теперь с помощью cd .. измените рабочий каталог на C:\Temp и запустите PathTest.bat следующим образом:

  • "TestDir\PathTest.bat"
  • ".\TestDir\PathTest.bat"
  • "\Temp\TestDir\PathTest.bat"
  • "C:\Temp\TestDir\PathTest.bat"

Результат для второго вывода для тестовых случаев 1 и 2 C:\TestDir\, который вообще не существует.

Запуск командного файла без двойных кавычек дает для всех 4 тестовых случаев правильный вывод.

Это очень ясно, что поведение вызвано ошибкой.

Всякий раз, когда командный файл запускается с двойными кавычками и с путём по отношению к текущему рабочему каталогу при запуске, %~dp0 не является надежным при получении пути к пакетному файлу при изменении текущего рабочего каталога во время пакетного выполнения.

Эта ошибка также сообщается Microsoft в соответствии с Ошибка оболочки Windows с тем, как разрешено% ~ dp0.

Обход этой ошибки можно обойти, сразу же назначив путь к пакетному файлу переменной среды, как показано выше, перед изменением рабочего каталога.

И затем укажите значение этой переменной везде, где требуется путь к пакетному файлу с использованием двойных кавычек, где это необходимо. Что-то вроде %BatchPath% всегда лучше читается как %~dp0.

Другим обходным путем является запуск командного файла всегда с полным путем (и с расширением файла) при использовании двойных кавычек, как это делает класс Process.

Ответ 6

Каждая новая строка в вашей партии, вызываемая вашим ProcessStart, независимо рассматривается как новая команда cmd.

Например, если вы попробуете это сделать:

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

Он работает.