2012-08-27 15 views
18

Tengo un archivo por lotes con el siguiente contenidoarchivo por lotes ~ dp0 cambios en el cambio de directorio

echo %~dp0 
CD Arvind 
echo %~dp0 

Incluso después de cambiar el valor de directorio si es ~ dp0 misma. Sin embargo, si ejecuto este archivo por lotes desde el programa CSharp, el valor de ~ dp0 cambia después del CD. Ahora apunta a un nuevo directorio. A continuación se muestra 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é existe una diferencia en el resultado al ejecutar el mismo script de diferentes formas? ¿Me falta algo aquí?

+5

Puede replicar el comportamiento cuando ejecuta el lote desde 'cmd' mediante' "mybatfile.cmd" '(yes, * with * quotes). Esa es exactamente la invocación que obtienes al ejecutar a través de 'Process.Start' como también puedes verificar mediante' echo'ing '% 0'. – Joey

+0

Gracias Joey, tus sugerencias fueron el truco. Funciona para mi ahora. –

+0

Bueno, fue solo una observación; Todavía no puedo explicar el comportamiento :-) – Joey

Respuesta

-3

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

Por ejemplo, si le dan una oportunidad como esta:

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

Funciona.

+1

No por la razón que crees que es. Es solo que '% ~ dp0' se reemplaza por los valores reales antes de la ejecución de la línea, por lo tanto, el segundo' echo' ya genera texto estático antes de ejecutar el 'cd'. – Joey

+0

@Joey: entonces, la ejecución de ProcessStart o de doble cita hace que el script se ejecute línea por línea, y no como un todo, por lo tanto, el reemplazo de% ~ dp0 que da valores diferentes en los 2 casos diferentes? – LaGrandMere

+1

Los archivos por lotes siempre se ejecutan línea por línea y siempre se ejecutan con una sola instancia de 'cmd'. Su supuesta solución es solo un artefacto de cómo se realiza la expansión variable. – Joey

4

La sugerencia de Joey ayudó. Sólo mediante la sustitución de

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

con

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

hizo el truco.

+5

Aún así, me encantaría saber qué está pasando –

2

Es un problema con las comillas y %~0.

cmd.exe se encarga de %~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 encuentra un archivo, utilizará 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 cual cmd /c myBatch.bat funciona, como entonces se llama a myBatch.bat sin comillas.
También puede iniciar el lote con una ruta completa, luego también funciona.

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

Una pequeña test.bat pueden demostrar los problemas de cmd.exe

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

Call que a través de (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 precederá al directorio de inicio.

En el caso de llamarlo a través

"test" "test" 

falla con

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

cmd.exe no es capaz de encontrar el archivo por lotes, incluso antes de que el directorio se ha cambiado, no se puede ampliar el nombre con el nombre completo de c:\temp\test.bat

+0

Parece que tanto C# 'ProcessStartInfo' como el procesador de Java 'ProcessBuilder' están agregando comillas al nombre del ejecutable, o el mecanismo subyacente de O/S que comparten está haciendo eso. Es eso controlable? – Rich

+0

@ Rich, hay un error en la gestión de cotizaciones de 'cmd'. Ver [aquí] (http://stackoverflow.com/a/26851883/2861476). Use rutas completas en la llamada o agregue código en el archivo por lotes para asegurarse de obtener los valores correctos. –

4

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

ProcessInfo.UseShellExecute = false; 

Vas a ver que si se omite esta declaración o asignar cierto que funciona como se esperaba.

Windows proporciona dos formas básicas para iniciar programas, 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 winapi de bajo nivel, hay muy poco pegamento entre ella y la función de kernel nativo (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 que CreateProcess() proporciona dos formas distintas de iniciar un programa. 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 hace no hacer esto cuando utiliza el argumento lpApplicationName, pasa la cadena tal como está.

Esta peculiaridad tiene un efecto en los programas que dependen de la forma exacta en que se especifica la línea de comandos. Particularmente para los programas C, suponen que argv[0] contiene la ruta a su archivo ejecutable. Y tiene un efecto en %~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, ¿qué son su supuesta a hacer, y esto es totalmente poco documentada en la documentación de .NET Framework, es que es ahora depende de usted para pasar el nombre de ruta completo al archivo. Por lo que el código apropiado debe ser similar:

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 ahora %~dp0 expande como se esperaba. Está utilizando path en lugar del directorio actual.

+0

¿Cuál es la solución correcta si no conoce la ruta completa (es decir, desea una búsqueda%% PATH%)? ¿Necesitas hacer una búsqueda%% PATH% manual en C#? – Rich

+1

Erm, sí. Usar cmd.exe/c llega a ser bastante atractivo por supuesto :) –

1

Línea de comando intérprete cmd.exe tiene una error en el código de en conseguir la ruta del archivo por lotes si el archivo por lotes se ha llamado con comillas dobles y con una ruta relativa al directorio de trabajo actual.

Crear un directorio C: \ Temp \ TestDir. Crear dentro de este directorio un archivo con el nombre PathTest.bat copiar y pegar 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 restoring working directory is: %~dp0 

A continuación abra una ventana de símbolo del sistema y configurar el directorio de trabajo a C: \ Temp \ TestDir usando el comando:

cd /D C:\Temp\TestDir 

Ahora llamar Prueba.bat en las formas siguientes:

  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

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

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

Ahora vamos a ver en los resultados en hacer lo mismo que antes, pero con el uso de comillas dobles nombre de 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"

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

Sin embargo, para los casos de prueba 1 a 4 de la segunda línea de salida es sólo C: \ temp \ en lugar de C: \ temp \ TestDir \.

Ahora usa cd .. para cambiar el directorio de trabajo a C: \ Temp y ejecutar 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.

Al iniciar el archivo de proceso por lotes sin las comillas dobles, se obtiene el resultado correcto para los 4 casos de prueba.

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 según Windows shell bug with how %~dp0 is resolved.

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 a continuación, 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% es siempre mejor legible como %~dp0.

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

+1

Sí, hay un error. Si está interesado, vea [mi respuesta] (http://stackoverflow.com/a/26851883/2861476). –

5

This question comenzó la discusión sobre este punto, y algunas pruebas se realizaron para determinar por qué. Así, después de una cierta depuración en el interior cmd.exe ... (esto es para un 32 bits de Windows XP cmd.exe pero a medida que el comportamiento es consistente en las versiones más recientes del sistema, probablemente se utiliza la misma o similar código)

Dentro de Jeb la respuesta se dice

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

y aquí Jeb es el correcto.

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), entonces los datos de la referencia del lote se ejecuta "variable" es usado.

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. Se codifica algo así como (ensamblador aquí simplificado para 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, la primera parte de la if se ejecuta y la referencia completa al archivo por lotes no es utilizado, en cambio, el valor recuperado es la cadena utilizada para hacer referencia al archivo por lotes cuando lo llama.

¿Qué sucede 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 simple archivo por lotes (test.cmd)

@echo off 
echo %~f0 

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

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

En el primer caso, sin comillas, se utiliza 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 ni se utiliza. Como estamos solicitando una ruta completa, se considera una referencia relativa a algo llamado test.

Este es el motivo. ¿Cómo resolver?

Desde el código C#

  • No use comillas: cmd /c batchfile.cmd

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

Desde el archivo por lotes

archivo por lotes puede ser invocada en cualquier forma desde cualquier lugar. La única forma confiable de recuperar la información del archivo por lotes actual es usar una subrutina. Si se utiliza 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 a la consola la ruta completa al archivo por lotes actual independtly de cómo se llama el 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 call, se crea un nuevo contexto de archivo por lotes en la memoria y la "variable" interna se utiliza para inicializar este contexto.

Cuestiones relacionadas