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

الأنابيب وإعادة التوجيه والتدفقات

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

الأنابيب وإعادة التوجيه والتدفقات

عند بدء تشغيل أي عملية في Unix، ترث فوراً ثلاثة واصفات ملفات مفتوحة: المدخل القياسي (stdin، fd 0)، والمخرج القياسي (stdout، fd 1)، ومخرج الخطأ القياسي (stderr، fd 2). إتقان طريقة توصيل هذه التدفقات ببعضها — وإعادة توجيهها إلى ملفات أو أجهزة أو عمليات أخرى — هو المهارة الأقوى في كتابة سكريبتات الشل. على مستوى الشركات الكبرى، تعالج خطوط الأنابيب تيرابايتات من بيانات السجلات كل ليلة؛ وقد أخفى غياب 2>&1 في مهمة cron رسائل خطأ بالغة الأهمية لسنوات دون أن يلاحظها أحد. هذا الدرس سيجعلك خبيراً في التعامل مع التدفقات.

التدفقات القياسية الثلاثة

عندما تكتب عملية ما نتيجةً، تذهب إلى stdout. وعندما تكتب تحذيراً أو رسالة تشخيصية، تذهب إلى stderr. وعندما تحتاج إلى قراءة بيانات، تقرأ من stdin. تتيح لك الشل توصيل أي من هذه التدفقات بملف أو جهاز أو أمر آخر أو /dev/null.

Standard streams: stdin, stdout, stderr flowing through a process stdin (fd 0) keyboard / file / pipe Process (grep / awk / curl…) stdout (fd 1) terminal / file / pipe stderr (fd 2) terminal / log file reads writes results writes errors
ترث كل عملية ثلاثة واصفات ملفات مفتوحة عند بدء التشغيل: stdin وstdout وstderr.

إعادة توجيه المخرجات

يعيد المشغّل > توجيه stdout إلى ملف مع حذف محتواه السابق. أما المشغّل >> فيُلحق البيانات بالملف دون حذف. هذان المشغّلان هما أساس كل سكريبت يكتب سجلات.

# الكتابة فوق الملف (حذف محتوى سابق) في كل مرة echo "Deployment started at $(date)" > /var/log/deploy.log # الإلحاق — آمن لتراكم السجلات عبر عدة تشغيلات echo "Step 1 complete" >> /var/log/deploy.log # إعادة توجيه stderr فقط (fd 2) إلى ملف منفصل — stdout لا يزال يذهب إلى الطرفية make build 2> /var/log/build-errors.log # إعادة توجيه stdout وstderr معاً إلى نفس الملف (الأكثر شيوعاً في مهام cron) ./backup.sh > /var/log/backup.log 2>&1 # اختصار bash الحديث (bash 4+) — مطابق تماماً، مفضّل في السكريبتات الجديدة ./backup.sh >& /var/log/backup.log
الترتيب مهم مع 2>&1. اكتبه بعد إعادة توجيه stdout: cmd > file 2>&1. إن كتبت cmd 2>&1 > file، سيُنسخ stderr إلى stdout الأصلي (الطرفية) قبل إعادة توجيه stdout إلى الملف — فتظهر الأخطاء على الشاشة رغم ذلك. هذا خطأ كلاسيكي شائع في سكريبتات cron.

لتجاهل المخرجات كلياً، استخدم الجهاز الفارغ:

# كتم stdout فقط (تجاهل رسائل التقدم الصاخبة) ./noisy-tool.sh > /dev/null # كتم جميع المخرجات — مفيد حين يهمك فقط كود الخروج ./health-check.sh >& /dev/null && echo "healthy" || echo "FAIL"

إعادة توجيه المدخلات

يُغذّي المشغّل < ملفاً في stdin الخاص بأمر ما. أما المستند المضمّن (<<EOF) فيدمج نصاً متعدد الأسطر مباشرةً في السكريبت دون ملف مؤقت. والسلسلة المضمّنة (<<<) تمرّر سلسلة نصية واحدة كـ stdin.

# تغذية ملف SQL مباشرةً إلى عميل mysql mysql -u root -p mydb < schema.sql # المستند المضمّن: إرسال نص متعدد الأسطر إلى stdin # يجب أن يكون المحدد (EOF) وحيداً في السطر الأخير بدون مسافات بادئة sendmail ops@example.com <<EOF Subject: Deploy complete Build #42 deployed to production at $(date). EOF # السلسلة المضمّنة: stdin من سطر واحد — تتجنب نمط echo | cmd المضادة grep "ERROR" <<< "$(cat /var/log/app.log)" # أبسط: استخدم grep مباشرةً، لكن السلسلة المضمّنة مفيدة لمحتوى المتغيرات base64 --decode <<< "SGVsbG8gV29ybGQ="

الأنابيب: توصيل الأوامر

يُوصل الأنبوب (|) stdout أمر ما مباشرةً بـ stdin الأمر التالي — في الذاكرة، دون ملف مؤقت. يُنشئ النواة أنبوباً مجهولاً في الذاكرة؛ وكلتا العمليتان تعملان في آنٍ واحد. هذا ليس تنفيذاً متتابعاً: producer | consumer تعني أن المستهلك يبدأ فوراً ويعالج البيانات فور وصولها.

# خط أنابيب كلاسيكي: إيجاد أكثر عشرة عناوين IP تكراراً في سجل nginx cat /var/log/nginx/access.log \ | awk '{print $1}' \ | sort \ | uniq -c \ | sort -rn \ | head -10 # عدّ أسطر ERROR في مخرجات journald للساعة الأخيرة journalctl --since "1 hour ago" --no-pager \ | grep -c "ERROR" # المراقبة الفورية: متابعة سجل وتصفية الأحداث الحرجة tail -F /var/log/app/production.log \ | grep --line-buffered "CRITICAL\|FATAL" \ | while read -r line; do echo "$line" # يمكن إرسال تنبيه Slack هنا أيضاً done
كود خروج خط الأنابيب: بشكل افتراضي، كود خروج خط الأنابيب هو كود خروج الأمر الأخير فقط. إن فشل awk في المنتصف بينما نجح grep في النهاية، لن يُلاحظ سكريبتك ذلك. فعّل set -o pipefail (مُغطى في الدرس 8) لكي يفشل خط الأنابيب إن فشل أي مرحلة — هذا إلزامي في السكريبتات الإنتاجية.

tee: تقسيم التدفق

تقرأ الأداة tee من stdin وتكتب في آنٍ واحد إلى كل من stdout وملف واحد أو أكثر. سُمّيت بهذا الاسم تيمّناً بوصلة التوزيع T في السباكة. استخدمها حين تحتاج إلى تسجيل المخرجات مع الاستمرار في تمريرها لأسفل في خط الأنابيب.

# تسجيل مخرجات البناء مع عرضها في الطرفية في آنٍ واحد make build 2>&1 | tee /var/log/build.log # وضع الإلحاق — tee -a يحتفظ بإدخالات السجل السابقة ./run-tests.sh 2>&1 | tee -a /var/log/test-runs.log # نمط إنتاجي حقيقي: تشغيل سكريبت، تسجيل كل شيء، وتحليل السجل ./deploy.sh 2>&1 \ | tee /var/log/deploy-$(date +%Y%m%d-%H%M%S).log \ | grep -E "ERROR|WARN" \ | mail -s "Deploy alerts" ops@example.com

استبدال العمليات

يُتيح استبدال العمليات معاملة مخرجات أمر ما كما لو كانت ملفاً. يُنشئ الصياغة <(cmd) أنبوباً مسمّىً (أو /dev/fd/N) يمكن لأمر آخر فتحه وقراءته. هذا ضروري حين يتطلب أمر ما وسيطة اسم ملف ولا يقرأ من stdin.

Process substitution: two command outputs compared by diff via named pipes cmd A sort prod-servers.txt cmd B sort staging-servers.txt Named Pipe A /dev/fd/63 Named Pipe B /dev/fd/62 diff reads both as files stdout delta lines
يُوصّل استبدال العمليات مخرجات أمرين بـdiff عبر واصفات ملفات افتراضية — دون ملفات مؤقتة.
# مقارنة قائمتين مرتّبتين دون إنشاء ملفات مؤقتة diff <(sort prod-servers.txt) <(sort staging-servers.txt) # مقارنة قائمة الحزم الحالية مع خط الأساس المعروف diff <(dpkg -l | awk '{print $2}' | sort) <(sort /etc/expected-packages.txt) # صيغة إعادة توجيه المخرجات: >(cmd) — الكتابة في أمر كما لو كان ملفاً # التسجيل في وجهتين في آنٍ واحد (بديل لـ tee) ./run-migration.sh > >(tee /var/log/migration.log) 2> >(tee /var/log/migration-errors.log >&2) # ضم مخرجات أمرين جنباً إلى جنب باستخدام paste paste <(cut -d: -f1 /etc/passwd | sort) <(cut -d: -f3 /etc/passwd | sort -n)
استبدال العمليات مقابل الأنابيب: استخدم الأنبوب حين يقرأ المستهلك من stdin. استخدم استبدال العمليات حين يتوقع المستهلك وسيطة اسم ملف — مثل diff وcomm وjoin وأي أداة تستدعي open(2) على وسيطاتها. الجمع بين الأسلوبين يُغطي فعلياً كل احتياجات توصيل البيانات في العالم الحقيقي.

تجميع كل شيء: خط أنابيب تحليل سجلات حقيقي

هذا سكريبت يحاكي بيئة الإنتاج الفعلية ويُجسّد كل مفهوم من مفاهيم التدفقات في هذا الدرس. هو النوع الذي ستجده في كتيّب تعليمات فريق SRE:

#!/usr/bin/env bash # analyze-errors.sh — تقرير الأخطاء اليومي من سجلات nginx # الاستخدام: ./analyze-errors.sh [logfile] LOG="${1:-/var/log/nginx/access.log}" REPORT_DIR="/var/reports/errors" DATE=$(date +%Y-%m-%d) REPORT="${REPORT_DIR}/${DATE}.txt" mkdir -p "$REPORT_DIR" { echo "=== Error Report: ${DATE} ===" echo "" echo "--- Top 10 IPs hitting 4xx/5xx ---" # awk يُصفّي الأسطر التي يبدأ فيها كود HTTP (الحقل 9) بـ 4 أو 5 awk '$9 ~ /^[45]/' "$LOG" \ | awk '{print $1}' \ | sort \ | uniq -c \ | sort -rn \ | head -10 echo "" echo "--- Status code distribution ---" awk '{print $9}' "$LOG" \ | grep -E '^[0-9]{3}$' \ | sort \ | uniq -c \ | sort -rn echo "" echo "--- New error paths not seen yesterday ---" diff \ <(awk '$9 ~ /^[45]/ {print $7}' "${REPORT_DIR}/$(date -d yesterday +%Y-%m-%d).txt" 2>/dev/null | sort -u) \ <(awk '$9 ~ /^[45]/ {print $7}' "$LOG" | sort -u) \ | grep '^>' | awk '{print $2}' } 2>&1 | tee "$REPORT" echo "Report written to: $REPORT" >&2

لاحظ كيف يُغلّف { ... } 2>&1 | tee "$REPORT" كتلة بأكملها — ينتقل كل من stdout وstderr من الكتلة إلى tee الذي يكتب ملف التقرير بينما يطبع أيضاً على الطرفية. أما echo الأخير فيُرسل إلى stderr (>&2) كي لا يُضمَّن في التقرير نفسه.

ممارسة الشركات الكبرى: في Google وMeta، تُعيد مهام cron وسكريبتات CI توجيه stdout وstderr إلى ملفات سجل مُختومة بالتاريخ والوقت، ثم ترسل هذه السجلات إلى نظام مركزي (Splunk أو Loki أو Cloud Logging). انضباط خطوط الأنابيب الذي تبنيه الآن — الاحتفاظ بـ stdout للبيانات وstderr للتشخيصات والتقاط كليهما بشكل صحيح — يُترجم مباشرةً إلى كيفية توصيل خطوط المراقبة الإنتاجية.