Bash’te Fonksiyon Tanımlama ve Parametre Kullanımı

Bash script yazarken en çok gözardı edilen ama en çok işe yarayan şey fonksiyonlardır. “Şu kodu kopyala yapıştır yeter” diye başlayıp sonunda yüzlerce satırlık, bakımı imkansız scriptler yazanlardan biri olduysanız, bu yazı tam size göre. Fonksiyonlar olmadan ciddi bir otomasyon scripti yazmak, tuş takımı olmadan konsol yönetmeye çalışmak gibidir. Teknik olarak mümkün ama gereksiz yere acı çekersiniz.

Bash’te Fonksiyon Nedir, Neden Kullanmalısınız?

Fonksiyon, bir kez tanımlayıp defalarca çağırabileceğiniz, isimlendirilmiş kod bloklarıdır. Sysadmin dünyasında bu çok kritik bir kavram çünkü işlerimizin büyük kısmı tekrar eden görevlerden oluşur. Sunucu kurulumu, log rotasyonu, yedek alma, servis kontrolü… Bunların hepsi benzer adımları tekrar eder.

Fonksiyon kullanmanın somut faydaları şunlardır:

  • Kod tekrarını önler: Aynı mantığı 5 farklı yere yazmak yerine bir kez tanımlarsınız.
  • Bakımı kolaylaştırır: Bir şeyi değiştirmeniz gerektiğinde tek yerden değiştirirsiniz.
  • Okunabilirliği artırır: check_disk_space fonksiyonu çağırdığınızda ne yaptığı anlaşılır.
  • Test edilebilirlik: Fonksiyonları ayrı ayrı test edebilirsiniz.
  • Hata yönetimini merkezileştirir: Bir hata işleme fonksiyonu yazıp her yerden çağırırsınız.

Fonksiyon Tanımlama Sözdizimi

Bash’te fonksiyon tanımlamanın iki yolu vardır ve ikisi de geçerlidir:

#!/bin/bash

# Yontem 1: function anahtar kelimesiyle
function merhaba() {
    echo "Merhaba Dunya!"
}

# Yontem 2: Sadece parantezle (POSIX uyumlu)
merhaba_posix() {
    echo "Merhaba POSIX Dunya!"
}

# Fonksiyonlari cagirma
merhaba
merhaba_posix

Ben genellikle ikinci yöntemi tercih ederim çünkü daha kısa ve POSIX uyumludur, yani sh ile de çalışır. Ama ekip ortamında çalışıyorsanız tutarlı olmanız yeterli, hangisini seçtiğinizden çok hepsinin aynı şekilde yazılması önemlidir.

Önemli bir nokta: Fonksiyonları çağırmadan önce tanımlamanız gerekir. Bash scripti yukarıdan aşağıya okur, henüz görmediği bir fonksiyonu çağıramazsınız. Bu yüzden iyi bir pratik, tüm fonksiyonlarınızı scriptin başında tanımlayıp, ana mantığı en altta çalıştırmaktır.

Parametreler ve Argümanlar

Bash fonksiyonlarına argüman geçmek, diğer dillerdeki gibi parantez içinde parametre adı yazmakla olmaz. Bunun yerine pozisyonel parametreler kullanılır:

  • $1: Birinci argüman
  • $2: İkinci argüman
  • $@: Tüm argümanlar (ayrı ayrı)
  • $*: Tüm argümanlar (tek string olarak)
  • $#: Argüman sayısı
  • $0: Scriptin adı (fonksiyon içinde hala scriptin adını verir)
#!/bin/bash

kullanici_olustur() {
    local kullanici_adi=$1
    local kullanici_grubu=$2
    local ev_dizini="/home/$kullanici_adi"

    if [ -z "$kullanici_adi" ]; then
        echo "HATA: Kullanici adi belirtilmedi!" >&2
        return 1
    fi

    echo "Kullanici olusturuluyor: $kullanici_adi"
    echo "Grup: ${kullanici_grubu:-users}"  # Varsayilan deger
    echo "Ev dizini: $ev_dizini"

    # Gercek ortamda bu satiri aktif ederdiniz:
    # useradd -m -g "${kullanici_grubu:-users}" -d "$ev_dizini" "$kullanici_adi"
}

kullanici_olustur "ahmet" "webteam"
kullanici_olustur "mehmet"  # Grup belirtilmedi, varsayilan kullanilir
kullanici_olustur  # Hata durumu test ediliyor

Burada dikkat etmeniz gereken birkaç şey var. local anahtar kelimesi çok önemli. Bunu kullanmazsanız değişkenleriniz global scope’ta kalır ve başka fonksiyonlarla çakışabilir. Bu tür hatalar bulmak son derece zordur, saatlerce debug yaparsınız.

${kullanici_grubu:-users} sözdizimi varsayılan değer atamanın bash yoludur. Eğer kullanici_grubu boşsa veya tanımlı değilse users değerini kullanır.

Return Değerleri ve Çıkış Kodları

Bash fonksiyonları diğer dillerdeki gibi gerçek anlamda değer döndürmez. return komutu sadece 0-255 arası bir çıkış kodu döner. Bu çıkış kodunu $? ile yakalarsınız.

#!/bin/bash

servis_kontrol() {
    local servis_adi=$1

    if systemctl is-active --quiet "$servis_adi" 2>/dev/null; then
        echo "$servis_adi calisiyor"
        return 0  # Basari
    else
        echo "$servis_adi calismıyor"
        return 1  # Basarisizlik
    fi
}

# Kullanim
servis_kontrol "nginx"
if [ $? -eq 0 ]; then
    echo "Her sey yolunda"
else
    echo "Servis problemi var, aksiyon gerekiyor"
fi

# Daha temiz kullanim
if servis_kontrol "postgresql"; then
    echo "Veritabani hazir"
fi

Gerçek değer döndürmek istiyorsanız iki yöntem vardır. Birincisi echo ile stdout’a yazdırıp command substitution ile yakalamak, ikincisi global değişken kullanmak. Ben genellikle birinci yöntemi tercih ederim:

#!/bin/bash

disk_kullanim_yuzde() {
    local dizin=${1:-/}
    local yuzde

    yuzde=$(df "$dizin" | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
    echo "$yuzde"
}

# Command substitution ile degeri yakalamak
kullanim=$(disk_kullanim_yuzde "/")
echo "Disk kullanimi: %$kullanim"

kullanim_var=$(disk_kullanim_yuzde "/var")
if [ "$kullanim_var" -gt 85 ]; then
    echo "UYARI: /var dizini dolmak uzere! (%$kullanim_var)"
fi

Gerçek Dünya Senaryosu: Sunucu Sağlık Kontrol Scripti

Şimdi öğrendiklerimizi gerçek bir senaryoya uygulayalım. Pek çok sunucuda çalışan basit bir health check scripti yazacağız:

#!/bin/bash

# ============================================
# Sunucu Saglik Kontrol Scripti
# Versiyon: 1.0
# ============================================

# Renk kodlari
KIRMIZI='33[0;31m'
YESIL='33[0;32m'
SARI='33[1;33m'
SIFIRLA='33[0m'

# Log fonksiyonu
log_yaz() {
    local seviye=$1
    local mesaj=$2
    local zaman
    zaman=$(date '+%Y-%m-%d %H:%M:%S')

    case "$seviye" in
        "INFO")  echo -e "${YESIL}[$zaman] [INFO]${SIFIRLA} $mesaj" ;;
        "WARN")  echo -e "${SARI}[$zaman] [WARN]${SIFIRLA} $mesaj" ;;
        "ERROR") echo -e "${KIRMIZI}[$zaman] [ERROR]${SIFIRLA} $mesaj" ;;
        *)       echo "[$zaman] $mesaj" ;;
    esac
}

# Disk kontrol fonksiyonu
disk_kontrol() {
    local esik=${1:-80}
    local sorun_var=0

    log_yaz "INFO" "Disk kullanimi kontrol ediliyor (esik: %$esik)..."

    while IFS= read -r satir; do
        local yuzde dizin
        yuzde=$(echo "$satir" | awk '{gsub(/%/,""); print $5}')
        dizin=$(echo "$satir" | awk '{print $6}')

        if [ "$yuzde" -ge "$esik" ]; then
            log_yaz "WARN" "Disk dolulugu yuksek: $dizin (%$yuzde)"
            sorun_var=1
        else
            log_yaz "INFO" "$dizin: %$yuzde kullaniliyor"
        fi
    done < <(df -h | awk 'NR>1 && /^// {print}')

    return $sorun_var
}

# Bellek kontrol fonksiyonu
bellek_kontrol() {
    local esik=${1:-90}
    local toplam kullanilan yuzde

    toplam=$(free -m | awk 'NR==2 {print $2}')
    kullanilan=$(free -m | awk 'NR==2 {print $3}')
    yuzde=$((kullanilan * 100 / toplam))

    log_yaz "INFO" "Bellek: ${kullanilan}MB / ${toplam}MB (%$yuzde)"

    if [ "$yuzde" -ge "$esik" ]; then
        log_yaz "WARN" "Bellek kullanimi yuksek: %$yuzde"
        return 1
    fi

    return 0
}

# Servis kontrol fonksiyonu - birden fazla servis alabilir
servisler_kontrol() {
    local basarisiz=0

    log_yaz "INFO" "Servis kontrolleri yapiliyor..."

    for servis in "$@"; do
        if systemctl is-active --quiet "$servis" 2>/dev/null; then
            log_yaz "INFO" "$servis: aktif"
        else
            log_yaz "ERROR" "$servis: CALISMIYOR!"
            basarisiz=$((basarisiz + 1))
        fi
    done

    return $basarisiz
}

# Ana calisma blogu
main() {
    log_yaz "INFO" "=== Sunucu Saglik Kontrolu Basliyor ==="
    log_yaz "INFO" "Sunucu: $(hostname)"

    local genel_durum=0

    disk_kontrol 80 || genel_durum=1
    bellek_kontrol 90 || genel_durum=1
    servisler_kontrol nginx postgresql redis || genel_durum=1

    if [ $genel_durum -eq 0 ]; then
        log_yaz "INFO" "=== Tum kontroller BASARILI ==="
    else
        log_yaz "ERROR" "=== Bazi kontroller BASARISIZ ==="
    fi

    return $genel_durum
}

main "$@"
exit $?

Bu script birkaç önemli pattern’i bir arada gösteriyor. main() fonksiyonu scriptin ana mantığını organize ediyor, her fonksiyon tek bir iş yapıyor ve "$@" ile argümanları main’e geçiriyoruz.

Gelişmiş Parametre Teknikleri

Adlandırılmış Parametreler Simülasyonu

Bash doğrudan adlandırılmış parametreleri desteklemiyor ama bunu simüle edebilirsiniz:

#!/bin/bash

yedek_al() {
    local kaynak=""
    local hedef=""
    local sikilastir=false
    local verbose=false

    # Parametreleri isle
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --kaynak|-k)
                kaynak="$2"
                shift 2
                ;;
            --hedef|-h)
                hedef="$2"
                shift 2
                ;;
            --sikistir|-s)
                sikilastir=true
                shift
                ;;
            --verbose|-v)
                verbose=true
                shift
                ;;
            *)
                echo "Bilinmeyen parametre: $1" >&2
                return 1
                ;;
        esac
    done

    # Zorunlu parametreleri kontrol et
    if [ -z "$kaynak" ] || [ -z "$hedef" ]; then
        echo "Kullanim: yedek_al --kaynak <yol> --hedef <yol> [--sikistir] [--verbose]" >&2
        return 1
    fi

    $verbose && echo "Yedek basliyor: $kaynak -> $hedef"

    if $sikilastir; then
        tar -czf "${hedef}/yedek_$(date +%Y%m%d_%H%M%S).tar.gz" "$kaynak"
    else
        rsync -a "$kaynak" "$hedef"
    fi

    $verbose && echo "Yedek tamamlandi"
}

# Kullanim ornekleri
yedek_al --kaynak /var/www/html --hedef /backup --sikistir --verbose
yedek_al -k /etc/nginx -h /backup/nginx -s

Bu pattern özellikle parametresi çok olan fonksiyonlarda hayat kurtarır. Scripti 3 ay sonra okuduğunuzda ne yaptığını anlamanız çok daha kolay olur.

Fonksiyonları Kütüphane Olarak Kullanmak

Gerçek sysadmin ortamında birden fazla scriptte aynı fonksiyonlara ihtiyaç duyarsınız. Bunun çözümü ortak bir kütüphane dosyası oluşturmaktır:

#!/bin/bash
# Dosya: /usr/local/lib/sysadmin_lib.sh

# Bu dosya direkt calistirilmasin, sadece source edilsin
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    echo "Bu dosya direkt calistirilmaz, source edilmelidir." >&2
    exit 1
fi

# Renk tanimlamalari
export RENK_KIRMIZI='33[0;31m'
export RENK_YESIL='33[0;32m'
export RENK_SIFIRLA='33[0m'

# Standart log fonksiyonu
log() {
    local seviye=${1^^}  # Buyuk harfe cevir
    local mesaj=$2
    echo -e "$(date '+%Y-%m-%d %H:%M:%S') [$seviye] $mesaj" | tee -a "${LOG_DOSYASI:-/tmp/script.log}"
}

# Root kontrolu
root_kontrol() {
    if [ "$EUID" -ne 0 ]; then
        log "error" "Bu script root yetkisi gerektiriyor!"
        exit 1
    fi
}

# Komut varlik kontrolu
komut_var_mi() {
    local komut=$1
    if ! command -v "$komut" &>/dev/null; then
        log "error" "Gerekli komut bulunamadi: $komut"
        return 1
    fi
    return 0
}
#!/bin/bash
# Dosya: kurulum_script.sh

# Kutuphanemizi yukluyoruz
# shellcheck source=/usr/local/lib/sysadmin_lib.sh
source /usr/local/lib/sysadmin_lib.sh

root_kontrol
komut_var_mi "apt" || exit 1

log "info" "Kurulum basliyor..."
# ... geri kalan kurulum kodu

${BASH_SOURCE[0]} kontrolü çok önemli bir güvenlik katmanıdır. Kütüphane dosyanızın yanlışlıkla direkt çalıştırılmasını önler.

Hata Yönetimi ve Trap Kullanımı

Profesyonel scriptlerde fonksiyonlarla birlikte trap kullanımı hayat kurtarır:

#!/bin/bash

# Gecici dosyalar icin liste
gecici_dosyalar=()

temizlik_yap() {
    echo "Temizlik yapiliyor..."
    for dosya in "${gecici_dosyalar[@]}"; do
        [ -f "$dosya" ] && rm -f "$dosya" && echo "Silindi: $dosya"
    done
}

hata_isle() {
    local hata_satiri=$1
    echo "HATA: Satir $hata_satiri'de beklenmeyen hata olustu!" >&2
    temizlik_yap
    exit 1
}

# Scriptden cikildiginda temizlik yap
trap temizlik_yap EXIT
trap 'hata_isle $LINENO' ERR

gecici_veri_olustur() {
    local gecici_dosya
    gecici_dosya=$(mktemp /tmp/islem_XXXXXX.tmp)
    gecici_dosyalar+=("$gecici_dosya")

    echo "Veri isleniyor: $gecici_dosya"
    # Islemler...
    echo "ornek veri" > "$gecici_dosya"

    echo "$gecici_dosya"  # Dosya yolunu dondur
}

# Kullanim
dosya1=$(gecici_veri_olustur)
dosya2=$(gecici_veri_olustur)

echo "Islem dosyalari olusturuldu: $dosya1 $dosya2"
# Script bittiginde trap devreye girip temizlik yapar

Bu pattern ile scriptiniz hata verse de, normal şekilde bitse de, Ctrl+C ile kesilse de temizlik fonksiyonunuz mutlaka çalışır. Production ortamında yarım kalan işlemlerin bıraktığı kirlilik büyük sorunlara yol açabilir.

Recursive Fonksiyonlar ve Sınırları

Bash’te recursive fonksiyonlar yazabilirsiniz ama dikkatli olun, her recursion için yeni bir subshell açılır ve sistem kaynakları hızla tükenir:

#!/bin/bash

# Dizin agaci olusturma - basit recursive ornek
dizin_tara() {
    local dizin=$1
    local girinti=${2:-0}
    local bosluk
    bosluk=$(printf '%*s' "$girinti" '')

    echo "${bosluk}$(basename "$dizin")/"

    for alt_dizin in "$dizin"/*/; do
        [ -d "$alt_dizin" ] || continue
        dizin_tara "$alt_dizin" $((girinti + 2))
    done
}

# Maksimum 3-4 seviye derinlikle kullanin
dizin_tara "/etc/nginx" 0

Derin recursive işlemler için bash yerine Python veya başka bir dil kullanmayı tercih edin. Bash’in stack limiti vardır ve bunu aşmak scriptin çökmesine neden olur.

Fonksiyon Dokümantasyonu

Takım ortamında çalışıyorsanız fonksiyonlarınızı belgelemeniz şarttır. Ben şu formatı kullanıyorum:

#!/bin/bash

# Kullanici oturumlarini analiz eder
#
# Parametreler:
#   $1 - kullanici_adi: Analiz edilecek kullanici (zorunlu)
#   $2 - gun_sayisi: Kac gunluk gecmis (varsayilan: 7)
#
# Donus degeri:
#   0 - Basarili
#   1 - Kullanici bulunamadi
#   2 - Log dosyasina erisilemedi
#
# Kullanim:
#   oturum_analiz "ahmet" 30
#   oturum_analiz "root"
oturum_analiz() {
    local kullanici=$1
    local gun_sayisi=${2:-7}

    if ! id "$kullanici" &>/dev/null; then
        echo "Kullanici bulunamadi: $kullanici" >&2
        return 1
    fi

    if [ ! -r /var/log/auth.log ]; then
        echo "Log dosyasina erisilemedi" >&2
        return 2
    fi

    echo "Son $gun_sayisi gun icinde $kullanici kullanicisinin oturumlari:"
    last "$kullanici" -n 20 | head -20

    return 0
}

Sonuç

Bash fonksiyonları ilk bakışta basit görünür ama üzerine inşa edeceğiniz otomasyon altyapısının temelidir. Bu yazıda ele aldığımız konuları özetlemek gerekirse:

  • Fonksiyon tanımlama: İki sözdizimi de geçerli, tutarlı olun
  • Parametreler: $1, $2, $@ ve mutlaka local kullanın
  • Return değerleri: Çıkış kodları için return, gerçek değerler için echo + command substitution
  • Adlandırılmış parametreler: while/case döngüsüyle simüle edin
  • Kütüphane pattern’i: Tekrar eden fonksiyonları ayrı dosyaya taşıyın
  • Trap ile temizlik: Production scriptlerinde olmazsa olmaz
  • Dokümantasyon: Kendiniz için değil, 3 ay sonraki kendiniz için yazın

Script yazmak bir pratik işidir. Ne kadar çok yazarsanız, hangi durumlarda fonksiyon kullanmanız gerektiğini o kadar iyi anlarsınız. Başlangıç için önerim şu: Mevcut scriptlerinizi açın ve 10 satırdan uzun tekrar eden her bloğu bir fonksiyona taşıyın. Farkı hemen göreceksiniz.

Yorum yapın