zsh'ı Yapılandırmak: .zshrc, Alias, Function, Plugin'ler, Prompt ve Başlangıç Hızı
İyi bir .zshrc sabit bir sırayla yazılır: history, completion, alias ve function, plugin'ler ve en sonda prompt; yavaşlığı çoğu zaman framework değil her açılışta çalışan dış komutlar getirir. Alias, function ve script'in ne zaman seçileceğini, zsh-autosuggestions ve zsh-syntax-highlighting'i elle kurmayı, git branch'ini ve .NET sürümünü gösteren Starship prompt'unu ve açılışı 206 ms'den 21 ms'ye indiren completion cache'ini gerçek ölçümlerle anlatıyoruz.
Sürüm notu: Bu yazıdaki ayarlar Ubuntu 24.04 üzerinde zsh 5.9 ve bash 5.2 ile; Starship 1.27.0, zsh-autosuggestions 0.7.1, zsh-syntax-highlighting (Ekim 2026 commit'i), Oh My Zsh (Kasım 2026 commit'i), .NET SDK 10.0.404 ve Angular CLI 22.2.7 kurulu temiz bir ortamda denenmiştir. Süre ölçümleri bu ortamda alındı; mutlak değerler makinene göre değişir, aradaki oranlar önemli.
Önce Tahmin Et
İki .zshrc var. Birincisi Oh My Zsh'nin hazır şablonu, beş plugin'le: git docker dotnet npm sudo. İkincisi benim elle yazdığım, framework kullanmayan bir dosya: history ayarları, compinit, iki plugin, Starship prompt'u ve önceki yazıdaki iki satır:
source <(dotnet completions script bash 2>/dev/null)
source <(SHELL=${commands[zsh]} ng completion script)
Hangisi daha yavaş açılır, ve aradaki fark kaç kat? "Oh My Zsh ağırdır" cümlesini çok duymuşsundur; ölçüm sonucu bu yazının ortasında ve o cümleyi pek desteklemiyor.
.zshrc'nin Düzeni: Bölüm Bölüm
İlk yazıdaki dosya tablosuna göre zsh'de iki dosya iş görür: .zprofile login'de bir kez okunur, .zshrc her interactive shell'de. PATH (komutların arandığı klasör listesi) ve ortam değişkenleri ilkine, geri kalan her şey ikincisine gider. Bookstore (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) üzerinde çalışırken kullandığım .zprofile:
# Login shell: PATH ve ortam değişkenleri
typeset -U path
path=($HOME/.dotnet/tools $HOME/.local/bin $path)
export EDITOR=code
export DOTNET_CLI_TELEMETRY_OPTOUT=1
typeset -U path zsh'ye path array'inde tekrar eden elemanları atmasını söyler; dosya ikinci kez okunsa da PATH şişmez. zsh'de path array'i ile PATH string'i birbirine bağlıdır, birini değiştirince öbürü de değişir.
.zshrc'yi sabit bir sırayla bölümlere ayırıyorum, çünkü sıra gerçekten önemli: completion fonksiyonları compinit'ten sonra tanımlanmalı, syntax highlighting plugin'i en son yüklenmeli, prompt ise diğer her şey hazır olduktan sonra kurulmalı. İskelet şöyle:
# 1. Seçenekler
setopt AUTO_CD INTERACTIVE_COMMENTS
# 2. History (önceki yazı)
# 3. Satır düzenleme: bindkey -e
# 4. Completion: compinit, zstyle, araçların completion'ları
# 5. Alias ve function'lar
# 6. Plugin'ler (syntax highlighting en sonda)
# 7. Prompt
AUTO_CD bir klasör adını tek başına yazınca oraya cd yapar (src yazıp Enter). INTERACTIVE_COMMENTS, satıra yapıştırdığın komuttaki # ile başlayan açıklamaların hata vermesini engeller; zsh'de interactive modda bu varsayılan olarak kapalıdır. History ve completion bölümleri önceki yazıdaki ayarların aynısı. Dosyanın tamamı bu yazının repo snapshot'ında shell/zsh/.zshrc olarak duruyor.
Alias mı, Function mı, Script mi?
En çok yazdığım komutlar için kısa adlar:
alias dt='dotnet test'
alias db='dotnet build'
alias dw='dotnet watch --project src/Bookstore.Api'
alias gs='git status -sb'
alias ngs='ng serve'
dtf() {
dotnet test --filter "FullyQualifiedName~${1:?usage: dtf <TestClassOrMethod>}" "${@:2}"
}
mkcd() {
mkdir -p -- "$1" && cd -- "$1"
}
Alias düz metin değiştirmedir: shell satırın ilk kelimesi dt ise onu dotnet test ile değiştirir, gerisini olduğu gibi bırakır. Argümanı ortaya koymak, koşul kurmak ya da birden fazla komut çalıştırmak gerekiyorsa function yaz. dtf ilk argümanı filtreye koyar, gerisini "${@:2}" ile dotnet test'e geçirir; ${1:?...} argüman verilmezse mesaj basıp durur. mkcd ise alias ile yapılamaz, çünkü çalıştığı shell'in klasörünü değiştirmesi gerekiyor. Ayrı bir script de yapamazdı: script yeni bir process'te çalışır ve onun cd'si senin shell'ini etkilemez.
Shell'in bir adı nasıl yorumladığını type söyler:
❯ type dt dtf gs
dt is an alias for dotnet test
dtf is a shell function from /home/ada/.zshrc
gs is an alias for git status -sb
❯ dtf IsbnTests --no-build | tail -1
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 6 ms - Bookstore.Api.Tests.dll (net10.0)
❯ dtf
dtf:1: 1: usage: dtf <TestClassOrMethod>
Alias ve function'lar yalnızca o interactive shell'de vardır. Aynı dt'yi bir script'in içinde kullanınca:
❯ cat /tmp/run-tests.sh
#!/usr/bin/env zsh
dt --no-build
❯ /tmp/run-tests.sh
/tmp/run-tests.sh:2: command not found: dt
Script .zshrc'yi okumaz, bu yüzden alias'ı bilmez. Benim kuralım şu: yalnızca benim parmaklarım için olan kısaltmalar alias, argüman işleyen veya klasör değiştiren kısa işler function, başka bir araçtan (CI, git hook, VS Code task) da çağrılacak ya da 15-20 satırı geçen işler ise PATH'teki bir klasörde (~/.local/bin) duran ayrı bir script.
Plugin Yaklaşımları: Oh My Zsh, Plugin Manager ya da Elle
zsh'yi zenginleştirmenin üç yolu var. Oh My Zsh bir framework: kurulum script'i ~/.oh-my-zsh klasörünü ve hazır bir .zshrc şablonu getirir, sen plugins=(...) listesine ad yazarsın. İndirdiğim sürümde 372 plugin klasörü vardı. Antidote, zinit ya da zim gibi plugin manager'lar plugin'leri GitHub'dan çekip yükler ve sırasını, güncellemesini yönetir. Üçüncü yol elle: plugin'i git clone ile bir klasöre indirip .zshrc'den source etmek.
| Yol | Artısı | Eksisi |
|---|---|---|
| Oh My Zsh | Hazır alias'lar ve plugin'ler, kolay başlangıç | Senin yazmadığın yüzlerce alias, framework'ün kendi güncelleme döngüsü |
| Plugin manager | Sürüm sabitleme, otomatik güncelleme | Bir araç daha ve onun syntax'ı |
| Elle | Ne yüklendiğini tam bilirsin, ek bağımlılık yok | Güncellemeyi sen yaparsın (git pull) |
Ben elle gidiyorum, çünkü gerçekten kullandığım iki plugin var. zsh-autosuggestions, history'den bulduğu öneriyi imlecin sağında soluk renkle gösterir; sağ ok kabul eder. PowerShell'deki InlineView'ın zsh karşılığı. zsh-syntax-highlighting yazdığın komutu Enter'a basmadan renklendirir: var olmayan bir komut kırmızı, doğru yazılmış komut yeşil görünür. Kurulum:
$ mkdir -p ~/.local/share/zsh/plugins && cd ~/.local/share/zsh/plugins
$ git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions
$ git clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting
ve .zshrc'de:
ZPLUGINS=${XDG_DATA_HOME:-$HOME/.local/share}/zsh/plugins
source $ZPLUGINS/zsh-autosuggestions/zsh-autosuggestions.zsh
# syntax highlighting en son yüklenmeli
source $ZPLUGINS/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
Syntax highlighting'in README'si bunu açıkça istiyor: kendinden sonra tanımlanan widget'ları (ZLE'ye bağlı fonksiyonları) saramadığı için dosyanın sonunda olmalı. macOS'ta aynı iki plugin brew install zsh-autosuggestions zsh-syntax-highlighting ile de kurulur; o zaman yolu $(brew --prefix)/share/... olarak verirsin. Autosuggestion'ın gerçek hâli: dtf I yazdığımda satır şöyle göründü, IsbnTests --no-build | tail -1 kısmı soluk renkli öneri:
❯ dtf IsbnTests --no-build | tail -1
Prompt: Starship, Powerlevel10k ve Eklentisiz vcs_info
Prompt'ta görmek istediğim üç şey var: hangi klasördeyim, hangi branch'teyim, bu klasörde hangi .NET SDK'sı ya da Node sürümü kullanılacak. Starship bunu tek bir binary ve bir TOML (basit bir ayar dosyası biçimi) dosyasıyla yapıyor ve bash, zsh, fish ve PowerShell'de aynı config'le çalışıyor. Windows'ta PowerShell, WSL'de (Windows içindeki Linux ortamı) zsh kullanan biri için bu, iki ayrı prompt temasıyla uğraşmamak demek. Kurulum: macOS'ta brew install starship, Windows'ta winget install --id Starship.Starship, Linux'ta resmi kurulum script'i; sonra .zshrc'nin en sonuna eval "$(starship init zsh)". Benim ~/.config/starship.toml'ım:
add_newline = false
format = "$directory$git_branch$git_status$dotnet$nodejs$cmd_duration$line_break$character"
[directory]
truncation_length = 3
[git_branch]
format = "[$symbol$branch]($style) "
symbol = "git:"
[dotnet]
format = "[.NET $version]($style) "
version_format = "${raw}"
[nodejs]
format = "[node $version]($style) "
version_format = "${raw}"
[cmd_duration]
min_time = 2000
format = "[took $duration]($style) "
Starship'in varsayılan sembolleri Nerd Font (ikon eklenmiş programcı fontları) ister; ben sembolleri metne çevirdiğim için normal bir fontla da düzgün görünüyor. Repo'nun kökünde, Angular projesinde ve 5 saniye süren bir komuttan sonra prompt (renk kodları temizlenmiş hâliyle):
bookstore git:main .NET 10.0.100
❯
bookstore/web/bookstore-web git:main node 24.22.3
❯
bookstore git:main .NET 10.0.100 took 5s
❯
Burada dikkat edilecek bir ayrıntı var: Starship'in dotnet modülü sürümü repo'daki global.json'dan okuyor. global.json 10.0.100 ve rollForward: latestFeature diyor; gerçekte çalışan SDK ise dotnet --version'ın söylediği 10.0.404. Prompt'taki sürüm "repo'nun istediği", dotnet --version "kullanılan" sürüm. global.json'ın nasıl çalıştığını ASP.NET Core Web API serisinin ".NET SDK ve global.json" yazısında anlattık.
Powerlevel10k zsh'de çok popüler ve hızlı bir tema, ama README'si projenin çok sınırlı desteğe sahip olduğunu ve yeni özellik eklenmeyeceğini söylüyor. Bugün sıfırdan kuruyorsan Starship daha güvenli bir seçim. Hiç ek araç istemiyorsan zsh'nin kendi vcs_info'su yeterli:
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats 'git:%b '
setopt PROMPT_SUBST
PROMPT='%3~ ${vcs_info_msg_0_}%# '
Bu beş satırın ürettiği prompt ada/bookstore/web git:main % oldu.
Başlangıç Hızını Ölçmek: time, zprof ve Cache
Yeni bir terminal sekmesi yarım saniyede açılıyorsa günde onlarca kez bunu beklersin. Ölçmenin en basit yolu shell'i interactive açıp hemen kapatmak, ve bunu birkaç kez tekrarlamak:
$ time (for i in {1..10}; do zsh -i -c exit; done)
real 0m0.232s
user 0m0.171s
sys 0m0.073s
$ time (for i in {1..10}; do HOME=/tmp/m3u zsh -i -c exit; done)
real 0m1.994s
user 0m1.799s
sys 0m0.381s
Ölçümü bash'ten yaptım; zsh'nin time çıktısı aynı bilgiyi tek satırda verir. İkinci komuttaki home, birinciyle aynı .zshrc'yi kullanıyor; tek farkı completion script'lerini her açılışta yeniden üretmesi. Aynı yöntemle beş yapılandırmayı onar kez açtım. Tahminin cevabı:
| Yapılandırma | Ortalama açılış |
|---|---|
Boş .zshrc | 7 ms |
Oh My Zsh, yalnız git plugin'i | 18 ms |
| Oh My Zsh, beş plugin | 19 ms |
| Elle yazılmış, completion'lar her açılışta üretiliyor | 206 ms |
| Elle yazılmış, completion'lar cache'ten | 21 ms |
Oh My Zsh'nin kendisi ölçüme göre ağır değil. Yavaşlığı getiren, her açılışta dış bir programı çalıştıran iki satır: dotnet completions script .NET runtime'ını, ng completion script Node'u ayağa kaldırıyor. Gerçek hayatta aynı etkiyi nvm'in init script'i, eval "$(pyenv init -)" gibi satırlar ve her açılışta brew --prefix çağırmak da yapar.
zsh'nin kendi profiler'ı zprof. .zshrc'nin ilk satırına zmodload zsh/zprof, son satırına zprof yazınca açılışta hangi fonksiyonun ne kadar sürdüğünü basar. Yavaş yapılandırmada çıktının başı:
num calls time self name
-----------------------------------------------------------------------------------
1) 1 9.98 9.98 98.17% 7.16 7.16 70.46% compinit
2) 2 2.82 1.41 27.71% 2.82 1.41 27.71% compaudit
3) 2 0.08 0.04 0.78% 0.08 0.04 0.78% compdef
Toplam yaklaşık 10 ms görünüyor, ama açılış 200 ms sürüyor. zprof yalnızca zsh fonksiyonlarını ölçer; source <(...) içinde çalışan dış process'leri hiç görmez. Bu yüzden önce time ile toplam süreye bak, zprof küçük bir kısım gösteriyorsa sebebi dış komutlarda ara: şüpheli satırları tek tek yoruma alıp yeniden ölç.
Çözüm, completion script'ini bir kez üretip dosyaya yazmak ve yalnızca araç güncellendiğinde yenilemek:
# Completion script'ini yalnız araç değişince yeniden üret
cache_completion() {
local tool=$1 bin=${commands[$1]}
local file=${XDG_CACHE_HOME:-$HOME/.cache}/zsh/$tool-completion.zsh
[[ -n $bin ]] || return 0
if [[ ! -s $file || $bin -nt $file ]]; then
mkdir -p ${file:h}
"${@:2}" >| $file 2>/dev/null
fi
source $file
}
cache_completion dotnet dotnet completions script bash
cache_completion ng env SHELL=${commands[zsh]} ng completion script
$bin -nt $file "binary cache dosyasından yeni mi" diye bakar; yeni bir SDK ya da Angular CLI kurunca bir sonraki açılışta script yeniden üretilir. Bu kontrol, PATH'teki dosyanın tarihine bakıyor; completion'da tuhaflık görürsen ~/.cache/zsh klasörünü silmen yeterli, ilk açılışta yeniden oluşur. Bu değişiklik açılışı 206 ms'den 21 ms'ye indirdi. Tek bir aracın init'i pahalıysa başka bir teknik lazy-load: aracı ilk çağırdığında yükleyen küçük bir function tanımlamak. Ama çoğu durumda cache yeterli ve daha basit.
bash Kullananlar İçin Karşılıklar ve fish'e Kısa Bakış
WSL'de bash'te kalmak istiyorsan aynı fikirler .bashrc'de de geçerli. Alias ve function'ların syntax'ı aynı. Prompt için Starship'i eval "$(starship init bash)" ile kurarsın; Starship yoksa PS1 ve komut substitution'la basit bir branch gösterimi yeter:
if command -v starship >/dev/null; then
eval "$(starship init bash)"
else
PS1='\W $(git branch --show-current 2>/dev/null)\$ '
fi
bash'te her prompt'tan önce çalışan kod PROMPT_COMMAND'dır; önceki yazıdaki history -a oradaydı. Starship'i kurunca bir kontrol yaptım: PROMPT_COMMAND'ın değeri starship_precmd oldu, ama Starship eski değeri STARSHIP_PROMPT_COMMAND="history -a" olarak saklayıp her prompt'ta çalıştırmaya devam ediyor. Kendi init script'i olan başka bir araç bu kadar dikkatli olmayabilir; bir şey bozulursa declare -p PROMPT_COMMAND ile bak.
fish'i de bilmekte fayda var: autosuggestion, syntax highlighting ve iyi bir completion ek plugin gerektirmeden geliyor. Karşılığında POSIX (Unix shell'leri için ortak standart) uyumlu değil; export X=1 yerine set -x X 1 yazılıyor ve internette gördüğün bash satırları her zaman çalışmıyor. Etkileşimli kullanım için fish, script için bash kullanan ekipler var; .NET + Angular ekibinde ortak dili bozduğu için ben zsh'de kalmayı öneriyorum.
Üretimde Dikkat
.zshrc bir geliştirici makinesi dosyası; sunucuya, container image'ına ya da CI runner'ına taşınmamalı. Build adımı dt ya da gs gibi bir kısaltmaya ihtiyaç duyuyorsa o artık kişisel kısayol değil ekibin aracı demektir: repo'da bir script ya da Makefile/task olarak durmalı. Bu ayrımın bir diğer sebebi tekrarlanabilirlik: CI'daki ve senin makinendeki davranış aynı script'ten gelmeli.
Plugin'leri git clone ile kurmak, o repo'nun kodunu her yeni shell'de çalıştırmak demek. Bilinen projelerden al, git pull'dan önce ne değiştiğine kısaca bak, hiç kullanmadığın plugin'i sil. Oh My Zsh'nin 372 plugin'inden kullanmadıklarını listeye eklemek de aynı şekilde gereksiz kod çalıştırmaktır.
Ayarlarını yeni bir makineye taşımak, sürümlemek ve makineler arasında senkron tutmak (dotfiles, chezmoi) Productivity ve Automation serisinin konusu; buradaki dosyalar o serinin ham malzemesi.
Sık Düşülen Tuzaklar
Birincisi, temel komutları alias ile değiştirmek. alias rm='rm -i' interactive shell'de sana güven hissi verir, ama script'ler ve başka makineler bu alias'ı bilmez:
❯ alias rm='rm -i'
❯ cd /tmp && touch a.txt && rm a.txt
rm: remove regular empty file 'a.txt'? n
❯ bash -c 'rm a.txt; ls a.txt'
ls: cannot access 'a.txt': No such file or directory
Alışkanlık edindiğin güvenlik ağı bir gün olmadığı yerde dosyayı siler. Onay istiyorsan ayrı bir ad kullan (alias rmi='rm -i'). Alias'ı tek seferlik atlamak için \rm ya da command rm yazılır.
İkincisi, aynı alias'ın macOS ve Linux'ta farklı davranması. macOS'taki ls, sed ve grep BSD (Berkeley Software Distribution; macOS'un Unix kökeni) sürümleridir, Linux'takiler GNU (Linux dağıtımlarının temel araç seti); ls --color=auto ya da sed -i gibi seçenekler iki tarafta aynı değildir. Bir alias'ı iki işletim sisteminde kullanacaksan [[ $OSTYPE == darwin* ]] ile ayır.
Üçüncüsü, sıralama hataları: compinit'ten önce compdef çağıran completion satırları, syntax highlighting'den sonra yüklenen plugin'ler, prompt'tan sonra PROMPT'u ezen bir ayar. Dördüncüsü, .zshenv'e yavaş ya da ekrana yazan bir şey koymak; her zsh script'i onu da okur.
Küçük Bir Deneme
★☆☆ Kendi .zshrc'ni yazıdaki yedi bölüme ayır. Bookstore için üç alias ve dtf function'ını ekle; type ile hepsinin doğru tanındığını kontrol et.
★★☆ time (for i in {1..10}; do zsh -i -c exit; done) ile kendi açılış süreni ölç. Sonra zprof'u aç ve toplam süreyle zprof'un gösterdiği süreyi karşılaştır. Aradaki fark büyükse hangi dış komutun sebep olduğunu satırları yoruma alarak bul.
★★★ cache_completion'ı kendi kullandığın bir araç için daha uygula (örneğin gh completion -s zsh ya da kubectl completion zsh). Starship'e Bookstore'un Docker context'ini ya da çalışan Kubernetes context'ini gösteren bir modül ekle ve prompt'un açılış süresine etkisini starship timings ile ölç.
zsh tarafı hazır. Bir sonraki yazıda Windows'taki geliştiricinin ana shell'ine geçiyoruz: PowerShell 7'nin object pipeline'ı, Bookstore API'sinden gelen JSON'u object olarak işlemek, $PROFILE, PSReadLine, posh-git ve bash bilen biri için karşılık tablosu.