PowerShell Scripting – Vertiefung
Variablen, Schleifen, Funktionen und Fehlerbehandlung in PowerShell: von Einzelbefehlen zu echten, wartbaren Skripten fuer den IT-Alltag.
Von Einzelbefehlen zum echten Skript
Die PowerShell-Grundlagen kennst du: Einzelbefehle in der Konsole, vielleicht ein Get-ADUser hier, ein Restart-Service dort. Aber sobald du dieselben Schritte zweimal machst, lohnt sich ein Skript. Diese Seite zeigt dir, wie du PowerShell richtig nutzt – mit sauberem Code, der auch in drei Monaten noch verstaendlich ist.
Das Ziel: Du kannst selbststaendig Skripte schreiben, die Variablen sinnvoll verwenden, ueber Objekte iterieren, mit Parametern arbeiten und Fehler sauber abfangen – statt beim ersten Ausnahmefall stillschweigend falsch weiterzulaufen.
Variablen und Datentypen
In PowerShell ist alles ein Objekt. Eine Variable ist nichts anderes als ein Name fuer einen Speicherplatz, und du musst den Typ nicht immer angeben – PowerShell erkennt ihn meist selbst.
# Einfache Typen
$name = "mmuster"
$zahl = 42
$aktiv = $true
$heute = Get-Date
# Explizite Typisierung – sicherer bei Parametern
[string]$server = "DC01"
[int]$port = 443
[bool]$dryRun = $false
[datetime]$cutoff = (Get-Date).AddDays(-90)
# Arrays
$computer = @("PC-01", "PC-02", "PC-03")
$computer += "PC-04" # Element hinzufuegen
$computer[0] # Erstes Element: "PC-01"
$computer.Count # Anzahl: 4
# Hashtables (Key-Value)
$user = @{
Name = "Max Muster"
Abteilung = "IT"
Email = "m.muster@firma.ch"
}
$user["Name"] # "Max Muster"
$user.Email # "m.muster@firma.ch"
String-Interpolation – ein haeufiger Stolperstein:
$name = "Seya"
# Doppelte Anfuehrungszeichen: Variable wird aufgeloest
Write-Host "Hallo $name" # Hallo Seya
# Einfache Anfuehrungszeichen: alles literal
Write-Host 'Hallo $name' # Hallo $name
# Ausdruecke in Strings: $() verwenden
$pc = Get-ComputerInfo
Write-Host "OS: $($pc.OsName)" # OS: Microsoft Windows 11 Pro
Bedingungen und Vergleichsoperatoren
PowerShell nutzt Textoperatoren statt Symbole wie == oder !=. Das ist gewoehnungsbeduerftig, aber eindeutiger.
# Vergleichsoperatoren
$a -eq $b # Equal
$a -ne $b # Not Equal
$a -gt $b # Greater Than
$a -lt $b # Less Than
$a -ge $b # Greater or Equal
$a -le $b # Less or Equal
# String-Operatoren
"Server01" -like "Server*" # Wildcard: true
"error 404" -match "^\w+\s\d+" # Regex: true
"HALLO" -eq "hallo" # false (case-sensitive)
"HALLO" -ieq "hallo" # true (-i = insensitive)
# Logische Operatoren
($a -gt 0) -and ($b -lt 100)
($a -eq 1) -or ($b -eq 1)
-not ($a -eq 0)
# if / elseif / else
$freeGB = (Get-PSDrive C).Free / 1GB
if ($freeGB -lt 10) {
Write-Warning "Wenig Speicher: $([math]::Round($freeGB, 1)) GB frei"
} elseif ($freeGB -lt 50) {
Write-Host "Speicher knapp – im Auge behalten"
} else {
Write-Host "Speicher OK: $([math]::Round($freeGB, 1)) GB frei"
}
# switch – besser als viele elseif
switch ($env:COMPUTERNAME) {
"DC01" { Write-Host "Domain Controller" }
"FS01" { Write-Host "File Server" }
"WSUS01" { Write-Host "Update Server" }
default { Write-Host "Unbekannter Server" }
}
Schleifen – ueber Objekte iterieren
Die Pipeline ist das Herzstueck von PowerShell. Statt Schleifen in anderen Sprachen nachzubauen, arbeite mit dem Datenstrom.
# ForEach-Object in der Pipeline (am haeufigsten)
Get-ADUser -Filter {Enabled -eq $true} | ForEach-Object {
Write-Host "$($_.SamAccountName) – $($_.DisplayName)"
}
# Kurzschreibweise mit $PSItem statt $_
Get-Service | Where-Object Status -eq "Stopped" | ForEach-Object {
Write-Host "Gestoppt: $($PSItem.Name)"
}
# foreach-Schleife (fuer Arrays, wenn kein Pipeline-Objekt)
$server = @("DC01", "FS01", "APP01")
foreach ($s in $server) {
$ping = Test-Connection -ComputerName $s -Count 1 -Quiet
Write-Host "$s erreichbar: $ping"
}
# for-Schleife (wenn Index benoetigt)
for ($i = 0; $i -lt $server.Count; $i++) {
Write-Host "[$($i + 1)/$($server.Count)] $($server[$i])"
}
# while-Schleife (Wartemuster)
$maxVersuche = 10
$versuch = 0
while ($versuch -lt $maxVersuche) {
if (Test-Connection -ComputerName "APP01" -Count 1 -Quiet) {
Write-Host "APP01 ist erreichbar"
break
}
$versuch++
Start-Sleep -Seconds 30
Write-Host "Warte... (Versuch $versuch)"
}
# do-while (mindestens einmal ausgefuehrt)
do {
$input = Read-Host "Servername eingeben"
} while ($input -eq "")
Funktionen und Parameter
Sobald du dieselben 5 Zeilen zweimal schreibst, baue eine Funktion. Gute Funktionen haben einen klaren Namen (Verb-Nomen nach PowerShell-Konvention), definierte Parameter und kein globales State-Chaos.
# Einfache Funktion
function Get-DiskReport {
param(
[string]$ComputerName = $env:COMPUTERNAME # Standardwert
)
$drives = Get-PSDrive -PSProvider FileSystem
foreach ($d in $drives) {
[PSCustomObject]@{
Computer = $ComputerName
Laufwerk = $d.Name
GesamtGB = [math]::Round(($d.Used + $d.Free) / 1GB, 1)
FreiGB = [math]::Round($d.Free / 1GB, 1)
FreiProzent = [math]::Round($d.Free / ($d.Used + $d.Free) * 100, 0)
}
}
}
# Aufrufen
Get-DiskReport
Get-DiskReport -ComputerName "FS01"
Get-DiskReport | Where-Object FreiProzent -lt 20 # Filterbar in Pipeline
Advanced Functions mit [CmdletBinding]
Mit [CmdletBinding()] wird eine Funktion zu einem echten Cmdlet: sie bekommt automatisch -Verbose, -WhatIf, -ErrorAction und andere Common Parameter.
function Set-UserStatus {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$Username,
[Parameter(Mandatory = $true)]
[ValidateSet("Enable", "Disable")]
[string]$Action,
[string]$Reason = "Kein Grund angegeben"
)
process {
$user = Get-ADUser -Identity $Username -ErrorAction Stop
# ShouldProcess prueft -WhatIf und -Confirm
if ($PSCmdlet.ShouldProcess($Username, "Account $Action")) {
switch ($Action) {
"Enable" { Enable-ADAccount -Identity $Username }
"Disable" { Disable-ADAccount -Identity $Username }
}
Write-Verbose "[$Action] $Username – Grund: $Reason"
}
}
}
# Verwendung
Set-UserStatus -Username "mmuster" -Action "Disable" -Reason "Mutterschaftsurlaub"
Set-UserStatus -Username "mmuster" -Action "Enable" -WhatIf # Dry Run
# Pipeline: alle inaktiven User aus CSV deaktivieren
Import-Csv "offboarding.csv" | ForEach-Object { $_.Username } |
Set-UserStatus -Action "Disable" -Reason "Offboarding"
Fehlerbehandlung
Ohne Fehlerbehandlung laeuft ein Skript bei der ersten Ausnahme entweder still falsch weiter oder bricht unguenstig ab. Beides ist schlechter als eine saubere try/catch-Struktur.
Terminierende vs. nicht-terminierende Fehler
PowerShell unterscheidet zwei Fehlerarten:
| Fehlerart | Auswirkung | Beispiel |
|---|---|---|
| Nicht-terminierend | Fehler wird ausgegeben, Skript laeuft weiter | Get-Item auf nicht vorhandene Datei |
| Terminierend | Skript bricht ab, catch wird ausgeloest | Get-ADUser bei falschem Domaenencontroller |
Mit -ErrorAction Stop machst du jeden Fehler zu einem terminierenden – das ist dein wichtigstes Werkzeug:
# Grundstruktur try/catch/finally
try {
$user = Get-ADUser -Identity "mmuster" -ErrorAction Stop
Write-Host "User gefunden: $($user.DisplayName)"
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
Write-Warning "User 'mmuster' existiert nicht im AD"
}
catch [System.Net.WebException] {
Write-Error "Netzwerkfehler: $_"
}
catch {
# Allgemeiner Catch fuer alles andere
Write-Error "Unerwarteter Fehler: $($_.Exception.Message)"
Write-Error "Stack Trace: $($_.ScriptStackTrace)"
}
finally {
# Wird IMMER ausgefuehrt – ideal fuer Aufraeumarbeiten
Write-Verbose "Verarbeitung abgeschlossen"
}
$ErrorActionPreference und -ErrorAction
# Global fuer das ganze Skript setzen
$ErrorActionPreference = "Stop" # Jeden Fehler als terminierend behandeln
# Nur fuer einen Befehl
Get-Service -Name "NichtVorhanden" -ErrorAction SilentlyContinue # Kein Output
Get-Service -Name "NichtVorhanden" -ErrorAction Ignore # Kein $Error-Eintrag
Get-Service -Name "NichtVorhanden" -ErrorAction Stop # Exception ausloesen
# $Error – Array der letzten Fehler
$Error[0] # Letzter Fehler
$Error[0].Exception.Message # Fehlermeldung
$Error.Clear() # Array leeren
Praxismuster: Robustes Skript mit Logging
# Einfaches Logging in Datei
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "[$timestamp] [$Level] $Message"
Add-Content -Path "C:\Logs\skript.log" -Value $line
if ($Level -eq "ERROR") { Write-Warning $Message }
else { Write-Verbose $Message }
}
# Verwendung im Skript
try {
Write-Log "Starte Verarbeitung von Server: $server"
# ... Hauptlogik ...
Write-Log "Erfolgreich abgeschlossen"
}
catch {
Write-Log "Fehler aufgetreten: $($_.Exception.Message)" -Level "ERROR"
exit 1 # Exit-Code 1 signalisiert Fehler (wichtig fuer Task Scheduler)
}
Praxisbeispiel: Inaktive Computer bereinigen
Ein vollstaendiges, praxisnahes Skript fuer den KMU-Alltag: Computer, die sich seit 90 Tagen nicht mehr am AD angemeldet haben, werden in eine OU verschoben und deaktiviert.
#Requires -Modules ActiveDirectory
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Deaktiviert und verschiebt inaktive Computer-Konten im Active Directory.
.DESCRIPTION
Sucht alle Computer-Konten, die seit X Tagen kein Logon hatten,
deaktiviert sie und verschiebt sie in eine dedizierte OU.
.PARAMETER InactiveDays
Anzahl Tage ohne Logon. Standard: 90
.PARAMETER TargetOU
Ziel-OU fuer inaktive Computer. Standard: OU=Inaktiv,DC=firma,DC=ch
.PARAMETER WhatIf
Dry Run – zeigt was getan wuerden, ohne Aenderungen vorzunehmen
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[int]$InactiveDays = 90,
[string]$TargetOU = "OU=Inaktiv,DC=firma,DC=ch",
[string]$LogPath = "C:\Logs\ad-cleanup.log"
)
# Logging-Funktion
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
Add-Content -Path $LogPath -Value $line -ErrorAction SilentlyContinue
switch ($Level) {
"WARN" { Write-Warning $Message }
"ERROR" { Write-Error $Message }
default { Write-Verbose $Message }
}
}
# Hauptlogik
$cutoff = (Get-Date).AddDays(-$InactiveDays)
$processed = 0
$errors = 0
Write-Log "Starte AD-Cleanup: Inaktiv seit $InactiveDays Tagen (vor $($cutoff.ToString('dd.MM.yyyy')))"
try {
$inactivePC = Get-ADComputer -Filter {
LastLogonDate -lt $cutoff -and Enabled -eq $true
} -Properties LastLogonDate, DistinguishedName -ErrorAction Stop
Write-Log "Gefunden: $($inactivePC.Count) inaktive Computer"
foreach ($pc in $inactivePC) {
try {
if ($PSCmdlet.ShouldProcess($pc.Name, "Deaktivieren und in $TargetOU verschieben")) {
Disable-ADAccount -Identity $pc.DistinguishedName -ErrorAction Stop
Move-ADObject -Identity $pc.DistinguishedName -TargetPath $TargetOU -ErrorAction Stop
Write-Log "OK: $($pc.Name) – letzter Login: $($pc.LastLogonDate?.ToString('dd.MM.yyyy') ?? 'unbekannt')"
$processed++
}
}
catch {
Write-Log "FEHLER bei $($pc.Name): $($_.Exception.Message)" -Level "ERROR"
$errors++
}
}
}
catch {
Write-Log "Kritischer Fehler beim AD-Abfrage: $($_.Exception.Message)" -Level "ERROR"
exit 1
}
Write-Log "Abgeschlossen: $processed verarbeitet, $errors Fehler"
Write-Host "Fertig: $processed Computer deaktiviert, $errors Fehler. Log: $LogPath"
Skripte sichern: ExecutionPolicy und Signierung
PowerShell fuehrt Skripte standardmaessig nicht aus – die ExecutionPolicy verhindert das. Das ist eine Sicherheitsmassnahme, kein Bug.
# Aktuelle Richtlinie anzeigen
Get-ExecutionPolicy -List
# Typische Werte:
# Restricted = Keine Skripte (Windows-Standard)
# AllSigned = Alle Skripte muessen signiert sein
# RemoteSigned = Lokale Skripte ohne Signatur, heruntergeladene mit Signatur
# Unrestricted = Alles erlaubt (nicht empfohlen)
# Bypass = Gar keine Pruefung (nur fuer automatisierte Systeme)
# Fuer Unternehmens-PCs empfohlen (als Admin):
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
# Fuer einzelne Session (ohne Admin):
Set-ExecutionPolicy Bypass -Scope Process
# Heruntergeladene Skripte entsperren (entfernt den ADS-ZoneID-Flag)
Unblock-File -Path "C:\Scripts\mein-skript.ps1"
Skript mit Selbstzertifikat signieren
# 1. Self-Signed-Zertifikat erstellen (Entwicklung/intern)
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=Firma IT Scriptsigning" `
-CertStoreLocation Cert:\CurrentUser\My `
-NotAfter (Get-Date).AddYears(3)
# 2. Skript signieren
Set-AuthenticodeSignature `
-FilePath "C:\Scripts\backup.ps1" `
-Certificate $cert
# 3. Signatur pruefen
Get-AuthenticodeSignature -FilePath "C:\Scripts\backup.ps1"
Module und Profilskripte
Wiederverwendbare Funktionen gehoeren in Module, nicht in jedes Skript kopiert.
# Einfaches Modul erstellen
# Datei: C:\Scripts\Modules\FirmaIT\FirmaIT.psm1
function Get-PCInventory {
[CmdletBinding()]
param([string]$ComputerName = $env:COMPUTERNAME)
[PSCustomObject]@{
Computer = $ComputerName
OS = (Get-CimInstance Win32_OperatingSystem -ComputerName $ComputerName).Caption
RAM_GB = [math]::Round((Get-CimInstance Win32_ComputerSystem -ComputerName $ComputerName).TotalPhysicalMemory / 1GB, 1)
Seriennummer = (Get-CimInstance Win32_BIOS -ComputerName $ComputerName).SerialNumber
}
}
Export-ModuleMember -Function Get-PCInventory
# Modul laden
Import-Module "C:\Scripts\Modules\FirmaIT\FirmaIT.psm1"
# Oder: Modul in PSModulePath ablegen, dann reicht der Name
$env:PSModulePath += ";C:\Scripts\Modules"
Import-Module FirmaIT
# Modul-Funktionen anzeigen
Get-Command -Module FirmaIT
PowerShell-Profil
Das Profil wird bei jedem Start einer neuen Session ausgefuehrt – ideal fuer Standard-Imports, Aliase und Einstellungen:
# Profil-Pfad anzeigen
$PROFILE
# Profil oeffnen/erstellen
notepad $PROFILE
# Beispiel-Inhalt eines Profils:
# Import-Module ActiveDirectory
# Import-Module FirmaIT
# Set-Location C:\Scripts
# function ll { Get-ChildItem @args | Format-Table -AutoSize }
Troubleshooting: Haeufige Fehler
| Fehlermeldung | Ursache | Loesung |
|---|---|---|
File cannot be loaded | ExecutionPolicy blockiert | Set-ExecutionPolicy RemoteSigned oder Unblock-File |
The term 'X' is not recognized | Modul nicht geladen oder Tippfehler | Import-Module X oder Get-Command X pruefen |
Access denied | Fehlende Rechte | PowerShell als Admin starten oder #Requires -RunAsAdministrator pruefen |
Cannot bind parameter | Falscher Typ (z.B. String statt Int) | Explizite Typisierung oder [int] Cast verwenden |
Pipe element type mismatch | Falsches Objekt in Pipeline | `$_ |
Exception calling "X" | .NET-Methode hat Fehler geworfen | $_.Exception.InnerException.Message fuer Details |
# Debugging-Helfer
# Objekt-Eigenschaften inspizieren
Get-ADUser -Identity "mmuster" | Get-Member
# Variablen-Inhalt pruefen
$meinObjekt | Format-List *
# Schrittweise debuggen mit Write-Debug
$DebugPreference = "Continue" # Debug-Ausgaben aktivieren
Write-Debug "Variable hat Wert: $($meinVariable)"
# Breakpoints in PS ISE oder VS Code setzen:
# Set-PSBreakpoint -Script ".\skript.ps1" -Line 42
Crosslinks
Fuer den naechsten Schritt: Skripte automatisch ausfuehren mit dem Task Scheduler oder tiefer in die AD-Verwaltung einsteigen mit Active Directory Benutzer und Gruppen. Den Einstieg in PowerShell findest du unter PowerShell im IT-Alltag. Batch-Skripting als Alternative – oder Ergaenzung – ist unter Batch / CMD Scripting beschrieben.
Weiterlernen
- Microsoft Learn: Funktionen in PowerShell 101 – offizielle Einfuehrung in Funktionen
- Microsoft Learn: Alles ueber Ausnahmen – tiefer Einblick in Fehlerbehandlung
- Microsoft Learn: about_Execution_Policies – vollstaendige Referenz zur ExecutionPolicy
- WindowsPro: Fehlerbehandlung mit try/catch/finally – praxisnaher Artikel auf Deutsch
- ScriptRunner: Advanced Functions – CmdletBinding und Parameter-Validierung vertieft
- SS64 PowerShell Referenz – schnelle Befehlsreferenz fuer alle Cmdlets
Videos
Kommentare
Frage, Verbesserungsvorschlag oder eigene Erfahrung zu diesem Artikel? Schreib einen Kommentar. Neue Beiträge erscheinen nach kurzer Moderation.
- Lade Kommentare …