Şubat 5 2026

Bash’te Hata Yönetimi: set -euo pipefail, Trap ve Bats ile Defansif Scripting

Gece saat 03:00. PagerDuty çalıyor. Sorun: Kubernetes cluster’ına yeni bir deployment çıkılırken pipeline yarıda kalmış ama deploy başarılıymış gibi davranıp eski stabil replicaları da temizlemiş. Loglara bakıyorsunuz; kritik bir environment variable tanımlanmadığı için script sessizce patlamış, fakat exit code 0 döndüğü için CI/CD pipeline’ı her şeyin yolunda olduğunu varsayarak devam etmiş. Hepimiz bu filmi en az bir kere izledik. Günümüz modern altyapılarında bash scripting ve linux tabanlı otomasyon süreçleri, devops mühendisliğinin görünmez omurgasını oluşturuyor. Ancak bu omurga genellikle oldukça kırılgan temeller üzerine kurulu.

Bu makalede, Bash betiklerinizi kurumsal seviyede, hata toleransı yüksek ve test edilebilir yapılara dönüştürmenin yollarını inceleyeceğiz. Temel kavramları bir kenara bırakıp, doğrudan production ortamlarında hayat kurtaran pratik yaklaşımlara odaklanacağız.

Sessiz Katilleri Durdurun: “set -euo pipefail” Anatomisi

Bash, varsayılan olarak aşırı iyimser bir kabuktur. Bir komut hata verse de, bir değişken tanımlanmamış olsa da “yola devam et” felsefesini benimser. Bu felsefe interaktif terminal kullanımı için harika olsa da, otomasyon scriptleri için tam bir felakettir. Bu felaketi önlemenin ilk adımı, script’lerin başına o meşhur üçlüyü eklemektir:

set -euo pipefail

Peki bu parametreler arka planda tam olarak ne yapar ve neden hayati önem taşırlar? Tek tek inceleyelim.

1. set -e (errexit)

Bu parametre, script içindeki herhangi bir komut non-zero (sıfır dışı) bir exit code ile sonuçlandığında script’in anında sonlandırılmasını sağlar. Varsayılan davranışta Bash, komut hata alsa bile bir sonraki satıra geçmeye çalışır.

Neden kritik? Aşağıdaki senaryoyu düşünün:

cd /tmp/non_existent_directory
rm -rf *

Eğer ilk satırdaki cd komutu dizin bulunamadığı için başarısız olursa ve set -e aktif değilse, script bir sonraki satıra geçer ve o an hangi dizinde bulunuyorsa (muhtemelen script’in çalıştığı root dizini) oradaki tüm dosyaları siler. set -e bu felaketi engeller.

2. set -u (nounset)

Eğer tanımlanmamış bir değişkeni okumaya çalışırsanız, Bash bunu boş bir string olarak kabul eder ve hata vermez. set -u, tanımlanmamış her değişkeni ölümcül bir hata (fatal error) olarak kabul eder ve script’i durdurur.

Neden kritik?

# set -u aktif değilse:
TARGET_DIR="" # ya da yanlışlıkla typo yapılmış bir değişken
rm -rf "${TARGET_DIR}/bin" # Sistem "rm -rf /bin" komutunu çalıştırır!

set -u aktif olduğunda, Bash bu satıra geldiğinde TARGET_DIR: unbound variable hatası fırlatacak ve execution’ı hemen durduracaktır.

3. set -o pipefail

Bash’te pipeline (boru) işlemlerinde varsayılan olarak sadece son komutun exit code’u dikkate alınır. Örneğin komut1 | komut2 | komut3 zincirinde komut1 patlasa bile, komut3 başarılı olursa tüm pipeline başarılı (exit 0) sayılır.

Neden kritik?

# pipefail aktif değilse:
curl -s https://invalid-url-destination.tar.gz | tar -xzf -
echo $? # Çıktı: 0

Yukarıdaki örnekte curl başarısız olacak ve hiçbir veri indiremeyecektir. Ancak tar komutu boş girdi aldığında (veya pipe kapandığında) hata üretse de, exit code bazen yanıltıcı olabilir. Daha da kötüsü, aradaki kritik log analiz araçlarında hatalar yutulur. set -o pipefail aktif edildiğinde, pipeline içindeki en sağdaki non-zero exit code tüm pipeline’ın dönüş kodu kabul edilir.

İstisnaları Yönetmek: set -e Altında Güvenli Hata Toleransı

set -e kullanmaya başladığınızda karşılaşacağınız ilk sorun, bazı komutların hata vermesinin aslında beklenen bir durum olmasıdır. Örneğin bir dizinin varlığını kontrol etmek veya grep ile bir log dosyasında spesifik bir kelimeyi aramak gibi durumlarda, exit code’un 1 gelmesi script’i patlatmamalıdır.

Hata Kodunu Güvenli Bir Şekilde Absorbe Etmek

Eğer bir komutun başarısız olabileceğini biliyor ve bunun script’i durdurmasını istemiyorsanız, OR (||) operatörünü kullanarak hata durumunu bypass edebilirsiniz:

# grep satır bulamazsa normalde exit 1 verir ve script set -e yüzünden durur.
# || true kullanarak bunu engelliyoruz.
result=$(grep "ERROR" /var/log/app.log || true)

# Veya alternatif olarak lokal bir hata değişkenine atayabilirsiniz:
grep "CRITICAL" /var/log/app.log || local_exit=$?
if [ ${local_exit:-0} -ne 0 ]; then
    echo "Kritik log bulunamadı, ama yola devam ediyoruz."
fi

Bir diğer profesyonel yaklaşım ise conditional block’lar kullanmaktır. if, elif, while veya until ifadelerinin içinde çalıştırılan komutlar, hata verseler dahi set -e engeline takılmazlar:

# Bu blok set -e aktif olsa bile güvenle çalışır
if ! ping -c 1 8.8.8.8 > /dev/null 2>&1; then
    echo "İnternet bağlantısı yok, ancak script devam ediyor."
fi

Trap Mekanizması: Kaynakları Temizlemek ve Graceful Shutdown

Script’iniz ister başarıyla tamamlansın, ister yarıda hata alıp dursun; arkasında çöp bırakmamalıdır. Geçici dosyalar (temp files), oluşturulan socket’ler, kilit dosyaları (lock files) veya aktif edilen SSH tünelleri her durumda temizlenmelidir. İşte bu noktada Linux kernel sinyallerini yakalayan trap devreye girer.

Aşağıdaki boilerplate şablonu, production seviyesindeki script’lerinizde güvenle kullanabilirsiniz:

#!/usr/bin/env bash

set -euo pipefail

# Geçici dizin oluşturuluyor
TMP_DIR=$(mktemp -d -t my-app-XXXXXX)
echo "Geçici çalışma alanı: ${TMP_DIR}"

# Temizlik fonksiyonu
cleanup() {
    local exit_code=$?
    echo "Temizlik işlemi başlatılıyor... Son exit code: ${exit_code}"
    rm -rf "${TMP_DIR}"
    
    # Eğer script bir hata yüzünden durduysa ek bildirimler buraya yazılabilir (Slack webhook vb.)
    if [ ${exit_code} -ne 0 ]; then
        echo "Script beklenmedik bir şekilde sonlandı!" >&2
    fi
}

# EXIT sinyalini yakala. Script nasıl biterse bitsin cleanup çalışacak.
trap cleanup EXIT

Neden EXIT Sinyali?

Birçok junior geliştirici trap için sadece ERR veya INT sinyallerini tanımlar. Ancak EXIT pseudosignal’i, script normal yollarla bittiğinde de, set -e yüzünden yarıda kesildiğinde de veya bir syntax hatası alındığında da tetiklenir. Bu yüzden tek bir merkezden temizlik yönetimi için en güvenli limandır.

Exit Code Standartları: Kurumsal Entegrasyon

Yazdığınız script’lerin diğer otomasyon araçları (Ansible, Jenkins, ArgoCD vb.) tarafından doğru yorumlanabilmesi için POSIX standartlarına uygun exit code’lar dönmesi gerekir. Sadece exit 1 yazıp geçmek, hata analizi yaparken size zaman kaybettirir.

Exit Code Anlamı Kullanım Senaryosu
0 Success Script görevini başarıyla tamamladı.
1 Catchall for general errors Genel hatalar (izin yetersizliği, parse hataları vb.)
2 Misuse of shell builtins Eksik veya hatalı argüman kullanımı.
126 Command invoked cannot execute Çalıştırılmak istenen dosya execute yetkisine sahip değil.
127 Command not found Script içindeki bir binary sistemde yüklü değil.

Scriptlerinizde custom hata kodları tanımlarken 64-113 (BSD standardı) veya 128’den büyük olmayan serbest kodları (örneğin 10, 20, 30 gibi) kullanmanız, sistem hata kodlarıyla çakışmayı önler:

readonly ERR_INVALID_ARG=2
readonly ERR_DB_CONNECTION=10
readonly ERR_DISK_FULL=11

if [ $# -lt 1 ]; then
    echo "Hata: En az bir argüman girmelisiniz." >&2
    exit ${ERR_INVALID_ARG}
fi

Bats (Bash Automated Testing System) ile Unit Test Yazımı

“Bash script’inin testi mi olur?” demeyin. Eğer yazdığınız script kritik bir deploy sürecini yönetiyorsa, her değişiklikten sonra manuel test yapmak hem riskli hem de zaman alıcıdır. Bats, Bash script’lerinizi gerçek birer yazılım projesi gibi test etmenizi sağlar.

Öncelikle test edeceğimiz basit ama fonksiyonel bir script yazalım (isim: deploy.sh):

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

deploy_artifact() {
    local env=$1
    if [ -z "${env}" ]; then
        echo "Hata: Environment belirtilmedi." >&2
        return 2
    fi
    
    if [ "${env}" == "prod" ]; then
        echo "Production deployment başarılı."
        return 0
    else
        echo "Bilinmeyen environment: ${env}" >&2
        return 10
    fi
}

# Script doğrudan çalıştırıldıysa (test import etmediyse) main'i çalıştır
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    deploy_artifact "${1:-}"
fi

Şimdi bu script için yazacağımız unit test dosyasına bakalım (isim: deploy.bats):

#!/usr/bin/env bats

# Test öncesi script'i load et
setup() {
    source ./deploy.sh
}

@test "Parametre verilmediğinde script exit code 2 ile patlamalı" {
    run deploy_artifact ""
    [ "$status" -eq 2 ]
    [ "$output" = "Hata: Environment belirtilmedi." ]
}

@test "Prod environment sağlandığında başarılı dönmeli" {
    run deploy_artifact "prod"
    [ "$status" -eq 0 ]
    [ "$output" = "Production deployment başarılı." ]
}

@test "Bilinmeyen env verildiğinde exit code 10 dönmeli" {
    run deploy_artifact "staging"
    [ "$status" -eq 10 ]
}

Bu testleri lokalinizde veya CI/CD pipeline’ınızda çalıştırmak için tek yapmanız gereken bats binary’sini çağırmaktır:

bats deploy.bats

Sonuçlar yeşil yandığında, refactoring yaparken hiçbir şeyi bozmadığınızdan emin olarak production’a güvenle push edebilirsiniz.

Sonuç: Defansif Scripting’i Alışkanlık Haline Getirin

Bash scripting, küçümsenen ama sistem mühendisliğinin en kritik katmanlarından biridir. Script yazarken kodun sadece “mutlu senaryoda” (happy path) nasıl çalışacağını değil, nerede ve nasıl patlayabileceğini tasarlamak gerekir. set -euo pipefail ile sessiz hataların önüne geçmek, trap ile sistem kaynaklarını her durumda temiz bırakmak ve bats ile bu süreçleri test edilebilir kılmak, sizi sıradan bir script yazıcısından profesyonel bir sistem mimarına dönüştürür.

Category: Genel | LEAVE A COMMENT
Ağustos 23 2024

Bash Script’lerinde Kurşun Geçirmez Hata Yönetimi: set -euo pipefail ve Ötesi

Hepimiz oradaydık: Gece yarısı gelen bir PagerDuty alarmı, yarıda kesilmiş bir CI/CD pipeline’ı ve sessizce hata fırlatıp “başarılı” (exit 0) olarak sonlanan bir bash script’i. Linux sistemlerde scripting yaparken, özellikle modern devops ve otomasyon süreçlerinde, hata yönetimi lüks değil bir zorunluluktur. Varsayılan olarak Bash, adeta bir nihilist gibi davranır: Adımlardan biri başarısız olsa bile, hiçbir şey olmamış gibi bir sonraki satıra geçer. Bu makalede, bu vurdumduymazlığı nasıl dizginleyeceğimizi ve prod-ready scriptler yazacağımızı inceleyeceğiz.

5+ yıllık deneyime sahip bir mühendis olarak muhtemelen set -e komutunu duymuş, hatta şablonlarınıza eklemişsinizdir. Ancak bu buzdağının sadece görünen kısmı. Gelin, işi profesyonel seviyeye taşıyalım.

Sihirli Üçlü: set -euo pipefail

Bash script’lerinizin başına ekleyeceğiniz bu sihirli satır, hata yönetiminizin temel taşıdır. Peki ama neden bu üç parametre birlikte kullanılmalı? Her birinin arka planda çözdüğü spesifik problemi inceleyelim.

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

1. set -e (errexit)

Herhangi bir komut sıfırdan farklı bir exit code ile dönerse, script’in çalışmasını anında durdurur.

Neden gerekli? Varsayılan senaryoda, diskte yer kalmadığı için başarısız olan bir tar komutundan sonra script çalışmaya devam eder ve bir sonraki adımdaki silme işlemini tetikleyebilir. set -e bunu engeller. Ancak alt kabuklarda (subshell) veya mantıksal operatörlerde (&&, ||) her zaman beklediğiniz gibi davranmaz. Bu yüzden tek başına kurtarıcı değildir.

2. set -u (nounset)

Tanımlanmamış bir değişken kullanılmaya çalışıldığında script’i hata vererek durdurur.

Neden gerekli? Şu felaket senaryosunu düşünün:

# set -u OLMADAN
TARGET_DIR="" # Bir hata sonucu boş kaldı
rm -rf "$TARGET_DIR/*" # Tebrikler, kök dizini (root) sildiniz.

Eğer set -u aktif olsaydı, Bash daha rm komutuna geçmeden TARGET_DIR is not set hatasıyla işlemi sonlandıracaktı.

3. set -o pipefail

Pipeline (boru hattı) içerisindeki herhangi bir komut hata aldığında, tüm pipeline’ın exit code’unu en son hata alan komutun kodu olarak belirler.

Neden gerekli? Bash varsayılan olarak sadece pipeline’ın en sonundaki komutun exit code’una bakar.

# set -o pipefail OLMADAN
non_existent_command | echo "Merhaba"
echo $? # Çıktı: 0 (Çünkü echo başarılı oldu!)

Yukarıdaki örnekte ilk komut crash olsa bile pipeline başarılı sayılır. set -o pipefail ile bu durum engellenir ve ilk komutun hatası tüm hattı başarısız kılar.

İstisnaları Yönetmek: “Peki ya hata almasını bekliyorsam?”

set -e kullandığınızda, bazen hata vermesi normal olan komutlar da script’inizi sonlandırır. Örneğin, bir portun açık olup olmadığını nc ile kontrol etmek istiyorsunuz:

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

# nc başarısız olursa script burada ölecektir.
nc -z -w3 10.0.0.5 80

echo "Bu satıra asla ulaşılamayacak."

Bunu aşmanın “temiz” yolu, komutun sonuna mantıksal || true eklemek veya hata durumunu inline olarak handle etmektir:

# Güvenli yaklaşım 1: || true ile bypass etmek
nc -z -w3 10.0.0.5 80 || true

# Güvenli yaklaşım 2: Hata durumunda alternatif aksiyon almak
if ! nc -z -w3 10.0.0.5 80; then
    echo "Port kapalı, alternatif akışa geçiliyor..."
    # Burası script'i durdurmaz çünkü if bloğu içindeyiz
fi

Trap Mekanizması ile Graceful Degradation ve Cleanup

Script’iniz yarıda kesildiğinde (ister hata sebebiyle, ister kullanıcı CTRL+C’ye bastığında), arkasında çöp bırakmamalıdır. Geçici dosyalar (temporary files), kilit mekanizmaları (lock files) veya açık SSH tünelleri mutlaka temizlenmelidir. İşte burada trap devreye girer.

Bash’teki sinyalleri (signals) yakalayarak, script sonlanırken mutlaka çalıştırılacak bir cleanup fonksiyonu tanımlayabiliriz:

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

# Geçici bir dizin oluşturalım
TMP_DIR=$(mktemp -d)
echo "Geçici dizin oluşturuldu: ${TMP_DIR}"

# Cleanup fonksiyonumuz
cleanup() {
    local exit_code=$?
    echo "Temizlik yapılıyor: ${TMP_DIR} siliniyor..."
    rm -rf "${TMP_DIR}"
    
    if [ $exit_code -ne 0 ]; then
        echo "Script HATA ile sonlandı! Exit code: ${exit_code}"
    else
        echo "Script başarıyla tamamlandı."
    fi
    exit $exit_code
}

# EXIT sinyalini yakala (Hata olsun ya da olmasın, script çıkarken çalışır)
trap cleanup EXIT

# İş yükü simülasyonu
echo "İşlem yapılıyor..."
touch "${TMP_DIR}/important_data.tmp"

# Hata simülasyonu (Burada script patlayacak ama trap çalışacak!)
grep "olmayan_pattern" "${TMP_DIR}/important_data.tmp"

Standartlara Uygun Exit Code Kullanımı

Gelişigüzel exit 1 yazıp geçmek, üst seviye bir otomasyonda kabul edilemez. Script’inizin çağıran sisteme (örneğin Jenkins, GitHub Actions veya bir cron job) neyin yanlış gittiğini net bir şekilde söylemesi gerekir.

POSIX standartlarına ve /usr/include/sysexits.h yapısına uygun olarak şu kodları tercih edin:

  • 0: Başarılı operasyon.
  • 64 (EX_USAGE): Yanlış CLI argüman kullanımı.
  • 69 (EX_UNAVAILABLE): Servis veya kaynak ulaşılamaz durumda.
  • 70 (EX_SOFTWARE): İçsel yazılım hatası (Internal error).
  • 74 (EX_IOERR): Giriş/Çıkış (I/O) hatası (Disk dolu, dosya yazılamadı).
# Örnek kullanım
if [[ $# -lt 2 ]]; then
    echo "Hata: Eksik parametre. Kullanım: $0  " >&2
    exit 64
fi

Script’lerinizi Test Edin: BATS (Bash Automated Testing System)

SRE dünyasında “test edilmeyen kod çalışmıyordur” kuralı geçerlidir. Bash script’lerinizi manuel test etmek yerine, declarative testler yazmak için bats-core kullanabilirsiniz.

Önce basit bir script’imiz olsun (deploy.sh):

#!/usr/bin/env bash
# deploy.sh
set -euo pipefail

check_env() {
    if [[ -z "${APP_ENV:-}" ]]; then
        echo "Hata: APP_ENV tanımlı değil!" >&2
        return 64
    fi
}

check_env
echo "Deploying to ${APP_ENV}"

Şimdi bu script için yazacağımız BATS test dosyası (deploy.bats):

#!/usr/bin/env bats

# Test 1: APP_ENV tanımlı değilse script 64 koduyla hata vermeli
@test "APP_ENV set edilmediğinde script hata fırlatmalı" {
  run ./deploy.sh
  [ "$status" -eq 64 ]
  [[ "$output" =~ "Hata: APP_ENV tanımlı değil!" ]]
}

# Test 2: APP_ENV tanımlı olduğunda başarılı olmalı
@test "APP_ENV set edildiğinde script başarılı olmalı" {
  export APP_ENV="production"
  run ./deploy.sh
  [ "$status" -eq 0 ]
  [[ "$output" =~ "Deploying to production" ]]
}

Bu testleri CI pipeline’ınızda bats deploy.bats komutuyla çalıştırarak, script’lerinizin gelecekteki değişikliklerde kırılmasını engelleyebilirsiniz.

Özet

Bash’te hata yönetimi, sadece set -e yazıp şansımıza güvenmekten ibaret değildir. Profesyonel otomasyon süreçlerinde, pipefail ile boru hatlarını güvenceye almak, trap ile arkamızı temizlemek, standart exit code’lar ile çağıran sistemlere anlamlı yanıtlar dönmek ve nihayetinde BATS ile bu davranışları test etmek gerekir. Unutmayın, iyi bir DevOps mühendisi sadece çalışan kod değil, güvenle patlayan kod yazar.

Category: Genel | LEAVE A COMMENT