Process Substitution ve /dev/fd Kullanımı: Geçici Dosya Olmadan Komutlar Arası Veri Akışı
Bir gün production sunucusunda iki komutun çıktısını karşılaştırmam gerekti. Aklıma gelen ilk çözüm geçici dosya oluşturmaktı: önce birini dosyaya yaz, sonra diğeriyle karşılaştır, sonra dosyayı sil. Ama bu hem hantal hem de gereksiz. İşte tam o anda process substitution’ı gerçek anlamda kavradım. O günden bu yana geçici dosya yaratma alışkanlığımdan büyük ölçüde kurtuldum.
Process Substitution Nedir ve Nasıl Çalışır?
Process substitution, bir komutun çıktısını veya girdisini sanki bir dosyaymış gibi başka bir komuta iletmenizi sağlayan Bash özelliğidir. Temelde iki sözdizimi vardır:
<(komut): Komutun çıktısını bir dosya gibi okumak için>(komut): Bir dosyaya yazılır gibi komuta veri göndermek için
Peki arka planda ne oluyor? Bash bu ifadeyi gördüğünde /dev/fd/ altında bir dosya tanımlayıcısı (file descriptor) oluşturuyor ya da sistemin desteklemediği durumlarda /dev/stdin benzeri mekanizmalara başvuruyor. Yani ortada gerçek bir dosya yok, sadece bir pipe var ve bu pipe bir dosya yolu gibi gösteriliyor.
# Basit bir test: process substitution'ın ne döndürdüğünü görelim
echo <(echo "merhaba")
# Çıktı: /dev/fd/63
Gördüğünüz gibi bash bize /dev/fd/63 gibi bir yol veriyor. Bu aslında bir pipe’ın file descriptor üzerinden dosya sistemi arayüzüne açılmış hali. Linux’ta /dev/fd/ dizini process’in açık dosya tanımlayıcılarına sembolik bağlantılar içerir ve bu mekanizma sayesinde komutlar arası veri akışını dosya işlemleriyle aynı API üzerinden gerçekleştirebiliyoruz.
diff ile Gerçek Güç
Process substitution’ın en klasik kullanım senaryosu diff komutudur. diff iki dosya adı bekler, pipe kabul etmez. İşte tam burada process substitution kurtarıcı oluyor.
# İki remote sunucudaki /etc/passwd dosyalarını karşılaştır
diff <(ssh sunucu1 cat /etc/passwd) <(ssh sunucu2 cat /etc/passwd)
Bunu geçici dosyalarla yapmak zorunda kalsaydınız:
# Eski yöntem - gereksiz karmaşıklık
ssh sunucu1 cat /etc/passwd > /tmp/passwd_s1
ssh sunucu2 cat /etc/passwd > /tmp/passwd_s2
diff /tmp/passwd_s1 /tmp/passwd_s2
rm /tmp/passwd_s1 /tmp/passwd_s2
Hem daha uzun, hem cleanup mantığı gerektiriyor, hem de bir şeyler ters gittiğinde geçici dosyalar diskte kalabiliyor.
Başka bir senaryo: production ve staging ortamlarındaki nginx konfigürasyonlarını karşılaştırmak.
# İki ortamdaki nginx config'ini anlık karşılaştır
diff <(ssh prod-server "nginx -T 2>/dev/null")
<(ssh staging-server "nginx -T 2>/dev/null")
Bu tek satır bana her sprint sonunda saatlerce zaman kazandırıyor.
sort ve comm ile Küme İşlemleri
comm komutu iki sıralı dosyayı karşılaştırır. Process substitution ile önce sıralayıp sonra karşılaştırmak çok temiz oluyor:
# Çalışan processlerin iki anlık görüntüsü arasındaki fark
# Hangi yeni processler başladı?
comm -13 <(ps aux | awk '{print $11}' | sort)
<(sleep 5 && ps aux | awk '{print $11}' | sort)
Pratik bir örnek daha: iki sunucudaki kurulu paketleri karşılaştırmak.
# Sunucu1'de olup sunucu2'de olmayan paketler
comm -23
<(ssh sunucu1 dpkg --get-selections | sort)
<(ssh sunucu2 dpkg --get-selections | sort)
Bu, yeni bir sunucu provizyon ederken baseline paket listesini doğrulamak için harika bir yöntem.
Çıktı Process Substitution: >(komut) Kullanımı
Bu form biraz daha az biliniyor ama en az <() kadar güçlü. Bir komutun çıktısını aynı anda birden fazla yere göndermek istediğinizde tee ile birlikte kullanımı özellikle etkileyici:
# Bir log dosyasını hem gzip ile sıkıştır hem de aynı anda analiz et
cat uygulama.log | tee >(gzip > arsiv.log.gz) >(grep "ERROR" > hatalar.txt) | wc -l
Bu komut şunları eş zamanlı yapıyor:
- Log dosyasının satır sayısını sayıyor
- Aynı anda sıkıştırılmış arşiv oluşturuyor
- Aynı anda sadece ERROR içeren satırları ayrı dosyaya yazıyor
Geçici dosya yok, tek geçiş, üç iş.
# Backup alırken hem checksum üret hem de sıkıştır
tar czf - /var/www/html | tee >(md5sum > backup.md5) > backup.tar.gz
echo "Backup tamamlandı, MD5: $(cat backup.md5)"
Bu pattern’i backup scriptlerinde çok kullanıyorum. Önce tar çık, pipe’la hem md5 üret hem de dosyaya yaz. Sonradan checksum doğrulaması için her zaman hazır.
/dev/fd Mekanizmasını Anlamak
Daha önce bahsetmiştim ama biraz daha derine inelim. Linux’ta her process /proc/self/fd/ altında kendi açık dosya tanımlayıcılarına sahip. /dev/fd/ ise genellikle bu dizine bir sembolik bağlantı.
# Mevcut process'in açık file descriptor'larını gör
ls -la /dev/fd/
# veya
ls -la /proc/$$/fd/
Process substitution <(komut) yazdığınızda Bash şunları yapıyor:
- Bir pipe oluşturuyor
- Komutu pipe’ın yazma ucuna yönlendirip fork’luyor
- Pipe’ın okuma ucunu
/dev/fd/Nolarak sunuyor
Bunu elle taklit edebilirsiniz, ama neden yapasınız ki:
# Process substitution'ın yaptığını anlamak için düşük seviyeli karşılık
exec 3< <(echo "test verisi")
cat /dev/fd/3
exec 3<&-
Burada exec 3< ile file descriptor 3’ü açtık, process substitution ile besliyoruz, okuyoruz, sonra kapatıyoruz. Bu pattern, process substitution çıktısını bir döngü içinde birden fazla kez kullanmak istediğinizde işe yarar.
Gerçek Dünya: Log Analizi Senaryoları
Şimdi biraz daha gerçekçi senaryolara geçelim. Bunlar benim production ortamında karşılaştığım ve process substitution ile çözdüğüm durumlar.
Senaryo 1: İki farklı formattaki log dosyasını merge edip analiz etmek
# Apache ve Nginx loglarını normalize edip birleştir, IP bazlı sırala
sort -k1
<(awk '{print $1, "apache", $7, $9}' /var/log/apache2/access.log)
<(awk '{print $1, "nginx", $7, $9}' /var/log/nginx/access.log)
| uniq -c | sort -rn | head -20
Senaryo 2: Canlı log takibi yaparken filtreleme
# Hem tüm logları kaydet hem de sadece kritik hataları terminale göster
tail -f /var/log/uygulama.log | tee >(cat >> /var/log/uygulama_arsiv.log) | grep --line-buffered "CRITICAL|FATAL"
Senaryo 3: Veritabanı dump karşılaştırması
# İki farklı ortamdaki tablo yapısını karşılaştır (dump almadan)
diff
<(mysql -h prod-db -u readonly -psifre myapp -e "SHOW CREATE TABLE usersG" 2>/dev/null)
<(mysql -h staging-db -u readonly -psifre myapp -e "SHOW CREATE TABLE usersG" 2>/dev/null)
Bu sorguyu her schema migration öncesinde çalıştırıyorum. Production ve staging arasındaki şema farklılıklarını anında görmek, çok ciddi sürprizlerden kurtarıyor.
while Döngüsü ile Process Substitution
Bir dosyayı pipe üzerinden while read ile işlemek yaygın bir pattern. Ama bir sürprizi var: pipe bir subshell oluşturduğu için döngü içinde atanan değişkenler dışarıda görünmüyor. Process substitution bu sorunu çözüyor.
# Yanlış yöntem: değişken döngü dışında görünmez
sayi=0
cat dosya.txt | while read satir; do
((sayi++))
done
echo $sayi # Her zaman 0 çıkar!
# Doğru yöntem: process substitution ile
sayi=0
while read satir; do
((sayi++))
done < <(cat dosya.txt)
echo $sayi # Gerçek değeri gösterir
Bu ince fark, uzun süre hata ayıklamaya neden olan klasik bir tuzak. Process substitution burada hem subshell sorununu çözüyor hem de temiz bir sözdizimi sunuyor.
# Pratik örnek: CSV dosyasından kullanıcı oluştur
while IFS=, read -r kullanici email grup; do
if ! id "$kullanici" &>/dev/null; then
useradd -m -G "$grup" -c "$email" "$kullanici"
echo "$kullanici oluşturuldu"
fi
done < <(grep -v "^#" kullanicilar.csv)
paste ve join ile Veri Birleştirme
paste ve join komutları dosya adı bekler, bu yüzden process substitution ile mükemmel uyum içinde çalışır.
# İki komutun çıktısını yan yana getir
paste <(ls -1 /etc/*.conf) <(wc -l /etc/*.conf | head -n -1 | awk '{print $1}')
# Sistem kaynak kullanımını anlık olarak birleştir
paste
<(top -bn1 | grep "Cpu" | awk '{print "CPU:", $2}')
<(free -h | grep Mem | awk '{print "RAM:", $3"/"$2}')
<(df -h / | tail -1 | awk '{print "Disk:", $3"/"$2}')
Bu tek satırla anlık sistem özetini ekrana basabilirsiniz. Monitoring script’lerine bu pattern’i gömmek çok pratik.
Named Pipe ile Karşılaştırma: Ne Zaman Hangisini Kullanmalı?
Process substitution ile named pipe (FIFO) arasında seçim yapmak kafa karıştırabilir. İkisini de kullandım, tercihlerimi paylaşayım:
- Process substitution tercih edin: Tek seferlik veri akışı, komut satırı one-liner’ları, okuma/yazma işlemi birlikte tamamlandığında
- Named pipe tercih edin: Farklı terminallerdeki processler arasında iletişim, persistent bir kanal gerekliyse, background processler arasında senkronizasyon gerektiğinde
# Named pipe ile karşılaştırma
mkfifo /tmp/veri_kanali
komut1 > /tmp/veri_kanali &
komut2 < /tmp/veri_kanali
rm /tmp/veri_kanali
# Process substitution ile aynı iş - çok daha temiz
komut2 < <(komut1)
Named pipe kullanmak zorunda kalmak genellikle daha karmaşık senaryolarda söz konusu. Eğer basit bir one-liner yazıyorsanız process substitution neredeyse her zaman daha iyi seçim.
Dikkat Edilmesi Gereken Noktalar
Process substitution kullanırken bir kaç şeye dikkat etmek gerek:
Hata yönetimi karmaşıklaşabilir. Process substitution içindeki komutun çıkış kodu ana komuta doğrudan yansımaz. Özellikle set -e modunda çalışıyorsanız bu sürpriz hatalara yol açabilir.
# Bu başarısız olabilir ama fark etmeyebilirsiniz
diff <(komut_hata_verebilir) <(diger_komut)
echo $? # Sadece diff'in çıkış kodunu gösterir
POSIX uyumluluğu yoktur. Process substitution Bash ve Zsh’a özgü bir özellik. Eğer scriptinizin /bin/sh ile de çalışması gerekiyorsa kullanmayın.
#!/bin/bash # Mutlaka bash belirtin, sh değil
Büyük veri akışlarında bellek kullanımına dikkat edin. Pipe buffer dolduğunda processler bloke olabilir. Çok büyük veri setlerinde bu bir sorun yaratabilir.
tee ile birlikte senkronizasyon sorunları. >(komut) kullanımında ana process bittiğinde arka plandaki process hala yazıyor olabilir. Bu durumda wait komutu kullanmak gerekebilir.
# Güvenli kullanım
{ komut | tee >(isleme_komutu > sonuc.txt); } && wait
Gelişmiş Kullanım: Çoklu Process Substitution
Aynı komutta birden fazla process substitution kullanmak mümkün ve bazen çok güçlü kombinasyonlar ortaya çıkabiliyor:
# Üç farklı sunucudaki disk kullanımını karşılaştır
diff3
<(ssh sunucu1 df -h)
<(ssh sunucu2 df -h)
<(ssh sunucu3 df -h)
# Python scriptinin ürettiği veriyi hem kaydet hem de görselleştir
python3 veri_uret.py | tee
>(gzip > veri_$(date +%Y%m%d).json.gz)
>(python3 grafik_ciz.py > grafik.png)
>(mail -s "Günlük Rapor" [email protected])
> /dev/null
Bu tek pipeline ile dört iş paralel yapılıyor. Eğer bunu sequential yapacak olsaydınız hem çok daha yavaş olurdu hem de script çok daha karmaşık görünürdü.
Monitoring ve Alerting Script’lerinde Kullanım
Son olarak, bu teknikleri bir arada kullanan gerçekçi bir monitoring örneği paylaşayım:
#!/bin/bash
# Basit disk ve servis sağlık kontrolü
ESIK=80
SUNUCULAR="web1 web2 db1"
for sunucu in $SUNUCULAR; do
# Her sunucudan disk kullanımı ve servis durumunu al, karşılaştır
diff
<(ssh "$sunucu" df -h | awk 'NR>1 {print $5, $6}' | sort)
<(ssh "$sunucu" df -h | awk -v esik="$ESIK" 'NR>1 && int($5)>esik {print $5, $6, "UYARI"}' | sort)
> /dev/null 2>&1 || {
echo "UYARI: $sunucu - Disk kullanımı $ESIK% üzerinde!"
ssh "$sunucu" df -h | awk -v esik="$ESIK" 'NR>1 && int($5)>esik'
}
done
Sonuç
Process substitution, öğrendikten sonra “bunu nasıl bilmeden yaşadım” dedirten türden bir özellik. Temel faydaları:
- Geçici dosya yaratma/silme döngüsünden kurtuluyorsunuz
- Race condition riski taşıyan dosya operasyonlarından kaçınıyorsunuz
- Kodunuz daha okunabilir ve daha az satır içeriyor
- Paralel veri işleme imkanı sunuyor
diff <(komut1) <(komut2) ile iki komutun çıktısını karşılaştırmak, tee >(komut) ile çıktıyı çoğaltmak ve while read döngülerinde < <(komut) kullanmak bu aracın en değerli üç kullanım senaryosu. Bunları alışkanlık haline getirdiğinizde hem günlük sysadmin işleriniz hem de yazdığınız scriptler ciddi ölçüde sadeleşecek.
/dev/fd mekanizmasını anlamak da sizi bir adım öteye taşıyor. Linux’taki “her şey dosyadır” felsefesinin bu kadar zarif bir uygulamasını başka yerde bulmak zor. Process substitution tam da bu felsefeyi günlük hayatınıza taşıyan araç.
