Koşullar, Döngüler ve Pipeline'ı Script'te Kullanmak: switch, foreach, Where-Object, ForEach-Object ve -Parallel
PowerShell'de foreach statement bellekteki koleksiyonda en hızlısıdır, dosyadan akan veride ise pipeline erken durabildiği için çok daha az iş yapar; ForEach-Object içindeki break ise script'in kalanını atlar. Karşılaştırma operatörlerini, switch -Regex ile log okumayı, begin/process/end bloklarını ve ForEach-Object -Parallel'i Bookstore'un 200 kitap kapağını kontrol eden bir script'le, $using:, race condition ve Format-Table tuzaklarıyla birlikte gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler PowerShell 7.6.10 (LTS) ve PowerShell 7.7.0 ile, Ubuntu 24.04 üzerinde denenmiştir; ölçümler aynı makinede iki sürümde de alındı ve aynı sonuçları verdi. Cmdlet'ler ve çıktıları Windows'ta aynıdır, yalnız yollar C:\ ile başlar. Windows PowerShell 5.1'de ForEach-Object -Parallel yok; o kısımlar yalnız PowerShell 7'de geçerli.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) bir bakım script'inde, bash'teki continue ve break alışkanlığıyla yazılmış iki döngü var. Biri pipeline'da ForEach-Object, diğeri foreach statement:
1..5 | ForEach-Object {
if ($_ -eq 3) { return }
"ForEach-Object: $_"
}
foreach ($n in 1..5) {
if ($n -eq 3) { continue }
"foreach: $n"
}
1..5 | ForEach-Object {
if ($_ -eq 3) { break }
"before break: $_"
}
'after the pipeline'
Script'i çalıştırınca ekrana gelen son satır ne olur? after the pipeline yazılır mı? İlk bloktaki return ne yapar: script'i mi bitirir, yalnız o elemanı mı atlar?
Önceki yazıda veriyi tiplere oturttuk. Bu yazıda o veri üzerinde karar vermeye ve dönmeye geçiyoruz: karşılaştırma operatörleri, switch, PowerShell'in üç ayrı döngü biçimi (foreach statement, ForEach-Object ve .ForEach()), pipeline'ı script içinde begin/process/end ile kullanmak ve ForEach-Object -Parallel. Sonunda Bookstore'un 200 kitap kapağını aynı anda kontrol edip kırık olanları CSV (virgülle ayrılmış değerler) dosyasına yazan bir script'imiz olacak.
Karşılaştırma Operatörleri ve if
PowerShell'de < ve > redirection'a ayrıldığı için karşılaştırmalar tire ile yazılır. Bookstore API'sinin access log'undan bir satır üzerinde en sık kullanılanlar:
PS> $line = '2026-11-15T04:58:21Z POST /api/payments 502 3004ms'
PS> $line -match ' (?<status>\d{3}) (?<ms>\d+)ms$'
True
PS> $Matches.status, $Matches.ms
502
3004
PS> $line -like '*/api/payments*'
True
PS> 502 -in 500, 502, 503, 504
True
PS> @(500, 502, 503, 504) -contains '502'
True
PS> '502' -is [int]
False
PS> [int]'502' -is [int]
True
PS> (Get-Content ./data/api-access.log) -match ' 404 '
2026-11-15T04:58:09Z GET /api/books/9780000000000 404 3ms
2026-11-15T04:58:31Z GET /covers/9780547928227.jpg 404 2ms
-like wildcard (*, ?), -match regex kullanır. -match başarılı olunca yakalanan grupları otomatik $Matches hashtable'ına koyar; adlandırılmış gruplar ((?<status>...)) okunurluğu artırır. Regex syntax'ının kendisi Regular Expressions serisinin konusu. -in ve -contains aynı soruyu iki yönden sorar: değer listede mi? Sağdaki '502' string'inin bulunması önceki yazıdaki kuralın sonucu; sol taraf int array'i olduğu için string, int'e çevrildi. -is ise dönüştürme yapmaz, tipe bakar. Son satır, solda bir array olunca -match'in de filtre gibi çalıştığını gösteriyor: dosyanın 404 içeren satırları döndü.
if'in kendisi C#'taki gibi, yalnız elseif tek kelime. Bir fark var: koşul bool olmak zorunda değil. Boş string, 0, $null ve boş array yanlış, geri kalan her şey doğru sayılır. Önceki yazıda $null karşılaştırmasında gördüğümüz tuzak buradan geliyor.
switch: -Regex, -Wildcard ve -File
switch, C#'takinden çok daha güçlü, ama iki farkı bilmeden kullanılmamalı:
PS> switch (404) { 404 { 'not found' } { $_ -ge 400 } { 'client error' } }
not found
client error
PS> switch (404) { 404 { 'not found'; break } { $_ -ge 400 } { 'client error' } }
not found
PS> switch -Wildcard ('Bookstore.Api.dll', 'appsettings.json', 'web.config') { '*.dll' { "binary: $_" } '*.json' { "config: $_" } default { "other: $_" } }
binary: Bookstore.Api.dll
config: appsettings.json
other: web.config
Birincisi, eşleşen bütün koşullar çalışır; C#'taki gibi ilk eşleşmede durmaz. 404 hem 404'e hem de $_ -ge 400 koşuluna uyduğu için iki satır yazdı. İlk eşleşmede durmak istiyorsan break ekle. Koşul bir değer, bir script block ({ $_ -ge 400 }), -Wildcard ile wildcard ya da -Regex ile regex olabilir. İkincisi, switch'e bir array verirsen her element için ayrı ayrı çalışır; -Wildcard örneğinde üç dosya adı tek tek sınıflandı.
Bu iki özellik birleşince switch log okumak için iyi bir araç oluyor. Aşağıdaki script access log'u satır satır okuyup HTTP sonuçlarını sayıyor ve bir saniyeden uzun süren istekleri ayrıca döndürüyor:
$summary = [ordered]@{ ok = 0; clientError = 0; serverError = 0; slow = 0 }
$slowRequests = foreach ($line in Get-Content ./data/api-access.log) {
switch -Regex ($line) {
' 2\d\d \d+ms$' { $summary.ok++ }
' 4\d\d \d+ms$' { $summary.clientError++ }
' 5\d\d \d+ms$' { $summary.serverError++ }
' (?<ms>\d+)ms$' {
if ([int]$Matches.ms -ge 1000) {
$summary.slow++
$line
}
}
}
}
[pscustomobject]$summary
$slowRequests
PS> ./lab/log-summary.ps1
ok clientError serverError slow
-- ----------- ----------- ----
7 3 2 2
2026-11-15T04:58:21Z POST /api/payments 502 3004ms
2026-11-15T04:58:22Z GET /api/books 200 1290ms
3004 ms süren satır hem serverError'a hem slow'a sayıldı, çünkü iki koşul da eşleşti; burada istediğimiz davranış bu. $Matches.ms, son başarılı regex eşleşmesinden geliyor. Dosyayı switch -Regex -File ./data/api-access.log ile doğrudan da verebilirsin; o zaman satırlar dosyadan tek tek okunur.
foreach, ForEach-Object ve .ForEach()
PowerShell'de bir koleksiyon üzerinde dönmenin üç yolu var ve üçü farklı şeyler:
foreach ($n in $numbers) { }: dilin döngü statement'ı. Önce koleksiyonun tamamını alır, sonra döner.breakvecontinuebeklendiği gibi çalışır.$numbers | ForEach-Object { }: bir cmdlet. Pipeline'dan gelen her elemanı geldiği anda işler; eleman$_(ya da uzun adıyla$PSItem) içindedir.$numbers.ForEach({ })ve.Where({ }): PowerShell'in her koleksiyona eklediği method'lar. Bellekte duran bir koleksiyon üzerinde çalışır, pipeline kullanmaz.
Aynı işi 200 000 elemanla yapınca fark ortaya çıkıyor:
$numbers = 1..200000
'foreach statement : {0,5:N0} ms' -f (Measure-Command { $r = foreach ($n in $numbers) { $n * 2 } }).TotalMilliseconds
'ForEach-Object : {0,5:N0} ms' -f (Measure-Command { $r = $numbers | ForEach-Object { $_ * 2 } }).TotalMilliseconds
'.ForEach() method : {0,5:N0} ms' -f (Measure-Command { $r = $numbers.ForEach({ $_ * 2 }) }).TotalMilliseconds
'Where-Object : {0,5:N0} ms' -f (Measure-Command { $r = $numbers | Where-Object { $_ % 7 -eq 0 } }).TotalMilliseconds
'.Where() method : {0,5:N0} ms' -f (Measure-Command { $r = $numbers.Where({ $_ % 7 -eq 0 }) }).TotalMilliseconds
PS> ./lab/loops.ps1
foreach statement : 86 ms
ForEach-Object : 413 ms
.ForEach() method : 174 ms
Where-Object : 342 ms
.Where() method : 65 ms
Bellekteki bir koleksiyonda foreach statement ve .Where() en hızlıları, cmdlet'ler ise eleman başına pipeline maliyeti ödüyor. Sayılar 7.7.0'da da aynı sırada çıktı. Ama hız tek ölçü değil. Büyük bir dosyada ilk 5xx satırını arayalım, 56 MB'lık bir log ile:
PS> (Get-Item ./lab/api-access-big.log).Length / 1MB
56.5528869628906
PS> (Measure-Command { $first = Get-Content ./lab/api-access-big.log | Where-Object { $_ -match ' 5\d\d ' } | Select-Object -First 1 }).TotalMilliseconds
11.877
PS> (Measure-Command { $first = foreach ($line in Get-Content ./lab/api-access-big.log) { if ($line -match ' 5\d\d ') { $line; break } } }).TotalMilliseconds
7357.0111
Pipeline 12 milisaniyede bitti, foreach 7,4 saniyede. Select-Object -First 1 ilk sonucu alınca pipeline'ı durduruyor ve Get-Content dosyanın kalanını hiç okumuyor. foreach ($line in Get-Content ...) ise önce 1,2 milyon satırın tamamını belleğe alıyor, döngü ancak ondan sonra başlıyor; içindeki break bunu kurtaramıyor. Kural: bellekte zaten duran bir koleksiyon için foreach, dosya ya da başka bir komuttan akan veri için pipeline.
Önce Tahmin Et'in cevabı da bu farktan geliyor:
PS> ./lab/flow.ps1
ForEach-Object: 1
ForEach-Object: 2
ForEach-Object: 4
ForEach-Object: 5
foreach: 1
foreach: 2
foreach: 4
foreach: 5
before break: 1
before break: 2
ForEach-Object'in script block'u bir döngü değil, her eleman için çağrılan bir script block. İçindeki return yalnız o çağrıyı bitirir, yani continue gibi davranır ve 3 atlandı. break ise bir döngü arar; ForEach-Object döngü olmadığı için yukarı doğru en yakın döngüye ya da script'in sonuna kadar çıkar. Script burada bitti ve after the pipeline hiç yazılmadı. about_Break dokümanı da pipeline içindeki break'in pipeline'dan çıkmakla kalmayıp bütün runspace'i bitirebileceği konusunda uyarıyor. Pipeline'ı erken durdurmak için Select-Object -First, döngüden çıkmak için foreach statement kullan.
Pipeline'ı Script'te Kullanmak: $_ ve begin/process/end
ForEach-Object'in üç bloğu var: -Begin ilk elemandan önce bir kez, -Process her eleman için, -End son elemandan sonra bir kez çalışır. Bu, toplam ya da sayaç tutmanın pipeline'ı bozmadan yapılan yolu:
PS> $inv = [cultureinfo]::InvariantCulture
PS> Import-Csv ./data/books.csv | ForEach-Object -Begin { $total = 0d } -Process { $total += [decimal]::Parse($_.Price, $inv) * [int]$_.Stock } -End { "stock value: $total" }
stock value: 1021.42
PS> Import-Csv ./data/books.csv | Where-Object { [int]$_.Stock -eq 0 } | ForEach-Object { "out of stock: $($PSItem.Title)" }
out of stock: The Hobbit
PS> $books = Import-Csv ./data/books.csv
PS> $books.Where({ [int]$_.Stock -lt 5 }, 'First', 2).Title
Emma
The Hobbit
PS> $books.ForEach('Title').Count
12
Kitaplar pipeline'dan tek tek geçti, toplam -End'de yazıldı ve hiçbir noktada bütün liste bir array'de tutulmadı. .Where()'ın ikinci parametresi önemli: 'First', 2 ilk iki eşleşmede durur; 'Last', 'SkipUntil' ve 'Split' gibi modları da var. .ForEach('Title') ise property adı verilince o property'leri toplar.
Aynı üç blok script'in kendisinde de olabilir. Bir script pipeline'dan girdi alacaksa parametresine [Parameter(ValueFromPipeline)] eklenir ve gövdesi begin, process, end bloklarına ayrılır; process her gelen eleman için bir kez çalışır. Parameter attribute'larının ayrıntısı beşinci yazıda, burada kapak script'inde kullanacağız.
ForEach-Object -Parallel: ThrottleLimit ve $using:
ForEach-Object -Parallel, PowerShell 7.0 ile geldi. Script block her eleman için ayrı bir runspace'te (kendi variable'ları ve scope'u olan ayrı bir PowerShell çalışma ortamı) çalışır, aynı anda en fazla -ThrottleLimit kadar block çalışır. Dokümana göre varsayılan 5 ve 7.1'den beri runspace'ler bir havuzdan yeniden kullanılıyor. Ayrı runspace'in iki sonucu var. Birincisi, dışarıdaki variable'lar görünmez; önceki yazıda gördüğümüz gibi ancak $using: ile kopyalanarak gelir. İkincisi, $using: ile gelen bir object'i birden çok thread aynı anda değiştirirse sonuç bozulabilir:
PS> foreach ($run in 1..3) { $stats = @{ checked = 0 }; 1..2000 | ForEach-Object -ThrottleLimit 16 -Parallel { $s = $using:stats; $s.checked++ }; "run ${run}: $($stats.checked)" }
run 1: 1999
run 2: 2000
run 3: 2000
PS> foreach ($run in 1..3) { $n = (1..2000 | ForEach-Object -ThrottleLimit 16 -Parallel { 1 } | Measure-Object -Sum).Sum; "run ${run}: $n" }
run 1: 2000
run 2: 2000
run 3: 2000
PS> 1..3 | ForEach-Object -Parallel { $using:stats.checked = 0 }
ParserError:
Line |
1 | 1..3 | ForEach-Object -Parallel { $using:stats.checked = 0 }
| ~~~~~~~~~~~~~~~~~~~~
| The assignment expression is not valid. The input to an assignment operator must be an
| object that is able to accept assignments, such as a variable or a property.
Bir hashtable'ı 16 thread'den 2 000 kez artırınca ilk denemede 1 999 çıktı. Hata yok, uyarı yok ve her çalıştırmada farklı. Race condition'ın (iki thread'in aynı veriyi aynı anda değiştirmesi) tipik görüntüsü bu. İkinci satırdaki yol her seferinde doğru: block'lar bir şey değiştirmiyor, sonuç üretiyor; toplama işini pipeline'ı toplayan tek thread yapıyor. Dokümanda ConcurrentDictionary gibi thread-safe tiplerle bir örnek de var, ama ilk tercihin bu olsun: paralel block'tan değer döndür, paylaşılan state'i değiştirme. Son satır, $using: variable'ına doğrudan atamanın parse sırasında bile reddedildiğini gösteriyor.
Bookstore: 200 Kitap Kapağını Paralel Kontrol Etmek
Katalog sayfasındaki kitap kapakları /covers/<isbn>.jpg adresinden geliyor. Bazıları silinmiş ya da hiç yüklenmemiş; hangilerinin kırık olduğunu bulmak istiyoruz. Lab notu: Bu yazıdaki istekler, önceki yazılardaki Bookstore.Api'nin küçük sürümüne gidiyor; bu sürüm her kapak isteğini yavaş bir depolama servisi gibi 200 ms bekletip ISBN'i (kitabın uluslararası standart numarası) 7 ile biten kapaklar için 404 dönüyor. data/cover-isbns.txt'te 200 ISBN var.
#!/usr/bin/env pwsh
#Requires -Version 7.6
<#
.SYNOPSIS
Checks Bookstore book cover URLs in parallel and reports the broken ones.
.DESCRIPTION
Takes ISBNs from the pipeline or from -Path, sends a HEAD request to
<BaseUrl>/covers/<isbn>.jpg for each one and returns one result per ISBN.
With -OutFile, every cover that did not return HTTP 200 is written to a CSV file.
.PARAMETER Isbn
An ISBN-13. Accepts pipeline input.
.PARAMETER Path
A text file with one ISBN per line.
.PARAMETER BaseUrl
Base address of the server that serves the covers.
.PARAMETER ThrottleLimit
How many requests run at the same time.
.PARAMETER TimeoutSec
How long to wait for each request, in seconds.
.PARAMETER OutFile
Optional. CSV file for the broken covers.
.EXAMPLE
./Test-BookstoreCovers.ps1 -Path ../data/cover-isbns.txt -OutFile ./broken-covers.csv
.EXAMPLE
Get-Content ../data/cover-isbns.txt | ./Test-BookstoreCovers.ps1 -ThrottleLimit 20
#>
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[string] $Isbn,
[string] $Path,
[string] $BaseUrl = 'http://127.0.0.1:5080',
[ValidateRange(1, 64)]
[int] $ThrottleLimit = 10,
[int] $TimeoutSec = 5,
[string] $OutFile
)
begin {
$isbns = [System.Collections.Generic.List[string]]::new()
if ($Path) {
foreach ($line in Get-Content -Path $Path) {
if ($line.Trim()) { $isbns.Add($line.Trim()) }
}
}
}
process {
if ($Isbn) { $isbns.Add($Isbn.Trim()) }
}
end {
# Her blok kendi runspace'inde çalışır: dışarıdaki variable'lar yalnız $using: ile gelir
$results = $isbns | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
# Her istek için ayrı bir progress bar basma
$ProgressPreference = 'SilentlyContinue'
$url = "$($using:BaseUrl)/covers/$_.jpg"
$watch = [System.Diagnostics.Stopwatch]::StartNew()
$statusCode = 0
$errorMessage = $null
try {
$response = Invoke-WebRequest -Uri $url -Method Head -TimeoutSec $using:TimeoutSec -SkipHttpErrorCheck
$statusCode = [int]$response.StatusCode
}
catch {
# Bağlantı reddi, timeout, DNS: sebebi sakla, gizleme
$errorMessage = $_.Exception.Message
}
[pscustomobject]@{
Isbn = $_
StatusCode = $statusCode
Ms = $watch.ElapsedMilliseconds
Url = $url
Error = $errorMessage
}
}
$broken = @($results | Where-Object StatusCode -ne 200)
Write-Verbose "Checked $(@($results).Count) covers, $($broken.Count) broken"
if ($OutFile) {
$broken | Sort-Object Isbn | Export-Csv -Path $OutFile -UseQuotes AsNeeded
}
$results
}
Script ISBN'leri iki yoldan alabiliyor: -Path ile bir dosyadan ya da pipeline'dan. begin listeyi hazırlıyor ve dosyayı okuyor, process pipeline'dan gelen her ISBN'i ekliyor, asıl iş end'de: liste hazır olunca paralel kontrol başlıyor. Her paralel block bir sonuç object'i döndürüyor, ortak bir listeye yazmıyor. HEAD isteği resmi indirmeden yalnız durumunu soruyor. -SkipHttpErrorCheck 404'ü exception'a çevirmiyor, catch ise yalnız bağlantı hatalarında devreye giriyor ve sebebi Error alanında saklıyor. [CmdletBinding()] script'e -Verbose gibi ortak parametreleri kazandırıyor.
PS> $results = ./scripts/Test-BookstoreCovers.ps1 -Path ./data/cover-isbns.txt -OutFile ./broken-covers.csv -Verbose
VERBOSE: Checked 200 covers, 17 broken
PS> $results | Group-Object StatusCode -NoElement
Count Name
----- ----
183 200
17 404
PS> Import-Csv ./broken-covers.csv | Select-Object -First 3 | Format-Table Isbn, StatusCode, Url
Isbn StatusCode Url
---- ---------- ---
9782071752347 404 http://127.0.0.1:5080/covers/9782071752347.jpg
9782214415917 404 http://127.0.0.1:5080/covers/9782214415917.jpg
9782545564827 404 http://127.0.0.1:5080/covers/9782545564827.jpg
PS> Get-Content ./data/cover-isbns.txt -TotalCount 3 | ./scripts/Test-BookstoreCovers.ps1 | Format-Table Isbn, StatusCode, Ms
Isbn StatusCode Ms
---- ---------- --
9780183751821 200 206
9786265347364 200 208
9788908907478 200 205
PS> foreach ($t in 1, 5, 20, 50) { '{0,2} -> {1,6:N1} s' -f $t, (Measure-Command { ./scripts/Test-BookstoreCovers.ps1 -Path ./data/cover-isbns.txt -ThrottleLimit $t }).TotalSeconds }
1 -> 41.6 s
5 -> 8.3 s
20 -> 2.2 s
50 -> 1.1 s
200 kapağın 17'si kırık. Pipeline ile verilen üç ISBN'in sonuçları geliş sırasıyla değil, bitiş sırasıyla döndü; paralel çıktının sırası garanti değil, sıra önemliyse sonradan Sort-Object kullan. Son satır -ThrottleLimit'in etkisini gösteriyor: tek tek 41,6 saniye süren iş, 5 paralel istekle 8,3, 20 ile 2,2, 50 ile 1,1 saniyeye indi. Burada iş ağ beklemesi olduğu için paralellik neredeyse doğrusal kazanç getirdi. Dokümanın da uyardığı gibi, CPU'da (işlemci) kısa süren işlerde ise her runspace'in kurulma maliyeti kazancı yer ve paralel sürüm sıralıdan yavaş çalışabilir.
Üretimde Dikkat
-ThrottleLimit'i karşı tarafa göre seç, kendi makinene göre değil. Kapakları bir CDN'den (içeriği kullanıcıya yakın sunuculardan dağıtan ağ) ya da kendi depolama servisinden kontrol ediyorsan, 50 paralel istek o servisin rate limit'ine takılabilir ya da gece çalışan bir job canlı trafiği yavaşlatabilir. 10–20 arası makul bir başlangıç; karşı taraf 429 dönmeye başlarsa düşür.
Paralel block'larda hata kaybolmasın. Block'un içinde yakalanmayan bir hata ana script'e error stream üzerinden gelir, ama hangi elemanda olduğunu bulmak zorlaşır. Bu script'te olduğu gibi hatayı yakalayıp sonuç object'ine yazmak, hem raporlamayı hem de "hangisi başarısız oldu" sorusunu kolaylaştırıyor. -TimeoutSeconds parametresi de bütün paralel işe bir üst süre koyar; gece job'larında takılan bir isteğin bütün işi bekletmesini önler.
Büyük girdide belleği düşün. Bu script ISBN'leri end bloğunda topluca işliyor, 200 eleman için sorun değil. Milyonlarca satırlık bir girdide ise sonuçları $results'ta toplamak yerine pipeline'dan doğrudan Export-Csv'ye akıtmak gerekir. Bash Scripting serisinde aynı işi xargs -P ile yapmıştık; PowerShell'de -Parallel sonuçları object olarak döndürdüğü için raporlama tarafı daha kolay.
Sık Düşülen Tuzaklar
Format-Table'dan sonra veri işlemek. Format-* cmdlet'leri veri değil, ekrana basılacak biçim talimatları üretir. Arkasına bir şey bağlarsan o talimatları işlersin:
PS> $results = ./scripts/Test-BookstoreCovers.ps1 -Path ./data/cover-isbns.txt -ThrottleLimit 20
PS> $results | Where-Object StatusCode -ne 200 | Format-Table Isbn, StatusCode | Export-Csv ./broken-wrong.csv
PS> Get-Content ./broken-wrong.csv -TotalCount 3
"ClassId2e4f51ef21dd47e99d3c952918aff9cd","pageHeaderEntry","pageFooterEntry","autosizeInfo","shapeInfo","groupingEntry"
"033ecb2bc07a4d43b5ef94ed5a35d280",,,,"Microsoft.PowerShell.Commands.Internal.Format.TableHeaderInfo",
"9e210fe47d09416682b841769c78b8a3",,,,,
PS> $results | Format-Table Isbn, StatusCode | Select-Object -First 2 | Get-Member | Select-Object -First 1 TypeName
TypeName
--------
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
CSV'de ISBN yerine biçimlendirme sisteminin iç class'ları var. Format-Table ve Format-List her zaman pipeline'ın en sonunda, yalnız ekrana bakarken kullanılır. Script'ler object döndürür, biçimlendirmeyi çağıran yapar.
-Parallel içinde $using:'i unutmak. Kapak script'indeki $($using:BaseUrl) yerine $BaseUrl yazınca:
PS> Get-Content ./data/cover-isbns.txt -TotalCount 2 | ./lab/covers-nousing.ps1 | Format-List Isbn, StatusCode, Url, Error
Isbn : 9786265347364
StatusCode : 0
Url : /covers/9786265347364.jpg
Error : Invalid URI: The hostname could not be parsed.
Isbn : 9780183751821
StatusCode : 0
Url : /covers/9780183751821.jpg
Error : Invalid URI: The hostname could not be parsed.
Paralel runspace'te $BaseUrl tanımlı değil, $null olarak boş string'e dönüştü ve URL host'suz kaldı. Hatayı Error alanında sakladığımız için sebep hemen görünüyor; catch bloğunu boş bıraksaydık 200 kapağın hepsi sebepsiz "kırık" görünecekti. İlk taslakta tam olarak bunu yaşadım: Invoke-WebRequest'te olmayan bir parametre kullanmıştım, catch bu parameter binding hatasını da yuttu ve rapor 200 kırık kapak gösterdi. Dördüncü yazıda catch'in neyi yakalaması gerektiğine döneceğiz; Set-StrictMode de bu tür hataları erken yakalamaya yardım ediyor.
switch'te break'i unutmak ve ForEach-Object'te break kullanmak. İkisi de yukarıda gördüğümüz gibi C# ve bash alışkanlığıyla ters düşer: switch ilk eşleşmede durmaz, ForEach-Object içindeki break ise script'in geri kalanını atlar.
Küçük Bir Deneme
★☆☆ log-summary.ps1'i switch -Regex -File kullanacak şekilde yeniden yaz ve Get-Content ile foreach'ten kurtul. Sonucun aynı kaldığını doğrula, sonra yavaş istek eşiğini parametre yap.
★★☆ Test-BookstoreCovers.ps1'i Get-Content ./data/cover-isbns.txt | ./scripts/Test-BookstoreCovers.ps1 biçiminde çağır ve process bloğunun kaç kez çalıştığını bir Write-Verbose satırıyla say. ISBN listesinin aynı ISBN'i iki kez içermesi durumunda script'in iki istek atmasını nasıl engellersin?
★★★ -ThrottleLimit değerini 1'den 64'e kadar ikinin kuvvetleriyle dene ve süreyi bir tablo olarak çıkar. Hangi noktadan sonra kazanç duruyor? Lab'daki API'nin her isteği 200 ms beklettiğini bilerek, 200 kapak için teorik en kısa süreyi hesapla ve ölçümle karşılaştır.
Kapak script'inde hata yakalamayı bir try/catch ile geçiştirdik ve ilk taslakta bunun bedelini ödedik. Bir sonraki yazı tamamen buna ayrıldı: terminating ve non-terminating hatalar, $ErrorActionPreference, try/catch/finally, dotnet ve git gibi native komutların exit code'ları ve dotnet test başarısız olunca deploy'u durduran bir build script'i.