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

الشروط والاختبارات

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

الشروط والاختبارات

كل سكريبت حقيقي يحتاج إلى اتخاذ قرارات. سكريبت النسخ الاحتياطي يجب أن يتوقف عندما يمتلئ القرص. سكريبت النشر يجب أن يرفض التشغيل على الفرع الخطأ. سكريبت فحص الصحة يجب أن ينبّه فريق الطوارئ حين يُعيد الخادم حالة غير 200. كل هذا يعتمد على بنية واحدة: الشرط. في هذا الدرس ستتقن الشروط في Bash بعمق كافٍ لكتابة منطق قرار بجودة الإنتاج.

كيف يُقيّم Bash الشروط

شروط Bash مبنية على أكواد الخروج وليس على قيم منطقية. كل أمر يُعيد عدداً صحيحاً بين 0 و 255. كود الخروج 0 يعني النجاح (صحيح)؛ وأي قيمة أخرى تعني الفشل (خطأ). الكلمة المفتاحية if تنفّذ أمراً وتتفرّع بناءً على كود خروجه.

# أبسط شرط ممكن: التفريع بناءً على كود خروج أي أمر if ping -c1 -W1 8.8.8.8 >/dev/null 2>&1; then echo "الشبكة متصلة" else echo "الشبكة منقطعة" fi # التحقق من كود الخروج يدوياً grep -q "ERROR" /var/log/app.log if [ $? -ne 0 ]; then echo "لا توجد أخطاء" fi # الأسلوب الاصطلاحي — اختبار مباشر، لا حاجة لـ $? if grep -q "ERROR" /var/log/app.log; then echo "تم رصد أخطاء — تنبيه فريق الطوارئ" fi
القاعدة الذهبية: اختبر دائماً أكواد الخروج لا المخرجات. تحليل مخرجات الأوامر هش؛ اختبار نجاح الأمر قوي. عندما تكتب if command; then فأنت تختبر كود الخروج مباشرة — دون subshell أو متغير أو مشاكل تقتيس.

[ ] مقابل [[ ]] — اعرف الفرق

[ ] هو أمر POSIX (/usr/bin/[). [[ ]] هو كلمة مفتاحية في Bash. في سكريبتات Bash، استخدم دائماً [[ ]]. ليست متاحة في /bin/sh، لكن للسكريبتات التي تبدأ بـ #!/usr/bin/env bash فهي الأداة الصحيحة لكل موقف.

الفروق العملية مهمة في الإنتاج:

  • تقسيم الكلمات: [ $var = "yes" ] ينفجر إذا احتوى $var على مسافات أو كان فارغاً. أما [[ $var == "yes" ]] فلا يُجري تقسيماً للكلمات حتى بدون تقتيس.
  • مطابقة الأنماط: [[ $branch == release/* ]] يدعم أنماط glob بصورة أصلية — دون حاجة لتقتيس الجانب الأيمن.
  • مطابقة التعبيرات النظامية: [[ $line =~ ^[0-9]+$ ]] يدعم POSIX ERE الكاملة. يجب أن يكون التعبير النظامي غير مقتَّس.
  • العوامل المنطقية: [[ $a == "x" && $b == "y" ]] تستخدم && و|| داخل الأقواس. مع [ ] عليك استخدام -a و-o اللتين عفا عليهما الزمن.
#!/usr/bin/env bash set -euo pipefail branch="release/2.4.1" version="42" filename="report 2024.csv" # --- اختبارات النصوص مع [[ ]] --- # المساواة وعدم المساواة [[ "$branch" == "main" ]] && echo "على الفرع الرئيسي" [[ "$branch" != "main" ]] && echo "ليس على الفرع الرئيسي" # مطابقة glob — بدون تقتيس النمط [[ "$branch" == release/* ]] && echo "تم رصد فرع إصدار" # مطابقة تعبير نظامي — يجب أن يكون التعبير غير مقتَّس [[ "$version" =~ ^[0-9]+$ ]] && echo "الإصدار رقمي" # نص فارغ / غير فارغ [[ -z "$branch" ]] && echo "الفرع فارغ" [[ -n "$branch" ]] && echo "الفرع محدد" # آمن حتى مع المسافات — [[ ]] لا يُجري تقسيماً للكلمات [[ "$filename" == *.csv ]] && echo "ملف CSV" # --- الدمج المنطقي --- [[ "$branch" == release/* && -n "$version" ]] && echo "إصدار برقم إصدار"

اختبارات الملفات

سكريبتات DevOps تستجوب نظام الملفات باستمرار — فحص وجود ملف الإعداد قبل قراءته، وكتابية الدليل قبل النشر فيه، ووجود المقبس قبل إرسال حركة المرور. توفر Bash مجموعة كاملة من عوامل اختبار الملفات:

CONFIG="/etc/app/config.yaml" LOG_DIR="/var/log/app" SOCKET="/run/gunicorn.sock" BINARY="/usr/local/bin/deploy-tool" # اختبارات الوجود [[ -e "$CONFIG" ]] && echo "موجود (ملف أو مجلد)" [[ -f "$CONFIG" ]] && echo "ملف عادي" [[ -d "$LOG_DIR" ]] && echo "مجلد" [[ -L "$CONFIG" ]] && echo "رابط رمزي" [[ -S "$SOCKET" ]] && echo "مقبس" # اختبارات الصلاحيات [[ -r "$CONFIG" ]] && echo "قابل للقراءة" [[ -w "$LOG_DIR" ]] && echo "قابل للكتابة" [[ -x "$BINARY" ]] && echo "قابل للتنفيذ" # اختبار الحجم [[ -s "$CONFIG" ]] && echo "ملف غير فارغ" # حماية عملية: توقف إذا كان الإعداد مفقوداً if [[ ! -f "$CONFIG" ]]; then echo "خطأ: لم يُعثر على الإعداد في $CONFIG" >&2 exit 1 fi # حماية عملية: إنشاء مجلد السجلات إذا لم يكن موجوداً if [[ ! -d "$LOG_DIR" ]]; then mkdir -p "$LOG_DIR" chmod 750 "$LOG_DIR" fi

المقارنات الرقمية

عوامل المساواة النصية (==، !=) تُجري مقارنة معجمية. للأرقام استخدم العوامل الحسابية: -eq، -ne، -lt، -le، -gt، -ge. بديلاً، استخدم سياق الحساب (( )) الذي يعامل القيم كأعداد صحيحة ويُعيد كود خروج 0 للقيم غير الصفرية (صحيح) و1 للصفر (خطأ).

disk_usage=87 max_allowed=80 http_status=503 retry_count=3 # مع [[ ]] وعوامل الحساب if [[ "$disk_usage" -gt "$max_allowed" ]]; then echo "تنبيه: استخدام القرص ${disk_usage}% يتجاوز الحد" >&2 fi # مع (( )) — سياق حسابي، أكثر وضوحاً للرياضيات if (( http_status >= 500 )); then echo "خطأ في الخادم: $http_status" fi if (( retry_count == 0 )); then echo "لا محاولات متبقية — فشل النشر" exit 1 fi # مركّب: القرص والعقد كلاهما بخير used_inodes=$(df -i /var | awk 'NR==2{print $5}' | tr -d '%') if (( disk_usage < 90 && used_inodes < 90 )); then echo "صحة القرص جيدة" fi
استخدم (( )) للشروط الحسابية. أوضح وأكثر إيجازاً، ويدعم عوامل أسلوب C مثل ++ و% و**، ويتجنب ضوضاء -gt/-lt. فقط انتبه أن (( 0 )) يُعيد كود خروج 1 (خطأ منطقي) — مفيد عند عدّ المحاولات نزولاً نحو الصفر.

جملة case

عندما يتحكم متغير واحد في فروع متعددة، فإن سلسلة من كتل elif تكون أصعب قراءةً وصيانةً من جملة case. تدعم case أنماط glob والتبادل باستخدام | وحالة شاملة *). هي الطريقة الاصطلاحية للتوزيع بناءً على أسماء البيئات أو مستويات السجلات أو أساليب HTTP أو الأوامر الفرعية.

#!/usr/bin/env bash set -euo pipefail ENV="${1:-}" # الوسيطة الأولى، فارغة إذا لم تُقدَّم HTTP_METHOD="POST" # --- التوزيع بناءً على اسم البيئة --- case "$ENV" in production|prod) REPLICAS=10 LOG_LEVEL="warn" ;; staging|stage) REPLICAS=2 LOG_LEVEL="info" ;; development|dev|"") REPLICAS=1 LOG_LEVEL="debug" ;; *) echo "خطأ: بيئة غير معروفة '$ENV'" >&2 echo "الاستخدام: $0 {production|staging|development}" >&2 exit 1 ;; esac echo "نشر بـ $REPLICAS نسخ، مستوى السجل=$LOG_LEVEL" # --- التوزيع بناءً على أسلوب HTTP (مفيد في معالجات webhook) --- case "$HTTP_METHOD" in GET|HEAD) echo "طلب للقراءة فقط" ;; POST|PUT|PATCH) echo "طلب تعديلي — التحقق من الجسم" ;; DELETE) echo "طلب تدميري — يتطلب تأكيداً" ;; *) echo "أسلوب غير معروف: $HTTP_METHOD"; exit 1 ;; esac
أضف دائماً حالة شاملة *). جملة case بدون حالة شاملة تُنفّذ لا شيء بصمت حين لا تُطابق القيمةُ أيَّ نمط — مصدر شائع للأخطاء حيث تُسبّب الأخطاء المطبعية في اسم البيئة تشغيل النشر دون أي إعداد. اجعل الحالة الشاملة تُخطئ بصوت عالٍ وتخرج بكود غير صفري.

مخطط: تدفق القرار الشرطي في سكريبت نشر

Conditional decision flow in a Bash deploy script deploy.sh branch == main? No Abort exit 1 Yes disk < 90%? No Alert exit 1 Yes case $ENV in prod | staging | dev
يُسلسل سكريبت النشر حراساً شرطية متعددة قبل الوصول إلى توزيع البيئة.

دمج الشروط: أنماط حقيقية

سكريبتات الإنتاج نادراً ما تختبر شرطاً واحداً. إليك دالة فحص صحة واقعية تجمع اختبارات الملفات والمقارنات الرقمية واختبارات النصوص بالأسلوب الذي ستراه في كود البنية التحتية الضخم:

#!/usr/bin/env bash set -euo pipefail APP_PID_FILE="/run/app/app.pid" APP_URL="http://localhost:8080/healthz" MAX_RESPONSE_MS=500 check_health() { local pid status elapsed # 1. ملف PID يجب أن يكون موجوداً وغير فارغ if [[ ! -s "$APP_PID_FILE" ]]; then echo "حرج: ملف PID مفقود أو فارغ" >&2 return 1 fi pid=$(cat "$APP_PID_FILE") # 2. يجب أن تكون العملية تعمل فعلاً if ! kill -0 "$pid" 2>/dev/null; then echo "حرج: العملية $pid لا تعمل" >&2 return 1 fi # 3. نقطة نهاية الصحة يجب أن تُعيد 200 read -r status elapsed <<< "$(curl -o /dev/null -s \ -w "%{http_code} %{time_total_ms}" \ --max-time 2 "$APP_URL")" if [[ "$status" != "200" ]]; then echo "حرج: healthz أعاد HTTP $status" >&2 return 1 fi # 4. يجب أن يكون الرد سريعاً بما يكفي if (( elapsed > MAX_RESPONSE_MS )); then echo "تحذير: healthz بطيء — ${elapsed}ms > ${MAX_RESPONSE_MS}ms" >&2 return 1 fi echo "جيد: التطبيق سليم (HTTP $status، ${elapsed}ms)" } check_health || { notify_oncall "فشل فحص صحة التطبيق"; exit 1; }
نمط إنتاجي — دوال حارسة لا سكريبتات. لفّ كل فحص في دالة تُعيد كود خروج ذا معنى. المُستدعي يقرر ما إذا كان سيُنبّه فريق الطوارئ أو يُعيد المحاولة أو يخرج بقوة. هذا الفصل للمخاوف هو كيفية كتابة فرق SRE للإجراءات التشغيلية القابلة للاختبار المستقل.

مرجع سريع: عوامل الاختبار

  • النصوص: -z (فارغ)، -n (غير فارغ)، ==، !=، <، > (معجمي)
  • الأرقام: -eq، -ne، -lt، -le، -gt، -ge — أو (( )) مع ==، !=، <، >
  • الملفات: -e (موجود)، -f (ملف عادي)، -d (مجلد)، -s (غير فارغ)، -r/-w/-x (صلاحيات)، -L (رابط رمزي)
  • التعبيرات النظامية: [[ $var =~ pattern ]] — POSIX ERE غير مقتَّس على اليمين
  • النفي: ! قبل أي اختبار: [[ ! -f "$file" ]]