Daten|teiler
Kopieren als Kulturtechnik

Batch und PowerShell in einer Datei

7. Januar 2012 von Christian Imhorst

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