¿Cuál es el motivo de que la ruta del archivo por lotes a la que se hace referencia con% ~ dp0 a veces cambie al cambiar de directorio?

Tengo un archivo por lotes con el siguiente contenido:

echo %~dp0 CD Arvind echo %~dp0 

Incluso después de cambiar el valor de directorio de %~dp0 es el mismo. Sin embargo, si ejecuto este archivo por lotes desde el progtwig CSharp, el valor de %~dp0 cambia después del CD . Ahora apunta a un nuevo directorio. A continuación está el código que uso:

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

¿Por qué hay una diferencia en el resultado al ejecutar el mismo script de diferentes formas?

¿Extraño algo aquí?

Esta pregunta comenzó la discusión sobre este punto, y se realizaron algunas pruebas para determinar por qué. Entonces, después de algunas depuraciones dentro de cmd.exe … (esto es para un cmd.exe de Windows XP de 32 bits pero como el comportamiento es consistente en las versiones de sistema más nuevas, probablemente se use el mismo código o uno similar)

Dentro de la respuesta de Jeb se afirma

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

y aquí Jeb está en lo cierto.

Dentro del contexto actual del archivo por lotes en ejecución hay una referencia al archivo por lotes actual, una “variable” que contiene la ruta completa y el nombre de archivo del archivo por lotes en ejecución.

Cuando se accede a una variable, su valor se recupera de una lista de variables disponibles, pero si la variable solicitada es %0 , y se ha solicitado algún modificador ( ~ se utiliza), se utilizan los datos en la “variable” de referencia del lote en ejecución.

Pero el uso de ~ tiene otro efecto en las variables. Si el valor es cotizado, las cotizaciones se eliminan. Y aquí hay un error en el código. Está codificado algo así como (aquí simplificado ensamblador a pseudocódigo)

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

Y sí, esto significa que cuando se cita el archivo por lotes, se ejecuta la primera parte del if y no se utiliza la referencia completa al archivo por lotes, sino que el valor recuperado es la cadena utilizada para hacer referencia al archivo por lotes al llamarlo.

¿Qué pasa entonces? Si cuando se llamó al archivo por lotes se utilizó la ruta completa, entonces no habrá problema. Pero si la ruta completa no se utiliza en la llamada, se debe recuperar cualquier elemento de la ruta que no esté presente en la llamada por lotes. Esta recuperación asume caminos relativos.

Un archivo por lotes simple ( test.cmd )

 @echo off echo %~f0 

Cuando se llama usando test (sin extensión, sin comillas), obtenemos c:\somewhere\test.cmd

Cuando se llama utilizando "test" (sin extensión, comillas), obtenemos c:\somewhere\test

En el primer caso, sin comillas, se usa el valor interno correcto. En el segundo caso, cuando se cita la llamada, la cadena utilizada para llamar al archivo de proceso por lotes ( "test" ) no se cita y se utiliza. Como estamos solicitando una ruta completa, se considera una referencia relativa a algo llamado test .

Este es el porqué ¿Cómo resolver?

Desde el código C #

  • No use comillas: cmd /c batchfile.cmd

  • Si se necesitan presupuestos, use la ruta completa en la llamada al archivo por lotes. De esa forma %0 contiene toda la información necesaria.

Del archivo por lotes

El archivo por lotes se puede invocar de cualquier manera desde cualquier lugar. La única forma confiable de recuperar la información del archivo por lotes actual es usar una subrutina. Si se usa cualquier modificador ( ~ ), el %0 usará la “variable” interna para obtener los datos.

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

Esto hará eco para consolar la ruta completa al archivo por lotes actual independientemente de cómo llame al archivo, con o sin comillas.

nota : ¿Por qué funciona? ¿Por qué la referencia %~f0 dentro de una subrutina devuelve un valor diferente? Los datos a los que se accede desde el interior de la subrutina no son los mismos. Cuando se ejecuta la call , se crea un nuevo contexto de archivo por lotes en la memoria y la “variable” interna se utiliza para inicializar este contexto.

Trataré de explicar por qué esto se comporta de manera tan extraña. Una historia bastante técnica y prolija, intentaré que se condense. El punto de partida para este problema es:

  ProcessInfo.UseShellExecute = false; 

Verá que si omite esta afirmación o asigna true que funciona como esperaba.

Windows proporciona dos formas básicas para iniciar progtwigs, ShellExecuteEx () y CreateProcess (). La propiedad UseShellExecute selecciona entre esos dos. La primera es la versión “inteligente y amigable”, sabe mucho sobre la forma en que funciona el caparazón, por ejemplo. Por eso puede, por ejemplo, pasar la ruta a un archivo arbitrario como “foo.doc”. Sabe cómo buscar la asociación de archivos .doc y encuentra el archivo .exe que sabe cómo abrir foo.doc.

CreateProcess () es la función de winapi de bajo nivel, hay muy poco pegamento entre ella y la función de núcleo nativa (NtCreateProcess). Tenga en cuenta los primeros dos argumentos de la función, lpApplicationName y lpCommandLine , puede hacer que coincidan fácilmente con las dos propiedades de ProcessStartInfo.

Lo que no es tan visible es que CreateProcess () proporciona dos formas distintas de iniciar un progtwig. El primero es donde deja lpApplicationName establecido en una cadena vacía y utiliza lpCommandLine para proporcionar toda la línea de comandos. Eso hace que CreateProcess sea amigable, expande automáticamente el nombre de la aplicación a la ruta completa después de ubicar el ejecutable. Entonces, por ejemplo, “cmd.exe” se expande a “c: \ windows \ system32 \ cmd.exe”. Pero esto no ocurre cuando utiliza el argumento lpApplicationName, pasa la cadena tal como está.

Esta peculiaridad tiene un efecto en los progtwigs que dependen de la forma exacta en que se especifica la línea de comando. Particularmente para los progtwigs C, suponen que argv[0] contiene la ruta a su archivo ejecutable. Y tiene un efecto sobre %~dp0 , también usa ese argumento. Y los problemas en su caso ya que la ruta con la que trabaja es simplemente “mybatfile.bat” en lugar de, digamos, “c: \ temp \ mybatfile.bat”. Lo que hace que devuelva el directorio actual en lugar de “c: \ temp”.

Entonces, lo que se supone que debes hacer, y esto está totalmente sub documentado en la documentación de .NET Framework, es que ahora depende de ti pasar el nombre completo de la ruta al archivo. Así que el código correcto debería ser similar a:

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

Y verá que %~dp0 ahora se expande como esperaba. Está utilizando path lugar del directorio actual.

La sugerencia de Joey ayudó. Solo reemplazando

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

con

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

Hizo el truco.

Es un problema con las comillas y %~0 .

cmd.exe maneja %~0 de una manera especial (que no sea %~1 ).
Comprueba si %0 es un nombre de archivo relativo, luego lo antepone al directorio de inicio.

Si se puede encontrar un archivo, usará esta combinación, de lo contrario, lo antepone al directorio real.
Pero cuando el nombre comienza con una cita, parece que no puede eliminar las comillas, antes de anteponer el directorio.

Esa es la razón por la que funciona cmd /c myBatch.bat , ya que se llama a myBatch.bat sin comillas.
También puede iniciar el lote con una ruta completa, luego también funciona.

O guarda la ruta completa en su lote, antes de cambiar el directorio.

Un pequeño test.bat puede demostrar los problemas de cmd.exe

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

Llámalo por (en C: \ temp)

 test test 

La salida debe ser

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

Por lo tanto, cmd.exe pudo encontrar test.bat , pero solo para %~fx0 el directorio de inicio.

En el caso de llamarlo a través de

 "test" "test" 

Falla con

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

cmd.exe no puede encontrar el archivo por lotes incluso antes de que se cambiara el directorio, no puede expandir el nombre al nombre completo de c:\temp\test.bat

El intérprete de línea de comandos cmd.exe tiene un error en el código al obtener la ruta del archivo por lotes si se invocó el archivo por lotes con comillas dobles y con una ruta relativa al directorio de trabajo actual.

Crea un directorio C: \ Temp \ TestDir . Cree dentro de este directorio un archivo con el nombre PathTest.bat y copie y pegue en este archivo por lotes el siguiente código:

 @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 restring working directory is: %~dp0 

Luego abra una ventana del símbolo del sistema y establezca el directorio de trabajo en C: \ Temp \ TestDir usando el comando:

 cd /DC:\Temp\TestDir 

Ahora llame a Test.bat de las siguientes maneras:

  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

La salida es cuatro veces C: \ Temp \ TestDir \ como se espera para los 10 casos de prueba.

Los casos de prueba 7 y 8 inician el archivo por lotes con una ruta relativa al directorio raíz de la unidad actual.

Ahora veamos los resultados al hacer lo mismo que antes, pero con el uso de comillas dobles para el nombre del archivo por lotes.

  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"

La salida es cuatro veces C: \ Temp \ TestDir \ como se esperaba para los casos de prueba 5 a 10.

Pero para los casos de prueba 1 a 4, la segunda línea de salida es simplemente C: \ Temp \ en lugar de C: \ Temp \ TestDir \ .

Ahora use cd .. para cambiar el directorio de trabajo a C: \ Temp y ejecute PathTest.bat de la siguiente manera:

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

El resultado para la segunda salida para los casos de prueba 1 y 2 es C: \ TestDir \ que no existe en absoluto.

Iniciar el archivo por lotes sin las comillas dobles produce para los 4 casos de prueba el resultado correcto.

Esto deja muy claro que el comportamiento es causado por un error.

Cada vez que se inicia un archivo por lotes con comillas dobles y con una ruta relativa al directorio de trabajo actual al inicio, %~dp0 no es confiable para obtener la ruta del archivo por lotes cuando se cambia el directorio de trabajo actual durante la ejecución del lote.

Este error también se informa a Microsoft de acuerdo con el error de shell de Windows con la forma en que se resuelve% ~ dp0 .

Es posible resolver este error asignando la ruta del archivo de proceso por lotes inmediatamente a una variable de entorno como se demostró con el código anterior antes de cambiar el directorio de trabajo.

Y luego haga referencia al valor de esta variable dondequiera que se necesite la ruta del archivo por lotes con el uso de comillas dobles cuando sea necesario. Algo como %BatchPath% siempre es mejor legible como %~dp0 .

Otra solución es comenzar el archivo por lotes siempre con la ruta completa (y con la extensión del archivo) al usar comillas dobles como lo hace la clase Process .

Cada nueva línea en su lote llamada por su ProcessStart se considera independientemente como un nuevo comando cmd.

Por ejemplo, si lo intentas así:

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

Funciona.