tarea - Cómo ejecutar un script de PowerShell dentro de un archivo por lotes de Windows
powershell tutorial (9)
¿Cómo tengo un script de PowerShell incrustado en el mismo archivo que un script por lotes de Windows?
Sé que este tipo de cosas es posible en otros escenarios:
- Incrustar SQL en un script por lotes usando
sqlcmd
y arreglos inteligentes de goto''s y comentarios al principio del archivo - En un entorno * nix que tenga el nombre del programa con el que desea ejecutar el script en la primera línea del script comentado, por ejemplo,
#!/usr/local/bin/python
.
Puede que no haya una manera de hacer esto, en cuyo caso tendré que llamar el script PowerShell separado del 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 usar las ventajas del entorno PowerShell sin el dolor de, por ejemplo, los personajes de escape.
Tengo algunas limitaciones inusuales y me gustaría encontrar una solución elegante. Sospecho que esta pregunta puede ser una respuesta de la variedad: "¿Por qué no intentas resolver este problema diferente en su lugar?" Basta con decir que estas son mis limitaciones, perdón por eso.
¿Algunas ideas? ¿Existe una combinación adecuada de comentarios inteligentes y caracteres de escape que me permitan lograr esto?
Algunas reflexiones 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 signo
&
normalmente se usa para separar los comandosecho Hello & echo World
da como resultado dos ecos en líneas separadas - % 0 te dará el script que se está ejecutando actualmente
Así que 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 como la de PowerShell:
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 una &
y pasan todo lo demás a PowerShell.
C:/tmp>dosps2
Hello World
Hello some@com & again
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 eso es aceptable.
@@echo off
@@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof
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"
Mi preferencia actual por 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 múltiples líneas con un solo comando en cada una, 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 salir por la derecha de mis ventanas de edición, y la columna de puntuación de la izquierda lo marca visualmente como el bloque de encabezado que la etiqueta horriblemente abusada de La primera línea dice que es. Segundo, los comandos del
y goto
están en sus propias líneas, por lo que aún se ejecutarán incluso si algo realmente funky se pasa como un argumento de script.
Llegué a preferir las soluciones que hacen un archivo .ps1
temporal a las que dependen de Invoke-Expression
, simplemente porque los mensajes de error inescrutables de PowerShell incluirán al menos números de línea significativos.
El tiempo que se tarda en hacer que el archivo temporal se vea afectado por completo por el tiempo que le toma a PowerShell entrar en acción, y 128 bits de %RANDOM%
incrustados en el nombre del archivo temporal prácticamente garantiza que no haya múltiples scripts concurrentes pisotear los archivos temporales del otro. El único inconveniente real del enfoque del 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, sería mucho menos molesto para PowerShell no ser tan anal sobre las extensiones de nombre de archivo que aceptará en los archivos de script, pero va a la 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"
De hecho, esto se rompe, debido a que el análisis de argumentos completamente inútil de cmd.exe
. En general, me he dado cuenta de que cuanto menos trabajo haga para tratar de ocultar las muchas limitaciones de cmd, menos errores imprevistos me causo a mí mismo (estoy seguro de que podría encontrar argumentos que contengan paréntesis que romperían la lógica de escape de mklement0). , por ejemplo). Menos doloroso, en mi opinión, solo para morder la bala y usar algo como
sample.cmd "A /"rock ^^^& roll/" life style"
Los escapes primero y tercero se consumen cuando esa línea de comando se analiza inicialmente; el segundo sobrevive para escapar del &
incrustado en la línea de comandos que se pasa a powershell.exe
. Sí, esto es feo. Sí, hace que sea más difícil fingir que cmd.exe
no es lo que se obtiene primero en el script. No te preocupes por eso. Documéntalo si importa.
En la mayoría de las aplicaciones del mundo real, el &
problema es discutible de todos modos. La mayoría de lo que se va a pasar como argumentos a una secuencia de comandos como esta serán las rutas de acceso que llegan mediante arrastrar y soltar. Windows los citará, lo cual es suficiente para proteger espacios y símbolos y, de hecho, cualquier otra cosa que no sean comillas, que de todos modos no están permitidas en las rutas de Windows.
Ni siquiera me refiero a Vinyl LP''s, 12"
apareciendo en un archivo CSV.
Otro ejemplo de script de lotes + PowerShell ... Es más simple que la otra solución propuesta, y tiene características que ninguna de ellas puede igualar:
- Sin creación de un archivo temporal => Mejor rendimiento y sin riesgo de sobrescribir nada.
- Sin prefijo especial del código de lote. Esto es sólo un lote normal. Y lo mismo para el código de PowerShell.
- Pasa todos los argumentos por lotes a PowerShell correctamente, incluso cadenas entre comillas con caracteres complicados como! % <> ''$
- Las comillas dobles se pueden pasar duplicándolas.
- La entrada estándar se puede utilizar 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 uso: # para los comentarios por lotes, en lugar de :: como lo hace la mayoría de las personas, ya que esto realmente los hace parecer comentarios de PowerShell. (O como la mayoría de los otros lenguajes de scripting en realidad.)
Parece que estás buscando lo que a veces se llama un "script 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 entre comillas, incluso puede hacer que sea 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 mirado.
Sin entender completamente tu pregunta, mi sugerencia sería algo 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%
Here el tema ha sido discutido. Los objetivos principales fueron evitar el uso de archivos temporales para reducir las lentas operaciones de E / S y ejecutar el script sin salida redundante.
Y aquí está la mejor solución para mí:
<# :
@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 here )
<# : 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 los argumentos de la línea de comando primero establecidos como variable 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 un prio más alto que los otros comandos. Pero las líneas que comienzan con :
en los archivos por lotes se toman como etiquetas, es decir, no se ejecutan. Todavía esto 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.
También considere este script de envoltura "políglota" , que admite PowerShell y / o VBScript / JScript cod e; se adaptó 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 se eliminó 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 después se crea en la carpeta %TEMP%
; hacerlo así simplifica enormemente la transmisión de argumentos (de manera razonable) de manera robusta, simplemente usando %*
La línea
<# ::
es una línea híbrida que PowerShell ve como el inicio de un bloque de comentarios, perocmd.exe
ignora, una técnica tomada de la respuesta de npocmaka .PowerShell ignora los comandos de archivo por lotes que comienzan con
@
, pero lo ejecutacmd.exe
; ya que la última línea@
-prefixed termina conexit /b
, que sale del archivo por lotes allí mismo,cmd.exe
ignora el resto del archivo, que 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 encierra el código de archivo por lotes.Debido a que el archivo completo es, por lo tanto, un archivo válido de PowerShell, no se necesita ningún
findstr
para extraer el código de PowerShell; sin embargo, dado que PowerShell solo ejecuta scripts que tienen la extensión de nombre 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 temporal 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.
(El uso de lasetlocal enabledelayedexpansion
hipotetic permite hacerlo como una sola línea, pero eso puede resultar en una interpretación no deseada de los caracteres!
En los argumentos).
Para demostrar la robustez 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]
Observe cómo los caracteres incrustados se pasaron como /"
.
Sin embargo, hay casos de borde 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 defectuoso del argumento de cmd.exe
y, en última instancia, no tiene sentido intentar ocultar estas fallas, como señala flabdablet en su excelente respuesta .
Como él explica, escapar 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"