Klavyeden Hızlı Olmak: Satır Düzenleme Kısayolları, History Arama, Tab Completion ve CLI Completion'ları
Ok tuşları yerine Ctrl-A/E/W/K/Y, Alt-. ve Ctrl-R ile çalışmak terminaldeki hızın büyük kısmını getirir; bash, zsh ve PSReadLine aynı kısayol ailesini paylaşır. History'nin secret'ları nasıl sakladığını üç shell'de gerçek denemeyle gösteriyor, doğru history ayarlarını, .NET 10'un dotnet completions script'ini, ng completion'ı ve zsh'de karşılaştığımız iki completion tuzağının çözümünü anlatıyoruz.
Sürüm notu: Bu yazıdaki komutlar Ubuntu 24.04 üzerinde bash 5.2, zsh 5.9 ve PowerShell 7.6.10 (PSReadLine 2.4.5) ile; .NET SDK 10.0.404, Node.js 24.22.3 ve Angular CLI 22.2.7 kurulu temiz bir ortamda denenmiştir. Windows ve macOS'a özgü tuş davranışları resmi dokümanlardan aktarıldı.
Önce Tahmin Et
Üç ayrı terminal açtın: Ubuntu'da hazır .bashrc ile bash, macOS'un varsayılan ayarlarıyla zsh ve hiç ayar yapılmamış PowerShell 7.6. Üçünde de aynı üç satırı yazıp shell'den çıkıyorsun. İlk satırın başında bir boşluk var:
export BOOKSTORE_API_TOKEN=abc123
export BOOKSTORE_API_TOKEN=abc123
curl -s -H "Authorization: Bearer abc123" localhost:5080/api/books
(PowerShell'de ilk iki satır $env:BOOKSTORE_API_TOKEN = "abc123" şeklinde.) Hangi shell'in history dosyasında token görünür? Hangi satırı üçü birden saklar? Cevap yazının ortasında; üç shell'in de beklenmedik bir davranışı var.
Satır Düzenleme: Readline, ZLE ve PSReadLine
Bir komutu yazarken ok tuşlarıyla karakter karakter geri gitmek, terminalde yavaş olmanın bir numaralı sebebi. Satırı düzenleyen katman shell'e göre değişiyor: bash GNU (özgür yazılım projesi) Readline library'sini, zsh kendi ZLE'sini (Zsh Line Editor; zsh'nin satır düzenleyicisi), PowerShell ise PSReadLine module'ünü kullanıyor. Üçü de Emacs'tan gelen aynı kısayol ailesini destekliyor. Aşağıdaki tablo üç shell'in varsayılan bağlamalarından derlendi (bind -p, bindkey ve Get-PSReadLineKeyHandler çıktıları):
| Tuş | İş | bash (Readline) | zsh (ZLE) | PSReadLine Emacs modu |
|---|---|---|---|---|
| Ctrl-A / Ctrl-E | Satır başı / sonu | beginning-of-line / end-of-line | aynı | BeginningOfLine / EndOfLine |
| Alt-B / Alt-F | Bir kelime geri / ileri | backward-word / forward-word | aynı | BackwardWord / ForwardWord |
| Ctrl-W | Önceki kelimeyi sil | unix-word-rubout | backward-kill-word | UnixWordRubout |
| Ctrl-U | İmlecin solunu sil | unix-line-discard | kill-whole-line (bütün satır) | BackwardKillInput |
| Ctrl-K | İmlecin sağını sil | kill-line | kill-line | KillLine |
| Ctrl-Y | Son silineni geri yapıştır | yank | yank | Yank |
| Alt-. | Önceki komutun son argümanı | yank-last-arg | insert-last-word | YankLastArg |
| Ctrl-L | Ekranı temizle | clear-screen | clear-screen | ClearScreen |
Ctrl-W, Ctrl-U ve Ctrl-K ile silinen metin kaybolmaz, bir kill ring'e (silinen parçaların tutulduğu tampon) gider ve Ctrl-Y ile geri gelir. Uzun bir dotnet test --filter ... satırının ortasında başka bir komut çalıştırman gerekirse Ctrl-A, Ctrl-K ile satırı kesip komutunu çalıştırır, sonra Ctrl-Y ile geri getirirsin. zsh'de Ctrl-U'nun bütün satırı silmesi bash'ten küçük bir farktır.
Windows kullanıcısı için önemli bir ayrıntı: PSReadLine'ın Windows'taki varsayılan edit mode'u Emacs değil, Windows'tur. Bu modda Ctrl-A bütün satırı seçer, Home/End satır başı ve sonudur, Ctrl+Backspace ve Ctrl-W kelime siler, Ctrl+Sol/Sağ ok kelime atlar, Escape satırı temizler. Aynı module'ü Windows moduna alıp tuşlarını listeleyince gördüğüm bağlamaların bir kısmı:
PS> Set-PSReadLineOption -EditMode Windows
PS> Get-PSReadLineKeyHandler -Bound | Where-Object Key -in 'Ctrl+a','Home','Ctrl+w','Escape','Ctrl+r','F8','Ctrl+z' | Format-Table Key,Function
Key Function
--- --------
Ctrl+w BackwardKillWord
Escape RevertLine
Ctrl+z Undo
Home BeginningOfLine
F8 HistorySearchBackward
Ctrl+r ReverseSearchHistory
Ctrl+a SelectAll
WSL'de (Windows içindeki Linux ortamı) bash, Windows'ta PowerShell kullanıyorsan iki ayrı kas hafızası taşımak yerine profile'ına Set-PSReadLineOption -EditMode Emacs yazabilirsin. Ben öyle yapıyorum; Ctrl-A'nın her yerde aynı işi yapması iki dakikalık alışmaya değiyor.
macOS'ta Alt'lı kısayollar varsayılan olarak çalışmaz, çünkü Option tuşu özel karakter üretir. Terminal.app'te profile ayarlarındaki Keyboard sekmesinden "Use Option as Meta key", iTerm2'de Profiles altında Keys bölümünden sol Option tuşu için "Esc+" seçmen gerekiyor.
Son Argüman, Son Komut, Son Klasör: !!, !$, Alt-. ve cd -
bash ve zsh'nin history expansion'ı (history'deki önceki komuttan parça çekme) yazdığını çalıştırmadan önce yerine koyar. En çok kullandıkların:
$ mkdir -p src/Bookstore.Api/Ordering
$ cd !$
cd src/Bookstore.Api/Ordering
$ cd -
/home/ada/bookstore
$ dotnet test --filter "FullyQualifiedName~Isbn"
...
Test summary: total: 6, failed: 0, succeeded: 6, skipped: 0, duration: 0.3s
$ !! --no-build
dotnet test --filter "FullyQualifiedName~Isbn" --no-build
...
!$ önceki komutun son argümanı, !! önceki komutun tamamı. Shell genişletilmiş hâli çalıştırmadan önce ekrana basar, böylece neyin çalıştığını görürsün. cd - bir önceki klasöre döner; PowerShell 7'de de aynı şekilde çalışır. !$'ın etkileşimli karşılığı Alt-.'dır: her basışta bir önceki komutun son argümanını getirir, art arda basınca daha eski komutlara gider ve sonucu çalıştırmadan önce görür, düzenleyebilirsin. PSReadLine'da Alt-. iki modda da YankLastArg'a bağlı.
Bu arada yukarıdaki çıktıda bir sürpriz var: FullyQualifiedName~Isbn filtresi 4 değil 6 test çalıştırdı. ~ operatörü "içerir" demek ve büyük/küçük harfe bakmıyor; BookCatalogTests.Find_returns_book_by_isbn gibi test adları da eşleşti. Tek bir class'ı hedefliyorsan tam adı ver:
$ dotnet test --no-build --filter "FullyQualifiedName~Bookstore.Api.Tests.IsbnTests" | tail -2
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 6 ms - Bookstore.Api.Tests.dll (net10.0)
History'de Aramak: Ctrl-R ve Tahminler
History'de yukarı ok ile tek tek gezmek yerine Ctrl-R'ye basıp aradığın komutun bir parçasını yaz. Üç shell'in ekranı:
(reverse-i-search)`stat': git status -s
bck-i-search: stat_
PS /home/ada/bookstore> git status -s
bck-i-search: stat_
İlk satır bash, ikincisi zsh, son ikisi PowerShell (bash ve zsh ilk eşleşmeyi aynı satırda, PowerShell bir üst satırda gösterir). Tekrar Ctrl-R daha eski eşleşmeye gider, Enter çalıştırır, Ctrl-G (PowerShell'de Escape) aramadan çıkar. Ctrl-S ileri arama yapar, ama çoğu Linux terminalinde önce terminali dondurur, çünkü PTY'de (terminal ile shell arasındaki sanal terminal katmanı) akış kontrolü (ixon) açıktır:
$ stty -a | grep -o -- ' -*ixon'
ixon
Ctrl-S'ye basıp ekran donduysa Ctrl-Q ile çözülür. .bashrc'ye stty -ixon yazınca Ctrl-S ileri arama olarak çalışır.
PowerShell'in burada ek bir özelliği var: predictive IntelliSense (history'ye ve plugin'lere bakıp yazdığın satırı tamamlamayı öneren özellik). PowerShell 7.6.10 ve PSReadLine 2.4.5'te VT (terminal kontrol dizileri) destekleyen bir terminalde varsayılan olarak açık geldiğini gördüm:
PS> Get-PSReadLineOption | Select-Object PredictionSource, PredictionViewStyle
PredictionSource PredictionViewStyle
---------------- -------------------
HistoryAndPlugin InlineView
InlineView öneriyi imlecin sağında soluk renkle gösterir, sağ ok tuşu kabul eder. F2 ile ListView'a geçince öneriler satırın altında liste olarak çıkar. dot yazdığımda ListView'ın gösterdiği:
PS /home/ada/bookstore> dot
<-/1> <History(1)>
> dotnet --version [History]
Benzer bir öneri zsh'de zsh-autosuggestions plugin'iyle gelir; bir sonraki yazıda kuruyoruz.
History Ayarları ve Secret'ları Dışarıda Tutmak
Baştaki tahminin cevabı, üç shell'de aynı üç satırı yazıp çıktıktan sonra history dosyalarının içeriği. bash Ubuntu'nun hazır .bashrc'siyle, zsh macOS'un /etc/zshrc dosyasındaki üç history satırıyla, PowerShell ayarsız çalıştı:
== bash
export BOOKSTORE_API_TOKEN=abc123
curl -s -H "Authorization: Bearer abc123" localhost:5080/api/books
exit
== zsh
export BOOKSTORE_API_TOKEN=abc123
export BOOKSTORE_API_TOKEN=abc123
curl -s -H "Authorization: Bearer abc123" localhost:5080/api/books
exit
== pwsh
curl -s -H "Authorization: Bearer abc123" localhost:5080/api/books
Get-Date -Format yyyy
exit
bash, Ubuntu'nun .bashrc'sindeki HISTCONTROL=ignoreboth sayesinde başında boşluk olan satırı atladı. zsh hiçbirini atlamadı: macOS'un /etc/zshrc dosyası HISTFILE, HISTSIZE=2000 ve SAVEHIST=1000 ayarlıyor ama HIST_IGNORE_SPACE açmıyor. PowerShell ise boşluğa bakmadı, iki $env:...TOKEN satırını da yazmadı: PSReadLine, içinde password, asplaintext, token, apikey ya da secret geçen satırları dosyaya kaydetmiyor. Üçünün de kaydettiği tek satır ise içinde gerçek bir token olan curl komutu oldu, çünkü hiçbir varsayılan kural Bearer'ı tanımıyor.
Benim kullandığım ayarlar, üç shell için. bash (~/.bashrc):
# Büyük history, tekrarsız, başında boşluk olanı kaydetme
HISTSIZE=50000
HISTFILESIZE=100000
HISTCONTROL=ignoreboth:erasedups
HISTIGNORE='*Bearer *'
# Kapanışta üzerine yazma, her komuttan sonra dosyaya ekle
shopt -s histappend
PROMPT_COMMAND="history -a${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
stty -ixon 2>/dev/null
zsh (~/.zshrc):
HISTFILE=~/.zsh_history
HISTSIZE=50000
SAVEHIST=50000
setopt HIST_IGNORE_SPACE HIST_IGNORE_ALL_DUPS HIST_REDUCE_BLANKS
setopt SHARE_HISTORY EXTENDED_HISTORY
# false dönerse satır history'ye yazılmaz
zshaddhistory() {
[[ $1 != *(Authorization:|Bearer )* ]]
}
PowerShell ($PROFILE):
Set-PSReadLineOption -HistoryNoDuplicates -MaximumHistoryCount 20000
Set-PSReadLineOption -AddToHistoryHandler {
param([string]$line)
if ($line -match 'Authorization:|Bearer\s') {
return $false
}
# Varsayılan filtreyi (token, password...) koru
return [Microsoft.PowerShell.PSConsoleReadLine]::GetDefaultAddToHistoryOption($line)
}
PowerShell'deki son satır önemli: AddToHistoryHandler verdiğinde PSReadLine'ın varsayılan hassas veri filtresini değiştirmiş olursun. Kendi kuralını uygulayıp gerisini GetDefaultAddToHistoryOption'a bırakmazsan $env:...TOKEN satırları yeniden history'ye düşmeye başlar. Aynı denemeyi bu ayarlarla tekrarladım; üç dosyada da yalnızca git status -s, dotnet --version ve exit kaldı. zsh'nin EXTENDED_HISTORY'si her satıra zaman damgası ekler (: 1794627661:0;git status -s), SHARE_HISTORY ise açık olan bütün zsh pencerelerinin history'yi anında paylaşmasını sağlar. bash'te aynı etkiye en yakın şey PROMPT_COMMAND'daki history -a; yeni pencere açınca öbür pencerelerin komutlarını görürsün.
Tab Completion: Shell'in Kendi Completion'ı
Tab completion'ın iki katmanı var. İlki shell'in kendisi: komut adları, dosya yolları, değişkenler. İkincisi programa özgü kurallar: git'in branch adlarını, dotnet'in alt komutlarını bilen completion fonksiyonları. Bu ikinci katman kendiliğinden gelmez.
bash'te bu iş bash-completion paketinin işi. Ubuntu masaüstünde kuruludur, minimal kurulumlarda ve container'larda genelde yoktur: sudo apt install bash-completion. zsh'de completion sistemi zsh'nin içinde ama açılması gerekiyor:
autoload -Uz compinit && compinit
# Tab'a iki kez basınca okla gezilebilen menü
zstyle ':completion:*' menu select
# küçük harfle yazınca büyük harfle de eşleş
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'
PowerShell'de tab completion hazır gelir ve cmdlet adlarını, parametreleri, dosya yollarını tamamlar. Windows modunda Tab seçenekler arasında sırayla döner; Ctrl+Space (Linux terminalinde Ctrl+@ olarak görünür) bütün seçenekleri menü olarak açar. Ben Tab'ı her zaman menüye bağlıyorum, yukarı/aşağı oku da yazdığım önekle history'de aramaya:
Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete
Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward
Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward
Son iki satırdan sonra dotnet yazıp yukarı oka basmak yalnızca dotnet ile başlayan eski komutları getirir. Bu, Ctrl-R'den bile hızlı.
dotnet, ng ve git Completion'ları
.NET 10 SDK ile dotnet CLI kendi completion script'ini üretebiliyor: dotnet completions script komutu bash, zsh, fish, nushell ve PowerShell için script basıyor. Shell adını vermezsen SHELL değişkenine bakıyor; değişken yoksa bir hata mesajıyla duruyor. Microsoft'un PowerShell için önerdiği satır:
dotnet completions script pwsh | Out-String | Invoke-Expression
Çalıştığını completion'ı elle tetikleyerek gördüm:
PS> (TabExpansion2 "dotnet test --fi" 16).CompletionMatches.CompletionText
--filter
PS> (TabExpansion2 "dotnet pack" 11).CompletionMatches.CompletionText
pack
package
bash'te source <(dotnet completions script bash) satırı dotnet te + Tab'ı dotnet test'e, --fi + Tab'ı --filter'a tamamladı. zsh'de ise SDK 10.0.404'ün ürettiği zsh script'i alt komutları doğru tamamlıyor ama dotnet build ve dotnet test'in seçeneklerinde hata veriyor:
% dotnet test --fi<Tab>
_arguments:comparguments:327: invalid argument: /nologo[Run test(s), without displaying Microsoft Testplatform banner]
Sebep, script'in /nologo ve /v gibi Windows tarzı seçenek adlarını zsh'nin _arguments fonksiyonuna kabul etmediği bir biçimde vermesi. Bu sürüm için çalışan çözüm, zsh'nin bash completion uyumluluk katmanını açıp bash script'ini kullanmak:
autoload -Uz bashcompinit && bashcompinit
if (( $+commands[dotnet] )); then
source <(dotnet completions script bash 2>/dev/null)
fi
Bu ayarla zsh'de dotnet test --fi + Tab yeniden --filter'a tamamlandı. Yeni bir SDK sürümüne geçince zsh script'ini tekrar dene; düzeltildiyse bashcompinit'e ihtiyacın kalmaz.
Angular CLI, completion'ı ilk kullanımda kendisi teklif ediyor. Yeni bir home'da bir ng komutu çalıştırınca:
Would you like to enable autocompletion? This will set up your terminal so pressing TAB while typing Angular CLI commands will show possible options and autocomplete arguments. (Enabling autocompletion will modify configuration files in your home directory.) Yes
Appended `source <(ng completion script)` to `/tmp/hn/.bashrc`. Restart your terminal or run the following to autocomplete `ng` commands:
source <(ng completion script)
ve .bashrc'nin sonuna şu üç satırı ekledi:
if [[ $- == *i* ]]; then
source <(ng completion script)
fi
Angular dokümanına göre ng completion yalnız bash ve zsh'yi destekliyor; Windows'ta Git Bash ya da WSL içindeki bash/zsh ile çalışıyor, PowerShell desteği yok. Burada bir tuzak buldum: ng completion script hangi shell için script üreteceğine o an içinde olduğun shell'e değil SHELL değişkenine bakarak karar veriyor. Login shell'in bash iken zsh açıp bu satırı çalıştırınca zsh'ye bash script'i gelir ve zsh command not found: complete hatası verir. Çözüm, değişkeni o komut için ayarlamak:
if (( $+commands[ng] )); then
source <(SHELL=${commands[zsh]} ng completion script)
fi
Bu ayarla ng gen + Tab ng generate'e tamamlandı, ng generate co + Tab component ve config seçeneklerini gösterdi.
git completion'ı zsh'de kendiliğinden gelir (_git fonksiyonu zsh ile birlikte dağıtılır), bash'te git paketi bash-completion'a kendi script'ini koyar. PowerShell'de ise git için completion yok; TabExpansion2 "git sw" boş döndü. Bunu PowerShell yazısında posh-git module'üyle çözüyoruz.
Üretimde Dikkat
History dosyası düz metindir ve home klasöründe durur: bash'te ~/.bash_history, zsh'de ~/.zsh_history, PowerShell'de Windows'ta $env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt, Linux ve macOS'ta ~/.local/share/powershell/PSReadLine/. Laptop yedeklerine, dotfiles repo'larına ve destek için paylaşılan ekran kayıtlarına bu dosyalar da girer. Bir secret'ı yanlışlıkla history'ye yazdıysan önce secret'ı iptal et ve yenisini üret; history'den silmek ikinci adım (bash'te history -d, PowerShell'de dosyayı düzenlemek).
Komut satırına argüman olarak verdiğin secret yalnızca history'de kalmaz, process çalıştığı sürece aynı makinedeki diğer kullanıcılar ps ile argümanları görebilir ve CI log'ları komut satırını basabilir. Token gerektiren komutlarda değeri bir dosyadan ya da secret store'dan okut (curl -H @headers.txt, read -s, PowerShell'de Get-Secret). .NET tarafında geliştirme secret'ları için User Secrets'ı ASP.NET Core Web API serisinin "Configuration ve Options Pattern" yazısında anlattık.
Completion script'leri her yeni shell'de çalışır. Bu ortamda dotnet completions script zsh yaklaşık 60 ms, ng completion script 130 ms sürdü; bir sonraki yazıda başlangıç süresini ölçüp bunları cache'lemeyi göstereceğim.
Sık Düşülen Tuzaklar
Birincisi, history'yi ayarladığını sanmak ama ayarı yanlış dosyaya yazmak. HISTCONTROL'u .bash_profile'a koyarsan login olmayan shell'lerde etkisi olmaz; ilk yazıdaki dosya tablosu burada da geçerli.
İkincisi, aynı anda açık birçok bash penceresinde history'nin kaybolması. histappend olmadan son kapanan pencere dosyanın üzerine yazar ve diğer pencerelerde yazdığın komutlar gider.
Üçüncüsü, $EDITOR ya da $VISUAL değişkenine vim yazınca zsh'nin satır düzenleyiciyi sessizce vi moduna alması. Bu ortamda denedim: EDITOR=vim iken zsh bindkey -A viins main, değişken yokken bindkey -A emacs main ile açıldı. Ctrl-A çalışmıyorsa .zshrc'ye bindkey -e yaz.
Dördüncüsü, PowerShell'de AddToHistoryHandler'ı varsayılanı çağırmadan yazmak ve hassas veri filtresini kapatmak; yukarıda anlattım. Beşincisi, VS Code'un integrated terminal'inin ya da Windows Terminal'in bazı kısayolları kendine almasını shell'in hatası sanmak: Windows Terminal Ctrl+Shift ile başlayan kısayolları kendisi kullanır, bir tuş çalışmıyorsa önce terminalin ayarlarına bak.
Küçük Bir Deneme
★☆☆ Bookstore repo'sunda dotnet test --filter "FullyQualifiedName~Isbn" satırını ok tuşlarına hiç dokunmadan yaz, sonra Ctrl-A ile başa gidip Alt-F ile --filter'ın üzerine gel, Ctrl-K ile kes ve Ctrl-Y ile geri yapıştır. Aynısını PowerShell'de Emacs ve Windows modlarında dene.
★★☆ Kendi shell'ine yazıdaki history ayarlarını ekle ve baştaki üç satırlık denemeyi tekrarla. History dosyasında hangi satırların kaldığını kontrol et. PowerShell'de AddToHistoryHandler'ı GetDefaultAddToHistoryOption çağrısı olmadan yazıp farkı gör.
★★★ dotnet, ng ve git completion'larını kur. Sonra yalnızca Tab ve history kullanarak şu üç komutu yaz: dotnet test --filter "FullyQualifiedName~Bookstore.Api.Tests.IsbnTests", web/bookstore-web içinde ng generate component catalog/book-list ve git switch ile var olan bir branch'e geçiş. Kaç tuşa bastığını say ve bir hafta sonra tekrar say.
Shell'in hazır özelliklerini kullanmaya başladın. Bir sonraki yazıda zsh'yi kalıcı olarak yapılandırıyoruz: .zshrc'nin düzeni, alias ve function, plugin'ler, git branch'ini ve .NET sürümünü gösteren bir prompt, ve bunların hepsine rağmen hızlı açılan bir shell.