مع نمو السكريبتات إلى ما هو أبعد من شاشة واحدة من النص، يظهر فخ شائع: كتل منسوخة ولصقة، ومنطق متشابك، وكابوس صيانة عند تغيّر المتطلبات. تتعامل فرق الهندسة المتميزة مع سكريبتات الشل كأي قاعدة كود أخرى — مع دوال، وواجهات واضحة، وعقود لرموز الخروج. يتناول هذا الدرس كيفية كتابة دوال آمنة وقابلة للتركيب والاختبار، وكيفية تنظيم السكريبتات بحيث يستطيع عضو جديد في الفريق التنقل فيها بثقة في الثالثة صباحًا خلال حادث طارئ.
تعريف الدوال واستدعاؤها
يدعم Bash طريقتين متكافئتين نحويًا لتعريف دالة. الصيغة المتوافقة مع POSIX هي المفضلة للتنقلية:
#!/usr/bin/env bash
set -euo pipefail
# التعريف المتوافق مع POSIX (المفضل)
log_info() {
echo "[INFO] $(date +%Y-%m-%dT%H:%M:%S) $*"
}
# التعريف الخاص بـ Bash (يعمل، لكنه أقل تنقلية)
function log_error {
echo "[ERROR] $(date +%Y-%m-%dT%H:%M:%S) $*" >&2
}
# استدعاء الدوال — مطابق لاستدعاء أي أمر
log_info "بدأ النشر"
log_error "مساحة القرص أقل من الحد الأدنى"
نقطتان مهمتان: أولًا، يجب تعريف الدالة قبل استدعائها — يقرأ Bash من الأعلى إلى الأسفل. ثانيًا، لاحظ أن $* يمرر جميع الوسائط كسلسلة واحدة، بينما "$@" يحافظ على حدود الكلمات وهو المفضل عند تمرير الوسائط إلى الأوامر الفرعية.
المتغيرات المحلية: إبقاء الحالة محاطة
بدون الكلمة المفتاحية local، كل متغير تضبطه داخل دالة يصبح عالميًا. في سكريبت يحتوي على عشرين دالة، يُنتج هذا تعارضات خفية يصعب تصحيحها. القاعدة على نطاق الإنتاج: كل متغير داخل دالة هو local ما لم يكن مشاركًا عن قصد.
#!/usr/bin/env bash
set -euo pipefail
get_free_disk_mb() {
local mount_point="${1:-/}"
local free_kb
free_kb=$(df -k "$mount_point" | awk 'NR==2 {print $4}')
echo $(( free_kb / 1024 ))
}
check_disk_space() {
local threshold_mb="${1:-500}"
local free_mb
free_mb=$(get_free_disk_mb "/")
if (( free_mb < threshold_mb )); then
log_error "مساحة قرص منخفضة: ${free_mb} ميجابايت متاح (الحد: ${threshold_mb} ميجابايت)"
return 1
fi
log_info "القرص بخير: ${free_mb} ميجابايت متاح"
}
check_disk_space 1000
لاحظ كيف أن free_kb وfree_mb كلاهما مُعلَّن بـlocal. عند إعادة get_free_disk_mb، تختفي تلك الأسماء ولا يمكنها تلويث نطاق المستدعي.
فخ الإنتاج — فخ المتغير المحلي غير المضبوط:local varname=$(command) تخرج دائمًا بكود 0، حتى لو فشل الأمر — لأن local نفسها هي الأمر الأخير وتنجح. مع set -e، يُبتلع الفشل. افصل التعريف: local varname في سطر، ثم varname=$(command) في السطر التالي.
رموز الخروج كعقد
تُبلّغ الدوال المستدعيَ بالنجاح أو الفشل عبر رموز الخروج، تمامًا كأي أمر Unix. الكود 0 يعني النجاح؛ أي قيمة غير صفرية تعني الفشل. هذا هو العقد الأساسي الذي يُمكّن جمل if، وتسلسل &&/||، وسلوك set -e من العمل بشكل صحيح.
#!/usr/bin/env bash
set -euo pipefail
wait_for_service() {
local host="$1"
local port="$2"
local max_retries="${3:-30}"
local retry_interval=2
local attempt=0
while (( attempt < max_retries )); do
if nc -z -w 2 "$host" "$port" 2>/dev/null; then
log_info "الخدمة ${host}:${port} متاحة"
return 0
fi
(( attempt++ )) || true
log_info "انتظار ${host}:${port} — المحاولة ${attempt}/${max_retries}"
sleep "$retry_interval"
done
log_error "لم تصبح الخدمة ${host}:${port} متاحة بعد ${max_retries} محاولة"
return 1
}
# المستدعي يقرر ما يفعله بالنجاح أو الفشل
if ! wait_for_service "db.internal" 5432 15; then
log_error "قاعدة البيانات غير متاحة — إلغاء النشر"
exit 1
fi
تضبط جملة return كود خروج الدالة. عند انتهاء دالة بدون return صريح، يصبح كود خروج الأمر الأخير المُنفَّذ هو كود خروج الدالة — إعداد افتراضي مفيد لكن يجب أن تكون واعيًا به.
فكرة رئيسية — دلالات رموز الخروج: قم بتوحيد الأكواد غير الصفرية عبر السكريبت. استخدام return 1 لكل فشل يعمل في السكريبتات الصغيرة؛ للأتمتة الأكبر، عرّف رموزًا مسماة في الأعلى (readonly ERR_DISK=2 ERR_NETWORK=3 ERR_AUTH=4) حتى تتمكن أنظمة المراقبة من تصنيف الأعطال تلقائيًا.
التقاط القيم المُعادة
لا تستطيع دوال Bash إعادة سلاسل نصية عشوائية كما تفعل معظم اللغات — فـreturn تحمل فقط عددًا صحيحًا. النمط الاصطلاحي هو echo الناتج إلى stdout والتقاطه باستبدال الأوامر.
#!/usr/bin/env bash
set -euo pipefail
# تُعيد الدالة قيمة بطباعتها إلى stdout
get_git_sha() {
local length="${1:-7}"
git rev-parse --short="${length}" HEAD
}
current_sha=$(get_git_sha)
long_sha=$(get_git_sha 12)
log_info "نشر commit: ${current_sha} (كامل: ${long_sha})"
# إعادة قيم متعددة: استخدم متغيرات عالمية (بتحفظ) أو أعمدة stdout
get_container_stats() {
local container_name="$1"
docker stats --no-stream --format "{{.CPUPerc}} {{.MemUsage}}" "$container_name"
}
read -r cpu mem <<< "$(get_container_stats "api-server")"
log_info "حاوية API — المعالج: ${cpu} الذاكرة: ${mem}"
تنظيم السكريبتات الكبيرة: التخطيط القياسي
يتقاطع دليل أسلوب Shell من Google وكتيب الأدوات الداخلية لـ Netflix على نفس النمط الهيكلي للسكريبتات التي تتجاوز حوالي 100 سطر. اتباع هذا النمط يعني أن أي مهندس في فريقك يمكنه فحص سكريبت ومعرفة مكان قطعة معينة من المنطق فورًا.
التخطيط ذو الخمسة أقسام المستخدم في سكريبتات الشل الإنتاجية على نطاق واسع. كل قسم له مسؤولية واحدة.
أهم النمط الهيكلي هو دالة main مع حارس نقطة الدخول. يتيح هذا للسكريبت أن يُنفَّذ مباشرة وأن يُستورد بواسطة سكريبتات أخرى أو أطر اختبار دون آثار جانبية.
#!/usr/bin/env bash
# =============================================================
# deploy.sh — مساعد النشر المتدرج
# الاستخدام: ./deploy.sh <البيئة> [--dry-run]
# =============================================================
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/deploy.log"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
# --- دوال الأدوات -------------------------------------------
log_info() { echo "[INFO] $(date +%T) $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[ERROR] $(date +%T) $*" | tee -a "$LOG_FILE" >&2; }
die() { log_error "$*"; exit 1; }
require_command() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || die "الأمر المطلوب غير موجود: ${cmd}"
}
# --- منطق العمل ---------------------------------------------
preflight_checks() {
require_command docker
require_command kubectl
require_command curl
check_disk_space 2000
log_info "اجتازت الفحوصات الأولية"
}
check_disk_space() {
local threshold_mb="$1"
local free_mb
free_mb=$(df -k / | awk 'NR==2 {print int($4/1024)}')
(( free_mb >= threshold_mb )) || die "قرص غير كافٍ: ${free_mb} ميجابايت متاح"
}
notify_slack() {
[[ -z "$SLACK_WEBHOOK" ]] && return 0
local message="$1"
curl -sf -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": \"${message}\"}" >/dev/null
}
# --- التنسيق الرئيسي ----------------------------------------
main() {
local env="${1:?الاستخدام: deploy.sh <البيئة>}"
local dry_run="${2:-}"
log_info "=== بدأ النشر إلى ${env} ==="
preflight_checks
notify_slack ":rocket: النشر إلى *${env}* بدأ بواسطة $(whoami)"
if [[ "$dry_run" == "--dry-run" ]]; then
log_info "وضع التجربة — تخطي خطوات النشر الفعلية"
else
log_info "جارٍ النشر..."
# منطق النشر الحقيقي هنا
fi
log_info "=== اكتمل النشر إلى ${env} ==="
notify_slack ":white_check_mark: النشر إلى *${env}* نجح"
}
# --- حارس نقطة الدخول ---------------------------------------
# يسمح باستيراد هذا الملف للاختبار دون تنفيذ main()
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
ممارسة احترافية — SCRIPT_DIR للاستيراد المحمول:readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" يمنحك المسار المطلق للمجلد الذي يحتوي على السكريبت الحالي، بغض النظر عن مكان استدعائه. استخدمه لاستيراد الملفات المجاورة: source "${SCRIPT_DIR}/lib/logging.sh". هذا هو النمط الكنسي في Google وGitHub وHashiCorp لمشاريع الشل متعددة الملفات.
بناء مكتبة مشتركة
عندما تشترك عدة سكريبتات في نفس دوال الأدوات، استخرجها إلى ملف مكتبة واستورده. هكذا تتجنب قواعد كود DevOps الكبيرة الانجراف بالنسخ واللصق:
الدوال المُعرَّفة في ملف مُستورَد توجد في بيئة السكريبت المستدعي طوال فترة حياته. يمنحك هذا كودًا معياريًا وقابلًا للإعادة دون أي مدير حزم أو حمل إضافي للمترجم.
نمط دالة die
كل سكريبت إنتاجي في Netflix وStripe ومؤسسات مماثلة يحتوي على دالة die. تمركز نمط "اطبع خطأً واخرج بفشل"، مما يُبقي بقية السكريبت نظيفة:
die() {
log_error "$*"
# اختياري: إرسال تنبيه، تنظيف الملفات المؤقتة، إلخ
exit 1
}
# الاستخدام — يُعوّض معالجة الأخطاء متعددة الأسطر في جميع أنحاء السكريبت
[[ -f "$CONFIG_FILE" ]] || die "ملف الإعداد غير موجود: ${CONFIG_FILE}"
[[ "$EUID" -eq 0 ]] || die "يجب تشغيل هذا السكريبت كمستخدم root"
[[ -n "$API_KEY" ]] || die "متغير البيئة API_KEY مطلوب"
الدوال المنظمة جيدًا، والمتغيرات المحلية الصارمة، وعقود رموز الخروج، والتخطيط ذو الخمسة أقسام — هذه ليست تفضيلات أسلوبية بل هي الفارق بين السكريبتات التي تصمد في حوادث الإنتاج وتلك التي تُسببها. تعامل مع كود الشل بنفس الانضباط الذي تطبقه على أي لغة أخرى في مكدسك التقني.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية