Mert Özen Her Satırda Daha İleri
DevOps PowerShell Scripting — #01

İlk PowerShell Script'i: .ps1, Çalıştırma Yolları, #Requires, Comment-Based Help ve pwsh mi powershell.exe mi?

Mert Özen 15 Kas 2026 16 dk 3 görüntülenme
İlk PowerShell Script'i: .ps1, Çalıştırma Yolları, #Requires, Comment-Based Help ve pwsh mi powershell.exe mi?

Bir PowerShell script'i PowerShell içinden çağrılınca aynı process'te kendi scope'unda, pwsh -File ile çağrılınca yeni bir process'te çalışır ve sonucunu exit code ile bildirir; Write-Error tek başına exit code'u değiştirmez. Bookstore API'sini kontrol eden Test-BookstoreHealth.ps1 ile &, dot-sourcing, -File ve -Command farkını, Linux'ta shebang'i, #Requires'ı, comment-based help'i, Windows PowerShell 5.1 ile 7.6 LTS farklarını ve BOM/CRLF tuzaklarını 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 .NET 10 SDK (10.0.404) kurulu bir ortamda denenmiştir. Cmdlet'ler ve çıktıları Windows'ta aynıdır, yalnız yollar C:\ ile başlar. Windows PowerShell 5.1 ve Windows'a özgü davranışlar Microsoft Learn dokümanlarından aktarıldı; bu kısımlar çalıştırılmadı.

Önce Tahmin Et

Ekipten biri deploy pipeline'ına küçük bir kontrol eklemiş. Bookstore API'sinin health endpoint'ine soruyor, cevap Healthy değilse hata yazıyor:

$r = Invoke-RestMethod http://127.0.0.1:5080/health -SkipHttpErrorCheck
if ($r.status -ne 'Healthy') {
    Write-Error "Bookstore API is unhealthy: $($r.status)"
}

CI adımı script'i pwsh -NoProfile -File ./check-health.ps1 diye çağırıyor. API o sırada 503 ve Unhealthy dönüyor. Ekranda kırmızı bir hata satırı göreceğin kesin; asıl soru şu: pwsh hangi exit code ile çıkar, ve CI adımı kırmızıya döner mi? Cevap "Çalıştırmanın Yolları" bölümünden hemen sonra.

Bu yazıyla PowerShell Scripting serisine başlıyoruz. Terminal ve Command Line serisinin dördüncü yazısı PowerShell 7'yi etkileşimli olarak kurdu: object pipeline, $PROFILE, PSReadLine, execution policy ve bash karşılıkları orada. Bu seri, aynı shell'i dosyaya yazılan, her seferinde aynı şekilde çalışan, test edilen script'lere ve module'lere taşımakla ilgili. Bash Scripting serisini okuduysan yapı tanıdık gelecek: sekiz yazı, aynı bloklar, aynı Bookstore işleri. Okur platformu Windows olduğu için script'ler önce Windows'taki pwsh düşünülerek yazılıyor, ama aynı dosyanın Linux'taki bir CI runner'ında da çalışması hedef. İlk yazıda iskeleti kuruyoruz: .ps1 dosyası, çalıştırma yolları, exit code, #Requires, comment-based help ve hangi PowerShell'i hedefleyeceğin.

İlk Script: Bookstore API'sinin Health Endpoint'ini Kontrol Etmek

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'si lab'da 127.0.0.1:5080'de dinliyor; /health sağlıklıyken 200 ve {"status":"Healthy"}, sorun varken 503 ve Unhealthy dönüyor. Seri boyunca script'leri repo'da scripts/ klasöründe topluyoruz. İlki Test-BookstoreHealth.ps1:

#!/usr/bin/env pwsh
#Requires -Version 7.6

<#
.SYNOPSIS
    Checks the health endpoint of the Bookstore API.

.DESCRIPTION
    Calls /health on Bookstore.Api and prints one status line.
    The exit code is the contract for CI pipelines, deploy scripts and monitoring tools.

.PARAMETER BaseUrl
    Base address of the API, without /health.

.PARAMETER TimeoutSec
    How long to wait for the API, in seconds.

.EXAMPLE
    ./Test-BookstoreHealth.ps1

    Checks the local API at http://127.0.0.1:5080.

.EXAMPLE
    ./Test-BookstoreHealth.ps1 -BaseUrl https://staging.bookstore.test -TimeoutSec 10

    Checks the staging API and waits up to 10 seconds.

.OUTPUTS
    None. The result is the exit code: 0 healthy, 1 unhealthy or unreachable.
#>
param(
    [string] $BaseUrl = 'http://127.0.0.1:5080',
    [int] $TimeoutSec = 5
)

$url = "$($BaseUrl.TrimEnd('/'))/health"

try {
    $response = Invoke-RestMethod -Uri $url -TimeoutSec $TimeoutSec -SkipHttpErrorCheck -StatusCodeVariable statusCode
}
catch {
    Write-Output "FAIL $url ($($_.Exception.Message))"
    exit 1
}

if ($statusCode -eq 200 -and $response.status -eq 'Healthy') {
    Write-Output "OK $url"
    exit 0
}

Write-Output "FAIL $url (HTTP $statusCode, status: $($response.status))"
exit 1

Yukarıdan aşağı: ilk satır Linux'ta shebang olarak iş görecek, Windows'ta sıradan bir yorumdur. #Requires script'in hangi PowerShell'i istediğini söylüyor. <# ... #> bloğu comment-based help; Get-Help bu bloğu okuyacak. param() bloğu script'e tipli ve varsayılan değerli parametreler veriyor: -BaseUrl bir string, -TimeoutSec bir int. Script'e -TimeoutSec abc verirsen script çalışmadan, parameter binding sırasında hata alırsın.

Invoke-RestMethod varsayılan olarak 4xx ve 5xx cevaplarını exception'a çevirir. -SkipHttpErrorCheck bunu kapatıyor, -StatusCodeVariable statusCode da HTTP kodunu $statusCode değişkenine yazıyor; ikisi de PowerShell 7 ile geldi. Böylece 503'ü exception yakalamadan, sıradan bir if ile ele alabiliyoruz. try/catch yalnız bağlantı kurulamadığında ya da süre dolduğunda devreye giriyor. Hata türlerinin ayrıntısı dördüncü yazının konusu; burada bilmen gereken tek şey, script'in her yolda bilinçli bir exit ile bitmesi.

Lab notu: Bu yazıdaki çıktılar, Bookstore.Api'nin /health ve /api/books endpoint'lerini veren küçük bir sürümüyle alındı; /tmp/bookstore-unhealthy dosyası varken health 503 dönüyor. 5099 portunda ise bağlantıyı kabul edip hiç cevap vermeyen bir test servisi var.

PS> ./scripts/Test-BookstoreHealth.ps1; $LASTEXITCODE
OK http://127.0.0.1:5080/health
0
PS> ./scripts/Test-BookstoreHealth.ps1; $LASTEXITCODE
FAIL http://127.0.0.1:5080/health (HTTP 503, status: Unhealthy)
1
PS> ./scripts/Test-BookstoreHealth.ps1 -BaseUrl http://127.0.0.1:5098; $LASTEXITCODE
FAIL http://127.0.0.1:5098/health (Connection refused (127.0.0.1:5098))
1
PS> ./scripts/Test-BookstoreHealth.ps1 -BaseUrl http://127.0.0.1:5099 -TimeoutSec 2; $LASTEXITCODE
FAIL http://127.0.0.1:5099/health (The request was canceled due to the configured HttpClient.Timeout of 2 seconds elapsing.)
1

Dört durum, iki sonuç: sağlıklıysa 0, değilse 1. API'nin hiç ayakta olmaması, 503 dönmesi ya da cevap vermeden beklemesi script'i çağıran taraf için aynı şey demek. Son satırdaki 2 saniyelik süre sınırını Üretimde Dikkat'te ayrıca konuşacağız.

Çalıştırmanın Yolları: ./, &, Dot-Sourcing ve pwsh -File

bash'te ./script.sh her zaman yeni bir process açar. PowerShell'de durum farklı: bir .ps1 dosyasını PowerShell içinden çağırdığında aynı process'te, ama yeni bir scope'ta çalışır. Farkı görmek için bir variable atayan, klasör değiştiren ve kendi PID'ini (process kimlik numarası) yazan küçük bir script yeterli:

PS> "shell: pid=$PID location=$PWD"
shell: pid=1879 location=/home/user/bookstore
PS> & ./lab/where.ps1
script: pid=1879 location=/tmp deployTarget=staging
PS> "after &: location=$PWD deployTarget=$deployTarget"
after &: location=/tmp deployTarget=
PS> Set-Location /home/user/bookstore
PS> . ./lab/where.ps1
script: pid=1879 location=/tmp deployTarget=staging
PS> "after dot-sourcing: location=$PWD deployTarget=$deployTarget"
after dot-sourcing: location=/tmp deployTarget=staging
PS> Set-Location /home/user/bookstore
PS> Remove-Variable deployTarget
PS> pwsh -NoProfile -File ./lab/where.ps1
script: pid=1905 location=/tmp deployTarget=staging
PS> "after pwsh -File: location=$PWD deployTarget=$deployTarget"
after pwsh -File: location=/home/user/bookstore deployTarget=
PS> . ./lab/bye.ps1; "after dot-sourcing bye.ps1, LASTEXITCODE=$LASTEXITCODE"
loading settings
after dot-sourcing bye.ps1, LASTEXITCODE=3

01-run-modes-tr.png

& ./lab/where.ps1 (call operator) ile ./lab/where.ps1 aynı şeyi yapar; &, yol bir variable'da ya da boşluk içeriyorsa gerekir: & "C:\Bookstore Tools\Test-BookstoreHealth.ps1". Script kendi scope'unda çalıştığı için $deployTarget geri dönmedi. Ama dikkat: Set-Location geri dönmedi değil, sende kaldı. Konum session'a ait, scope'a değil; bash'te alt process'te kalan cd, PowerShell'de çağıranın konumunu değiştiriyor. Klasör değiştiren script'lerde Push-Location ve Pop-Location'ı bir try/finally içinde kullanmanın sebebi bu.

Dot-sourcing (. ./lab/where.ps1, nokta ve boşluk) script'i senin scope'unda çalıştırır: variable da, function'lar da sende kalır. bash'teki source'un karşılığı; ortak function'ları bir common.ps1'de toplayıp script'lerin başında dot-source etmek için kullanılır. Bu işin daha düzgün yolu module, beşinci yazıda oraya geçeceğiz. pwsh -File ise gerçekten yeni bir process açar (PID 1905), script bitince geriye yalnız exit code kalır. CI, scheduled task, cmd.exe ya da bash gibi PowerShell dışından yapılan her çağrı bu yolu kullanır.

Son satır bash'ten gelenleri şaşırtır: dot-source edilen bir script'teki exit, session'ı kapatmadı. PowerShell'de script dosyasındaki exit yalnız o script'i bitirir ve değeri $LASTEXITCODE'a yazar; session'ı kapatan, prompt'a doğrudan yazılan exit'tir.

Tahminin Cevabı: -File ile -Command Arasındaki Exit Code Farkı

Önce Tahmin Et'teki script'in gerçek çıktısı:

PS> pwsh -NoProfile -File ./lab/check-health.ps1; "exit code: $LASTEXITCODE"
Write-Error: Bookstore API is unhealthy: Unhealthy
exit code: 0
PS> pwsh -NoProfile -Command ./lab/check-health.ps1; "exit code: $LASTEXITCODE"
Write-Error: Bookstore API is unhealthy: Unhealthy
exit code: 0

Hata ekranda, ama exit code 0: CI adımı yeşil kalır ve deploy devam eder. Write-Error bir non-terminating error üretir; hata kaydedilir ve ekrana basılır, ama script çalışmaya devam eder ve normal biter. about_Pwsh dokümanına göre -File ile çalışan bir script normal biterse exit code her zaman 0 olur; 0 dışında bir değer için ya exit ile kendin vermelisin ya da script terminating bir hatayla durmalı (o zaman 1). Bu yüzden Test-BookstoreHealth.ps1 sonucu Write-Error ile değil, exit ile bildiriyor.

-Command ile çağırınca bir tuzak daha var. Bu modda process'in exit code'u son komutun başarılı olup olmamasına göre 0 ya da 1'dir; script'in exit 3 ile verdiği değer 1'e indirgenir:

$ pwsh -NoProfile -File ./lab/bye.ps1; echo "exit=$?"
loading settings
exit=3
$ pwsh -NoProfile -Command ./lab/bye.ps1; echo "exit=$?"
loading settings
exit=1
$ pwsh -NoProfile -Command './lab/bye.ps1; exit $LASTEXITCODE'; echo "exit=$?"
loading settings
exit=3
$ pwsh ./lab/bye.ps1; echo "exit=$?"
loading settings
exit=3

Kural basit: script'i başka bir programdan çağırıyorsan pwsh -NoProfile -File kullan. -Command şartsa sonuna exit $LASTEXITCODE ekle. Parametre vermeden yazılan pwsh ./lab/bye.ps1 da -File sayılır; PowerShell 6'dan beri ilk positional parametre -Command değil -File. Windows PowerShell 5.1'in powershell.exe'sinde varsayılan hâlâ -Command'dır.

Linux'ta Shebang ile Çalıştırmak

Script'in ilk satırındaki #!/usr/bin/env pwsh, Linux ve macOS'ta dosyayı bash'ten ya da cron'dan doğrudan çalıştırmayı sağlıyor. Bash Scripting serisinin ilk yazısındaki kurallar burada da geçerli: çalıştırma izni gerekir ve ilk iki byte #! olmalı.

$ ls -l scripts/Test-BookstoreHealth.ps1
-rw-r--r-- 1 user user 1262 Nov 15 05:05 scripts/Test-BookstoreHealth.ps1
$ ./scripts/Test-BookstoreHealth.ps1
bash: ./scripts/Test-BookstoreHealth.ps1: Permission denied
$ chmod +x scripts/Test-BookstoreHealth.ps1 lab/bom.ps1 lab/utf16.ps1 lab/crlf.ps1
$ ./scripts/Test-BookstoreHealth.ps1; echo "exit=$?"
OK http://127.0.0.1:5080/health
exit=0
$ head -c 24 scripts/Test-BookstoreHealth.ps1 | od -c | head -2
0000000   #   !   /   u   s   r   /   b   i   n   /   e   n   v       p
0000020   w   s   h  \n   #   R   e   q

PowerShell için # yorum işareti olduğu için aynı satır Windows'ta zararsız. Shebang'li çalıştırma, pwsh'u -File moduyla çağırmakla aynı sonucu verir; exit code da aynen geçer.

#Requires: Script'in Ön Koşullarını Yazmak

#Requires satırları script çalışmadan önce kontrol edilir. Koşul sağlanmazsa script'in tek satırı bile çalışmaz. Dokümana göre satır script'in herhangi bir yerinde olabilir ve her zaman bütün script için geçerlidir; okunurluk için en üste yaz. Üç lab script'i, her biri tek bir koşulla:

#Requires -Version 7.7
#Requires -PSEdition Desktop
#Requires -Modules @{ ModuleName = "Pester"; ModuleVersion = "6.0.0" }
PS> ./lab/req77.ps1
./lab/req77.ps1: The script 'req77.ps1' cannot be run because it contained a "#requires" statement for PowerShell 7.7. The version of PowerShell that is required by the script does not match the currently running version of PowerShell 7.6.10.
PS> ./lab/reqdesk.ps1
./lab/reqdesk.ps1: The script 'reqdesk.ps1' cannot be run because it contained a "#requires" statement for PowerShell editions 'Desktop'. The edition of PowerShell that is required by the script does not match the currently running PowerShell Core edition.
PS> ./lab/reqmod.ps1
./lab/reqmod.ps1: The script 'reqmod.ps1' cannot be run because the following modules that are specified by the "#requires" statements of the script are missing: The module 'Pester' cannot be found with ModuleVersion '6.0.0'..
PS> $PSVersionTable.PSEdition
Core

Aynı req77.ps1 PowerShell 7.7.0'da çalışıyor; -Version en düşük sürümü belirtir, üst sınır koymaz:

PS> ./lab/req77.ps1
running on 7.7.0
PS> $PSVersionTable.PSVersion.ToString()
7.7.0

-Version 7.6 en sık kullanacağın satır: script'in Windows PowerShell 5.1'de yanlışlıkla çalışıp yarı yolda garip hatalar vermesi yerine baştan net bir mesajla durmasını sağlar. -PSEdition Core bunu sürümden bağımsız söyler (5.1'in edition'ı Desktop). -Modules eksik module'ü import etmeyi dener, bulamazsa durur; Pester testlerini çalıştıran script'te görmek istediğin şey tam olarak bu. -RunAsAdministrator ise yalnız Windows'ta anlamlı, dokümana göre diğer işletim sistemlerinde yok sayılıyor; IIS (Windows'un web sunucusu) ya da Windows Service yöneten script'lerde kullanacağız.

pwsh mi powershell.exe mi?

Windows'ta iki ayrı PowerShell var. powershell.exe Windows PowerShell 5.1: .NET Framework üzerinde çalışır, Windows'un bir parçasıdır ve yeni özellik almaz. pwsh.exe ise PowerShell 7: .NET üzerinde çalışır, Windows, Linux ve macOS'ta aynıdır ve ayrı kurulur. Script yazarken aradaki farklardan en çok karşına çıkanlar aşağıda. Encoding satırındaki BOM (dosyanın başına konup encoding'i belirten byte order mark işareti), ANSI (Windows'un dile göre değişen eski karakter tablosu) ve Unicode'un karakter başına en az iki byte kullanan biçimi (UTF-16LE) Sık Düşülen Tuzaklar'da tekrar karşına çıkacak.

KonuWindows PowerShell 5.1PowerShell 7.6 / 7.7
Dosyaya yazarken varsayılan encodingCmdlet'e göre değişir (> ve Out-File UTF-16LE, Set-Content ANSI)Her yerde BOM'suz UTF-8
&& ve ||Yok, parse hatası7.0'dan beri var
Ternary a ? b : cYok7.0'dan beri var
??, ??=, ?.Yok?? 7.0, ?. 7.1'den beri
ForEach-Object -ParallelYok7.0'dan beri var
Invoke-RestMethod -SkipHttpErrorCheckYok, 4xx/5xx her zaman exception7.0'dan beri var
PlatformYalnız WindowsWindows, Linux, macOS
DestekWindows'un yaşam döngüsüne bağlı, yeni özellik yok7.6 LTS 14 Kasım 2028'e kadar

PowerShell 7 tarafının satırları lab'da çalıştı:

PS> $r = Invoke-RestMethod http://127.0.0.1:5080/health
PS> $r.status -eq 'Healthy' ? 'deploy can continue' : 'stop'
deploy can continue
PS> $version = $env:BOOKSTORE_VERSION ?? '0.0.0-local'
PS> $version
0.0.0-local
PS> $r?.status
PS> ${r}?.status
Healthy
PS> dotnet --version && 'sdk found'
10.0.404
sdk found
PS> dotnet build --nologo -v q ./NoSuch.csproj > $null || "build failed with exit code $LASTEXITCODE"
build failed with exit code 1
PS> $OutputEncoding.WebName
utf-8

$r?.status'un boş dönmesi bir hata değil: PowerShell'de ? variable adının parçası olabildiği için bu ifade $r? adında, tanımlı olmayan bir variable'ı okuyor. Null-conditional operator için variable adını süslü parantezle yazmak gerekiyor: ${r}?.status. Dokümanda da bu kural açıkça yazıyor. && ve ||, cmdlet'lerde $?'e, native komutlarda exit code'a bakıyor; yukarıdaki dotnet build satırı ikincisine örnek.

Hangisini hedeflemeli? Yeni yazdığın her script için PowerShell 7.6 LTS. Microsoft'un destek sayfasına göre 7.6, 18 Mart 2026'da .NET 10 üzerinde çıktı ve 14 Kasım 2028'e kadar destekleniyor. 10 Kasım 2026'da çıkan 7.7 ise .NET 11 üzerinde bir stable sürüm; aynı sayfaya göre bu tür sürümler bir sonraki LTS çıktıktan sonra yaklaşık altı ay daha destek alıyor. Sunucularda iki yılda bir LTS'ten LTS'e geçmek daha az iş. Script'lerin 7.7'ye özgü bir şey kullanmadığı sürece #Requires -Version 7.6 ikisinde de çalışır. Windows PowerShell 5.1'i yalnız pwsh'un kurulu olmadığı bir sunucuda çalışması gereken küçük bootstrap script'leri için hedefle; örneğin pwsh'u kuran script'in kendisi. Bu sürüm farklarına ve 5.1'den 7'ye taşımaya serinin son yazısında döneceğiz.

Verb-Noun İsimlendirme ve Comment-Based Help

PowerShell komutları Verb-Noun (fiil-isim) biçiminde adlandırılır ve fiil, onaylı listeden seçilir. Script'in adı Check-BookstoreHealth.ps1 değil Test-BookstoreHealth.ps1; çünkü Check listede yok:

PS> Get-Verb -Verb Test, Check, Verify | Format-Table -AutoSize

Verb AliasPrefix Group      Description
---- ----------- -----      -----------
Test t           Diagnostic Verifies the operation or consistency of a resource
PS> (Get-Verb).Count
100
PS> Get-Verb | Group-Object Group | Sort-Object Count -Descending | Format-Table Count, Name -AutoSize

Count Name
----- ----
   34 Common
   24 Data
   22 Lifecycle
    7 Diagnostic
    6 Communications
    6 Security
    1 Other

Listede 100 fiil var. Test, "bir şey doğru çalışıyor mu" sorusu için ayrılmış; Get okur, Set değiştirir, Invoke bir işlem başlatır, Restart ve Start ise Lifecycle grubundan. Script'lerde bu kural zorunlu değil, ama beşinci yazıda function'ları module'e taşıdığımızda onaylı olmayan fiiller import sırasında uyarı üretecek. Baştan doğru adla başlamak, okuyanın komutun ne yaptığını adından tahmin etmesini sağlıyor. İsim tarafında tekil ve kendi önekinle yaz: BookstoreHealth, BookstoreBackup.

Comment-based help'in karşılığı da burada: script'in kullanımını kod okumadan öğrenmek.

PS> Get-Help ./scripts/Test-BookstoreHealth.ps1 -Parameter TimeoutSec

-TimeoutSec <Int32>
    How long to wait for the API, in seconds.

    Required?                    false
    Position?                    2
    Default value                5
    Accept pipeline input?       false
    Aliases
    Accept wildcard characters?  false

PS> Get-Help ./scripts/Test-BookstoreHealth.ps1 -Examples

NAME
    /home/user/bookstore/scripts/Test-BookstoreHealth.ps1

SYNOPSIS
    Checks the health endpoint of the Bookstore API.

    -------------------------- EXAMPLE 1 --------------------------

    PS > ./Test-BookstoreHealth.ps1

    Checks the local API at http://127.0.0.1:5080.

    -------------------------- EXAMPLE 2 --------------------------

    PS > ./Test-BookstoreHealth.ps1 -BaseUrl https://staging.bookstore.test -TimeoutSec 10

    Checks the staging API and waits up to 10 seconds.

Get-Help ./scripts/Test-BookstoreHealth.ps1 -Full bütün bölümleri, ./scripts/Test-BookstoreHealth.ps1 -? ise kısa özeti gösterir. Parametre açıklamaları .PARAMETER anahtar kelimelerinden, varsayılan değerler doğrudan param() bloğundan geliyor. Position? 2 satırına dikkat: script parametreleri varsayılan olarak positional, yani ./Test-BookstoreHealth.ps1 http://127.0.0.1:5080 10 de çalışır. Buna güvenmenin neden kötü bir fikir olduğunu beşinci yazıda göreceğiz.

Üretimde Dikkat

Script'i başka bir programdan çağırırken her zaman pwsh -NoProfile -NonInteractive -File kullan. -NoProfile, sunucudaki birinin profile'ına eklediği bir satırın script'ini etkilemesini engeller; Terminal ve Command Line serisinin dördüncü yazısında bunun yan etkisini gördük. -NonInteractive ise dokümana göre Read-Host ya da onay sorusu gibi kullanıcı bekleyen bir yere gelindiğinde script'i sonsuza kadar bekletmek yerine hata verdiriyor; gece çalışan bir scheduled task için tam istediğin davranış.

Ağa çıkan her çağrıya süre sınırı koy. Invoke-RestMethod'un -TimeoutSec parametresi PowerShell 7.4'ten beri -ConnectionTimeoutSeconds'ın alias'ı ve varsayılan değeri 0, yani süresiz. Yukarıdaki lab'da cevap vermeyen servise 2 saniyelik sınırla bağlanan script 2 saniyede bitti; sınır olmasaydı deploy pipeline'ı da onunla birlikte beklerdi. Uzun indirmelerde ayrıca -OperationTimeoutSeconds var; o, akıştaki iki veri parçası arasındaki bekleme süresini sınırlıyor.

Exit code script'in sözleşmesi. Belgelediğin kodları kullan (burada 0 sağlıklı, 1 sağlıksız), her yolun bilinçli bir exit ile bittiğinden emin ol ve script'i -File ile çağıran tarafın bu kodu gerçekten kontrol ettiğini doğrula. Klasör değiştiren script'lerde konumu Push-Location/Pop-Location ile geri al, dosya yollarını da çalışma dizinine göre değil script'in kendi klasörüne göre kur; $PSScriptRoot ile bunu altıncı yazıda ayrıntılı yapacağız.

Sık Düşülen Tuzaklar

Encoding ve satır sonu. Windows PowerShell 5.1'de > ya da Out-File ile üretilen bir script UTF-16LE, bazı editörlerin kaydettiği dosyalar BOM'lu UTF-8 olur. Windows'ta CRLF (carriage return + line feed, Windows'un satır sonu) ise neredeyse varsayılan. pwsh üçünü de okur, ama shebang ile çalıştırınca Linux kernel'i ilk iki byte'a bakar ve üçü de kırılır:

$ file lab/bom.ps1 lab/utf16.ps1 lab/crlf.ps1
lab/bom.ps1:   a pwsh script, Unicode text, UTF-8 (with BOM) text executable
lab/utf16.ps1: a pwsh script, Unicode text, UTF-16, little-endian text executable
lab/crlf.ps1:  a pwsh script, ASCII text executable, with CRLF line terminators
$ ./lab/bom.ps1; echo "exit=$?"
./lab/bom.ps1: line 1: #!/usr/bin/env: No such file or directory
./lab/bom.ps1: line 4: syntax error near unexpected token `newline'
./lab/bom.ps1: line 4: `<#'
exit=2
$ ./lab/utf16.ps1; echo "exit=$?"
bash: ./lab/utf16.ps1: cannot execute binary file: Exec format error
exit=126
$ ./lab/crlf.ps1; echo "exit=$?"
/usr/bin/env: ‘pwsh\r’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
exit=127
$ pwsh -NoProfile -File lab/utf16.ps1; echo "exit=$?"
OK http://127.0.0.1:5080/health
exit=0
$ pwsh -NoProfile -File lab/crlf.ps1; echo "exit=$?"
OK http://127.0.0.1:5080/health
exit=0

BOM'lu dosyada kernel shebang'i tanımadı, bash dosyayı kendisi shell script'i sanıp yorumlamaya çalıştı. UTF-16 dosya "binary" sayıldı. CRLF'de yorumlayıcının adı pwsh\r oldu. Üçünde de pwsh -File ile çalıştırmak sorunsuz. Encoding için çözüm dosyaları BOM'suz UTF-8 kaydetmek (pwsh'un varsayılanı), satır sonu için kalıcı çözüm de repo'da bir .gitattributes:

# PowerShell files keep LF line endings so the shebang also works on Linux
*.ps1 text eol=lf
*.psm1 text eol=lf
*.psd1 text eol=lf

Terminal ve Command Line serisinin dördüncü yazısında *.ps1 için eol=crlf önermiştik; o öneri yalnız Windows'ta çalışan script'ler için geçerli. Aynı dosya Linux'ta shebang ile de çalışacaksa LF (line feed, Linux'un satır sonu) seç; pwsh iki satır sonunu da sorunsuz okuyor (yukarıdaki CRLF dosyası -File ile çalıştı). 5.1'de de kullanılacak bir script'i bir kez Windows PowerShell'de çalıştırıp kontrol et; bu yazıda 5.1 çalıştırılmadı. ASCII (İngilizce temel 128 karakter) dışı karakter içeren ve 5.1'de de çalışması gereken bir script'in ise BOM'lu UTF-8 olması gerekebilir; Microsoft'un encoding dokümanına göre 5.1, BOM'suz bir dosyayı sistemin ANSI code page'iyle okuyor. Seri boyunca script'lerde ASCII dışı karakter kullanmayarak bu sorunu baştan kapatıyoruz.

Help bloğunun yeri. Comment-based help'ten önce help'e ait olmayan bir yorum varsa, araya en az bir boş satır gerekiyor. #Requires satırıyla <# arasındaki boş satırı silince help tamamen kayboluyor ve Get-Help yalnız otomatik üretilen syntax'ı gösteriyor:

PS> Get-Help ./lab/nohelp.ps1
nohelp.ps1 [[-BaseUrl] <string>] [[-TimeoutSec] <int>]

Execution policy'yi güvenlik sınırı sanmak. Windows'ta "script çalıştırılamıyor" hatası execution policy'den gelir; neyi koruyup neyi korumadığını Terminal ve Command Line serisinin dördüncü yazısında anlattık. Kısaca: istemeden script çalıştırmayı zorlaştıran bir emniyet ayarı, güvenlik kararı değil. pwsh -ExecutionPolicy Bypass ile her şeyi açmak yerine kendi script'lerini imzala ya da RemoteSigned ile çalış. Dokümana göre bu parametre Windows dışında yok sayılıyor.

Windows'ta yalnız .ps1. about_Pwsh'a göre PowerShell 7.2'den beri Windows'ta -File yalnız .ps1 uzantılı dosyaları kabul ediyor. Linux'ta uzantısız bookstore-health gibi bir dosya da çalışır, ama aynı script'i iki platformda kullanacaksan uzantıyı koru.

Küçük Bir Deneme

★☆☆ check-health.ps1'i, API sağlıksızken 1 ile çıkacak şekilde düzelt ve pwsh -NoProfile -File ile çağırıp $LASTEXITCODE'u kontrol et. Sonra aynı script'i -Command ile çağır ve exit 1 yerine exit 4 yazdığında ne değiştiğini gözle.

★★☆ Test-BookstoreHealth.ps1'e [switch] $Quiet parametresi ekle: verilirse hiçbir şey yazmasın, yalnız exit code dönsün. Help bloğuna .PARAMETER Quiet ve üçüncü bir .EXAMPLE ekleyip Get-Help -Examples ile kontrol et. Linux'ta ./scripts/Test-BookstoreHealth.ps1 -Quiet && echo up satırı bash'te ne yazar?

★★★ Script'in başına Set-Location (Split-Path $PSCommandPath) ekleyip & ile çağır ve çağırdıktan sonra hangi klasörde olduğuna bak. Aynı işi konumu bozmadan yapan bir Push-Location/try/finally/Pop-Location sürümü yaz ve script'in ortasında exit 1 olsa bile konumun geri geldiğini doğrula.

Script'in iskeleti hazır, ama içinde kullandığımız variable'lara, string içindeki $() ifadelerine ve tiplere henüz bakmadık. Bir sonraki yazıda variable'lar, tipler ve koleksiyonlarla devam ediyoruz: [type] cast, array, hashtable, PSCustomObject, splatting ve string'ler. Bookstore'un kitap listesini tutan CSV (virgülle ayrılmış değerler) dosyasından yayınevine göre bir rapor çıkarırken PowerShell'in Türkçe culture'da sayıları nasıl okuduğunu da göreceğiz.