Bir script yazıyorsunuz, her şey güzel gidiyor, test ortamında mükemmel çalışıyor. Sonra production’a alıyorsunuz ve bir gece yarısı alarm geliyor: script yarıda kesilmiş, veriler yarım kalmış, sistem tutarsız bir durumda. Üstelik hiçbir log yok, hiçbir hata mesajı yok. Ne olduğunu anlamak için saatler harcıyorsunuz. İşte bu senaryo, Bash’te hata yönetimini neden ciddiye almanız gerektiğini tek başına açıklıyor.
Bu yazıda exit kodlarını, trap mekanizmasını ve production kalitesinde script yazmanın temel prensiplerini ele alacağız. Bunlar “ileri seviye” konular değil, aslında her script yazan kişinin bilmesi gereken temel pratikler.
Exit Kodları Nedir ve Neden Önemli?
Her Unix/Linux komutu çalışmasını tamamladıktan sonra bir çıkış kodu döndürür. Bu kod, işlemin başarılı mı yoksa hatalı mı tamamlandığını belirtir. 0 başarıyı, 0 dışındaki her değer bir hatayı ifade eder.
ls /var/log
echo $? # 0 döner, başarılı
ls /olmayan/dizin
echo $? # 2 döner, dosya/dizin bulunamadı
$? değişkeni, her zaman en son çalıştırılan komutun exit kodunu tutar. Bu kadar basit ama pek çok scriptte bu bilgi tamamen görmezden geliniyor.
Yaygın Exit Kod Değerleri
- 0: Başarılı çalışma
- 1: Genel hata (birçok uygulama tarafından kullanılır)
- 2: Yanlış kullanım, komut satırı hatası
- 126: Komut bulundu ama çalıştırılamadı (izin hatası)
- 127: Komut bulunamadı
- 128: Geçersiz exit argümanı
- 128+n: n sinyali ile sonlandırıldı (örneğin 130 = CTRL+C yani SIGINT)
- 130: Script, CTRL+C ile sonlandırıldı
- 137: SIGKILL ile öldürüldü (128 + 9)
- 255: Exit kodu aralık dışı
Kendi Exit Kodlarınızı Tanımlayın
Script yazarken anlamlı exit kodları kullanmak, hem debugging’i kolaylaştırır hem de scripti çağıran diğer sistemlerin (cron, CI/CD, monitoring) ne olduğunu anlamasını sağlar.
#!/bin/bash
# Exit kodlarını sabit olarak tanımla
readonly EXIT_SUCCESS=0
readonly EXIT_GENERAL_ERROR=1
readonly EXIT_INVALID_ARGS=2
readonly EXIT_FILE_NOT_FOUND=3
readonly EXIT_PERMISSION_DENIED=4
readonly EXIT_NETWORK_ERROR=5
backup_database() {
local db_name="$1"
local backup_dir="$2"
if [[ -z "$db_name" || -z "$backup_dir" ]]; then
echo "HATA: Veritabanı adı ve backup dizini gerekli." >&2
exit $EXIT_INVALID_ARGS
fi
if [[ ! -d "$backup_dir" ]]; then
echo "HATA: Backup dizini bulunamadı: $backup_dir" >&2
exit $EXIT_FILE_NOT_FOUND
fi
if [[ ! -w "$backup_dir" ]]; then
echo "HATA: Backup dizinine yazma izni yok: $backup_dir" >&2
exit $EXIT_PERMISSION_DENIED
fi
# Backup işlemi
pg_dump "$db_name" > "$backup_dir/${db_name}_$(date +%Y%m%d_%H%M%S).sql" || {
echo "HATA: Veritabanı backup alınamadı." >&2
exit $EXIT_GENERAL_ERROR
}
echo "Backup başarıyla tamamlandı."
exit $EXIT_SUCCESS
}
backup_database "$1" "$2"
set -e, set -u ve set -o pipefail
Bash scriptlerinde hata yönetiminin ilk katmanı bu üç ayardır. Çoğu kişi bunların ne işe yaradığını bilir ama farkında olmadan hatalı kullanır.
#!/bin/bash
set -euo pipefail
- set -e: Herhangi bir komut sıfır dışı exit kodu döndürürse script durur
- set -u: Tanımlanmamış değişken kullanılırsa hata verir
- set -o pipefail: Pipe içindeki herhangi bir komut başarısız olursa tüm pipe başarısız sayılır
pipefail olmadan şu senaryo sessizce geçer:
# pipefail olmadan bu başarılı görünür!
cat /olmayan/dosya | grep "bir şey" | wc -l
echo $? # 0 döner çünkü wc -l başarılı
# pipefail ile
set -o pipefail
cat /olmayan/dosya | grep "bir şey" | wc -l
echo $? # 1 döner, cat başarısız olduğu için
set -e kullanırken dikkat etmeniz gereken bir nokta var: bazı komutların başarısız olması normaldir ve bunu bekliyorsunuzdur. Bu durumda || veya if kullanın:
#!/bin/bash
set -euo pipefail
# Bu script'i patlatır çünkü grep bulamazsa 1 döner
grep "pattern" /var/log/syslog
# Bunun yerine:
if grep "pattern" /var/log/syslog; then
echo "Pattern bulundu"
else
echo "Pattern bulunamadı, bu normal"
fi
# Ya da:
grep "pattern" /var/log/syslog || true
trap Komutu: Gerçek Güç Burada
trap, belirli sinyaller veya olaylar gerçekleştiğinde çalışacak komutları veya fonksiyonları tanımlamanızı sağlar. Bu, hata yönetiminin en güçlü aracıdır.
Temel sözdizimi şöyle: trap 'komut' SINYAL
Temel trap Kullanımı
#!/bin/bash
set -euo pipefail
# Script sonlandığında temizlik yap
trap 'echo "Script sonlandı"' EXIT
# CTRL+C yakalandığında
trap 'echo "Kullanıcı tarafından iptal edildi"; exit 130' INT
# Hata yakalandığında
trap 'echo "Satır $LINENO: Hata oluştu"' ERR
echo "İşlem başlıyor..."
sleep 5
echo "İşlem tamamlandı."
Cleanup Fonksiyonu: Production’da Olmazsa Olmaz
Gerçek dünyada trap en çok geçici dosyaları, lock dosyalarını ve yarım kalan işlemleri temizlemek için kullanılır:
#!/bin/bash
set -euo pipefail
# Geçici dosya ve dizinler için değişkenler
TEMP_DIR=""
LOCK_FILE="/var/run/myapp.lock"
LOG_FILE="/var/log/myapp/deploy_$(date +%Y%m%d_%H%M%S).log"
cleanup() {
local exit_code=$?
echo "Temizlik yapılıyor..." | tee -a "$LOG_FILE"
# Geçici dizini sil
if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
echo "Geçici dizin silindi: $TEMP_DIR" | tee -a "$LOG_FILE"
fi
# Lock dosyasını kaldır
if [[ -f "$LOCK_FILE" ]]; then
rm -f "$LOCK_FILE"
echo "Lock dosyası kaldırıldı." | tee -a "$LOG_FILE"
fi
# Hata durumunda bildirim gönder
if [[ $exit_code -ne 0 ]]; then
echo "HATA: Script $exit_code kodu ile sonlandı." | tee -a "$LOG_FILE"
# Burada mail veya Slack bildirimi gönderilebilir
# send_alert "Deploy scripti başarısız: exit code $exit_code"
fi
exit $exit_code
}
# trap'i en başta tanımla
trap cleanup EXIT INT TERM
# Lock dosyası kontrolü
if [[ -f "$LOCK_FILE" ]]; then
echo "HATA: Script zaten çalışıyor (lock dosyası mevcut: $LOCK_FILE)" >&2
exit 1
fi
# Lock oluştur
echo $$ > "$LOCK_FILE"
# Geçici dizin oluştur
TEMP_DIR=$(mktemp -d)
echo "Geçici dizin oluşturuldu: $TEMP_DIR"
# Asıl iş burada yapılır
echo "Deploy işlemi başlıyor..."
# ... işlemler ...
echo "Deploy tamamlandı."
Hata Satırını Yakalamak
Debugging için altın değerinde bir teknik: hata oluştuğunda hangi satırda olduğunu loglamak.
#!/bin/bash
set -euo pipefail
error_handler() {
local exit_code=$?
local line_number=$1
local command="$2"
echo "========================================" >&2
echo "HATA DETAYI:" >&2
echo " Satır numarası : $line_number" >&2
echo " Başarısız komut: $command" >&2
echo " Exit kodu : $exit_code" >&2
echo " Zaman : $(date '+%Y-%m-%d %H:%M:%S')" >&2
echo " Script : $0" >&2
echo "========================================" >&2
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
# Test
echo "Adım 1 başlıyor..."
cp /olmayan/dosya /tmp/hedef # Bu başarısız olacak
echo "Buraya ulaşılamaz"
Bu çıktı size tam olarak nerede, ne başarısız olduğunu söyler. Gece yarısı prodction’da hata ayıklarken bunu ne kadar çok değerlediğinizi anlarsınız.
Gerçek Dünya Senaryosu: Database Backup Script
Şimdi tüm bu kavramları birleştiren, production’da kullanılabilecek gerçekçi bir örnek görelim:
#!/bin/bash
set -euo pipefail
###############################################
# PostgreSQL Backup Script
# Kullanım: ./pg_backup.sh <db_name> <backup_dir>
###############################################
# Sabitler
readonly SCRIPT_NAME=$(basename "$0")
readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S)
readonly LOG_FILE="/var/log/pg_backup/${TIMESTAMP}.log"
readonly LOCK_FILE="/var/run/pg_backup.lock"
readonly RETENTION_DAYS=7
# Exit kodları
readonly E_SUCCESS=0
readonly E_ARGS=2
readonly E_LOCK=3
readonly E_DIRS=4
readonly E_BACKUP=5
readonly E_COMPRESS=6
# Genel değişkenler
TEMP_DIR=""
DB_NAME=""
BACKUP_DIR=""
log() {
local level="$1"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
cleanup() {
local exit_code=$?
log "INFO" "Temizlik başlıyor..."
[[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
[[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
if [[ $exit_code -eq 0 ]]; then
log "INFO" "Backup başarıyla tamamlandı."
else
log "ERROR" "Backup BAŞARISIZ oldu. Exit kodu: $exit_code"
# Burada alerting sisteminizi tetikleyebilirsiniz
fi
}
error_handler() {
log "ERROR" "Satır $1'de hata: $2"
}
trap cleanup EXIT
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
trap 'log "WARN" "Script kullanıcı tarafından durduruldu"; exit 130' INT TERM
check_args() {
if [[ $# -ne 2 ]]; then
log "ERROR" "Kullanım: $SCRIPT_NAME <db_name> <backup_dir>"
exit $E_ARGS
fi
DB_NAME="$1"
BACKUP_DIR="$2"
}
check_prerequisites() {
# Lock kontrolü
if [[ -f "$LOCK_FILE" ]]; then
local pid
pid=$(cat "$LOCK_FILE")
if kill -0 "$pid" 2>/dev/null; then
log "ERROR" "Backup zaten çalışıyor (PID: $pid)"
exit $E_LOCK
else
log "WARN" "Eski lock dosyası bulundu, temizleniyor..."
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
# Dizin kontrolleri
if [[ ! -d "$BACKUP_DIR" ]]; then
log "INFO" "Backup dizini oluşturuluyor: $BACKUP_DIR"
mkdir -p "$BACKUP_DIR" || { log "ERROR" "Dizin oluşturulamadı"; exit $E_DIRS; }
fi
# pg_dump kontrolü
if ! command -v pg_dump &>/dev/null; then
log "ERROR" "pg_dump bulunamadı"
exit $E_BACKUP
fi
}
run_backup() {
TEMP_DIR=$(mktemp -d)
local backup_file="${TEMP_DIR}/${DB_NAME}_${TIMESTAMP}.sql"
local final_file="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
log "INFO" "Backup başlıyor: $DB_NAME"
if ! pg_dump "$DB_NAME" > "$backup_file"; then
log "ERROR" "pg_dump başarısız oldu"
exit $E_BACKUP
fi
log "INFO" "Sıkıştırılıyor..."
if ! gzip -c "$backup_file" > "$final_file"; then
log "ERROR" "Sıkıştırma başarısız oldu"
exit $E_COMPRESS
fi
local size
size=$(du -sh "$final_file" | cut -f1)
log "INFO" "Backup tamamlandı: $final_file ($size)"
}
cleanup_old_backups() {
log "INFO" "${RETENTION_DAYS} günden eski backuplar temizleniyor..."
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +"$RETENTION_DAYS" -delete
log "INFO" "Eski backup temizliği tamamlandı."
}
main() {
mkdir -p "$(dirname "$LOG_FILE")"
check_args "$@"
check_prerequisites
run_backup
cleanup_old_backups
}
main "$@"
Subshell’lerde Hata Yönetimi
set -e ve trap ERR subshell’lerde farklı davranabilir. Bunu bilmek önemli:
#!/bin/bash
set -euo pipefail
trap 'echo "Ana shell hatası: satır $LINENO"' ERR
riskli_islem() {
# Bu fonksiyon içindeki hata ERR trap'i tetikler
cp /olmayan/dosya /tmp/hedef
}
# Direkt çağrı - ERR trap tetiklenir
riskli_islem
# Subshell - ERR trap ana shell'de tetiklenmez
(
cp /olmayan/dosya /tmp/hedef
) || {
echo "Subshell hatası yakalandı: $?"
}
# Command substitution
sonuc=$(riskli_islem) || {
echo "Command substitution hatası: $?"
}
Sinyal Yönetimi ve Graceful Shutdown
Özellikle uzun süren scriptlerde graceful shutdown kritik öneme sahiptir:
#!/bin/bash
set -euo pipefail
PROCESSING=false
SHUTDOWN_REQUESTED=false
shutdown_handler() {
echo ""
echo "Kapatma sinyali alındı..."
SHUTDOWN_REQUESTED=true
if [[ "$PROCESSING" == "true" ]]; then
echo "Mevcut işlem tamamlanana kadar bekleniyor..."
# İşlemin bitmesini bekle
while [[ "$PROCESSING" == "true" ]]; do
sleep 1
done
fi
echo "Güvenli kapatma tamamlandı."
exit 0
}
trap shutdown_handler SIGTERM SIGINT
process_item() {
local item="$1"
PROCESSING=true
echo "İşleniyor: $item"
sleep 2 # Simüle edilmiş işlem
echo "Tamamlandı: $item"
PROCESSING=false
}
# Ana işlem döngüsü
items=("dosya1" "dosya2" "dosya3" "dosya4" "dosya5")
for item in "${items[@]}"; do
if [[ "$SHUTDOWN_REQUESTED" == "true" ]]; then
echo "Kapatma istendi, yeni işlem başlatılmıyor."
break
fi
process_item "$item"
done
Hata Yönetimi Kütüphanesi Oluşturmak
Birden fazla scriptte aynı hata yönetimi kodunu tekrarlamak yerine, kaynak alınabilir bir kütüphane oluşturun:
#!/bin/bash
# /usr/local/lib/bash/error_lib.sh
# Kullanım: source /usr/local/lib/bash/error_lib.sh
# Renkli çıktı
readonly RED='33[0;31m'
readonly YELLOW='33[1;33m'
readonly GREEN='33[0;32m'
readonly NC='33[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%H:%M:%S') $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*" >&2; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; }
die() {
local message="${1:-Bilinmeyen hata}"
local exit_code="${2:-1}"
log_error "$message"
exit "$exit_code"
}
require_root() {
[[ $EUID -eq 0 ]] || die "Bu script root yetkisi gerektiriyor." 1
}
require_command() {
local cmd="$1"
command -v "$cmd" &>/dev/null || die "'$cmd' komutu bulunamadı. Lütfen yükleyin." 127
}
require_file() {
local file="$1"
[[ -f "$file" ]] || die "Dosya bulunamadı: $file" 3
}
require_dir() {
local dir="$1"
[[ -d "$dir" ]] || die "Dizin bulunamadı: $dir" 3
}
setup_error_handling() {
set -euo pipefail
trap 'log_error "Satır $LINENO: '''$BASH_COMMAND''' başarısız (exit: $?)"' ERR
}
Bu kütüphaneyi kullanan bir script:
#!/bin/bash
source /usr/local/lib/bash/error_lib.sh
setup_error_handling
require_root
require_command "rsync"
require_command "pg_dump"
require_dir "/backup"
log_info "Yedekleme başlıyor..."
rsync -av /var/www/ /backup/www/ || die "rsync başarısız oldu" 5
log_info "Yedekleme tamamlandı."
Sık Yapılan Hatalar ve Kaçınma Yolları
Hata yönetimi yaparken en çok rastlanan yanlışlar şunlar:
set -esonrası test komutlarını unutmak:grep,diff,testgibi komutlar başarısızlıkta 1 döner. Bunlarıifbloğuna alın ya da|| trueekleyin.
$?‘yi geciktirmek:$?her komut çalıştırıldığında sıfırlanır. Kontrol etmeniz gereken exit kodunu hemen yakalayın.
- Stderr’i görmezden gelmek: Hata mesajlarını her zaman
>&2ile stderr’e yönlendirin. Çağıran sistemler stdout’u veri olarak, stderr’i hata olarak değerlendirir.
- trap’i geç tanımlamak:
traptanımlanmadan önce oluşan hatalar yakalanmaz. Script’in en başında tanımlayın.
- Cleanup’ta exit kodunu kaybetmek: Cleanup fonksiyonu içinde başka komutlar çalıştırırsanız
$?değişir. İlk satırdalocal exit_code=$?ile saklayın.
Sonuç
Exit kodları ve trap mekanizması, güvenilir Bash scriptlerinin temel taşlarıdır. Sadece “hata olursa ne yapalım” değil, “nasıl temiz bir şekilde kapanırız ve sistemi tutarlı bırakırız” sorusunun cevabıdır.
Özetleyecek olursak: Her scripte set -euo pipefail ekleyin, trap cleanup EXIT ile her zaman temizlik yapın, anlamlı exit kodları kullanın ve hataları her zaman stderr’e yazın. Bu dört pratik bile script kaliteniizi dramatik şekilde artırır.
Production’da çalışan scriptler bir gün mutlaka beklenmedik bir durumla karşılaşır. O an geldiğinde sisteminizin nerede durduğunu bilmek, ne kadar veri kaybettiğinizi anlamak ve olayı hızla toplamak; hepsi bu temellere bağlıdır. Gece yarısı alarm geldiğinde “en azından log var” diyebilmek, paha biçilmez bir rahatlıktır.