Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Terminal ve Command Line — #04

PowerShell 7 ile Geliştirici Terminali: Object Pipeline, $PROFILE, PSReadLine ve Bash Kullanıcısı için Karşılıklar

Mert Özen 14 Kas 2026 13 dk 1 görüntülenme
PowerShell 7 ile Geliştirici Terminali: Object Pipeline, $PROFILE, PSReadLine ve Bash Kullanıcısı için Karşılıklar

PowerShell'de pipe'tan metin değil .NET object'leri geçer; bu yüzden Bookstore API'sinin JSON'u Invoke-RestMethod ile doğrudan filtrelenip gruplanabilir (fiyatlar double gelir, para için decimal'e çevir). $PROFILE ve module'ler, execution policy'nin gerçekte neyi koruduğu, winget, bash karşılık tablosu, curl.exe, 7.6 LTS'in getirdikleri ve 7.4'ün 10 Kasım 2026'da biten desteği, encoding ve CRLF tuzaklarını gerçek çıktılarla anlatıyoruz.

Sürüm notu: Bu yazıdaki komutlar PowerShell 7.6.10 (PSReadLine 2.4.5, PSResourceGet 1.2.1) ile Ubuntu 24.04 üzerinde; .NET 10 SDK, posh-git 1.1.0 ve Starship 1.27.0 kurulu temiz bir ortamda denenmiştir. Cmdlet'ler ve çıktıları Windows'ta aynıdır, yalnız yollar C:\ ile başlar. Execution policy, winget ve Windows'a özgü alias davranışı Microsoft Learn dokümanlarından aktarıldı; bu kısımlar Windows'ta çalıştırılmadı.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'si /api/books'tan beş kitabı JSON olarak döndürüyor. Fiyatlar C# tarafında decimal. PowerShell'de şunu çalıştırıyorsun:

PS> $books = Invoke-RestMethod http://localhost:5080/api/books
PS> $books.Count
PS> $books[0].price.GetType().FullName

İlk satır ne döndürür: bir JSON string'i mi, beş elemanlı bir collection mı? İkinci satır System.Decimal mı der, System.Double mı, System.String mı? İkinci sorunun cevabı, PowerShell'de para hesabı yaparken bilmen gereken bir ayrıntı.

Text Pipeline ve Object Pipeline

bash'te pipe'tan geçen şey metindir. ps aux | grep dotnet | awk '{print $2}' yazdığında awk, ps'in çıktısındaki ikinci sütunun PID (process kimlik numarası) olduğunu "bildiğini" varsayar; çıktı biçimi değişirse komut sessizce yanlış sonuç verir. Metin pipeline'ının nasıl kurulduğunu Linux ve Shell serisinde anlatıyoruz.

PowerShell'de pipe'tan geçen şey .NET object'leridir. Sütun numarası yerine property adı kullanırsın:

PS> Get-Process | Where-Object CPU -gt 1 | Sort-Object CPU -Descending | Select-Object -First 3 Name, Id, CPU, @{ n = "MemMB"; e = { [int]($_.WorkingSet64 / 1MB) } }
Name           Id  CPU MemMB
----           --  --- -----
VBCSCompiler 1663 3.29   215
VBCSCompiler 4615 2.79   226
pwsh         8295 2.21   156

Where-Object CPU -gt 1 sayısal karşılaştırmadır, metin eşleştirme değil. @{ n = ...; e = ... } hesaplanmış bir property tanımlar: burada byte'ı MB'a çeviriyoruz. Tablo biçimi en sonda, ekrana basılırken oluşur; ondan önce her adımda elinde gerçek object'ler var. Bir object'in hangi property'leri olduğunu bilmiyorsan Get-Member söyler. Bu, PowerShell'deki en önemli keşif aracı.

Bookstore API'sinden JSON'u Object Olarak İşlemek

Tahminin cevabı burada. Invoke-RestMethod cevabın Content-Type'ına bakar ve JSON'u kendisi object'e çevirir:

PS> $books = Invoke-RestMethod http://localhost:5080/api/books
PS> $books.GetType().Name; $books.Count
Object[]
5
PS> $books[0] | Get-Member -MemberType NoteProperty
   TypeName: System.Management.Automation.PSCustomObject
Name     MemberType   Definition
----     ----------   ----------
author   NoteProperty string author=Frank Herbert
category NoteProperty string category=science-fiction
currency NoteProperty string currency=TRY
isbn     NoteProperty string isbn=9780441172719
price    NoteProperty double price=420
stock    NoteProperty long stock=12
title    NoteProperty string title=Dune

Beş elemanlı bir array ve her eleman bir PSCustomObject. price ise double: JSON'da sayı tipi tek, decimal ile double ayrımı yok, PowerShell de ondalıklı sayıları double'a çeviriyor. Ekranda görmek için sorun değil, ama toplam ve indirim hesabında [decimal]$_.price ile açıkça çevir. Gerisi tek satırlık sorgular:

PS> $books | Where-Object Stock -eq 0 | Select-Object Title, Author
title author
----- ------
Emma  Jane Austen
PS> $books | Sort-Object Price -Descending | Select-Object -First 3 Title, Price
title        price
-----        -----
Dune        420.00
The Hobbit  310.00
Middlemarch 265.00
PS> $books | Group-Object Category | Select-Object Name, Count
Name            Count
----            -----
classics            3
fantasy             1
science-fiction     1
PS> $books | Where-Object Stock -lt 5 | Select-Object Isbn, Title, Stock | Export-Csv low-stock.csv; Get-Content low-stock.csv
"isbn","title","stock"
"9780141439587","Emma","0"
"9780141439549","Middlemarch","3"

Property adları büyük/küçük harfe duyarlı değil: JSON'da stock olsa da Stock yazabilirsin. Tek kitabı alıp tekrar JSON'a çevirmek için ConvertTo-Json, bir dosyadaki JSON'u okumak için Get-Content x.json | ConvertFrom-Json var. Bulunamayan bir ISBN'de (kitabın uluslararası standart numarası) Invoke-RestMethod 404'ü exception'a çevirir: Response status code does not indicate success: 404 (Not Found). Aynı işleri bash'te curl ve jq ile nasıl yaptığımızı bir sonraki yazıda göreceğiz; iki yolu da bilmek, hangi shell'de olursan ol aynı soruya cevap verebilmek demek.

Bu sorgulardan biri tekrar tekrar lazım olacaksa onu bir script'e çevir. Repo'ya scripts/low-stock.ps1 ekledim:

#Requires -Version 7.6
param(
    [string]$BaseUrl = 'http://localhost:5080',
    [int]$Threshold = 5,
    [string]$OutFile
)

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

$books = Invoke-RestMethod "$BaseUrl/api/books"
$low = $books |
    Where-Object stock -lt $Threshold |
    Sort-Object stock |
    Select-Object isbn, title, stock, @{ Name = 'price'; Expression = { [decimal]$_.price } }

if ($OutFile) {
    $low | Export-Csv $OutFile
    Write-Output "$($low.Count) books written to $OutFile"
}
else {
    $low
}
PS> ./scripts/low-stock.ps1

isbn          title       stock  price
----          -----       -----  -----
9780141439587 Emma            0 185.50
9780141439549 Middlemarch     3 265.00

PS> ./scripts/low-stock.ps1 -Threshold 10 -OutFile low.csv
3 books written to low.csv

param bloğu script'e tipli parametreler verir; -Threshold yazınca tab completion da çalışır. #Requires -Version 7.6 script'in Windows PowerShell 5.1'de yanlışlıkla çalışmasını engeller.

$PROFILE ve Modüller: PSReadLine, posh-git, Terminal-Icons

PowerShell'in açılış dosyası $PROFILE. Dört profile var; senin ilgilendiğin "Current User, Current Host" olanı. Linux'ta değerleri:

PS> $PROFILE | Select-Object *
AllUsersAllHosts       : /usr/share/powershell/.store/powershell.linux.arm64/7.6.10/powershell.linu
                         x.arm64/7.6.10/tools/net10.0/any/profile.ps1
AllUsersCurrentHost    : /usr/share/powershell/.store/powershell.linux.arm64/7.6.10/powershell.linu
                         x.arm64/7.6.10/tools/net10.0/any/Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts    : /home/ada/.config/powershell/profile.ps1
CurrentUserCurrentHost : /home/ada/.config/powershell/Microsoft.PowerShell_profile.ps1
Length                 : 61

Windows'ta Microsoft'un dokümanına göre aynı dosya $HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1. Doküman bir uyarı da yapıyor: Documents klasörü OneDrive'a ya da bir ağ paylaşımına yönlendirilmişse module'ler yüklenemeyebilir ve profile hata verebilir. Dosya yoksa New-Item -ItemType File -Path $PROFILE -Force ile oluşturup code $PROFILE ile açarsın. VS Code'un PowerShell extension'ı kendi host'unu kullanır ve Microsoft.VSCode_profile.ps1'i okur; iki yerde aynı ayarı istiyorsan ortak kısmı CurrentUserAllHosts'a (profile.ps1) koy.

Benim profile'ımın bu yazıda eklenen kısmı:

# Sık kullanılan komutlar (alias argüman alamaz, function alır)
function dt { dotnet test @args }
function db { dotnet build @args }
function gs { git status -sb @args }
function dtf {
    param(
        [Parameter(Mandatory)][string]$Name,
        [Parameter(ValueFromRemainingArguments)][string[]]$Rest
    )
    dotnet test --filter "FullyQualifiedName~$Name" @Rest
}

# Module'ler ve prompt
if (Get-Module posh-git -ListAvailable) {
    Import-Module posh-git
}
if (Get-Command starship -ErrorAction Ignore) {
    Invoke-Expression (&starship init powershell)
}

PowerShell'de Set-Alias yalnızca bir komut adını başka bir ada bağlar, argüman taşıyamaz; dotnet test gibi iki kelimelik kısaltmalar function olmak zorunda. @args function'a gelen bütün argümanları olduğu gibi geçirir. dtf'de ise [Parameter(Mandatory)] kullanınca function "advanced function" olur ve @args çalışmaz; bu yüzden kalan argümanları ValueFromRemainingArguments ile topluyoruz. İlk denememde bunu bilmeden yazınca dtf IsbnTests --no-build şu hatayı verdi: A positional parameter cannot be found that accepts argument '--no-build'.

Module'leri PowerShell Gallery'den PSResourceGet ile kuruyorum; PowerShell 7.6 ile 1.2.1 sürümü geliyor:

PS> Install-PSResource posh-git -Scope CurrentUser -TrustRepository
PS> Import-Module posh-git; (TabExpansion2 "git sw" 6).CompletionMatches.CompletionText; (TabExpansion2 "git switch ma" 13).CompletionMatches.CompletionText
switch
main

Önceki yazıda PowerShell'de git completion'ı yoktu; posh-git hem alt komutları hem branch adlarını tamamlıyor ve prompt'a branch bilgisini ekliyor (~/bookstore [main]>). Ben prompt için Starship'i kullandığım için posh-git'i yalnız completion için yüklüyorum; zsh'deki aynı starship.toml PowerShell'de de aynı prompt'u veriyor. Terminal-Icons ise Get-ChildItem çıktısına dosya türü ikonları ekler ve Nerd Font ister. posh-git'in son sürümü 1.1.0, Terminal-Icons'ınki 0.11.0 ve iki repo da 2024'ten beri pek değişmemiş; çalışıyorlar ama aktif geliştirme beklemiyorum.

Execution Policy Neyi Korur, Neyi Korumaz

Windows'ta ilk kez profile ya da script çalıştıran birinin karşılaştığı ilk engel execution policy'dir. Microsoft'un dokümanı bunu net söylüyor: execution policy bir güvenlik sınırı değil, kullanıcının istemeden script çalıştırmasını zorlaştıran bir emniyet ayarı. Script'in içeriğini kopyalayıp komut satırına yapıştıran biri onu zaten atlatır. Dokümana göre PowerShell 7'nin Windows'taki varsayılanı RemoteSigned: yerel script'ler çalışır, internetten indirilmiş ve imzasız olanlar çalışmaz. Hiçbir scope'ta değer tanımlı değilse Windows client'ta etkin policy Restricted olur ve profile bile yüklenmez. Kendi makinende bakmak için:

PS> Get-ExecutionPolicy -List
PS> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

"İnternetten geldi" bilgisi, tarayıcının dosyaya eklediği Mark of the Web işaretinden gelir; Unblock-File bu işareti kaldırır. Dokümana göre curl.exe, Invoke-WebRequest ve Invoke-RestMethod ile indirilen dosyalar bu işareti almayabilir, yani RemoteSigned onları yerel dosya sayar. Linux ve macOS'ta execution policy uygulanmaz; aynı komut orada her scope için Unrestricted döndürdü. Kısacası execution policy'yi "hiçbir script çalışmıyor" sorununu çözmek için bil, güvenlik kararını ona bırakma.

winget ile Araç Kurmak

Windows'ta geliştirici araçlarını installer indirip next-next ile kurmak yerine winget kullan. Bu seride geçen araçların winget-pkgs deposundaki kimlikleri:

PS> winget install --id Microsoft.PowerShell --source winget
PS> winget install --id Microsoft.WindowsTerminal --source winget
PS> winget install --id Git.Git --source winget
PS> winget install --id Microsoft.DotNet.SDK.10 --source winget
PS> winget install --id OpenJS.NodeJS.LTS --source winget
PS> winget install --id Starship.Starship --source winget
PS> winget upgrade --all

--id ile tam kimlik vermek, benzer adlı başka bir paketin seçilmesini önler. Kurulumdan sonra PATH (komutların arandığı klasör listesi) yeni açılan terminallerde güncellenir; aynı pencerede dotnet bulunamıyorsa yeni bir sekme aç. Yeni bir makineyi baştan kurmayı bir script'e çevirmek Productivity ve Automation serisinin konusu.

Bash ↔ PowerShell Karşılıkları ve curl.exe

bash'ten gelen biri için en sık ihtiyaç duyulan karşılıklar:

bashPowerShell 7Not
ls -laGet-ChildItem -Force (gci)Object döner: Length, LastWriteTime
cat fileGet-Content file (gc)-Tail 20 -Wait = tail -f
grep -rn Austen srcGet-ChildItem src -Recurse -Filter *.cs | Select-String AustenSonuç object, .LineNumber var
export X=1, echo $X$env:X = '1', $env:XYalnız o process ve child'ları için
which dotnetGet-Command dotnet-All ile bütün kopyalar
cmd > /dev/nullcmd > $null2>&1 aynı
a && b, a || baynıPowerShell 7.0'dan beri
source ./env.sh. ./env.ps1Dot-sourcing, aynı scope'ta çalıştırır
curl -s url | jqInvoke-RestMethod url (irm)JSON kendiliğinden object olur

Alias'larda platform farkı var. Windows'ta ls, cat, cp, rm PowerShell cmdlet'lerinin alias'larıdır, bu yüzden ls -la Windows'ta hata verir (Get-ChildItem'ın -la diye bir parametresi yok). Linux ve macOS'ta ise bu alias'lar tanımlı değil, gerçek ls çalışır:

PS> Get-Alias curl, wget, ls, cat, cd, cp
Get-Alias: This command cannot find a matching alias because an alias with the name 'curl' does not exist.
Get-Alias: This command cannot find a matching alias because an alias with the name 'wget' does not exist.
Get-Alias: This command cannot find a matching alias because an alias with the name 'ls' does not exist.
Get-Alias: This command cannot find a matching alias because an alias with the name 'cat' does not exist.
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           cd -> Set-Location
Get-Alias: This command cannot find a matching alias because an alias with the name 'cp' does not exist.

curl ve wget özel bir durum: Windows PowerShell 5.1'de ikisi de Invoke-WebRequest'in alias'ıydı ve curl -H "..." url gibi gerçek curl komutları anlaşılmaz hatalar veriyordu. PowerShell 7'de bu iki alias tanımlı değil; Windows'ta da curl yazınca gerçek curl çalışır. Windows 10 ve 11 gerçek curl'ü curl.exe olarak getiriyor; script'te hangisinin çalışacağından emin olmak için curl.exe diye uzantısıyla yaz. Networking ve HTTP serisindeki curl -w örnekleri PowerShell 7'de curl.exe ile aynen çalışır.

PowerShell 7.6 LTS: Ne Geldi, 7.4 Ne Zaman Bitti

PowerShell 7.6, 18 Mart 2026'da .NET 10 üzerinde LTS (Long Term Support; uzun süreli destek) olarak çıktı ve Microsoft'un destek sayfasına göre 14 Kasım 2028'e kadar destekleniyor. Aynı sayfa 7.4 LTS ile 7.5'in desteğinin 10 Kasım 2026'da bittiğini söylüyor; bu yazının yazıldığı gün itibarıyla ikisi de güvenlik güncellemesi almıyor. Hâlâ 7.4'te olan makineleri 7.6'ya taşımanın zamanı geldi. Duyurudaki değişikliklerden günlük kullanımda fark edeceklerin:

  • Tab completion iyileşmeleri: provider'lar arasında daha iyi yol tamamlama, bazı cmdlet parametreleri için değer tamamlama, module'ü kısa adıyla tamamlama.
  • Önceden experimental olan özelliklerin kalıcı hâle gelmesi: aralarında komut bulunamayınca öneri sunan feedback provider ve çıktıyı bir değişkene yönlendirme var.
  • Get-Clipboard -Delimiter, Get-Command -ExcludeModule ve stderr çıktısında NO_COLOR ortam değişkenine uyulması.
  • Join-Path -ChildPath'in artık birden fazla değer alması gibi birkaç küçük breaking change.

Değişkene yönlendirme 7.6'da artık ek ayar gerektirmeden çalışıyor:

PS> dotnet --version > variable:sdk; "SDK: $sdk"
SDK: 10.0.404

Hangi özelliklerin hâlâ experimental olduğunu Get-ExperimentalFeature listeler; bu sürümde üç tane kaldı ve hiçbiri açık değil.

Üretimde Dikkat

İlk yazıda gördük: pwsh -c ve pwsh -File, -NoProfile verilmezse profile'ı yükler. Bunun gerçek bir yan etkisini kendi profile'ımda yaşadım. Prediction görünümünü ListView yapan satır, terminale bağlı olmayan bir pwsh -c çağrısında her seferinde şu uyarıyı bastı:

WARNING: The prediction 'ListView' is temporarily disabled because the current window size of the console is too small. To use the 'ListView', please make sure the 'WindowWidth' is not less than '50' and the 'WindowHeight' is not less than '5'.

Build script'inin log'una bunun düştüğünü düşün. İki çözümü birlikte uyguluyorum: script ve build adımlarını her zaman pwsh -NoProfile -File ile çağırmak, ve profile'da yalnızca interactive anlamı olan ayarları korumak:

Set-PSReadLineOption -PredictionSource History
if (-not [Console]::IsOutputRedirected) {
    Set-PSReadLineOption -PredictionViewStyle ListView
}

İkinci konu hata yönetimi. $ErrorActionPreference = 'Stop' cmdlet hatalarında script'i durdurur ama git, dotnet, npm gibi dış programların sıfırdan farklı exit code'larını varsayılan olarak görmez. PowerShell 7.4'ten beri kalıcı olan $PSNativeCommandUseErrorActionPreference bunu değiştirir:

$ cat check.ps1
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

git -C /home/ada/bookstore switch no-such-branch
Write-Output "this line never runs"
$ pwsh -NoProfile -File check.ps1; echo "exit code: $?"
fatal: invalid reference: no-such-branch
NativeCommandExitException: /tmp/check.ps1:4
Line |
   4 |  git -C /home/ada/bookstore switch no-such-branch
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Program "git" ended with non-zero exit code: 128 (0x80).
exit code: 1

Script git'in hatasında durdu ve sonraki satır çalışmadı. İkinci değişken olmadan aynı script hatayı ekrana basıp bir sonraki satırla devam ediyordu. CI'da çalışacak her PowerShell script'inin başına bu iki satırı yazmayı alışkanlık hâline getir.

Sık Düşülen Tuzaklar

Birincisi, Windows PowerShell 5.1 ile PowerShell 7'yi karıştırmak. powershell.exe 5.1'dir, pwsh.exe 7. Profile klasörleri ayrı (Documents\WindowsPowerShell ve Documents\PowerShell), module klasörleri ayrı, davranışlar farklı. "Profile'ım çalışmıyor" şikâyetinin çoğu yanlış PowerShell'de olmaktan gelir; $PSVersionTable.PSEdition ile kontrol et.

İkincisi encoding. PowerShell 7 dosyaya yazarken varsayılan olarak BOM'suz (dosya başındaki byte order mark işareti olmadan) UTF-8 kullanır:

PS> "Les Misérables" > title.txt; Format-Hex title.txt
   Label: /home/ada/bookstore/title.txt
          Offset Bytes                                           Ascii
                 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
          ------ ----------------------------------------------- -----
0000000000000000 4C 65 73 20 4D 69 73 C3 A9 72 61 62 6C 65 73 0A Les Misérables�
PS> "Les Misérables" | Out-File title-bom.txt -Encoding utf8BOM; (Format-Hex title-bom.txt).Bytes[0..3] -join " "
239 187 191 76

é iki byte (C3 A9) olarak yazıldı ve dosya BOM ile başlamıyor; utf8BOM istersen ilk üç byte EF BB BF olur. Microsoft'un dokümanına göre Windows PowerShell 5.1'de durum tutarsız: > ve Out-File UTF-16LE (karakter başına en az iki byte kullanan Unicode biçimi) yazar, Set-Content sistemin ANSI (Windows'un eski, dile göre değişen karakter tablosu) code page'ini kullanır. 5.1'in BOM'suz bir UTF-8 script'i ANSI sanıp Türkçe karakterleri bozması da bilinen bir sorun; 5.1'de de çalışması gereken, ASCII (İngilizce temel 128 karakter) dışı karakter içeren script'leri BOM'lu UTF-8 kaydet.

Üçüncüsü satır sonları. Windows'ta kaydedilen bir bash script'i CRLF (Windows'un satır sonu: carriage return + line feed) ile biterse Linux'ta, WSL'de (Windows içindeki Linux ortamı) ya da container'da şöyle patlar:

$ ./restore.sh
/usr/bin/env: ‘bash\r’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines
$ od -c restore.sh | head -2
0000000   #   !   /   u   s   r   /   b   i   n   /   e   n   v       b
0000020   a   s   h  \r  \n   e   c   h   o       "   r   e   s   t   o

Çözüm editörde değil repo'da olmalı: .gitattributes'a *.sh text eol=lf ve *.ps1 text eol=crlf yaz, böylece herkesin checkout'u aynı satır sonlarıyla gelir. Satır sonu ayarlarını Git & GitHub serisinin git config yazısında ayrıntılı anlattık.

Dördüncüsü, JSON'dan gelen para tutarlarını double olarak toplamak; yukarıda gördük, hesaplamadan önce [decimal]'a çevir.

Küçük Bir Deneme

★☆☆ Get-ChildItem -Recurse -File src | Sort-Object Length -Descending | Select-Object -First 5 FullName, Length komutunu Bookstore repo'sunda çalıştır. Aynı soruyu bash'te find, sort ve head ile cevapla ve iki yaklaşımı karşılaştır.

★★☆ API çalışırken kategori başına toplam stok değerini (fiyat çarpı stok) [decimal] kullanarak hesaplayan tek satırlık bir pipeline yaz. İpucu: Group-Object, sonra Measure-Object -Sum ve hesaplanmış bir property.

★★★ scripts/low-stock.ps1'e bir -Category parametresi ekle, [ValidateSet()] ile izin verilen değerleri sınırla ve tab completion'ın bu değerleri önerdiğini gör. Script'i pwsh -NoProfile -File ile çalıştırıp API kapalıyken exit code'un 0'dan farklı olduğunu doğrula.

Shell'lerin ikisi de ayarlı. Bir sonraki yazıda bunların üstüne günlük işi hızlandıran modern CLI araçlarını ekliyoruz: ripgrep, fd, bat, eza, zoxide, fzf ve Bookstore API'sinin JSON'unu bash'te işlemek için jq.