Come eseguire uno script PowerShell all’interno di un file batch di Windows

Come posso incorporare uno script PowerShell nello stesso file di uno script batch di Windows?

So che questo tipo di cose è ansible in altri scenari:

  • Incorporare SQL in uno script batch usando sqlcmd e una disposizione intelligente di goto e commenti all’inizio del file
  • In un ambiente * nix con il nome del programma con cui si desidera eseguire lo script nella prima riga dello script commentato, ad esempio, #!/usr/local/bin/python .

Potrebbe non esserci un modo per farlo – nel qual caso dovrò chiamare lo script PowerShell separato dallo script di avvio.

Una ansible soluzione che ho considerato è quella di echeggiare lo script di PowerShell e quindi eseguirlo. Un buon motivo per non farlo è che parte del motivo per tentare questo è quello di utilizzare i vantaggi dell’ambiente PowerShell senza il dolore, ad esempio, dei caratteri di escape

Ho alcuni vincoli insoliti e vorrei trovare una soluzione elegante. Sospetto che questa domanda possa essere la risposta esaltante della varietà: “Perché non provi a risolvere questo diverso problema”. Basti dire che questi sono i miei limiti, mi dispiace per quello.

Qualche idea? Esiste una combinazione adeguata di commenti intelligenti e personaggi di fuga che mi consentiranno di ottenere questo risultato?

Alcuni pensieri su come ottenere questo:

  • Una carat ^ alla fine di una riga è una continuazione, come un trattino basso in Visual Basic
  • Una & commerciale & genere viene utilizzata per separare i comandi echo Hello & echo World produce due echo Hello & echo World su linee separate
  • % 0 ti darà lo script attualmente in esecuzione

Quindi qualcosa del genere (se potessi farlo funzionare) andrebbe bene:

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

Tranne …

  • Non funziona … perché
  • l’estensione del file non è come piace a PowerShell: l’ 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.
  • Anche CMD non è del tutto contento della situazione – sebbene inciampi su '#', it is not recognized as an internal or external command, operable program or batch file.

Questo solo passa le linee giuste a PowerShell:

dosps2.cmd :

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

L’ espressione regolare esclude le righe che iniziano con @f e include un & e passa tutto il resto a PowerShell.

 C:\tmp>dosps2 Hello World Hello [email protected] & again 

Sembra che tu stia cercando quello che a volte viene chiamato “script poliglotta”. Per 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 $('[email protected](^&{$args} %POWERSHELL_BAT_ARGS%);'+[String]::Join([char]10,$((Get-Content '%~f0') -notmatch '^^@@'))) & goto :EOF 

Se non hai bisogno di supportare argomenti citati, puoi persino renderlo un one-liner:

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

Tratto da http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx . Quello era PowerShell v1; potrebbe essere più semplice nella v2, ma non l’ho visto.

Qui l’argomento è stato discusso. Gli obiettivi principali erano evitare l’uso di file temporanei per ridurre le operazioni di I / O lente e per eseguire lo script senza output ridondante.

Ed ecco la soluzione migliore secondo me:

 <# : @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; 

Modifica (un modo migliore visto qui )

 <# : 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 

dove POWERSHELL_BAT_ARGS sono argomenti della riga di comando impostati per primi come variabili nella parte batch.

Il trucco è nella priorità di reindirizzamento batch – questa riga <# : sarà analizzata come :<# perché il reindirizzamento è con un prio più alto rispetto agli altri comandi. Ma le righe che iniziano con : nei file batch vengono prese come etichette, cioè non eseguite. Ancora questo rimane un commento di PowerShell valido.

L'unica cosa che rimane è trovare un modo corretto per PowerShell di leggere ed eseguire %~f0 che è il percorso completo per lo script eseguito da cmd.exe.

Questo sembra funzionare, se non ti dispiace un errore in PowerShell all’inizio:

dosps.cmd :

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

Considera anche questo script di wrapper “polyglot” , che supporta codec PowerShell e / o VBScript / JScript incorporati ; è stato adattato da questo originale geniale , che l’autore stesso, flabdablet , aveva pubblicato nel 2013, ma ha perso la sua validità a causa di una sola risposta di collegamento, che è stata cancellata nel 2015.

Una soluzione che migliora l’eccellente risposta di 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 file *.ps1 temporaneo che viene ripulito in seguito viene creato nella cartella %TEMP% ; così facendo si semplifica enormemente il passaggio degli argomenti attraverso (ragionevolmente) in modo robusto, semplicemente usando %*

  • Line <# :: è una linea ibrida che PowerShell vede come inizio di un blocco di commenti, ma cmd.exe ignora, una tecnica presa in prestito dalla risposta di npocmaka .

  • I comandi batch-file che iniziano con @ vengono quindi ignorati da PowerShell, ma eseguiti da cmd.exe ; dato che l'ultima riga @ -prefixed termina con exit /b , che esce proprio dal file batch, cmd.exe ignora il resto del file, che è quindi libero di contenere codice di file non batch, ad esempio codice PowerShell.

  • La riga #> termina il blocco dei commenti di PowerShell che include il codice del file batch.

  • Poiché il file nel suo complesso è quindi un file PowerShell valido, non è necessario alcun trucco di findstr per estrarre il codice di PowerShell; tuttavia, poiché PowerShell esegue solo script con estensione .ps1 , è necessario creare una copia (temporanea) del file batch; %TEMP%\%~0n.ps1 crea la copia temporanea nella cartella %TEMP% chiamata per il file batch ( %~0n ), ma con l'estensione .ps1 ; il file temporaneamente viene automaticamente rimosso al completamento.

  • Si noti che sono necessarie 3 righe separate di istruzioni cmd.exe per passare il codice di uscita del comando PowerShell.
    (L'uso setlocal enabledelayedexpansion consente ipoteticamente di farlo come una singola riga, ma ciò può causare un'interpretazione indesiderata di ! Char. Negli argomenti.)


Per dimostrare la robustezza dell'argomento che passa :

Supponendo che il codice sopra sia stato salvato come sample.cmd , invocandolo come:

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

produce qualcosa come il seguente:

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

Nota come sono stati passati " caratteri " come \" .
Tuttavia, ci sono casi limite relativi ai caratteri " incorporati " :

 :: # 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" 

Queste difficoltà sono dovute cmd.exe errata dell'argomento di cmd.exe e , in definitiva, è inutile cercare di hide questi difetti, come sottolinea flabdablet nella sua eccellente risposta .

Come spiega, l' escape dei seguenti metacaratteri cmd.exe con ^^^ (sic) all'interno della sequenza \"...\" risolve il problema:

 & | < > 

Utilizzando l'esempio sopra:

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

Senza comprendere appieno la tua domanda, il mio suggerimento sarebbe qualcosa di simile:

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

o meglio ancora

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

Questo supporta argomenti a differenza della soluzione pubblicata da Carlos e non interrompe comandi multi-linea o l’uso di param come la soluzione postata da Jay. L’unico svantaggio è che questa soluzione crea un file temporaneo. Per il mio caso d’uso è accettabile.

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

La mia attuale preferenza per questa attività è un’intestazione poliglotta che funziona allo stesso modo della prima soluzione di 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, $_ } 

Preferisco posizionare l’intestazione di cmd come linee multiple con un singolo comando su ognuna, per una serie di motivi. In primo luogo, penso che sia più facile vedere cosa sta succedendo: le righe di comando sono abbastanza corte da non scorrere a destra delle mie windows di modifica, e la colonna di punteggiatura a sinistra la contrassegna visivamente come il blocco di intestazione che l’etichetta orribilmente abusata su la prima riga dice che lo è. In secondo luogo, i comandi del e goto sono sulle loro linee, quindi continueranno a funzionare anche se qualcosa di veramente funky viene passato come argomento di script.

Sono giunto a preferire soluzioni che rendano un file .ps1 temporaneo a quelli che si basano su Invoke-Expression , semplicemente perché i messaggi di errore imperscrutabili di PowerShell includeranno almeno numeri di riga significativi.

Il tempo necessario per rendere il file temporaneo è in genere completamente assorbito dal tempo impiegato da PowerShell per eseguire l’azione e 128 bit del %RANDOM% incorporati nel nome del file temporaneo garantisce praticamente che più script simultanei non saranno mai stomp reciprocamente i file temporanei. L’unico vero svantaggio dell’approccio al file temporaneo è la ansible perdita di informazioni sulla directory da cui è stato invocato lo script cmd originale, che è la motivazione della variabile di ambiente dir creata sulla seconda riga.

Ovviamente sarebbe molto meno fastidioso per PowerShell non essere così anonimo riguardo alle estensioni dei nomi dei file che accetterà sui file di script, ma vai in guerra con la shell che hai, non con la shell che desideri.

A proposito: come osserva mklement0,

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

Questo, in effetti, si interrompe, a causa dell’analisi dell’argomento completamente privo di valore di cmd.exe . Generalmente ho trovato che meno lavoro faccio per hide le numerose limitazioni di cmd, meno errori imprevisti mi causano (sono sicuro che potrei inventare argomenti contenenti parentesi che spezzerebbero la logica altrimenti ineccepibile di mklement0 e la logica di fuga , per esempio). Meno doloroso, a mio avviso, solo per mordere il proiettile e usare qualcosa del genere

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

Il primo e il terzo ^ escapes vengono mangiati quando la riga di comando viene inizialmente analizzata; il secondo sopravvive per sfuggire al & incorporato nella riga di comando passata a powershell.exe . Sì, questo è brutto. Sì, rende più difficile far finta che cmd.exe non sia quello che ottiene il primo crack nello script. Non preoccuparti per questo Documentalo se è importante.

Nella maggior parte delle applicazioni del mondo reale, il problema è comunque valido. La maggior parte di ciò che verrà passato come argomento a uno script come questo sarà il percorso che arriva tramite trascinamento della selezione. Windows citerà quelli, che è sufficiente per proteggere gli spazi e la e commerciale e in realtà qualsiasi cosa diversa dalle virgolette, che comunque non sono consentite nei nomi di percorso di Windows.

Non riesco nemmeno a farmi iniziare su Vinyl LP's, 12" si presenta in un file CSV.

Un altro batch di esempio + script PowerShell … È più semplice dell’altra soluzione proposta e presenta caratteristiche che nessuno di loro può eguagliare:

  • Nessuna creazione di un file temporaneo => Prestazioni migliori e nessun rischio di sovrascrivere nulla.
  • Nessun prefisso speciale del codice batch. Questo è solo un normale batch. E la stessa cosa per il codice PowerShell.
  • Passa correttamente tutti gli argomenti batch a PowerShell, anche le stringhe citate con caratteri complicati come! % <> ‘$
  • Le doppie virgolette possono essere passate raddoppiandole.
  • L’input standard è utilizzabile in PowerShell. (Contrariamente a tutte le versioni che collegano il batch stesso a PowerShell.)

Questo esempio mostra le transizioni della lingua e il lato di PowerShell mostra l’elenco degli argomenti ricevuti dal lato batch.

 <# :# 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 

Nota che uso: # per i commenti in batch, anziché :: come fa la maggior parte delle altre persone, in quanto ciò li rende effettivamente simili ai commenti di PowerShell. (O come la maggior parte dei commenti in altri linguaggi di scripting).