Birkaç satırlık bir bash betiği yazmak kolaydır; ama o betik yüz satırı geçtiğinde, aynı komut dizisini üç ayrı yerde tekrarladığınızı ve bir yerde işler ters gittiğinde betiğin yarı yolda takılıp yarım iş bıraktığını fark edersiniz. Bir yedekleme betiğinin ortasında disk dolduğunda ya da bir dosya kopyalanamadığında, kötü yazılmış bir betik hiçbir şey söylemeden çalışmaya devam eder ve sonunda size "yedek alındı" der; oysa yedek yarım kalmıştır. İşte fonksiyonlar ve hata yönetimi, betiklerinizi bu tür sessiz felaketlerden koruyan ve onları tek kullanımlık komut yığınından güvenilir üretim araçlarına dönüştüren iki temel yapı taşıdır.
Bu rehberde önce fonksiyonların nasıl tanımlanacağını, onlara nasıl argüman geçileceğini ve local değişkenlerle nasıl temiz tutulacaklarını göreceğiz. Ardından her komutun geriye bıraktığı çıkış kodu kavramını, return, exit ve $? ile bu kodların nasıl okunup üretileceğini işleyeceğiz. Sonra bir betiğin en tehlikeli anlarını disipline eden set -euo pipefail üçlüsünü tek tek çözümleyecek, trap ile hem geçici dosyaları temizlemeyi hem de hataları merkezi bir noktada yakalamayı öğreneceğiz. En sonunda tüm bu parçaları birleştirip, gerçek bir sunucuda gönül rahatlığıyla çalıştırabileceğiniz sağlam bir betik kalıbı ortaya koyacağız. Temel komutlarla aranız henüz çok iyi değilse, önce temel Linux komutları yazımıza göz atmanızı öneririm.
Fonksiyon Nedir ve Nasıl Tanımlanır#
Fonksiyon, bir betiğin içinde ada sahip, tekrar tekrar çağırabileceğiniz bir komut bloğudur. Aynı beş satırlık tar ve rsync dizisini betiğin üç ayrı yerine kopyalamak yerine, onu bir kez yedek_al adıyla tanımlar ve gerektiği yerde yedek_al diye çağırırsınız. Bu hem kod tekrarını ortadan kaldırır hem de bir değişiklik gerektiğinde tek bir yeri düzeltmenizi sağlar. Bash'te fonksiyon tanımlamanın iki söz dizimi vardır ve pratikte ikisi de aynı işi yapar:
# Yaygın ve önerilen biçim
selamla() {
echo "Merhaba, sunucu yönetimine hoş geldiniz!"
}
# Alternatif (function anahtar kelimesiyle)
function selamla {
echo "Merhaba, sunucu yönetimine hoş geldiniz!"
}
Fonksiyonu tanımlamak onu çalıştırmaz; yalnızca bash'e "böyle bir fonksiyon var" diye kaydeder. Çalıştırmak için adını parantezsiz olarak, sıradan bir komut gibi yazarsınız. Çok önemli bir kural: bir fonksiyon, çağrılmadan önce tanımlanmış olmalıdır. Bash betiği yukarıdan aşağıya okuduğu için, henüz tanımlanmamış bir fonksiyonu çağırırsanız "command not found" hatası alırsınız. Bu yüzden fonksiyonları betiğin üst kısmında toplamak, asıl mantığı ise en altta çağırmak yerleşik bir alışkanlıktır:
#!/bin/bash
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
servisi_kontrol_et() {
log "Nginx durumu kontrol ediliyor..."
systemctl is-active --quiet nginx
}
# Asıl akış burada başlar
log "Betik başladı"
servisi_kontrol_et
log "Betik bitti"
Buradaki log fonksiyonu küçük ama son derece işlevsel bir örnektir: her çıktının başına saat damgası ekleyerek betiğinizin ürettiği kayıtları çok daha okunur hâle getirir. $* ifadesi fonksiyona geçilen tüm argümanları temsil eder; birazdan bu konuya geleceğiz.
Fonksiyonlara Argüman Geçmek ve local Değişkenler#
Fonksiyonlar da tıpkı betiğin kendisi gibi konumsal parametrelerle argüman alır. Fonksiyonu çağırırken adının arkasına yazdığınız her kelime, fonksiyonun içinde $1, $2, $3 olarak erişilebilir hâle gelir. $0 ise fonksiyonun adı değil, hâlâ betiğin adını taşır; bu ayrıntı sık kafa karıştırır. Aşağıdaki tablo fonksiyon içinde en çok kullanılan özel değişkenleri özetliyor:
| Değişken | Anlamı |
|---|---|
$1, $2, ... | Birinci, ikinci, ... argüman |
$# | Geçilen argüman sayısı |
$* | Tüm argümanlar tek bir dize olarak |
$@ | Tüm argümanlar ayrı ayrı (tırnak içinde "$@" tercih edilir) |
$0 | Betiğin adı (fonksiyon adı değil) |
Örneğin bir dizini belirtilen hedefe sıkıştıran bir fonksiyon şöyle görünür:
arsivle() {
local kaynak="$1"
local hedef="$2"
if [[ -z "$kaynak" || -z "$hedef" ]]; then
echo "Kullanım: arsivle <kaynak_dizin> <hedef_dosya>" >&2
return 1
fi
tar -czf "$hedef" "$kaynak"
echo "$kaynak -> $hedef arşivlendi"
}
arsivle /var/www/site /yedek/site.tar.gz
Buradaki local anahtar kelimesi kritik öneme sahiptir. Bash'te varsayılan olarak tüm değişkenler globaldir; yani bir fonksiyonun içinde kaynak adında bir değişken oluşturursanız, o değişken fonksiyondan çıktıktan sonra da yaşamaya devam eder ve betiğin başka bir yerindeki aynı adlı değişkeni sessizce ezebilir. Bu, büyük betiklerde saatlerce sürecek hata avlarının başlıca kaynağıdır. local ile tanımlanan değişken yalnızca o fonksiyonun içinde geçerlidir ve fonksiyon bittiğinde yok olur. Kural basittir: fonksiyon içinde tanımladığınız her değişkeni local yapın. Argüman doğrulaması yapan if bloğuna da dikkat edin; boş argümanları erkenden yakalayıp return 1 ile çıkmak, fonksiyonun yanlış girdiyle ilerlemesini engeller.
Çıkış Kodları: return, exit ve $?#
Linux'ta çalışan her komut, işi bittiğinde geriye 0 ile 255 arasında bir sayı bırakır: buna çıkış kodu (exit code) denir. Anlamı evrenseldir: 0 başarı, sıfırdan farklı her değer bir tür başarısızlıktır. grep aradığını bulamazsa 1 döndürür, bir komut bulunamazsa 127 döner, izin reddedilirse genelde 126 gelir. Bu sayı, betiklerin "bir önceki komut yolunda gitti mi?" sorusunu cevaplama biçimidir ve tüm hata yönetiminin temelini oluşturur.
Bir önceki komutun çıkış kodunu $? özel değişkeninden okursunuz. Ancak $? her komuttan sonra güncellendiği için, değerini kullanacaksanız hemen bir değişkene atamak gerekir:
systemctl restart nginx
sonuc=$?
if [[ $sonuc -eq 0 ]]; then
echo "Nginx başarıyla yeniden başlatıldı"
else
echo "Nginx yeniden başlatılamadı (kod: $sonuc)" >&2
fi
Fonksiyonlar da kendi çıkış kodlarını return ile üretir. return, C veya Python'daki gibi bir değer döndürmez; yalnızca 0-255 aralığında bir çıkış durumu belirler. Bu yüzden bir fonksiyondan gerçek bir veri (örneğin bir dosya adı ya da sayı) döndürmek istiyorsanız, return değil echo kullanır ve çağıran taraf çıktıyı $() ile yakalar. return ise "başarılı mıydı, değil miydi?" bilgisini taşır:
disk_dolu_mu() {
local esik="${1:-90}"
local kullanim
kullanim=$(df / | awk 'NR==2 {gsub("%",""); print $5}')
if [[ "$kullanim" -ge "$esik" ]]; then
return 0 # evet, dolu (koşul sağlandı = başarı)
else
return 1 # hayır, yer var
fi
}
if disk_dolu_mu 85; then
echo "Dikkat: kök disk %85'in üzerinde!"
fi
Burada disk_dolu_mu bir soru soran fonksiyondur ve tıpkı [[ ... ]] gibi doğrudan if'in içinde kullanılabilir; çünkü if, komutun çıkış kodunu kontrol eder. Bir betiğin tamamını sonlandırmak isterseniz return değil exit kullanırsınız: exit 0 betiği başarıyla, exit 1 hatayla bitirir. return yalnızca fonksiyondan çıkarken, exit ise betiğin tamamından çıkarken kullanılır; bu ikisini karıştırmak yaygın bir hatadır. Kendi araçlarınızı yazarken anlamlı çıkış kodları üretmek, o betiği bir başka betikten ya da bir zamanlanmış cron görevinden çağırdığınızda başarıyı otomatik olarak denetlemenizi sağlar.
set -euo pipefail: Betiğinizi Katı Moda Almak#
Bash varsayılan olarak son derece hoşgörülüdür ve bu hoşgörü tehlikelidir. Bir komut hata verse bile betik bir sonraki satıra geçer; tanımlanmamış bir değişkeni boş dize olarak kabul eder; bir boru hattının ortasındaki komut çökse de son komut başarılıysa her şeyi yolunda sanar. Bu davranışların üçü de gerçek felaketlere yol açar. Klasik rm -rf "$DIZIN/" örneğini düşünün: $DIZIN yanlışlıkla boşsa, komut rm -rf / hâline gelir. İşte bu üç tehlikeyi kapatan satır neredeyse her ciddi betiğin ilk komutu olmalıdır:
#!/bin/bash
set -euo pipefail
Bu tek satır aslında üç ayrı korumayı bir araya getirir. Her birinin ne yaptığını tek tek görelim:
| Seçenek | Uzun adı | Ne yapar |
|---|---|---|
set -e | errexit | Herhangi bir komut sıfırdan farklı kod döndürürse betiği anında durdurur |
set -u | nounset | Tanımlanmamış bir değişken kullanılırsa hata verip durur |
set -o pipefail | — | Boru hattındaki komutlardan biri bile başarısızsa hattı başarısız sayar |
set -e sayesinde mkdir /yedek başarısız olursa betik oracıkta durur ve içeriği var olmayan bir dizine yazmaya çalışmaz. set -u ise $DIZIN gibi bir değişkeni tanımlamayı unuttuğunuzda, onu boş kabul edip ilerlemek yerine "unbound variable" diyerek sizi uyarır; yukarıdaki rm -rf faciası tam da bu koruma ile önlenir. pipefail olmadan, cat gunluk.log | grep HATA | wc -l gibi bir hatta cat başarısız olsa bile hattın çıkış kodu son komut olan wc'ye bakar ve 0 döner; pipefail bu boşluğu kapatır.
Bu katı modun bir de dikkat gerektiren yanı var. set -e altında, çıkış kodunu bilerek kontrol ettiğiniz komutlar betiği kazara durdurabilir. Örneğin grep bir şey bulamazsa 1 döner ve set -e betiği kapatır. Bunu önlemek için, başarısızlığına izin verdiğiniz komutları || true ile yumuşatır ya da doğrudan bir if içinde denetlersiniz:
# grep bir şey bulamazsa betik durmasın
if grep -q "HATA" /var/log/uygulama.log; then
echo "Günlükte hata bulundu"
fi
# ya da çıkış kodunu görmezden gel
satir_sayisi=$(grep -c "HATA" /var/log/uygulama.log || true)
Ayrıca set -u altında, tanımlanmamış olabilecek bir değişkeni güvenle kullanmak için ${DEGISKEN:-varsayilan} biçimini kullanın; bu, değişken boşsa yerine bir varsayılan koyar ve hatayı önler. Bu üç seçeneği yazının sonundaki üretim kalıbında bir arada göreceksiniz.
trap ile Temizlik ve Hata Yakalama#
Bir betik, işini yaparken sık sık geride izler bırakır: geçici bir dizin oluşturur, bir kilit dosyası koyar, bir veritabanı dökümü alır. Betik yarıda kesilirse (kullanıcı Ctrl+C bastı, set -e bir hatada durdurdu ya da sunucu bir sinyal gönderdi) bu geçici dosyalar ortada kalır ve zamanla diski doldurur. trap komutu tam da bunun için vardır: belirli bir olay gerçekleştiğinde çalışacak bir komut kaydeder. En değerli olay EXIT'tir, çünkü betik nasıl biterse bitsin — başarıyla, hatayla veya kesintiyle — çalışır:
#!/bin/bash
set -euo pipefail
# Güvenli, benzersiz bir geçici dizin oluştur
TMPDIR=$(mktemp -d)
# Betik nasıl biterse bitsin bu dizini sil
temizle() {
rm -rf "$TMPDIR"
echo "Geçici dosyalar temizlendi: $TMPDIR"
}
trap temizle EXIT
# Artık gönül rahatlığıyla TMPDIR içinde çalış
echo "veri" > "$TMPDIR/calisma.txt"
# ... betik burada hata verse bile temizle() çalışır
Bu kalıp son derece güçlüdür: TMPDIR'i silmeyi betiğin sonuna yazmanız gerekmez, çünkü betik oraya hiç ulaşamayabilir. trap ... EXIT ile temizliği bir kez kaydedersiniz ve gerisini bash halleder. İkinci önemli olay ERR'dir; bu, herhangi bir komut hata verdiğinde (yani set -e'nin betiği durdurduğu anda) tetiklenir ve hatanın tam olarak hangi satırda oluştuğunu raporlayan merkezi bir hata işleyici yazmak için biçilmiş kaftandır:
hata_yakala() {
local kod=$?
local satir=$1
echo "HATA: betik $satir. satırda $kod koduyla durdu" >&2
# Buraya bildirim gönderme, geri alma vb. eklenebilir
}
trap 'hata_yakala $LINENO' ERR
$LINENO hatanın oluştuğu satır numarasını, $? ise başarısız komutun çıkış kodunu taşır. Bu iki trap'i birlikte kullandığınızda, betiğiniz hem her koşulda arkasını toplar hem de bir şey ters gittiğinde size tam olarak nerede ve neyin patladığını söyler. Ayrıca INT (Ctrl+C) ve TERM sinyallerini de yakalayarak, uzun süren bir işlem elle durdurulduğunda düzgün bir kapanış yapabilirsiniz. Uzun ömürlü betiklerinizin ürettiği hata bildirimlerini bir e-posta ya da mesajlaşma kanalına yönlendirmek isterseniz, sunucu yönetimi hizmetimiz kapsamında bu tür izleme kurulumlarında da yardımcı oluyoruz.
Sağlam Bir Üretim Betiği Kalıbı#
Şimdi öğrendiğimiz her şeyi tek bir iskelette birleştirelim. Aşağıdaki kalıp, üretim ortamında güvenle çalıştırabileceğiniz bir betiğin omurgasıdır: katı mod, saat damgalı günlükleme, argüman doğrulama, local değişkenler, anlamlı çıkış kodları ve hem temizlik hem hata yakalama için trap. Örnek olarak basit bir dizin yedekleme betiği yazalım, ama bu iskeleti kendi işleriniz için doldurabilirsiniz:
#!/bin/bash
#
# yedek.sh - Bir dizini zaman damgalı bir arşive yedekler
#
set -euo pipefail
readonly KAYNAK="${1:-/var/www}"
readonly HEDEF_DIZIN="${2:-/yedek}"
readonly TARIH=$(date '+%Y%m%d-%H%M%S')
readonly ARSIV="$HEDEF_DIZIN/yedek-$TARIH.tar.gz"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
hata_yakala() {
local kod=$?
log "HATA: betik $1. satırda $kod koduyla durdu"
exit "$kod"
}
trap 'hata_yakala $LINENO' ERR
on_kontrol() {
if [[ ! -d "$KAYNAK" ]]; then
log "Kaynak dizin bulunamadı: $KAYNAK"
return 1
fi
mkdir -p "$HEDEF_DIZIN"
}
yedek_al() {
log "Yedekleniyor: $KAYNAK -> $ARSIV"
tar -czf "$ARSIV" -C "$(dirname "$KAYNAK")" "$(basename "$KAYNAK")"
local boyut
boyut=$(du -h "$ARSIV" | cut -f1)
log "Yedek tamamlandı ($boyut)"
}
eski_yedekleri_temizle() {
log "7 günden eski yedekler siliniyor..."
find "$HEDEF_DIZIN" -name 'yedek-*.tar.gz' -mtime +7 -delete
}
main() {
log "Yedekleme başladı"
on_kontrol
yedek_al
eski_yedekleri_temizle
log "Tüm işlemler başarıyla tamamlandı"
}
main "$@"
Bu betiğin öğrettiği birkaç ince nokta var. readonly ile tanımlanan değişkenler bir daha değiştirilemez; bu, KAYNAK veya ARSIV gibi kritik değerlerin betiğin ortasında kazara ezilmesini önler. Tüm mantık küçük, tek işe odaklı fonksiyonlara bölünmüştür ve hepsi main fonksiyonundan sırayla çağrılır; bu yapı betiği okunur tutar ve her adımı bağımsız test etmenizi sağlar. main "$@" ifadesi betiğe gelen tüm argümanları main fonksiyonuna aktarır, böylece asıl akış da bir fonksiyonun içinde, düzenli biçimde yaşar. set -euo pipefail ile birleşince, tar başarısız olursa ERR tuzağı devreye girer, hatanın satırını bildirir ve betik doğru çıkış koduyla sonlanır; bir sonraki adıma asla geçmez. Böyle bir betiği bir cron görevine bağladığınızda, başarısızlıkları e-postayla ya da izleme sisteminizle otomatik olarak yakalayabilirsiniz. Ürettiğiniz arşivleri güvenli bir dış konumda saklamak için yedekleme çözümlerimize, bu betikleri koşturacağınız kaynak izole bir ortam için de VDS sunucularımıza göz atabilirsiniz.
Sıkça Sorulan Sorular#
return ile exit arasındaki fark nedir?#
İkisi de bir çıkış kodu üretir ama kapsamları farklıdır. return yalnızca içinde bulunduğu fonksiyondan çıkar ve kontrolü fonksiyonu çağıran satıra geri verir; betik çalışmaya devam eder. exit ise betiğin tamamını sonlandırır ve kabuğa geri döner. Fonksiyonun başarılı olup olmadığını bildirmek için return, betiği tümüyle bitirmek için exit kullanın. Bir fonksiyonun içinde yanlışlıkla exit kullanırsanız, o fonksiyonu çağıran betik de beklenmedik biçimde kapanır.
set -e kullandığım hâlde betiğim neden hata verince durmuyor?#
set -e'nin görmezden geldiği birkaç durum vardır ve en yaygını, komutun bir if, while, && ya da || bağlamında bulunmasıdır. Örneğin komut || true yazdığınızda ya da if komut; then içinde kullandığınızda, bash o komutun başarısızlığını "kontrol ediliyor" sayar ve betiği durdurmaz. Aynı şekilde bir fonksiyonun çıkış kodunu bir if ile denetliyorsanız, fonksiyon içindeki set -e beklediğiniz gibi tetiklenmeyebilir. Bir de boru hatlarında yalnızca son komut değerlendirildiği için set -o pipefail eklemeniz gerekir.
Bir fonksiyondan gerçek bir değer nasıl döndürürüm?#
Bash'te return yalnızca 0-255 arası bir çıkış kodu üretir, veri döndürmez. Gerçek bir sonuç (bir dosya adı, sayı ya da metin) döndürmek isterseniz fonksiyon içinde onu echo ile yazdırın ve çağıran tarafta sonuc=$(fonksiyonum) biçiminde komut ikamesiyle yakalayın. Alternatif olarak, sonucu global veya local yerine adı çağıran tarafça bilinen bir değişkene atayabilirsiniz. En temiz ve en yaygın yöntem echo ile yazdırıp $() ile yakalamaktır; bu sayede fonksiyon hem çıkış kodunu hem de verisini ayrı ayrı iletebilir.
local anahtar kelimesini kullanmayı unutursam ne olur?#
local olmadan tanımladığınız değişken global olur; yani fonksiyon bittikten sonra da yaşamaya devam eder ve betiğin başka yerlerinde aynı adı taşıyan değişkenleri sessizce ezebilir. Küçük betiklerde bu fark edilmeyebilir ama fonksiyon sayısı arttıkça, bir yerdeki i sayacının başka bir döngüyü bozması gibi bulması çok zor hatalara yol açar. Kural olarak, fonksiyon içinde oluşturduğunuz her değişkeni local ile tanımlayın; bu, fonksiyonları birbirinden yalıtır ve betiği öngörülebilir kılar.
trap EXIT her durumda çalışır mı, kill -9 dahil mi?#
trap ... EXIT, betik normal biterse, exit çağrılırsa, set -e bir hatada durdurursa ve Ctrl+C gibi çoğu sinyalle kesilirse çalışır; bu yüzden geçici dosya temizliği için en güvenilir yerdir. Ancak kill -9 (yani SIGKILL) sinyali bir istisnadır: bu sinyal süreci çekirdek düzeyinde anında sonlandırır ve yakalanamaz, dolayısıyla trap çalışamaz. SIGKILL nadiren ve son çare olarak kullanıldığı için pratikte bir sorun oluşturmaz; yine de kritik temizlik işlerinde, betiğin bir sonraki çalışmasının başında da eski geçici dosyaları kontrol etmek iyi bir alışkanlıktır.
Kapanış#
Fonksiyonlar betiklerinizi tekrar eden komut yığınından okunur, bakımı kolay araçlara dönüştürür; set -euo pipefail onları sessiz hatalara karşı zırhlar; trap ise her koşulda arkalarını toplayıp bir şey ters gittiğinde sizi uyarır. Bu üç alışkanlığı birlikte kullandığınızda, gece yarısı çalışan bir yedekleme ya da bakım betiğine gerçekten güvenebilir, sabah "acaba dün gece işini yaptı mı?" diye tedirgin olmazsınız. Yeni yazdığınız her betikte önce iskeleti kurun, sonra mantığı doldurun; disiplin en baştan gelirse, betikleriniz büyüdükçe karmaşaya değil olgunluğa doğru gider.
Bu betikleri güvenle koşturabileceğiniz izole ve yönetilebilir bir ortam arıyorsanız Clou.TR olarak yanınızdayız. Kaynaklarını tam kontrol edebileceğiniz VDS ve sanal sunucularımız, otomasyon dostu hosting paketlerimiz ve kurulumdan güvenliğe kadar her adımda destek veren sunucu yönetimi hizmetimiz ile altyapınızı sağlam temellere oturtabilirsiniz. Betiklerinizin ürettiği yedekleri güvende tutmak için de yedekleme çözümlerimizi incelemenizi öneririz.