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

Modern CLI Araçları: fzf, ripgrep, fd, bat, eza, zoxide ve jq ile Günlük İş Akışı

Mert Özen 14 Kas 2026 12 dk 1 görüntülenme
Modern CLI Araçları: fzf, ripgrep, fd, bat, eza, zoxide ve jq ile Günlük İş Akışı

ripgrep ve fd .gitignore'a uyduğu için Bookstore repo'sunda grep'ten daha az ama doğru sonucu yaklaşık 90 kat hızlı buluyor; fzf ise her listenin önüne konabilen bir seçici. Araçları brew, winget ve apt ile kurmayı (Ubuntu'daki fdfind/batcat adları ve eski fzf), Ctrl-R/Ctrl-T entegrasyonunu, branch ve test seçicilerini, jq ile API, dotnet package list ve npm ls çıktılarını işlemeyi ve bu araçlara script'te neden bağımlı olmaman gerektiğini anlatıyoruz.

Sürüm notu: Bu yazıdaki komutlar Ubuntu 24.04 üzerinde zsh 5.9, bash 5.2 ve PowerShell 7.6.10 ile; fzf 0.75.0, ripgrep 15.2.0, fd 10.5.2, bat 0.26.1, eza 0.23.5, zoxide 0.10.0, jq 1.8.2, PSFzf 2.7.12, .NET SDK 10.0.404 ve Angular CLI 22.2.7 kurulu temiz bir ortamda denenmiştir. Karşılaştırma için Ubuntu 24.04'ün apt paketleri de kuruldu. Windows (winget) ve macOS (Homebrew) kurulum komutları araçların resmi dokümanlarından aktarıldı.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) repo'sunda isbn geçen yerleri arıyorsun. Repo'da bir kez build alınmış .NET projeleri (bin, obj) ve 309 MB'lık node_modules'lu bir Angular projesi var; diskte 18.541 dosya duruyor, git'in takip ettiği dosya sayısı ise 37. İki komut:

$ grep -rni "isbn" . | wc -l
$ rg -ni isbn | wc -l

Hangisi daha çok satır bulur, hangisi daha hızlı biter ve aradaki fark kaç kat? Bir de şunu düşün: daha çok satır bulan, daha doğru sonuç mu veriyor?

Neyin Yerine, Neden: ripgrep, fd, bat, eza, zoxide

Bu araçların hiçbiri klasik Unix araçlarını öğrenmenin yerine geçmez; geliştirici makinesinde aynı işi daha az yazarak ve daha doğru varsayılanlarla yaparlar. Ortak noktaları .gitignore'a saygı göstermeleri ve renkli, okunur çıktı vermeleri. Tahminin cevabı:

$ time (grep -rni "isbn" . | wc -l)
grep: ./.git/index: binary file matches
grep: ./src/Bookstore.Api/bin/Debug/net10.0/Bookstore.Api.pdb: binary file matches
grep: ./src/Bookstore.Api/bin/Debug/net10.0/Bookstore.Api.dll: binary file matches
...
16

real	0m0.437s
user	0m0.388s
sys	0m0.049s
$ time (rg -ni isbn | wc -l)
13

real	0m0.005s
user	0m0.004s
sys	0m0.001s

grep 15 binary dosya için uyarı bastı ve 16 satır buldu; bunların bir kısmı node_modules içindeki üç dosyadan geliyor. ripgrep 13 satır buldu, hepsi bizim yazdığımız 7 dosyada. Fark, ripgrep'in .gitignore'daki bin/, obj/ ve node_modules/ kurallarına uyması, gizli klasörleri (.git) ve binary dosyaları varsayılan olarak atlaması. Sonuçta daha az ama doğru satır, yaklaşık 90 kat daha kısa sürede. Ignore edilen yerlere de bakmak istediğinde rg -u ignore kurallarını, rg -uu ek olarak gizli dosyaları, rg -uuu binary dosyaları da açar.

fd aynı fikri find'a uygular:

$ fd -e cs
src/Bookstore.Api/Catalog/Book.cs
src/Bookstore.Api/Catalog/BookCatalog.cs
src/Bookstore.Api/Catalog/Isbn.cs
src/Bookstore.Api/Program.cs
tests/Bookstore.Api.Tests/BookCatalogTests.cs
tests/Bookstore.Api.Tests/IsbnTests.cs
$ fd -e cs | wc -l; find . -name "*.cs" | wc -l
6
12

find'ın bulduğu fazladan 6 dosya obj klasöründe build'in ürettiği .cs dosyaları. find ile aynı sonucu almak için her ignore klasörünü -not -path "*/obj/*" ile tek tek dışlaman gerekiyor.

bat, cat'in syntax highlighting, satır numarası ve git değişiklik işaretleri ekleyen hâli. Terminale yazarken süslü, pipe'a yazarken düz cat gibi davranır:

$ bat src/Bookstore.Api/Program.cs | head -3
using Bookstore.Api.Catalog;

var builder = WebApplication.CreateBuilder(args);

Belirli satırları görmek için bat -r 8:14 Program.cs, dili zorlamak için bat -l json kullanılır. eza ise ls'in yerini alır: eza -l --git her dosyanın git durumunu, eza --tree --level 2 --git-ignore ignore edilenleri gizleyerek ağacı gösterir. eza, artık bakımı yapılmayan exa'nın topluluk tarafından sürdürülen devamı.

zoxide, cd'yi hatırlayan bir klasör atlayıcı. Girdiğin klasörleri ne sıklıkla ve ne kadar yakın zamanda kullandığına göre puanlar; z ile adının bir parçasını yazman yeter:

❯ z catal
src/Bookstore.Api/Catalog git:main
❯ z app
bookstore-web/src/app git:main node 24.22.3
❯ zoxide query --list --score | head -5
   8.0 /home/ada/bookstore/src/Bookstore.Api/Catalog
   8.0 /home/ada/bookstore/web/bookstore-web/src/app
   4.0 /home/ada/bookstore/tests/Bookstore.Api.Tests

Kurulum: brew, winget, apt ve Ubuntu'nun Farklı Adları

macOS'ta tek satır: brew install fzf ripgrep fd bat eza zoxide jq. Windows'ta winget kimlikleri junegunn.fzf, BurntSushi.ripgrep.MSVC, sharkdp.fd, sharkdp.bat, eza-community.eza, ajeetdsouza.zoxide ve jqlang.jq. WSL'deki (Windows içindeki Linux ortamı) Ubuntu'da ise apt ile kurmak iki sürpriz getiriyor. Birincisi adlar: Debian ve Ubuntu'da fd'nin komutu fdfind, bat'inki batcat, çünkü bu adlar dağıtımda başka paketlerle çakışıyor. fd'nin README'si de önerdiği gibi kısa adı kendi ~/.local/bin'ine link'lersin:

$ sudo apt install fzf ripgrep fd-find bat eza zoxide jq
$ mkdir -p ~/.local/bin
$ ln -s "$(command -v fdfind)" ~/.local/bin/fd
$ ln -s "$(command -v batcat)" ~/.local/bin/bat

İkincisi sürümler. Ubuntu 24.04 LTS (uzun süreli destek sürümü) paketleri dağıtım çıktığı anda dondurulur ve yalnızca güvenlik ve kritik hata düzeltmesi alır:

AraçUbuntu 24.04 aptSon sürüm (Kasım 2026)
fzf0.44.10.75.0
ripgrep14.1.015.2.0
fd9.0.010.5.2
bat0.24.00.26.1
eza0.18.20.23.5
zoxide0.9.30.10.0
jq1.7.11.8.2

Çoğu fark küçük, ama fzf'teki fark işini doğrudan etkiliyor. fzf'in README'sindeki shell entegrasyonu satırı (source <(fzf --zsh)) yeni sürümlerde var; apt'nin 0.44.1'i bunu tanımıyor:

$ /usr/bin/fzf --zsh
unknown option: --zsh

zoxide'ın README'si de zi (fzf ile interaktif seçim) için en az fzf 0.51'i istiyor. Bu yüzden WSL'de fzf'i resmi GitHub release'inden ~/.local/bin'e kurup apt sürümünü kaldırmanı öneririm. apt sürümünde kalmak istersen Ubuntu paketi entegrasyon script'lerini /usr/share/doc/fzf/examples/key-bindings.zsh ve completion.zsh olarak getiriyor; onları source edersin.

fzf: Ctrl-R, Ctrl-T ve Alt-C

fzf bir "fuzzy finder": stdin'den gelen satır listesinden yazdığın harflere göre süzerek seçim yapmanı sağlar ve seçtiğini stdout'a basar. Tek başına küçük bir araç; asıl gücü shell entegrasyonundan ve başka komutlarla birleşmesinden geliyor. .zshrc'deki bölüm:

if (( $+commands[fzf] )); then
    export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix'
    export FZF_CTRL_T_COMMAND=$FZF_DEFAULT_COMMAND
    export FZF_DEFAULT_OPTS='--height 40% --layout reverse --border'
    cache_completion fzf fzf --zsh
fi
cache_completion zoxide zoxide init zsh

cache_completion önceki yazıda completion script'leri için yazdığımız fonksiyon; init script'leri için de aynen işe yarıyor. Bu iki satırı ekledikten sonra açılış süresi 24 ms oldu. Shell entegrasyonu üç tuş getiriyor. Ctrl-R, history aramasını fzf ile yapar; yazdıkça liste daralır:

╭──────────────────────────────────────────────────────────────────────────────╮
│ > curl                                                                       │
│   1/12 (0) +S ────────────────────────────────────────────────────────────── │
│ ▌ 13      curl -s localhost:5080/api/books | jq length                       │
╰──────────────────────────────────────────────────────────────────────────────╯

Ctrl-T, imlecin olduğu yere bir dosya yolu yapıştırır. bat yazıp Ctrl-T'ye basınca liste FZF_CTRL_T_COMMAND'dan, yani fd'den geliyor ve ignore edilen dosyalar hiç görünmüyor:

╭──────────────────────────────────────────────────────────────────────────────╮
│ > catal                                                                      │
│   4/31 (0) ───────────────────────────────────────────────────────────────── │
│ ▌ src/Bookstore.Api/Catalog/Book.cs                                          │
│ ▌ src/Bookstore.Api/Catalog/Isbn.cs                                          │
│ ▌ src/Bookstore.Api/Catalog/BookCatalog.cs                                   │
│ ▌ tests/Bookstore.Api.Tests/BookCatalogTests.cs                              │
╰──────────────────────────────────────────────────────────────────────────────╯

(İki kutuda da alttaki boş satırları kısalttım.) Enter'a basınca satır bat src/Bookstore.Api/Catalog/Book.cs oldu. Alt-C ise seçtiğin alt klasöre cd yapar. bash'te entegrasyon eval "$(fzf --bash)" ile açılır.

PowerShell'de aynı tuşları PSFzf module'ü verir. Profile'a eklediğim kısım:

if ((Get-Command fzf -ErrorAction Ignore) -and (Get-Module PSFzf -ListAvailable)) {
    Import-Module PSFzf
    Set-PsFzfOption -PSReadlineChordProvider 'Ctrl+t' -PSReadlineChordReverseHistory 'Ctrl+r'
}
if (Get-Command zoxide -ErrorAction Ignore) {
    Invoke-Expression (& { (zoxide init powershell | Out-String) })
}

Install-PSResource PSFzf -Scope CurrentUser ile kurduktan sonra Get-PSReadLineKeyHandler Ctrl+t için Fzf Provider Select, Ctrl+r için Fzf Reverse History Select gösterdi; zoxide'ın z ve zi komutları da PowerShell'de alias olarak geldi.

fzf ile Kendi Seçicilerin: Branch ve Test

fzf'in en çok işe yaradığı yer, bir listeden seçim isteyen her komutun önüne konabilmesi. Branch değiştirmek için:

# Branch'i fzf ile seç ve geç
gsw() {
    local branch
    branch=$(git branch --format='%(refname:short)' | fzf --prompt='branch> ') &&
        git switch "$branch"
}
╭──────────────────────────────────────────────────────────────────────────────╮
│ branch> isbn                                                                 │
│   1/4 ────────────────────────────────────────────────────────────────────── │
│ ▌ fix/isbn-validation                                                        │
╰──────────────────────────────────────────────────────────────────────────────╯
Switched to branch 'fix/isbn-validation'

git branch'in düz çıktısı yerine --format='%(refname:short)' kullanıyorum; düz çıktıdaki * ve baştaki boşluklar seçilen değeri bozar. fzf Escape ile kapatılırsa sıfırdan farklı bir kodla çıkar, && sayesinde git switch hiç çalışmaz.

Aynı fikir test seçmek için:

# Tek bir testi fzf ile seç ve yalnız onu çalıştır
dtp() {
    local test
    test=$(dotnet test --list-tests --no-build 2>/dev/null |
        sed -n 's/^    //p' | sed 's/(.*//' | sort -u |
        fzf --prompt='test> ') &&
        dotnet test --no-build --filter "FullyQualifiedName=$test"
}

dotnet test --list-tests testleri dört boşluk girintiyle listeler; theory'ler her veri satırı için ayrı görünür (Valid_isbn13_passes(value: "9780441172719")). sed girintiyi ve parantezli kısmı atar, sort -u tekrarları siler. Sonuç:

╭──────────────────────────────────────────────────────────────────────────────╮
│ test> wrong                                                                  │
│   2/6 ────────────────────────────────────────────────────────────────────── │
│ ▌ Bookstore.Api.Tests.IsbnTests.Wrong_length_fails                           │
│ ▌ Bookstore.Api.Tests.IsbnTests.Wrong_check_digit_fails                      │
╰──────────────────────────────────────────────────────────────────────────────╯
  Bookstore.Api.Tests test net10.0 succeeded (0.3s)
Test summary: total: 1, failed: 0, succeeded: 1, skipped: 0, duration: 0.3s

Burada = kullandım, ~ değil: ikinci yazıda gördüğümüz gibi ~ "içerir" demek ve başka testleri de yakalayabilir.

jq ile JSON İşlemek

jq, JSON için sed ve awk neyse o: bir filtre dili. Önceki yazıda PowerShell'in JSON'u kendiliğinden object'e çevirdiğini gördük; bash ve zsh'de aynı işi jq yapıyor. Bookstore API'si çalışırken:

$ curl -s localhost:5080/api/books | jq '.[0]'
{
  "isbn": "9780441172719",
  "title": "Dune",
  "author": "Frank Herbert",
  "category": "science-fiction",
  "price": 420.00,
  "stock": 12,
  "currency": "TRY"
}

. girdinin kendisi, .[0] array'in ilk elemanı, .[] bütün elemanları tek tek akıtır. Gerisi bu parçaların pipe'la birleşmesi:

$ curl -s localhost:5080/api/books | jq -r '.[] | select(.stock < 5) | "\(.isbn)\t\(.title)\t\(.stock)"'
9780141439587	Emma	0
9780141439549	Middlemarch	3
$ curl -s localhost:5080/api/books | jq 'map(.price * .stock) | add'
11575
$ curl -s localhost:5080/api/books | jq -c 'group_by(.category) | map({category: .[0].category, books: length})'
[{"category":"classics","books":3},{"category":"fantasy","books":1},{"category":"science-fiction","books":1}]
$ curl -s localhost:5080/api/books | jq -r --arg author "Jane Austen" '.[] | select(.author == $author) | .title'
Emma
Pride and Prejudice
$ curl -s localhost:5080/api/books | jq -r '.[] | [.isbn, .title, .price] | @csv'
"9780441172719","Dune",420.00
"9780141439587","Emma",185.50
"9780547928227","The Hobbit",310.00
"9780141439549","Middlemarch",265.00
"9780141439518","Pride and Prejudice",170.00

Bilmen gereken birkaç ayrıntı. select(condition) koşulu sağlamayan elemanı akıştan çıkarır. map(f) array'in her elemanına f'yi uygular, add toplar; stok değeri 11.575 TRY (Türk lirası) çıktı. -r (raw) string'leri tırnaksız basar; script'te değeri başka bir komuta vereceksen hemen hep lazım. -c çıktıyı tek satıra sıkıştırır. --arg shell değişkenini jq'ya güvenli şekilde geçirir; değeri filtre metninin içine string birleştirerek koymak, içinde tırnak olan bir değerde filtreyi bozar. jq 1.7'den beri sayıların yazıldığı biçim korunuyor, bu yüzden 420.00 çıktıda da 420.00 kaldı.

jq ile dotnet ve npm Çıktıları

jq'nun API dışında en çok işe yaradığı yer, JSON çıktı veren CLI'lar. .NET SDK'da dotnet package list (dotnet list package'ın .NET 10'daki yeni adı) --format json ile makine tarafından okunur çıktı veriyor. Repo'daki bütün projelerin doğrudan paketleri:

$ dotnet package list --format json | jq -r '.projects[] | .path as $p | .frameworks[]?.topLevelPackages[]? | "\($p | split("/") | last)\t\(.id)\t\(.resolvedVersion)"'
Bookstore.Api.Tests.csproj	coverlet.collector	6.0.4
Bookstore.Api.Tests.csproj	Microsoft.NET.Test.Sdk	17.14.1
Bookstore.Api.Tests.csproj	xunit	2.9.3
Bookstore.Api.Tests.csproj	xunit.runner.visualstudio	3.1.4

.path as $p proje yolunu bir değişkene alıyor, []? ise alan yoksa hata vermeden geçiyor: Bookstore.Api projesinin hiç paketi yok ve çıktısında topLevelPackages alanı hiç bulunmuyor. Windows'ta yollar ters bölü ile geldiği için orada split("\\") kullanırsın. Aynı yöntem --outdated ve --vulnerable ile de çalışır; CI'da "eski paket var mı" kontrolünü bununla yazabilirsin.

Angular projesinde npm'in doğrudan bağımlılıkları:

$ npm ls --json --depth=0 | jq -r '.dependencies | to_entries[] | "\(.key)@\(.value.version)"' | head -6
@angular/build@22.2.7
@angular/cli@22.2.7
@angular/common@22.2.6
@angular/compiler-cli@22.2.6
@angular/compiler@22.2.6
@angular/core@22.2.6

to_entries bir object'i {key, value} çiftlerinden oluşan bir array'e çevirir; key'leri bilmediğin object'lerde gezinmenin standart yolu bu.

Sunucuda Bu Araçlar Yokken

SSH (uzak makineye şifreli bağlantı) ile bağlandığın bir sunucuda, bir container'ın içinde ya da müşterinin makinesinde bu araçların hiçbiri olmayabilir. Klasik karşılıklarını bilmek bu yüzden şart:

ModernKlasik karşılığı
rg -n isbngrep -rn --exclude-dir={.git,bin,obj,node_modules} -I isbn .
fd -e csfind . -name '*.cs' -not -path '*/obj/*' -not -path '*/bin/*'
bat fileless -N file
eza --treefind . -maxdepth 2 -not -path '*/.*'
z catalcd ve cd -, pushd/popd
jqGenelde yok; python3 -m json.tool en azından okunur hâle getirir

grep'teki -I binary dosyaları atlar ve yukarıdaki 15 uyarıyı ortadan kaldırır. Bu klasik araçların sunucuda nasıl kullanıldığını, pipe ve redirection'ın temellerini Linux ve Shell serisinin log okuma yazısında anlatıyoruz.

Üretimde Dikkat

Bu araçlar geliştirici makinesi için. Repo'ya koyduğun bir script onlara bağımlıysa CI'da ilk satırda kırılır. .NET SDK'nın resmi container image'ında denedim:

$ docker run --rm mcr.microsoft.com/dotnet/sdk:10.0 bash -c 'rg -l TODO src; echo "exit code: $?"'
bash: line 1: rg: command not found
exit code: 127

Aynı image'da grep ve find var, jq, fzf, fd ve bat yok. Kuralım şu: repo'daki script'ler POSIX (Unix shell'leri için ortak standart) araçlarını kullanır; jq gerçekten gerekiyorsa script'in başında varlığı kontrol edilir ve CI adımında açıkça kurulur. fzf hiçbir zaman script'e girmez, çünkü bir insanın seçim yapmasını bekler ve CI'da sonsuza kadar bekleyebilir. Bash'te script yazmanın ayrıntıları ve jq'nun script içindeki kullanımı Bash Scripting serisinde.

İkinci nokta ripgrep'in ignore kuralları: "kodda bu string geçiyor mu" sorusuna hızlı ve doğru cevap verir, ama "bu secret repo'nun herhangi bir yerinde, build çıktısı dahil, kaldı mı" sorusu için yanlış araçtır. Güvenlik taramasında rg -uuu kullan ya da bu iş için yapılmış bir secret scanner'a bırak.

Sık Düşülen Tuzaklar

Birincisi, cat'i bat'e alias'lamak. bat pipe'a yazarken kendiliğinden sadeleşir, ama renk ve süslemeyi zorlarsan pipe'tan geçen metin değişir:

$ alias cat="bat --color=always --decorations=always"
$ cat src/Bookstore.Api/Program.cs | grep -c "MapGet(\"/api/books"
0
$ command cat src/Bookstore.Api/Program.cs | grep -c "MapGet(\"/api/books"
2

Renk kodları satırın içine girdiği için grep aradığı metni bulamadı; 428 byte'lık dosya pipe'tan 3.949 byte olarak geçti. Alias istiyorsan seçenek zorlamayan bir ad ver (alias b='bat') ve cat'i olduğu gibi bırak.

İkincisi, apt'nin eski fzf'iyle fzf --zsh çağırmak. Hata stderr'e gider; önceki yazıdaki cache_completion stderr'i attığı için cache dosyası boş kalır ve Ctrl-R sessizce eski hâliyle çalışır. Kendi denememde login olmayan bir shell'de PATH (komutların arandığı klasör listesi) /usr/bin/fzf'i buldu ve tam olarak bu oldu; which fzf ve fzf --version ile hangi kopyanın çalıştığına bak.

Üçüncüsü, ripgrep'in bir dosyayı bulamamasına şaşırmak: dosya .gitignore'da, gizli bir klasörde ya da binary ise varsayılan olarak aranmaz. rg --debug neden atlandığını söyler. Dördüncüsü, Ubuntu'da fd ve bat komutlarının bulunamaması; adlar fdfind ve batcat.

Küçük Bir Deneme

★☆☆ Araçları kendi işletim sistemine kur ve Bookstore repo'sunda grep -rn ile rg -n'i, find ile fd'yi aynı aramada karşılaştır. Sonuç sayıları neden farklı, açıkla.

★★☆ API çalışırken, stoğu 10'dan az olan kitapların toplam stok değerini kategori başına veren bir jq filtresi yaz (group_by, map, add). Aynı soruyu önceki yazıdaki PowerShell pipeline'ıyla cevapla ve iki sonucu karşılaştır.

★★★ dtp'yi, birden fazla test seçmeye izin verecek şekilde genişlet (fzf --multi) ve seçilen testleri | ile birleştirip tek bir --filter ifadesine çevir. PowerShell'de aynı işi Invoke-Fzf ile yapan bir function yaz.

Araç kutusu tamamlandı. Serinin son yazısında hepsini tek bir pencerede düzenliyoruz: tmux ile pane'ler, session'lar, dotnet watch, ng serve ve log'u yan yana çalıştırmak ve SSH bağlantısı kopsa da süren işler.