Bash script yazıyorsun, her şey güzel gidiyor, script çalışıyor, sonuçlar geliyor. Sonra bir gün production’da bir şeyler ters gidiyor ve script hataları sessizce yutarak çalışmaya devam etmiş. Veritabanı yedeği alınamamış ama script “başarıyla” tamamlanmış. Tanıdık geldi mi? İşte bu yüzden güvenli script yazımı sadece bir tercih değil, zorunluluk.
Bash’in varsayılan davranışı oldukça affedici, belki de fazla affedici. Bir komut hata verse bile script çalışmaya devam eder. Tanımlı olmayan bir değişken kullansan boş string gibi davranır. Pipe’ın ortasında bir komut başarısız olsa son komut başarılı döndüğü sürece kimse fark etmez. Bu “affedici” davranış aslında sinsi hataların kapısını ardına kadar açar.
Bu yazıda set -e, set -u, set -o pipefail üçlüsünü, bunların nasıl çalıştığını, gerçek dünya senaryolarında nasıl kullanılacağını ve bu ayarların kendin aleyhine nasıl çalışabileceğini detaylıca ele alacağız.
Bash’in Varsayılan Davranışı Neden Tehlikeli?
Önce problemi somutlaştıralım. Şu script’e bak:
#!/bin/bash
echo "Yedek alınıyor..."
cp /var/db/production.db /backup/ # Bu başarısız oldu diyelim
echo "Yedek tamamlandı!"
echo "Eski yedekler siliniyor..."
rm -rf /backup/old/
echo "Temizlik bitti!"
Eğer /backup/ dizini yoksa veya izin sorunu varsa cp komutu başarısız olur. Ama script durmaz. “Yedek tamamlandı!” mesajı ekrana gelir, eski yedekler silinir. Elinde yeni yedek yok, eski yedekler de gitmiş. Aferin.
İşte set -e, set -u ve pipefail tam bu noktada devreye giriyor.
set -e: Hata Olunca Dur
set -e (veya uzun formuyla set -o errexit) Bash’e şunu söyler: “Herhangi bir komut sıfır dışı bir exit code ile dönerse, script’i hemen durdur.”
#!/bin/bash
set -e
echo "Başlıyoruz..."
ls /var/olmayan_dizin # Bu hata verir, script burada durur
echo "Bu satıra hiç gelinmez"
Script çalıştırdığında ls komutu başarısız olur ve script orada durur. Çok basit ama çok güçlü.
set -e’nin İnce Noktaları
Burada dikkat edilmesi gereken birkaç nokta var. set -e her durumda beklendiği gibi çalışmaz.
if bloğu içindeki komutlar: if koşulunda kullanılan komutlar set -e‘yi tetiklemez çünkü Bash bu komutların başarısız olabileceğini biliyor:
#!/bin/bash
set -e
# Bu if bloğu içindeki başarısız komut set -e'yi tetiklemez
if ls /olmayan_dizin; then
echo "Dizin var"
else
echo "Dizin yok, devam ediyoruz"
fi
echo "Script devam etti"
&& ve || zincirlerindeki komutlar: || ile kullandığında son komut başarılı olduğu için script durmaz:
#!/bin/bash
set -e
# Bu set -e'yi TETIKLEMEZ çünkü || sayesinde son exit code 0
mkdir /yeni_dizin || echo "Dizin zaten var, tamam"
echo "Devam ediyoruz"
Bu aslında bazen istediğimiz bir davranış. “Komut başarısız olursa şunu yap” mantığını bu şekilde kurabilirsin.
Fonksiyon dönüş değerleri: Bir fonksiyonun son komutu başarısız olursa fonksiyon da başarısız sayılır:
#!/bin/bash
set -e
kontrol_et() {
ls /olmayan_dizin # Bu başarısız olur
}
kontrol_et # Bu fonksiyon çağrısı başarısız olur, script durur
echo "Bu satıra gelinmez"
set -u: Tanımsız Değişkenler Hata Versin
set -u (veya set -o nounset) tanımlanmamış değişkenleri kullanmaya çalıştığında script’i durdurur. Bu özellikle yanlış yazılan değişken isimlerini yakalamak için hayat kurtarıcı.
Klasik senaryo şu:
#!/bin/bash
BACKUP_DIR="/var/backup"
# ...
# 200 satır sonra, yorgunken yazıyorsun:
rm -rf $BACKUP_DRI/* # Dikkat: BACKUP_DRI, BACKUP_DIR değil
set -u olmadan $BACKUP_DRI boş string olarak genişler ve bu komut rm -rf /* haline gelir. Geceleri uyuyamaz hale getiren türden bir hata.
set -u ile:
#!/bin/bash
set -u
BACKUP_DIR="/var/backup"
echo "Yedek dizini: $BACKUP_DIR"
echo "Hatalı değişken: $BACKUP_DRI" # Burada script durur!
Script hemen hata verir: bash: BACKUP_DRI: unbound variable. Kurtarıcı.
set -u ile Varsayılan Değerler
set -u açıkken bazen gerçekten opsiyonel bir değişkenin boş olmasını isteyebilirsin. Bunun için Bash’in parametre genişletme sözdizimini kullanırsın:
#!/bin/bash
set -u
# Değişken tanımlı değilse "varsayilan" kullan
ORTAM=${ORTAM:-"production"}
# Değişken tanımlı değilse boş string kullan
EKSTRA_PARAMETRE=${EKSTRA_PARAMETRE:-""}
# Değişken tanımlı değilse hata mesajı ver ve çık
ZORUNLU_PARAM=${ZORUNLU_PARAM:?"ZORUNLU_PARAM değişkeni tanımlanmalı!"}
echo "Ortam: $ORTAM"
echo "Ekstra: $EKSTRA_PARAMETRE"
:- sözdizimi değişken tanımsız veya boşsa varsayılan değeri kullanır. :? ise değişken tanımsızsa verilen mesajla birlikte script’i sonlandırır. Bunları ezberlersen set -u ile çalışmak çok daha rahat hale gelir.
set -o pipefail: Pipe’larda Gizlenen Hatalar
Bu üçlünün belki de en az bilinen ama en sinsi hataları önleyeni. Bash’te pipe kullandığında (|), varsayılan olarak sadece son komutun exit code’u önemlidir. Önceki komutlar başarısız olsa da kimse fark etmez.
#!/bin/bash
set -e
# set -o pipefail OLMADAN:
# grep başarısız olursa (eşleşme bulamazsa exit 1 döner)
# ama wc -l başarılı olduğu için tüm pipe başarılı sayılır
grep "hata" /var/log/uygulama.log | wc -l
echo "Exit code: $?" # 0 gelir, oysa grep başarısız oldu
set -o pipefail ile pipe içindeki herhangi bir komut başarısız olursa, tüm pipe başarısız sayılır:
#!/bin/bash
set -e
set -o pipefail
# Artık grep başarısız olursa tüm komut başarısız sayılır
grep "kritik_hata" /var/log/uygulama.log | sort | uniq -c
Gerçek dünya senaryosu düşün: Log dosyasını sıkıştırıp S3’e yüklüyorsun:
#!/bin/bash
set -e
set -o pipefail
# pipefail olmadan: gzip başarısız olsa bile aws komutu
# boş bir şey yükler ama script başarılı görünür
gzip -c /var/log/app.log | aws s3 cp - s3://bucket/app.log.gz
pipefail olmadan gzip başarısız olsa AWS’ye boş bir dosya yüklenebilir ve sen “başarılı” bir backup aldığını sanırsın.
Üçünü Birden Kullanan Gerçek Script
Şimdi bu üç ayarı birlikte kullanan, production’da kullanabileceğin bir script örneği görelim:
#!/bin/bash
set -euo pipefail
# set -e: herhangi bir komut hata verirse dur
# set -u: tanımsız değişken kullanılırsa dur
# set -o pipefail: pipe içinde hata varsa dur
# Kısa yazım: set -euo pipefail
# Sabitler
readonly BACKUP_DIR="/var/backup/mysql"
readonly DB_NAME="${DB_NAME:?"DB_NAME ortam değişkeni tanımlanmalı"}"
readonly DB_USER="${DB_USER:-"backup_user"}"
readonly RETENTION_DAYS="${RETENTION_DAYS:-7}"
readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S)
readonly BACKUP_FILE="${BACKUP_DIR}/backup_${DB_NAME}_${TIMESTAMP}.sql.gz"
# Temizlik fonksiyonu - script herhangi bir şekilde bitince çalışır
temizlik() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "HATA: Script başarısız oldu (exit code: $exit_code)" >&2
# Yarım kalan yedek dosyasını sil
[ -f "$BACKUP_FILE" ] && rm -f "$BACKUP_FILE"
fi
}
trap temizlik EXIT
# Yedek dizini oluştur (yoksa)
mkdir -p "$BACKUP_DIR"
echo "[$TIMESTAMP] Yedek alınıyor: $DB_NAME"
# mysqldump + gzip pipe - pipefail sayesinde mysqldump hatası da yakalanır
mysqldump
--user="$DB_USER"
--single-transaction
--routines
--triggers
"$DB_NAME" | gzip -9 > "$BACKUP_FILE"
echo "Yedek boyutu: $(du -sh "$BACKUP_FILE" | cut -f1)"
# Eski yedekleri temizle
echo "Eski yedekler temizleniyor ($RETENTION_DAYS günden eski)..."
find "$BACKUP_DIR" -name "backup_${DB_NAME}_*.sql.gz"
-mtime "+${RETENTION_DAYS}" -delete
echo "Yedek başarıyla tamamlandı: $BACKUP_FILE"
Bu script’te dikkat et: set -euo pipefail hepsini tek satırda yazabilirsin. trap ile EXIT sinyali yakalayarak temizlik yapıyoruz, bu sayede script başarılı da olsa başarısız da olsa temizlik fonksiyonu çalışıyor.
trap ile Hata Yönetimi
set -e ile birlikte trap ERR kullanmak hata ayıklamayı çok kolaylaştırır. Hangi satırda hata olduğunu görebilirsin:
#!/bin/bash
set -euo pipefail
hata_yakala() {
local exit_code=$?
local satir_no=$1
echo "HATA: Satır $satir_no'de başarısız oldu (exit code: $exit_code)" >&2
echo "Komut: ${BASH_COMMAND}" >&2
}
trap 'hata_yakala $LINENO' ERR
echo "Adım 1: Dizin kontrol ediliyor..."
ls /olmayan_dizin
echo "Bu satıra gelinmez"
Çıktı şuna benzer bir şey olur:
Adım 1: Dizin kontrol ediliyor...
HATA: Satır 12'de başarısız oldu (exit code: 2)
Komut: ls /olmayan_dizin
Hem EXIT hem de ERR trap’lerini birlikte kullanmak oldukça güçlü bir pattern:
#!/bin/bash
set -euo pipefail
GECICI_DOSYA=""
temizlik() {
[ -n "$GECICI_DOSYA" ] && [ -f "$GECICI_DOSYA" ] && rm -f "$GECICI_DOSYA"
}
hata_yakala() {
echo "HATA OLUŞTU - Satır: $1, Komut: ${BASH_COMMAND}" >&2
}
trap temizlik EXIT
trap 'hata_yakala $LINENO' ERR
GECICI_DOSYA=$(mktemp)
echo "Geçici dosya: $GECICI_DOSYA"
# İşlemler...
grep "pattern" /var/log/app.log > "$GECICI_DOSYA"
wc -l < "$GECICI_DOSYA"
# Temizlik trap sayesinde otomatik yapılır
set -e’nin Beklenmez Davrandığı Durumlar
set -e kullanırken bazı durumların farklı davrandığını bilmek gerekiyor. Bunları bilmeden script yazarsan başın ağrıyabilir.
Subshell’lerde: set -e subshell’lerde de devralınır ama bazı ince noktalar var:
#!/bin/bash
set -e
# Subshell hata verirse üst shell de etkilenir
sonuc=$(ls /olmayan_dizin) # Script burada durur
# Ama bunu kontrol etmek istersen:
if sonuc=$(ls /olmayan_dizin 2>/dev/null); then
echo "Dizin içeriği: $sonuc"
else
echo "Dizin bulunamadı"
fi
Aritmetik ifadelerde: Bu çok sürpriz bir davranış. (( )) aritmetik ifadeler 0 döndürdüğünde (yani sonuç sıfır olduğunda) başarısız sayılır!
#!/bin/bash
set -e
SAYAC=1
# BU SCRIPT'I DURDURUR! Çünkü (( 1 - 1 )) = 0, yani false
(( SAYAC-- )) # SAYAC şimdi 0, ama bu "başarısız" sayılır
echo "Bu satıra gelinmez"
Çözüm:
#!/bin/bash
set -e
SAYAC=1
# Güvenli yol:
SAYAC=$(( SAYAC - 1 )) || true
# veya:
(( SAYAC-- )) || true
# veya set -e'yi geçici kapat:
set +e
(( SAYAC-- ))
set -e
echo "SAYAC: $SAYAC"
|| true eklemek, komutun exit code’unu her zaman 0 yaparak set -e‘yi devre dışı bırakır. Ama bunu bilinçsizce her yere eklemek set -e‘nin amacını bozar, dikkatli kullan.
Geçici Olarak Devre Dışı Bırakmak
Bazen bir komutun başarısız olmasına izin vermek istersin. Bunun için set +e kullanırsın (- aktif eder, + devre dışı bırakır):
#!/bin/bash
set -euo pipefail
# Hizmet zaten çalışmıyor olabilir, hata olsa da devam et
set +e
systemctl stop eski-servis
set -e
# Artık tekrar aktif
systemctl start yeni-servis
Ya da daha temiz bir yol:
#!/bin/bash
set -euo pipefail
# Tek satır için || true
systemctl stop eski-servis || true
systemctl start yeni-servis
# Dönen değeri almak istiyorsan
ping -c 1 8.8.8.8 || agbağlantisi_yok=true
if [ "${agbağlantisi_yok:-false}" = "true" ]; then
echo "Ağ bağlantısı yok!"
fi
Tam Bir Production Script Şablonu
İşte yukarıdaki her şeyi entegre eden, production’da başlangıç noktası olarak kullanabileceğin bir şablon:
#!/bin/bash
# =============================================================================
# Script Adı: deploy.sh
# Açıklama : Uygulama deployment scripti
# Kullanım : ./deploy.sh [ortam] [versiyon]
# =============================================================================
set -euo pipefail
# Renk kodları (terminal destekliyorsa)
KIRMIZI='33[0;31m'
YESIL='33[0;32m'
SARI='33[1;33m'
SIFIRLA='33[0m'
log_info() { echo -e "${YESIL}[INFO]${SIFIRLA} $(date '+%H:%M:%S') $*"; }
log_warn() { echo -e "${SARI}[WARN]${SIFIRLA} $(date '+%H:%M:%S') $*" >&2; }
log_hata() { echo -e "${KIRMIZI}[HATA]${SIFIRLA} $(date '+%H:%M:%S') $*" >&2; }
# Kullanım mesajı
kullanim() {
echo "Kullanım: $0 <ortam> <versiyon>"
echo " ortam : staging|production"
echo " versiyon: uygulama versiyonu (örn: 1.2.3)"
exit 1
}
# Argüman kontrolü
[ $# -ne 2 ] && kullanim
readonly ORTAM="$1"
readonly VERSIYON="$2"
readonly DEPLOY_DIR="/opt/uygulama"
readonly BACKUP_DIR="/opt/uygulama-backup"
readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Ortam doğrulama
[[ "$ORTAM" =~ ^(staging|production)$ ]] || {
log_hata "Geçersiz ortam: $ORTAM"
kullanim
}
# Temizlik ve hata yakalama
ROLLBACK_GEREKLI=false
temizlik() {
local cikis_kodu=$?
if [ $cikis_kodu -ne 0 ] && [ "$ROLLBACK_GEREKLI" = "true" ]; then
log_warn "Hata tespit edildi, rollback başlatılıyor..."
rollback
fi
}
trap temizlik EXIT
rollback() {
log_info "Önceki versiyona dönülüyor..."
if [ -d "${BACKUP_DIR}/son_basarili" ]; then
rsync -a "${BACKUP_DIR}/son_basarili/" "${DEPLOY_DIR}/"
systemctl restart uygulama
log_info "Rollback tamamlandı"
else
log_hata "Rollback yapılamadı: yedek bulunamadı"
fi
}
# Ana deployment akışı
log_info "Deployment başlıyor: $VERSIYON -> $ORTAM"
log_info "Mevcut versiyon yedekleniyor..."
mkdir -p "$BACKUP_DIR"
rsync -a "${DEPLOY_DIR}/" "${BACKUP_DIR}/son_basarili/"
ROLLBACK_GEREKLI=true
log_info "Yeni paket indiriliyor..."
curl -fsSL "https://repo.sirket.com/releases/uygulama-${VERSIYON}.tar.gz"
| tar -xz -C "$DEPLOY_DIR"
log_info "Dependency'ler güncelleniyor..."
cd "$DEPLOY_DIR"
npm ci --production 2>&1 | grep -v "^npm warn" || true
log_info "Servis yeniden başlatılıyor..."
systemctl restart uygulama
log_info "Health check yapılıyor..."
sleep 3
curl -fsSL http://localhost:3000/health | grep -q '"status":"ok"'
ROLLBACK_GEREKLI=false
log_info "Deployment başarıyla tamamlandı: $VERSIYON"
set -x ile Debug Modunu Birleştirmek
Son olarak, hata ayıklarken set -x‘i de bu üçlüyle birlikte kullanabilirsin. set -x her çalıştırılan komutu ekrana yazdırır:
#!/bin/bash
set -euo pipefail
# Debug için başlat ya da ortam değişkeniyle kontrol et
[ "${DEBUG:-0}" = "1" ] && set -x
echo "Debug modu: ${DEBUG:-kapalı}"
Scripti DEBUG=1 ./script.sh şeklinde çalıştırınca tüm komutları görebilirsin. Production’da DEBUG=0 olur, geliştirme veya hata ayıklama sırasında DEBUG=1.
Sonuç
set -euo pipefail üçlüsü Bash scriptlerinin sağlamlığını dramatik şekilde artıran, yazmaya başlaması bir dakika süren ama senelerce baş ağrısından kurtaran bir pratiktir. Her yeni script’e bu üç satırla başlamayı alışkanlık haline getir.
Özet olarak ne yapıyorlar:
- set -e: Herhangi bir komut başarısız olduğunda script’i durdurur, hataların sessizce geçmesini engeller
- set -u: Tanımsız değişken kullanımını hata olarak işaretler, yazım hatalarını erken yakalar
- set -o pipefail: Pipe zincirinde herhangi bir komut başarısız olursa tüm pipe’ı başarısız sayar
Bu üçünün de ince noktaları var, özellikle set -e‘nin if blokları, || zincirleri ve aritmetik ifadelerde farklı davrandığını unutma. || true kullanımını gerektiğinde kullan ama körü körüne her yere ekleme.
Gerçek güç ise bunları trap, düzgün loglama ve rollback mekanizmalarıyla birleştirdiğinde ortaya çıkıyor. Production ortamında çalışan bir script, sadece başarılı senaryolar için değil, başarısız senaryolar için de tasarlanmış olmalı. Bu üçlü tam da bu felsefenin temel taşı.