set -euo pipefail, Bash betiğinin başına yazılan ve üç ayrı kabuk seçeneğini birlikte açan bir satırdır. -e (errexit) başarısız olan bir komutta betiği durdurur, -u (nounset) tanımlanmamış bir değişkenin genişletilmesini hata sayar, -o pipefail ise bir borunun (pipeline) çıkış durumunu yalnızca son komuta değil, hata veren en sağdaki komuta bağlar. Üçü birlikte, sessizce yanlış çalışmaya devam eden betik yerine sorunu ilk anda görünür kılan bir betik elde etmenizi sağlar.

Ancak bu satır sihirli bir güvenlik kalkanı değildir. GNU Bash kılavuzu, -e seçeneğinin bilerek devre dışı kaldığı bir dizi durumu tek tek sayar ve bu istisnalar bilinmediğinde betik ya beklenmedik yerde durur ya da durması gereken yerde devam eder. Bu yazıda her seçeneğin kılavuzdaki tanımını, istisnalarını, en sık düşülen tuzakları ve önerilen bir başlangıç şablonunu, GNU Bash 5.2.37 ile çalıştırılmış örneklerle anlatıyoruz. Bash'e yeni başlıyorsanız konuların sırası için Bash öğrenme rehberi sayfasına bakabilirsiniz.

set ile seçenek açmak ve kapatmak

set yerleşik komutu iki iş yapar: kabuk seçeneklerini açıp kapatır ve konumsal parametreleri yeniden ayarlar. Bir seçeneği tek harfiyle (set -e) ya da uzun adıyla (set -o errexit) açabilirsiniz; ikisi aynı şeydir. Kapatmak için tire yerine artı işareti kullanılır: set +e veya set +o errexit. pipefail seçeneğinin tek harfli bir kısaltması yoktur, bu yüzden her zaman -o pipefail diye yazılır.

O anda açık olan tek harfli seçenekler $- özel parametresinde durur. Uzun adlı bir seçeneğin durumunu öğrenmek için [[ -o seçenek ]] koşulunu, tüm listeyi görmek için argümansız set -o komutunu kullanabilirsiniz. Kılavuza göre set ile verilebilen tek harfli seçeneklerin hepsi Bash çağrılırken de verilebilir; yani bash -eu betik.sh yazmak, betiğin ilk satırında set -eu çalıştırmakla aynı etkiyi yaratır.

bash
set -euo pipefail
echo "$-"
# çıktı: ehuB
[[ -o pipefail ]] && echo "pipefail açık"
# çıktı: pipefail açık
set +e
echo "$-"
# çıktı: huB

Çıktıdaki h ve B harfleri sizin açtığınız seçenekler değildir: h komut yollarını hatırlayan hashall, B ise süslü parantez genişletmesini açan braceexpand seçeneğidir ve ikisi de varsayılan olarak açıktır. set +e sonrasında e harfinin listeden düştüğünü görüyorsunuz; u ise hâlâ açık.

set -e (errexit): ilk hatada durmak

Bash varsayılan olarak bir komut başarısız olduğunda hiçbir şey yapmaz; hatayı ekrana yazar ve bir sonraki satıra geçer. Bu, etkileşimli kullanımda mantıklıdır ama bir betikte zincirleme hasara yol açabilir. Aşağıdaki betikte cd başarısız olduğu hâlde sonraki satır çalışır; bu satır bir silme ya da kopyalama komutu olsaydı yanlış klasörde çalışmış olurdu.

bash
#!/usr/bin/env bash
cd ./olmayan-klasor
echo "cd başarısız oldu ama betik devam etti"
# çıktı: ornek.sh: line 2: cd: ./olmayan-klasor: No such file or directory
# çıktı: cd başarısız oldu ama betik devam etti

Aynı betiğe set -e eklendiğinde kabuk, sıfırdan farklı bir durumla biten ilk komutta çıkar ve kendi çıkış kodu olarak o komutun durumunu kullanır. Kılavuzdaki tanım geniştir: tek bir basit komut, bir boru, bir komut listesi veya bir bileşik komut sıfır dışı bir değer döndürdüğünde kabuk hemen sonlanır. ERR için bir trap tanımlıysa çıkmadan önce o çalıştırılır.

bash
#!/usr/bin/env bash
set -e
cd ./olmayan-klasor
echo "bu satır çalışmaz"
# çıktı: ornek.sh: line 3: cd: ./olmayan-klasor: No such file or directory
# $? = 1

Çıkış kodlarının anlamı ve $? ile nasıl okunduğu Bash exit kodları yazısında ayrıntılı olarak anlatılıyor. Burada bilmeniz gereken tek şey, Bash için 0 değerinin başarı, diğer her değerin başarısızlık sayılmasıdır.

set -e hangi durumlarda devreye girmez

set -e hangi durumlarda devreye girmez: while veya until sözcüğünden hemen sonra gelen koşul listesindeki komutlar., if ve elif sözcüğünden sonra gelen test bölümündeki komutlar...

Kılavuz, bir komut başarısız olsa bile kabuğun çıkmadığı durumları açıkça listeler. Bu liste ezberlenmeye değer, çünkü errexit ile ilgili şaşırtıcı davranışların neredeyse tamamı buradan çıkar:

  • while veya until sözcüğünden hemen sonra gelen koşul listesindeki komutlar.
  • if ve elif sözcüğünden sonra gelen test bölümündeki komutlar.
  • && veya || ile kurulan bir listedeki komutlar; yalnızca son && ya da || işaretinden sonra gelen komut bunun dışındadır.
  • Bir borudaki son komut dışındaki komutlar (bu kural pipefail seçeneğinin durumuna bağlıdır).
  • Çıkış durumu ! ile tersine çevrilen komutlar.

Kılavuz iki önemli ek kural daha koyar. Birincisi: -e göz ardı edilirken içinde bir komut başarısız olduğu için sıfır dışı değer döndüren bir bileşik komut (alt kabuk hariç) kabuğu sonlandırmaz. İkincisi ve daha şaşırtıcı olanı: bir bileşik komut ya da fonksiyon, -e seçeneğinin yok sayıldığı bir bağlamda çalışıyorsa, gövdesindeki hiçbir komut -e ayarından etkilenmez. Yani bir fonksiyonu if koşulunda çağırdığınızda, fonksiyonun içindeki tüm komutlar için errexit fiilen kapanır.

bash
set -e
kontrol() {
  false
  echo "false'tan sonra devam edildi"
}
if kontrol; then
  echo "if içinde: kontrol başarılı sayıldı"
fi
kontrol
echo "buraya gelinmez"
# çıktı: false'tan sonra devam edildi
# çıktı: if içinde: kontrol başarılı sayıldı
# $? = 1

Örnekte aynı fonksiyon iki kez çağrılıyor. if içinde çağrıldığında false fonksiyonu durdurmaz; fonksiyonun son komutu olan echo başarılı olduğu için fonksiyon 0 döndürür ve if dalı çalışır. Fonksiyon doğrudan çağrıldığında ise false satırında betik sonlanır. Aynı kodun bağlama göre farklı davranması, errexit'e tek başına güvenmemeniz gerektiğinin en açık göstergesidir. Önemli bir kontrol yapan fonksiyonlarda hatayı açıkça return ile bildirmek daha güvenlidir; fonksiyon yazımı için Bash fonksiyon tanımlama yazısına bakabilirsiniz.

Sık görülen errexit tuzakları

Sık görülen errexit tuzakları: Fonksiyonun son satırındaki && listesi, Sıfırdan başlayan sayaçta ((sayac++)), local ve export komut ikamesinin hatasını gizler, Komut ikamesinin...

Fonksiyonun son satırındaki && listesi

[[ koşul ]] && komut biçimi kısa bir "eğer" gibi kullanılır. Üst düzeyde, koşul yanlış olduğunda bu satır betiği durdurmaz, çünkü başarısız olan komut && listesinin son elemanı değildir. Fakat aynı satır bir fonksiyonun son komutu olduğunda listenin durumu, yani 1, fonksiyonun dönüş değeri olur. Fonksiyonu çağıran satır artık errexit istisnası değildir ve betik orada sonlanır.

bash
set -e
ayar_yukle() {
  [[ -f ./ayar.conf ]] && echo "ayar yüklendi"
}
[[ -f ./ayar.conf ]] && echo "ayar bulundu"
echo "üst düzeydeki && betiği durdurmadı"
ayar_yukle
echo "buraya gelinmez"
# çıktı: üst düzeydeki && betiği durdurmadı
# $? = 1

Çözüm, kısa yazım yerine if [[ -f ./ayar.conf ]]; then ...; fi kullanmaktır. Kılavuza göre hiçbir koşul doğru çıkmadığında if komutunun durumu 0'dır, bu yüzden dosya yoksa fonksiyon başarıyla döner. Bu ayrım, && ve || listelerinin genel davranışıyla birlikte Bash ve/veya operatörleri yazısında da işleniyor.

Sıfırdan başlayan sayaçta ((sayac++))

(( ifade )) aritmetik komutu, ifadenin değeri sıfır olduğunda 1, sıfır dışı olduğunda 0 durumuyla biter. Son ek artırma sayac++ önce eski değeri, yani 0'ı döndürdüğü için ilk artırmada komut başarısız sayılır ve set -e betiği durdurur.

bash
set -e
sayac=0
(( sayac++ ))
echo "buraya gelinmez"
# $? = 1
bash
set -e
sayac=0
(( ++sayac ))
sayac=$(( sayac + 1 ))
echo "sayac = $sayac"
# çıktı: sayac = 2

Ön ek artırma ++sayac yeni değeri (1) döndürdüğü için sorun çıkarmaz; sayac=$(( sayac + 1 )) ise bir atama olduğundan her zaman 0 durumuyla biter. Aritmetik işlemlerin ayrıntısı Bash aritmetik işlemler yazısında.

local ve export komut ikamesinin hatasını gizler

local degisken=$(komut) satırında çıkış durumunu belirleyen komut değil, local yerleşik komutunun kendisidir. local geçerli bir adla çağrıldığında 0 döndürdüğü için içerideki hata kaybolur. Bildirimi ve atamayı iki satıra ayırdığınızda atamanın durumu komut ikamesinin durumuna eşit olur ve errexit yeniden çalışır.

bash
set -e
yukle() {
  local icerik=$(false)
  echo "local, false'un hatasını gizledi"
  local veri
  veri=$(false)
  echo "buraya gelinmez"
}
yukle
# çıktı: local, false'un hatasını gizledi
# $? = 1

Komut ikamesinin içinde errexit kapalıdır

Kılavuza göre POSIX modunda değilken Bash, komut ikamesi için açtığı alt kabukta -e seçeneğini temizler. Yani $( ... ) içindeki başarısız bir komut, ikamenin geri kalanını durdurmaz. Bu davranışı değiştirmek için shopt -s inherit_errexit kullanılır; bu seçenek açıkken alt kabuk errexit değerini üst kabuktan devralır. POSIX modu açıldığında bu seçenek kendiliğinden açılır.

bash
set -e
x=$(false; echo "alt kabuk devam etti")
echo "x = $x"
shopt -s inherit_errexit
y=$(false; echo "bu satır yazılmaz")
echo "buraya gelinmez"
# çıktı: x = alt kabuk devam etti
# $? = 1

İkinci atamada alt kabuk false satırında 1 ile çıkar; atamanın durumu da bu olur ve üst kabuk errexit nedeniyle sonlanır. Komut ikamesinin diğer özellikleri için Bash komut ikamesi, shopt seçeneklerinin tamamı için Bash shopt seçenekleri yazısına bakabilirsiniz.

set -u (nounset): tanımsız değişkeni yakalamak

Bash, hiç atanmamış bir değişkeni sessizce boş metin olarak genişletir. Değişken adında yapılan tek harflik bir yazım hatası bu yüzden fark edilmez ve "$hedef/" gibi bir yol beklenmedik biçimde kök dizine dönüşebilir. set -u açıkken tanımsız bir parametrenin genişletilmesi hata sayılır: kabuk standart hataya bir mesaj yazar ve etkileşimli olmayan kabuk (yani betik) sonlanır.

bash
set -u
hedef="./yedek"
echo "Hedef: $hedf"
echo "buraya gelinmez"
# çıktı: ornek.sh: line 3: hedf: unbound variable
# $? = 1

Kılavuz bu kuralın bazı istisnalarını belirtir: @ ve * özel parametreleri ile @ ya da * alt simgesiyle kullanılan diziler hata vermez. Bu sayede argümansız çalışan bir betikte "$@" veya boş bir dizide "${dizi[@]}" kullanmak güvenlidir. İsteğe bağlı bir değer beklediğiniz yerde ise varsayılan değer genişletmesiyle değişkenin tanımsız olabileceğini kabuğa açıkça bildirirsiniz.

bash
set -u
bos=()
echo "argüman sayısı: $#, hepsi: [$*]"
echo "dizi: [${bos[@]}]"
echo "ilk argüman: ${1:-verilmedi}"
echo "ortam: ${YEDEK_DIZIN-tanımsız}"
# çıktı: argüman sayısı: 0, hepsi: []
# çıktı: dizi: []
# çıktı: ilk argüman: verilmedi
# çıktı: ortam: tanımsız

${1:-verilmedi} ile ${YEDEK_DIZIN-tanımsız} arasındaki iki nokta farkı (boş değerin de varsayılana düşüp düşmediği) ve ${v:?mesaj} ile zorunlu değişken tanımlama Bash parametre genişletme ve varsayılan değer yazısında anlatılıyor. set -u kullanan bir betikte bu genişletmeler, "bu değişken bilerek boş olabilir" demenin standart yoludur.

set -o pipefail: borudaki hatayı kaybetmemek

Kılavuza göre bir borunun çıkış durumu, normalde borudaki son komutun durumudur. komut1 | komut2 yazdığınızda komut1 başarısız olsa bile komut2 başarılıysa tüm boru başarılı sayılır; bu yüzden set -e tek başına borunun başındaki hatayı yakalayamaz. pipefail açıkken borunun durumu, sıfır dışı değerle biten en sağdaki komutun durumudur; tüm komutlar başarılıysa sonuç 0'dır. Seçenek varsayılan olarak kapalıdır.

bash
( exit 3 ) | true
echo "pipefail kapalı: $?"
set -o pipefail
( exit 3 ) | true
echo "pipefail açık: $?"
( exit 2 ) | ( exit 5 ) | true
echo "en sağdaki hatalı komut: $?"
# çıktı: pipefail kapalı: 0
# çıktı: pipefail açık: 3
# çıktı: en sağdaki hatalı komut: 5

Son satırda iki komut başarısız olduğu hâlde sonuç 5'tir, çünkü kural "ilk hata" değil "en sağdaki hata"dır. Borudaki her komutun durumunu ayrı ayrı görmek isterseniz PIPESTATUS dizisine bakmanız gerekir; bu dizi ve lastpipe seçeneği Bash pipe, PIPESTATUS ve pipefail yazısında ele alınıyor.

Üç seçeneğin karşılaştırması

Üç seçeneğin karşılaştırması: errexit, nounset, -o pipefail, errtrace
SeçenekUzun adıNe yaparDikkat edilecek nokta
-eerrexitBaşarısız komutta kabuğu sonlandırırif, while, until, && ve || listeleri ve ! ile tersine çevrilen komutlarda devre dışı
-unounsetTanımsız değişken genişletmesini hata sayar$@, $* ve [@] ile kullanılan diziler istisna; isteğe bağlı değerler için ${v:-} kullanın
-o pipefailpipefailBorunun durumunu hata veren en sağdaki komuta bağlarKısaltması yok; erken kapanan okuyucularda beklenmedik hata verebilir
-EerrtraceERR trap'ini fonksiyonlara ve alt kabuklara aktarırAçılmazsa fonksiyon içindeki hatalarda ERR trap'i çalışmaz

Önerilen başlangıç şablonu

Aşağıdaki betik, üç seçeneği bir ERR trap'i ile birleştirir ve başarısız olması normal olan komutların nasıl işaretleneceğini gösterir. Kılavuz, ERR trap'inin errexit ile tamamen aynı istisna koşullarına uyduğunu belirtir; yani trap tam olarak kabuğun çıkacağı durumlarda tetiklenir ve çıkmadan önce hangi satırda hata olduğunu yazabilir.

bash
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "HATA: satır $LINENO, çıkış kodu $?" >&2' ERR

hedef="${1:-./cikti}"
mkdir -p -- "$hedef"
echo "rapor" > "$hedef/rapor.txt"

# Başarısız olabileceğini bildiğiniz komutlar:
rm -f -- "$hedef/eski.txt"
if ! cat -- "$hedef/istege-bagli.txt" 2>/dev/null; then
  echo "isteğe bağlı dosya yok, devam"
fi
grep -q "bulunmayan" "$hedef/rapor.txt" || true

echo "bitti"
cat -- "$hedef/kayip.txt"
echo "buraya gelinmez"
# çıktı: isteğe bağlı dosya yok, devam
# çıktı: bitti
# çıktı: cat: ./cikti/kayip.txt: No such file or directory
# çıktı: HATA: satır 17, çıkış kodu 1
# $? = 1

Şablondaki üç kalıp, errexit ile yaşamanın temel araçlarıdır. if ! komut başarısızlığı bir koşul olarak ele alır ve kendi dalınızda işlem yapmanıza izin verir. komut || true "bu komutun durumu önemsiz" demenin en kısa yoludur. rm -f gibi bazı araçlar ise bu durumu kendi seçenekleriyle zaten ele alır. Trap kullanımı, geçici dosya temizliği ve sinyaller için Bash trap ile sinyal ve temizlik yazısını okuyabilirsiniz.

Sık yapılan hatalar

  • set -e'yi hata yönetiminin tamamı sanmak. Kılavuzdaki istisnalar nedeniyle kritik adımlarda durumu yine if veya || ile açıkça kontrol etmeniz gerekir.
  • Kontrol fonksiyonlarını if içinde çağırıp errexit beklemek. Fonksiyon if, while veya && bağlamında çağrıldığında gövdesindeki tüm komutlar için -e etkisizdir.
  • local x=$(komut) yazmak. Bildirimi ve atamayı ayırmazsanız komutun hatası kaybolur.
  • Sayaçta (( i++ )) kullanmak. Değer 0 iken komut başarısız sayılır; (( ++i )) veya i=$(( i + 1 )) yazın.
  • set -u ile birlikte $1 kullanmak. Argüman zorunlu değilse ${1:-} yazın; zorunluysa açık bir kullanım mesajı verin.
  • pipefail'i unutup yalnızca set -e kullanmak. Boru başındaki hata, son komut başarılı olduğu sürece görünmez kalır.

Sık Sorulan Sorular

set -euo pipefail betiğin neresine yazılmalı?

Genellikle shebang satırının hemen altına yazılır, böylece betiğin tamamı bu seçeneklerle çalışır. Seçenekleri betiğin bir bölümünde kapatıp sonra yeniden açmak da mümkündür (set +e ... set -e), ama bu bölümlerde hata kontrolünü elle yapmanız gerekir. Betik dosyasının yapısı için Bash script yazma ve çalıştırma yazısına bakın.

Seçenekler alt kabuklara geçer mi?

Kılavuza göre -e seçeneği kabuk ortamına ve her alt kabuk ortamına ayrı ayrı uygulanır; bir alt kabuk, içindeki komut başarısız olduğunda kendi işini tamamlamadan çıkabilir. Komut ikamesi bunun istisnasıdır: POSIX modu dışında errexit orada temizlenir, inherit_errexit bunu değiştirir. Alt kabukların genel davranışı Bash subshell ve çalışma ortamı yazısında.

Etkileşimli terminalde set -e açmak sorun çıkarır mı?

Evet. Kılavuzdaki tanım etkileşimli kabuk için de geçerlidir; terminalde set -e açtıktan sonra başarısız olan ilk komut kabuğu kapatır ve oturumunuz sona erer. Bu seçenekleri betiklerde kullanın, .bashrc gibi başlangıç dosyalarına eklemeyin.

set -u ile boş dizi kullanmak güvenli mi?

"${dizi[@]}" ve "${dizi[*]}" biçimleri kılavuzda nounset istisnası olarak geçer ve Bash 5.2.37 ile boş dizide hata vermediğini yukarıdaki örnekte gördünüz. Ancak ${dizi[0]} gibi belirli bir elemana erişim, eleman yoksa hata verir. Diziler için Bash dizi kullanımı yazısına bakabilirsiniz.

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 · Bash trap · Bash exit kodları · PIPESTATUS ve pipefail · Bash hata ayıklama · Varsayılan değer genişletmesi