trap Komutu ile Bash Script’lerinde Sinyal Yakalama ve Temiz Çıkış Yönetimi
Üretim ortamında çalışan bir script’in tam ortasında Ctrl+C’ye bastığınızda ne olur? Geçici dosyalar diskte kalır, yarı yazılmış log’lar orada öylece durur, lock dosyaları bir sonraki çalışmada sizi engeller. Bu kaosun önüne geçmek için trap komutu var ve bir kez alışkanlık haline getirdikten sonra onsuz script yazmak mümkün gelmiyor.
trap Nedir ve Neden Önemlidir?
trap, bash’in sinyal ve olay yakalama mekanizmasıdır. Bir process’e sinyal gönderildiğinde veya script belirli noktalara ulaştığında ne yapılacağını önceden tanımlarsınız. Bunu bir tür “son istek” mekanizması gibi düşünebilirsiniz.
Linux’ta her process, kernel tarafından çeşitli sinyaller alabilir. SIGINT (klavyeden Ctrl+C), SIGTERM (kill komutu), SIGHUP (terminal kapandığında) bunların en yaygınları. Normalde bu sinyaller geldiğinde bash script’iniz anında durur ve temizlik yapılmaz. trap ile bu sinyalleri yakalayıp istediğiniz kodun çalışmasını sağlayabilirsiniz.
Temel sözdizimi şu şekildedir:
trap 'komut_veya_fonksiyon' SINYAL [SINYAL2 ...]
Temel Kullanım: İlk trap Deneyimi
En basit örneğe bakalım. Geçici dosya oluşturan ve temizlenmesi gereken bir script:
#!/bin/bash
TEMP_FILE=$(mktemp /tmp/islem_XXXXXX)
cleanup() {
echo "Temizlik yapılıyor..."
rm -f "$TEMP_FILE"
echo "Geçici dosya silindi: $TEMP_FILE"
}
trap cleanup EXIT
echo "Script çalışıyor, geçici dosya: $TEMP_FILE"
echo "Buraya önemli veriler yazılıyor..." > "$TEMP_FILE"
# Simüle edilmiş uzun işlem
sleep 30
echo "İşlem tamamlandı."
Burada EXIT özel bir pseudo-sinyal. Script nasıl çıkarsa çıksın, normal sona erme, hata, Ctrl+C fark etmeksizin cleanup fonksiyonu çalışır. Bu tek satır, script güvenilirliğinizi ciddi ölçüde artırır.
Yaygın Sinyaller ve Anlamları
Sinyalleri teker teker anlamak, doğru kullanım için şart:
- EXIT: Script herhangi bir şekilde sonlandığında tetiklenir. Pseudo-sinyal, gerçek bir OS sinyali değil
- SIGINT (2): Ctrl+C ile üretilir. Kullanıcı scripti kesmek istediğinde
- SIGTERM (15):
kill PIDile gönderilir. Sistemin script’i nazikçe durdurma isteği - SIGHUP (1): Terminal kapandığında veya
nohupolmadan arka planda çalışırken - SIGKILL (9):
kill -9ile gönderilir. YAKALANAMAZ. trap ile engellenemez - SIGQUIT (3): Ctrl+\ ile üretilir. Core dump oluşturur
- ERR: Her hata komutundan sonra tetiklenir (set -e ile kombinasyonu dikkat ister)
- DEBUG: Her komuttan önce tetiklenir, debugging için kullanışlı
Birden Fazla Sinyal Yakalama
Gerçek dünyada genellikle birden fazla sinyali ele almanız gerekir:
#!/bin/bash
LOG_FILE="/var/log/myapp/process.log"
LOCK_FILE="/var/run/myapp.lock"
TEMP_DIR=$(mktemp -d /tmp/myapp_XXXXXX)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
cleanup() {
local exit_code=$?
log "Sinyal alındı, temizlik başlıyor..."
# Geçici dizini temizle
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
log "Geçici dizin temizlendi: $TEMP_DIR"
fi
# Lock dosyasını kaldır
if [ -f "$LOCK_FILE" ]; then
rm -f "$LOCK_FILE"
log "Lock dosyası kaldırıldı"
fi
log "Script sonlandı (exit code: $exit_code)"
exit $exit_code
}
trap cleanup EXIT SIGINT SIGTERM SIGHUP
# Lock dosyası oluştur
echo $$ > "$LOCK_FILE"
log "Script başladı (PID: $$)"
# Asıl iş burada yapılır
for i in $(seq 1 10); do
log "Adım $i işleniyor..."
sleep 5
done
Burada $? exit code’unu yakalamak önemli. cleanup fonksiyonu çağrıldığında son komutun exit code’u hâlâ $? içinde. Bu değeri saklayıp en sonda geri dönüyoruz, böylece script’in gerçek exit code’u korunuyor.
trap ile Lock Dosyası Yönetimi
Cron job’larda çakışmayı önlemek için lock mekanizması şart. trap olmadan lock dosyaları hayalet gibi ortalıkta kalır:
#!/bin/bash
LOCK_FILE="/var/run/veri_senkronizasyon.lock"
SCRIPT_NAME=$(basename "$0")
acquire_lock() {
if [ -f "$LOCK_FILE" ]; then
local eski_pid
eski_pid=$(cat "$LOCK_FILE" 2>/dev/null)
if kill -0 "$eski_pid" 2>/dev/null; then
echo "Script zaten çalışıyor (PID: $eski_pid). Çıkılıyor."
exit 1
else
echo "Eski lock dosyası bulundu ama process yok, temizleniyor."
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
echo "Lock alındı (PID: $$)"
}
release_lock() {
rm -f "$LOCK_FILE"
echo "Lock serbest bırakıldı"
}
trap release_lock EXIT
acquire_lock
echo "Senkronizasyon başladı..."
# rsync veya benzeri işlemler burada
sleep 20
echo "Senkronizasyon tamamlandı."
Bu pattern özellikle cron’da çalışan script’lerde hayat kurtarır. Sunucu beklenmedik şekilde kapanıp açıldığında bile bir sonraki çalışmada eski PID’in hayatta olup olmadığını kontrol ediyoruz.
ERR Sinyali ile Hata Yönetimi
ERR pseudo-sinyali, hata ayıklama ve üretim script’lerinde son derece güçlü bir araç:
#!/bin/bash
set -euo pipefail
hata_yonetici() {
local satir_no=$1
local exit_code=$2
local komut=$3
echo "HATA: Satır $satir_no'da başarısız oldu" >&2
echo "Komut: $komut" >&2
echo "Exit code: $exit_code" >&2
# Slack veya PagerDuty'e bildirim gönderilebilir
# curl -s -X POST "$WEBHOOK_URL" -d "{"text": "Script hatası: $komut"}"
cleanup
exit $exit_code
}
cleanup() {
# Geçici kaynakları temizle
[ -d "${TEMP_DIR:-}" ] && rm -rf "$TEMP_DIR"
}
trap 'hata_yonetici $LINENO $? "$BASH_COMMAND"' ERR
trap cleanup EXIT
TEMP_DIR=$(mktemp -d)
echo "Yedekleme başladı..."
# Bu komut başarısız olursa hata yöneticisi devreye girer
tar -czf "$TEMP_DIR/yedek.tar.gz" /var/www/html/
# S3'e yükle
aws s3 cp "$TEMP_DIR/yedek.tar.gz" s3://sirket-yedekleri/
echo "Yedekleme tamamlandı."
$BASH_COMMAND değişkeni, hata veren son komutu içerir. $LINENO ise script içindeki satır numarasını verir. Bu ikisini birleştirince hata raporları çok daha anlamlı hale gelir.
Sinyal Yayılımı: Alt Process’leri Yönetmek
trap’ın en sık gözden kaçan konularından biri: trap sadece mevcut shell için geçerlidir, fork’lanan child process’lere otomatik yayılmaz. Uzun süren arka plan işleri başlatıyorsanız bu kritik:
#!/bin/bash
ARKAPLAN_PIDLER=()
cleanup() {
echo "Temizlik: arka plan process'leri durduruluyor..."
for pid in "${ARKAPLAN_PIDLER[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
echo "Process durduruluyor: $pid"
kill -SIGTERM "$pid" 2>/dev/null
# SIGTERM'e yanıt vermezse 5 saniye bekle sonra SIGKILL
local sayac=0
while kill -0 "$pid" 2>/dev/null && [ $sayac -lt 5 ]; do
sleep 1
((sayac++))
done
if kill -0 "$pid" 2>/dev/null; then
echo "Process yanıt vermedi, SIGKILL gönderiliyor: $pid"
kill -SIGKILL "$pid" 2>/dev/null
fi
fi
done
echo "Temizlik tamamlandı"
}
trap cleanup EXIT SIGINT SIGTERM
# Paralel işlemler başlat
for servis in "web" "cache" "queue"; do
./islemci.sh "$servis" &
pid=$!
ARKAPLAN_PIDLER+=($pid)
echo "$servis başlatıldı (PID: $pid)"
done
echo "Tüm servisler başlatıldı, bekleniyor..."
wait
echo "Tüm işlemler tamamlandı."
PID’leri dizide takip edip cleanup’ta hepsini düzenli şekilde sonlandırmak, özellikle paralel işlem yapan script’lerde şart.
Deployment Script’i: Gerçek Dünya Örneği
Şimdiye kadar gördüklerimizi birleştiren, gerçekçi bir deployment script’i:
#!/bin/bash
set -euo pipefail
# Değişkenler
APP_DIR="/var/www/uygulama"
BACKUP_DIR="/var/backups/uygulama"
DEPLOY_USER="deploy"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TEMP_DIR=$(mktemp -d /tmp/deploy_XXXXXX)
ROLLBACK_GEREKLI=false
SERVIS_DURDURULDU=false
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
hata() {
log "HATA: $*" >&2
}
rollback() {
if [ "$ROLLBACK_GEREKLI" = true ]; then
log "Rollback başlatılıyor..."
local son_yedek
son_yedek=$(ls -t "$BACKUP_DIR"/app_*.tar.gz 2>/dev/null | head -1)
if [ -n "$son_yedek" ]; then
tar -xzf "$son_yedek" -C "$APP_DIR" --strip-components=1
log "Rollback tamamlandı: $son_yedek"
else
hata "Rollback için yedek bulunamadı!"
fi
fi
}
cleanup() {
local exit_code=$?
log "Temizlik aşaması..."
# Geçici dizini temizle
[ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
# Servis durdurulduysa ve başarısız olduysa rollback ve yeniden başlat
if [ "$SERVIS_DURDURULDU" = true ] && [ $exit_code -ne 0 ]; then
rollback
log "Servis yeniden başlatılıyor..."
systemctl start uygulama || hata "Servis başlatılamadı!"
fi
exit $exit_code
}
trap cleanup EXIT
trap 'hata "İşlem kullanıcı tarafından kesildi"; exit 130' SIGINT
trap 'hata "SIGTERM alındı"; exit 143' SIGTERM
log "Deployment başladı: $TIMESTAMP"
# Mevcut uygulamayı yedekle
log "Yedekleme alınıyor..."
ROLLBACK_GEREKLI=true
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/app_${TIMESTAMP}.tar.gz" -C "$APP_DIR" .
# Servisi durdur
log "Servis durduruluyor..."
systemctl stop uygulama
SERVIS_DURDURULDU=true
# Yeni kodu deploy et
log "Yeni sürüm yükleniyor..."
tar -xzf "/tmp/release_${TIMESTAMP}.tar.gz" -C "$TEMP_DIR"
rsync -av --delete "$TEMP_DIR/" "$APP_DIR/"
# Migration çalıştır
log "Database migration çalıştırılıyor..."
cd "$APP_DIR" && php artisan migrate --force
# Servisi başlat
log "Servis başlatılıyor..."
systemctl start uygulama
SERVIS_DURDURULDU=false
# Health check
sleep 3
if ! curl -sf http://localhost/health > /dev/null; then
hata "Health check başarısız!"
exit 1
fi
ROLLBACK_GEREKLI=false
log "Deployment başarıyla tamamlandı!"
Bu örnekte birkaç önemli pattern var. ROLLBACK_GEREKLI ve SERVIS_DURDURULDU flag’leri, cleanup’ın tam olarak nerede başarısız olunduğunu bilmesini sağlıyor. Deployment ortasında Ctrl+C geleseydi bile sistem tutarlı bir durumda kalırdı.
trap’ı Geçici Olarak Devre Dışı Bırakmak
Bazen belirli bir blok için trap’ı askıya almanız gerekir. Bunu trap '' ile yapabilirsiniz:
#!/bin/bash
cleanup() {
echo "Cleanup çalışıyor..."
rm -f /tmp/onemli_dosya
}
trap cleanup EXIT SIGINT
echo "Normal çalışma..."
# Bu blok sırasında Ctrl+C'yi yoksay
trap '' SIGINT
echo "Kritik bölge başladı, Ctrl+C devre dışı..."
sleep 10
echo "Kritik bölge bitti"
trap cleanup SIGINT # Yeniden aktif et
echo "Normal çalışmaya devam..."
sleep 5
Dikkat: SIGINT’i susturmak kullanıcı deneyimini olumsuz etkiler. Sadece gerçekten kesilemeyecek kritik bölümler için kullanın ve süreyi kısa tutun.
DEBUG Trap ile Script Tracing
Geliştirme aşamasında her komuttan önce çalışan DEBUG trap’ı inanılmaz derecede faydalı:
#!/bin/bash
# Sadece DEBUG modunda etkinleştir
if [ "${DEBUG_SCRIPT:-0}" = "1" ]; then
trap 'echo ">> Satır $LINENO: $BASH_COMMAND"' DEBUG
fi
trap 'echo "Script bitti (exit: $?)"' EXIT
KULLANICI="ahmet"
DIZIN="/home/$KULLANICI"
if [ -d "$DIZIN" ]; then
echo "Dizin mevcut: $DIZIN"
ls -la "$DIZIN"
fi
Çalıştırmak için:
DEBUG_SCRIPT=1 ./script.sh
Bu, set -x alternatifinden daha okunabilir çıktı üretir ve koşullu olarak etkinleştirebildiğiniz için production’da log’ları kirletmez.
Sık Yapılan Hatalar
Tecrübelerimden derlediğim kaçınılması gereken durumlar:
- Cleanup içinde exit code’u kaybetmek:
cleanup()başındalocal exit_code=$?ile saklayın, fonksiyon sonundaexit $exit_codekullanın - Cleanup’ın başarısız olması: cleanup fonksiyonu içindeki komutların hata vermesi asıl exit code’u bozabilir.
|| trueekleyerek devam etmesini sağlayın - SIGKILL’i yakalamaya çalışmak:
kill -9yakalanamaz, buna göre tasarım yapın - Subshell’lerde trap beklemek:
$(komut)içindeki trap’lar parent shell’e yayılmaz - trap’ı döngü içinde tanımlamak: trap tanımı global, her iterasyonda yeniden tanımlamak gerek yok ve performansı etkiler
Sonuç
trap, bash script yazmanın olmazsa olmaz parçalarından biri. Basit bir cleanup fonksiyonu ile başlayıp zamanla daha karmaşık sinyal yönetimi senaryolarına geçebilirsiniz.
Benim kişisel yaklaşımım şu: her script’te minimum olarak trap cleanup EXIT kullanmak. Bu tek satır, script’inizin ne zaman durduğundan bağımsız olarak temizlik yapmasını garantiler. Geçici dosyalar, lock dosyaları, database bağlantıları, arka plan process’leri; bunların hepsi cleanup fonksiyonunda ele alınabilir.
Üretim ortamındaki script’lerde ise SIGINT ve SIGTERM’i ayrı ayrı ele alıp uygun exit code’ları döndürmek profesyonel bir yaklaşım. Monitoring sistemleri ve process manager’lar (systemd, supervisor) bu exit code’lara göre karar verir.
Son olarak, trap kullanmak bir güvenlik ağı değil, tasarımın parçası olmalı. Script’inizi “bu aniden durduğunda sistem ne durumda kalır?” sorusunu sorarak yazın ve trap’ı bu sorunun cevabına göre şekillendirin.
