Cómo ejecutar un script de PowerShell dentro de un archivo por lotes de Windows

¿Cómo tengo un script de PowerShell incrustado en el mismo archivo que un script de lote de Windows?

Sé que este tipo de cosas es posible en otros escenarios:

  • Incrustar SQL en un script por lotes usando sqlcmd y una disposición inteligente de goto’s y comentarios al comienzo del archivo
  • En un entorno * nix que tenga el nombre del progtwig con el que desea ejecutar el script, en la primera línea del script se comentará, por ejemplo, #!/usr/local/bin/python .

Es posible que no haya una forma de hacerlo, en cuyo caso tendré que llamar al script de PowerShell por separado desde el script de inicio.

Una posible solución que he considerado es repetir el script de PowerShell y luego ejecutarlo. Una buena razón para no hacer esto es que parte de la razón para intentar esto es utilizar las ventajas del entorno de PowerShell sin el dolor de, por ejemplo, caracteres de escape.

Tengo algunas limitaciones inusuales y me gustaría encontrar una solución elegante. Sospecho que esta pregunta puede ser respuestas de cebo de la variedad: “Por qué no tratas de resolver este problema diferente”. Basta con decir que estas son mis limitaciones, lo siento.

¿Algunas ideas? ¿Existe una combinación adecuada de comentarios inteligentes y personajes de escape que me permitan lograr esto?

Algunas ideas sobre cómo lograr esto:

  • Un quilate ^ al final de una línea es una continuación, como un guión bajo en Visual Basic
  • Un ampersand & general, se usa para separar los comandos echo Hello & echo World da como resultado dos ecos en líneas separadas
  • % 0 le dará la secuencia de comandos que se está ejecutando actualmente

Entonces algo como esto (si pudiera hacerlo funcionar) sería bueno:

 # & call powershell -psconsolefile %0 # & goto :EOF /* From here on in we're running nice juicy powershell code */ Write-Output "Hello World" 

Excepto…

  • No funciona … porque
  • la extensión del archivo no es del gusto de PowerShell: la extensión del archivo de Windows PowerShell console file "insideout.bat" extension is not psc1. Windows PowerShell console file extension must be psc1. Windows PowerShell console file "insideout.bat" extension is not psc1. Windows PowerShell console file extension must be psc1.
  • CMD tampoco está del todo contento con la situación, aunque tropieza con '#', it is not recognized as an internal or external command, operable program or batch file.

Este solo pasa las líneas correctas a PowerShell:

dosps2.cmd :

 @findstr/v "^@f.*&" "%~f0"|powershell -&goto:eof Write-Output "Hello World" Write-Output "Hello some@com & again" 

La expresión regular excluye las líneas que comienzan con @f e incluyen un & y pasa todo lo demás a PowerShell.

 C:\tmp>dosps2 Hello World Hello some@com & again 

Parece que estás buscando lo que a veces se llama una “secuencia de comandos políglota”. Para CMD -> PowerShell,

 @@:: This prolog allows a PowerShell script to be embedded in a .CMD file. @@:: Any non-PowerShell content must be preceeded by "@@" @@setlocal @@set POWERSHELL_BAT_ARGS=%* @@if defined POWERSHELL_BAT_ARGS set POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"% @@PowerShell -Command Invoke-Expression $('$args=@(^&{$args} %POWERSHELL_BAT_ARGS%);'+[String]::Join([char]10,$((Get-Content '%~f0') -notmatch '^^@@'))) & goto :EOF 

Si no necesita admitir argumentos entrecomillados, incluso puede hacerlo de una sola línea:

 @PowerShell -Command Invoke-Expression $('$args=@(^&{$args} %*);'+[String]::Join([char]10,(Get-Content '%~f0') -notmatch '^^@PowerShell.*EOF$')) & goto :EOF 

Tomado de http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx . Eso fue PowerShell v1; puede ser más simple en v2, pero no he buscado.

Aquí el tema ha sido discutido. Los objectives principales eran evitar el uso de archivos temporales para reducir las operaciones lentas de IO y ejecutar el script sin resultados redundantes.

Y esta es la mejor solución según yo:

 <# : @echo off setlocal set "POWERSHELL_BAT_ARGS=%*" if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%" endlocal & powershell -NoLogo -NoProfile -Command "$input | &{ [ScriptBlock]::Create( ( Get-Content \"%~f0\" ) -join [char]10 ).Invoke( @( &{ $args } %POWERSHELL_BAT_ARGS% ) ) }" goto :EOF #> param( [string]$str ); $VAR = "Hello, world!"; function F1() { $str; $script:VAR; } F1; 

Editar (una mejor manera vista aquí )

 <# : batch portion (begins PowerShell multi-line comment block) @echo off & setlocal set "POWERSHELL_BAT_ARGS=%*" echo ---- FROM BATCH powershell -noprofile -NoLogo "iex (${%~f0} | out-string)" exit /b %errorlevel% : end batch / begin PowerShell chimera #> $VAR = "---- FROM POWERSHELL"; $VAR; $POWERSHELL_BAT_ARGS=$env:POWERSHELL_BAT_ARGS $POWERSHELL_BAT_ARGS 

donde POWERSHELL_BAT_ARGS son argumentos de línea de comando establecidos primero como variables en la parte del lote.

El truco está en la prioridad de redirección por lotes: esta línea <# : se analizará como :<# porque la redirección es con prio más alto que los otros comandos. Pero las líneas que comienzan con : en los archivos por lotes se toman como tags, es decir, no se ejecutan. Aún así, este sigue siendo un comentario válido de Powershell.

Lo único que queda es encontrar una forma adecuada para que powershell lea y ejecute %~f0 que es la ruta completa al script ejecutado por cmd.exe.

Esto parece funcionar, si no te importa un error en PowerShell al principio:

dosps.cmd :

 @powershell -<%~f0&goto:eof Write-Output "Hello World" Write-Output "Hello World again" 

Considere también esta secuencia de comandos de envoltorio “políglota” , que admite cómputo incorporado de PowerShell y / o VBScript / JScript ; fue adaptado de este ingenioso original , que el propio autor, flabdablet , había publicado en 2013, pero languideció debido a que era una respuesta de solo enlace, que fue eliminada en 2015.

Una solución que mejora la excelente respuesta de Kyle :

 <# :: @setlocal & copy "%~f0" "%TEMP%\%~0n.ps1" >NUL && powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\%~0n.ps1" %* @set "ec=%ERRORLEVEL%" & del "%TEMP%\%~0n.ps1" @exit /b %ec% #> # Paste arbitrary PowerShell code here. # In this example, all arguments are echoed. 'Args:' $Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ } 

Nota: Un archivo temporal *.ps1 que se limpia posteriormente se crea en la carpeta %TEMP% ; hacerlo simplifica enormemente el pasar argumentos a través de (razonablemente) robustos, simplemente usando %*

  • Line <# :: es una línea híbrida que PowerShell ve como el inicio de un bloque de comentarios, pero cmd.exe ignora, una técnica tomada de la respuesta de npocmaka .

  • Los comandos de archivo por lotes que comienzan con @ son, por tanto, ignorados por PowerShell, pero ejecutados por cmd.exe ; dado que la última línea @prefixed finaliza con exit /b , que sale del archivo de proceso por lotes allí mismo, cmd.exe ignora el rest del archivo, por lo tanto, es libre de contener código de archivo no por lotes, es decir, código de PowerShell.

  • La línea #> finaliza el bloque de comentarios de PowerShell que incluye el código del archivo por lotes.

  • Debido a que el archivo como un todo es, por lo tanto, un archivo válido de PowerShell, no se necesita engaño de findstr para extraer el código de PowerShell; sin embargo, dado que PowerShell solo ejecuta scripts que tienen la extensión de archivo .ps1 , se debe .ps1 una copia (temporal) del archivo por lotes; %TEMP%\%~0n.ps1 crea la copia temporal en la carpeta %TEMP% nombrada para el archivo por lotes ( %~0n ), pero con la extensión .ps1 en .ps1 lugar; el archivo temporalmente se elimina automáticamente al finalizar.

  • Tenga en cuenta que se necesitan 3 líneas separadas de sentencias cmd.exe para pasar el código de salida del comando PowerShell.
    (Usar setlocal enabledelayedexpansion hipotéticamente permite hacerlo como una sola línea, pero eso puede dar como resultado una interpretación no deseada de ! Chars. En argumentos).


Para demostrar la solidez del argumento que pasa :

Suponiendo que el código anterior se haya guardado como sample.cmd , invocándolo como:

 sample.cmd "val. w/ spaces & special chars. (\|<>'), on %OS%" 666 "Lisa \"Left Eye\" Lopez" 

produce algo como lo siguiente:

 Args: arg #1: [val. w/ spaces & special chars. (\|<>'), on Windows_NT] arg #2: [666] arg #3: [Lisa "Left Eye" Lopez] 

Tenga en cuenta cómo incrustados " caracteres. Se pasaron como \" .
Sin embargo, hay casos límite relacionados con " caracteres " incrustados.

 :: # BREAKS, due to the `&` inside \"...\" sample.cmd "A \"rock & roll\" life style" :: # Doesn't break, but DOESN'T PRESERVE ARGUMENT BOUNDARIES. sample.cmd "A \""rock & roll\"" life style" 

Estas dificultades se deben al análisis de argumentos defectuoso de cmd.exe , y en última instancia es inútil tratar de ocultar estos defectos, como señala flabdablet en su excelente respuesta .

Como explica, escapando de los siguientes metacaracteres cmd.exe con ^^^ (sic) dentro de la secuencia \"...\" resuelve el problema:

 & | < > 

Usando el ejemplo de arriba:

 :: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped. sample.cmd "A \"rock ^^^& roll\" life style" 

Sin entender completamente tu pregunta, mi sugerencia sería algo así como:

 @echo off set MYSCRIPT="some cool powershell code" powershell -c %MYSCRIPT% 

o mejor aún

 @echo off set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1 powershell %MYSCRIPTPATH% 

Esto admite argumentos a diferencia de la solución publicada por Carlos y no rompe los comandos de varias líneas o el uso de param como la solución publicada por Jay. El único inconveniente es que esta solución crea un archivo temporal. Para mi caso de uso es aceptable.

 @@echo off @@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof 

Mi preferencia actual para esta tarea es un encabezado políglota que funciona de la misma manera que la primera solución de mklement0 :

 <# :cmd header for PowerShell script @ set dir=%~dp0 @ set ps1="%TMP%\%~n0-%RANDOM%-%RANDOM%-%RANDOM%-%RANDOM%.ps1" @ copy /b /y "%~f0" %ps1% >nul @ powershell -NoProfile -ExecutionPolicy Bypass -File %ps1% %* @ del /f %ps1% @ goto :eof #> # Paste arbitrary PowerShell code here. # In this example, all arguments are echoed. $Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ } 

Prefiero colocar el encabezado cmd como líneas múltiples con un solo comando en cada uno, por varias razones. Primero, creo que es más fácil ver lo que está pasando: las líneas de comando son lo suficientemente cortas como para no funcionar a la derecha de mis ventanas de edición, y la columna de puntuación a la izquierda lo marca visualmente como el bloque de encabezado que la etiqueta horriblemente abusada la primera línea dice que es. En segundo lugar, los comandos del y goto están en sus propias líneas, por lo que seguirán ejecutándose incluso si pasa algo realmente funky como argumento del script.

He llegado a preferir soluciones que crean un archivo .ps1 temporal para aquellos que dependen de Invoke-Expression , simplemente porque los mensajes de error inescrutable de PowerShell incluirán al menos números de línea significativos.

El tiempo que demora en crear el archivo temporal por lo general se ve completamente afectado cuando PowerShell tarda en activarse, y 128 bits en %RANDOM% incrustados en el nombre del archivo temporal garantiza que múltiples scripts concurrentes no lo harán nunca. pisar los archivos temporales de cada uno. El único inconveniente real del enfoque de archivo temporal es la posible pérdida de información sobre el directorio desde el que se invocó el script cmd original, que es el fundamento de la variable de entorno dir creada en la segunda línea.

Obviamente, PowerShell no sería tan molesto con las extensiones de nombre de archivo que aceptará en los archivos de script, pero usted entra en guerra con el shell que tiene, no con el shell que desea tener.

Hablando de eso: como mklement0 observa,

 # BREAKS, due to the `&` inside \"...\" sample.cmd "A \"rock & roll\" life style" 

Esto sí se rompe, debido al análisis de argumentos completamente inútil de cmd.exe . En general, he descubierto que cuanto menos trabajo hago para tratar de ocultar las muchas limitaciones de cmd, menos errores imprevistos me encuentro al final de la línea (estoy seguro de que podría encontrar argumentos que contengan paréntesis que rompan la validación de mklement0, que de otro modo sería impecable) , por ejemplo). Menos doloroso, en mi opinión, solo para morder la bala y usar algo como

 sample.cmd "A \"rock ^^^& roll\" life style" 

El primer y el tercer escape se come cuando esa línea de comando se analiza inicialmente; el segundo sobrevive para escapar del & embedded en la línea de comando pasada a powershell.exe . Sí, esto es feo Sí, hace más difícil pretender que cmd.exe no es lo que obtiene el primer crack en el script. No te preocupes por eso Documentarlo si es importante.

En la mayoría de las aplicaciones del mundo real, el problema es discutible de todos modos. La mayoría de lo que se pasará como argumentos para un script como este serán los nombres de ruta que lleguen mediante arrastrar y soltar. Windows los citará, lo cual es suficiente para proteger espacios y símbolos y, de hecho, cualquier cosa que no sean comillas, que de todos modos no están permitidas en las rutas de Windows.

Ni siquiera me pones a trabajar en Vinyl LP's, 12" apareciendo en un archivo CSV.

Otro lote de muestra + secuencia de comandos de PowerShell … Es más simple que la otra solución propuesta y tiene características que ninguno de ellos puede igualar:

  • Sin creación de un archivo temporal => Mejor rendimiento y sin riesgo de sobrescribir nada.
  • Sin prefixing especial del código de lote. Esto es solo un lote normal. Y lo mismo para el código de PowerShell.
  • Pasa todos los argumentos por lotes a PowerShell correctamente, incluso cadenas de caracteres complicados como! % <> ‘$
  • Se pueden pasar comillas dobles al doblarlas.
  • La entrada estándar se puede usar en PowerShell. (Al contrario de todas las versiones que canalizan el lote a PowerShell).

Este ejemplo muestra las transiciones de idioma, y ​​el lado de PowerShell muestra la lista de argumentos que recibió del lado del lote.

 <# :# PowerShell comment protecting the Batch section @echo off :# Disabling argument expansion avoids issues with ! in arguments. setlocal EnableExtensions DisableDelayedExpansion :# Prepare the batch arguments, so that PowerShell parses them correctly set ARGS=%* if defined ARGS set ARGS=%ARGS:"=\"% if defined ARGS set ARGS=%ARGS:'=''% :# The ^ before the first " ensures that the Batch parser does not enter quoted mode :# there, but that it enters and exits quoted mode for every subsequent pair of ". :# This in turn protects the possible special chars & | < > within quoted arguments. :# Then the \ before each pair of " ensures that PowerShell's C command line parser :# considers these pairs as part of the first and only argument following -c. :# Cherry on the cake, it's possible to pass a " to PS by entering two "" in the bat args. echo In Batch PowerShell -c ^"Invoke-Expression ('^& {' + [io.file]::ReadAllText(\"%~f0\") + '} %ARGS%')" echo Back in Batch. PowerShell exit code = %ERRORLEVEL% exit /b ############################################################################### End of the PS comment around the Batch section; Begin the PowerShell section #> echo "In PowerShell" $Args | % { "PowerShell Args[{0}] = '$_'" -f $i++ } exit 0 

Tenga en cuenta que utilizo: # para los comentarios por lotes, en lugar de :: como la mayoría de las personas lo hacen, ya que esto realmente los hace parecerse a los comentarios de PowerShell. (O como la mayoría de los otros comentarios de idiomas de scripting en realidad).