Komut Çıktısını Değişkene Atama: Command Substitution Kullanımı

Bir shell script yazarken en çok ihtiyaç duyduğun şeylerden biri, bir komutun çıktısını alıp başka bir yerde kullanmaktır. Örneğin şu anki tarihi bir log dosyasının adına eklemek, çalışan process sayısını kontrol etmek ya da bir sunucunun hostname’ini dinamik olarak almak… İşte tam bu noktada command substitution devreye girer. Bash’in bu özelliği olmadan anlamlı bir otomasyon scripti yazmak neredeyse imkansız olurdu.

Command Substitution Nedir?

Command substitution, bir komutun standart çıktısını (stdout) alıp bunu doğrudan bir değişkene atamana veya başka bir komutun içinde kullanmana olanak tanıyan shell mekanizmasıdır. Teknik olarak söylemek gerekirse, shell önce iç içe geçmiş komutu çalıştırır, çıktısını alır ve bu çıktıyı sanki sen oraya doğrudan yazmışsın gibi yerine koyar.

Bash’te command substitution’ın iki farklı sözdizimi vardır:

# Modern sözdizimi (önerilen)
degisken=$(komut)

# Eski backtick sözdizimi
degisken=`komut`

İkisi de aynı işi yapar ama modern $() sözdizimini kullanmanı kesinlikle tavsiye ederim. Nedeni basit: iç içe kullanımlarda backtick sözdizimi kaçış karakterleri yüzünden çok çabuk karmaşıklaşır, okunması ve debug etmesi zorlaşır. $() ile istediğin kadar derinlikte iç içe kullanım yapabilirsin.

Temel Kullanım Örnekleri

En basit haliyle başlayalım:

# Şu anki tarihi değişkene ata
bugun=$(date +%Y-%m-%d)
echo "Bugünün tarihi: $bugun"

# Hostname'i al
sunucu_adi=$(hostname)
echo "Bu sunucu: $sunucu_adi"

# Çalışan kullanıcı sayısını öğren
kullanici_sayisi=$(who | wc -l)
echo "Şu an $kullanici_sayisi kullanıcı bağlı"

Gördüğün gibi kullanım son derece sezgisel. $() içine komutunu yazıyorsun, çıktı değişkene atanıyor. Ama işin güzelliği burada bitmiyor.

Değişken Ataması Olmadan Doğrudan Kullanım

Command substitution’ı mutlaka bir değişkene atamak zorunda değilsin. Doğrudan başka bir komutun içinde de kullanabilirsin:

# Doğrudan echo içinde kullanım
echo "Kernel versiyonu: $(uname -r)"

# mkdir ile dinamik dizin oluşturma
mkdir -p /backup/$(date +%Y%m%d)

# rsync ile tarihli yedek alma
rsync -av /var/www/ /backup/www_$(date +%Y%m%d_%H%M%S)/

Bu yaklaşım özellikle tek seferlik işlemler için kod tasarrufu sağlar. Ama genel kural olarak, aynı değere birden fazla kez ihtiyaç duyacaksan değişkene atamak daha mantıklıdır.

Gerçek Dünya Senaryosu 1: Log Dosyası Yönetimi

Diyelim ki bir uygulama sunucusundasın ve her gece çalışan bir backup scriptine ihtiyacın var. Tarihli log dosyaları ve yedekler oluşturman gerekiyor:

#!/bin/bash

# Zaman damgası değişkenleri
tarih=$(date +%Y-%m-%d)
saat=$(date +%H:%M:%S)
zaman_damgasi=$(date +%Y%m%d_%H%M%S)
sunucu=$(hostname -s)

# Log dosyası adını dinamik oluştur
log_dosyasi="/var/log/backup/backup_${sunucu}_${tarih}.log"

# Log dizinini oluştur
mkdir -p /var/log/backup

echo "[$saat] Backup başlatıldı - Sunucu: $sunucu" >> "$log_dosyasi"

# Disk kullanımını kontrol et
disk_kullanim=$(df -h / | awk 'NR==2{print $5}' | tr -d '%')

if [ "$disk_kullanim" -gt 85 ]; then
    echo "[$saat] UYARI: Disk kullanımı %$disk_kullanim" >> "$log_dosyasi"
fi

# Backup al
tar -czf "/backup/${sunucu}_${zaman_damgasi}.tar.gz" /etc/ 2>> "$log_dosyasi"
echo "[$saat] Backup tamamlandı: ${sunucu}_${zaman_damgasi}.tar.gz" >> "$log_dosyasi"

Bu script production ortamlarda kullandığım gerçek bir yapıya benziyor. Command substitution olmadan her şey statik olurdu ve her sunucu için ayrı script yazman gererdi.

İç İçe Command Substitution

$() sözdiziminin güçlü yanlarından biri, rahatlıkla iç içe kullanabilmesidir:

# İç içe kullanım örneği
en_buyuk_dosya=$(ls -la $(find /var/log -name "*.log" -mtime -1) 2>/dev/null | sort -k5 -rn | head -1 | awk '{print $NF}')

echo "Dün değiştirilen en büyük log dosyası: $en_buyuk_dosya"

# Daha okunabilir versiyonu
son_degisen_loglar=$(find /var/log -name "*.log" -mtime -1 2>/dev/null)
en_buyuk=$(ls -la $son_degisen_loglar 2>/dev/null | sort -k5 -rn | head -1 | awk '{print $NF}')
echo "En büyük dosya: $en_buyuk"

İç içe kullanımlarda okunabilirliği artırmak için ara değişkenler kullanmayı tercih ediyorum. Bir ay sonra scripte bakacak olan sen ya da başka bir sysadmin için hayat çok daha kolay oluyor.

Gerçek Dünya Senaryosu 2: Servis Monitoring Scripti

Üretim ortamında servisleri izleyen basit ama etkili bir script:

#!/bin/bash

# Kontrol edilecek servisler
servisler="nginx mysql redis"

# Bildirim için
sunucu_ip=$(hostname -I | awk '{print $1}')
kontrol_zamani=$(date "+%d/%m/%Y %H:%M:%S")

for servis in $servisler; do
    # Servis durumunu al
    durum=$(systemctl is-active "$servis" 2>/dev/null)
    
    if [ "$durum" != "active" ]; then
        # Kaç dakikadır durduğunu bul
        durus_zamani=$(systemctl show "$servis" --property=InactiveEnterTimestamp | cut -d= -f2)
        
        echo "KRITIK [$kontrol_zamani] - $sunucu_ip üzerinde $servis servisi çalışmıyor!"
        echo "Son duruş: $durus_zamani"
        
        # Servisi yeniden başlatmayı dene
        systemctl restart "$servis"
        yeni_durum=$(systemctl is-active "$servis")
        echo "Yeniden başlatma sonrası durum: $yeni_durum"
    else
        # Servis çalışıyorsa PID bilgisini al
        pid=$(systemctl show "$servis" --property=MainPID | cut -d= -f2)
        bellek=$(ps -o rss= -p "$pid" 2>/dev/null | awk '{printf "%.1f MB", $1/1024}')
        echo "OK - $servis çalışıyor (PID: $pid, Bellek: $bellek)"
    fi
done

Bu script command substitution’ı birkaç farklı yerde kullanıyor: IP adresini almak için, servis durumunu kontrol etmek için, PID bilgisini çekmek için ve bellek kullanımını hesaplamak için. Hepsi birbirine bağlı ve dinamik bir yapı oluşturuyor.

Çok Satırlı Çıktıları İşlemek

Command substitution bazen birden fazla satır döndürür. Bu durumu nasıl yönetirsin?

# Çok satırlı çıktıyı değişkene alma
aktif_baglantilar=$(netstat -tuln 2>/dev/null || ss -tuln)

# Tüm çıktıyı bir anda işle
echo "$aktif_baglantilar" | grep LISTEN | wc -l

# Döngüyle satır satır işle
while IFS= read -r satir; do
    echo "Port açık: $satir"
done <<< "$aktif_baglantilar"

# Büyük dosya listesini al ve işle
buyuk_dosyalar=$(find / -size +100M -type f 2>/dev/null)

if [ -z "$buyuk_dosyalar" ]; then
    echo "100MB üzeri dosya bulunamadı"
else
    dosya_sayisi=$(echo "$buyuk_dosyalar" | wc -l)
    echo "$dosya_sayisi adet büyük dosya bulundu:"
    echo "$buyuk_dosyalar"
fi

Burada dikkat etmen gereken önemli bir nokta var: Değişkeni echo ile kullanırken mutlaka çift tırnak içine al ("$degisken"). Tırnaksız kullanırsan shell çok satırlı çıktıdaki boşlukları ve newline karakterlerini birleştirir, verini bozar.

Hata Yönetimi ve Exit Code Kontrolü

Command substitution yaparken komutun başarılı olup olmadığını kontrol etmek kritik öneme sahiptir:

#!/bin/bash

# Komutun başarılı olup olmadığını kontrol et
db_boyutu=$(mysql -u root -p"$DB_PASS" -e "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) FROM information_schema.tables WHERE table_schema='myapp';" 2>/dev/null | tail -1)

if [ $? -ne 0 ] || [ -z "$db_boyutu" ]; then
    echo "HATA: Veritabanı boyutu alınamadı"
    exit 1
fi

echo "Veritabanı boyutu: ${db_boyutu} MB"

# Daha temiz bir yol: Değişken atama ve kontrol birlikte
if git_surum=$(git --version 2>/dev/null); then
    echo "Git kurulu: $git_surum"
else
    echo "Git kurulu değil, kuruluyor..."
    apt-get install -y git
fi

if ifadesinin hemen içinde command substitution yapabilirsin. Bu hem değişkene atar hem de exit code’u kontrol eder, oldukça temiz bir pattern.

Gerçek Dünya Senaryosu 3: Otomatik SSL Sertifika Kontrolü

Bu script, SSL sertifikalarının sona erme tarihini kontrol eden ve uyarı veren bir tool:

#!/bin/bash

DOMAIN_LISTESI="example.com api.example.com mail.example.com"
UYARI_GUN=30
bugun_epoch=$(date +%s)

for domain in $DOMAIN_LISTESI; do
    # Sertifika bitiş tarihini al
    bitis_tarihi=$(echo | openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    
    if [ -z "$bitis_tarihi" ]; then
        echo "HATA: $domain için sertifika bilgisi alınamadı"
        continue
    fi
    
    # Epoch'a çevir
    bitis_epoch=$(date -d "$bitis_tarihi" +%s 2>/dev/null)
    
    # Kalan gün sayısını hesapla
    kalan_saniye=$((bitis_epoch - bugun_epoch))
    kalan_gun=$((kalan_saniye / 86400))
    
    # Sertifika sahibini al
    sahip=$(echo | openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null | sed 's/.*CN=//')
    
    if [ "$kalan_gun" -lt "$UYARI_GUN" ]; then
        echo "UYARI: $domain sertifikası $kalan_gun gün içinde sona eriyor! (CN: $sahip)"
    else
        echo "OK: $domain - $kalan_gun gün kaldı (CN: $sahip)"
    fi
done

Bu script her satırda command substitution kullanıyor ve gerçekten işe yarayan bir SSL monitoring aracı. Cron’a ekleyip her sabah mail atmasını sağlayabilirsin.

Performans: Ne Zaman Dikkatli Olmalısın?

Command substitution her çağrıldığında bir subshell başlatır. Döngü içinde sık sık kullanırsan bu performans sorununa yol açabilir:

# KÖTÜ: Her iterasyonda subshell açılıyor
for i in $(seq 1 1000); do
    tarih=$(date +%s)  # Her seferinde yeni subshell
    echo "$i: $tarih"
done

# İYİ: Değişmeyen değeri döngü dışında al
tarih=$(date +%s)
for i in $(seq 1 1000); do
    echo "$i: $tarih"
done

# KÖTÜ: Büyük dosya listesi için command substitution döngüsü
for dosya in $(find /var -name "*.log" 2>/dev/null); do
    # Bu yaklaşım dosya adlarında boşluk varsa bozulur
    wc -l "$dosya"
done

# İYİ: find ile -exec ya da while + read kullan
find /var -name "*.log" 2>/dev/null | while IFS= read -r dosya; do
    wc -l "$dosya"
done

Genel kural: Döngü içinde sabit kalan bir değer için command substitution kullanıyorsan, o çağrıyı döngünün dışına çıkar.

Sık Yapılan Hatalar

Boşluk bırakmamak: Atama sırasında = etrafında boşluk olmamalı.

# YANLIŞ
tarih = $(date)
tarih= $(date)

# DOĞRU
tarih=$(date)

Tırnaksız değişken kullanımı: Özellikle çok satırlı çıktılarda tırnaksız kullanım veriyi bozar.

dosyalar=$(ls /etc/)

# YANLIŞ: Boşluklar ve newline'lar birleşir
echo $dosyalar

# DOĞRU: Çift tırnak koru
echo "$dosyalar"

Komut yoksa ne olur: Command substitution sıfır uzunlukta string döndürür, hata vermez. Bunu test etmeyi unutma.

sonuc=$(komut_yok 2>/dev/null)

# Sonuç boş mu diye kontrol et
if [ -z "$sonuc" ]; then
    echo "Komut çalışmadı veya çıktı üretmedi"
fi

Sayısal işlemlerde dikkat: Command substitution’dan gelen değerlerde bazen sondaki newline karakteri sorun çıkarır, ama bash bunu otomatik temizler. Yine de beklenmedik boşluklar için tr -d ' n' kullanabilirsin.

Backtick ile Modern Sözdizimi Farkları

Eski sistemlerde veya sh scriptlerinde backtick görebilirsin. İkisi arasındaki temel farkları bilmek işe yarar:

# Backtick ile iç içe kullanım - kaçış gerektirir, çirkin görünür
sonuc=`echo `uname -r``

# Modern sözdizimi ile iç içe kullanım - temiz ve okunabilir
sonuc=$(echo $(uname -r))

# Çok satırlı komutlarda backtick zorlanır
# Modern sözdizimi ile çok daha rahat
sonuc=$(
    df -h | 
    grep -v tmpfs | 
    awk 'NR>1 {print $6, $5}'
)
echo "$sonuc"

Backtick kullanmak için tek geçerli senaryo, eski POSIX sh uyumlu scriptler yazmaktır. Onun dışında her zaman $() kullan.

Pratik Tek Satırlık Örnekler

Günlük sysadmin işlerinde işine yarayacak pratik örnekler:

# En çok CPU kullanan process
top_process=$(ps aux --sort=-%cpu | awk 'NR==2{print $11, "(%CPU:", $3, "| %MEM:", $4 ")"}')
echo "En çok CPU: $top_process"

# Toplam bellek kullanımı (MB cinsinden)
toplam_bellek=$(free -m | awk 'NR==2{print $3}')
echo "Kullanılan RAM: ${toplam_bellek}MB"

# Son başarısız login denemeleri
basarisiz_giris=$(grep "Failed password" /var/log/auth.log 2>/dev/null | wc -l)
echo "Başarısız giriş denemesi: $basarisiz_giris"

# Aktif network bağlantısı sayısı
baglanti_sayisi=$(ss -tn state established 2>/dev/null | tail -n +2 | wc -l)
echo "Aktif TCP bağlantısı: $baglanti_sayisi"

# En son yüklenen paket
son_paket=$(grep "install " /var/log/dpkg.log 2>/dev/null | tail -1 | awk '{print $4}')
echo "Son yüklenen paket: $son_paket"

Bu tek satırlıkları cron joblarında, monitoring scriptlerinde veya doğrudan terminalden kullanabilirsin.

Sonuç

Command substitution, bash scripting’in en temel yapı taşlarından biridir. Basit görünse de doğru kullanıldığında son derece güçlü automation scriptleri ortaya çıkarmana olanak tanır. Özetlemek gerekirse:

  • Her zaman $() sözdizimini tercih et, backtick kullanma
  • Değişkeni çift tırnak içinde kullan ("$degisken"), özellikle çok satırlı çıktılarda
  • Exit code kontrolü yap, komut başarısız olabilir ve boş değer dönebilir
  • Döngü içinde gereksiz subshell açmaktan kaçın, sabit değerleri dışarıda hesapla
  • Okunabilirliği önde tut, karmaşık iç içe yapılar yerine ara değişkenler kullan

Bu konuyu gerçekten özümsemenin en iyi yolu pratik yapmaktır. Günlük manuel yaptığın işlemleri düşün ve bunları command substitution kullanarak otomatikleştirebileceğin scriptlere dönüştür. Disk kontrolü, servis monitoring, log yönetimi, yedekleme… Hepsinde bu yapıya ihtiyaç duyacaksın ve zamanla neredeyse refleks gibi yazacaksın.

Yorum yapın