AzLearn

التنظيف باستخدام trap

Trap Cleanup

مفهوم ~18 دقيقة

التنظيف باستخدام trap — Trap Cleanup

الأمر trap يسمح لك بتشغيل أمر عند حدوث إشارة أو عند خروج السكربت. الاستخدام الأشهر هو تنظيف ملف مؤقت. إذا أنشأت ملفاً في /tmp ثم فشل السكربت قبل النهاية، قد يبقى الملف. مع trap 'rm -f "$tmp"' EXIT تضمن حذف الملف سواء نجح السكربت أو فشل أو قُطع.

لا تستخدم trap كتعويض عن تصميم سيئ. الهدف أن تنظف موارد مؤقتة أو تطبع رسالة واضحة، لا أن تخفي الأخطاء. دالة التنظيف يجب أن تكون آمنة: تقتبس المسار، ولا تحذف شيئاً واسعاً أو غير متوقع.

الأنموذج الكامل — mktemp + trap EXIT

عند التعامل مع ملفات مؤقتة، استخدم mktemp بدلاً من اسم ثابت. الاسم الثابت قد يتعارض مع تشغيل موازٍ أو يفتح ثغرة أمنية. الأنموذج الصحيح:

script.sh

mktemp -t azlearn.XXXXXX ينشئ ملفاً في /tmp باسم عشوائي كـ/tmp/azlearn.a3kT9x. الـtrap المسجّل على EXIT يحذفه فور خروج السكربت بأي سبب: نجاح، خطأ، أو Ctrl+C.

ERR trap — التقاط فشل الأوامر

إلى جانب EXIT، توجد إشارة ERR تُطلق في كل مرة يفشل أمر (غير صفر exit code). هذا مفيد لطباعة معلومات تشخيصية عند الفشل:

script.sh

المتغير $LINENO يعطيك رقم السطر الذي فشل فيه الأمر، مما يُسرّع التشخيص.

تحذير مهم: set -e داخل الشروط

يوجد سلوك يفاجئ كثيرين: set -e وERR trap لا يعملان داخل تعابير if، أو while، أو سلاسل || و&&. Bash تعطّل -e تلقائياً عندما تقيّم شرطاً، لأنه يتوقع أن تفشل الأوامر فيه بصورة طبيعية.

مثال:

set -euo pipefail
trap 'echo "ERR at $LINENO" >&2' ERR

if false; then   # false هنا لا يُطلق ERR trap ولا يوقف السكربت
  echo "لن يحدث"
fi

false || echo "هذا ينجح"   # false هنا أيضاً لا يُطلق ERR trap

false   # هذا فقط يُطلق ERR trap ويوقف السكربت

إذا أردت تسجيل كل فشل بصرف النظر عن السياق، استخدم RETURN trap داخل الدوال أو اجعل كل أمر خطير يستدعي الـerror handler صراحة.

إعادة ضبط الـtrap — Resetting trap

أحياناً تريد أن يعمل الـtrap فقط عند الفشل، وأن تُزيله عند النجاح. مثال شائع: نسخة ذرّية آمنة — تبني الناتج في ملف مؤقت، وتحذفه إذا فشلت، لكن تتركه إذا نجحت ونقلته:

# مثال: نسخ آمن مع نقطة نجاح — Successful checkpoint clears the cleanup trap
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
build_artifact > "$tmp"
mv "$tmp" final-artifact     # نقل ذرّي — atomic rename
trap - EXIT                  # نجح؛ أزل الحارس — succeeded; remove cleanup

trap - EXIT يُزيل الـhandler تماماً، فلا يُشغَّل عند الخروج. لاحظ الفرق بين ثلاثة أشكال:

  • trap 'rm -f "$tmp"' EXIT — سجّل أمر التنظيف.
  • trap - EXIT — احذف الـhandler (يتصرف السكربت كأن trap لم يُسجَّل أبداً).
  • trap '' EXIT — تجاهل الإشارة كلياً (الإشارة تصل لكنها لا تُنفَّذ). هذا مختلف تماماً عن -: الـ- يعيد التصرف الافتراضي، أما '' يمنع أي تصرف.

في معظم حالات النسخ الآمن، استخدم trap - عند نقطة النجاح — وليس trap ''.

إشارات أخرى — INT و TERM

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

cleanup() {
    rm -f "$tmp"
}
trap 'cleanup' EXIT INT TERM
  • INT تُطلق عند Ctrl+C (المستخدم يقاطع).
  • TERM تُطلق عند kill <pid> (مدير العمليات أو systemd يوقف السكربت).

سجّل الإشارات الثلاث معاً في سطر واحد إذا كانت دالة التنظيف آمنة لكل سياق. لاحظ أن EXIT ستُطلق كذلك عند INT وTERM في معظم الحالات — لذا في سكربتات بسيطة، EXIT وحده كافٍ. أما إذا أردت رسالة مخصصة لكل إشارة، سجّل handlers منفصلة.

تحدي — Challenge