trap, Bash'in bir sinyal geldiğinde ya da belirli bir kabuk olayı yaşandığında çalıştıracağı komutu tanımlayan yerleşik komuttur. Biçimi trap 'komut' SİNYAL... şeklindedir. En yaygın kullanım, betik hangi yoldan biterse bitsin geçici dosyaları silmek için trap temizle EXIT yazmaktır. Gerçek sinyallerin (INT, TERM, HUP) yanında Bash dört sahte sinyal daha tanır: EXIT, ERR, DEBUG ve RETURN.

Bu yazıda trap'in kılavuzdaki kurallarını, işleyici komutun ne zaman genişletildiğini, sinyallerin bekleyen komutlarla nasıl etkileştiğini, temizlik kalıplarını ve alt kabuklarda trap'lerin neden sıfırlandığını GNU Bash 5.2.37 ile denenmiş örneklerle anlatıyoruz. Konunun Bash öğrenme sırasındaki yerini görmek için Bash öğrenme rehberi sayfasına bakabilirsiniz.

trap sözdizimi ve temel seçenekler

trap sözdizimi ve temel seçenekler

Kılavuzdaki tam biçim trap [-lpP] [eylem] [sinyal ...] şeklindedir. eylem, sinyal geldiğinde okunup çalıştırılacak komut metnidir. Sinyaller adıyla ya da numarasıyla yazılabilir; adlarda büyük küçük harf fark etmez ve SIG öneki isteğe bağlıdır, yani INT, int ve SIGINT aynı sinyali gösterir. Eylem yerine - verirseniz (veya tek bir sinyal verip eylemi hiç yazmazsanız) sinyalin davranışı kabuk başladığındaki hâline döner. Eylem boş bir metinse (trap '' INT) sinyal hem kabuk hem de kabuğun başlattığı komutlar tarafından yok sayılır.

Argümansız trap veya trap -p, tanımlı trap'leri yeniden girdi olarak kullanılabilecek trap -- ... satırları biçiminde yazar. trap -l sinyal adlarını numaralarıyla listeler. Bir sinyalin numarasını öğrenmenin kısa yolu kill -l komutudur: ad verirseniz numarayı, 128'den büyük bir çıkış kodu verirseniz o kodu üreten sinyalin adını yazar.

bash
kill -l INT TERM HUP
# çıktı: 2
# çıktı: 15
# çıktı: 1
kill -l 130
# çıktı: INT

130 değerinin INT çıkması tesadüf değildir: kılavuza göre ölümcül bir sinyalle (numarası N) sonlanan komutun çıkış kodu 128+N olur. Bu kuralın ayrıntısı Bash exit kodları ve çıkış durumu yazısında.

EXIT trap'i ile geçici dosya temizliği

EXIT (veya 0) için tanımlanan eylem, kabuk sonlanırken çalışır. Kılavuz, exit yerleşik komutunun da kabuk kapanmadan önce bu trap'i çalıştırdığını belirtir. Bu sayede betik normal biterken, exit ile erken çıkarken ya da set -e nedeniyle bir hatada dururken temizlik kodu tek bir yerde kalır. Aşağıdaki örnek, geçici bir klasörü bulunduğu dizinin altında açar ve çıkışta siler.

bash
#!/usr/bin/env bash
set -euo pipefail

temizle() {
  if [[ -n ${gecici:-} && -d $gecici ]]; then
    rm -r -- "$gecici"
    echo "geçici klasör silindi"
  fi
}
trap temizle EXIT

gecici=$(mktemp -d ./is.XXXXXX)
echo "ara sonuç" > "$gecici/veri.txt"
echo "işlem tamam, satır sayısı: $(wc -l < "$gecici/veri.txt")"
false
echo "buraya gelinmez"
# çıktı: işlem tamam, satır sayısı: 1
# çıktı: geçici klasör silindi
# $? = 1

Betik false satırında errexit nedeniyle durmasına rağmen temizlik çalıştı ve kabuğun çıkış kodu 1 olarak korundu. Temizlik fonksiyonundaki ${gecici:-} kontrolü önemlidir: trap, mktemp satırından önce tanımlandığı için betik o satıra gelmeden biterse değişken hiç atanmamış olur. Boş ya da tanımsız bir değişkenle silme komutu çalıştırmak tehlikelidir; bu yüzden önce değerin dolu ve gerçekten bir klasör olduğundan emin olun.

EXIT trap'i içinde $?, kabuğun çıkmak üzere olduğu durumu verir. Bu değeri bir değişkene alıp kayıt yazabilir, sonra trap bittiğinde kabuk aynı durumla çıkar.

bash
trap 'echo "çıkış kodu: $?"' EXIT
echo "çalışıyor"
exit 3
# çıktı: çalışıyor
# çıktı: çıkış kodu: 3
# $? = 3

Tek tırnak mı çift tırnak mı: eylem ne zaman genişletilir

Trap eylemi bir metindir ve sinyal geldiğinde yeniden okunup çalıştırılır. Eylemi çift tırnakla yazarsanız içindeki değişkenler trap komutunun çalıştığı anda genişletilir ve trap'e sabit bir metin kaydedilir. Tek tırnakla yazarsanız metin olduğu gibi saklanır, değişkenler ancak trap tetiklendiğinde genişletilir. trap -p çıktısı bu farkı doğrudan gösterir.

bash
ad="ilk"
trap "echo çift tırnak: $ad" USR1
trap 'echo tek tırnak: $ad' USR2
ad="son"
trap -p USR1 USR2
# çıktı: trap -- 'echo çift tırnak: ilk' SIGUSR1
# çıktı: trap -- 'echo tek tırnak: $ad' SIGUSR2

Genel kural: trap tetiklendiği andaki değeri görmek istiyorsanız (örneğin $LINENO, $? veya sonradan atanan bir geçici dosya adı) tek tırnak kullanın. Uzun eylemler için en temiz yol, eylemi bir fonksiyona taşıyıp trap'e yalnızca fonksiyon adını vermektir. Tırnak kurallarının kendisi Bash tek tırnak çift tırnak farkı yazısında anlatılıyor.

Sinyal yakalamak: TERM ve INT

Sinyal yakalamak: TERM ve INT

Sinyal trap'lerinde bilmeniz gereken en önemli kural kılavuzun 3.7.6 bölümünde yer alır: Bash bir komutun bitmesini beklerken trap tanımlı bir sinyal alırsa, trap'i o komut tamamlanana kadar çalıştırmaz. Yani ön planda uzun süren bir komut varken gelen TERM, ancak o komut bittiğinde işlenir. Buna karşılık kabuk arka plandaki bir işi wait ile bekliyorsa, trap tanımlı sinyal geldiğinde wait hemen 128'den büyük bir durumla döner ve trap hemen ardından çalışır. Hızlı tepki vermesi gereken döngülerde komut & ve wait $! kalıbı bu yüzden kullanılır.

Aşağıdaki iki dosyadan cocuk.sh, TERM için bir işleyici kurar ve bekleme döngüsüne girer. ana.sh onu arka planda başlatır, hazır olmasını bekler, sinyali gönderir ve çıkış kodunu okur. Sinyal yalnızca örneğin kendi başlattığı işe gönderiliyor.

bash
#!/usr/bin/env bash
# cocuk.sh: TERM gelince temizleyip çıkar
trap 'echo "çocuk: TERM alındı, temizleyip çıkıyorum"; exit 143' TERM
echo "hazır" > ./hazir
while :; do
  sleep 1 &
  wait $!
done
bash
#!/usr/bin/env bash
# ana.sh
bash ./cocuk.sh &
cocuk=$!
until [[ -e ./hazir ]]; do sleep 0.1; done
kill -TERM "$cocuk"
wait "$cocuk"
echo "ana: çocuğun çıkış kodu $?"
rm -f ./hazir
# çıktı: çocuk: TERM alındı, temizleyip çıkıyorum
# çıktı: ana: çocuğun çıkış kodu 143

İşleyicinin sonundaki exit 143 bilinçli bir seçimdir. Trap eylemi çıkmazsa kabuk sinyali "ele alınmış" sayar ve kaldığı yerden devam eder; TERM için trap kurup exit yazmayı unutan bir betik, bu sinyalle durdurulamaz hâle gelir. 143 değeri ise 128+15 kuralına uyarak çağıran tarafa "TERM ile sonlandım" bilgisini korur.

INT (genellikle Ctrl-C ile üretilir) için kılavuz ek ayrıntılar verir. İş denetimi (job control) kapalıyken, yani betiklerde varsayılan durumda, arka plana atılan asenkron komutlar SIGINT ve SIGQUIT sinyallerini yok sayar. Ön planda bir komut çalışırken Ctrl-C'ye basıldığında sinyal hem komuta hem kabuğa gider; komut bu sinyalle ölürse Bash da sinyali kendisi almış gibi davranır, trap'i çalıştırır veya çıkar. Komut sinyali kendisi işleyip ölmezse Bash da bunu ölümcül saymaz, ama tanımlı bir trap varsa yine çalıştırır. Etkileşimli kabukta ise trap yoksa Bash SIGTERM'ü yok sayar ve SIGINT geldiğinde çalışan döngülerden çıkar.

EXIT ile sinyal işleyicisini birlikte kullanmak

Sık görülen bir kalıp trap temizle EXIT INT TERM yazmaktır. Bunun iki sorunu vardır: temizle çıkış yapmıyorsa sinyal geldiğinde temizlik çalışır ama betik devam eder; çıkış yapıyorsa bu kez exit EXIT trap'ini tetikler ve temizlik ikinci kez çalışır. Daha öngörülebilir yöntem, temizliği yalnızca EXIT'e bağlamak, sinyal işleyicisinde ise sadece uygun kodla çıkmaktır. Böylece temizlik her durumda bir kez çalışır.

bash
#!/usr/bin/env bash
# cocuk2.sh: EXIT temizliği ve sinyal işleyicisi ayrı
temizle() { echo "çocuk: temizlik yapıldı"; }
trap temizle EXIT
trap 'echo "çocuk: TERM geldi"; exit 143' TERM
echo "hazır" > ./hazir
while :; do sleep 1 & wait $!; done
bash
#!/usr/bin/env bash
# ana.sh
bash ./cocuk2.sh &
cocuk=$!
until [[ -e ./hazir ]]; do sleep 0.1; done
kill -TERM "$cocuk"
wait "$cocuk"
echo "ana: çıkış kodu $?"
rm -f ./hazir
# çıktı: çocuk: TERM geldi
# çıktı: çocuk: temizlik yapıldı
# çıktı: ana: çıkış kodu 143

Sinyali yok saymak ve varsayılana döndürmek

Kritik bir bölümün (örneğin bir dosyayı yerine taşıma işleminin) yarıda kesilmesini istemiyorsanız sinyali geçici olarak yok sayabilir, bölüm bitince varsayılan davranışa döndürebilirsiniz. Kılavuz bir sınırlamayı da not eder: etkileşimli olmayan bir kabuk, başlarken zaten yok sayılan sinyalleri yakalayamaz ve sıfırlayamaz. Yani betiğinizi sinyali yok sayan bir ortam başlattıysa, betik içinden o sinyale trap kurmak işe yaramaz.

bash
trap '' INT
trap -p INT
# çıktı: trap -- '' SIGINT
mv_bolumu="kritik bölüm burada çalışır"
echo "$mv_bolumu"
# çıktı: kritik bölüm burada çalışır
trap - INT
trap -p INT
echo "INT varsayılana döndü"
# çıktı: INT varsayılana döndü

ERR, RETURN ve DEBUG sahte sinyalleri

ERR trap'i, bir boru, liste veya bileşik komut sıfır dışı bir durumla bittiğinde çalışır. Kılavuz bu trap için errexit ile aynı istisnaları sayar: if, elif, while ve until koşulları, son eleman dışındaki && / || komutları, borunun son komutu dışındakiler ve ! ile tersine çevrilen komutlar trap'i tetiklemez. Ayrıca ERR trap'i varsayılan olarak fonksiyonlara, komut ikamelerine ve alt kabuklara aktarılmaz; bunun için set -E (errtrace) açılmalıdır.

bash
trap 'echo "ERR: satır $LINENO, komut: $BASH_COMMAND, kod: $?"' ERR
f() {
  false
  echo "f devam etti"
}
f
set -E
f
# çıktı: f devam etti
# çıktı: ERR: satır 3, komut: false, kod: 1
# çıktı: f devam etti

İlk çağrıda fonksiyon içindeki false trap'i tetiklemedi, çünkü trap fonksiyona aktarılmıyordu. set -E sonrasında ise aynı satır yakalandı. BASH_COMMAND değişkeni, trap çalışırken o anda yürütülen komutun metnini verir. RETURN trap'i bir fonksiyon ya da source ile okunan bir dosya bittiğinde çalışır; DEBUG trap'i ise her basit komuttan, for, case, (( )) ve [[ ]] komutlarından önce tetiklenir. Kılavuza göre bu iki trap de fonksiyonlara varsayılan olarak aktarılmaz; fonksiyona declare -t ile trace niteliği vermek ya da set -T (functrace) açmak gerekir.

bash
yukle() {
  trap 'echo "yukle bitti"' RETURN
  echo "yukle çalışıyor"
}
yukle
# çıktı: yukle çalışıyor
# çıktı: yukle bitti

DEBUG trap'i, adım adım izleme ve hata ayıklama araçlarının temelidir; BASH_COMMAND ve LINENO ile birlikte kullanımı Bash script hata ayıklama ve set -x yazısında ayrıntılı olarak gösteriliyor.

Alt kabuklarda trap'ler sıfırlanır

Kılavuz, kabuğun yakaladığı (yok saymadığı) sinyallerin bir alt kabuk veya alt kabuk ortamı oluşturulduğunda özgün değerlerine döndüğünü belirtir. ( ... ) grupları, komut ikamesi, boru elemanları ve & ile başlatılan işler bu kapsamdadır. Bir istisna vardır: trap -p ve trap -P, alt kabukta trap değiştirilmeden önce kullanılırsa üst kabuğun trap'lerini gösterebilir; bu sayede eski=$(trap -p INT) ile mevcut işleyiciyi kaydedip daha sonra geri yükleyebilirsiniz. Yok sayılan sinyaller ise alt kabukta ve başlatılan programlarda da yok sayılmaya devam eder. Alt kabuk kavramının tamamı için Bash subshell ve çalışma ortamı yazısına bakın.

bash
trap 'echo "ana kabuk çıkıyor"' EXIT
kayit=$(trap -p EXIT)
echo "kaydedilen: $kayit"
( echo "alt kabuk bitti" )
# çıktı: kaydedilen: trap -- 'echo "ana kabuk çıkıyor"' EXIT
# çıktı: alt kabuk bitti
# çıktı: ana kabuk çıkıyor

Trap türleri özet tablosu

HedefNe zaman çalışırNot
EXIT veya 0Kabuk sonlanırkenexit, betik sonu ve errexit ile çıkışta çalışır
ERRKomut sıfır dışı durumla bitinceset -e ile aynı istisnalar; fonksiyonlar için set -E
RETURNFonksiyon veya source edilen dosya bitinceFonksiyonlara aktarım için set -T veya declare -t
DEBUGHer basit komuttan ve bazı bileşik komutlardan önceHata ayıklama ve izleme için
INT, TERM, HUP...İlgili sinyal geldiğindeÖn plandaki komut bitene kadar ertelenir; wait hemen döner

Sık yapılan hatalar

Sık yapılan hatalar: Değişkenli eylemi çift tırnakla yazmak., TERM işleyicisinde exit yazmamak., Temizliği hem EXIT hem sinyallere bağlamak., Uzun bir ön plan komutu sırasında...
  • Değişkenli eylemi çift tırnakla yazmak. Değer trap kurulurken sabitlenir; geç atanan dosya adları ve $? yanlış çıkar.
  • TERM işleyicisinde exit yazmamak. Betik sinyali yutar ve çalışmaya devam eder.
  • Temizliği hem EXIT hem sinyallere bağlamak. Temizlik iki kez çalışabilir; temizliği EXIT'te tutun, sinyal işleyicisinde yalnızca çıkın.
  • Uzun bir ön plan komutu sırasında anında tepki beklemek. Trap komut bitene kadar ertelenir; gerekiyorsa komutu arka planda başlatıp wait kullanın.
  • Fonksiyon içindeki hataların ERR trap'ine düşmesini beklemek. set -E olmadan düşmez.
  • Silme komutunu kontrol etmeden çalıştırmak. Trap çok erken tetiklenebilir; değişken henüz boş olabilir.

Sık Sorulan Sorular

Bir sinyal için birden fazla trap tanımlanabilir mi?

Hayır, her sinyal için tek bir eylem saklanır; aynı sinyale ikinci kez trap kurmak öncekini değiştirir. Birden fazla iş yapmak istiyorsanız bunları tek bir fonksiyonda toplayın ya da eylemi ; ile birleştirin.

trap ile kill komutunun ilişkisi nedir?

kill bir sürece sinyal gönderir, trap ise sinyali alan kabuğun ne yapacağını belirler. kill sinyal belirtilmezse SIGTERM gönderir. Arka plan işlerine %1 gibi iş numaralarıyla sinyal göndermek için Bash arka plan işleri yazısına bakabilirsiniz.

Trap eylemi içinde return kullanılabilir mi?

Evet, bir fonksiyon çalışırken tetiklenen trap içinde return o fonksiyonu sonlandırır. Kılavuz bir ayrıntıyı not eder: trap işleyicisi içinde değersiz return kullanılırsa dönüş durumu, işleyicinin içindeki komutlara göre değil, trap çalışmadan hemen önce yürütülen son komuta göre belirlenir. Belirli bir değer istiyorsanız return 1 gibi açıkça yazın.

Windows'taki Git Bash'te sinyaller Linux gibi mi davranır?

Her zaman değil. Bu yazıdaki örnekler davranışı Linux ile aynı olacak biçimde seçildi; örneğin kılavuza göre Linux'ta iş denetimi kapalıyken arka plan işleri SIGINT sinyalini yok sayar, MSYS ortamında ise farklı sonuç alabilirsiniz. Sinyal davranışını sunucuya taşımadan önce hedef Linux sisteminde deneyin.

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 exit kodları · Bash hata ayıklama · Bash subshell · Arka plan işleri