Bash'te her komut bittiğinde 0 ile 255 arasında bir çıkış durumu (exit status) bırakır ve bu değer hemen ardından $? özel parametresiyle okunabilir. Kural basittir: 0 başarı, 0 dışındaki her değer başarısızlık demektir. Bash kendi özel değerlerini de kullanır: komut bulunamazsa 127, bulunur ama çalıştırılamazsa 126, N numaralı ölümcül bir sinyalle sonlanırsa 128+N. Betiğin kendi çıkış kodunu exit n, bir fonksiyonun dönüş değerini return n belirler.

Bu yazıda $? değerinin ne zaman değiştiğini, kılavuzun tanımladığı özel kodları, 255'ten büyük değerlerin nasıl kırpıldığını, exit ile return farkını ve kendi betiklerinizde anlamlı çıkış kodlarını nasıl tasarlayacağınızı GNU Bash 5.2.37 ile çalıştırılmış örneklerle anlatıyoruz. Bash öğrenme yolunun tamamı için Bash öğrenme rehberi sayfasına bakabilirsiniz.

Neden 0 başarı demek?

Neden 0 başarı demek?

İlk bakışta ters görünse de bu seçimin mantığı kılavuzda açıklanır: başarının tek ve net bir anlamı vardır, başarısızlığın ise pek çok türü olabilir. 0 değeri "her şey yolunda" demek için ayrılır, 1'den 255'e kadar olan değerler farklı hata türlerini ayırt etmek için kullanılır. Bash'in if, while, until komutları ve && / || listeleri karar verirken doğrudan bu durumu kullanır. true yerleşik komutu hiçbir şey yapmadan 0, false ise hiçbir şey yapmadan sıfır dışı bir değer döndürür.

$? her komuttan sonra değişir

$? yalnızca en son çalıştırılan komutun durumunu tutar. Bu yüzden durumu bir kez yazdırmak ya da test etmek bile değeri değiştirir. Aşağıdaki örnekte ikinci echo, false komutunun değil, ilk echo komutunun durumunu görür.

bash
false
echo "ilk okuma: $?"
echo "ikinci okuma: $?"
# çıktı: ilk okuma: 1
# çıktı: ikinci okuma: 0
true
echo "true sonrası: $?"
# çıktı: true sonrası: 0

Durumu birden fazla yerde kullanacaksanız hemen bir değişkene alın: komut; durum=$?. Kılavuz, Bash'in yerleşik komutlarının başarıda 0, başarısızlıkta sıfır dışı değer döndürdüğünü ve yanlış kullanım (geçersiz seçenek, eksik argüman) için genellikle 2 kullandığını da belirtir.

bash
return
echo "return durumu: $?"
read -Q
echo "read durumu: $?"
# çıktı: ornek.sh: line 1: return: can only `return' from a function or sourced script
# çıktı: return durumu: 2
# çıktı: ornek.sh: line 3: read: -Q: invalid option
# çıktı: read: usage: read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]
# çıktı: read durumu: 2

Bash'in özel çıkış kodları: 126, 127 ve 128+N

Kılavuzun 3.7.5 bölümü, kabuğun belirli hata türleri için kullandığı değerleri tanımlar. Komut adı bir fonksiyon, yerleşik komut ya da PATH içinde bir dosya olarak bulunamazsa, komutu çalıştırmak için oluşturulan alt süreç 127 döndürür. Komut bulunur ama çalıştırılamazsa sonuç 126 olur. Aşağıdaki örnekte bir klasörü komut gibi çalıştırmaya çalışıyoruz; yol doğru ama hedef çalıştırılabilir bir program değil.

bash
mkdir -p ./klasor
./klasor
echo "klasör çalıştırma: $?"
olmayan_komut_xyz
echo "bulunamayan komut: $?"
# çıktı: ornek.sh: line 2: ./klasor: Is a directory
# çıktı: klasör çalıştırma: 126
# çıktı: ornek.sh: line 4: olmayan_komut_xyz: command not found
# çıktı: bulunamayan komut: 127

126 kodunu en sık, çalıştırma izni olmayan bir betiği ./betik.sh diye çağırdığınızda görürsünüz; mesaj bu durumda Permission denied olur. İzinlerin nasıl verildiği Linux dosya izinleri ve chmod yazısında, bu ve benzer mesajların çözümleri Bash sık hata mesajları yazısında anlatılıyor.

Bir komut N numaralı ölümcül bir sinyalle sonlanırsa Bash çıkış kodu olarak 128+N kullanır. SIGTERM sinyalinin numarası 15 olduğu için kill ile sonlandırılan bir sürecin durumu 143 olur. kill -l, 128'den büyük bir koda karşılık gelen sinyalin adını yazar.

bash
sleep 5 &
kill -TERM "$!"
wait "$!"
kod=$?
echo "sleep durumu: $kod"
echo "sinyal: $(kill -l "$kod")"
# çıktı: sleep durumu: 143
# çıktı: sinyal: TERM

Ctrl-C ile kesilen bir komut aynı mantıkla genellikle 130 (128+2, SIGINT) değerini bırakır. Sinyallerin kendisi ve betikte nasıl yakalandıkları Bash trap ile sinyal ve temizlik yazısının konusu.

Genişletme veya yönlendirme sırasında bir hata olursa da durum sıfırdan büyük olur. Örneğin var olmayan bir dosyadan girdi yönlendirmek, komut hiç çalışmadan hatayla sonuçlanır.

bash
: < ./yok.txt
echo "yönlendirme hatası: $?"
# çıktı: ornek.sh: line 1: ./yok.txt: No such file or directory
# çıktı: yönlendirme hatası: 1

0-255 aralığı ve 8 bit kırpma

0-255 aralığı ve 8 bit kırpma

Çıkış durumu 0 ile 255 arasında tutulur. return için kılavuz bunu açıkça söyler: verilen sayının yalnızca en anlamsız 8 biti kullanılır. Pratikte bu, değerin 256'ya göre modunun alınması demektir; 256 sıfıra, 300 ise 44'e dönüşür. Negatif değerler de aynı kurala göre aralığın içine sarılır. exit ile denediğimizde de aynı sonuçları aldık. Bu yüzden 255'ten büyük kodlar kullanmayın; özellikle hesaplanan bir sayıyı (örneğin hata adedini) doğrudan çıkış kodu yapmak, 256 hatada "başarı" bildirmek anlamına gelebilir.

bash
( exit 256 ); echo "exit 256 -> $?"
( exit 300 ); echo "exit 300 -> $?"
( exit -1 );  echo "exit -1  -> $?"
f() { return 300; }
f; echo "return 300 -> $?"
# çıktı: exit 256 -> 0
# çıktı: exit 300 -> 44
# çıktı: exit -1  -> 255
# çıktı: return 300 -> 44

Örnekte exit komutlarını parantez içinde çalıştırdık; parantez bir alt kabuk açtığı için exit yalnızca o alt kabuğu kapatır ve betik devam eder. Bu farkın ayrıntısı Bash subshell ve çalışma ortamı yazısında.

exit ve return farkı

exit [n] kabuğu (betik içinde betiğin kendisini) sonlandırır ve n değerini üst sürece iletir. n verilmezse son çalıştırılan komutun durumu kullanılır ve kabuk kapanmadan önce varsa EXIT trap'i çalışır. return [n] ise yalnızca bir fonksiyonu veya source ile okunan bir dosyayı bitirir ve çağırana döner; n verilmezse yine son komutun durumu kullanılır. Fonksiyon dışında ve source edilmemiş bir betikte return kullanmak, yukarıda gördüğünüz gibi 2 durumuyla hata verir.

bash
dosya_var_mi() {
  [[ -e $1 ]]
}
durdur() {
  echo "kritik hata: $1" >&2
  exit 4
}
if dosya_var_mi ./yok.txt; then echo "var"; else echo "yok (dönüş: 1)"; fi
dosya_var_mi ./ornek.sh && echo "betik dosyası var"
durdur "ayar dosyası okunamadı"
echo "buraya gelinmez"
# çıktı: yok (dönüş: 1)
# çıktı: betik dosyası var
# çıktı: kritik hata: ayar dosyası okunamadı
# $? = 4

dosya_var_mi fonksiyonunda return yazılmadı; fonksiyonun durumu, gövdedeki son komut olan [[ ]] testinin durumudur. Bu kalıp, "evet/hayır" cevabı veren fonksiyonlar için en temiz yoldur. Fonksiyondan metin döndürmek ise başka bir iştir: çıkış durumu yalnızca küçük bir sayıdır, metin veya büyük sayı için echo ile yazıp komut ikamesiyle yakalarsınız. Bu ayrım Bash fonksiyon tanımlama yazısında örneklerle anlatılıyor.

Çıkış durumunu doğru kontrol etmek

if komutu bir koşul ifadesi değil, bir komutun çıkış durumunu kontrol eder. Bu yüzden komut; if [ $? -eq 0 ]; then yazmak yerine komutu doğrudan if içine koymak hem daha kısa hem de daha güvenlidir: arada başka bir komut çalışıp $? değerini değiştiremez. Başarısızlık durumunu sınamak için ! kullanılır; kılavuza göre ! bir borunun çıkış durumunu mantıksal olarak tersine çevirir.

bash
if grep -q "kök" <<< "kök dizin"; then
  echo "eşleşme bulundu"
fi
if ! grep -q "yok" <<< "kök dizin"; then
  echo "eşleşme yok"
fi
! true
echo "! true durumu: $?"
# çıktı: eşleşme bulundu
# çıktı: eşleşme yok
# çıktı: ! true durumu: 1

Bir boruda yalnızca son komutun durumu $? değerine yansır. Her elemanın durumunu görmek için PIPESTATUS dizisi, başta kalan hataları yakalamak için set -o pipefail kullanılır; ikisi de Bash pipe, PIPESTATUS ve pipefail yazısında. Koşul yazımının kendisi için Bash if elif else yazısına bakabilirsiniz.

bash
true | false | true
echo "son komut: $?"
true | false | true
echo "tüm boru: ${PIPESTATUS[*]}"
# çıktı: son komut: 0
# çıktı: tüm boru: 0 1 0

Atamaların ve komut ikamesinin durumu

Yalnızca değişken atamasından oluşan bir satırın da bir çıkış durumu vardır. Kılavuzun 3.7.1 bölümüne göre böyle bir satırda komut adı kalmazsa ve genişletmeler arasında bir komut ikamesi varsa, satırın durumu son yapılan komut ikamesinin durumudur; hiç komut ikamesi yoksa durum 0'dır. Bu kural, bir komutun çıktısını alırken başarısını da aynı satırda sınamanızı sağlar. Ancak atama local, export veya declare gibi bir yerleşik komutun argümanı olduğunda durum artık o yerleşik komuta aittir ve içerideki hata kaybolur.

bash
sonuc=$(false)
echo "yalın atama: $?"
sonuc=$(true)
echo "başarılı ikame: $?"
export SONUC=$(false)
echo "export ile: $?"
if surum=$(bash --version); then
  echo "komut çalıştı, çıktı alındı"
fi
# çıktı: yalın atama: 1
# çıktı: başarılı ikame: 0
# çıktı: export ile: 0
# çıktı: komut çalıştı, çıktı alındı

Son örnekteki if degisken=$(komut) kalıbı, çıktıyı değişkene alırken komut başarısızsa farklı bir yola sapmanın kısa yoludur. export satırında görülen kayıp ise set -e kullanan betiklerde sık rastlanan bir tuzaktır; ayrıntısı set -euo pipefail yazısındaki errexit tuzakları bölümünde. Komut ikamesinin kendisi için Bash komut ikamesi yazısına bakabilirsiniz.

Arka plan işlerinin durumu ve wait

Bir komutu & ile arka plana attığınızda kabuk onun bitmesini beklemez; kılavuza göre bu durumda dönüş değeri hemen 0 olur. Yani $? o anda işin sonucunu değil, yalnızca "iş başlatıldı" bilgisini verir. Gerçek sonucu almak için wait yerleşik komutunu kullanırsınız: wait, verilen sürecin bitmesini bekler ve onun çıkış durumunu döndürür. Verilen kimlik kabuğun çocuğu değilse sonuç 127 olur.

bash
( sleep 0.2; exit 7 ) &
echo "& sonrası: $?"
wait "$!"
echo "wait ile alınan: $?"
# çıktı: & sonrası: 0
# çıktı: wait ile alınan: 7

Birden fazla işi paralel başlatıp her birinin sonucunu toplamak istediğinizde, her işin süreç numarasını $! ile bir diziye kaydedip ardından tek tek wait çağırmak yaygın bir kalıptır. İş denetimi, jobs ve %1 gibi iş numaraları Bash arka plan işleri yazısında anlatılıyor.

Kendi betiğiniz için çıkış kodu tasarlamak

Betiğiniz başka bir betikten, bir zamanlanmış görevden veya bir servis yöneticisinden çağrılacaksa, anlamlı çıkış kodları çağıranın hatayı ayırt etmesini sağlar. Kılavuzdaki genel eğilimi izlemek iyi bir başlangıçtır: 0 başarı, 1 genel hata, 2 yanlış kullanım. 126 ve üstü kabuğun kendi anlamları için kullanıldığından, kendi kodlarınızı 125'in altında tutmak çakışmayı önler. Kodları betiğin başında salt okunur sabitler olarak tanımlamak okunabilirliği artırır.

bash
#!/usr/bin/env bash
readonly BASARI=0 KULLANIM_HATASI=2 GIRDI_YOK=3

kullanim() {
  echo "kullanım: $0 DOSYA" >&2
  exit "$KULLANIM_HATASI"
}

[[ $# -eq 1 ]] || kullanim
[[ -r $1 ]] || { echo "okunamıyor: $1" >&2; exit "$GIRDI_YOK"; }
echo "işleniyor: $1"
exit "$BASARI"
# çıktı: kullanım: rapor.sh DOSYA
# $? = 2

Betik argümansız çağrıldığı için kullanım mesajı yazıldı ve 2 ile çıkıldı. Aynı betik okunamayan bir dosyayla çağrılsaydı 3, geçerli bir dosyayla çağrılsaydı 0 dönecekti. Argümanların sayısı ve $0 gibi özel parametreler Bash script argümanları yazısında, seçenek ayrıştırma ve kullanım mesajı kalıbı ise Bash getopts yazısında anlatılıyor.

Çıkış kodları özet tablosu

DeğerAnlamıKaynak
0BaşarıTüm komutlar
1Genel hataÇoğu komut ve yerleşik; genişletme veya yönlendirme hatası
2Yerleşik komutun yanlış kullanımıGeçersiz seçenek, eksik argüman
126Komut bulundu ama çalıştırılamadıİzin yok, hedef bir klasör
127Komut bulunamadıYanlış yazılmış ad, PATH dışında
128+NN numaralı sinyalle sonlandı130 = INT (2), 143 = TERM (15)

Sık yapılan hatalar

Sık yapılan hatalar: $? değerini iki kez okumak., 255'ten büyük kod kullanmak., Fonksiyondan metin döndürmek için return kullanmak., Fonksiyon içinde exit ile return'ü karıştırmak.
  • $? değerini iki kez okumak. İlk echo veya [ ] testinden sonra değer değişir; hemen bir değişkene kaydedin.
  • 255'ten büyük kod kullanmak. Değer 8 bite kırpılır; 256 sıfıra dönüşür.
  • Fonksiyondan metin döndürmek için return kullanmak. return yalnızca durum kodu taşır; metni echo ile yazıp $( ) ile alın.
  • Fonksiyon içinde exit ile return'ü karıştırmak. exit tüm betiği sonlandırır; yalnızca fonksiyondan çıkmak için return kullanın.
  • Hatada da exit 0 ile çıkmak. Çağıran araçlar hatayı göremez.
  • Komutu çalıştırıp sonra if [ $? -eq 0 ] yazmak. Komutu doğrudan if içine koymak daha güvenlidir.

Sık Sorulan Sorular

exit komutunu sayı vermeden yazarsam ne olur?

Kılavuza göre n verilmezse çıkış durumu, son çalıştırılan komutun durumudur. Örneğin son komut başarısız olduysa betik de başarısız olarak biter. Niyetinizi açık göstermek için betiğin sonunda exit 0 veya anlamlı bir sabit yazmanız önerilir.

Betiğin tamamı hangi çıkış koduyla biter?

exit çağrılmadıysa Bash son çalıştırılan komutun durumuyla çıkar. Bir sözdizimi hatası olursa kılavuza göre sıfır dışı bir değerle sonlanır. set -e açıksa başarısız olan komutun durumu betiğin çıkış kodu olur; bu seçenek için Bash set -euo pipefail yazısına bakın.

Sinyalle ölen bir komutu normal hatadan nasıl ayırırım?

Durum 128'den büyükse komutun bir sinyalle sonlanmış olması muhtemeldir; kill -l "$durum" sinyalin adını verir. Ancak bir program kendi isteğiyle de 128'den büyük bir kodla çıkabilir, bu yüzden bu yorum her zaman kesin değildir.

Bir betiğin çıkış kodunu başka bir betikten nasıl okurum?

Çağırdığınız betik de bir komuttur; onu bash ./alt.sh veya ./alt.sh diye çalıştırdıktan hemen sonra $? değeri, alt betiğin exit ile bildirdiği kodu verir. Kodu doğrudan karar için kullanmak daha temizdir: if ./alt.sh; then ...; fi veya farklı kodlara göre dallanmak için ./alt.sh; durum=$? ardından case $durum in 2) ... ;; 3) ... ;; esac. Alt betik kendi içinde hangi kodları kullandığını belgelediğinde bu dallanma güvenilir hâle gelir.

Çıkış kodunu bir değişkende saklamak gerekir mi?

Değeri bir kez ve hemen kullanacaksanız gerekmez. Birden fazla karşılaştırma yapacak, kayıt yazacak veya temizlik sonrası aynı kodla çıkacaksanız durum=$? ile saklamak, aradaki komutların değeri ezmesini önler.

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 · Bash trap · Bash fonksiyonlar · PIPESTATUS · Bash hata mesajları