برمجة الصدفة والأتمتة

الدوال وتنظيم السكريبتات

18 دقيقة الدرس 5 من 28

الدوال وتنظيم السكريبتات

مع نمو السكريبتات إلى ما هو أبعد من شاشة واحدة من النص، يظهر فخ شائع: كتل منسوخة ولصقة، ومنطق متشابك، وكابوس صيانة عند تغيّر المتطلبات. تتعامل فرق الهندسة المتميزة مع سكريبتات الشل كأي قاعدة كود أخرى — مع دوال، وواجهات واضحة، وعقود لرموز الخروج. يتناول هذا الدرس كيفية كتابة دوال آمنة وقابلة للتركيب والاختبار، وكيفية تنظيم السكريبتات بحيث يستطيع عضو جديد في الفريق التنقل فيها بثقة في الثالثة صباحًا خلال حادث طارئ.

تعريف الدوال واستدعاؤها

يدعم 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 سطر. اتباع هذا النمط يعني أن أي مهندس في فريقك يمكنه فحص سكريبت ومعرفة مكان قطعة معينة من المنطق فورًا.

Standard Shell Script Layout 1. Shebang + set -euo pipefail + Header Comment Purpose, usage, author, date 2. Constants & Configuration readonly TIMEOUT=30 readonly LOG_FILE="/var/log/deploy.log" 3. Utility Functions log_info() log_error() die() require_command() 4. Business Logic Functions check_disk_space() wait_for_db() run_migrations() notify_slack() 5. main() + Entry Point Guard main "$@" — only at the bottom, only when sourced check passes
التخطيط ذو الخمسة أقسام المستخدم في سكريبتات الشل الإنتاجية على نطاق واسع. كل قسم له مسؤولية واحدة.

أهم النمط الهيكلي هو دالة 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 الكبيرة الانجراف بالنسخ واللصق:

# lib/logging.sh — يُستورد من جميع السكريبتات #!/usr/bin/env bash readonly LOG_LEVEL="${LOG_LEVEL:-INFO}" _log() { local level="$1"; shift echo "[${level}] $(date +%Y-%m-%dT%H:%M:%S) $*" } log_info() { _log INFO "$@"; } log_warn() { _log WARN "$@" >&2; } log_error() { _log ERROR "$@" >&2; } log_debug() { [[ "$LOG_LEVEL" == "DEBUG" ]] && _log DEBUG "$@" || true; } # في deploy.sh — تحميل المكتبة source "${SCRIPT_DIR}/lib/logging.sh"

الدوال المُعرَّفة في ملف مُستورَد توجد في بيئة السكريبت المستدعي طوال فترة حياته. يمنحك هذا كودًا معياريًا وقابلًا للإعادة دون أي مدير حزم أو حمل إضافي للمترجم.

نمط دالة 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 مطلوب"

الدوال المنظمة جيدًا، والمتغيرات المحلية الصارمة، وعقود رموز الخروج، والتخطيط ذو الخمسة أقسام — هذه ليست تفضيلات أسلوبية بل هي الفارق بين السكريبتات التي تصمد في حوادث الإنتاج وتلك التي تُسببها. تعامل مع كود الشل بنفس الانضباط الذي تطبقه على أي لغة أخرى في مكدسك التقني.