Variable'lar, Tipler ve Koleksiyonlar: [type] Cast, Array, Hashtable, PSCustomObject, Splatting ve String'ler
PowerShell'de karşılaştırmanın sonucu soldaki değerin tipine bağlıdır ve Import-Csv her sütunu string okur; bu yüzden veriyi en başta gerçek tiplere çevirmek gerekir. Bookstore'un kitap CSV'sinden yayınevi raporu çıkarırken $null'ı sola yazma kuralını, array ve +=, List, hashtable, PSCustomObject, splatting, string'ler, Türkçe culture'da sayı ve tarih tuzaklarını ve PowerShell 7.7'de [decimal] cast'inin .NET 11 yüzünden farklı sonuç vermesini 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; iki sürümün farklı sonuç verdiği yerler ayrıca gösterildi. Cmdlet'ler ve çıktıları Windows'ta aynıdır, yalnız yollar C:\ ile başlar ve ayırıcı \ olur. Windows PowerShell 5.1'e ait bilgiler Microsoft Learn dokümanlarından aktarıldı, çalıştırılmadı.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) katalog ekibi, kitap listesini data/books.csv olarak veriyor: ISBN (kitabın uluslararası standart numarası), başlık, yazar, yayınevi, fiyat, stok ve baskı tarihi. İlk iş en ucuz üç kitabı ve fiyatı 8'in altında olanları bulmak:
$books = Import-Csv ./data/books.csv
$books | Sort-Object Price | Select-Object -First 3 Title, Price
$books | Where-Object Price -lt 8 | Select-Object Title, Price
Listede en ucuz kitaplar 6.99 (Pride and Prejudice) ve 7.99 (Emma). İlk komut hangi üç kitabı getirir? İkincisi kaç satır döner? Bir ipucu: CSV (virgülle ayrılmış değerler) dosyasında tip diye bir şey yok.
Önceki yazıda script'in iskeletini kurduk. Bu yazıda içine giren veriye bakıyoruz: variable'lar, tipler, array, hashtable, PSCustomObject, splatting ve string'ler. Sonunda Bookstore'un kitap CSV'sinden yayınevine göre bir stok raporu çıkaran bir script yazacağız. Yol üzerinde PowerShell 7.7'nin .NET 11 ile getirdiği ve fiyat hesabını doğrudan etkileyen bir davranış değişikliğine de rastlayacağız.
Variable'lar, Tipler ve Karşılaştırmanın Sol Tarafı
Tahminin cevabı:
PS> $books = Import-Csv ./data/books.csv
PS> $books.Count
12
PS> $books | Sort-Object Price | Select-Object -First 3 Title, Price
Title Price
----- -----
The Hobbit 10.99
Middlemarch 11.50
The Fellowship of the Ring 11.99
PS> $books | Where-Object Price -lt 8 | Select-Object Title, Price
Title Price
----- -----
Dune 12.99
Emma 7.99
The Hobbit 10.99
Middlemarch 11.50
Pride and Prejudice 6.99
Crime and Punishment 17.00
Norwegian Wood 16.95
The Fellowship of the Ring 11.99
PS> $books[0].Price.GetType().FullName
System.String
Import-Csv her sütunu string olarak okur. Sort-Object Price string'leri alfabetik sıraladı: "10.99", "6.99"'dan önce gelir. Where-Object Price -lt 8 ise sekiz satır döndü ve aralarında 17.00 da var. Sebebi PowerShell'in karşılaştırma kuralı: sağdaki değer, soldakinin tipine çevrilir. Solda string olduğu için 8 de "8" string'ine dönüştü ve "17.00" -lt "8" alfabetik olarak doğru. Bu kural her şeyin temeli:
PS> $stock = 14
PS> $stock.GetType().Name
Int32
PS> $stock = 'fourteen'
PS> $stock.GetType().Name
String
PS> [int]$count = 14
PS> $count = 'fourteen'
MetadataError: Cannot convert value "fourteen" to type "System.Int32". Error: "The input string 'fourteen' was not in a correct format."
PS> $count = '15'
PS> $count.GetType().Name
Int32
PS> [decimal]'12.99' * 3
38.97
PS> [datetime]'2026-11-15' | Get-Date -Format 'yyyy-MM-dd dddd'
2026-11-15 Sunday
PS> [System.IO.Path]::Combine('releases', '1.4.2', 'Bookstore.Api.dll')
releases/1.4.2/Bookstore.Api.dll
PS> '12.99' -lt 8
True
PS> 8 -gt '12.99'
False
PS> [decimal]'12.99' -lt 8
False
PowerShell'de variable'ın tipi, içindeki değerin tipidir ve her atamada değişebilir. Variable'ı [int]$count gibi bir tiple tanımlarsan o variable'a tip kısıtı konur: her atama önce int'e çevrilmeye çalışılır, olmuyorsa hata verir. '15' string'i bu yüzden 15 sayısı olarak saklandı. Köşeli parantez .NET tiplerine de doğrudan kapı açar: [datetime], [decimal], [System.IO.Path]::Combine gibi static method'lar, C# tarafında bildiğin her şey kullanılabilir. Son üç satır karşılaştırma kuralının özeti: aynı iki değer, hangisi solda durduğuna göre farklı sonuç veriyor. Veriyi karşılaştırmadan önce tipine çevir.
Aynı kural $null karşılaştırmasını da etkiler. Solda bir array olduğunda -eq karşılaştırma yapmaz, filtre gibi çalışır ve eşleşen element'leri döndürür:
PS> $publishers = @('Ace', $null, 'Vintage', $null)
PS> ($publishers -eq $null).Count
2
PS> if ($publishers -eq $null) { 'publishers is null' } else { 'publishers has values' }
publishers is null
PS> if ($null -eq $publishers) { 'publishers is null' } else { 'publishers has values' }
publishers has values
PS> $publishers = @('Ace', $null, 'Vintage')
PS> if ($publishers -eq $null) { 'publishers is null' } else { 'publishers has values' }
publishers has values
PS> 'Dune' -eq 'DUNE'
True
PS> 'Dune' -ceq 'DUNE'
False
PS> $books = Import-Csv ./data/books.csv
PS> $books.Publisher -contains 'penguin classics'
True
PS> 'Vintage' -in $books.Publisher
True
PS> $books.Title -like '*dune*'
Dune
Children of Dune
İki $null içeren array'de $publishers -eq $null, iki element'li bir array döndürdü ve if bunu doğru saydı; tek $null kalınca dönen tek element'li array yanlış sayıldı. Aynı satır veriye göre farklı karar veriyor. $null'ı her zaman sola yaz: $null -eq $publishers, array'in kendisinin null olup olmadığını sorar. PSScriptAnalyzer da bunu PSPossibleIncorrectComparisonWithNull kuralıyla yakalıyor; serinin son yazısında açacağız. Son satırlar diğer bir varsayılanı gösteriyor: -eq, -like, -contains ve -in büyük/küçük harfe duyarsızdır. Duyarlı karşılaştırma için başlarına c koy: -ceq, -clike, -ccontains.
Array'ler: @(), += ve List
PowerShell array'i .NET'teki object[]'dir ve boyutu sabittir. += var olan array'i büyütmez; yeni bir array oluşturup variable'ı ona bağlar:
PS> $isbns = @('9780441172719', '9780141439587')
PS> $copy = $isbns
PS> $isbns += '9780547928227'
PS> "isbns: $($isbns.Count), copy: $($copy.Count)"
isbns: 3, copy: 2
PS> $isbns.GetType().Name
Object[]
$copy hâlâ eski, iki element'li array'i gösteriyor. Bu davranışın maliyeti eski bir PowerShell tavsiyesinin kaynağı: döngüde += kullanma. Microsoft'un performans dokümanı PowerShell 7.5'te +='nin iyileştirildiğini söylüyor; ne kadar iyileştiğini görmek için aynı işi üç yolla yapan küçük bir ölçüm:
foreach ($n in 10000, 20000, 40000) {
$plus = (Measure-Command { $a = @(); foreach ($i in 1..$n) { $a += "book-$i" } }).TotalMilliseconds
$list = (Measure-Command {
$l = [System.Collections.Generic.List[string]]::new()
foreach ($i in 1..$n) { $l.Add("book-$i") }
}).TotalMilliseconds
$direct = (Measure-Command { $r = foreach ($i in 1..$n) { "book-$i" } }).TotalMilliseconds
'{0,6} +=: {1,8:N0} ms List: {2,4:N0} ms foreach: {3,4:N0} ms' -f $n, $plus, $list, $direct
}
PS> ./lab/measure-add.ps1
10000 +=: 49 ms List: 18 ms foreach: 7 ms
20000 +=: 481 ms List: 8 ms foreach: 3 ms
40000 +=: 2,052 ms List: 18 ms foreach: 8 ms
PowerShell 7.7.0'da sayılar aynı aralıkta: 37, 523 ve 2 146 ms. Eleman sayısı ikiye katlanınca +='nin süresi kabaca dört katına çıkıyor, List ve foreach ise neredeyse sabit. Yani 7.5'teki iyileştirme küçük array'lerde farkı kapatmış, ama birkaç bin elemanın üstünde eski tavsiye hâlâ geçerli. Dokümana göre bu iyileştirme 7.5 öncesinde, dolayısıyla Windows PowerShell 5.1'de de yok. En temiz yol üçüncüsü: döngünün çıktısını doğrudan bir variable'a atamak. Döngü boyunca biriktirip arada eleman çıkarman da gerekiyorsa generic List kullan:
PS> $l = [System.Collections.Generic.List[string]]::new()
PS> $l.Add('Dune')
PS> $l.Add(42)
PS> $l
Dune
42
PS> $l.Remove('Dune')
True
PS> $l.Count
1
Add ve Remove gibi method'lar bir değer döndürüyorsa (burada Remove'un True'su) o değer çıktıya karışır. Script içinde $null = $list.Remove('Dune') diye yutmak gerekir; function'ların çıktısını konuştuğumuz beşinci yazıda bu konuya döneceğiz. List[string]'e verilen 42 de string'e çevrildi; tip kısıtı burada da çalışıyor.
Hashtable, [ordered] ve PSCustomObject
Hashtable anahtar-değer çiftleri tutar, C#'taki Dictionary'ye benzer. Varsayılan hashtable'ın iki özelliği var: anahtarlar büyük/küçük harfe duyarsızdır ve sıra korunmaz. Sıra önemliyse [ordered] kullan:
PS> $stock = @{ Dune = 14; Emma = 3; 'The Hobbit' = 0 }
PS> $stock['emma']
3
PS> $stock.Dune
14
PS> $stock.Keys
The Hobbit
Emma
Dune
PS> $ordered = [ordered]@{ Dune = 14; Emma = 3; 'The Hobbit' = 0 }
PS> $ordered.Keys
Dune
Emma
The Hobbit
PS> $ordered[0]
14
PS> $book = [pscustomobject]@{ Isbn = '9780441172719'; Title = 'Dune'; Price = 12.99d; Stock = 14 }
PS> $book
Isbn Title Price Stock
---- ----- ----- -----
9780441172719 Dune 12.99 14
PS> $book.Price.GetType().Name
Decimal
PS> $book | Add-Member -NotePropertyName StockValue -NotePropertyValue ($book.Price * $book.Stock)
PS> $book.StockValue
181.86
PS> $book | Get-Member -MemberType NoteProperty | Select-Object Name, Definition
Name Definition
---- ----------
Isbn string Isbn=9780441172719
Price decimal Price=12.99
Stock int Stock=14
StockValue decimal StockValue=181.86
Title string Title=Dune
$stock['emma'], Emma anahtarını buldu. Keys eklediğimiz sırayla gelmedi, [ordered] ise sırayı korudu ve index ile de erişilebiliyor. Bir kayıt, yani "kitap" gibi sabit alanları olan bir şey için hashtable yerine [pscustomobject] kullan. Literal bir hashtable'ı [pscustomobject]'e cast etmek alan sırasını korur, sonuç tablo olarak düzgün görünür, Export-Csv ve ConvertTo-Json ile beklediğin gibi çalışır. 12.99d'deki d son eki sayıyı doğrudan decimal yapar; bu ayrıntı birazdan önemli olacak.
Splatting
Bir cmdlet'e çok sayıda parametre veriyorsan onları bir hashtable'da toplayıp @ ile geçirebilirsin. Buna splatting deniyor:
PS> $request = @{ Uri = 'http://127.0.0.1:5080/api/books'; TimeoutSec = 5; Headers = @{ Accept = 'application/json' } }
PS> $catalog = Invoke-RestMethod @request
PS> $catalog.Count
5
PS> $catalog[0].price.GetType().Name
Double
PS> $request.TimeoutSec = 2
PS> Invoke-RestMethod @request -Uri http://127.0.0.1:5098/api/books
Invoke-RestMethod: Connection refused
PS> Invoke-RestMethod $request
Invoke-RestMethod: Cannot bind parameter 'Uri'. Cannot convert the "System.Collections.Hashtable" value of type "System.Collections.Hashtable" to type "System.Uri".
Hashtable'ın anahtarları parametre adları. Değerleri koşula göre değiştirip aynı çağrıyı yeniden kullanabilirsin. PowerShell 7.1'den beri splat edilen bir parametreyi komutta açıkça yazarak ezebilirsin; -Uri satırı bunu gösteriyor. Son satır sık yapılan hatayı gösteriyor: $request yazınca hashtable tek bir değer olarak ilk positional parametreye, yani -Uri'ye gitti. Splatting için @ şart. Array ile splatting de var: $dotnetArgs = @('test', '--nologo') ve dotnet @dotnetArgs; dördüncü yazıda build script'inde kullanacağız. price'ın Double gelmesini de not et; birazdan döneceğiz.
String'ler: Tırnaklar, $(), Here-String ve -f
PS> $book = [pscustomobject]@{ Title = 'Dune'; Price = 12.99d; Stock = 14 }
PS> 'Title: $book.Title'
Title: $book.Title
PS> "Title: $book.Title"
Title: @{Title=Dune; Price=12.99; Stock=14}.Title
PS> "Title: $($book.Title), stock value: $($book.Price * $book.Stock)"
Title: Dune, stock value: 181.86
PS> '{0,-12}{1,8:N2}{2,6}' -f $book.Title, $book.Price, $book.Stock
Dune 12.99 14
Tek tırnak içindeki her şey olduğu gibi kalır. Çift tırnakta variable'lar genişletilir, ama yalnız variable adı: "$book.Title", $book'u string'e çevirip arkasına .Title yazdı. Property, method çağrısı ya da hesap için $() subexpression kullan. -f operatörü .NET'in String.Format'ıdır: {0,-12} sola yaslı 12 karakter, {1,8:N2} sağa yaslı 8 karakter ve iki ondalık. Çok satırlı metin için here-string var; @" ile "@ kendi satırlarında durmalı:
PS> Get-Content ./lab/here.ps1
$release = '1.4.2'
$notes = @"
Bookstore.Api $release
Built: $(Get-Date -Format 'yyyy-MM-dd')
Path: C:\deploy\$release
"@
$notes
PS> ./lab/here.ps1
Bookstore.Api 1.4.2
Built: 2026-11-15
Path: C:\deploy\1.4.2
@" genişletir, @' genişletmez. Windows yolundaki \ PowerShell'de kaçış karakteri değil (kaçış karakteri backtick); C:\deploy\$release bu yüzden sorunsuz.
Culture: Türkçe Bölge Ayarında Sayılar ve Tarihler
Okur Windows'ta çalışıyor ve çoğu Türk geliştiricinin makinesinde bölge ayarı Türkçe. Bunun script'e etkisini görmek için session'ın culture'ını tr-TR yapalım:
PS> [cultureinfo]::CurrentCulture = 'tr-TR'
PS> (Get-Culture).Name
tr-TR
PS> $price = 12.99d
PS> "interpolation: $price"
interpolation: 12.99
PS> 'format: {0:N2}' -f $price
format: 12,99
PS> "ToString: $($price.ToString())"
ToString: 12,99
PS> [decimal]'12.99'
12,99
PS> [decimal]::Parse('12.99')
1299
PS> [decimal]::Parse('12.99', [cultureinfo]::InvariantCulture)
12,99
PS> 'title' -eq 'TİTLE'
False
PS> 'title'.ToUpper()
TİTLE
PS> 'title'.ToUpperInvariant()
TITLE
Üç farklı davranış var. String interpolation ("$price") ve PowerShell cast'leri ([decimal]'12.99') culture'dan bağımsızdır, dokümandaki adıyla invariant culture kullanır. -f, ToString() ve ekrana yazılan çıktı ise o anki culture'ı kullanır, bu yüzden 12,99 oldu. En tehlikelisi .NET method'larının culture almayan sürümleri: [decimal]::Parse('12.99') Türkçe ayarda noktayı binlik ayırıcı sayıp 1299 döndürdü. Hata yok, uyarı yok, yalnız yüz kat yanlış fiyat. Son satırlar da Türkçe İ konusunu gösteriyor: -eq invariant culture ile karşılaştırdığı için 'title' -eq 'TİTLE' yanlış, ToUpper() ise Türkçe kuralla TİTLE üretiyor.
Tarihlerde durum daha da şaşırtıcı:
PS> [cultureinfo]::CurrentCulture = 'tr-TR'
PS> ([datetime]'06.05.2003').ToString('yyyy-MM-dd')
2003-06-05
PS> (Get-Date -Date '06.05.2003').ToString('yyyy-MM-dd')
2003-05-06
PS> ([datetime]'2003-05-06').ToString('yyyy-MM-dd')
2003-05-06
PS> 'title' -eq 'TITLE'
True
Aynı '06.05.2003' string'i cast ile 5 Haziran, Get-Date -Date parametresiyle 6 Mayıs oldu. Dokümana göre invariant culture ABD tarzı, ay önce gelen tarih kullanır; cmdlet'ler ise parameter binding sırasında culture'a duyarlı çeviri yapar. Script'lerde ve veri dosyalarında tarihi her zaman yyyy-MM-dd biçiminde tut; üçüncü satır iki yoldan da aynı sonucu veriyor.
Bir de sürüm farkı var. PowerShell 7.7 .NET 11 üzerinde çalışıyor ve .NET 11'in breaking change listesinde, double'dan decimal'a çevirinin artık 15 anlamlı basamakta yuvarlanmadığı yazıyor. PowerShell'in string'den [decimal]'a cast'i bu çeviriden geçtiği için aynı satır iki sürümde farklı sonuç veriyor:
PS> $PSVersionTable.PSVersion.ToString()
7.6.10
PS> [decimal]'12.99'
12.99
PS> [decimal]'6.99' * 21
146.79
PS> [decimal]::Parse('6.99', [cultureinfo]::InvariantCulture) * 21
146.79
PS> 6.99d * 21
146.79
PS> $PSVersionTable.PSVersion.ToString()
7.7.0
PS> [decimal]'12.99'
12.990000000000000213162820728
PS> [decimal]'6.99' * 21
146.79000000000000447641923529
PS> [decimal]::Parse('6.99', [cultureinfo]::InvariantCulture) * 21
146.79
PS> 6.99d * 21
146.79
7.7'deki değer, 12.99 double'ının tam karşılığı; yani cast, string'i önce double olarak okuyor ve .NET 11 aradaki yuvarlamayı kaldırdı. [decimal]::Parse ile string doğrudan decimal'a çevriliyor ve iki sürümde de doğru. Terminal ve Command Line serisinin dördüncü yazısında JSON'dan gelen double fiyatları [decimal]'a çevirmeyi önermiştik; 7.6'da bu doğru sonuç veriyor, 7.7'de ise çevirdikten sonra [math]::Round(..., 2) ile yuvarlaman gerekiyor. Para hesabında kural: string'den [decimal]::Parse(..., [cultureinfo]::InvariantCulture), literal için d son eki.
Scope'lar: $script:, $global: ve $using:
Bir function içinde script'teki bir variable'ı okuyabilirsin, ama ona atama yaptığında function kendi scope'unda aynı adla yeni bir variable oluşturur:
$outOfStock = 0
function Test-Stock($book) {
if ($book.Stock -eq 0) { $outOfStock++ }
}
function Test-StockScript($book) {
if ($book.Stock -eq 0) { $script:outOfStock++ }
}
$books = Import-Csv ./data/books.csv
foreach ($book in $books) { Test-Stock $book }
"after Test-Stock: $outOfStock"
foreach ($book in $books) { Test-StockScript $book }
"after Test-StockScript: $outOfStock"
PS> ./lab/scope.ps1
after Test-Stock: 0
after Test-StockScript: 1
PS> $limit = 8
PS> 1..3 | ForEach-Object -Parallel { "item $_, limit: $limit" }
item 1, limit:
item 2, limit:
item 3, limit:
PS> 1..3 | ForEach-Object -Parallel { "item $_, limit: $using:limit" }
item 1, limit: 8
item 2, limit: 8
item 3, limit: 8
Test-Stock, $outOfStock'un kendi kopyasını artırdı ve script'teki sayaç 0'da kaldı. $script:outOfStock açıkça script scope'undaki variable'ı değiştirdi. $global: ise bütün session'a yazar; script bittikten sonra da kalır ve bir sonraki çalıştırmayı etkileyebilir, bu yüzden script'lerde kullanma. Daha temiz yol, function'ın sonucu döndürmesi ve sayacı çağıranın tutması; beşinci yazıda function'ları bu şekilde yazacağız.
$using: başka bir runspace'te (ayrı bir PowerShell çalışma ortamı) çalışan koda yerel bir değeri taşır. ForEach-Object -Parallel blokları ayrı runspace'lerde çalıştığı için $limit'i göremedi; $using:limit ile değer kopyalandı. Aynı şey remoting'de Invoke-Command için de geçerli. Ayrıntısını bir sonraki yazıda paralel çalışmada, remoting'i de yedinci yazıda göreceğiz.
Bookstore: Kitap CSV'sinden Yayınevi Raporu
Şimdi hepsini bir araya getirelim. CSV dosyasının başı:
Isbn,Title,Author,Publisher,Price,Stock,Published
9780441172719,Dune,Frank Herbert,Ace,12.99,14,1990-09-01
9780141439587,Emma,Jane Austen,Penguin Classics,7.99,3,2003-05-06
9780547928227,The Hobbit,J.R.R. Tolkien,Mariner,10.99,0,2012-09-18
Script, satırları bir kez ve açık bir culture ile gerçek tiplere çeviriyor, sonra yayınevine göre gruplayıp her yayınevi için bir PSCustomObject döndürüyor:
#!/usr/bin/env pwsh
#Requires -Version 7.6
<#
.SYNOPSIS
Summarizes the Bookstore book list per publisher.
.DESCRIPTION
Reads a book CSV (Isbn, Title, Author, Publisher, Price, Stock, Published),
converts the columns to real types and returns one object per publisher:
number of titles, total stock, stock value, average price and newest edition.
.PARAMETER Path
The book CSV. Defaults to data/books.csv in the repo.
.PARAMETER OutFile
Optional. Writes the report as a semicolon-separated CSV for Excel.
.EXAMPLE
./Get-BookstorePublisherReport.ps1 | Format-Table
.EXAMPLE
./Get-BookstorePublisherReport.ps1 -OutFile ./publisher-report.csv
#>
param(
[string] $Path = (Join-Path -Path $PSScriptRoot -ChildPath '..', 'data', 'books.csv'),
[string] $OutFile
)
$invariant = [cultureinfo]::InvariantCulture
# CSV sütunları string: hepsini burada, bir kez ve culture belirterek çevir.
# Fiyat doğrudan decimal olarak parse ediliyor; 7.7'de [decimal] cast double üzerinden geçiyor.
$books = foreach ($row in Import-Csv -Path $Path) {
[pscustomobject]@{
Isbn = $row.Isbn
Title = $row.Title
Publisher = $row.Publisher
Price = [decimal]::Parse($row.Price, $invariant)
Stock = [int]$row.Stock
Published = [datetime]::ParseExact($row.Published, 'yyyy-MM-dd', $invariant)
}
}
$report = foreach ($group in $books | Group-Object Publisher) {
$stock = 0
$stockValue = 0d
$priceSum = 0d
foreach ($book in $group.Group) {
$stock += $book.Stock
$stockValue += $book.Price * $book.Stock
$priceSum += $book.Price
}
$newest = $group.Group | Sort-Object Published -Descending | Select-Object -First 1
[pscustomobject]@{
Publisher = $group.Name
Titles = $group.Count
Stock = $stock
StockValue = $stockValue
AvgPrice = [math]::Round($priceSum / $group.Count, 2)
Newest = $newest.Published.ToString('yyyy-MM-dd', $invariant)
}
}
$report = $report | Sort-Object StockValue -Descending
if ($OutFile) {
$csvOptions = @{
Path = $OutFile
Delimiter = ';'
Encoding = 'utf8BOM'
UseQuotes = 'AsNeeded'
}
$report | Export-Csv @csvOptions
Write-Output "Report written to $OutFile ($(@($report).Count) publishers)"
}
else {
$report
}
Birkaç karar var. Varsayılan dosya yolu $PSScriptRoot'a göre kuruluyor, böylece script hangi klasörden çağrılırsa çağrılsın data/books.csv'yi bulur. Para toplamları Measure-Object yerine döngüde tutuluyor, çünkü Measure-Object'in Sum ve Average sonuçları double. Script tablo basmıyor, object döndürüyor; biçimlendirme çağırana kalıyor:
PS> ./scripts/Get-BookstorePublisherReport.ps1 | Select-Object -First 2
Publisher : Penguin Classics
Titles : 4
Stock : 39
StockValue : 320.67
AvgPrice : 8.87
Newest : 2003-05-06
Publisher : Ace
Titles : 2
Stock : 19
StockValue : 231.81
AvgPrice : 11.49
Newest : 2008-07-01
PS> ./scripts/Get-BookstorePublisherReport.ps1 | Format-Table
Publisher Titles Stock StockValue AvgPrice Newest
--------- ------ ----- ---------- -------- ------
Penguin Classics 4 39 320.67 8.87 2003-05-06
Ace 2 19 231.81 11.49 2008-07-01
Signet Classic 1 18 179.82 9.99 1961-01-01
Harper Perennial 1 11 103.29 9.39 2002-01-01
Vintage 2 6 101.90 16.98 2000-09-12
Mariner 2 7 83.93 11.49 2012-09-18
PS> $report = ./scripts/Get-BookstorePublisherReport.ps1
PS> $report[0].StockValue.GetType().Name
Decimal
PS> ($report | Measure-Object Titles -Sum).Sum
12
PS> ./scripts/Get-BookstorePublisherReport.ps1 -OutFile ./publisher-report.csv
Report written to ./publisher-report.csv (6 publishers)
PS> Get-Content ./publisher-report.csv -TotalCount 3
Publisher;Titles;Stock;StockValue;AvgPrice;Newest
Penguin Classics;4;39;320.67;8.87;2003-05-06
Ace;2;19;231.81;11.49;2008-07-01
Altı property'li bir object varsayılan olarak liste görünümünde basılır (biçim tanımı olmayan object'lerde PowerShell dört property'ye kadar tablo, beş ve üstünde liste seçiyor); tablo isteyen çağıran Format-Table ekliyor. Object döndürmenin asıl kazancı üçüncü satırda: rapor bir variable'a alınıp Measure-Object ile yeniden işlenebiliyor ve StockValue hâlâ Decimal. -OutFile ise Excel için noktalı virgülle ayrılmış ve BOM'lu (başında byte order mark işareti olan) bir Unicode dosyası (UTF-8) yazıyor; Türkçe Excel'in neden bu ikisini istediğini .NET'te I/O İşlemleri serisinin CSV yazısında anlattık. Aynı komut Türkçe culture'da çalışınca:
PS> [cultureinfo]::CurrentCulture = 'tr-TR'
PS> ./scripts/Get-BookstorePublisherReport.ps1 -OutFile ./publisher-report-tr.csv
Report written to ./publisher-report-tr.csv (6 publishers)
PS> Get-Content ./publisher-report-tr.csv -TotalCount 3
Publisher;Titles;Stock;StockValue;AvgPrice;Newest
Penguin Classics;4;39;320,67;8,87;2003-05-06
Ace;2;19;231,81;11,49;2008-07-01
PS> Format-Hex ./publisher-report-tr.csv -Count 16
Label: /home/user/bookstore/publisher-report-tr.csv
Offset Bytes Ascii
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
------ ----------------------------------------------- -----
0000000000000000 EF BB BF 50 75 62 6C 69 73 68 65 72 3B 54 69 74 Publisher;Tit
Export-Csv sayıları o anki culture ile yazdı: 320,67. Türkçe Excel için doğru, ama aynı dosyayı okuyan başka bir script için sürpriz. Dosyanın ilk üç byte'ı (EF BB BF) da BOM.
Üretimde Dikkat
Bir script'in ürettiği dosya, script'in çalıştığı makinenin culture'ına bağlı olmamalı. Gece çalışan bir rapor sunucuda İngilizce, geliştiricinin makinesinde Türkçe culture ile çalışınca iki farklı dosya üretir. Makineler arası veri için culture'dan bağımsız biçim seç: JSON, ISO (Uluslararası Standardizasyon Örgütü) 8601 tarih biçimi (yyyy-MM-dd), noktalı ondalık. İnsan için üretilen Excel dosyasında ise culture'ı bilinçli seç ve script'in başında [cultureinfo]::CurrentCulture ile sabitle.
Veriyi okurken tipini bir kez ve sınırda belirle. Raporda olduğu gibi CSV'den gelen satırları en başta gerçek tiplere çevir; script'in geri kalanı string'lerle değil, decimal, int ve datetime ile çalışsın. Böylece Önce Tahmin Et'teki sıralama ve filtreleme hataları baştan oluşmaz. Para için decimal, string'den çevirirken [decimal]::Parse kullan; 7.7'deki değişiklik, 7.6'da doğru çalışan bir script'in sessizce farklı toplam üretmesine yol açabilir.
Büyük veride bellek de önemli. Import-Csv bütün dosyayı bir array'e alırsa 1 milyon satırlık bir sipariş dosyası belleğe sığmayabilir. Satırları pipeline'da tek tek işlemek (Import-Csv ... | ForEach-Object { ... }) belleği sabit tutar; bir sonraki yazının konusu bu.
Sık Düşülen Tuzaklar
Tek elemanlı sonucun array olmaması. Bir komut tek bir sonuç döndürürse PowerShell onu array'e sarmaz:
PS> $books = Import-Csv ./data/books.csv
PS> $titles = $books | Where-Object Publisher -eq 'Signet Classic' | ForEach-Object Title
PS> $titles
1984
PS> $titles.GetType().Name
String
PS> $titles[0]
1
PS> $titles.Count
1
PS> $titles = @($books | Where-Object Publisher -eq 'Signet Classic' | ForEach-Object Title)
PS> $titles[0]
1984
PS> $none = $books | Where-Object Publisher -eq 'Tor'
PS> $null -eq $none
True
PS> @($none).Count
0
Signet Classic'in tek kitabı var, bu yüzden $titles bir array değil, "1984" string'i oldu ve $titles[0] ilk karakteri, 1'i döndürdü. Count'un 1 demesi yanıltıcı: PowerShell tek bir değere de Count verir. Sonucu array olarak kullanacaksan @() ile sar. Hiç sonuç yoksa variable $null olur, @() ile sarınca boş array.
-eq'nun array'de filtre gibi davranması. Yukarıda $null ile gördük; aynısı if ($books.Publisher -eq 'Ace') gibi satırlarda da olur. Sonuç bir array'dir ve if onu boş olup olmamasına göre değerlendirir. Tek bir değeri kontrol etmek istiyorsan -contains ya da -in kullan, bunlar her zaman True ya da False döner.
Büyük/küçük harf. -eq ve hashtable anahtarları varsayılan olarak duyarsız, C#'tan gelen biri için beklenmedik. ISBN gibi duyarsızlığın sorun olmadığı yerde rahatlık, dosya adı ya da token karşılaştırmasında ise risk. Linux'ta dosya sistemi büyük/küçük harfe duyarlı olduğu için -eq ile eşit bulduğun iki dosya adı aslında iki ayrı dosya olabilir; gerekiyorsa -ceq kullan.
Hashtable'ı tek değer olarak geçirmek. Splatting'de $ yerine @ yazılır. Invoke-RestMethod $request hata verdi, ama hashtable'ı kabul eden bir parametreye denk gelseydi hiç hata vermeden yanlış çalışabilirdi.
Küçük Bir Deneme
★☆☆ Import-Csv'den gelen kitapları [decimal]::Parse ile çevirip Önce Tahmin Et'teki iki komutu tekrar çalıştır. Sonra aynı filtreyi Where-Object { [decimal]$_.Price -lt 8 } ile yaz ve 7.6'da sonucun neden yine doğru olduğunu açıkla.
★★☆ Rapor script'ine -Culture parametresi ekle (varsayılan tr-TR); -OutFile verildiğinde dosyayı o culture ile yazsın, script bittiğinde de session'ın culture'ını eski hâline getirsin. -Culture en-US ile üretilen dosyayı Türkçe Excel'de açınca ne görürsün?
★★★ Invoke-RestMethod ile /api/books'tan gelen kitapların toplam stok değerini PowerShell 7.6 ve 7.7'de ayrı ayrı hesapla. İki sonucu karşılaştır, sonra 7.7'de de 7.6 ile aynı sonucu veren bir dönüşüm yaz. İpucu: JSON'daki fiyat zaten double olarak geliyor.
Rapor çalışıyor, ama foreach'i ve Group-Object'i şimdilik açıklamadan kullandık. Bir sonraki yazıda koşullara, döngülere ve pipeline'ı script'te kullanmaya geçiyoruz: switch, foreach ile ForEach-Object arasındaki fark, begin/process/end blokları ve Bookstore'un 200 kitap kapağını ForEach-Object -Parallel ile aynı anda kontrol etmek.