← blog · 21 Eylül 2026

Bash Betiklerinde Sessiz Hatalar: set -e, pipefail ve Katı Modun Sizi Terk Ettiği Yerler

set -euo pipefail betiğinizi güvenli yapmaz, yalnızca başlatır. Fonksiyon gövdesinde kapanan -e, SIGPIPE ile sahte hata üreten pipefail, boş değişkeni görmeyen -u ve iç içe kabukta kırılan tırnak: her biri için neden ve çare.

Otomasyon betiklerinin en tehlikeli hâli çöken değil, çökmeyen olanıdır. Bir dağıtım betiği ortasındaki adım başarısız olur, sonraki adımlar çalışmaya devam eder, çıkış kodu sıfır döner ve gösterge yeşil kalır. Sorun, gece yedeğinin aslında haftalardır alınmadığı ya da yapılandırmanın yarısının uygulanmadığı fark edildiğinde çıkar. Bu yazı, bash betiklerinin neden bu kadar sessiz bozulduğunu ve bunu nasıl engelleyeceğinizi anlatıyor. Kabuk temellerini bildiğinizi varsayıyorum; odak, deneyimli insanların da düştüğü tuzaklarda.

Üç yaklaşım

Betikleri dayanıklı yapmanın üç aşaması var ve çoğu ekip ilkinde takılı kalıyor.

Hiçbir şey yapmamak. Bash varsayılan olarak her komutu bağımsız kabul eder: biri düşse de bir sonrakine geçer. Etkileşimli kullanımda bu doğru davranıştır, betikte felakettir. Özellikle cd başarısız olup sonraki rm yanlış dizinde çalıştığında.

Katı mod. Betik başına set -euo pipefail yazmak. -e ilk başarısız komutta çıkar, -u tanımsız değişkeni hata sayar, pipefail bir boru hattındaki herhangi bir komutun hatasını hattın hatası yapar. Bu, çabaya oranla en yüksek getiriyi verir, ama bir söz değil bir başlangıçtır. Aşağıda göreceğiniz gibi, üçünün de sizi terk ettiği durumlar vardır ve bilmezseniz katı modun verdiği güven daha tehlikelidir.

Katı mod artı doğrulama. Betiğin bir işi yaptığını iddia etmek yerine, sonucu ölçmek: dosya gerçekten yazıldı mı, servis gerçekten sağlıklı döndü mü, yedek gerçekten geri yüklenebiliyor mu. Betik uzun ömürlü ve önemliyse buraya gelmek zorundasınız.

Kararım şu: yazdığınız her betik ikinci aşamada başlasın; üretim yolunda olanlar üçüncüye çıksın. Şimdi ikinci aşamanın delikleri.

set -e sandığınız yerde çalışmaz

Bash, bir komutun çıkış kodu zaten sınanıyorsa -e kuralını askıya alır. if, while, until koşulları, && ve || zincirlerinin son elemanı hariç her parçası, ! ile olumsuzlanmış komutlar bu kapsamdadır. Buraya kadar makul. Asıl sürpriz, bu askıya almanın fonksiyon gövdesinin tamamına yayılmasıdır:

set -e
hazirla() {
  false          # burada çıkmasını beklersiniz
  echo "devam"   # ama basılır
}
if hazirla; then echo "hazır"; fi

hazirla bir if koşulunda çağrıldığı için gövdesindeki her komut -e korumasından çıkar. Fonksiyonu if içinde çağıran kişi bunu görmez, fonksiyonu yazan kişi de bilmez. Çözüm, fonksiyonların kendi hata yönetimini açıkça yapması: kritik adımı komut || return 1 biçiminde yazın, -e'ye güvenmeyin.

İkinci delik komut ikamesi. yol=$(hesapla) yazdığınızda hesapla düşerse -e tetiklenir, ama local yol=$(hesapla) yazarsanız tetiklenmez: satırın çıkış kodu local komutuna aittir ve local her zaman sıfır döner. Aynı şey export için de geçerli. Bildirimi ve atamayı ayırın:

local yol
yol=$(hesapla)

Bash 4.4 ve sonrasında shopt -s inherit_errexit ile komut ikamesinin içindeki alt kabuğun da -e ayarını devralmasını sağlayabilirsiniz; varsayılan olarak devralmaz.

pipefail ve erken kapanan boru

pipefail doğru bir araçtır, ama boru hattında erken çıkan tüketiciler yüzünden sahte hata üretir. Klasik örnek:

set -o pipefail
printf '%s\n' "$icerik" | grep -q -- "$desen"

grep -q ilk eşleşmede çıkar. printf hâlâ yazıyorsa okuyucusu kapanmış bir boruya yazmaya çalışır, SIGPIPE alır ve 141 ile ölür. pipefail bu 141'i hattın sonucu yapar, koşulunuz yanlış döner ve betik "desen bulunamadı" diye yanlış dala sapar. Küçük girdide asla görmezsiniz; içerik çok satırlıysa ve boru tamponunu aşıyorsa (örneğin binlerce satırlık bir komut çıktısı) ortaya çıkar, yani ancak üretimde.

Çare, tüketicinin erken çıkmasına gerek bırakmamak. Küçük veride üreticiyi bir değişkene alıp grep yerine bash'in kendi desen eşleşmesini kullanın: [[ $icerik == *"$desen"* ]]. Büyük veride grep -q yerine çıktıyı tüketip sonucu sayın, ya da bu tek satır için pipefaili geçici olarak kapatın. Aynı tuzağın başka biçimleri head -n1, awk içinde erken exit, sayfalayıcıya yazan komutlardır.

set -u yalnızca yarım koruma

-u tanımsız değişkende durur, boş değişkende durmaz. Felaket komutlarının çoğu boş değişkenle olur:

rm -rf "$HEDEF"/*

HEDEF boş dize olarak tanımlıysa -u sesini çıkarmaz ve komut kökü süpürür. Yıkıcı her komuttan önce değişkenin dolu ve beklediğiniz biçimde olduğunu ayrıca sınayın:

[[ -n $HEDEF && $HEDEF == /srv/* ]] || { echo "HEDEF hatalı: '$HEDEF'" >&2; exit 1; }

Bu satır sıkıcı görünür; sıkıcı olduğu için yazılmaz; yazılmadığı için haberlere çıkan olaylar olur.

Uyar ve devam et, arızayı gizler

Katı modun dışına çıkan kalıp komut || echo "UYARI: adım başarısız" biçimidir. Yazan kişi bilinçli bir seçim yaptığını sanır, ama iki soruyu sormamıştır: bu adım düşerse sonraki adımlar hâlâ anlamlı mı, ve uyarıyı kim okuyacak. Uyarı bir log satırına düşer, gösterge yeşil kalır, arıza bir sonraki kurbanına kadar saklanır.

Kural basit: bir adım isteğe bağlı değilse, düşünce betik düşmelidir. İsteğe bağlıysa bunu kodda açıkça ifade edin ve uyarıyı çıkış koduna da yansıtın; örneğin betik sonunda "uyarı sayısı sıfırdan büyükse 2 ile çık" gibi. Sıfırla çıkan bir betiğin logunda "UYARI" aramak kimsenin işi değildir.

Sekmeyle ayrılmış alanlar ve read

Kabuk betiklerinde tablo verisi işlerken sekme ayıracı çekicidir, ama read sizi yanıltır. IFS içindeki boşluk sınıfı karakterler (boşluk, sekme, satır sonu) ardışık geldiğinde tek ayıraç sayılır. Üç sütunlu bir satırda ortadaki alan boşsa iki sekme yan yana gelir, read bunu tek ayıraç görür ve üçüncü sütunun değeri ikinciye kayar. Betik hiç hata vermez, sadece yanlış kaydı yanlış etiketle işler.

IFS=$'\t' yazmak da kurtarmaz; sekme hâlâ boşluk sınıfındadır. Boş alan taşıyabilen veride boşluk sınıfından olmayan bir ayıraç seçin (örneğin |) ya da ayrıştırmayı awk -F'\t' gibi alan sayısını koruyan bir araca bırakın. Daha da iyisi, veri yapılıysa JSON üretip jq ile okuyun.

Uzaktaki kabuğa tırnak taşıma

ssh sunucu "..." ya da docker exec kap bash -c '...' gibi iç içe kabuk katmanlarında yorum yazmak, özellikle apostrof içeren kelime yazmak, tek tırnaklı dizeyi erken kapatır. Geri kalan metin dış kabukta yorumlanır, oradaki değişkenler boş genişler ve yukarıdaki rm senaryosu bu kez sizin makinenizde koşar. Kural: iç içe kabuk dizesinin içine yorum yazmayın ve bir iki satırı aşan her şeyi tırnak kaçışıyla değil heredoc ile gönderin:

ssh sunucu 'bash -s' <<'EOF'
set -euo pipefail
cd /var/lib/uygulama
./guncelle.sh
EOF

Sınırlayıcının tırnaklı olması (<<'EOF') yerel kabuğun içeriği genişletmesini engeller; betik karşı tarafa yazdığınız gibi gider.

Alışkanlık hâline getirilecek beş şey

Betiğin başına set -euo pipefail koyun, ama yukarıdaki delikleri bilerek koyun. Hata anında nerede olduğunuzu görmek için bir ERR tuzağı ekleyin; trap 'echo "hata: satır $LINENO" >&2' ERR tek başına saatler kazandırır. Geçici dosyaları mktemp ile açın ve EXIT tuzağında silin. Asılabilecek her dış çağrıyı timeout ile sarın; sonsuza kadar bekleyen bir betik de sessiz arızadır. Ve her betiği shellcheckten geçirin; tırnaksız değişken, sınanmamış cd, yanlış read kullanımı gibi hataların büyük kısmını siz çalıştırmadan bulur.

Bir de kendi korumalarınızı sabote edin. Koşulu tersine çevirin, değişkeni boşaltın ve betiğin kırmızı döndüğünü gözünüzle görün. Sabotaja rağmen yeşil kalan bir koruma hiçbir şeyi korumuyordur.

Ne zaman bash yazmamalı

Betik yüz satırı geçtiyse, JSON ya da YAML üretiyor veya ayrıştırıyorsa, yeniden deneme ve eşzamanlılık gerektiriyorsa, birden fazla kişi bakım yapacaksa bash yanlış araçtır. Kabuk, komutları birbirine bağlamak için mükemmeldir; mantık taşımak için değildir. Bu çizgiyi geçtiğinizde Python ya da Go gibi hata yönetimi dile gömülü bir ortama taşınmak, yukarıdaki tuzakların tamamını bir seferde ortadan kaldırır. Bash betiğine üçüncü if katmanını eklediğiniz an, taşınma zamanının geldiği andır.