İleri Seviye Bash: Process Substitution ve Here-doc Kullanımı

Bash scripting öğrenirken bir noktadan sonra temel komutlar yetmez hale gelir. Pipe’lar ve yönlendirmeler güzel, ama bazı senaryolarda daha fazlasına ihtiyacın olur. İşte tam bu noktada process substitution ve here-doc devreye girer. Bu iki özellik, script’lerini gerçekten güçlü kılan, “evet bunu bilen yazmış” dedirten yapılardır. Bugün bu konuyu derinlemesine inceleyeceğiz, gerçek dünya örnekleriyle pekiştireceğiz.

Here-Doc Nedir ve Neden Kullanıyoruz?

Here-doc (heredoc), yani “here document”, bir komuta birden fazla satır girdi sağlamanın en temiz yoludur. Normalde bir komuta girdi verirken ya dosyadan okursun ya da echo ile tek satır geçersin. Ama ya 20 satırlık bir SQL sorgusu çalıştırman gerekiyorsa? Ya da bir config dosyası oluşturacaksan? İşte heredoc tam burada hayat kurtarır.

Temel sözdizimi şöyle:

komut << ETIKET
içerik satırı 1
içerik satırı 2
ETIKET

Buradaki ETIKET istediğin herhangi bir kelime olabilir. Geleneksel olarak EOF (End Of File) veya EOL kullanılır, ama bu zorunlu değil. Tek kural: kapanış etiketi satırın en başında, önünde hiçbir boşluk olmadan bulunmalıdır.

Basit Bir Heredoc Örneği

#!/bin/bash
# Kullanıcıya bilgi mesajı gönderme
mail -s "Sunucu Bakım Bildirimi" [email protected] << EOF
Merhaba,

Bu gece 02:00 - 04:00 arasında sunucularımızda bakım yapılacaktır.
Etkilenecek sistemler:
- web-01
- web-02
- db-master

Sorularınız için [email protected] adresine yazabilirsiniz.

Sistem Yönetimi Ekibi
EOF

Bu kadar basit. Artık echo ile satır satır uğraşmak yok, tek bir blokta tüm içeriği veriyorsun.

Heredoc ile Değişken Genişletme

Heredoc’un güçlü taraflarından biri, içinde Bash değişkenlerini, komut çıktılarını ve hatta aritmetik ifadeleri kullanabilmektir.

#!/bin/bash
SUNUCU_ADI=$(hostname)
TARIH=$(date '+%Y-%m-%d %H:%M')
DISK_KULLANIM=$(df -h / | awk 'NR==2 {print $5}')
ADMIN_MAIL="[email protected]"

cat << EOF
=== Günlük Sistem Raporu ===
Sunucu: $SUNUCU_ADI
Tarih: $TARIH
Root Disk Kullanımı: $DISK_KULLANIM

Bu rapor otomatik olarak oluşturulmuştur.
EOF

Script çalıştığında değişkenler otomatik olarak genişletilir. Çıktı şöyle görünür:

=== Günlük Sistem Raporu ===
Sunucu: web-01.sirket.com
Tarih: 2024-01-15 08:30
Root Disk Kullanımı: 67%

Bu rapor otomatik olarak oluşturulmuştur.

Değişken Genişletmeyi Kapatmak: Tırnaklı Heredoc

Bazen içeriğin olduğu gibi kalmasını isteyebilirsin. Mesela bir Bash script şablonu oluşturuyorsun ve $değişken ifadelerinin genişletilmesini istemiyorsun. Bu durumda etiketi tek tırnak içine alırsın:

#!/bin/bash
# Yeni bir monitoring script şablonu oluşturuyor
cat << 'EOF' > /opt/scripts/monitor_template.sh
#!/bin/bash
# Bu script otomatik oluşturulmuştur
HOSTNAME=$(hostname)
DATE=$(date)
THRESHOLD=${1:-80}

if [ $(df -h / | awk 'NR==2 {print $5}' | tr -d '%') -gt $THRESHOLD ]; then
    echo "UYARI: $HOSTNAME üzerinde disk kullanımı %$THRESHOLD limitini aştı!"
fi
EOF

chmod +x /opt/scripts/monitor_template.sh
echo "Şablon oluşturuldu."

Burada 'EOF' şeklinde tırnak kullandığımız için içindeki $HOSTNAME, $DATE gibi ifadeler genişletilmedi. Dosyaya olduğu gibi yazıldı. Bu özellik özellikle script üretecek script’ler yazarken çok işe yarıyor.

Girintili Heredoc: <<-

Büyük script’lerde kod okunabilirliği önemlidir. Normal heredoc’ta kapanış etiketi mutlaka satır başında olmak zorunda, bu da girintili kod yazarken çirkin durumlar ortaya çıkarır. <<- operatörü bu sorunu çözer: kapanış etiketinin ve içeriğin başındaki tab karakterlerini yok sayar.

#!/bin/bash
deploy_config() {
    local APP_NAME=$1
    local PORT=$2
    local ENV=$3
    
    cat <<- EOF > /etc/nginx/conf.d/${APP_NAME}.conf
		server {
		    listen $PORT;
		    server_name ${APP_NAME}.sirket.com;
		    
		    location / {
		        proxy_pass http://127.0.0.1:8080;
		        proxy_set_header Host $host;
		        proxy_set_header X-Real-IP $remote_addr;
		    }
		    
		    access_log /var/log/nginx/${APP_NAME}_${ENV}.log;
		}
		EOF
    
    echo "$APP_NAME için nginx config oluşturuldu."
}

deploy_config "myapp" "443" "production"

Dikkat: <<- sadece tab karakterlerini siler, boşlukları değil. Editörünüzde tab kullandığınızdan emin olun.

Heredoc ile Veritabanı İşlemleri

Sysadmin’lerin en çok kullandığı yerlerden biri veritabanı yönetimidir. MySQL/PostgreSQL ile heredoc kombinasyonu inanılmaz temiz bir kullanım sunar:

#!/bin/bash
DB_HOST="localhost"
DB_USER="admin"
DB_PASS="gizli_sifre"
DB_NAME="uygulama_db"
YEDEK_TARIH=$(date '+%Y%m%d')

# Günlük temizlik ve istatistik scripti
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" << EOF
-- 30 günden eski log kayıtlarını temizle
DELETE FROM sistem_loglari 
WHERE olusturma_tarihi < DATE_SUB(NOW(), INTERVAL 30 DAY);

-- Temizlenen kayıt sayısını göster
SELECT ROW_COUNT() as silinen_kayit_sayisi;

-- Tablo istatistiklerini güncelle
ANALYZE TABLE sistem_loglari;
ANALYZE TABLE kullanici_olaylari;

-- Mevcut tablo boyutlarını raporla
SELECT 
    table_name as tablo,
    ROUND(((data_length + index_length) / 1024 / 1024), 2) as boyut_mb
FROM information_schema.TABLES 
WHERE table_schema = '$DB_NAME'
ORDER BY boyut_mb DESC;
EOF

echo "Veritabanı bakımı tamamlandı: $YEDEK_TARIH"

Bu script’i cron’a ekleyip her gece çalıştırabilirsin. Hem temizlik hem de raporlama tek script’te.

Process Substitution Nedir?

Process substitution, bir komutun çıktısını sanki bir dosyaymış gibi başka bir komuta verebilmeni sağlar. Sözdizimi <(komut) veya >(komut) şeklindedir.

Neden bu gerekli? Pipe’lar güçlüdür ama tek yönlüdür: komut1 | komut2. Eğer iki komutun çıktısını karşılaştırmak ya da aynı anda iki farklı kaynaktan okumak istersen, pipe yetmez. Process substitution burada devreye girer.

Basit bir örnek: İki dizinin içeriğini karşılaştırma.

#!/bin/bash
# İki sunucudaki kurulu paketleri karşılaştır
# Normalde bunu yapamazsın:
# diff "ssh web-01 dpkg -l" "ssh web-02 dpkg -l"

# Process substitution ile:
diff <(ssh web-01 dpkg -l | awk '{print $2}' | sort) 
     <(ssh web-02 dpkg -l | awk '{print $2}' | sort)

# Çıktı: Hangi paketler bir sunucuda var diğerinde yok

Bu örnekte <(...) ifadesi Bash’e şunu söylüyor: “Bu komutu çalıştır, çıktısını geçici bir dosyaya (aslında named pipe veya /dev/fd) yaz ve o dosyanın yolunu bana ver.” diff komutu ise iki normal dosya aldığını sanıyor.

Process Substitution Gerçek Kullanım Senaryoları

Log Analizi ve Karşılaştırma

Sysadmin olarak en sık yaptığım şeylerden biri: iki farklı kaynaktan gelen verileri karşılaştırmak.

#!/bin/bash
# Aktif bağlantıları DNS ile doğrula
# Firewall'daki izin verilen IP'ler ile aktif bağlantıları karşılaştır

FIREWALL_IPS="/etc/firewall/allowed_ips.txt"

echo "=== Yetkisiz Bağlantı Kontrolü ==="
comm -23 
    <(ss -tn state established | awk 'NR>1 {print $5}' | cut -d: -f1 | sort -u) 
    <(sort "$FIREWALL_IPS")

# comm -23: sadece ilk dosyada olup ikincide olmayanları göster
# Yani: aktif bağlantılarda var ama firewall listesinde yok = şüpheli!

Backup Doğrulama Script’i

#!/bin/bash
KAYNAK_DIR="/var/www/html"
YEDEK_DIR="/backup/web"
LOG_FILE="/var/log/backup_verify.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Yedek doğrulama başlıyor..."

# Kaynak ve yedekte hangi dosyalar var karşılaştır
FARKLI_DOSYALAR=$(diff 
    <(find "$KAYNAK_DIR" -type f -exec md5sum {} ; | sort) 
    <(find "$YEDEK_DIR" -type f -exec md5sum {} ; | sed "s|$YEDEK_DIR|$KAYNAK_DIR|" | sort) 
    | grep "^[<>]" | wc -l)

if [ "$FARKLI_DOSYALAR" -eq 0 ]; then
    log "BAŞARILI: Tüm dosyalar doğrulandı, yedek bütünlüklü."
else
    log "UYARI: $FARKLI_DOSYALAR dosya farklı veya eksik!"
    # Hangi dosyalar farklı, detaylı göster
    diff 
        <(find "$KAYNAK_DIR" -type f | sort) 
        <(find "$YEDEK_DIR" -type f | sed "s|$YEDEK_DIR|$KAYNAK_DIR|" | sort) 
        | grep "^[<>]" >> "$LOG_FILE"
fi

Çıktı Yönlendirme ile Process Substitution: >(komut)

>(komut) sözdizimi de son derece kullanışlı. Bir komutun çıktısını birden fazla hedefe aynı anda göndermek istediğinde harika çalışır:

#!/bin/bash
# Büyük bir log dosyasını işle:
# - Hata satırlarını hata.log'a yaz
# - Uyarı satırlarını uyari.log'a yaz
# - Tüm çıktıyı aynı zamanda ana log'a yaz

LOG_KAYNAK="/var/log/uygulama/app.log"
TARIH=$(date '+%Y%m%d')

cat "$LOG_KAYNAK" | tee 
    >(grep "ERROR" > "/var/log/analiz/${TARIH}_hatalar.log") 
    >(grep "WARN" > "/var/log/analiz/${TARIH}_uyarilar.log") 
    >(grep "CRITICAL" | mail -s "KRİTİK: Acil müdahale gerekli" [email protected]) 
    > "/var/log/analiz/${TARIH}_tum.log"

echo "Log analizi tamamlandı."
echo "Hata sayısı: $(grep -c 'ERROR' /var/log/analiz/${TARIH}_hatalar.log)"
echo "Uyarı sayısı: $(grep -c 'WARN' /var/log/analiz/${TARIH}_uyarilar.log)"

Bu script tek bir cat ile dosyayı bir kez okuyarak dört farklı hedefe aynı anda veri gönderiyor. Performans açısından da verimli.

Process Substitution ile While Loop: Alt Shell Tuzağı

Bash’in en sinir bozucu davranışlarından biri şudur: pipe’ın sağ tarafı bir alt shell’de çalışır. Bu yüzden şu kod beklendiği gibi çalışmaz:

# YANLIŞ - değişken kalıcı olmaz
TOPLAM=0
cat /var/log/access.log | while read satir; do
    TOPLAM=$((TOPLAM + 1))
done
echo "Toplam: $TOPLAM"  # Her zaman 0 çıkar!

Process substitution ile bu sorunu çözebilirsin:

#!/bin/bash
# DOĞRU - process substitution ile ana shell'de çalışır
TOPLAM=0
HATA_SAYISI=0
UYARI_SAYISI=0

while IFS= read -r satir; do
    TOPLAM=$((TOPLAM + 1))
    if echo "$satir" | grep -q "ERROR"; then
        HATA_SAYISI=$((HATA_SAYISI + 1))
    elif echo "$satir" | grep -q "WARN"; then
        UYARI_SAYISI=$((UYARI_SAYISI + 1))
    fi
done < <(cat /var/log/uygulama/app.log)
# Dikkat: < <(komut) - bir boşluk var aralarında!

echo "Toplam satır: $TOPLAM"
echo "Hata: $HATA_SAYISI"
echo "Uyarı: $UYARI_SAYISI"

< <(komut) sözdizimi ilginç görünüyor ama mantıklı: birinci < dosyadan yönlendirme, ikincisi process substitution başlangıcı.

Heredoc ve Process Substitution Birlikte

Bu iki özelliği birleştirince gerçekten güçlü script’ler yazabilirsin. Gerçek bir senaryo: dinamik config üretip hemen doğrulama yapmak.

#!/bin/bash
# Nginx config üret ve syntax kontrolü yap

APP_NAME="${1:-myapp}"
UPSTREAM_PORT="${2:-8080}"
SSL_CERT="/etc/ssl/certs/${APP_NAME}.crt"
SSL_KEY="/etc/ssl/private/${APP_NAME}.key"

# Config'i geçici bir process olarak üret ve nginx'e doğrulat
nginx -t -c <(cat << EOF
worker_processes 1;
error_log /dev/stderr;
pid /tmp/nginx_test.pid;

events {
    worker_connections 1024;
}

http {
    upstream ${APP_NAME}_backend {
        server 127.0.0.1:${UPSTREAM_PORT};
    }
    
    server {
        listen 80;
        server_name ${APP_NAME}.sirket.com;
        return 301 https://$server_name$request_uri;
    }
    
    server {
        listen 443 ssl;
        server_name ${APP_NAME}.sirket.com;
        
        ssl_certificate $SSL_CERT;
        ssl_certificate_key $SSL_KEY;
        
        location / {
            proxy_pass http://${APP_NAME}_backend;
            proxy_set_header Host $host;
        }
    }
}
EOF
)

if [ $? -eq 0 ]; then
    echo "Config syntax doğru! Gerçek config dosyasına yazılıyor..."
    # Gerçek dosyaya yaz
    cat << EOF > "/etc/nginx/conf.d/${APP_NAME}.conf"
# ... aynı içerik ...
EOF
    nginx -s reload
else
    echo "HATA: Config syntax hatası var, değişiklik yapılmadı."
    exit 1
fi

Çok Satırlı SSH Komutları

Uzak sunucularda karmaşık işlemler yaparken heredoc inanılmaz temiz bir kullanım sunar:

#!/bin/bash
# Birden fazla sunucuda deployment yap

SUNUCULAR=("web-01" "web-02" "web-03")
APP_VERSION="${1:-latest}"
DEPLOY_USER="deploy"

for SUNUCU in "${SUNUCULAR[@]}"; do
    echo "=== $SUNUCU üzerinde deployment başlıyor ==="
    
    ssh -T "${DEPLOY_USER}@${SUNUCU}" << ENDSSH
        set -e
        echo "[$(hostname)] Deployment başlıyor: $APP_VERSION"
        
        cd /opt/uygulama
        
        # Mevcut versiyonu yedekle
        cp -r current "backup_$(date +%Y%m%d_%H%M%S)"
        
        # Yeni versiyonu çek
        git fetch origin
        git checkout "$APP_VERSION"
        
        # Bağımlılıkları güncelle
        npm install --production 2>&1
        
        # Uygulamayı yeniden başlat
        systemctl restart uygulama.service
        
        # Sağlık kontrolü
        sleep 3
        if systemctl is-active --quiet uygulama.service; then
            echo "[$(hostname)] Deployment başarılı!"
        else
            echo "[$(hostname)] HATA: Servis başlamadı!"
            exit 1
        fi
ENDSSH
    
    if [ $? -ne 0 ]; then
        echo "HATA: $SUNUCU deployment başarısız! Deployment durduruluyor."
        exit 1
    fi
done

echo "Tüm sunucularda deployment tamamlandı."

SSH heredoc kullanırken dikkat: ENDSSH etiketini tırnak içine almadığında değişkenler lokal makinede genişletilir. Bu örnekte $APP_VERSION lokal değişken, $(hostname) ise uzak sunucuda çalışmasını istediğimiz için kaçış karakteri kullandık.

Hata Yakalama ve Güvenli Kullanım Önerileri

Production script’lerinde şunlara dikkat etmek gerekir:

Geçici dosya temizliği: Process substitution genellikle /dev/fd veya named pipe kullanır, ama bazı sistemlerde geçici dosya oluşturabilir. trap ile temizlik yap:

#!/bin/bash
set -euo pipefail

# Geçici dosyalar için trap
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT

# Heredoc ile güvenli config üretimi
CONFIG_FILE="$TEMP_DIR/uygulama.conf"

cat << EOF > "$CONFIG_FILE"
# Otomatik oluşturuldu: $(date)
[database]
host = ${DB_HOST:-localhost}
port = ${DB_PORT:-5432}
name = ${DB_NAME:?DB_NAME tanımlı değil!}

[cache]
backend = redis
host = ${REDIS_HOST:-localhost}
ttl = 3600
EOF

# Config'i doğrula
if grep -q "^$" "$CONFIG_FILE" | head -1; then
    echo "Config boş satır içeriyor, kontrol et."
fi

# Yerine koy
cp "$CONFIG_FILE" /etc/uygulama/config.ini
echo "Config güncellendi."

set -euo pipefail ile çalışırken process substitution içindeki hatalar her zaman yakalanmayabilir. Kritik komutlar için dönüş değerlerini açıkça kontrol et.

Performans İpuçları

Process substitution, named pipe kullandığı için büyük veri setlerinde pipe kadar verimlidir. Ama bazı durumlar vardır ki dikkatli olmak gerekir:

  • Aynı komutu birden fazla process substitution’da kullanıyorsan ve komut yavaşsa, önce sonucu bir değişkene ya da geçici dosyaya kaydet.
  • diff <(uzun_komut1) <(uzun_komut2) yapısında her iki komut paralel çalışır, bu genellikle avantajdır.
  • Heredoc büyük bloklar için echo veya printf döngülerine göre çok daha hızlıdır, çünkü her satır için ayrı process fork edilmez.

Sonuç

Process substitution ve heredoc, Bash’in en az kullanılan ama en etkili özelliklerinden ikisidir. heredoc ile çok satırlı içerikleri temiz ve okunabilir şekilde yönetebilir, config dosyaları üretebilir, uzak sunucularda karmaşık komut dizileri çalıştırabilirsin. process substitution ile ise pipe’ların çözemediği problemleri, özellikle birden fazla komut çıktısını karşılaştırma veya paralel işleme durumlarını çözebilirsin.

Bu özellikleri öğrendikten sonra geriye bakıp “ben bunu nasıl yapmıyordum?” diyeceksin. Özellikle deployment script’leri, monitoring araçları ve log analiz script’leri yazdığında bu yapılar sana muazzam esneklik sağlar. Kod okunabilirliği de bir o kadar artar, üç ay sonra script’ine baktığında ne yaptığını anlayabilirsin, ki sysadmin dünyasında bu çoğu zaman en önemli kriter haline gelir.

Bir sonraki adım olarak bu örnekleri kendi ortamında denemenizi öneririm. Önce basit heredoc’larla başlayın, ardından process substitution’ı küçük karşılaştırma görevlerinde kullanın. Zamanla bu yapılar sezgisel hale gelir ve script yazmak çok daha keyifli bir hal alır.

Yorum yapın