Zum Inhalt springen
sw
en

Tippe um zu suchen

Scripting & Automatisierung

PowerShell Scripting – Vertiefung

Variablen, Schleifen, Funktionen und Fehlerbehandlung in PowerShell: von Einzelbefehlen zu echten, wartbaren Skripten fuer den IT-Alltag.

11 Min Lesezeit Fortgeschritten Zuletzt aktualisiert:

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:

FehlerartAuswirkungBeispiel
Nicht-terminierendFehler wird ausgegeben, Skript laeuft weiterGet-Item auf nicht vorhandene Datei
TerminierendSkript bricht ab, catch wird ausgeloestGet-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

FehlermeldungUrsacheLoesung
File cannot be loadedExecutionPolicy blockiertSet-ExecutionPolicy RemoteSigned oder Unblock-File
The term 'X' is not recognizedModul nicht geladen oder TippfehlerImport-Module X oder Get-Command X pruefen
Access deniedFehlende RechtePowerShell als Admin starten oder #Requires -RunAsAdministrator pruefen
Cannot bind parameterFalscher Typ (z.B. String statt Int)Explizite Typisierung oder [int] Cast verwenden
Pipe element type mismatchFalsches 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

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

Videos

YouTube
Effiziente Scripte durch Schleifen - PowerShell Tutorial Deutsch #11

Kommentare

Frage, Verbesserungsvorschlag oder eigene Erfahrung zu diesem Artikel? Schreib einen Kommentar. Neue Beiträge erscheinen nach kurzer Moderation.

  • Lade Kommentare …
Kommentar schreiben