Process substitution (süreç ikamesi), bir komutun çıktısını veya girdisini bir dosya adı gibi kullanmanızı sağlayan Bash genişletmesidir. <(komut) yazdığınız yere, okunduğunda o komutun çıktısını veren bir dosya adı gelir; >(komut) ise yazıldığında veriyi o komuta ileten bir dosya adı üretir. En bilinen kullanımları, iki komutun çıktısını diff <(komut1) <(komut2) ile karşılaştırmak ve while read ...; done < <(komut) ile döngüdeki değişkenleri kaybetmeden bir komutun çıktısını satır satır okumaktır.

Bu yazıda GNU Bash kılavuzunun Process Substitution bölümünü temel alıyoruz: sözdizimi, iki yönün farkı, işlemin hangi genişletmelerle aynı anda yapıldığı, eş zamansız çalışmanın sonuçları ve sık yapılan hatalar. Örnekler çalıştırılmış ve çıktıları gösterilmiştir. Borular için pipe ve PIPESTATUS yazısına, yönlendirmeler için yönlendirme yazısına bakabilirsiniz; bu yazı ikisinin arasındaki boşluğu dolduran aracı anlatıyor.

Sözdizimi ve ne ürettiği

Sözdizimi ve ne ürettiği

Kılavuzdaki iki biçim <(liste) ve >(liste) şeklindedir. Parantez içindeki komut listesi eş zamansız olarak, yani mevcut komutla aynı anda arka planda çalıştırılır. Onun girdisi veya çıktısı bir dosya adı olarak görünür ve bu dosya adı, genişletmenin sonucu olarak mevcut komuta argüman diye verilir. Yani komut gerçek bir dosya değil, bir boruya bağlı özel bir yol alır. Aşağıdaki örnekte bu yolu ekrana yazdırmak yerine yalnızca biçimini sınıyoruz, çünkü numara çalışmadan çalışmaya değişebilir; sistemimizde /dev/fd/ ile başlayan bir yol üretildi.

bash
yol=$(echo <(true))
[[ $yol == /dev/fd/* ]] && echo "bir /dev/fd yolu"
# çıktı: bir /dev/fd yolu
cat <(echo "süreçten gelen satır")
# çıktı: süreçten gelen satır

Kılavuz bu özelliğin adlandırılmış boruları (FIFO) veya açık dosyaları /dev/fd yöntemiyle adlandırmayı destekleyen sistemlerde bulunduğunu belirtir. Hangi yöntemin kullanılacağı sisteme bağlıdır; betiğinizin üretilen yolun belirli bir biçimde olmasına güvenmemesi gerekir. Ek B'de de süreç ikamesi Bash'in Bourne kabuğuna göre eklediği özellikler arasında sayılır. #!/bin/sh ile başlayan ve başka bir POSIX kabuğunda çalışabilecek betiklerde bu sözdizimine güvenmeyin; ayrıntılar Bash ve sh farkı yazısında.

<(...) ile okumak: komutları dosya gibi karşılaştırmak

Birçok araç girdisini dosya adı olarak bekler ve standart girdiden yalnızca tek bir akış alabilir. İki komutun çıktısını karşılaştırmak istediğinizde bu bir sorundur: ikisini önce geçici dosyalara yazmanız, karşılaştırmanız ve sonra dosyaları silmeniz gerekir. Süreç ikamesi bu üç adımı tek satıra indirir. diff iki "dosyayı" okur ve farkları basar; dosya sistemi üzerinde kalıcı hiçbir şey oluşmaz.

bash
diff <(printf '%s\n' a b c) <(printf '%s\n' a c d)
# çıktı:
#   2d1
#   < b
#   3a3
#   > d

Çıktıdaki 2d1 "birinci girdinin 2. satırı silinmiş", 3a3 ise "birinci girdinin 3. satırından sonra ikinci girdinin 3. satırı eklenmiş" anlamına gelir. diff farklar bulduğunda 1 ile çıkar; bu bir hata değil, aracın kendi sözleşmesidir. Aynı teknik, iki sunucudan alınmış paket listelerini, iki dizinin dosya listelerini veya bir yapılandırmanın iki sürümünü karşılaştırmak için de kullanılır.

Birden fazla akışı yan yana birleştiren araçlar da süreç ikamesinden faydalanır. Aşağıda paste iki listeyi satır satır virgülle birleştiriyor. Bunu boruyla yapamazsınız, çünkü bir borunun yalnızca tek bir girdi ucu vardır.

bash
paste -d, <(printf '%s\n' 1 2 3) <(printf '%s\n' bir iki üç)
# çıktı:
#   1,bir
#   2,iki
#   3,üç

Döngüde değişkeni kaybetmemek: done < <(komut)

Süreç ikamesinin en pratik kullanımı, borunun yol açtığı değişken kaybını önlemektir. komut | while read ... yazdığınızda döngü borunun bir parçası olduğu için alt kabukta çalışır ve döngü içinde atanan değişkenler sonrasında kaybolur. Döngünün girdisini bir yönlendirmeyle < <(komut) biçiminde verirseniz döngü mevcut kabukta çalışır, alt kabukta çalışan yalnızca komut olur.

bash
sayac=0
while read -r s; do
  sayac=$((sayac + 1))
done < <(printf '%s\n' a b c)
echo "sayaç: $sayac"
# çıktı: sayaç: 3

Sözdizimine dikkat edin: iki ayrı işaret vardır. İlk < sıradan bir girdi yönlendirmesidir; ikinci <( ise süreç ikamesinin başlangıcıdır ve ikisinin arasında boşluk bulunur. Yani satır, "şu dosyayı standart girdi yap" demektir ve o dosya, komutun çıktısıdır. Aynı yöntem bir komutun çıktısını doğrudan diziye almak için mapfile ile de kullanılır:

bash
mapfile -t satirlar < <(printf '%s\n' bir iki üç)
echo "${#satirlar[@]} satır, ikincisi: ${satirlar[1]}"
# çıktı: 3 satır, ikincisi: iki

mapfile seçenekleri için mapfile ve readarray yazısına, satır okuma döngüsünün ayrıntıları için while döngüsü ve read yazılarına bakabilirsiniz. Aynı sonucu shopt -s lastpipe ile de alabilirsiniz, ancak o seçenek yalnızca iş denetimi kapalıyken çalışır; süreç ikamesi ise etkileşimli kabukta da aynı şekilde davranır.

>(...) ile yazmak: çıktıyı bir komuta akıtmak

>(liste) biçiminde ters yön geçerlidir: üretilen dosyaya yazılan her şey, parantez içindeki komutun standart girdisine gider. Bu biçim en çok, çıktıyı aynı anda birkaç hedefe dağıtan araçlarla kullanılır. Aşağıda tee gelen veriyi hem kopya.txt dosyasına yazıyor hem de süreç ikamesinin ürettiği yola gönderiyor; o yolun arkasındaki wc -l satırları sayıp sonucu ayrı bir dosyaya yazıyor.

bash
printf '%s\n' satır1 satır2 | tee >(wc -l > sayi.txt) > kopya.txt
wait $!
echo "kopya: $(wc -l < kopya.txt) satır, sayım: $(cat sayi.txt)"
# çıktı: kopya: 2 satır, sayım: 2

Örnekteki wait $! satırı önemlidir. Süreç ikamesi eş zamansız çalıştığı için ana komut bittiğinde parantez içindeki komut henüz bitmemiş olabilir; sonucu hemen okursanız boş veya yarım bir dosya görebilirsiniz. Kılavuz, wait komutunun son çalıştırılan süreç ikamesini, onun süreç kimliği $! ile aynıysa bekleyebildiğini belirtir. Bu yüzden sonuca ihtiyaç duyduğunuz yerde wait $! ile beklemek, zamanlamaya bağlı rastgele hataları önler.

bash
echo "merhaba dünya" > >(tr a-z A-Z > buyuk.txt)
wait $!
cat buyuk.txt
# çıktı: MERHABA DüNYA

Bu ikinci örnekte echo çıktısını doğrudan bir süreç ikamesine yönlendiriyoruz: > >(komut). Yine iki ayrı işaret vardır, ilk > yönlendirme, ikinci >( süreç ikamesidir. Çıktıda ü harfinin küçük kaldığını görüyorsunuz; bunun sebebi tr a-z A-Z aralığının yalnızca İngilizce harfleri kapsamasıdır. Türkçe büyük-küçük harf dönüşümü için Bash'in kendi ${degisken^^} genişletmesine bakın; string işlemleri yazısı bunu ve yerel ayar sınırlamalarını anlatıyor.

Genişletme sırası ve çıkış durumu

Kılavuz, süreç ikamesinin parametre ve değişken genişletmesi, komut ikamesi ve aritmetik genişletme ile aynı anda, soldan sağa yapıldığını söyler. Pratikte bunun anlamı, <(...) içindeki komutun bir dosya adı olarak genişletilip sonra sözcük bölme ve dosya adı genişletmesi aşamalarından geçmesidir. Üretilen yolda boşluk bulunmadığı için bu aşamalar sorun çıkarmaz. Genişletmelerin genel sırası brace expansion ve tilde yazısında özetleniyor.

Daha önemli bir sonuç çıkış durumuyla ilgilidir. Süreç ikamesindeki komut arka planda çalışır ve ana komutun çıkış durumuna katılmaz. Aşağıdaki örnekte false başarısız oluyor ama cat boş girdiyi sorunsuz okuduğu için $? 0 çıkıyor. set -e veya pipefail de bu başarısızlığı yakalamaz.

bash
cat <(false)
echo "cat durumu: $?"
# çıktı: cat durumu: 0

Süreç ikamesindeki komutun başarısını önemsiyorsanız iki yolunuz vardır. Birincisi wait $! ile o komutu bekleyip wait komutunun döndürdüğü durumu kontrol etmektir; kılavuza göre wait, kimliği verilen sürecin çıkış durumunu döndürür. İkincisi, önemli komutu önce ayrı çalıştırıp çıktısını bir değişkene veya geçici dosyaya almak, sonra işlemektir. Çıkış durumlarının yorumlanması için exit kodları yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: < ile ( arasına boşluk koymak, Süreç ikamesini sh ile çalıştırmak, Sonucu beklemeden okumak, Aynı yolu iki kez okumaya çalışmak

< ile ( arasına boşluk koymak

Kılavuz, < veya > ile sol parantez arasında boşluk olamayacağını, aksi halde yapının yönlendirme olarak yorumlanacağını açıkça belirtir. Boşluk koyduğunuzda kabuk bir yönlendirme bekler, ardından bir parantez görür ve sözdizimi hatası verir:

bash
cat < (echo merhaba)
# çıktı:
#   ornek.sh: line 1: syntax error near unexpected token `('
#   ornek.sh: line 1: `cat < (echo merhaba)'

Bu örnek bilerek hatalı yazıldı; çıktısını betiği çalıştırarak aldık. Doğrusu cat <(echo merhaba) biçimidir. Döngü girdisinde ise tersine iki işaretin arasında boşluk olmalıdır: done < <(komut). done <<(komut) yazarsanız kabuk << işaretini here document operatörü olarak okur ve aynı "unexpected token" sözdizimi hatasını verir.

Süreç ikamesini sh ile çalıştırmak

Betik sh betik.sh ile çağrıldığında, sisteminizdeki sh Bash olmayabilir. Süreç ikamesini desteklemeyen bir kabuk bu satırda sözdizimi hatası verir. Bash özelliklerini kullanan betikleri bash betik.sh ile veya doğru shebang satırıyla çalıştırın; bunun için script yazma ve çalıştırma yazısına bakın.

Sonucu beklemeden okumak

>(...) ile bir dosyaya yazdırılan sonucu hemen ardından okumak, zaman zaman boş dosya görmenize yol açar. Bu tür hatalar çoğu çalıştırmada görünmediği için yakalanması zordur. Sonuca ihtiyaç duyduğunuz noktada wait $! kullanın.

Aynı yolu iki kez okumaya çalışmak

Süreç ikamesinin ürettiği yol bir boruya bağlıdır; veri bir kez okunduktan sonra tükenir. Bir araç girdisini iki kez taramak isterse (örneğin önce boyutuna bakıp sonra baştan okumak isterse) bu yol işe yaramaz. Böyle araçlara gerçek bir geçici dosya verin.

Kısa uygulama: sıralanmamış iki listeyi karşılaştırmak

Gerçek hayatta karşılaştırdığınız listeler çoğu zaman aynı sırada değildir. İki sunucuda kurulu paketlerin listesi ya da iki ortamın yapılandırma anahtarları farklı sırayla üretilmiş olabilir; bu durumda diff her satırı farklı sanır. Çözüm, karşılaştırmadan önce iki girdiyi de sıralamaktır. Geleneksel yolda bunun için iki geçici dosya oluşturur, sıralanmış halleri oraya yazar, karşılaştırır ve dosyaları silersiniz. Süreç ikamesiyle sıralama işi doğrudan diff komutunun argümanlarına taşınır.

bash
printf '%s\n' port=80 kullanici=web dizin=/srv > a.conf
printf '%s\n' dizin=/srv port=8080 kullanici=web zaman_asimi=30 > b.conf
diff <(sort a.conf) <(sort b.conf)
# çıktı:
#   3c3,4
#   < port=80
#   ---
#   > port=8080
#   > zaman_asimi=30

Burada iki dosya kalıcıdır, ama sıralanmış halleri hiçbir yere yazılmaz. diff iki ayrı yol alır; her birinin arkasında kendi sort süreci çalışır. Çıktı bize b.conf dosyasında port değerinin farklı olduğunu ve zaman_asimi anahtarının yalnızca orada bulunduğunu söylüyor. Aynı kalıp, bir dizindeki dosya adlarını bir yedek listesine karşı denetlemek gibi işlerde de doğrudan kullanılabilir.

Süreç ikamesini dosya tanımlayıcılarıyla birleştirerek iki akışı aynı döngüde satır satır da okuyabilirsiniz. Aşağıda iki ayrı komutun çıktısı 3 ve 4 numaralı tanımlayıcılara bağlanıyor ve her turda her birinden birer satır okunuyor. Döngünün standart girdisi boş kaldığı için döngü içinde kullanıcıdan girdi almak gibi başka işler de yapılabilir. Numaralı tanımlayıcılarla çalışma dosya tanımlayıcıları ve exec yazısında ayrıntılı olarak anlatılıyor.

bash
while read -r ad <&3 && read -r rol <&4; do
  echo "$ad: $rol"
done 3< <(printf '%s\n' ayse mehmet) 4< <(printf '%s\n' yonetici editor)
# çıktı:
#   ayse: yonetici
#   mehmet: editor

Bir başka yararlı kalıp, yalnızca hata akışını bir süreç ikamesine yönlendirmektir: komut 2> >(işleyici). Böylece standart çıktı olduğu gibi akarken hata satırları ayrı bir işleyiciden geçer; örneğin başlarına etiket eklenir ve ayrı bir dosyada toplanır. Boruyla bunu yapmak için akışların yerini değiştirmeniz gerekirdi, süreç ikamesiyle tek bir yönlendirme yeterli oluyor. Sonuç dosyasını okumadan önce yine wait $! ile işleyicinin bitmesini bekliyoruz.

bash
f() { echo "normal çıktı"; echo "hata mesajı" >&2; }
f 2> >(while read -r s; do echo "[HATA] $s"; done > hatalar.txt)
wait $!
cat hatalar.txt
# çıktı:
#   normal çıktı
#   [HATA] hata mesajı

Üretilen tanımları source ile yüklemek

source (veya .) komutu bir dosya adı alır ve o dosyadaki komutları mevcut kabukta çalıştırır. Süreç ikamesi de bir dosya adı ürettiği için, başka bir komutun ürettiği değişken tanımlarını geçici dosya kullanmadan doğrudan yükleyebilirsiniz. Aşağıda printf bir atama satırı üretiyor ve source bu satırı mevcut kabukta çalıştırıyor; değişken komuttan sonra da kullanılabiliyor.

bash
source <(printf 'ad=%s\n' "Ayşe")
echo "ad: $ad"
# çıktı: ad: Ayşe

Ne zaman hangisi: boru, süreç ikamesi, komut ikamesi

Ne zaman hangisi: boru, süreç ikamesi, komut ikamesi: komut1 | komut2, <(komut), >(komut), done < <(komut)
AraçNe üretirTipik kullanım
komut1 | komut2komut2 için standart girditek akışı aşama aşama işlemek
<(komut)okunabilen bir dosya adıdosya adı bekleyen araca birden fazla girdi vermek
>(komut)yazılabilen bir dosya adıçıktıyı tee ile birkaç hedefe dağıtmak
done < <(komut)döngü için standart girdidöngüdeki değişkenleri korumak
$(komut)komutun çıktısı metin olarakçıktıyı değişkene veya argümana koymak

Komut ikamesi çıktıyı metne çevirip sözcük olarak yerleştirir; süreç ikamesi ise metni değil, ona ulaşan yolu yerleştirir. Büyük çıktılarda bu fark önemlidir, çünkü süreç ikamesi veriyi belleğe toplamaz, akış halinde iletir. Komut ikamesinin kuralları komut ikamesi yazısında anlatılıyor.

Sık Sorulan Sorular

Süreç ikamesi geçici dosya oluşturur mu?

Kılavuz yalnızca adlandırılmış boruların veya /dev/fd yönteminin kullanıldığını söyler. /dev/fd yönteminde diskte dosya oluşmaz; adlandırılmış boru yönteminde ise dosya sisteminde bir boru girişi bulunur ama veri diske yazılmaz. Hangi yöntemin seçildiği sisteme bağlıdır.

Parantez içinde birden fazla komut yazabilir miyim?

Evet. Kılavuzdaki sözdizimi bir komut listesi alır: <(komut1; komut2) iki komutun çıktısını art arda verir. Parantez içi bir alt kabukta çalıştığı için orada atanan değişkenler dışarı çıkmaz.

Parantez içindeki komutun hata mesajları nereye gider?

Süreç ikamesi yalnızca komutun standart çıktısını (<(...) biçiminde) veya standart girdisini (>(...) biçiminde) bağlar. Diğer akışlar kabuktan olduğu gibi devralınır; yani parantez içindeki komutun hata mesajları, ayrıca yönlendirmediğiniz sürece kabuğun standart hatasına, çoğu zaman ekrana düşer. Bunları ayırmak için parantezin içinde 2> dosya gibi bir yönlendirme yazabilirsiniz.

Süreç ikamesi etkileşimli kabukta da çalışır mı?

Evet, sözdizimi aynıdır. diff <(komut1) <(komut2) gibi satırlar etkileşimli kullanımda da sık başvurulan kısayollardır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Bash öğrenme rehberi · Pipe, PIPESTATUS ve pipefail · Komut ikamesi · mapfile ile diziye okuma · Subshell ve çalışma ortamı · Dosya tanımlayıcıları ve exec