PowerShell ile Hata Ayıklama ve Try Catch Kullanımı

Bir PowerShell scripti yazıyorsunuz, her şey güzel gidiyor, sonra bir noktada script sessizce başarısız oluyor. Ne hata mesajı var, ne de log. Sadece beklediğiniz sonuç yok. İşte bu an, hata ayıklama ve hata yönetiminin ne kadar kritik olduğunu anlatan andır. PowerShell’de try-catch mekanizmasını ve hata ayıklama araçlarını doğru kullanmak, sadece scriptlerinizi daha sağlam yapmaz; aynı zamanda saatler sürebilecek sorun giderme sürecini dakikalara indirir.

PowerShell’de Hata Türleri

PowerShell’de iki temel hata türü vardır ve bunları ayırt etmek, hata yönetiminin temelidir.

Terminating Error (Sonlandırıcı Hata): Script’in çalışmasını tamamen durduran hatalardır. Try-Catch bloklarıyla yakalanabilirler.

Non-Terminating Error (Sonlandırmayan Hata): Script çalışmaya devam eder ama hata konsola yazılır. Bu hataları varsayılan haliyle Try-Catch yakalayamaz.

İşte burada çoğu sysadmin’in düştüğü tuzak var. Bir dosyayı silmeye çalışıyorsunuz, dosya bulunamıyor, PowerShell hata yazıyor ama script çalışmaya devam ediyor. Try-Catch içine almışsınız ama yine de catch bloğuna girmiyor. Bunun sebebi o hatanın non-terminating olmasıdır.

Bunu aşmak için iki yöntem var:

# Yöntem 1: ErrorAction parametresi
Remove-Item "C:olmayan_dosya.txt" -ErrorAction Stop

# Yöntem 2: $ErrorActionPreference değişkeni (tüm script için)
$ErrorActionPreference = "Stop"

-ErrorAction Stop parametresi, non-terminating bir hatayı terminating hata haline getirir ve Try-Catch’in yakalamasını sağlar.

$Error Değişkeni ve Hata Geçmişi

PowerShell, son gerçekleşen hataları otomatik olarak $Error adlı bir dizide saklar. Bu dizi varsayılan olarak son 256 hatayı tutar.

# Son hatayı görüntüle
$Error[0]

# Son hatanın tam detayını görüntüle
$Error[0] | Format-List * -Force

# Hata mesajını al
$Error[0].Exception.Message

# Hangi satırda hata oluştu
$Error[0].InvocationInfo.ScriptLineNumber

# Hata geçmişini temizle
$Error.Clear()

# Kaç hata var
$Error.Count

Özellikle $Error[0].InvocationInfo.ScriptLineNumber bilgisi, uzun scriptlerde hatanın tam olarak nerede oluştuğunu bulmanızı sağlar. Bunu log’a yazdırmak, ilerleyen dönemlerde büyük kolaylık sağlar.

Temel Try-Catch-Finally Yapısı

Try-Catch bloğunun temel yapısı şu şekildedir:

try {
    # Riskli kod buraya yazılır
    $sonuc = Get-Content "C:configsettings.txt" -ErrorAction Stop
    Write-Host "Dosya başarıyla okundu: $($sonuc.Count) satır"
}
catch {
    # Hata oluşursa burası çalışır
    Write-Host "Hata oluştu: $($_.Exception.Message)" -ForegroundColor Red
}
finally {
    # Hata olsa da olmasa da burası çalışır
    Write-Host "İşlem tamamlandı (finally bloğu)"
}

$_ ifadesi catch bloğu içinde mevcut hata nesnesini temsil eder. Bu nesne üzerinden hatanın pek çok detayına ulaşabilirsiniz:

  • $_.Exception.Message: Hata mesajının okunabilir metni
  • $_.Exception.GetType().FullName: Hatanın .NET tip adı
  • $_.InvocationInfo.ScriptLineNumber: Hatanın oluştuğu satır numarası
  • $_.InvocationInfo.Line: Hatanın oluştuğu satırın tam içeriği
  • $_.ScriptStackTrace: Hata stack trace bilgisi

Finally bloğu, veritabanı bağlantısı kapatma, geçici dosya silme gibi temizleme işlemleri için idealdir. Hata olsa da olmasa da bu blok çalışacaktır.

Belirli Hata Türlerini Yakalamak

Gerçek dünyada farklı hata türlerine farklı tepkiler vermek gerekir. Birden fazla catch bloğu kullanarak bunu sağlayabilirsiniz:

try {
    # Uzak sunucuya bağlanmayı dene
    $session = New-PSSession -ComputerName "SUNUCU01" -ErrorAction Stop
    Invoke-Command -Session $session -ScriptBlock {
        Get-Service -Name "wuauserv" -ErrorAction Stop
    }
}
catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
    Write-Host "Uzak bağlantı hatası: Sunucuya erişilemiyor" -ForegroundColor Red
    Write-Host "Sunucu adını ve ağ bağlantısını kontrol edin" -ForegroundColor Yellow
}
catch [System.Management.Automation.RuntimeException] {
    Write-Host "Çalışma zamanı hatası: $($_.Exception.Message)" -ForegroundColor Red
}
catch {
    # Yukarıdakilerle eşleşmeyen tüm hatalar buraya düşer
    Write-Host "Beklenmeyen hata: $($_.Exception.Message)" -ForegroundColor Red
}
finally {
    if ($session) {
        Remove-PSSession $session
        Write-Host "Oturum kapatıldı"
    }
}

Hata tipini nasıl öğrenirsiniz? Önce kodu çalıştırın, hata oluştuktan sonra $Error[0].Exception.GetType().FullName komutunu çalıştırın. Size tam tip adını verecektir.

Gerçek Dünya Senaryosu: Disk Alanı Kontrolü ve Uyarı Scripti

Şimdi pratik bir örnek yapalım. Birden fazla sunucuda disk alanını kontrol eden ve kritik durumları log’layan bir script:

function Get-DiskSpaceReport {
    param(
        [string[]]$Sunucular = @("SUNUCU01", "SUNUCU02", "SUNUCU03"),
        [int]$KritikEsik = 10,  # Yüzde
        [string]$LogDosyasi = "C:Logsdisk_rapor_$(Get-Date -Format 'yyyyMMdd').log"
    )

    # Log dizini yoksa oluştur
    $logDizin = Split-Path $LogDosyasi
    if (-not (Test-Path $logDizin)) {
        try {
            New-Item -ItemType Directory -Path $logDizin -Force -ErrorAction Stop | Out-Null
            Write-Host "Log dizini oluşturuldu: $logDizin"
        }
        catch {
            Write-Host "Log dizini oluşturulamadı: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    }

    $rapor = @()

    foreach ($sunucu in $Sunucular) {
        try {
            Write-Host "Kontrol ediliyor: $sunucu" -ForegroundColor Cyan

            $diskler = Get-WmiObject -Class Win32_LogicalDisk `
                       -ComputerName $sunucu `
                       -Filter "DriveType=3" `
                       -ErrorAction Stop

            foreach ($disk in $diskler) {
                $bosYuzde = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 2)
                $durum = if ($bosYuzde -lt $KritikEsik) { "KRİTİK" } else { "Normal" }

                $kayit = [PSCustomObject]@{
                    Sunucu    = $sunucu
                    Sürücü   = $disk.DeviceID
                    ToplamGB  = [math]::Round($disk.Size / 1GB, 2)
                    BosGB     = [math]::Round($disk.FreeSpace / 1GB, 2)
                    BosYuzde  = $bosYuzde
                    Durum     = $durum
                    Zaman     = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
                }
                $rapor += $kayit

                if ($durum -eq "KRİTİK") {
                    $mesaj = "[$($kayit.Zaman)] KRİTİK: $sunucu - $($disk.DeviceID) - Boş: %$bosYuzde"
                    Add-Content -Path $LogDosyasi -Value $mesaj
                    Write-Host $mesaj -ForegroundColor Red
                }
            }
        }
        catch [System.Runtime.InteropServices.COMException] {
            $hataMsg = "[$( Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] HATA: $sunucu - WMI bağlantısı başarısız"
            Add-Content -Path $LogDosyasi -Value $hataMsg
            Write-Host $hataMsg -ForegroundColor Red
        }
        catch {
            $hataMsg = "[$( Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] HATA: $sunucu - $($_.Exception.Message)"
            Add-Content -Path $LogDosyasi -Value $hataMsg
            Write-Host $hataMsg -ForegroundColor Red
        }
    }

    return $rapor
}

# Scripti çalıştır
$sonuclar = Get-DiskSpaceReport
$sonuclar | Format-Table -AutoSize

Hata Ayıklama Araçları: Write-Debug ve Set-PSDebug

Write-Debug komutu, debug modunda ek bilgi göstermenizi sağlar. Normal çalışmada bu mesajlar görünmez, sadece debug modunda görünür:

function Invoke-FileProcess {
    param([string]$DosyaYolu)

    Write-Debug "Fonksiyon başladı. Parametre: $DosyaYolu"

    try {
        if (-not (Test-Path $DosyaYolu)) {
            throw "Dosya bulunamadı: $DosyaYolu"
        }

        Write-Debug "Dosya mevcut, içerik okunuyor..."
        $icerik = Get-Content $DosyaYolu -ErrorAction Stop

        Write-Debug "Dosya okundu. Satır sayısı: $($icerik.Count)"
        return $icerik
    }
    catch {
        Write-Error "Dosya işleme hatası: $($_.Exception.Message)"
        return $null
    }
}

# Normal çalışma (debug mesajları görünmez)
$veri = Invoke-FileProcess -DosyaYolu "C:datainput.txt"

# Debug modunda çalıştır (debug mesajları görünür)
$DebugPreference = "Continue"
$veri = Invoke-FileProcess -DosyaYolu "C:datainput.txt"
$DebugPreference = "SilentlyContinue"

Set-PSDebug ise daha kapsamlı bir debug aracıdır:

# Her satır çalışmadan önce onay ister
Set-PSDebug -Step

# Her satırı çalıştırırken ekrana yazar
Set-PSDebug -Trace 1

# Değişken atamalarını da gösterir
Set-PSDebug -Trace 2

# Debug modunu kapat
Set-PSDebug -Off

Özel Hata Oluşturma: Throw ve Write-Error

Kendi hata mesajlarınızı oluşturmak için throw ve Write-Error kullanabilirsiniz:

function Test-ServiceStatus {
    param(
        [string]$ServisAdi,
        [string]$Sunucu = $env:COMPUTERNAME
    )

    # Parametre doğrulama
    if ([string]::IsNullOrEmpty($ServisAdi)) {
        throw "ServisAdi parametresi boş olamaz"
    }

    try {
        $servis = Get-Service -Name $ServisAdi -ComputerName $Sunucu -ErrorAction Stop

        if ($servis.Status -ne "Running") {
            # Non-terminating hata oluştur
            Write-Error "Servis çalışmıyor: $ServisAdi - Durum: $($servis.Status)"
            return $false
        }

        Write-Host "Servis çalışıyor: $ServisAdi" -ForegroundColor Green
        return $true
    }
    catch [Microsoft.PowerShell.Commands.ServiceCommandException] {
        throw "Servis bulunamadı: $ServisAdi - Sunucu: $Sunucu"
    }
    catch {
        throw "Servis kontrolü başarısız: $($_.Exception.Message)"
    }
}

# Kullanım
try {
    $sonuc = Test-ServiceStatus -ServisAdi "wuauserv" -Sunucu "SUNUCU01"
    if ($sonuc) {
        Write-Host "Windows Update servisi aktif"
    }
}
catch {
    Write-Host "Kritik hata: $($_.Exception.Message)" -ForegroundColor Red
}

Gerçek Dünya Senaryosu: Merkezi Log Fonksiyonu

Profesyonel scriptlerde her zaman merkezi bir log fonksiyonu kullanmanızı tavsiye ederim. Bu yaklaşım hem kod tekrarını önler hem de log formatını tutarlı tutar:

function Write-Log {
    param(
        [string]$Mesaj,
        [ValidateSet("INFO", "WARNING", "ERROR", "SUCCESS")]
        [string]$Seviye = "INFO",
        [string]$LogDosyasi = "C:Logsscript.log"
    )

    $zaman = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logSatiri = "[$zaman] [$Seviye] $Mesaj"

    # Konsola renkli yaz
    switch ($Seviye) {
        "INFO"    { Write-Host $logSatiri -ForegroundColor White }
        "WARNING" { Write-Host $logSatiri -ForegroundColor Yellow }
        "ERROR"   { Write-Host $logSatiri -ForegroundColor Red }
        "SUCCESS" { Write-Host $logSatiri -ForegroundColor Green }
    }

    # Dosyaya yaz
    try {
        $logDizin = Split-Path $LogDosyasi
        if (-not (Test-Path $logDizin)) {
            New-Item -ItemType Directory -Path $logDizin -Force | Out-Null
        }
        Add-Content -Path $LogDosyasi -Value $logSatiri -Encoding UTF8
    }
    catch {
        Write-Host "LOG YAZMA HATASI: $($_.Exception.Message)" -ForegroundColor Magenta
    }
}

# Kullanım örneği
function Invoke-BackupOperation {
    param([string]$KaynakDizin, [string]$HedefDizin)

    Write-Log "Yedekleme başlatıldı: $KaynakDizin -> $HedefDizin" -Seviye "INFO"

    try {
        if (-not (Test-Path $KaynakDizin)) {
            throw "Kaynak dizin bulunamadı: $KaynakDizin"
        }

        Copy-Item -Path $KaynakDizin -Destination $HedefDizin -Recurse -Force -ErrorAction Stop
        Write-Log "Yedekleme tamamlandı" -Seviye "SUCCESS"
    }
    catch {
        Write-Log "Yedekleme başarısız: $($_.Exception.Message)" -Seviye "ERROR"
        Write-Log "Hata satırı: $($_.InvocationInfo.ScriptLineNumber)" -Seviye "ERROR"
        return $false
    }

    return $true
}

Trap Kullanımı

Trap ifadesi, eski bir hata yakalama yöntemidir ama bazı durumlarda hala işe yarar. Özellikle script genelinde beklenmedik hataları yakalamak için kullanılabilir:

# Script başına global trap koy
trap {
    Write-Host "Beklenmedik hata yakalandı!" -ForegroundColor Red
    Write-Host "Hata: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Satır: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
    Write-Log "KRİTİK HATA: $($_.Exception.Message) - Satır: $($_.InvocationInfo.ScriptLineNumber)" -Seviye "ERROR"
    break  # Script'i durdur
}

# Script kodları devam eder...

Ancak modern PowerShell scriptlerinde trap yerine Try-Catch kullanmak daha okunabilir ve yönetilebilir kod üretir.

Verbose ve Warning Mesajları

Hata yönetiminin yanı sıra, scriptlerinizde bilgi mesajları için de doğru araçları kullanmak önemlidir:

function Update-ConfigFile {
    [CmdletBinding()]
    param(
        [string]$KonfigDosyasi,
        [hashtable]$Ayarlar
    )

    Write-Verbose "Config dosyası güncelleniyor: $KonfigDosyasi"

    try {
        $icerik = Get-Content $KonfigDosyasi -Raw -ErrorAction Stop
        Write-Verbose "Dosya okundu: $($icerik.Length) karakter"

        foreach ($anahtar in $Ayarlar.Keys) {
            $eskiDeger = if ($icerik -match "$anahtars*=s*(.+)") { $Matches[1] } else { "bulunamadı" }

            if ($eskiDeger -eq "bulunamadı") {
                Write-Warning "$anahtar anahtarı config dosyasında bulunamadı, atlanıyor"
                continue
            }

            $icerik = $icerik -replace "$anahtars*=s*.+", "$anahtar = $($Ayarlar[$anahtar])"
            Write-Verbose "$anahtar güncellendi: $eskiDeger -> $($Ayarlar[$anahtar])"
        }

        Set-Content -Path $KonfigDosyasi -Value $icerik -Encoding UTF8 -ErrorAction Stop
        Write-Verbose "Dosya kaydedildi"
    }
    catch {
        Write-Error "Config güncellenemedi: $($_.Exception.Message)"
    }
}

# Normal çalıştırma
Update-ConfigFile -KonfigDosyasi "C:appconfig.ini" -Ayarlar @{ "Port" = "8080"; "Timeout" = "30" }

# Verbose mesajlarla çalıştırma
Update-ConfigFile -KonfigDosyasi "C:appconfig.ini" -Ayarlar @{ "Port" = "8080" } -Verbose

Hata Ayıklamada PowerShell ISE ve VS Code

Script geliştirirken IDE breakpoint desteği büyük kolaylık sağlar. VS Code’da PowerShell Extension ile:

  • Satır numarasına tıklayarak breakpoint koyabilirsiniz
  • F5 ile debug modunda çalıştırabilirsiniz
  • Değişkenlerin anlık değerlerini izleyebilirsiniz
  • Step Over (F10) ve Step Into (F11) ile adım adım ilerleyebilirsiniz

Breakpoint’e alternatif olarak kod içinde şu yöntemi de kullanabilirsiniz:

# Script'i belirli bir noktada durdur ve interaktif console aç
$Host.EnterNestedPrompt()

# Veya Wait-Debugger kullan (PowerShell 5+)
Wait-Debugger

Sonuç

PowerShell’de hata yönetimi, bir script’in “çalışan bir şey” ile “production’a alınabilecek güvenilir bir araç” arasındaki farkı belirler. Try-Catch blokları, $Error değişkeni, -ErrorAction Stop parametresi ve merkezi log fonksiyonları bir arada kullanıldığında, scriptleriniz hem hatalara karşı dayanıklı hale gelir hem de sorun çıktığında nedenini bulmak çok daha kolaylaşır.

Özellikle şu noktalara dikkat edin: Non-terminating hataları unutmayın ve gerektiğinde -ErrorAction Stop ile terminating hata haline getirin. Her catch bloğunda sadece Write-Host ile ekrana yazmak yerine mutlaka log dosyasına da yazın. Finally bloğunu kaynak temizliği için kullanın. Ve $_.InvocationInfo.ScriptLineNumber bilgisini her zaman log’a ekleyin; ilerleyen dönemde sizi kurtaracak olan bilgi budur.

Sağlam hata yönetimi yazmak başta zahmetli görünebilir. Ancak gece 2’de pagerduty alarmıyla uyandığınızda ve log dosyasında tam olarak nerede, ne hatası oluştuğunu gördüğünüzde, bu zahmete değdiğini anlarsınız.

Yorum yapın