Batch und PowerShell in einer Datei
Zwischen den Jahren — wie man so schön sagt — habe ich das wirklich gute aber leider nicht mehr verfügbare Buch Windows 2003 Shell Scripting. Abläufe automatisieren ohne Programmierkenntnisse von Armin Hanisch gelesen. Alternativ kann man noch das etwas sehr teure und leider auch DRM geschützte eBook bei Addison-Wesley herunterladen. Auch wenn das Buch für Windows-Server 2003 geschrieben wurde, ist es auch für Windows Server 2008 R2 und Windows 7 ein klasse Einstieg in das Skripten mit der Windows Shell cmd.exe. Dabei lernt man die automatisierte Administration von Windows mit vergleichsweise einfachen Shell-Befehlen und ohne umfassende Programmierkenntnisse kennen. Man erfährt aber nicht nur von den Möglichkeiten, sondern auch von Grenzen der Windows Shell.
Microsoft hat lange versäumt, den Befehlsumfang der Kommandozeile an die ständige Weiterentwicklung von Windows anzupassen. Dafür wurde der Windows Script Host (WSH) als Basis für die befehlsbasierte Administration eingeführt, der allerdings Wissen in der objektorientierten Programmierung voraussetzt und dessen Befehle man nicht direkt in die Kommandozeile eingeben kann, sondern nur über eine Skriptsprache wie VBScript. Viele Administratoren von Windows-Servern sind aber nicht bereit, sich Wissen zu diesem Thema anzueignen. Das hat Microsoft auch mitbekommen und neue Kommandozeilenbefehle ab Windows Server 2008 eingeführt und alte, die im Windows Resource Kit gelandet waren, wieder in die Standardinstallation aufgenommen. Dass Microsoft dabei Unix-Administratoren über die Schultern geschaut hat, ist kein Geheimnis, man merkt es an der Entwicklung der PowerShell, aber auch an neuen Befehlen für die Kommandozeilen wie zum Beispiel where
und whoami
.
Im letzten Kapitel des Buchs prophezeit Armin Hanisch der cmd.exe und der Batch eine düstere Zukunft, da sich 2006 schon die PowerShell angekündigt hat. Aber auch für die PowerShell benötigt man, wenn man sie intensiv nutzen will, wie für den WSH Grundkenntnisse in der objektorientierten Programmierung, die Lernkurve ist also steiler. Die Befehle der PowerShell, auch Cmdlets genannt, liefern nicht bloß Text zurück, wie die textbasierte Kommandozeile der Windows Shell, sondern gleich .NET-Objekte, deren Eigenschaften man im weiteren Verlauf gezielt ansprechen kann. Wobei es für Windows-Administratoren oder Power-User unbedingt lohnt, sich in die PowerShell einzuarbeiten, um Windows zu automatisieren. Einfacher geht das aber immer noch mit der cmd.exe, ihren mächtigen Befehlen wie for
oder wmic
und Batchskripten, wenn man nicht programmieren mag. Daher denke ich, dass uns Batchdateien noch eine ganze Weile begleiten werden, und ich hoffe, dass es bald eine überarbeitete Neuauflage des Buchs für Windows Server 2008 R2 geben wird.
Da man mit der Windows Shell schnell an seine Grenzen stößt, geht es in einem interessanten Kapitel des Buchs um das Zusammenspiel zwischen Batch und VBScript, um zum Beispiel Messageboxen zu erstellen. Man muss den Code aber nicht unbedingt in eine temporäre Datei auslagern, es geht auch mit einem here document:
' 2>nul & @echo off ' 2>nul & cls ' 2>nul & title mixed.bat - VBScript und Batch in einer Datei ' 2>nul & echo Das ist die Windows Shell ' 2>nul & time /t ' 2>nul & echo. ' 2>nul & echo Schau doch mal unter eventvwr.msc nach! ' 2>nul & echo. ' 2>nul & cscript //Nologo //E:vbs %~f0 :: WScript.Echo "Das ist der WSH!" & vbCrLf :: set sh = WScript.CreateObject("WScript.Shell") :: sh.LogEvent 2, "Kaffe ist alle" :: set sh = Nothing |
Die Batch kennt das Hochkomma nicht und reagiert mit einer Fehlermeldung, die dann mit 2>nul
ins Nirvana geschickt wird.
Anschließend wird der nächste Befehl mit dem &-Zeichen angeknüpft. Der Teil mit dem VBScript besteht für das Shell Script nur aus Kommentaren, wie man an den beiden Doppelpunkten sieht. VBScript wiederum sieht das Hochkomma als Kommentarzeichen an und ignoriert deshalb das Batchprogramm. Der Doppelpunkt ist in VBScript ein Trennzeichen für mehrere Befehle in einer Zeile, womit ::
zwar ein gültiger Befehl ist, in diesem Fall aber nichts bewirkt.
Batch und PowerShell mit dem Parameter Command
Das Mischen von Batch und Powershell ist wesentlich eleganter. Schnell und einfach funktioniert das mit einem Einzeiler, weil man der PowerShell im Gegensatz zum WSH über den Parameter -Command
einzelne Befehle übergeben kann:
@echo off title cmdPS.bat - Demo um ein PowerShell-Skript von einem Batch zu starten echo. echo Das ist die Windows Shell echo %time:~0,-3% echo. powershell -command "& {Write-Host;Write-Warning 'Starte PowerShell';gps power*,cmd*;Write-Host;Write-Warning 'Beende PowerShell'}" echo. echo Das ist wieder die Windows Shell echo %time:~0,-3% echo. pause & exit /b |
Nun, was macht man aber, wenn man einen ganzen Skriptblock verarbeiten will? Wenn mit der Batch-Datei nur eine Funktion gestartet werden soll, kann man sie ans Ende der Batch-Datei schreiben und mit dem Befehl more
an die Powershell übergeben:
@echo off echo. more +5 %0 | powershell -command - exit /b Function Get-Test { param() Begin{ Write-Host -fore green "Starte PowerShell" } Process{ gps power*,cmd* <# Kommentarblock #> } End{ Write-Host -fore green "`nBeende PowerShell" } } Get-Test exit |
Mit der Option „+5“ liest more
die Batch-Datei ab Zeile 5 nochmal ein. Der komplette Powershell-Code wird dann an Powershell.exe -command
zur Ausführung übergeben.
Möchte man allerdings eine Powershell-Funktion einbauen und anschließend mit der Batch weitermachen, wird es komplizierter. Ein schönes Beispiel dafür habe ich im französischsprachigen Blog von Walid Toumi gefunden, welches ich aber ein bisschen abgewandelt habe:
@echo off title cmdPS.bat - Demo um ein PowerShell-Skript von einem Batch zu starten echo. echo Das ist die Windows Shell echo %time:~0,-3% echo. :: Begin Function Get-Test for /f "delims=:" %%a in (' findstr /BN "::@PS$Get-Test" %~f0 ') do set /a Line=%%a more +%Line% %~f0 | powershell -command - :: End Function Get-Test echo. echo Das ist wieder die Windows Shell echo %time:~0,-3% echo. pause & exit /b ::@PS$Get-Test Function Get-Test { param() Begin{ Write-Warning "Starte PowerShell" } Process{ gps power*,cmd* <# Kommentarblock #> } End{ Write-Host Write-Warning "Beende PowerShell" } } Get-Test exit :: End Function Get-Test |
Zentrale Punkte des Skripts sind die For-Schleife, der Findstr- und der More-Befehl, sowie letztendlich der PowerShell Parameter -command
oder kurz -c
:
:: Begin Function Get-Test for /f "delims=:" %%a in (' findstr /BN "::@PS$Get-Test" %~f0 ') do set /a Line=%%a more +%Line% %~f0 | powershell -command - :: End Function Get-Test |
Bei der For-Schleife steht die Option /f
für das Durchsuchen von Dateien (engl. files). Mit delims=:
wird der Doppelpunkt als Trennzeichen festgelegt.
Warum der Doppelpunkt, oder warum überhaupt ein Trennzeichen? Weil findstr
— das einzige Werkzeug, das in der Windows Shell mit grep
vergleichbar ist — wegen der Option /B
in jeder Zeile des Skripts, dafür steht %~f0
, schaut, ob sie mit dem Ausdruck „::@PS$Get-Test“ beginnt. Der Ausdruck ist dabei beliebig, es sollte bloß eine Zeichenfolge sein, die so weder in einer Batchdatei noch in einem PowerShell-Skript vorkommt. Wenn das der Fall ist, sorgt die Option /N
dafür, dass die Zeilennummer, in diesem Fall 21, mit set /a Line=%%a
der Variable Line
übergeben wird.
Das Ergebnis von findstr
sieht folgendermaßen aus: 21:@::PS$Get-Test
. Da wir aber nur die Zeilennummer brauchen, schneiden wir den Teil danach inklusive des Doppelpunkts einfach mit "delims=:"
ab. Die Zeilenummer wird für das Kommando more
benötigt, das mit der Anzeige des PowerShell-Codes aus der Batchdatei ab dieser Zeile beginnt: more +%Line% %~f0
.
Das Ergebnis wird dann direkt an die PowerShell weitergeleitet.
Durch den Paramter -Command
werden die angegebenen Befehle so ausgeführt, als wären sie direkt in der PowerShell-Eingabeaufforderung eingegeben worden, so wie bei dem Beispiel mit dem Einzeiler weiter oben. Wenn nach -Command
ein „-“ folgt, kann die Zeichenfolge auch ein Skriptblock sein, was uns hier zugute kommt, da durch more
und der Verkettung mit |
ja schließlich ein Skriptblock an die PowerShell weitergeleitet wird. Im PowerShell-Skriptblock wird die Funktion Get-Test
definiert, die nichts weiter aufregendes macht, als alle PowerShell- und CMD-Prozesse anzuzeigen. Am Ende wird die Funktion mit Get-Test
ausgeführt und die PowerShell mit exit
wieder beendet.
Batch und PowerShell mit dem Parameter EncodedCommand
Als weitere Möglichkeit, Batch- und PowerShell-Code zu mischen, kann man den Skriptblock mit Base64 codieren. Dazu muss man den Block an eine Variable übergeben, beispielsweise mit $code = {}
. Alles was zwischen den geschweiften Klammern steht landet in der Variable $code
.
$code = { Function Get-Test { param() Begin{ Write-Warning "Starte PowerShell" } Process{ gps power*,cmd* <# Kommentarblock #> } End{ Write-Host Write-Warning "Beende PowerShell" } } } |
Danach wird der Inhalt der Variable konvertiert:
[convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($code)) |
Der mit Base64 codierte Block muss dann in einer Zeile in die Batchdatei eingefügt werden:
@echo off title cmdPS.bat - Demo um ein PowerShell-Skript von einem Batch zu starten echo. echo Das ist die Windows Shell echo %time:~0,-3% echo. powershell -EncodedCommand DQAKAEYAdQBuAGMAdABpAG8AbgAgAEcAZQB0AC0AVABlAHMAdAAgAHsADQAKACAAIABwAGEAcgBhAG0AKAApAA0ACgAgACAAQgBlAGcAaQBuAHsADQAKACAAIAAgACAAVwByAGkAdABlAC0AVwBhAHIAbgBpAG4AZwAgACIAUwB0AGEAcgB0AGUAIABQAG8AdwBlAHIAUwBoAGUAbABsACIAIAANAAoAIAAgAH0ADQAKACAAIABQAHIAbwBjAGUAcwBzAHsADQAKACAAIAAgACAAZwBwAHMAIABwAG8AdwBlAHIAKgAsAGMAbQBkACoAIAAgAA0ACgAgACAAIAAgADwAIwAgAA0ACgAgACAAIAAgACAAIABLAG8AbQBtAGUAbgB0AGEAcgBiAGwAbwBjAGsADQAKACAAIAAgACAAIwA+AA0ACgAgACAAfQANAAoAIAAgAEUAbgBkAHsADQAKACAAIAAgAFcAcgBpAHQAZQAtAEgAbwBzAHQADQAKACAAIAAgAFcAcgBpAHQAZQAtAFcAYQByAG4AaQBuAGcAIAAiAEIAZQBlAG4AZABlACAAUABvAHcAZQByAFMAaABlAGwAbAAiAA0ACgAgACAAfQANAAoAfQANAAoARwBlAHQALQBUAGUAcwB0AA0ACgA= echo. echo Das ist wieder die Windows Shell echo %time:~0,-3% echo. pause & exit /b |
Der Vorteil dieser Variante liegt darin, dass die Batchdatei übersichtlicher ist. Der Nachteil ist natürlich, dass man keinen direkten Einfluss auf den PowerShell-Code mehr hat und ihn bei jeder Änderung wieder in Base64 umwandeln muss.
Da PowerShell-Skripte aus Sicherheitsgründen nicht ganz so einfach gestartet werden können, eignen sich Batchdateien sehr gut für die Unterstützung von PowerShell-Skripten. Man kann mit den Batchprogrammen zum Beispiel bei der Verteilung von PowerShell-Skripten die ein oder andere Ungewissheit umschiffen, zum Beispiel wenn man nicht weiß, ob das Zielsystem aufgrund der „Execution Policy“ grundsätzlich bereit ist, Powershell-Skripte auszuführen:
powershell -ExecutionPolicy Unrestricted -c "& {write-warning 'Hallo Welt'}; exit" |
Oder, um mit den Remotefähigkeiten der Kommandozeilenwerkzeuge wie AT
das Gastsystem gezielt auf den Einsatz eines PowerShell-Skripts vorzubereiten, indem man die PowerShell dort mit einem Taskjob direkt aus dem Taskplaner startet.
Geschrieben in Batch, Powershell, Windows