Process Substitution ile Geçici Dosya Oluşturmadan İki Komutun Çıktısını Karşılaştırma ve İşleme
Sistem yöneticiliğinde geçici dosya yaratma alışkanlığı, farkında olmadan edindiğimiz kötü alışkanlıklardan biridir. “Şu komutun çıktısını bir yere yazayım, sonra karşılaştırayım” diye düşünürsünüz, /tmp/output1.txt yaratırsınız, sonra temizlemeyi unutursunuz. Haftalarca sunucuda ıssız geçici dosyalar birikir. Process substitution tam da bu problemi ortadan kaldırmak için var.
Process Substitution Nedir ve Neden Önemlidir?
Process substitution, Bash ve Zsh’ın sunduğu bir özelliktir. Bir komutun çıktısını, sanki bir dosyaymış gibi başka bir komuta geçirmenizi sağlar. Sözdizimine bakıldığında iki form görürsünüz:
<(komut): Komutun çıktısını okumak için kullanılır>(komut): Bir komutun çıktısını başka bir sürece yazmak için kullanılır
Kabuk bu ifadeyi gördüğünde arka planda bir named pipe (isimli boru) oluşturur ve bunu /dev/fd/ veya /proc/self/fd/ altında bir dosya tanımlayıcısı olarak sunar. Yani işletim sisteminin gözünde bir dosya var gibi görünür ama diskten yer kaplamaz, geçici bir /tmp kirliliği oluşturmaz.
# Ne döndüğünü merak ediyorsanız doğrudan echo ile görebilirsiniz
echo <(ls)
# Çıktı: /dev/fd/63 gibi bir şey görürsünüz
Bu çıktı, sürecin gerçekten bir dosya tanımlayıcısı kullandığını kanıtlar. Ama siz bu detaylarla uğraşmak zorunda değilsiniz; bash bunu sizin için halleder.
diff ile İki Komutun Çıktısını Karşılaştırma
En klasik kullanım senaryosu diff komutudur. İki sunucu arasındaki paket listesini karşılaştırmak istediğinizi düşünün.
Eski yöntem şöyle görünürdü:
# Çirkin ve zamanı yiyen eski yol
ssh sunucu1 "dpkg -l | awk '{print $2}'" > /tmp/paketler1.txt
ssh sunucu2 "dpkg -l | awk '{print $2}'" > /tmp/paketler2.txt
diff /tmp/paketler1.txt /tmp/paketler2.txt
rm /tmp/paketler1.txt /tmp/paketler2.txt
Process substitution ile:
diff <(ssh sunucu1 "dpkg -l | awk '{print $2}' | sort")
<(ssh sunucu2 "dpkg -l | awk '{print $2}' | sort")
Tek satır, geçici dosya yok, cleanup yok. Üstelik iki SSH bağlantısı paralel olarak açılır, yani eski yönteme göre daha hızlıdır da.
Bunu daha da işlevsel hale getirmek için diff‘in formatlama seçeneklerini kullanabilirsiniz:
# Yan yana karşılaştırma, sadece farklı satırları göster
diff --side-by-side --suppress-common-lines
<(ssh sunucu1 "dpkg -l | awk '{print $2}' | sort")
<(ssh sunucu2 "dpkg -l | awk '{print $2}' | sort")
Gerçek Dünya Senaryosu: Nginx Konfigürasyonlarını Karşılaştırma
Production ortamında çalışan iki nginx sunucunuz var ve aralarında konfigürasyon farkı olup olmadığını anlamak istiyorsunuz. Konfigürasyon dosyaları birden fazla include ile dağılmış olabilir, ama aktif konfigürasyonu nginx -T komutu düzgünce derleyip çıkarır.
diff <(ssh web1 "sudo nginx -T 2>/dev/null")
<(ssh web2 "sudo nginx -T 2>/dev/null")
Bu komut çıktısı size iki sunucu arasındaki tüm nginx konfigürasyonu farklarını gösterir. Hiçbir dosyaya dokunmadınız, hiçbir şey diskte kalmadı. Eğer fark varsa aşağıdaki gibi bir çıktı göreceksiniz:
< worker_processes 4;
> worker_processes 8;
Hatta bunu monitoring script’inizde kullanabilirsiniz:
#!/bin/bash
# nginx-config-check.sh
if ! diff -q <(ssh web1 "sudo nginx -T 2>/dev/null")
<(ssh web2 "sudo nginx -T 2>/dev/null") > /dev/null 2>&1; then
echo "UYARI: web1 ve web2 nginx konfigürasyonlari farkli!" |
mail -s "Nginx Config Drift Algilandi" [email protected]
fi
diff -q sadece farklı olup olmadığını söyler, detayları değil. Bu sayede script minimal kalır.
comm Komutu ile Set Operasyonları
diff satır bazlı farkları gösterirken, comm komutu size set teorisi operasyonları yapma imkanı verir. comm sıralanmış iki dosyayı karşılaştırır ve üç sütun çıkarır: sadece birincide olanlar, sadece ikincide olanlar, her ikisinde de olanlar.
Process substitution olmadan comm kullanmak zordu çünkü sıralanmış dosya istiyordu. Şimdi şöyle yapabilirsiniz:
# Birinci sunucuda olup ikincisinde olmayan paketleri bul
comm -23 <(ssh sunucu1 "dpkg -l | awk '{print $2}' | sort")
<(ssh sunucu2 "dpkg -l | awk '{print $2}' | sort")
-23: 2. ve 3. sütunları gizler, yani sadece birincisinde olanlar kalır-13: Sadece ikincisinde olanları gösterir-12: Her iki sunucuda da olan paketleri gösterir
Bu teknik özellikle inventori yönetiminde işe yarar. Hangi sunucularda hangi servisler çalışıyor, hangi portlar açık gibi soruları hızlıca cevaplayabilirsiniz:
# Hangi portlar sadece birinci sunucuda açık?
comm -23 <(ss -tlnp | awk '{print $4}' | grep -oP ':Kd+' | sort -n | uniq)
<(ssh uzak-sunucu "ss -tlnp | awk '{print $4}' | grep -oP ':Kd+' | sort -n | uniq")
Çıktıyı Hem İşlemek Hem de Kaydetmek: tee ile Kombinasyon
Process substitution’ın >(komut) formu daha az bilinir ama son derece güçlüdür. tee komutuyla birleştiğinde bir çıktıyı aynı anda birden fazla yere gönderebilirsiniz.
Diyelim ki bir deployment script’iniz var ve hem log dosyasına yazmak hem de ekrana basmak hem de hata varsa mail göndermek istiyorsunuz:
./deployment.sh 2>&1 | tee
>(grep -i "error|fail" | mail -s "Deployment Hatalari" [email protected])
>(logger -t deployment)
/var/log/deployments/$(date +%Y%m%d_%H%M%S).log
Bu tek boru zinciri şunları yapar:
- Çıktıyı ekrana basar (tee’nin varsayılan davranışı)
- Hata içeren satırları mail ile gönderir
- Tüm çıktıyı sistem log’una yazar (
loggerile syslog’a) - Timestamp’li bir log dosyasına kaydeder
Üstelik bunların hepsi paralel çalışır.
paste ve join ile Veri Birleştirme
İki komutun çıktısını yan yana getirmek istediğinizde paste kullanılır. Örneğin bir dizindeki dosyaların hem boyutunu hem de satır sayısını aynı anda görmek istiyorsunuz:
paste <(wc -l /etc/*.conf | awk '{print $1}')
<(ls -lh /etc/*.conf | awk '{print $5, $9}')
Ya da daha pratik bir senaryo: Disk kullanımını hem yüzde hem de gerçek değer olarak görmek:
paste <(df -h | awk 'NR>1 {print $5, $6}')
<(df | awk 'NR>1 {print $3"K kullanılıyor"}')
join komutu ise SQL’deki JOIN mantığıyla iki dosyayı ortak bir alana göre birleştirir. Process substitution ile çok güçlü kombinasyonlar yapılabilir:
# /etc/passwd ve /etc/shadow'u uid bazında birleştir
join -t: -1 1 -2 1
<(sort /etc/passwd)
<(sudo sort /etc/shadow)
Log Analizi: Gerçekten İşe Yarayan Bir Senaryo
Production’da karşılaştığınız en sık problemlerden biri iki log dosyasını karşılaştırmak veya birden fazla kaynaktan gelen log’ları analiz etmektir. Diyelim ki iki farklı web sunucunuzun access log’larını karşılaştırmak istiyorsunuz, özellikle hangi IP’lerin sadece birinde görüldüğünü bulmak istiyorsunuz:
#!/bin/bash
# Sadece web1'e giden ama web2'ye gitmeyen IP'leri bul
# Bu, load balancer sorunu veya DNS split-brain işareti olabilir
LOGFILE_WEB1="/var/log/nginx/access.log"
LOGFILE_WEB2_REMOTE="web2:/var/log/nginx/access.log"
comm -23
<(awk '{print $1}' $LOGFILE_WEB1 | sort | uniq)
<(ssh web2 "awk '{print $1}' /var/log/nginx/access.log | sort | uniq")
Bu script load balancer’ınızın düzgün çalışıp çalışmadığını kontrol etmek için sabah cron’una eklenebilir.
Bir başka gerçekçi senaryo: Canlı ortamda iki farklı uygulama versiyonunun hangi endpoint’leri kullandığını karşılaştırmak:
# v1 ve v2'nin eriştiği endpoint'leri karşılaştır
diff
<(grep "v1" /var/log/app/access.log | awk '{print $7}' | sort | uniq -c | sort -rn)
<(grep "v2" /var/log/app/access.log | awk '{print $7}' | sort | uniq -c | sort -rn)
Bakım Scriptlerinde Process Substitution
Rutin bakım görevlerinde process substitution hayat kurtarır. Örneğin her hafta çalışan ve sistemin beklenen durumdan sapıp sapmadığını kontrol eden bir script:
#!/bin/bash
# system-drift-check.sh
# Her Pazartesi sabahı çalışır, baseline ile karşılaştırır
BASELINE_DIR="/etc/system-baseline"
ALERT_EMAIL="[email protected]"
DIFF_FOUND=0
# Açık portları kontrol et
if ! diff -q
<(ss -tlnp | awk 'NR>1 {print $4}' | sort)
"$BASELINE_DIR/open-ports.baseline" > /dev/null 2>&1; then
echo "Port degisikligi tespit edildi:"
diff <(ss -tlnp | awk 'NR>1 {print $4}' | sort)
"$BASELINE_DIR/open-ports.baseline"
DIFF_FOUND=1
fi
# Cron job'ları kontrol et
if ! diff -q
<(crontab -l 2>/dev/null | grep -v "^#" | sort)
"$BASELINE_DIR/crontab.baseline" > /dev/null 2>&1; then
echo "Cron degisikligi tespit edildi:"
diff <(crontab -l 2>/dev/null | grep -v "^#" | sort)
"$BASELINE_DIR/crontab.baseline"
DIFF_FOUND=1
fi
# Yüklü servisleri kontrol et
if ! diff -q
<(systemctl list-units --type=service --state=active | awk '{print $1}' | sort)
"$BASELINE_DIR/active-services.baseline" > /dev/null 2>&1; then
echo "Servis degisikligi tespit edildi"
DIFF_FOUND=1
fi
if [ $DIFF_FOUND -eq 1 ]; then
# Tüm çıktıyı topla ve mail gönder
{
echo "Sunucu: $(hostname)"
echo "Tarih: $(date)"
echo "---"
diff <(ss -tlnp | awk 'NR>1 {print $4}' | sort)
"$BASELINE_DIR/open-ports.baseline"
} | mail -s "Sistem Drift Algilandi: $(hostname)" $ALERT_EMAIL
fi
Bu script’i ilk çalıştırırken baseline’ı oluşturmanız gerekir:
mkdir -p /etc/system-baseline
ss -tlnp | awk 'NR>1 {print $4}' | sort > /etc/system-baseline/open-ports.baseline
crontab -l 2>/dev/null | grep -v "^#" | sort > /etc/system-baseline/crontab.baseline
systemctl list-units --type=service --state=active | awk '{print $1}' | sort > /etc/system-baseline/active-services.baseline
Sık Yapılan Hatalar ve Dikkat Edilmesi Gerekenler
Process substitution kullanırken birkaç önemli noktayı aklınızda tutmanız gerekir.
POSIX uyumluluğu: Process substitution bash ve zsh’a özgüdür. Script’inizin başına #!/bin/bash yazmayı unutmayın. #!/bin/sh ile çalışmaz çünkü POSIX sh bu özelliği desteklemez. Birçok kez /bin/sh‘ın aslında dash olduğunu unutup saatlerce debug yapan insanlar gördüm.
Hata yönetimi: Process substitution içindeki komutların exit code’u ana komuta direkt yansımaz. Bunu aklınızda bulundurarak kritik script’lerde ekstra kontrol ekleyin.
Büyük veri setleri: Named pipe’lar bellekten geçtiğinden, çok büyük veri setleriyle çalışırken pipe buffer’ı dolabilir ve deadlock oluşabilir. Gigabyte’larca veri karşılaştırıyorsanız geçici dosya kullanmak daha güvenli olabilir. Günlük sysadmin işlerinde bu sorunla karşılaşmazsınız ama bilinmesi gerekir.
Paralel çalışma: <(komut1) ve <(komut2) paralel başlatılır. Bu çoğunlukla iyidir, ama her iki komut da aynı kaynağa (örneğin veritabanı) erişiyorsa yük beklentinizin iki katı olacağını unutmayın.
# Bu iki sorgu paralel çalışır, dikkatli olun
diff <(mysql -u root -p'sifre' -e "SELECT user FROM mysql.user ORDER BY user;")
<(mysql -u root -p'sifre' -e "SELECT user FROM mysql.user WHERE Host='localhost' ORDER BY user;")
Kubernetes ve Container Ortamlarında Kullanım
Modern DevOps ortamlarında bu teknik özellikle container imaj karşılaştırmalarında çok işe yarar:
# İki farklı imajın kurulu paketlerini karşılaştır
diff
<(docker run --rm ubuntu:20.04 dpkg -l | awk '{print $2}' | sort)
<(docker run --rm ubuntu:22.04 dpkg -l | awk '{print $2}' | sort)
Ya da iki Kubernetes namespace’indeki pod konfigürasyonlarını karşılaştırmak:
diff
<(kubectl get pods -n production -o jsonpath='{range .items[*]}{.metadata.name}{"n"}{end}' | sort)
<(kubectl get pods -n staging -o jsonpath='{range .items[*]}{.metadata.name}{"n"}{end}' | sort)
Sonuç
Process substitution, öğrendiğinizde “bunu neden daha önce bilmiyordum” dedirten tekniklerden biridir. Arayüzü biraz tuhaf görünebilir ama bir kez alıştıktan sonra elinizdeki en güçlü araçlardan biri haline gelir.
Özetlemek gerekirse:
- Geçici dosya kirliliğini ortadan kaldırır:
/tmp‘ye ne yazıldığını artık takip etmek zorunda değilsiniz - Paralel çalışma sağlar: İki komut aynı anda başlar, script’leriniz daha hızlı çalışır
- Okunabilirlik artar: Tek bir boru zinciri, üç ayrı komut ve iki geçici dosyadan çok daha net anlaşılır
- Hata riskini azaltır: Temizlemeyi unutma, yanlış dosyayı silme gibi insan hataları ortadan kalkar
Eğer bugün sadece bir şey deneyin derseniz şunu söylüyorum: Bir dahaki sefere iki şeyi diff ile karşılaştırmak için geçici dosya yaratmak üzereyken durun ve diff <(...) <(...) formunu kullanın. İlk kullanımdan sonra geri dönmek istemezsiniz.
