Bash scriptleri büyüdükçe bir noktada kaçınılmaz bir sorunla yüzleşirsin: her şeyi tek dosyaya tıkmışsın ve artık o dosyaya bakmak bile istemiyorsun. 800 satırlık bir script dosyası, içinde kaybolduğun bir labirentle aynı şeydir. İşte tam bu noktada modüler script tasarımı devreye giriyor. Kod parçalarını mantıksal birimler halinde ayırmak, yeniden kullanılabilir kütüphaneler oluşturmak ve source komutuyla bunları bir araya getirmek, hem bakımı kolaylaştırır hem de ekip çalışmasını mümkün kılar.
Neden Modüler Yaklaşım?
Tek dosyalı scriptlerin yarattığı sorunları muhtemelen yaşadın. Aynı log fonksiyonunu 5 farklı scripte kopyaladın, birinde bir bug buldun ama düzeltmeyi diğerlerine taşımayı unuttun. Ya da bir iş arkadaşın scripte “küçük bir ekleme” yaptı ve beklenmedik bir değişkeni ezdi.
Modüler yaklaşımın sağladığı temel avantajlar şunlar:
- Tekrar kullanılabilirlik: Bir kez yaz, her yerden kullan
- Bakım kolaylığı: Bir fonksiyonu tek bir yerde güncelle
- Test edilebilirlik: İzole fonksiyonları bağımsız test edebilirsin
- Ekip uyumu: Farklı kişiler farklı modüller üzerinde çalışabilir
- Okunabilirlik: Ana script temiz ve anlaşılır kalır
Bash’te Include Mekanizması: source ve .
Bash’te başka bir dosyayı dahil etmenin iki eşdeğer yolu var: source komutu ve nokta (.) operatörü. İkisi de aynı işi yapar, sadece yazımı farklı.
# Bu iki satır tamamen eşdeğer
source /opt/scripts/lib/logging.sh
. /opt/scripts/lib/logging.sh
Kritik nokta şu: source komutu, hedef dosyayı yeni bir subprocess içinde değil, mevcut shell ortamında çalıştırır. Bu yüzden kaynak dosyada tanımlanan her fonksiyon, değişken ve alias, çağıran scriptte de erişilebilir olur.
Bunu basit bir örnekle gösterelim:
# /tmp/test_lib.sh
MY_VAR="Merhaba Dünya"
say_hello() {
echo "Selam! Ben bir kütüphane fonksiyonuyum."
}
#!/usr/bin/env bash
# /tmp/test_main.sh
source /tmp/test_lib.sh
echo "$MY_VAR"
say_hello
Çalıştırdığında hem değişkeni hem de fonksiyonu sorunsuz kullanabiliyorsun. Eğer source yerine bash /tmp/test_lib.sh kullansaydın, yeni bir subprocess başlayacak ve tanımlanan şeyler ana scripte aktarılmayacaktı.
Güvenli Include: Dosya Var mı Kontrol Et
Production scriptlerinde include etmeden önce dosyanın varlığını kontrol etmek şart. Eksik bir kütüphane dosyası, scripti belirsiz hatalarla çökertir.
#!/usr/bin/env bash
# Kütüphane yükleme fonksiyonu
load_library() {
local lib_path="$1"
if [[ ! -f "$lib_path" ]]; then
echo "HATA: Kütüphane dosyası bulunamadı: $lib_path" >&2
exit 1
fi
if [[ ! -r "$lib_path" ]]; then
echo "HATA: Kütüphane dosyası okunamıyor: $lib_path" >&2
exit 1
fi
source "$lib_path"
echo "Yüklendi: $lib_path"
}
# Kullanım
LIB_DIR="/opt/scripts/lib"
load_library "${LIB_DIR}/logging.sh"
load_library "${LIB_DIR}/utils.sh"
load_library "${LIB_DIR}/network.sh"
Bu yaklaşımın bir adım ötesine geçerek, Python’daki import mantığına benzer şekilde, bir kütüphanenin birden fazla kez yüklenmesini de önleyebilirsin:
#!/usr/bin/env bash
# Yüklü kütüphaneleri takip eden associative array
declare -A _LOADED_LIBS
safe_source() {
local lib_path
lib_path=$(realpath "$1" 2>/dev/null) || {
echo "HATA: Geçersiz path: $1" >&2
return 1
}
# Zaten yüklüyse atla
if [[ -n "${_LOADED_LIBS[$lib_path]}" ]]; then
return 0
fi
if [[ ! -f "$lib_path" || ! -r "$lib_path" ]]; then
echo "HATA: Dosya erişilemiyor: $lib_path" >&2
return 1
fi
source "$lib_path" && _LOADED_LIBS[$lib_path]=1
}
Dizin Yapısı: Gerçek Dünya Önerisi
Kurumsal ortamlarda kullandığım ve işe yarayan bir dizin yapısı şu şekilde:
/opt/scripts/
├── bin/ # Çalıştırılabilir scriptler
│ ├── backup.sh
│ ├── deploy.sh
│ └── monitor.sh
├── lib/ # Kütüphane dosyaları
│ ├── core/
│ │ ├── logging.sh
│ │ ├── config.sh
│ │ └── validation.sh
│ ├── modules/
│ │ ├── database.sh
│ │ ├── network.sh
│ │ └── filesystem.sh
│ └── init.sh # Tüm core kütüphaneleri yükleyen bootstrap
├── conf/ # Konfigürasyon dosyaları
│ ├── global.conf
│ └── backup.conf
└── tests/ # Test dosyaları
├── test_logging.sh
└── test_utils.sh
Logging Kütüphanesi: Temel Bir Örnek
Her projede ihtiyaç duyduğun ilk modül genellikle logging olur. İşte production’da kullandığım bir logging kütüphanesi:
#!/usr/bin/env bash
# /opt/scripts/lib/core/logging.sh
# Kullanım: source logging.sh
# Guard: çift yüklemeyi önle
[[ -n "${_LIB_LOGGING_LOADED}" ]] && return 0
readonly _LIB_LOGGING_LOADED=1
# Varsayılan değerler (dışarıdan override edilebilir)
LOG_LEVEL="${LOG_LEVEL:-INFO}"
LOG_FILE="${LOG_FILE:-/var/log/scripts/app.log}"
LOG_DATE_FORMAT="${LOG_DATE_FORMAT:-%Y-%m-%d %H:%M:%S}"
# Log seviyeleri
declare -A _LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)
_log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date +"$LOG_DATE_FORMAT")
local current_level="${_LOG_LEVELS[$LOG_LEVEL]:-1}"
local msg_level="${_LOG_LEVELS[$level]:-1}"
[[ $msg_level -lt $current_level ]] && return 0
local formatted="[$timestamp] [$level] $message"
# Stderr'e yaz (ERROR ve üstü)
if [[ $msg_level -ge ${_LOG_LEVELS[ERROR]} ]]; then
echo "$formatted" >&2
else
echo "$formatted"
fi
# Dosyaya yaz
if [[ -n "$LOG_FILE" ]]; then
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null
echo "$formatted" >> "$LOG_FILE"
fi
}
log_debug() { _log "DEBUG" "$*"; }
log_info() { _log "INFO" "$*"; }
log_warn() { _log "WARN" "$*"; }
log_error() { _log "ERROR" "$*"; }
log_fatal() { _log "FATAL" "$*"; exit 1; }
Bu kütüphanede dikkat etmen gereken birkaç pattern var. Guard mekanizması (_LIB_LOGGING_LOADED değişkeni), dosyanın birden fazla kez source edilmesini engeller. readonly ile bu değişkenin sonradan değiştirilmesinin önüne geçilir. Değişken adlarının başındaki alt çizgi (_) ise bu değişkenlerin “iç kullanım” olduğunu işaret eder.
Konfigürasyon Yönetimi Modülü
Scriptlerin hardcoded değerler yerine dışarıdan konfigürasyon okuması, production ortamlarında büyük esneklik sağlar.
#!/usr/bin/env bash
# /opt/scripts/lib/core/config.sh
[[ -n "${_LIB_CONFIG_LOADED}" ]] && return 0
readonly _LIB_CONFIG_LOADED=1
# Config dosyası yükleme
config_load() {
local config_file="$1"
[[ ! -f "$config_file" ]] && {
log_error "Config dosyası bulunamadı: $config_file"
return 1
}
# Sadece KEY=VALUE formatındaki satırları işle
# Yorum satırları ve boş satırları atla
while IFS='=' read -r key value; do
# Başındaki/sonundaki boşlukları temizle
key="${key// /}"
value="${value%"${value##*[![:space:]]}"}"
# Yorum ve boş satırları atla
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
# Değişkeni export et
export "$key"="$value"
done < "$config_file"
log_info "Config yüklendi: $config_file"
}
# Zorunlu değişken kontrolü
config_require() {
local missing=0
for var in "$@"; do
if [[ -z "${!var}" ]]; then
log_error "Zorunlu konfigürasyon eksik: $var"
missing=1
fi
done
return $missing
}
Bootstrap Dosyası: Her Şeyi Bir Araya Getirmek
Her scriptin başına 10 satır source komutu yazmak yerine, tek bir bootstrap dosyası kullanmak çok daha temiz:
#!/usr/bin/env bash
# /opt/scripts/lib/init.sh
# Ana scriptlerde: source /opt/scripts/lib/init.sh
# Script'in kendi dizinini bul (symlink'leri de çözer)
_SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
SCRIPTS_BASE="$(dirname "$_SCRIPT_DIR")"
LIB_DIR="${SCRIPTS_BASE}/lib"
# Core kütüphaneleri sırayla yükle
_CORE_LIBS=(
"core/logging.sh"
"core/config.sh"
"core/validation.sh"
)
for _lib in "${_CORE_LIBS[@]}"; do
_lib_path="${LIB_DIR}/${_lib}"
if [[ ! -f "$_lib_path" ]]; then
echo "FATAL: Core kütüphane eksik: $_lib_path" >&2
exit 1
fi
source "$_lib_path"
done
# Global config yükle (varsa)
_GLOBAL_CONF="${SCRIPTS_BASE}/conf/global.conf"
[[ -f "$_GLOBAL_CONF" ]] && config_load "$_GLOBAL_CONF"
log_debug "Bootstrap tamamlandı. Kütüphaneler yüklendi."
Artık her script şöyle başlayabilir:
#!/usr/bin/env bash
source /opt/scripts/lib/init.sh
# Direkt kütüphane fonksiyonlarını kullan
log_info "Script başladı"
config_load "/opt/scripts/conf/backup.conf"
config_require "BACKUP_SOURCE" "BACKUP_DEST" "RETENTION_DAYS"
Gerçek Dünya Senaryosu: Backup Scripti
Tüm bunları bir araya getiren gerçekçi bir senaryo görelim. Elimizde hem daily backup hem de weekly backup scripti var ve ikisi de aynı temel işlemleri kullanıyor.
#!/usr/bin/env bash
# /opt/scripts/lib/modules/filesystem.sh
[[ -n "${_LIB_FILESYSTEM_LOADED}" ]] && return 0
readonly _LIB_FILESYSTEM_LOADED=1
# Güvenli dizin oluşturma
fs_mkdir_safe() {
local dir="$1"
local mode="${2:-755}"
if [[ -d "$dir" ]]; then
log_debug "Dizin zaten mevcut: $dir"
return 0
fi
mkdir -p -m "$mode" "$dir" || {
log_error "Dizin oluşturulamadı: $dir"
return 1
}
log_info "Dizin oluşturuldu: $dir (mode: $mode)"
}
# Eski dosyaları temizleme
fs_cleanup_old_files() {
local dir="$1"
local days="$2"
local pattern="${3:-*}"
[[ ! -d "$dir" ]] && { log_warn "Dizin bulunamadı: $dir"; return 1; }
local count
count=$(find "$dir" -name "$pattern" -mtime +"$days" | wc -l)
if [[ $count -gt 0 ]]; then
find "$dir" -name "$pattern" -mtime +"$days" -delete
log_info "$count eski dosya temizlendi ($days günden eski)"
else
log_debug "Temizlenecek eski dosya yok"
fi
}
# Disk kullanım kontrolü
fs_check_disk_space() {
local path="$1"
local threshold="${2:-90}"
local usage
usage=$(df "$path" | awk 'NR==2 {print $5}' | tr -d '%')
if [[ $usage -ge $threshold ]]; then
log_warn "Disk kullanımı yüksek: $path - %$usage (eşik: %$threshold)"
return 1
fi
log_debug "Disk kullanımı normal: $path - %$usage"
return 0
}
Şimdi bu modülü kullanan backup scripti:
#!/usr/bin/env bash
# /opt/scripts/bin/backup.sh
source /opt/scripts/lib/init.sh
source /opt/scripts/lib/modules/filesystem.sh
config_load "/opt/scripts/conf/backup.conf"
config_require "BACKUP_SOURCE" "BACKUP_DEST" "RETENTION_DAYS"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}.tar.gz"
BACKUP_PATH="${BACKUP_DEST}/${BACKUP_NAME}"
log_info "Backup başlıyor: $BACKUP_SOURCE -> $BACKUP_PATH"
# Disk alanı kontrolü
fs_check_disk_space "$BACKUP_DEST" 85 || {
log_fatal "Yeterli disk alanı yok, backup iptal edildi"
}
# Hedef dizini oluştur
fs_mkdir_safe "$BACKUP_DEST"
# Backup al
if tar -czf "$BACKUP_PATH" "$BACKUP_SOURCE" 2>/dev/null; then
log_info "Backup başarılı: $BACKUP_NAME"
# Eski backupları temizle
fs_cleanup_old_files "$BACKUP_DEST" "$RETENTION_DAYS" "backup_*.tar.gz"
else
log_error "Backup başarısız!"
exit 1
fi
İşte modüler tasarımın güzelliği burada ortaya çıkıyor. backup.sh son derece temiz ve okunabilir. Her fonksiyonun ne yaptığını biliyorsun ve gerektiğinde ilgili modüle gidip detaya bakabiliyorsun.
Namespace Kullanımı: İsim Çakışmalarını Önlemek
Birden fazla kütüphane kullandığında fonksiyon ve değişken isimlerinin çakışması kaçınılmaz olur. Bunu önlemenin en basit yolu namespace convention kullanmak. Her kütüphane kendi prefix’ini kullanır:
- log_: logging.sh fonksiyonları
- config_: config.sh fonksiyonları
- fs_: filesystem.sh fonksiyonları
- net_: network.sh fonksiyonları
- db_: database.sh fonksiyonları
Dahili (dışarıya expose edilmemesi gereken) fonksiyonlar için alt çizgi ile başla: _log_format_message, _config_parse_line gibi. Bu naming convention, kod okuduğunda bir fonksiyonun hangi modülden geldiğini anında anlamana sağlar.
Modülleri Test Etmek
Modüler yapının en büyük avantajlarından biri test edilebilirlik. Her modülü bağımsız olarak test edebilirsin:
#!/usr/bin/env bash
# /opt/scripts/tests/test_filesystem.sh
source /opt/scripts/lib/init.sh
source /opt/scripts/lib/modules/filesystem.sh
# Basit test framework
_tests_passed=0
_tests_failed=0
assert_eq() {
local desc="$1"
local expected="$2"
local actual="$3"
if [[ "$expected" == "$actual" ]]; then
echo " PASS: $desc"
((_tests_passed++))
else
echo " FAIL: $desc"
echo " Beklenen: $expected"
echo " Gerçekleşen: $actual"
((_tests_failed++))
fi
}
assert_exit_zero() {
local desc="$1"
shift
if "$@"; then
echo " PASS: $desc"
((_tests_passed++))
else
echo " FAIL: $desc (exit code: $?)"
((_tests_failed++))
fi
}
echo "=== filesystem.sh testleri ==="
# Test: Dizin oluşturma
TEST_DIR="/tmp/test_$$"
assert_exit_zero "Dizin oluşturma" fs_mkdir_safe "$TEST_DIR"
assert_eq "Dizin var mı" "1" "$([[ -d $TEST_DIR ]] && echo 1 || echo 0)"
# Test: Varolan dizin için tekrar çağırma
assert_exit_zero "Varolan dizin için çağırma" fs_mkdir_safe "$TEST_DIR"
# Temizlik
rm -rf "$TEST_DIR"
echo ""
echo "Sonuç: $_tests_passed başarılı, $_tests_failed başarısız"
[[ $_tests_failed -eq 0 ]] && exit 0 || exit 1
Sık Yapılan Hatalar ve Çözümleri
Değişken kirliliği en yaygın sorun. Kütüphane dosyalarında global değişken tanımlarken dikkatli ol. Mümkünse fonksiyon içindeki değişkenleri local ile tanımla. Kütüphane seviyesi değişkenler için uppercase ve anlamlı prefix kullan.
Circular dependency de başa bela olabilir. A kütüphanesi B’yi, B kütüphanesi A’yı source ederse sonsuz döngüye girersin. Guard mekanizması ([[ -n "${_LIB_X_LOADED}" ]] && return 0) bu sorunu çözer.
Hata yayılımı konusuna da dikkat et. Kütüphane fonksiyonlarında exit yerine return kullan. Exit kodu ana scriptten yayılmalı, kütüphaneden değil. Çünkü kütüphanedeki bir exit 1 tüm scripti öldürür ve bu her zaman istenen davranış olmayabilir.
Relative path sorunları da sık karşılaşılan bir tuzak. Kütüphane dosyalarında başka dosyalara referans verirken her zaman absolute path ya da ${BASH_SOURCE[0]} tabanlı relative path kullan. Çünkü scriptin çalıştırıldığı dizin ($PWD) tahmin ettiğin yer olmayabilir.
Sonuç
Modüler bash scripting, başlangıçta küçük bir overhead gibi görünse de orta ve uzun vadede ciddi bir yatırım. Dört beş kişilik bir sysadmin ekibinde çalışıyorsan ya da onlarca farklı otomasyon scripti yönetiyorsan, bu yapıyı kurmak için harcadığın bir günlük zaman ilerleyen aylarda defalarca karşılığını verir.
Özetlemek gerekirse: Core kütüphaneleri (logging.sh, config.sh, validation.sh) her projede yeniden kullanılabilir şekilde yaz. Proje bazlı modülleri (database.sh, network.sh gibi) ayrı tutup bootstrap mekanizmasıyla bir araya getir. Guard mekanizması ve namespace convention’larını ihmal etme. Her modül için en azından temel testler yaz.
Bash’in sınırlı bir dil olduğu doğru, ama iyi tasarımla ondan da son derece bakımı kolay, okunabilir ve güvenilir sistemler çıkarmak mümkün. Karmaşıklık script sayısından değil, kötü organizasyondan kaynaklanır.