Process Substitution ve /dev/fd ile Geçici Boru Hattı Dosyaları Oluşturma ve Yönetme

Bash’te bazı işlemler var ki, öğrendikten sonra “bunu daha önce neden bilmiyordum?” diye kendi kendine soruyorsunuz. Process substitution da tam olarak böyle bir özellik. Günlük sysadmin işlerinde onlarca kez “keşke iki komutu dosyaya yazmadan karşılaştırabilseydim” ya da “keşke bu pipe’ı doğrudan dosya gibi kullanabilseydim” diye düşünmüşsünüzdür. İşte process substitution tam olarak bu sorunu çözüyor.

Process Substitution Nedir?

Process substitution, bir komutun çıktısını geçici bir dosya gibi kullanmanıza izin veren Bash (ve Zsh) özelliğidir. Normalde bir komutun çıktısını başka bir komuta pipe ile gönderebilirsiniz. Ama pipe’ın bir kısıtı var: sadece tek bir stdin akışı kabul edebilirsiniz. İki farklı komutun çıktısını aynı anda karşılaştırmanız gerekiyorsa, klasik yöntemle önce bunları dosyalara yazmanız, sonra karşılaştırmanız gerekir.

Process substitution bu geçici dosya yazma adımını ortadan kaldırır. Sözdizimi şöyle:

  • <(komut): Komutun çıktısını okumak için geçici bir dosya tanımlayıcısı oluşturur
  • >(komut): Bir komuta yazmak için geçici bir dosya tanımlayıcısı oluşturur

Bu geçici dosyalar aslında /dev/fd/ altında birer dosya tanımlayıcısı olarak görünür. Yani aslında bir named pipe veya /proc/self/fd/ altında bir sembolik link söz konusudur.

/dev/fd ile İlişkisi

/dev/fd dizini, çalışan bir sürecin açık dosya tanımlayıcılarına erişim sağlar. Linux’ta bu aslında /proc/self/fd/ için bir sembolik linktir.

ls -la /dev/fd/

Bu komutu çalıştırdığınızda genellikle şunları görürsünüz:

lrwxrwxrwx 1 root root 13 ... /dev/fd -> /proc/self/fd
dr-x------ 2 kullanici kullanici 0 ... 0 -> /dev/pts/0
dr-x------ 2 kullanici kullanici 0 ... 1 -> /dev/pts/0
dr-x------ 2 kullanici kullanici 0 ... 2 -> /dev/pts/0

Burada 0, 1 ve 2 sırasıyla stdin, stdout ve stderr’dir. Process substitution kullandığınızda Bash yeni bir dosya tanımlayıcısı oluşturur (genellikle 3, 4, 5 gibi numaralarla) ve bunu /dev/fd/3 gibi bir path olarak size sunar. Komutun argümanı olarak bu path’i geçer, işiniz bitince de tanımlayıcıyı kapatır.

Bunu doğrudan görmek için şu örneği deneyin:

echo <(ls)

Bu komut size /dev/fd/63 gibi bir şey çıktı verecek. İşte o geçici dosya tanımlayıcısı bu.

Temel Kullanım: diff ile Karşılaştırma

En klasik kullanım senaryosu iki komutun çıktısını anında karşılaştırmaktır. Örneğin iki sunucudaki açık portları karşılaştırmak istiyorsunuz:

diff <(ssh sunucu1 "ss -tlnp | awk '{print $4}' | sort") 
     <(ssh sunucu2 "ss -tlnp | awk '{print $4}' | sort")

Burada iki SSH oturumu açılır, her birinin çıktısı ayrı birer geçici dosya tanımlayıcısına yazılır ve diff bu iki “dosyayı” karşılaştırır. Hiçbir yere geçici dosya yazmadınız, disk I/O’sunuz yok, işiniz bittiğinde temizlenecek dosya yok.

Aynı mantıkla iki dizindeki dosya listelerini karşılaştırabilirsiniz:

diff <(ls -la /etc/nginx/sites-enabled/ | awk '{print $NF}' | sort) 
     <(ls -la /etc/apache2/sites-enabled/ | awk '{print $NF}' | sort)

Gerçek Dünya Senaryosu 1: Log Analizi

Üretim ortamında log analizi yaparken process substitution hayat kurtarır. Diyelim ki iki farklı tarih aralığındaki hata sayılarını karşılaştırıyorsunuz:

diff 
  <(grep "ERROR" /var/log/uygulama/app.log | grep "2024-01-15" | wc -l) 
  <(grep "ERROR" /var/log/uygulama/app.log | grep "2024-01-16" | wc -l)

Ya da daha pratik bir senaryo: Nginx access log’undan dün ve bugünkü 500 hatalarını veren IP’leri karşılaştırın:

comm -23 
  <(grep " 500 " /var/log/nginx/access.log | grep "$(date +%d/%b/%Y)" | awk '{print $1}' | sort -u) 
  <(grep " 500 " /var/log/nginx/access.log | grep "$(date -d yesterday +%d/%b/%Y)" | awk '{print $1}' | sort -u)

comm komutu iki sıralı dosyayı karşılaştırır. -23 parametresi sadece ilk dosyaya özgü satırları gösterir. Yani bugün 500 hatası verip dün vermemiş IP’leri bulmuş oldunuz.

Gerçek Dünya Senaryosu 2: Çoklu Komut Çıktısını Birleştirme

paste komutu iki dosyanın sütunlarını yan yana koyar. Bunu geçici dosyasız yapmak için:

paste <(cat /etc/passwd | cut -d: -f1) 
      <(cat /etc/passwd | cut -d: -f3) 
      <(cat /etc/passwd | cut -d: -f6)

Bu, kullanıcı adı, UID ve home dizinini yan yana listeler. Tabii bu örnekte awk daha verimli olurdu ama paste ile process substitution kombinasyonunun gücünü görmek için iyi bir örnek.

Daha pratik bir senaryo: Birden fazla sunucudaki disk kullanımını tek tabloda görmek.

paste 
  <(echo "SUNUCU1"; ssh sunucu1 "df -h / | tail -1") 
  <(echo "SUNUCU2"; ssh sunucu2 "df -h / | tail -1") 
  <(echo "SUNUCU3"; ssh sunucu3 "df -h / | tail -1")

Yazma Yönlü Process Substitution: >()

Şimdiye kadar hep okuma yönlü <() kullandık. Yazma yönlü >() ise bir komuta sanki bir dosyaya yazıyormuşsunuz gibi veri göndermenizi sağlar.

Klasik kullanım: Bir komutun çıktısını hem ekrana göstermek hem de bir dosyaya kaydetmek istiyorsunuz. tee ile yapabilirsiniz ama >() ile çok daha esnek hale gelir:

komut | tee >(gzip > /var/log/backup/cikti.log.gz) | grep "HATA"

Bu örnekte komutun çıktısı hem sıkıştırılarak dosyaya yazılıyor hem de aynı anda grep ile filtrelenip ekrana basılıyor.

Daha gelişmiş bir senaryo: Bir yedekleme işleminin çıktısını hem loglayın hem de hataları ayrı bir dosyaya yazın:

rsync -avz /data/ yedek-sunucu:/yedek/ > 
  >(tee /var/log/rsync/basari.log | mail -s "Rsync Tamamlandi" [email protected]) 
  2> >(tee /var/log/rsync/hata.log | mail -s "Rsync HATASI" [email protected] >&2)

Burada stdout ve stderr ayrı ayrı yönetiliyor. Başarılı çıktılar bir yere, hatalar başka bir yere gidiyor.

tee ile Çoklu Çıktı Yönetimi

tee ve >() kombinasyonu özellikle monitoring script’lerinde çok işe yarar:

./sistem-kontrol.sh | tee 
  >(grep -i "kritik" >> /var/log/monitor/kritik.log) 
  >(grep -i "uyari" >> /var/log/monitor/uyari.log) 
  >(mail -s "Sistem Kontrol Raporu" [email protected]) 
  > /var/log/monitor/genel.log

Tek bir komutun çıktısı dört farklı yere aynı anda yönlendiriliyor. Kritik mesajlar kendi log’una, uyarılar kendi log’una, tamamı mail olarak gidiyor ve genel log’a da yazılıyor.

Gerçek Dünya Senaryosu 3: Veritabanı Yedek Doğrulama

Veritabanı yedeklerini alırken ve doğrularken process substitution çok işe yarar. Örneğin bir MySQL dump’ının satır sayısını canlı veritabanıyla karşılaştırmak:

diff 
  <(mysql -u root -p'sifre' veritabani -e "SELECT COUNT(*) FROM kullanicilar;" | tail -1) 
  <(zcat /yedek/veritabani_$(date +%Y%m%d).sql.gz | grep "INSERT INTO `kullanicilar`" | wc -l)

Ya da iki veritabanı ortamının şemalarını karşılaştırmak (dev ve prod):

diff 
  <(mysql -h dev-db -u root -p'sifre' veritabani -e "SHOW TABLES;" | sort) 
  <(mysql -h prod-db -u root -p'sifre' veritabani -e "SHOW TABLES;" | sort)

Bu size prod’da olup dev’de olmayan ya da tam tersi olan tabloları anında gösterir.

Named Pipe ile Fark

Process substitution’ı daha iyi anlamak için mkfifo ile oluşturulan named pipe’larla farkını bilmek gerekir.

Named pipe ile aynı işi şöyle yaparsınız:

mkfifo /tmp/gecici_pipe
ls /dizin1 | sort > /tmp/gecici_pipe &
diff /tmp/gecici_pipe <(ls /dizin2 | sort)
rm /tmp/gecici_pipe

Process substitution ile:

diff <(ls /dizin1 | sort) <(ls /dizin2 | sort)

Fark açık. Named pipe’ta manuel oluşturma, arka plan işlemi başlatma ve temizleme adımları var. Process substitution bunların hepsini otomatik halleder. Ama named pipe’ın bir avantajı var: Birden fazla farklı süreç aynı named pipe’ı kullanabilir ve süreçler arası kalıcı iletişim için kullanılabilir.

Process Substitution ve Subshell Davranışı

Önemli bir detay: Process substitution bir subshell içinde çalışır. Bu, içeride tanımladığınız değişkenlerin dışarıda görünmeyeceği anlamına gelir.

toplam=0
while IFS= read -r satir; do
    echo "Satir: $satir"
    ((toplam++))
done < <(cat /etc/passwd | grep "bash")
echo "Toplam: $toplam"

Burada < <(...) yapısına dikkat edin. Bu, process substitution çıktısını while döngüsüne stdin olarak bağlar. while döngüsü pipe ile beslenirse subshell’de çalışır ve toplam değişkeni dışarıdan görünmez. Bu yapıyla değişken ana shell’de kalır.

Bu pattern özellikle dosya işlerken çok kullanışlıdır:

hata_sayisi=0
while IFS= read -r dosya; do
    if [[ ! -r "$dosya" ]]; then
        echo "UYARI: $dosya okunamıyor"
        ((hata_sayisi++))
    fi
done < <(find /etc -name "*.conf" -type f)
echo "Toplam hata: $hata_sayisi"

Gerçek Dünya Senaryosu 4: Konfigürasyon Yönetimi

Bir konfigürasyon yönetimi script’i yazıyorsunuz. Mevcut Nginx konfigürasyonunu bir template ile karşılaştırıp farkları bulmak istiyorsunuz:

#!/bin/bash

TEMPLATE_URL="https://konfig-repo.sirket.com/nginx/template.conf"
MEVCUT_KONFIG="/etc/nginx/nginx.conf"

echo "=== Konfigürasyon Fark Raporu ==="
echo "Tarih: $(date)"
echo ""

diff 
  <(curl -s "$TEMPLATE_URL" | grep -v "^#" | grep -v "^$") 
  <(cat "$MEVCUT_KONFIG" | grep -v "^#" | grep -v "^$") 
  | tee >(mail -s "Nginx Konfig Fark Raporu" [email protected])

if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    echo "UYARI: Konfigürasyon template'den farklı!"
    exit 1
fi

join ile Birden Fazla Kaynaktan Veri Birleştirme

join komutu SQL’deki JOIN gibi iki sıralı dosyayı ortak alan üzerinden birleştirir. Process substitution ile dosyasız kullanımı:

join 
  <(getent passwd | cut -d: -f1,3 | sort) 
  <(getent group | cut -d: -f1,3 | sort)

Ya da daha pratik: Bir sistemdeki process listesi ile açık portları kullanıcı bazında eşleştirmek:

join -1 1 -2 1 
  <(ps aux | awk 'NR>1 {print $1, $2}' | sort -k1) 
  <(ss -tlnp | grep -oP 'pid=K[0-9]+' | sort -u | xargs -I{} sh -c 'echo $(ps -p {} -o user=) {}')

Performans Notları

Process substitution disk I/O’sundan kaçındığı için büyük veri setlerinde belirgin performans avantajı sağlar. Ama dikkat edilmesi gereken noktalar var:

  • Bellek kullanımı: Geçici dosya yerine pipe kullandığı için veri bellekte tutulur. Çok büyük veri setlerinde dikkatli olun.
  • Sıralı okuma: Process substitution sonucunda elde edilen dosya tanımlayıcısı seek edilemez. Yani diff gibi komutlar çalışır ama dosyada ileri geri atlama yapan komutlar sorun yaşayabilir.
  • Bash versiyonu: Process substitution Bash 2.x’ten beri var ama bazı minimal ortamlarda /bin/sh üzerinden çalışıyorsanız bu özellik olmayabilir. Her zaman #!/bin/bash ile başlayın.

Hata Ayıklama

Process substitution içindeki komutlarda hata olduğunda fark etmek zor olabilir. Şu tekniği kullanabilirsiniz:

set -o pipefail

diff 
  <(komut1 || echo "KOMUT1 HATASI" >&2) 
  <(komut2 || echo "KOMUT2 HATASI" >&2)

Ya da her bir substitution’ın çıktısını önce test edin:

cikti1=$(komut1) || { echo "komut1 basarisiz"; exit 1; }
cikti2=$(komut2) || { echo "komut2 basarisiz"; exit 1; }
diff <(echo "$cikti1") <(echo "$cikti2")

Sonuç

Process substitution, sysadmin araç kutusunun gizli silahlarından biri. Özellikle şu durumlarda kullanmaktan çekinmeyin:

  • Birden fazla komutun çıktısını anında karşılaştırmanız gerektiğinde (diff, comm, join)
  • Geçici dosya oluşturmak zorunda kalmak istemediğinizde
  • Bir komutun çıktısını aynı anda birden fazla yere yönlendirmeniz gerektiğinde (tee ile)
  • while döngüsünde değişken değerlerini korumak istediğinizde (< <(...) paterni)

/dev/fd ile ilişkisi anlaşıldığında, aslında bu özelliğin sihir olmadığını, sadece işletim sisteminin dosya tanımlayıcı mekanizmasının akıllıca kullanımı olduğunu görürsünüz. Bash bize bu mekanizmayı kolayca kullanabileceğimiz bir sözdizimi sunuyor, biz de bunu üretim script’lerimizde geçici dosyalar, race condition’lar ve temizleme sorunlarından kurtulmak için kullanabiliyoruz.

Bir sonraki log analizi ya da sistem karşılaştırma görevinizde geçici dosyalara uzanmadan önce process substitution’ı düşünün. Büyük ihtimalle tek satırda çözebilirsiniz.

Bir yanıt yazın

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