Comment exécuter un script PowerShell dans un fichier de commandes Windows

Comment puis-je intégrer un script PowerShell dans le même fichier qu’un script de lot Windows?

Je sais que ce genre de chose est possible dans d’autres scénarios:

  • Incorporation de SQL dans un script de commandes à l’aide de sqlcmd et arrangements astucieux de goto et de commentaires au début du fichier
  • Dans un environnement * nix ayant le nom du programme que vous souhaitez exécuter avec la première ligne du script, par exemple, #!/usr/local/bin/python .

Il n’y a peut-être pas moyen d’y parvenir, auquel cas je devrai appeler le script PowerShell séparé à partir du script de lancement.

Une solution possible est de faire écho au script PowerShell, puis de l’exécuter. Une bonne raison de ne pas le faire est qu’une partie de la raison de cette tentative est d’utiliser les avantages de l’environnement PowerShell sans se heurter, par exemple, à des caractères d’échappement.

J’ai des contraintes inhabituelles et j’aimerais trouver une solution élégante. Je soupçonne que cette question est peut-être à l’origine de réponses de la variété: “Pourquoi ne tentez-vous pas de résoudre ce problème à la place?” Autant dire que ce sont mes contraintes, désolé pour ça.

Des idées? Existe-t-il une combinaison appropriée de commentaires astucieux et de caractères d’évasion qui me permettront d’y parvenir?

Quelques reflections sur la façon d’y parvenir:

  • Un carat ^ à la fin d’une ligne est une suite – comme un soulignement dans Visual Basic
  • Une esperluette & est généralement utilisée pour séparer les commandes echo Hello & echo World génère deux échos sur des lignes séparées
  • % 0 vous donnera le script en cours d’exécution

Donc, quelque chose comme ça (si je pouvais le faire fonctionner) serait bien:

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

Sauf…

  • Ça ne marche pas … parce que
  • l’extension du fichier n’est pas conforme aux préférences de PowerShell: le 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 n’est pas vraiment satisfait de la situation non plus – même s’il se heurte à '#', it is not recognized as an internal or external command, operable program or batch file.

Celui-ci ne passe que les bonnes lignes à PowerShell:

dosps2.cmd :

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

L’ expression régulière exclut les lignes commençant par @f et incluant un & et transmet tout le rest à PowerShell.

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

On dirait que vous recherchez ce qu’on appelle parfois un “script polyglotte”. Pour 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%);'+[Ssortingng]::Join([char]10,$((Get-Content '%~f0') -notmatch '^^@@'))) & goto :EOF 

Si vous n’avez pas besoin de prendre en charge les arguments cités, vous pouvez même en faire un document simple:

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

Tiré de http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx . C’était PowerShell v1; c’est peut-être plus simple en v2, mais je n’ai pas regardé.

Ici le sujet a été discuté. Les principaux objectives étaient d’éviter l’utilisation de fichiers temporaires pour réduire les opérations d’E / S lentes et exécuter le script sans sortie redondante.

Et voici la meilleure solution selon moi:

 <# : @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( [ssortingng]$str ); $VAR = "Hello, world!"; function F1() { $str; $script:VAR; } F1; 

Edit (un meilleur moyen vu ici )

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

POWERSHELL_BAT_ARGS sont des arguments de ligne de commande d’abord définis comme variable dans la partie batch.

L’astuce réside dans la priorité de la redirection par lots – cette ligne <# : sera analysée comme suit :<# car la redirection est supérieure au prio par rapport aux autres commandes. Mais les lignes commençant par : dans les fichiers batch sont considérées comme des étiquettes - c'est-à-dire non exécutées. Cela rest un commentaire valide de Powershell.

La seule chose qui rest à faire est de trouver un moyen pour que Powershell puisse lire et exécuter %~f0 qui est le chemin complet du script exécuté par cmd.exe.

Cela semble fonctionner, si cela ne vous dérange pas une erreur dans PowerShell au début:

dosps.cmd :

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

Considérez également ce script d’encapsulation “polyglotte” , qui prend en charge les codecs PowerShell et / ou VBScript / JScript incorporés ; Il a été adapté de cet ingénieux original , que l’auteur lui-même, flabdablet , avait publié en 2013, mais il est resté en suspens en raison d’une réponse par lien uniquement, qui a été supprimée en 2015.

Une solution qui améliore l’excellente réponse 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, $_ } 

Remarque: Un fichier temporaire *.ps1 nettoyé par la suite est créé dans le dossier %TEMP% ; Cela simplifie grandement le passage des arguments via (raisonnablement) robustement, simplement en utilisant %*

  • Line <# :: est une ligne hybride que PowerShell considère comme le début d'un bloc de commentaires, mais cmd.exe ignore, une technique empruntée à la réponse de npocmaka .

  • Les commandes de fichiers par lots commençant par @ sont donc ignorées par PowerShell, mais exécutées par cmd.exe ; Comme la dernière ligne @ -prefixed se termine par exit /b , qui quitte le fichier de commandes directement, cmd.exe ignore le rest du fichier, qui est donc libre de contenir du code de fichier non-batch, à savoir le code PowerShell.

  • La ligne #> termine le bloc de commentaires PowerShell qui contient le code du fichier de traitement par lots.

  • Le fichier dans son ensemble étant donc un fichier PowerShell valide, aucune findstr n'est nécessaire pour extraire le code PowerShell; Toutefois, comme PowerShell n'exécute que les scripts .ps1 extension .ps1 , une copie (temporaire) du fichier de commandes doit être créée. %TEMP%\%~0n.ps1 crée la copie temporaire dans le dossier %TEMP% nommé pour le fichier de commandes ( %~0n ), mais avec l'extension .ps1 ; le fichier temporaire est automatiquement supprimé à la fin.

  • Notez que 3 lignes distinctes d'instructions cmd.exe sont nécessaires pour transmettre le code de sortie de la commande PowerShell.
    (L'utilisation de setlocal enabledelayedexpansion permet de le faire en tant que ligne unique , mais cela peut entraîner une interprétation indésirable des caractères dans les arguments.)


Démontrer la robustesse de l'argument en passant :

En supposant que le code ci-dessus a été enregistré sous le nom sample.cmd , sample.cmd -le en tant que:

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

donne quelque chose comme ceci:

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

Notez comment les caractères incorporés ont été passés en tant que \" .
Cependant, il existe des cas marginaux liés aux caractères incorporés :

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

Ces difficultés sont dues à l'parsing des arguments erronés de cmd.exe , et finalement, il est inutile d'essayer de cacher ces failles, comme le souligne flabdablet dans son excellente réponse .

Comme il l'explique, échapper aux métacaractères cmd.exe suivants avec ^^^ (sic) dans la séquence \"...\" résout le problème:

 & | < > 

En utilisant l'exemple ci-dessus:

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

Sans bien comprendre votre question, ma suggestion serait la suivante:

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

ou mieux encore

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

Cela supporte les arguments contrairement à la solution affichée par Carlos et ne rompt pas les commandes multi-lignes ou l’utilisation de param comme la solution publiée par Jay. Le seul inconvénient est que cette solution crée un fichier temporaire. Pour mon cas d’utilisation, c’est acceptable.

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

Ma préférence actuelle pour cette tâche est un en-tête polyglotte qui fonctionne de la même manière que la première solution 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, $_ } 

Je préfère placer l’en-tête de cmd sous la forme de plusieurs lignes avec une seule commande sur chacune, pour un certain nombre de raisons. Tout d’abord, je pense qu’il est plus facile de voir ce qui se passe: les lignes de commande sont suffisamment courtes pour ne pas dépasser la droite de mes fenêtres d’édition, et la colonne de ponctuation à gauche indique le bloc d’en-tête la première ligne le dit. Deuxièmement, les commandes del et goto sont sur leurs propres lignes, donc elles fonctionneront toujours même si quelque chose de vraiment funky est passé en argument de script.

Je suis venu à préférer les solutions qui font un fichier .ps1 temporaire à celles qui reposent sur Invoke-Expression , uniquement parce que les messages d’erreur impénétrables de PowerShell incluront au moins des numéros de ligne significatifs.

Le temps nécessaire pour que le fichier temporaire soit complètement saturé par le temps que met lui-même PowerShell à briller, et le fait que %RANDOM% intégré à 128 bits dans le nom du fichier temporaire garantit que plusieurs scripts simultanés ne seront jamais exécutés. piétinez les fichiers temporaires de l’autre. Le seul inconvénient réel de l’approche par fichier temporaire est la perte possible d’informations sur le répertoire depuis lequel le script cmd d’origine a été appelé, ce qui dir la variable d’environnement dir créée sur la deuxième ligne.

Évidemment, il serait beaucoup moins gênant que PowerShell ne soit pas si anxieux quant aux extensions de noms de fichiers qu’il acceptera sur les fichiers de script, mais vous allez en guerre avec le shell que vous avez, pas avec le shell que vous souhaitez avoir.

En parlant de cela: comme mklement0 observe,

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

Cela se casse, en raison de l’parsing des arguments sans valeur de cmd.exe . J’ai généralement trouvé que moins je faisais de travail pour essayer de cacher les nombreuses limitations de cmd, moins il y avait de bogues imprévus (je suis sûr que je pourrais trouver des arguments contenant des parenthèses qui briseraient la logique , par exemple). Moins douloureux, à mon avis, juste pour mordre la balle et utiliser quelque chose comme

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

Les premier et troisième ^ échappements se mangent lorsque cette ligne de commande est initialement analysée; le second survit pour échapper à la ligne de commande intégrée à powershell.exe . Oui, c’est moche. Oui, il est plus difficile de prétendre que cmd.exe n’est pas ce qui fait craquer le script. Ne vous inquiétez pas à ce sujet. Documentez-le si cela compte.

Dans la plupart des applications du monde réel, le problème est sans object. La plupart des éléments qui seront transmis comme arguments à un script comme celui-ci seront des chemins d’access qui arrivent par glisser-déposer. Windows les citera, ce qui est suffisant pour protéger les espaces et les perluètes et en fait tout ce qui n’est pas des guillemets, ce qui n’est pas autorisé dans les noms de chemins Windows.

Ne me lancez même pas sur Vinyl LP's, 12" dans un fichier CSV.

Un autre exemple de lot + script PowerShell … Il est plus simple que l’autre solution proposée et présente des caractéristiques qu’aucun d’entre eux ne peut égaler:

  • Pas de création de fichier temporaire => Meilleure performance, et aucun risque de réécriture.
  • Pas de préfixe spécial du code de lot. Ceci est juste un lot normal. Et même chose pour le code PowerShell.
  • Transmet tous les arguments de lot à PowerShell correctement, même les chaînes avec des caractères délicats comme! % <> ‘$
  • Les doubles citations peuvent être passées en les doublant.
  • L’entrée standard est utilisable dans PowerShell. (Contrairement à toutes les versions qui dirigent le lot vers PowerShell.)

Cet exemple affiche les transitions de langue et le côté PowerShell affiche la liste des arguments reçus du côté du lot.

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

Notez que j’utilise: # pour les commentaires par lot, au lieu de :: comme la plupart des gens le font, car cela les fait ressembler à des commentaires PowerShell. (Ou comme la plupart des commentaires de langages de script en fait.)