Bir bash scripti yazdınız, çalıştırdınız ve hiçbir şey beklendiği gibi olmadı. Hata mesajı yok, sadece yanlış sonuç. Ya da daha kötüsü, script yarıda kesildi ve nerede durduğunu bilmiyorsunuz. İşte bu noktada çoğu sysadmin echo satırları eklemeye başlar, scripti yeniden çalıştırır, tekrar echo ekler… Bu döngü saatler alabilir. Oysa bash’in yerleşik debug araçlarını kullanmayı öğrenmek bu süreci dramatik biçimde kısaltır.
Debug Moduna Giriş: bash -x Nedir?
bash -x parametresi, bir scripti trace modunda çalıştırır. Bu mod aktifken bash, her komutu çalıştırmadan önce o komutun genişletilmiş halini ekrana yazar. Yani değişken substitution, glob expansion ve diğer tüm genişletmeler gerçekleştikten sonra ne çalıştırılacağını tam olarak görürsünüz.
En basit kullanım şekli şudur:
bash -x script.sh
Ya da doğrudan scriptin shebang satırını değiştirebilirsiniz:
#!/bin/bash -x
Bu yöntemi sevmiyorum çünkü debug işi bittiğinde shebang’i geri değiştirmeyi unutabilirsiniz. Daha iyi alternatifler var, bunlara geleceğiz.
-x çıktısı + işaretiyle başlar. Her + bir komut seviyesini temsil eder. Bir fonksiyon içindeki komutlar ++ ile, o fonksiyon başka bir fonksiyondan çağrılıyorsa +++ ile gösterilir. Bu hiyerarşi, nested yapıları takip ederken çok işe yarar.
set -x ile Hedefli Debug
bash -x tüm scripti debug eder. Ama 500 satırlık bir scriptte sadece 20-30 satırlık bir bölümde sorun olduğunu biliyorsanız, tüm çıktıyı okumak zaman kaybıdır. İşte burada set -x devreye giriyor.
set -x komutunu scriptin içine koyarak debug modunu istediğiniz noktada açıp kapatabilirsiniz:
#!/bin/bash
# Bu kısım normal çalışır, debug kapalı
echo "Script başladı"
HOSTNAME=$(hostname)
DATE=$(date +%Y%m%d)
# Sorunlu bölümü debug et
set -x
for dir in /var/log /tmp /home; do
if [ -d "$dir" ]; then
du -sh "$dir" 2>/dev/null
fi
done
set +x
# Debug bitti, normal akış devam eder
echo "Script tamamlandı"
set +x ile debug modunu kapatıyorsunuz. Bu ikili kullanım, büyük scriptlerde sinyali gürültüden ayırmanın en pratik yoludur.
Gerçek Dünya Senaryosu 1: Yanlış Çalışan Backup Scripti
Şöyle bir senaryo düşünün. Sunucularınızda çalışan bir backup scripti var ve bazı dizinleri yedeklemiyor. Log dosyasına bakıyorsunuz ama anlamlı bir hata yok.
#!/bin/bash
BACKUP_DIR="/backup/daily"
SOURCE_DIRS="/etc /home /var/www"
DATE=$(date +%Y-%m-%d)
EXCLUDE_PATTERN="*.tmp *.log"
backup_directory() {
local src=$1
local dest="${BACKUP_DIR}/${DATE}/$(basename $src)"
set -x
tar --exclude="$EXCLUDE_PATTERN" -czf "${dest}.tar.gz" "$src"
set +x
if [ $? -eq 0 ]; then
echo "Başarılı: $src"
else
echo "HATA: $src yedeklenemedi"
fi
}
mkdir -p "${BACKUP_DIR}/${DATE}"
for dir in $SOURCE_DIRS; do
backup_directory "$dir"
done
set -x çıktısına baktığınızda şöyle bir şey görürsünüz:
+ tar --exclude='*.tmp *.log' -czf /backup/daily/2024-01-15/etc.tar.gz /etc
Hemen fark ediyorsunuz: --exclude parametresi tek bir string olarak geçiyor. Birden fazla exclude pattern için birden fazla --exclude parametresi gerekiyor. Debug olmadan bu hatayı bulmak çok daha uzun sürerdi.
PS4 Değişkeni ile Debug Çıktısını Zenginleştirme
Varsayılan + işareti bazen yetersiz kalır. Hangi satırda olduğunuzu, hangi fonksiyonda çalıştığınızı görmek isteyebilirsiniz. PS4 değişkeni bu çıktıyı özelleştirmenizi sağlar.
#!/bin/bash
# Satır numarası ve fonksiyon adını göster
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
process_users() {
local userlist=$1
while IFS= read -r user; do
id "$user" > /dev/null 2>&1 && echo "$user mevcut" || echo "$user yok"
done < "$userlist"
}
process_users /tmp/userlist.txt
Bu ayarla çıktı şöyle görünür:
+(debug_script.sh:14): process_users(): id admin
+(debug_script.sh:14): process_users(): echo 'admin mevcut'
Artık her satır için tam dosya adı, satır numarası ve hangi fonksiyonda olduğunuzu görüyorsunuz. Büyük scriptlerde bu bilgi altın değerindedir.
set -e, set -u ve set -o pipefail: Debug’un Müttefikleri
set -x tek başına güçlüdür ama diğer set seçenekleriyle birlikte kullanıldığında çok daha etkili hale gelir.
set -e: Herhangi bir komut sıfır dışı çıkış kodu döndürdüğünde scripti durdurur. Sessiz hataların önüne geçer.
set -u: Tanımlanmamış bir değişken kullanıldığında hata verir. Yazım hatalarını anında yakalar.
set -o pipefail: Pipe zincirindeki herhangi bir komut başarısız olursa tüm pipe başarısız sayılır. Normalde pipe’ın son komutunun çıkış kodu kullanılır, bu seçenek olmadan ara komutların hataları gözden kaçar.
#!/bin/bash
# Production scriptlerde bu üçlüyü her zaman kullanın
set -euo pipefail
# Opsiyonel: debug için buna da ekleyin
set -x
LOGFILE="/var/log/app/application.log"
OUTPUT_FILE="/tmp/error_summary.txt"
# Bu satır set -u olmadan sessizce boş string dönerdi
# set -u ile hemen hata verir
echo "Log dosyası: ${LOG_FILE}" # Yazım hatası! LOGFILE değil LOG_FILE yazdık
grep "ERROR" "$LOGFILE" | awk '{print $1, $NF}' | sort | uniq -c > "$OUTPUT_FILE"
set -u sayesinde LOG_FILE değişkeninin tanımlı olmadığını hemen görürsünüz. set -x de hangi satırda durduğunu gösterir. İkisi birlikte hata ayıklamayı çok daha hızlı hale getirir.
Gerçek Dünya Senaryosu 2: Cron Job Hataları
Cron job’lar sysadmin’lerin en sık şikayet ettiği konular arasındadır. Script elle çalıştırıldığında mükemmel çalışıyor ama cron’da çalışmıyor. Debug çıktısını bir dosyaya yönlendirerek bu durumu çözebilirsiniz.
#!/bin/bash
# Cron debug için: çıktıyı dosyaya yönlendir
exec > /tmp/cron_debug_$(date +%Y%m%d_%H%M%S).log 2>&1
set -x
# Cron'da PATH genellikle minimal olur, açıkça tanımlayın
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# Cron'da HOME değişkeni farklı olabilir
export HOME="/root"
# Script işlemleri
MYSQL_USER="backup_user"
MYSQL_PASS="secretpass"
DATABASES=$(mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "SHOW DATABASES;" 2>/dev/null | grep -v "Database|information_schema|performance_schema")
for db in $DATABASES; do
echo "Yedekleniyor: $db"
mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASS" "$db" > "/backup/mysql/${db}_$(date +%Y%m%d).sql"
done
echo "Tamamlandı: $(date)"
exec > file 2>&1 satırı tüm stdout ve stderr’i dosyaya yönlendirir. set -x de her adımı kayıt altına alır. Cron çalıştıktan sonra log dosyasını incelediğinizde ne olduğunu tam olarak görürsünüz.
trap Komutu ile Hata Noktasını Yakalama
set -x komutu takip eder ama script bir hatayla kapanırken hangi durumda olduğunu göstermez. trap komutu script çıkış sinyallerini yakalamanızı sağlar.
#!/bin/bash
set -euo pipefail
# Hata anındaki durumu raporla
error_handler() {
local exit_code=$?
local line_number=$1
echo "-------------------------------------------"
echo "HATA RAPORU"
echo "Çıkış kodu : $exit_code"
echo "Satır numarası: $line_number"
echo "Tarih/Saat : $(date)"
echo "Çalışan script: $0"
echo "-------------------------------------------"
}
# ERR sinyali yakalanırsa error_handler çalışsın
trap 'error_handler $LINENO' ERR
# Temizlik fonksiyonu
cleanup() {
echo "Temizlik yapılıyor..."
rm -f /tmp/process_$$.lock
rm -f /tmp/tempfile_$$
}
# Script çıkarken cleanup çalışsın
trap cleanup EXIT
# Lock dosyası oluştur
touch /tmp/process_$$.lock
# Test: kasıtlı hata üretelim
NONEXISTENT_DIR="/path/that/does/not/exist"
set -x
cd "$NONEXISTENT_DIR" # Bu satır hata verecek
echo "Bu satıra hiç ulaşılmaz"
Bu yapıyla hem hata anındaki satır numarasını hem de çıkış kodunu görürsünüz. trap cleanup EXIT ise script hatayla da normal şekilde de kapansa temizlik işlemlerinin yapılmasını garantiler.
Debug Çıktısını Renklendirme
Uzun debug çıktılarında gözünüz kayabilir. Özellikle kritik hataları veya belirli komutları vurgulamak için renk kullanabilirsiniz:
#!/bin/bash
# Renkli PS4 tanımı
RED='33[0;31m'
GREEN='33[0;32m'
YELLOW='33[1;33m'
NC='33[0m'
# Debug çıktısını renklendir
export PS4="${YELLOW}DEBUG${NC} [${GREEN}${LINENO}${NC}]: "
set -x
# Örnek: disk kullanımı kontrol scripti
check_disk_usage() {
local threshold=$1
local filesystem=$2
local usage
usage=$(df -h "$filesystem" | awk 'NR==2 {gsub("%",""); print $5}')
if [ "$usage" -gt "$threshold" ]; then
echo -e "${RED}UYARI: $filesystem kullanımı %$usage (eşik: %$threshold)${NC}"
return 1
else
echo -e "${GREEN}OK: $filesystem kullanımı %$usage${NC}"
return 0
fi
}
check_disk_usage 80 "/"
check_disk_usage 80 "/var"
check_disk_usage 90 "/home"
Not: Renklendirme terminal emülatöre bağımlıdır. Log dosyasına yönlendirme yapıyorsanız ANSI kaçış kodları okunabilirliği bozabilir. Bu durumda renkli PS4 kullanmayın.
Gerçek Dünya Senaryosu 3: Karmaşık Bir Deploy Scripti Debug Etme
Production deploy scriptleri genellikle birçok adımdan oluşur ve herhangi bir adımda hata olabilir. Şöyle bir yapı kuralım:
#!/bin/bash
set -euo pipefail
# Sadece belirli fonksiyonları debug et
export PS4='+(${BASH_LINENO[0]}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
APP_NAME="myapp"
DEPLOY_DIR="/var/www/${APP_NAME}"
REPO_URL="[email protected]:company/${APP_NAME}.git"
BACKUP_DIR="/backup/deploys"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
backup_current() {
log "Mevcut versiyon yedekleniyor..."
set -x
if [ -d "$DEPLOY_DIR" ]; then
tar -czf "${BACKUP_DIR}/${APP_NAME}_${TIMESTAMP}.tar.gz" "$DEPLOY_DIR"
fi
set +x
log "Yedekleme tamamlandı"
}
pull_latest() {
log "Son kod çekiliyor..."
set -x
cd "$DEPLOY_DIR"
git fetch origin
git reset --hard origin/main
set +x
log "Kod güncellendi: $(git log -1 --oneline)"
}
run_migrations() {
log "Veritabanı migration'ları çalıştırılıyor..."
set -x
cd "$DEPLOY_DIR"
php artisan migrate --force 2>&1
set +x
}
restart_services() {
log "Servisler yeniden başlatılıyor..."
set -x
systemctl reload php8.1-fpm
systemctl reload nginx
set +x
log "Servisler hazır"
}
# Ana akış
log "Deploy başladı: $APP_NAME"
backup_current
pull_latest
run_migrations
restart_services
log "Deploy tamamlandı"
Bu yapıda her kritik fonksiyon kendi set -x / set +x bloğuna sahip. Sorun yaşayan adımın detaylı çıktısını görürken diğer adımlar temiz kalır.
xtrace Çıktısını Dosyaya Yönlendirme
Bazen debug çıktısı terminalde çok hızlı akar ve okuyamazsınız. Ya da uzak bir sunucuda çalışan bir scriptin debug çıktısını kaydetmek istiyorsunuzdur. BASH_XTRACEFD değişkeni bunu mümkün kılar:
#!/bin/bash
# Debug çıktısını ayrı bir dosyaya yönlendir
# File descriptor 3'ü debug log dosyasına bağla
exec 3>/tmp/debug_output_$$.log
# BASH_XTRACEFD: set -x çıktısının nereye gideceğini belirler
export BASH_XTRACEFD=3
export PS4='+(${BASH_SOURCE}:${LINENO}): '
set -x
# Normal çıktı terminale gitmeye devam eder
echo "Bu terminalde görünür"
# Ama debug çıktısı dosyaya gider
for i in $(seq 1 5); do
result=$((i * i))
echo " $i^2 = $result"
done
# File descriptor'ı kapat
exec 3>&-
echo "Debug log: /tmp/debug_output_$$.log"
echo "İncelemek için: cat /tmp/debug_output_$$.log"
Bu yaklaşımın güzelliği şu: script kullanıcısı normal çıktıyı görürken siz arka planda tam debug kaydını alırsınız. Cron job’lar için ideal bir yapıdır.
Yaygın Hatalar ve Debug Stratejileri
Debug yaparken sık karşılaşılan durumlar ve bunlarla başa çıkma yolları:
Değişken genişletme sorunları: Boşluk içeren değerlerin tırnak içine alınmaması en yaygın hatalardan biridir. set -x çıktısında değişkenin nasıl genişlediğini görerek bu hatayı hemen fark edersiniz.
Arithmetic expansion hataları: $(( )) içindeki ifadeler bazen beklenmedik sonuçlar üretir. Debug modunda değişkenlerin sayısal değerlerini görebilirsiniz.
Subshell sorunları: Pipe içindeki değişken atamaları parent shell’de görünmez. set -x bu davranışı gösterir ama çözmez, bunu bilmek önemlidir.
IFS sorunları: Özellikle dosya okuma döngülerinde IFS değerini değiştirmek beklenmedik davranışlara yol açar.
#!/bin/bash
# IFS sorununu debug etme örneği
set -x
# Sorunlu versiyon: dosya adında boşluk varsa bozulur
for file in $(ls /var/log/*.log); do
echo "İşleniyor: $file"
done
set +x
echo "--- Doğru yöntem ---"
set -x
# Doğru versiyon
while IFS= read -r -d '' file; do
echo "İşleniyor: $file"
done < <(find /var/log -name "*.log" -print0)
Pratik İpuçları ve En İyi Uygulamalar
Production ortamlarında script yazarken debug araçlarını nasıl entegre edeceğinize dair bazı pratik öneriler:
- Debug flag’i parametre olarak alın: Scripte
--debugparametresi ekleyin, bu parametre geldiğindeset -xaktif olsun. Böylece aynı scripti debug modda ve normal modda çalıştırabilirsiniz.
- Log seviyelerini ayırın: Normal log, hata log ve debug log için ayrı fonksiyonlar yazın. Debug fonksiyonu sadece bir değişken aktif olduğunda çalışsın.
- Geçici debug kodlarını işaretleyin:
# DEBUGyorumuyla işaretlenmiş satırları production’a taşımadan önce temizleyin.
- set -x’i fonksiyon scope’unda kullanın: Fonksiyon içinde
local -ile set değişkenlerini local yapabilirsiniz. Bu sayede fonksiyon bittiğinde debug modu otomatik kapanır.
#!/bin/bash
# Parametre ile debug modu
DEBUG=${DEBUG:-0}
debug_log() {
[ "$DEBUG" -eq 1 ] && echo "[DEBUG] $*" >&2
}
debug_function() {
# local - ile set seçeneklerini bu fonksiyona local yap
local -
set -x
# Bu fonksiyondan çıkınca set -x otomatik kapanır
local result
result=$(find /etc -name "*.conf" | wc -l)
echo "Konfig dosyası sayısı: $result"
}
# Kullanım: DEBUG=1 bash script.sh
[ "$DEBUG" -eq 1 ] && set -x
debug_log "Script başladı"
debug_function
debug_log "Script bitti"
local - kullanımı bash 4.4 ve sonrasında çalışır. Bu özelliği kullanmadan önce bash versiyonunuzu kontrol edin: bash --version.
Sonuç
bash -x ve set -x, sysadmin’in araç kutusundaki en değerli ama en az kullanılan araçlardan ikisidir. echo ile debug yapmak alışkanlık haline gelebilir, ama bu araçları bir kez düzgünce öğrendiğinizde geri dönmek istemezsiniz.
Özetle şunu söyleyebilirim: Küçük scriptlerde bash -x script.sh yeterlidir. Büyük ve karmaşık scriptlerde set -x / set +x ikilisiyle hedefli debug yapın. Production ortamı için BASH_XTRACEFD ile debug çıktısını dosyaya yönlendirin ve PS4 değişkenini zenginleştirerek satır numarası bilgisi ekleyin. set -euo pipefail üçlüsünü her zaman kullanın, hataları sessizce geçiştirmeyin.
Debug bir beceri, sadece bir araç değil. Ne zaman hangi aracı kullanacağınızı bilmek, sorunu ne kadar hızlı çözeceğinizi doğrudan etkiler. Bir dahaki sefere scriptte bir sorun çıktığında, echo eklemeden önce bir set -x deneyin. Muhtemelen sizi şaşırtacak.