compgen ve Bash Yerleşik Komutlarıyla Dosya ve Dizin Tamamlama Mekanizmasını Anlama ve Özelleştirme

Tab tuşuna basmak kadar basit görünen bir şey, aslında altında ciddi bir mekanizma barındırıyor. Bash’in tamamlama sistemi, çoğu sysadmin’in farkında bile olmadan her gün onlarca kez kullandığı ama hiç derinlemesine incelemediği bir yapı. Ben de yıllarca öyle kullandım. Ta ki bir gün yazdığım özel bir deployment script’inde tamamlamanın neden çalışmadığını anlamaya çalışırken compgen ile tanışana kadar.

Bu yazıda bash’in tamamlama mekanizmasını içten dışa anlatacağım. Sadece teorik değil, gerçekten işe yarayan, production ortamında kullandığım örneklerle.

Bash Tamamlama Mekanizması Nasıl Çalışır?

Bash’te Tab tuşuna bastığınızda arka planda birkaç şey oluyor. Bash önce o an ne tamamlamaya çalıştığınıza bakıyor: bir komut mu, bir dosya mı, bir değişken mi, bir kullanıcı adı mı? Bağlama göre farklı tamamlama listesi üretiyor.

Bu sürecin merkezinde iki şey var: readline kütüphanesi ve bash’in yerleşik tamamlama komutları. compgen bu yerleşik komutların en önemlisi, çünkü tamamlama için kullanılabilecek olası değerleri size liste olarak veriyor. complete komutu ise belirli bir komut için hangi tamamlama fonksiyonunun kullanılacağını tanımlıyor.

Şunu kafanıza kazıyın: compgen bir şeyi tamamlamaz, tamamlanabilecek şeyleri listeler. Bu ayrım önemli.

compgen Komutunun Anatomisi

compgen aslında son derece sade bir araç. Temel sözdizimi şu:

compgen [secenek] [on_ek]

Ön ek verirseniz sadece o ön ekle başlayan sonuçları getirir. Vermezseniz hepsini listeler.

En çok kullanılan seçenekler:

  • -c: Tüm çalıştırılabilir komutları listeler (PATH üzerindekiler dahil)
  • -b: Sadece bash built-in komutları listeler
  • -k: Bash keyword’lerini listeler (if, for, while vb.)
  • -a: Tanımlı alias’ları listeler
  • -A function: Tanımlı fonksiyonları listeler
  • -v: Tanımlı shell değişkenlerini listeler
  • -e: Export edilmiş değişkenleri listeler
  • -u: Kullanıcı adlarını listeler
  • -g: Grup adlarını listeler
  • -d: Sadece dizinleri listeler
  • -f: Dosya ve dizinleri listeler
  • -W “kelime_listesi”: Manuel olarak verilen kelimelerden tamamlama yapar

Birkaç pratik örnek görelim hemen:

# "sy" ile başlayan tüm komutları bul
compgen -c sy

# "git" ile başlayan alias varsa göster
compgen -a git

# Mevcut dizindeki "log" ile başlayan dosyalar
compgen -f log

# Sistemdeki tüm kullanıcıları listele
compgen -u

# "DA" ile başlayan environment variable'ları bul
compgen -e DA

Bu komutların çıktısını script’lerde kullanmak inanılmaz işe yarıyor. Mesela bir komutun sistemde kurulu olup olmadığını kontrol etmek için:

if compgen -c | grep -qx "docker"; then
    echo "Docker kurulu"
else
    echo "Docker bulunamadi"
fi

grep -qx kullanmak önemli burada, çünkü “dockerd” gibi şeyleri de eşleştirmemesi için tam eşleşme istiyoruz.

Yerleşik complete Komutuyla Özel Tamamlama Tanımlama

complete komutu, belirli bir komut için nasıl tamamlama yapılacağını bash’e söylüyor. Sözdizimi biraz karmaşık görünse de mantığını bir kere oturtursanız çok güçlü hale geliyor.

Basit bir örnekle başlayalım. Diyelim ki myservice diye bir komutunuz var ve bu komut şu alt komutları alıyor: start, stop, restart, status, reload.

complete -W "start stop restart status reload" myservice

Artık terminalde myservice yazıp Tab’a bastığınızda bu seçenekleri görürsünüz. myservice s yazıp Tab’a basarsanız “start”, “stop”, “status” gelir.

Ama bu statik bir çözüm. Gerçek senaryolarda dinamik tamamlama gerekiyor.

Tamamlama Fonksiyonları Yazmak

complete -F ile bir bash fonksiyonunu tamamlama mekanizması olarak tanımlayabilirsiniz. Bu noktada işler ilginçleşiyor.

Tamamlama fonksiyonlarında iki önemli değişken var:

  • COMP_WORDS: Mevcut komut satırındaki kelimeleri içeren dizi
  • COMP_CWORD: İmlecin şu an hangi kelime üzerinde olduğunu gösteren index
  • COMPREPLY: Tamamlama önerilerini koyacağınız dizi, bunu doldurmak sizin işiniz

Şimdi gerçekten işe yarayan bir örnek yazalım. Bir veritabanı yönetim script’i için tamamlama fonksiyonu:

_mydb_completion() {
    local cur prev commands subcommands
    
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    
    commands="backup restore list connect drop create"
    
    case "$prev" in
        backup|restore)
            # Bu alt komutlar veritabanı adı bekliyor
            # Gerçek ortamda buraya DB listesi çekilir
            local dbs=$(mysql -e "SHOW DATABASES;" 2>/dev/null | tail -n +2)
            COMPREPLY=( $(compgen -W "$dbs" -- "$cur") )
            return 0
            ;;
        connect)
            # Bağlantı için kullanıcı adları öner
            COMPREPLY=( $(compgen -u -- "$cur") )
            return 0
            ;;
        *)
            COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
            return 0
            ;;
    esac
}

complete -F _mydb_completion mydb

Bu fonksiyonu ~/.bashrc veya özel bir tamamlama dosyasına koyup source’ladığınızda mydb komutu için akıllı tamamlama aktif hale geliyor.

Dosya ve Dizin Tamamlamasını Özelleştirme

Bazen bir komutun sadece belirli uzantılı dosyaları tamamlamasını istersiniz. -X filtresi ve -o filenames seçeneği bu işe yarıyor. Ama dikkatli olun, -X pattern’ı “hariç tut” mantığıyla çalışıyor, yani verdiğiniz pattern eşleşenleri listeden çıkarıyor. Başına ! koyarsanız tersine çalışıyor.

# Sadece .log dosyalarını tamamla
_log_viewer_completion() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    COMPREPLY=( $(compgen -f -- "$cur") )
    
    # .log uzantısı olmayanları filtrele
    local filtered=()
    for item in "${COMPREPLY[@]}"; do
        if [[ -d "$item" ]] || [[ "$item" == *.log ]]; then
            filtered+=("$item")
        fi
    done
    COMPREPLY=("${filtered[@]}")
}

complete -F _log_viewer_completion -o filenames logview

Dizinleri de listeye dahil ediyorum çünkü dizin içinde de gezinebilmek gerekiyor, aksi halde /var/log/ gibi dizinlerin içine giremezsiniz.

Daha temiz bir yaklaşım -o plusdirs seçeneğiyle:

# Sadece .conf dosyaları ve dizinler
complete -f -X '!*.conf' -o plusdirs myconfig

Bu tek satır, myconfig komutu için sadece .conf dosyalarını ve dizinleri tamamlayacak şekilde yapılandırıyor.

bash_completion Altyapısını Anlamak

Sisteminizde bash-completion paketi kuruluysa (ve genellikle kurulu oluyor), /etc/bash_completion.d/ ve /usr/share/bash-completion/completions/ dizinlerinde onlarca hazır tamamlama tanımı bulunuyor. Bu dosyaları incelemek inanılmaz öğretici.

Örneğin git tamamlaması bu dosyalardan geliyor. Kendi script’leriniz için de bu dizinlere dosya koyabilirsiniz:

# Özel tamamlama dosyası oluştur
sudo nano /etc/bash_completion.d/mytools

# Ya da kullanıcı bazlı (daha iyi pratik)
mkdir -p ~/.local/share/bash-completion/completions/
nano ~/.local/share/bash-completion/completions/mytools

Kullanıcı bazlı dizin tercih ediyorum çünkü sudo gerektirmiyor ve sadece kendi ortamınızı etkiliyor.

Gerçek Dünya Senaryosu: Deployment Script Tamamlaması

Bu konuya girmemin sebebi olan senaryo buydu. Ekibimizin kullandığı bir deployment script’i vardı, deploy.sh, ve sürekli yanlış environment veya servis adı yazılıyordu. Tamamlama ekleyince bu sorun bitti.

_deploy_completion() {
    local cur prev opts
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    
    # Birinci argüman: environment
    if [[ $COMP_CWORD -eq 1 ]]; then
        opts="production staging development"
        COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
        return 0
    fi
    
    # İkinci argüman: servis adı (docker-compose.yml'den çek)
    if [[ $COMP_CWORD -eq 2 ]]; then
        local env="${COMP_WORDS[1]}"
        local compose_file="./environments/${env}/docker-compose.yml"
        
        if [[ -f "$compose_file" ]]; then
            # docker-compose.yml'den servis adlarını parse et
            local services=$(grep -E "^  [a-zA-Z]" "$compose_file" | 
                           grep -v "#" | 
                           awk '{print $1}' | 
                           tr -d ':')
            COMPREPLY=( $(compgen -W "$services" -- "$cur") )
        else
            COMPREPLY=( $(compgen -W "web api worker scheduler" -- "$cur") )
        fi
        return 0
    fi
    
    # Üçüncü argüman: işlem tipi
    if [[ $COMP_CWORD -eq 3 ]]; then
        opts="--rolling --full --config-only --skip-migration"
        COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
        return 0
    fi
}

complete -F _deploy_completion deploy.sh
complete -F _deploy_completion ./deploy.sh

Hem deploy.sh hem de ./deploy.sh için tanımlamak gerekiyor, çünkü bash bunları farklı komutlar olarak görüyor.

compgen ile Script İçi Doğrulama

compgen‘i sadece tamamlama için değil, script içinde doğrulama amaçlı da kullanıyorum. Bu kullanım genellikle göz ardı ediliyor.

Mesela bir script’e geçilen argümanın geçerli bir komut olup olmadığını kontrol etmek:

#!/bin/bash

validate_command() {
    local cmd="$1"
    if compgen -c | grep -qx "$cmd"; then
        return 0
    else
        echo "Hata: '$cmd' komutu sistemde bulunamadi." >&2
        return 1
    fi
}

# Kullanım
required_tools=("jq" "curl" "aws" "terraform")

for tool in "${required_tools[@]}"; do
    if ! validate_command "$tool"; then
        echo "Gerekli araç eksik: $tool"
        exit 1
    fi
done

echo "Tüm gereksinimler mevcut, devam ediliyor..."

Bu yaklaşım command -v veya which kullanmaktan daha hızlı değil aslında, ama compgen’in built-in olması bazı edge case’lerde fark yaratıyor, özellikle shell fonksiyonları ve alias’ları da kontrol etmek istediğinizde.

# Alias dahil herşeyi kontrol et
check_available() {
    local name="$1"
    # Komut, alias, fonksiyon, built-in her şeyi tara
    compgen -cab | grep -qx "$name" || 
    compgen -A function | grep -qx "$name"
}

Dinamik Tamamlama: Uzak Sunucu Adları

Ssh tamamlaması konusunda klasik bir ihtiyaç var: .ssh/config dosyasındaki host adlarını tamamlamak. Bazen bu dosya büyüyünce aklınızda tutmak imkansız hale geliyor:

_ssh_config_hosts() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    local hosts
    
    # .ssh/config'den host adlarını çek
    hosts=$(grep -E "^Host " ~/.ssh/config 2>/dev/null | 
            awk '{print $2}' | 
            grep -v "*")
    
    # /etc/hosts'tan da ekle (opsiyonel)
    local etc_hosts=$(grep -v "^#" /etc/hosts 2>/dev/null | 
                      awk '{for(i=2;i<=NF;i++) print $i}' | 
                      grep -v "^localhost")
    
    COMPREPLY=( $(compgen -W "$hosts $etc_hosts" -- "$cur") )
}

# Mevcut ssh tamamlamasını geçersiz kıl
complete -F _ssh_config_hosts ssh
complete -F _ssh_config_hosts scp
complete -F _ssh_config_hosts rsync

Dikkat: Mevcut ssh tamamlaması zaten var olabilir. Bunu override ediyorsunuz, sistem güncellemelerinde geri gelebilir.

compopt ile Tamamlama Davranışını İnce Ayarlama

compopt komutu tamamlama fonksiyonu çalışırken seçenekleri dinamik olarak değiştirmenizi sağlıyor. En çok kullandığım iki seçenek:

  • -o nospace: Tamamlamadan sonra otomatik boşluk eklenmesini engeller (örneğin dizin tamamlamasında / sonrası boşluk istemezsiniz)
  • -o filenames: Dosya adı olarak işlem yapılmasını sağlar (tilde genişletme vb.)
_smart_cd_completion() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    
    # Sadece dizinleri tamamla
    COMPREPLY=( $(compgen -d -- "$cur") )
    
    # Tamamlama tek bir dizinse sona slash ekle, boşluk ekleme
    if [[ ${#COMPREPLY[@]} -eq 1 ]] && [[ -d "${COMPREPLY[0]}" ]]; then
        COMPREPLY[0]="${COMPREPLY[0]}/"
        compopt -o nospace
    fi
}

complete -F _smart_cd_completion mycd

Tamamlama Tanımlarını Yönetmek ve Debug Etmek

Tanımladığınız tamamlamaları görüntülemek için:

# Tüm complete tanımlarını listele
complete -p

# Belirli bir komut için
complete -p ssh

# Tamamlamayı kaldır
complete -r mycommand

Debug için set -x kullanabilirsiniz ama tamamlama fonksiyonlarında bu çok fazla gürültü üretiyor. Bunun yerine log dosyasına yazmak daha temiz:

_debug_completion() {
    echo "[DEBUG] COMP_WORDS: ${COMP_WORDS[*]}" >> /tmp/completion_debug.log
    echo "[DEBUG] COMP_CWORD: $COMP_CWORD" >> /tmp/completion_debug.log
    echo "[DEBUG] cur: ${COMP_WORDS[COMP_CWORD]}" >> /tmp/completion_debug.log
    
    # Normal tamamlama mantığı...
    COMPREPLY=( $(compgen -W "test1 test2 test3" -- "${COMP_WORDS[COMP_CWORD]}") )
}

Başka bir terminalde tail -f /tmp/completion_debug.log ile izleyebilirsiniz.

Kalıcı Yapılandırma

Tüm bu tanımları kalıcı hale getirmenin en düzenli yolu:

# ~/.bashrc veya ~/.bash_profile sonuna ekle
if [ -d ~/.bash_completion.d ]; then
    for f in ~/.bash_completion.d/*; do
        source "$f"
    done
fi

Sonra her tool için ayrı dosya:

mkdir -p ~/.bash_completion.d
# Tamamlama fonksiyonlarını buraya koy
nano ~/.bash_completion.d/mytools
nano ~/.bash_completion.d/deploy

Bu yapı hem düzenli tutuyor hem de bir tamamlamayı devre dışı bırakmak istediğinizde sadece o dosyayı silmeniz yeterli oluyor.

Sonuç

compgen ve complete kombinasyonu, bash’in en az keşfedilen ama en pratik özelliklerinden biri. Günlük iş akışınızda ne kadar çok özel script ve tool kullanıyorsanız, bu mekanizmayı özelleştirmenin getirisi o kadar yüksek oluyor.

Başlamak için öneri: Bugün en çok kullandığınız bir internal script’i alın ve onun için basit bir complete -W tanımı yapın. Beş dakika sürer, ama her gün onlarca Tab tuşuna basışta zaman kazandırır. Sonra işi büyütün, fonksiyon yazın, dinamik liste çekin.

Tamamlama fonksiyonlarında performans önemli; çünkü her Tab basışında çalışıyor. Ağ çağrısı veya ağır hesaplama yapıyorsanız, sonuçları cache’leyin. Bunun dışında limit yok. Sisteminizi kendi çalışma tarzınıza göre şekillendirmek bash’in bize sunduğu en güzel özgürlüklerden biri.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir