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

الحلقات والتكرار

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

الحلقات والتكرار

السكريبتات الإنتاجية نادرًا ما تنفذ عملية واحدة مرة واحدة. فهي تنشر التحديثات على عشرة خوادم، وتدور ثلاثين ملف سجل، وتتحقق من كل سطر في ملف إعدادات، أو تعيد تشغيل خدمة حتى تستعيد عافيتها. الحلقات هي ما يحوّل أمرًا منفردًا إلى أتمتة شاملة. يغطي هذا الدرس أربعة أنماط للحلقات ستستخدمها يوميًا: حلقة for-in للقوائم، وحلقة while القائمة على شرط، وقراءة الملفات سطرًا بسطر، وحلقة الـ glob لمعالجة محتويات المجلدات بأمان.

حلقة for-in: التكرار على قائمة

أكثر حلقات Bash شيوعًا هي التي تتكرر على قائمة كلمات مفصولة بمسافات. صيغتها for variable in list; do ... done. يمكن أن تكون القائمة كلمات حرفية، أو استبدال أمر، أو توسيع بالأقواس، أو نمط glob.

#!/usr/bin/env bash set -euo pipefail # --- قائمة حرفية --- ENVS=(staging canary production) for env in "${ENVS[@]}"; do echo "Deploying to: ${env}" # ./deploy.sh "${env}" done # --- توسيع بالأقواس (تسلسل) --- for i in {1..5}; do echo "Attempt ${i}" done # --- استبدال أمر: التكرار على نطاقات kubectl --- for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do echo "Namespace: ${ns}" done
فخ تقسيم الكلمات: لا تستخدم أبدًا for item in $(command) عندما يمكن أن يحتوي الناتج على مسافات أو أسطر جديدة داخل عنصر منطقي واحد. فأسماء الملفات التي تحتوي على مسافات ستُقسَّم إلى تكرارات منفصلة. استخدم while IFS= read -r (المُغطّى أدناه) أو mapfile عندما يكون المدخل مفصولًا بأسطر جديدة.

حلقة while: التكرار المبني على شرط

تعمل حلقة while طالما يُقيَّم شرطها إلى true (حالة خروج صفر). هي الأداة المناسبة حين لا تعرف مسبقًا عدد التكرارات — كانتظار استعداد خدمة، أو استطلاع واجهة برمجية، أو تفريغ طابور انتظار.

#!/usr/bin/env bash set -euo pipefail # --- انتظار حتى تصبح الخدمة جاهزة (نمط إنتاجي) --- SERVICE_URL="http://localhost:8080/health" MAX_WAIT=120 # ثوانٍ INTERVAL=5 elapsed=0 echo "Waiting for service at ${SERVICE_URL}..." while ! curl -sf "${SERVICE_URL}" >/dev/null; do if (( elapsed >= MAX_WAIT )); then echo "ERROR: service did not become healthy within ${MAX_WAIT}s" >&2 exit 1 fi echo " not ready yet — retrying in ${INTERVAL}s (${elapsed}s elapsed)" sleep "${INTERVAL}" (( elapsed += INTERVAL )) done echo "Service is healthy after ${elapsed}s."

تفصيلان مهمان هنا: أولًا، ! تعكس حالة خروج curl -sf، فتستمر الحلقة طالما الخدمة غير مستجيبة. ثانيًا، حارس المهلة مع exit 1 الصريح يمنع الحلقة من الدوران إلى ما لا نهاية إذا كان ثمة عطل حقيقي — وهو شبكة أمان أساسية لأنابيب CI وأتمتة المناوبات.

نمط احترافي — التراجع الأسي: في حلقات إعادة المحاولة لطلبات الشبكة، استخدم تراجعًا أسيًا بدلًا من فترة ثابتة: sleep $(( INTERVAL * 2 ** attempt )). يمنع هذا إغراق خدمة متعثرة بالطلبات. مكتبات مثل retry (دالة shell صغيرة تلصقها في سكريبتك) تُنفّذ هذا النمط بشكل نظيف.

قراءة الملفات سطرًا بسطر

من أشيع مهام DevOps معالجة قائمة مخزّنة في ملف: قائمة خوادم، قائمة كائنات S3، ملف CSV لأسماء مستخدمين. النمط الآمن الموحّد هو while IFS= read -r line. لا تستخدم for line in $(cat file) — فهو يتعطل مع المسافات والمسافات البادئة/اللاحقة.

#!/usr/bin/env bash set -euo pipefail HOSTS_FILE="./hosts.txt" # القراءة الآمنة سطرًا بسطر # IFS= يمنع تجريد المسافات البادئة/اللاحقة # -r يمنع تفسير الشرطة المائلة العكسية while IFS= read -r host; do # تجاهل الأسطر الفارغة وأسطر التعليقات [[ -z "${host}" || "${host}" == \#* ]] && continue echo "Checking SSH on: ${host}" ssh -o ConnectTimeout=5 -o BatchMode=yes "${host}" "uptime" \ && echo " OK" \ || echo " FAILED — adding to alert queue" done < "${HOSTS_FILE}"

إعادة التوجيه < "${HOSTS_FILE}" تُغذّي الملف إلى stdin حلقة while. هذا أكفأ من توصيله عبر cat (الذي ينشئ عملية فرعية) ويُبقي الحلقة في الـ shell الحالي، فتبقى تعيينات المتغيرات داخل الحلقة مرئية بعد انتهائها — فرق دقيق لكنه مهم.

مشكلة نطاق العملية الفرعية: حين تكتب cat file | while read -r line; do VAR=something; done، يعمل جسم الحلقة في shell فرعي بسبب الأنبوب. أي متغير تضبطه بداخله يضيع حين تنتهي الحلقة. إعادة التوجيه while ... done < file تتجنب هذا لأنه لا يوجد أنبوب.

حلقات الـ Glob: معالجة الملفات بأمان

حين تحتاج إلى العمل على كل ملف يطابق نمطًا — كل ملفات .log في مجلد، كل إعدادات *.yaml — يعدّ توسيع glob في Bash أكثر أمانًا وسرعةً من تحليل ناتج ls. الـ shell يوسّع الـ glob قبل تشغيل الحلقة، فيكون كل اسم ملف كلمةً منفصلة ومُقتبسة بشكل صحيح حتى لو احتوى على مسافات.

#!/usr/bin/env bash set -euo pipefail LOG_DIR="/var/log/myapp" ARCHIVE_DIR="/mnt/cold-storage/logs" CUTOFF_DAYS=7 mkdir -p "${ARCHIVE_DIR}" # حلقة glob على جميع ملفات .log المعدَّلة منذ أكثر من CUTOFF_DAYS # 'shopt -s nullglob' يجعل الحلقة تتخطى إذا لم تُطابق أي ملفات (ضروري!) shopt -s nullglob for logfile in "${LOG_DIR}"/*.log; do if [[ $(find "${logfile}" -mtime "+${CUTOFF_DAYS}" 2>/dev/null) ]]; then echo "Archiving: ${logfile}" gzip --best --keep "${logfile}" mv "${logfile}.gz" "${ARCHIVE_DIR}/" rm -f "${logfile}" fi done shopt -u nullglob # استعادة السلوك الافتراضي

سطر shopt -s nullglob بالغ الأهمية. بدونه، إذا لم تطابق أي ملفات النمط، يمرر Bash النص الحرفي /var/log/myapp/*.log كتكرار أول (ووحيد) — مما يجعل سكريبتك يحاول أرشفة ملف غير موجود. مع تفعيل nullglob، تجعل مجموعة المطابقات الصفرية جسم الحلقة لا ينفذ أبدًا. اضبطه دائمًا قبل حلقة glob قد لا تطابق شيئًا.

Four Bash Loop Patterns and Their Typical Use Cases Bash Loop Patterns — When to Use Each for … in Known list of items Servers, envs, args Brace expansions Arrays while Unknown iteration count Health checks, polling Queue draining Retry loops while read -r File line by line Host lists, CSVs stdin piped input Manifest files glob loop Files by pattern *.log, *.yaml Directory contents Safe with spaces Common Pitfalls to Avoid for in $(cat file) splits on spaces while without timeout infinite loop risk pipe | while read subshell loses vars glob without nullglob literal string if no match
أربعة أنماط حلقات Bash — حالة الاستخدام المثالية لكل منها والفخ الذي يحمله إن استُخدم بشكل خاطئ.

التحكم في الحلقة: break وcontinue وحالات الخروج

كلمتان مدمجتان تُعدّلان تنفيذ الحلقة. break تخرج من الحلقة الداخلية فورًا؛ continue تتخطى بقية التكرار الحالي وتبدأ التالي. كلتاهما تقبلان وسيطة عدد صحيح اختيارية للاستهداف في الحلقات المتداخلة.

#!/usr/bin/env bash set -euo pipefail # معالجة طابور من المهام؛ التوقف إذا فشلت مهمة حرجة declare -a JOBS=("migrate-db" "seed-cache" "warm-cdn" "deploy-app") for job in "${JOBS[@]}"; do echo "Running job: ${job}" if [[ "${job}" == "seed-cache" ]]; then echo " Skipping non-critical seed in prod" continue # تخطي هذا التكرار فقط fi # محاكاة تشغيل المهمة (استبدل بأمر حقيقي) ./run-job.sh "${job}" || { echo "CRITICAL: ${job} failed — halting pipeline" >&2 break # إيقاف كل المهام المتبقية } echo " ${job} completed successfully" done
حالات خروج الحلقة: حالة خروج الحلقة هي حالة خروج آخر أمر نُفّذ بداخلها. إذا احتجت إلى نشر فشل تم اصطياده داخل break، اضبط متغير علامة قبل الكسر — PIPELINE_FAILED=1 — ثم تحقق منه بعد الحلقة ونفّذ exit 1 إذا كان مضبوطًا. مع تفعيل set -e هذا التمييز مهم: الأمر الفاشل في حلقة مع || true لاحق لا يوقف السكريبت، مما يمنحك تحكمًا في الإخفاقات الحرجة.

مثال إنتاجي عملي: تدوير السجلات على خوادم متعددة

دمج كل ما في هذا الدرس: سكريبت يقرأ قائمة خوادم من ملف، يتكرر على كل خادم، يستخدم حلقة glob عن بُعد لأرشفة السجلات القديمة، ويُعيد المحاولة على الخوادم الفاشلة قبل إرسال التنبيه.

#!/usr/bin/env bash set -euo pipefail HOSTS_FILE="${1:-/etc/myapp/hosts.txt}" LOG_DIR="/var/log/myapp" FAILED_HOSTS=() shopt -s nullglob while IFS= read -r host; do [[ -z "${host}" || "${host}" == \#* ]] && continue echo "=== ${host} ===" if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "${host}" \ "find ${LOG_DIR} -name '*.log' -mtime +7 -exec gzip -f {} \;"; then echo " WARNING: log rotation failed on ${host}" >&2 FAILED_HOSTS+=("${host}") fi done < "${HOSTS_FILE}" shopt -u nullglob if (( ${#FAILED_HOSTS[@]} > 0 )); then echo "ALERT: rotation failed on: ${FAILED_HOSTS[*]}" >&2 # ./notify.sh "log-rotation" "${FAILED_HOSTS[*]}" exit 1 fi echo "Log rotation complete on all hosts."

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