Bir bash scripti yazıyorsunuz, her şey güzel gidiyor, sonra kullanıcı Ctrl+C’ye basıyor. Script yarım kalıyor, geçici dosyalar ortalıkta duruyor, lock dosyası silinmiyor, yarım kalan işlemler sistem kaynaklarını tüketmeye devam ediyor. Tanıdık geldi mi? İşte trap komutu tam olarak bu tür kaosları önlemek için var.
trap, bash scriptlerinizde sinyal ve olay yakalamayı sağlayan bir yerleşik komuttur. Script beklenmedik şekilde sonlandığında, bir sinyal aldığında ya da normal çıkış yaptığında ne olacağını siz belirlersiniz. Bu yazıda trap mekanizmasını derinlemesine inceleyecek, gerçek dünya senaryolarıyla nasıl kullanacağınızı göreceğiz.
Sinyal Nedir, Neden Önemlidir?
Linux’ta sinyal, bir prosese gönderilen asenkron bildirimdir. Kullanıcı Ctrl+C’ye bastığında SIGINT gönderilir, kill komutu varsayılan olarak SIGTERM gönderir, sistem kapanırken SIGHUP gelir. Bash scriptleri bu sinyalleri varsayılan davranışla işler, yani çoğunlukla hemen sonlanır.
Sysadmin olarak yazdığınız scriptler genellikle:
- Geçici dosya ve dizinler oluşturur
- Lock dosyaları tutar
- Veritabanı bağlantıları açar
- Yarım kalan işlemler başlatır
- Log dosyalarına yazar
Bunların temizlenmesi gerekir. trap olmadan bu temizlik işlemi şansa bırakılmış demektir.
trap Sözdizimi
trap 'komut_veya_fonksiyon' SINYAL [SINYAL2 ...]
Temel kullanım bu kadar basit. Ama pratikte genellikle bir fonksiyon çağrısı yapılır:
trap temizle EXIT
trap 'temizle; exit 1' INT TERM
Yaygın sinyal isimleri şunlardır:
- EXIT: Script herhangi bir şekilde çıkış yaptığında (en kullanışlı olanı)
- INT: Ctrl+C ile gelen SIGINT sinyali
- TERM:
killkomutunun gönderdiği SIGTERM - HUP: Terminal kapandığında veya
SIGHUPalındığında - ERR: Herhangi bir komut hata ile dönüş yaptığında (set -e ile birlikte kullanışlı)
- DEBUG: Her komut çalışmadan önce tetiklenir
- QUIT: Ctrl+ ile gelen SIGQUIT
trap ile mevcut tanımlamaları görmek için:
trap -p
Belirli bir sinyalin tanımını kaldırmak için:
trap - EXIT
Sinyali yok saymak için (boş string):
trap '' INT
En Temel Kullanım: EXIT ile Temizlik
EXIT yakalama noktası, trap kullanımının en temel ve en pratik şeklidir. Script nasıl çıkarsa çıksın, normal çıkış, exit komutu, sinyal, hata, her durumda tetiklenir.
#!/bin/bash
GECICI_DIR=$(mktemp -d)
LOCK_DOSYASI="/var/run/benim_scriptim.lock"
temizle() {
echo "Temizlik yapılıyor..."
rm -rf "$GECICI_DIR"
rm -f "$LOCK_DOSYASI"
echo "Temizlik tamamlandı."
}
trap temizle EXIT
# Lock dosyası oluştur
touch "$LOCK_DOSYASI"
echo "Geçici dizin: $GECICI_DIR"
echo "İşlemler başlıyor..."
# Burada asıl iş yapılır
cp -r /etc/nginx "$GECICI_DIR/"
tar -czf /backup/nginx_config_$(date +%Y%m%d).tar.gz -C "$GECICI_DIR" .
echo "Yedekleme tamamlandı."
Bu örnekte script Ctrl+C ile kesilse de, hata verse de, normal tamamlansa da temizle fonksiyonu her zaman çalışır. Geçici dizin ve lock dosyası kesinlikle temizlenir.
INT ve TERM ile Graceful Shutdown
Production ortamında servis benzeri çalışan scriptlerde INT ve TERM sinyallerini ayrı ele almak gerekir:
#!/bin/bash
DEVAM=true
ISLENEN=0
LOG_DOSYASI="/var/log/toplu_islem.log"
baslik_logla() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DOSYASI"
}
kapatma_islemi() {
baslik_logla "Durdurma sinyali alındı. Mevcut işlem tamamlanıyor..."
DEVAM=false
}
cikis_raporu() {
baslik_logla "Script sonlandı. Toplam işlenen: $ISLENEN kayıt"
}
trap kapatma_islemi INT TERM
trap cikis_raporu EXIT
baslik_logla "Toplu işlem başladı. PID: $$"
# Veri dosyasından satır satır işle
while IFS= read -r satir && $DEVAM; do
# Simüle edilmiş işlem
sleep 0.1
ISLENEN=$((ISLENEN + 1))
if (( ISLENEN % 100 == 0 )); then
baslik_logla "$ISLENEN kayıt işlendi"
fi
done < /data/islenecek_liste.txt
if $DEVAM; then
baslik_logla "Tüm kayıtlar başarıyla işlendi."
else
baslik_logla "Script erken sonlandırıldı. Kaldığı yer kaydedildi."
fi
Buradaki kritik nokta DEVAM değişkeni. Sinyal geldiğinde hemen exit yapmak yerine döngünün mevcut iterasyonu tamamlanmasına izin veriyoruz. Bu, veri tutarlılığı açısından çok önemli.
ERR Sinyali ile Hata Yönetimi
ERR sinyali, sıfırdan farklı çıkış kodu döndüren her komutta tetiklenir. set -e ile birlikte kullanıldığında güçlü bir hata yönetimi sağlar:
#!/bin/bash
set -euo pipefail
HATA_SATIRI=""
HATA_KODU=""
hata_isle() {
HATA_KODU=$?
HATA_SATIRI=$BASH_LINENO
echo "HATA: Satır $HATA_SATIRI'de komut başarısız oldu (çıkış kodu: $HATA_KODU)" >&2
echo "Komut: $BASH_COMMAND" >&2
}
temizle() {
local cikis_kodu=$?
if [ $cikis_kodu -ne 0 ]; then
echo "Script hata ile sonlandı. Çıkış kodu: $cikis_kodu" >&2
# Acil durum temizliği
rm -f /tmp/islem_kilidi
fi
}
trap hata_isle ERR
trap temizle EXIT
echo "Deployment başlıyor..."
# Bu komutlardan biri başarısız olursa hata_isle tetiklenir
systemctl stop uygulamam
cp /deploy/yeni_surum /opt/uygulamam/
systemctl start uygulamam
systemctl is-active uygulamam
echo "Deployment tamamlandı."
BASH_LINENO, BASH_COMMAND gibi özel değişkenler hata ayıklamada altın değerindedir.
Gerçek Dünya Senaryosu: Veritabanı Yedekleme Scripti
Şimdi daha kapsamlı bir örnek. Production’da kullandığım türde bir yedekleme scripti:
#!/bin/bash
set -euo pipefail
# Konfigürasyon
DB_HOST="localhost"
DB_USER="backup_user"
DB_PASS="gizli_sifre"
YEDEK_DIZINI="/backup/mysql"
GECICI_DIZIN=""
LOCK_DOSYASI="/var/run/mysql_yedek.lock"
BILDIRIM_EMAIL="[email protected]"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$1] $2" | tee -a /var/log/mysql_yedek.log
}
bildirim_gonder() {
local konu=$1
local mesaj=$2
echo "$mesaj" | mail -s "$konu" "$BILDIRIM_EMAIL" 2>/dev/null || true
}
temizle() {
local cikis_kodu=$?
log "INFO" "Temizlik başlıyor..."
# Geçici dizini temizle
if [ -n "$GECICI_DIZIN" ] && [ -d "$GECICI_DIZIN" ]; then
rm -rf "$GECICI_DIZIN"
log "INFO" "Geçici dizin silindi: $GECICI_DIZIN"
fi
# Lock dosyasını kaldır
if [ -f "$LOCK_DOSYASI" ]; then
rm -f "$LOCK_DOSYASI"
log "INFO" "Lock dosyası kaldırıldı"
fi
# Hata durumunda bildirim gönder
if [ $cikis_kodu -ne 0 ]; then
log "ERROR" "Script $cikis_kodu koduyla sonlandı"
bildirim_gonder
"HATA: MySQL Yedekleme Başarısız - $(hostname)"
"$(date): Yedekleme scripti başarısız oldu. Çıkış kodu: $cikis_kodu"
fi
}
kesinti_isle() {
log "WARN" "Kesinti sinyali alındı, temizlik yapılıyor..."
exit 130
}
trap temizle EXIT
trap kesinti_isle INT TERM
# Çakışma kontrolü
if [ -f "$LOCK_DOSYASI" ]; then
PID=$(cat "$LOCK_DOSYASI")
if kill -0 "$PID" 2>/dev/null; then
log "ERROR" "Script zaten çalışıyor (PID: $PID)"
exit 1
else
log "WARN" "Eski lock dosyası bulundu, temizleniyor"
rm -f "$LOCK_DOSYASI"
fi
fi
# Lock oluştur
echo $$ > "$LOCK_DOSYASI"
log "INFO" "Yedekleme başladı. PID: $$"
# Geçici dizin oluştur
GECICI_DIZIN=$(mktemp -d /tmp/mysql_yedek_XXXXXX)
# Veritabanlarını listele ve yedekle
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS"
-e "SHOW DATABASES;" --skip-column-names 2>/dev/null |
grep -Ev "^(information_schema|performance_schema|sys)$" |
while read -r db; do
log "INFO" "Yedekleniyor: $db"
mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS"
--single-transaction
--routines
--triggers
"$db" > "$GECICI_DIZIN/$db.sql"
gzip "$GECICI_DIZIN/$db.sql"
log "INFO" "$db yedeği tamamlandı: $(du -sh "$GECICI_DIZIN/$db.sql.gz" | cut -f1)"
done
# Yedeği asıl dizine taşı
TARIH=$(date '+%Y%m%d_%H%M%S')
FINAL_DOSYA="$YEDEK_DIZINI/yedek_$TARIH.tar.gz"
tar -czf "$FINAL_DOSYA" -C "$GECICI_DIZIN" .
log "INFO" "Yedekleme tamamlandı: $FINAL_DOSYA ($(du -sh "$FINAL_DOSYA" | cut -f1))"
# 30 günden eski yedekleri temizle
find "$YEDEK_DIZINI" -name "yedek_*.tar.gz" -mtime +30 -delete
log "INFO" "Eski yedekler temizlendi"
Bu script, birden fazla senaryo için trap kullanıyor: normal çıkışta temizlik, sinyal durumunda zarif kapanış, hata durumunda bildirim.
DEBUG Sinyali ile Script Takibi
DEBUG sinyali her komuttan önce tetiklenir. Üretimde pek kullanılmaz ama hata ayıklama ve audit log oluşturmak için çok işe yarar:
#!/bin/bash
AUDIT_LOG="/var/log/script_audit.log"
komut_takip() {
echo "[$(date '+%Y-%m-%d %H:%M:%S.%N')] [PID:$$] KOMUT: $BASH_COMMAND" >> "$AUDIT_LOG"
}
# Sadece belirli kritik bölümlerde aktif et
trap komut_takip DEBUG
# Takip edilmesini istediğimiz kritik bölüm
echo "Kritik bölüm başlıyor"
useradd -m yeni_kullanici
passwd yeni_kullanici
usermod -aG sudo yeni_kullanici
echo "Kritik bölüm bitti"
# DEBUG trapini kaldır
trap - DEBUG
echo "Bu komut artık takip edilmiyor"
İç İçe trap Kullanımı ve Alt Scriptlerde Dikkat Edilecekler
Alt scriptler (subshell) üst scriptin trap tanımlarını miras almaz. Sinyal davranışını kontrol etmek istiyorsanız her scriptte ayrı tanımlamanız gerekir:
#!/bin/bash
PARCA_SCRIPTLERI=()
PARCA_PIDLERI=()
tum_parcelari_durdur() {
echo "Ana script sonlanıyor, parçalar durduruluyor..."
for pid in "${PARCA_PIDLERI[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid"
echo "PID $pid durduruldu"
fi
done
wait
echo "Tüm parçalar durduruldu"
}
trap tum_parcelari_durdur EXIT INT TERM
# Alt işleri arka planda başlat
for i in 1 2 3; do
(
# Alt scriptte de kendi trap tanımı
trap 'echo "Parça $i temizlendi"; rm -f /tmp/parca_$i.pid' EXIT
echo $$ > "/tmp/parca_$i.pid"
while true; do
echo "Parça $i çalışıyor..."
sleep 5
done
) &
PARCA_PIDLERI+=($!)
echo "Parça $i başlatıldı, PID: $!"
done
echo "Tüm parçalar çalışıyor. Durdurmak için Ctrl+C"
wait
trap ile Rollback Mekanizması
Deployment scriptlerinde yanlış giden bir şey olduğunda önceki duruma dönmek hayat kurtarır:
#!/bin/bash
set -euo pipefail
UYGULAMA_DIZINI="/opt/uygulamam"
YEDEK_DIZINI="/opt/uygulamam_yedek"
SERVIS_ADI="uygulamam"
ROLLBACK_GEREKLI=false
log() { echo "[$(date '+%H:%M:%S')] $*"; }
rollback() {
local cikis=$?
if $ROLLBACK_GEREKLI && [ $cikis -ne 0 ]; then
log "HATA ALINDI! Rollback başlatılıyor..."
systemctl stop "$SERVIS_ADI" 2>/dev/null || true
if [ -d "$YEDEK_DIZINI" ]; then
rm -rf "$UYGULAMA_DIZINI"
mv "$YEDEK_DIZINI" "$UYGULAMA_DIZINI"
systemctl start "$SERVIS_ADI"
log "Rollback tamamlandı, eski sürüm geri yüklendi"
else
log "KRITIK: Yedek dizin bulunamadı, manuel müdahale gerekli!"
fi
elif [ -d "$YEDEK_DIZINI" ] && [ $cikis -eq 0 ]; then
# Başarılı deployment, yedeği temizle
rm -rf "$YEDEK_DIZINI"
log "Deployment başarılı, yedek temizlendi"
fi
}
trap rollback EXIT
log "Deployment başlıyor..."
# Mevcut versiyonu yedekle
cp -r "$UYGULAMA_DIZINI" "$YEDEK_DIZINI"
log "Mevcut versiyon yedeklendi"
# Artık rollback aktif
ROLLBACK_GEREKLI=true
log "Servis durduruluyor..."
systemctl stop "$SERVIS_ADI"
log "Yeni dosyalar kopyalanıyor..."
rsync -av --delete /deploy/yeni_surum/ "$UYGULAMA_DIZINI/"
log "Bağımlılıklar güncelleniyor..."
cd "$UYGULAMA_DIZINI"
pip install -r requirements.txt -q
log "Veritabanı migrasyonu çalıştırılıyor..."
python manage.py migrate --noinput
log "Servis başlatılıyor..."
systemctl start "$SERVIS_ADI"
sleep 3
log "Sağlık kontrolü yapılıyor..."
systemctl is-active "$SERVIS_ADI"
curl -sf http://localhost:8080/health > /dev/null
log "Deployment başarıyla tamamlandı!"
Bu örnekte ROLLBACK_GEREKLI bayrağını kullanarak sadece kısmen tamamlanmış deployment’larda rollback yapmayı sağlıyoruz. Yedekleme henüz tamamlanmamışken script çökerse rollback yapılmaz.
Sinyal Maskeleme ve Kritik Bölümler
Bazı durumlarda kodun belirli bir bölümünün kesinlikle kesintisiz çalışmasını isteyebilirsiniz. Sinyal maskeleyerek bunu sağlayabilirsiniz:
#!/bin/bash
# Sinyal maskeleme: kritik bölümde kesintisiz çalış
kritik_islem() {
local geciktirilmis_sinyal=""
# Kritik bölümde INT ve TERM'i geciktir
trap 'geciktirilmis_sinyal=INT' INT
trap 'geciktirilmis_sinyal=TERM' TERM
echo "Kritik işlem başlıyor, kesilmeyecek..."
# Kritik veritabanı işlemi - bu bloğun ortada kesilmesi veri kaybına yol açar
mysql -e "BEGIN;"
mysql -e "UPDATE hesaplar SET bakiye = bakiye - 100 WHERE id = 1;"
mysql -e "UPDATE hesaplar SET bakiye = bakiye + 100 WHERE id = 2;"
mysql -e "COMMIT;"
echo "Kritik işlem tamamlandı"
# Normal sinyal işleyicilerini geri yükle
trap - INT TERM
# Geciktirilmiş sinyal varsa şimdi işle
if [ -n "$geciktirilmis_sinyal" ]; then
echo "Geciktirilmiş sinyal işleniyor: $geciktirilmis_sinyal"
kill -"$geciktirilmis_sinyal" $$
fi
}
trap 'echo "INT alındı, kritik işlem sonrası çıkılacak"; exit 130' INT
trap 'echo "TERM alındı"; exit 143' TERM
echo "Script başladı"
kritik_islem
echo "Script devam ediyor..."
Pratik İpuçları ve Sık Yapılan Hatalar
trap kullanırken dikkat edilmesi gereken bazı noktalar var:
- Değişken expansion zamanlaması:
trap 'echo $DEGISKEN' EXITifadesinde$DEGISKEN, trap tanımlanırken değil çalışırken genişletilir. Bu genellikle istenen davranıştır ama farkında olun.
- trap bir fonksiyona atanırsa: Fonksiyon içindeki
returnkodu değil, fonksiyonu tetikleyen olayın çıkış kodu$?olarak gelir. Bunu düzgün yakalamak için fonksiyonun başındalocal cikis=$?yapın.
- set -e ve trap ERR birlikteliği:
set -eaktifken bir komut başarısız olduğunda hemERRhemEXITtetiklenir. Bunu hesaba katın.
- Subshell’lerde trap: Parantez içindeki komutlar
(komutlar)alt proseste çalışır ve üst scriptin trap’lerini miras almaz. Pipe içindeki komutlar da öyle.
- exit kodunu korumak:
EXIThandler’ı içinde yeni komutlar çalıştırırsanız$?değeri değişir. Başta kaydedin:
temizle() {
local CIKIS_KODU=$?
# Temizlik komutları $? değiştirse de orijinal kodu koruduk
rm -rf "$GECICI_DIR"
# Orijinal çıkış kodu ile çık
exit $CIKIS_KODU
}
trap temizle EXIT
- İnteraktif ve non-interaktif davranış farkı: Cron’dan çalışan scriptlerde terminal sinyalleri gelmez ama
SIGTERMprocess sonlandırma için hala kullanılır.
Sonuç
trap, bash scriptlerinizi gerçek production kalitesine taşıyan en önemli araçlardan biri. Temizlik işlemleri, rollback mekanizmaları, graceful shutdown, hata bildirimleri, hepsi trap ile tutarlı ve güvenilir hale gelir.
Önerdiğim temel kurallar şunlar: Her script başında en azından trap temizle EXIT koyun, kritik scriptlerde INT ve TERM sinyallerini ayrı ele alın, set -euo pipefail ile birlikte ERR trapini kullanmayı düşünün. Lock dosyaları, geçici dizinler, açık bağlantılar ne kadar önemli bir kaynaksa o kadar dikkatli bir temizlik stratejisi kurun.
En önemli nokta şu: Script’in NASIL sonlandığı değil, sonlandıktan sonra sistemi hangi durumda bıraktığı önemlidir. trap size bu kontrolü verir. Kullanın.