Bash'te boru (pipe), | işaretiyle bağlanan komutların her birinin standart çıktısını bir sonrakinin standart girdisine aktarır: komut1 | komut2 | komut3. Borunun çıkış durumu varsayılan olarak yalnızca en sondaki komutun durumudur; aradaki bir komut başarısız olsa bile $? 0 olabilir. Her komutun ayrı ayrı durumunu PIPESTATUS dizisinden okursunuz; set -o pipefail açıldığında ise borunun durumu, sıfırdan farklı bir durumla biten en sağdaki komutun durumu olur. |& hem standart çıktıyı hem standart hatayı boruya verir.
Bu yazı GNU Bash kılavuzunun Pipelines bölümünü ve ona bağlanan PIPESTATUS, pipefail, lastpipe ve time ayrıntılarını çalıştırılmış örneklerle anlatıyor. Ayrıca boru içindeki döngüde değiştirilen değişkenin neden kaybolduğunu ve bunun nasıl çözüleceğini gösteriyoruz. sort, head, grep gibi harici araçları yalnızca örnek olarak kullanıyoruz; bunların seçenekleri için Linux sunucu komutları yazısına bakabilirsiniz.
Boru sözdizimi

Kılavuzdaki genel biçim [time [-p]] [!] komut1 [ | veya |& komut2 ] ... şeklindedir. Tek bir komut da kılavuzun tanımına göre bir borudur, yalnızca içinde bağlantı yoktur. Bağlantılar kurulurken önemli bir ayrıntı vardır: borunun bağlantısı, komut1 için yazılmış yönlendirmelerden önce yapılır. Yani komut1 > dosya | komut2 yazarsanız komut1 önce boruya bağlanır, ardından standart çıktısı dosyaya taşınır ve komut2 hiçbir şey almaz.
printf '%s\n' kiraz elma armut | sort | head -n 2
# çıktı:
# armut
# elmaBorunun her parçası aynı anda çalışmaya başlar; ilk komut veri ürettikçe ikinci komut okur. Kılavuz, boru arka planda çalıştırılmıyorsa kabuğun tüm komutların bitmesini beklediğini belirtir. Bu sayede büyük bir dosyayı birkaç aşamada işleyen bir boru, ara sonuçları diske yazmadan sonuç üretebilir.
Borunun çıkış durumu
Kılavuza göre borunun dönüş durumu, pipefail kapalıyken son komutun çıkış durumudur. Bu tek kural pek çok sessiz hatanın kaynağıdır: veri_uret | sort > rapor.txt satırında veri_uret başarısız olsa bile sort başarıyla bittiği için satırın durumu 0 olur ve betik hiçbir şey olmamış gibi devam eder.
false | true
echo "false | true: $?"
true | false
echo "true | false: $?"
# çıktı:
# false | true: 0
# true | false: 1Bu davranış, if ve && gibi yapılarda da aynıdır. if veri_uret | grep -q hata satırında if yalnızca grep sonucuna bakar. Çoğu durumda istenen de budur; sorun, öndeki komutun başarısızlığının da önemli olduğu yerlerde başlar. Çıkış durumlarının genel anlamı için exit kodları yazısına, && ve || listeleri için ve/veya operatörleri yazısına bakabilirsiniz.
PIPESTATUS ile her komutun durumu
PIPESTATUS, en son çalışan ön plan borusundaki her komutun çıkış durumunu sırayla tutan bir dizidir. İlk komutun durumu ${PIPESTATUS[0]}, ikincisi ${PIPESTATUS[1]} ve böyle devam eder. Kılavuz, Bash'in bu diziyi yalnızca çok elemanlı borulardan sonra değil, basit komutlardan, ( ) ile açılan alt kabuklardan, [[ ve (( bileşik komutlarından sonra da güncellediğini söyler.
( exit 3 ) | ( exit 0 ) | ( exit 5 )
echo "${PIPESTATUS[@]}"
# çıktı: 3 0 5
( exit 3 ) | true
durum=("${PIPESTATUS[@]}")
echo "ilk: ${durum[0]}, son: ${durum[1]}"
# çıktı: ilk: 3, son: 0
false
echo "${PIPESTATUS[@]}"
# çıktı: 1
echo "${PIPESTATUS[@]}"
# çıktı: 0Örneğin son iki satırı önemli bir tuzağı gösteriyor: PIPESTATUS her komuttan sonra yeniden yazılır. İlk echo diziyi okuduktan sonra kendisi de bir basit komut olarak çalıştığı için dizi onun durumuyla, yani 0 ile güncellenir. Değerleri birden fazla yerde kullanacaksanız borunun hemen ardından durum=("${PIPESTATUS[@]}") ile bir kopyasını alın. if gibi bir yapı da arada komut çalıştırır; durumu if satırından önce kaydetmek en güvenli yoldur. Diziyle çalışma için dizi kullanımı yazısına bakabilirsiniz.
set -o pipefail

pipefail seçeneği açıkken borunun dönüş durumu, sıfırdan farklı bir durumla çıkan en sağdaki (son) komutun durumudur; tüm komutlar başarılıysa sıfırdır. Kılavuz bu seçeneğin varsayılan olarak kapalı olduğunu belirtir. Aşağıdaki örnekte seçenek açıldıktan sonra öndeki ( exit 3 ) artık gizlenmiyor. İki komut birden başarısız olduğunda ise sağdakinin durumu, yani 4, döndürülüyor.
( exit 3 ) | true
echo "pipefail kapalı: $?"
set -o pipefail
( exit 3 ) | true
echo "pipefail açık: $?"
( exit 2 ) | ( exit 4 ) | true
echo "en sağdaki hata: $?"
true | true
echo "hepsi başarılı: $?"
# çıktı:
# pipefail kapalı: 0
# pipefail açık: 3
# en sağdaki hata: 4
# hepsi başarılı: 0pipefail çoğunlukla set -e ile birlikte kullanılır. set -e tek başına yalnızca borunun genel durumuna baktığı için, öndeki bir komutun başarısızlığı pipefail olmadan betiği durdurmaz. Bu ikilinin birlikte nasıl davrandığı ve set -e'nin devreye girmediği durumlar set -euo pipefail yazısında anlatılıyor.
! ile sonucu tersine çevirmek
Borunun başına ! ayrılmış sözcüğü yazıldığında çıkış durumu mantıksal olarak tersine çevrilir: başarı başarısızlık, başarısızlık başarı olur. Kılavuz bunun boru durumunun yukarıda anlatılan hesaplamasından sonra uygulandığını söyler; yani pipefail açıksa önce o kurala göre durum bulunur, sonra tersine çevrilir. ! ile sözcük arasında boşluk bırakmanız gerekir. Koşul yazımı için if, elif, else yazısına bakabilirsiniz.
if ! printf 'abc\n' | grep -q x; then
echo "x yok"
fi
# çıktı: x yok
! true | true
echo "durum: $?"
# çıktı: durum: 1Standart hatayı da boruya vermek: |&
Bir boru varsayılan olarak yalnızca standart çıktıyı taşır. Aşağıdaki ilk örnekte f fonksiyonunun hata mesajı borudan geçmiyor, doğrudan ekrana düşüyor; bu yüzden etiketsiz görünüyor ve etiketli satırdan önce basılıyor.
f() { echo "normal çıktı"; echo "hata mesajı" >&2; }
etiketle() { while read -r s; do echo "[boru] $s"; done; }
f | etiketle
# çıktı:
# hata mesajı
# [boru] normal çıktıKılavuza göre |& operatörü 2>&1 | yazımının kısaltmasıdır: komut1'in standart hatası da standart çıktısıyla birlikte boruya bağlanır. Bu örtük yönlendirme, komut1 için yazılmış diğer yönlendirmelerden sonra yapılır. Yalnızca hata akışını boruya vermek istediğinizde ise yönlendirme sırasından yararlanırsınız: 2>&1 > /dev/null önce hata akışını borunun kopyası yapar, sonra standart çıktıyı atar. Sıranın neden önemli olduğu yönlendirme yazısında ayrıntılı olarak anlatılıyor.
f() { echo "normal çıktı"; echo "hata mesajı" >&2; }
etiketle() { while read -r s; do echo "[boru] $s"; done; }
f |& etiketle
# çıktı:
# [boru] normal çıktı
# [boru] hata mesajı
f 2>&1 > /dev/null | etiketle
# çıktı: [boru] hata mesajıBurada kullandığımız etiketle fonksiyonu borunun sağ tarafında da bir fonksiyon çalıştırılabileceğini gösteriyor. Fonksiyonlar, gruplanmış komutlar ve döngüler borunun herhangi bir parçası olabilir. Fonksiyon yazımı için fonksiyon tanımlama yazısına bakabilirsiniz.
Borudaki döngüde değişken kaybı ve lastpipe
Kılavuz, birden fazla komut içeren bir borudaki her komutun kendi alt kabuğunda, yani ayrı bir süreçte çalıştığını açıkça söyler. Bu yüzden borunun sağındaki bir while döngüsü içinde değiştirdiğiniz değişkenler, döngü bitince ana kabuğa geri dönmez. Aşağıdaki örneğin ilk yarısında sayaç 0 kalıyor.
Kılavuz bir çıkış yolu da tanımlar: shopt -s lastpipe açıksa ve iş denetimi (job control) etkin değilse, arka planda çalışmayan bir borunun son komutu mevcut kabukta çalıştırılabilir. Betikler varsayılan olarak iş denetimi olmadan çalıştığı için bu seçenek betiklerde etkilidir; örneğin ikinci yarısında sayaç 3 oluyor. Etkileşimli kabukta ise iş denetimi genellikle açık olduğu için lastpipe tek başına işe yaramaz.
sayac=0
printf '%s\n' a b c | while read -r s; do
sayac=$((sayac + 1))
done
echo "lastpipe kapalı: $sayac"
shopt -s lastpipe
sayac=0
printf '%s\n' a b c | while read -r s; do
sayac=$((sayac + 1))
done
echo "lastpipe açık: $sayac"
# çıktı:
# lastpipe kapalı: 0
# lastpipe açık: 3Aynı sorunun daha yaygın çözümleri de vardır: girdiyi bir dosyadan veya process substitution ile done < <(komut) biçiminde vermek ya da tek satırlık bir girdi için here string kullanmak. Alt kabuğun hangi durumlarda açıldığı ve neyin içeri taşınıp neyin dışarı çıkamadığı subshell ve çalışma ortamı yazısında toplu halde anlatılıyor.
time ile boruyu ölçmek
time bir komut değil, Bash'in ayrılmış bir sözcüğüdür ve borunun başına yazılır. Kılavuza göre boru bittiğinde geçen gerçek süreyi ve kullanıcı ile sistem modunda harcanan işlemci süresini basar. Ayrılmış sözcük olması, yerleşik komutları, fonksiyonları ve tüm boruyu ölçebilmesini sağlar; harici bir time programı bunları kolayca ölçemez. -p seçeneği çıktıyı POSIX biçimine çevirir.
Çıktının biçimini TIMEFORMAT değişkeni belirler. %R geçen süreyi, %U kullanıcı modundaki işlemci saniyesini, %S sistem modundaki işlemci saniyesini, %P işlemci yüzdesini verir. Yüzde işaretinden sonraki isteğe bağlı rakam ondalık hane sayısını seçer; 0 verilirse ondalık kısım hiç basılmaz. Aşağıdaki çok kısa sürdüğü için 0 saniye yazan örnekte bu biçimi görüyorsunuz; kendi sisteminizde daha uzun bir komutla denediğinizde gerçek süreyi görürsünüz. time çıktısını standart hataya yazar.
TIMEFORMAT='geçen süre: %0R saniye'
time printf '%s\n' a b | sort > /dev/null
# çıktı: geçen süre: 0 saniyeSık yapılan hatalar

- Borunun durumunu
$?ile kontrol edip öndeki komutları gözden kaçırmak.pipefailaçın ya daPIPESTATUSokuyun. PIPESTATUSdizisini ikinci kez okumaya çalışmak. Arada çalışan her komut onu yeniden yazar.- Borunun sağındaki döngüde toplanan değişkeni döngüden sonra kullanmak. Döngü alt kabukta çalışır; process substitution veya
lastpipekullanın. - Hata mesajlarının boruya gittiğini varsaymak. Varsayılan boru yalnızca standart çıktıyı taşır;
|&veya2>&1 |gerekir. komut > dosya | diğeryazıpdiğerkomutunun neden boş girdi aldığını merak etmek. Boru bağlantısı önce yapılır, ardından yönlendirme standart çıktıyı dosyaya taşır.
Kısa uygulama: güvenli bir rapor borusu
Günlük dosyasından bir özet çıkaran bir betik düşünün: bir komut veriyi üretiyor, ikincisi süzüyor, üçüncüsü sıralıyor. Betiğin başına set -o pipefail koyarsanız zincirdeki herhangi bir halkanın başarısızlığı borunun sonucuna yansır. Borunun hemen ardından durum=("${PIPESTATUS[@]}") ile durumların kopyasını alıp, örneğin süzme komutunun "eşleşme bulunamadı" anlamındaki 1 değerini hata saymamak gibi araca özgü kararları ayrı ayrı verebilirsiniz. Hata mesajlarını özetten ayrı tutmak istiyorsanız boruya |& yerine yalnızca | verip hata akışını 2>> hata.log ile bir günlüğe yönlendirirsiniz. Bu üç adım, boruların tipik "sessizce başarılı görünme" sorununu büyük ölçüde ortadan kaldırır.
Aşağıdaki küçük örnekte süzme adımı hiçbir satır bulamıyor. grep eşleşme bulamadığında 1 ile çıkar ve pipefail açık olduğu için borunun genel durumu da 1 olur. Durumları hemen kopyaladığımız için hangi halkanın neden 1 döndürdüğünü ayrı ayrı görebiliyor ve yalnızca ikinci komutun 1 değerini kabul edilebilir saymaya karar verebiliyoruz. Aynı kararı genel $? değeriyle vermek mümkün olmazdı, çünkü o değer hangi komuttan geldiğini söylemez.
set -o pipefail
printf '%s\n' "INFO başladı" "INFO bitti" | grep ERROR | sort > hatalar.txt
durum=("${PIPESTATUS[@]}")
echo "durumlar: ${durum[*]}"
if (( durum[1] == 1 )); then
echo "hata satırı yok, sorun değil"
fi
# çıktı:
# durumlar: 0 1 0
# hata satırı yok, sorun değilBurada (( durum[1] == 1 )) ifadesinde dizi elemanına dolar işareti olmadan erişiliyor; aritmetik bağlamda değişken adları doğrudan kullanılabilir. Bu kural aritmetik işlemler yazısında anlatılıyor.
Sık Sorulan Sorular
pipefail her betikte açılmalı mı?
Borunun öndeki komutlarının başarısızlığı sizin için önemliyse evet, çoğu betikte faydalıdır. Ancak erken kapanan okuyucular ve "eşleşme yok" anlamında 1 döndüren araçlar nedeniyle bazı borular yanlışlıkla başarısız sayılabilir. Bu boruları ayrıca ele alın.
PIPESTATUS ile pipefail birlikte kullanılabilir mi?
Evet. pipefail yalnızca borunun genel dönüş durumunu değiştirir; PIPESTATUS her durumda her komutun kendi değerini tutar. Genel karar için $?, ayrıntılı tanı için PIPESTATUS kullanmak iyi bir ayrımdır.
Arka plandaki bir borunun durumu nedir?
Kılavuza göre & ile arka planda başlatılan bir borunun dönüş durumu hemen 0'dır; çünkü kabuk bitmesini beklemez. Gerçek sonucu almak için wait kullanılır. Arka plan işleri jobs, fg ve bg yazısında anlatılıyor.
Boruda hangi komutlar alt kabukta çalışır?
Kılavuza göre boru oluşturulan çok komutlu bir boruda her komut kendi alt kabuğunda çalışır. Bu kural yerleşik komutlar için de geçerlidir: cd, read veya bir değişken ataması borunun içindeyse etkisi o komutun alt kabuğunda kalır. Tek istisna, lastpipe açıkken ve iş denetimi kapalıyken son komutun mevcut kabukta çalışabilmesidir. Tek bir komuttan oluşan "boru" ise alt kabuk açmaz.
Boru mu, process substitution mı?
Bir komutun çıktısını başka bir komutun girdisi yapmak için boru yeterlidir. Komut girdiyi bir dosya adı olarak bekliyorsa, birden fazla girdiyi aynı anda istiyorsa ya da döngüdeki değişkenlerin korunması gerekiyorsa process substitution daha uygun olur.
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 · set -euo pipefail · Process substitution · Subshell ve çalışma ortamı · Yönlendirme: stdout ve stderr · Exit kodları