مقدمة عملية إلى Bash Scripting: التحكم بالمتغيرات، منطق الشروط، وهندسة الحلقات التكرارية
تُعد صدفة باش Bash الواجهة البرمجية المباشرة للتعامل مع نواة أنظمة Linux وإدارة بنيتها التحتية؛ ورغم توفر لغات عالية المستوى مثل Python وGo، يبقى Bash Scripting الخيار الأسرع والأكثر فاعلية لأتمتة مهام DevOps وإدارة الخوادم. يهدف هذا الدليل إلى تفكيك أساسيات الصدفة والتركيز على المتغيرات، الشروط، والحلقات، مع ترسيخ ممارسات البرمجة الدفاعية التي تضمن استقرار السكربت في بيئات الإنتاج الفعلية.
تهيئة بيئة التنفيذ ومبدأ البرمجة الدفاعية
يبدأ كل سكربت باش بسطر التوجيه المعروف باسم Shebang. يحدد هذا السطر للنواتِ المفسرَ المطلوب لمعالجة الكود. الطريقة التقليدية هي كتابة #!/bin/bash، لكن الخيار الأكثر توافقية عبر مختلف توزيعات لينكس وأنظمة BSD هو استخدام مسار البيئة:
#!/usr/bin/env bash
هذا السطر يبحث عن المسار الحقيقي لأمر bash داخل متغير البيئة PATH الخاص بالنظام، مما يمنع تعطل السكربت إذا كان مفسر باش مثبتاً في مسار غير قياسي مثل /usr/local/bin/bash.
تكمن مشكلة الصدفة الكبرى في سلوكها المتساهل مع الأخطاء افتراضياً؛ فإذا واجه السكربت متغيراً مجهولاً أو فشل أمر ما، يستمر التنفيذ مسبباً كوارث محتملة في مساحات التخزين أو قواعد البيانات. لتجنب ذلك، يجب تفعيل ما يسمى بوضع الحماية الصارم (Strict Mode) في بداية كل ملف:
set -euo pipefail
-e: إيقاف تنفيذ السكربت فوراً عند فشل أي أمر (إرجاع Exit Code غير صفري).-u: اعتبار استدعاء أي متغير غير معرّف خطأً فادحاً يوقف التشغيل بدلاً من معاملته كنص فارغ.-o pipefail: التقاط الأخطاء داخل الأنابيب البرمجية (Pipes)؛ فبدونه، تُهمل الأخطاء التي تحدث في بداية الأنبوب إذا نجح الأمر الأخير.
المتغيرات، التوسيع، والعمليات الحسابية
تتعامل صدفة Bash مع كل المتغيرات كنصوص برمجية مجردة ما لم يُطلب منها خلاف ذلك، وتتطلب قواعد صارمة عند الإسناد والاستدعاء.
قواعد إسناد المتغيرات والاقتباس
القاعدة الذهبية في باش هي منع المسافات تماماً حول علامة المساواة (=). وجود مسافة يجعل المفسر يعامل اسم المتغير كأمر والطرف الآخر كمعاملات له. يُستدعى المتغير بإضافة علامة الدولار قبل اسمه، ويُفضل وضعه داخل أقواس معقوفة ${VAR} لتجنب تداخل الأسماء مع النصوص المجاورة.
username="deploy_user"
log_path="/var/log/${username}_activity.log"
هناك فرق جذري بين علامات الاقتباس:
- الاقتباس المزدوج
" ": يسمح بتوسيع المتغيرات (Variable Expansion) وحساب الأوامر مع حماية النص من التقسيم المفرط للمسافات (Word Splitting). - الاقتباس المفرد
' ': يعطل كافة أشكال المعالجة، ويعامل كل حرف داخله كنص مصمت دون استثناء.
المتغيرات التلقائية المدمجة
توفر الصدفة متغيرات خاصة يملؤها النظام تلقائياً أثناء تشغيل السكربت، وتعد العمود الفقري لمعالجة المدخلات من سطر الأوامر:
$0: اسم ملف السكربت الجاري تنفيذه.$1 ... $9: المعاملات (Arguments) الممررة للسكربت حسب ترتيبها.$#: عدد المعاملات الممررة فعلياً.$?: رمز خروج (Exit Status) آخر عملية نُفذت؛ حيث تعني القيمة0النجاح، وأي قيمة أخرى تدل على نوع محدد من الفشل.$@: مصفوفة تحتوي على كافة المعاملات مقتبسة فردياً، وهو الخيار الآمن للتكرار على المدخلات، على عكس$*الذي يدمج كل المدخلات في نص واحد.
العمليات الحسابية عبر $(( ))
بما أن باش لا تدعم الأعداد افتراضياً ككيانات منفصلة، تُجرى العمليات الحسابية داخل التعبير المزدوج $(( ... )) للعمليات الصحيحة (Integer Arithmetic):
count=10
increment=2
total=$(( count + increment ))
remainder=$(( total % 3 ))
صناعة القرار: الأقواس المزدوجة والجمل الشرطية
اتخاذ القرارات في باش يعتمد على ناتج فحص الأوامر. توفر بيئات يونكس أداة الفحص القديمة [ ]، إلا أن المعيار الحديث في باش يفرض استخدام الأقواس المزدوجة [[ ]] لأنها بنية داخلية في الصدفة تمنع أخطاء تفكيك الكلمات وتدعم التعابير النمطية (Regex).
أنواع المقارنات وفحوصات نظام الملفات
تختلف معاملات المقارنة في باش بناءً على نوع البيانات، ويعد الخلط بينها من أكبر مصادر الأخطاء البرمجية:
- مقارنة النصوص:
[[ "$str1" == "$str2" ]]و[[ "$str1" != "$str2" ]]، وفحص الفراغ عبر[[ -z "$str" ]](فارغ) أو[[ -n "$str" ]](غير فارغ). - مقارنة الأعداد الصحيحة: لا تستخدم رموز
>أو<مع الأرقام داخل الشروط الكلاسيكية، بل استخدم الرموز الوصفية:-eq(يساوي)،-ne(لا يساوي)،-lt(أصغر من)،-gt(أكبر من)،-le(أصغر أو يساوي)،-ge(أكبر أو يساوي). - فحص الملفات والمجلدات:
[[ -f "$path" ]](ملف عادي موجود)،[[ -d "$path" ]](مجلد موجود)،[[ -r "$path" ]](صلاحية قراءة)، و[[ -w "$path" ]](صلاحية كتابة).
التحكم متعدد الخيارات بواسطة case
عند الحاجة لفحص متغير واحد مقابل عدة أنماط، توفر بنية case ... esac كوداً أكثر وضوحاً من تكرار if ... elif:
action="start"
case "$action" in
start)
echo "Starting service..."
;;
stop|restart)
echo "Stopping or restarting..."
;;
*)
echo "Unknown action!"
exit 1
;;
esac
هندسة الحلقات التكرارية (Loops) ومعالجة البيانات
تتيح الحلقات التكرار على القوائم، أو معالجة تدفق النصوص، أو انتظار شروط معينة في النظام.
حلقة for بأنماطها المختلفة
يمكن استخدام حلقة for للتكرار على الملفات عبر تفريغ المسارات (Globbing)، أو محاكاة أسلوب لغة C التقليدي:
# Iterating over files safely using globbing
for log_file in /var/log/*.log; do
echo "Processing ${log_file}"
done
# C-style loop for indexed operations
for (( i=1; i<=5; i++ )); do
echo "Counter at: ${i}"
done
حلقة while وقراءة الملفات سطراً بسطر
أخطر خطأ يقع فيه المطورون هو محاولة قراءة أسطر ملف نصي بحلقة for، لأنها تقسم الكلمات عند كل مسافة فارغة بدلاً من الانتقال بين الأسطر. الطريقة القياسية والآمنة لقراءة ملف نصي هي الجمع بين while والأمر read مع تصفير محدد الحقول IFS وتفعيل القراءة الخام -r لمنع تفسير الرموز المائلة (Backslashes):
while IFS= read -r line; do
echo "Read line: ${line}"
done < "servers_list.txt"
حلقة until وأدوات التحكم break و continue
تعمل حلقة until بعكس while تماماً؛ فهي تستمر في التكرار طالما أن الشرط غير متحقق (يرجع قيمة خطأ)، وتتوقف فور نجاحه، مما يجعلها مثالية لانتظار إتاحة منفذ شبكي أو تشغيل خدمة. كما تُستخدم break لإنهاء الحلقة مبكراً، وcontinue لتخطي التكرار الحالي والانتقال للدورة التالية مباشرة.
مثال تطبيقي: سكربت تدوير وأرشفة سجلات الخادم
يطبق السكربت التالي كافة المفاهيم المشروحة: وضع الحماية الصارم، التحقق من المعاملات وصلاحيات المجلدات، العمليات الحسابية، وحلقات المعالجة الذكية للملفات مع إنشاء تقرير نهائي.
#!/usr/bin/env bash
set -euo pipefail
# Verify minimum argument count
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <source_directory> <archive_directory>"
exit 1
fi
SOURCE_DIR="$1"
ARCHIVE_DIR="$2"
PROCESSED_COUNT=0
TOTAL_BYTES=0
# Validate paths
if [[ ! -d "$SOURCE_DIR" ]]; then
echo "Error: Source directory '$SOURCE_DIR' does not exist." >&2
exit 2
fi
if [[ ! -d "$ARCHIVE_DIR" ]]; then
echo "Archive directory not found. Creating '$ARCHIVE_DIR'..."
mkdir -p "$ARCHIVE_DIR"
fi
echo "Starting log maintenance pipeline..."
# Iterate over matching log files safely
for file_path in "$SOURCE_DIR"/*.log; do
# Guard against non-expanded globs when no matching files exist
if [[ ! -f "$file_path" ]]; then
echo "No log files found to process."
break
fi
filename=$(basename "$file_path")
filesize=$(stat -c%s "$file_path")
timestamp=$(date +%Y%m%d_%H%M%S)
target_archive="${ARCHIVE_DIR}/${filename%.*}_${timestamp}.tar.gz"
echo "Compressing: $filename (${filesize} bytes)"
tar -czf "$target_archive" -C "$SOURCE_DIR" "$filename"
# Remove the original file upon successful compression
rm -f "$file_path"
# Track metrics using arithmetic expressions
PROCESSED_COUNT=$(( PROCESSED_COUNT + 1 ))
TOTAL_BYTES=$(( TOTAL_BYTES + filesize ))
done
echo "Maintenance completed."
echo "Total logs archived: ${PROCESSED_COUNT}"
echo "Total disk space freed: ${TOTAL_BYTES} bytes"
exit 0
تحليل خطوات السكربت
- الأسطر 1-2: تحديد البيئة بواسطة
/usr/bin/env bashوتفعيل نمط الحمايةset -euo pipefailلحظر أي تجاوز للأخطاء البرمجية. - الأسطر 5-9: التحقق من عدد المعاملات الممررة باستخدام
$#؛ فإذا كانت أقل من 2 يخرج السكربت برمز خطأ1. - الأسطر 15-23: التحقق من وجود المجلد المصدر والمجلد المستهدف عبر معامل
-d، وإنشاء المجلد المستهدف آلياً بواسطةmkdir -pإذا لم يكن موجوداً. - السطر 28: بدء حلقة
forمباشرة على نمط الملفات*.log، وهي الطريقة السليمة لتفادي انهيار البرنامج في حال احتوت أسماء الملفات على مسافات. - الأسطر 30-33: فحص وقائي بواسطة
-fللتأكد من أن النمط قد توسع فعلياً إلى ملف حقيقي وليس نصاً مجرداً (حالة عدم وجود ملفات متطابقة). - الأسطر 35-43: استخراج أسماء الملفات وأحجامها، وضغط الملف عبر أداة
tarثم حذفه فقط في حال اكتمال الأرشفة. - الأسطر 46-47: إجراء عمليات جمع حسابية باستخدام
$(( ... ))لجمع عدد الملفات المؤرشفة وإجمالي حجم البيانات.
المستجدات البرمجية وأدوات التدقيق الحديثة
لا تتوقف صدفة باش عن التطور، لكن المطورين يواجهون تحديات توافقية جوهرية يجب الانتباه لها:
- تحديث Bash 5.3: قدم الإصدار الجديد في عام 2025 دعماً لتحسينات جوهرية في استبدال الأوامر المباشر عبر الصيغة
${ command; }والتي تنفذ الأمر داخل نفس العملية بدون الحاجة لإنشاء عملية فرعية كاملة (Subshell Forking)، مما يرفع سرعة معالجة المهام الحلقية الضخمة مقارنة بالصيغة القديمة$(command). - معضلة macOS وBash 3.2: يأتي نظام macOS مزوداً بنسخة Bash 3.2 القديمة (منذ عام 2007) لأسباب قانونية تتعلق برخصة GPLv3؛ ولذلك لن تعمل ميزات مثل المصفوفات المترابطة (Associative Arrays) على النظام الافتراضي لنظام Mac ما لم تقم بتثبيت إصدار حديث عبر مدير الحزم Homebrew أو اعتماد التوافقية الرجعية.
- فحص الأكواد عبر ShellCheck: أصبحت أداة التحليل الساكن
ShellCheckخطوة إلزامية في خطوط أنابيب DevOps (CI/CD Pipelines)؛ فهي ترصد أخطاء الاقتباس والتوافقية وتحذر من استدعاءات المتغيرات غير الآمنة قبل نشر السكربت.
الأخطاء الشائعة عند كتابة سكربتات باش
هناك أنماط متكررة من الأخطاء يقع فيها المبرمجون تسبب سلوكيات غير متوقعة:
- إغفال علامات الاقتباس حول المتغيرات داخل الأوامر: كتابة
rm $fileأو[ $path == "/var" ]ستنهار برمجياً إذا احتوى المسار على مسافات، مسببة معالجة الاسم كمعاملين منفصلين. الحل هو وضع المتغيرات دوماً بين علامتي اقتباس مزدوجتين:rm "$file". - استخدام
forمع ناتج الأمرls: بناء الحلقات عبرfor f in $(ls *.txt)من أسوأ الممارسات البرمجية، لأنlsتُخرِج نصوصاً مفصولة بمسافات تتسبب في انكسار أسماء الملفات التي تحتوي فراغات. الحل الصحيح هو استخدام الـ Globbing المباشر:for f in *.txt. - الخلط بين المقارنات النصية والرقمية داخل الأقواس: استخدام رموز المقارنة المباشرة
>أو<داخل الأقواس الفردية[ $a > $b ]يُترجم من قِبل الصدفة كإعادة توجيه لملفات (Redirection) بدلاً من المقارنة الرقمية، مما يؤدي إلى إنشاء ملف جديد باسم القيمة وتخريب النتيجة الشرطية. الصحيح هو استخدام-gtو-ltللأرقام. - استخدام الأقواس الفردية
[ ]بدلاً من[[ ]]: الأول عبارة عن أمر نظام قديم يسمىtestيفتقر للمرونة ويتطلب حذراً بالغاً في عزل المتغيرات، بينما الثاني بنية لغوية توفر حماية ذاتية من التقسيم وتسمح بالمعاملات المنطقية الحديثة&&و||بدلاً من معاملات-aو-oالمعرضة للمشاكل.
متى تعتمد على Bash ومتى يجب تجنبها؟
تُعد صدفة باش الأداة المثالية عندما تقتصر مهمتك على أتمتة أوامر النظام، أو الربط السريع بين أدوات CLI المختلفة، أو كتابة مراحل التشغيل في ملفات Dockerfile وخطوط أتمتة CI/CD؛ فهي خفيفة، ولا تتطلب تثبيت مفسرات خارجية، وتتعامل مع مجاري النصوص والأنابيب البرمجية بسلاسة لا تضاهى. في المقابل، يجب تجنب باش فور أن يبدأ السكربت في التعامل مع هياكل بيانات معقدة (مثل JSON أو YAML)، أو عندما تزداد الحاجة إلى عمليات حسابية معقدة وفاصلة عائمة (Floating-point Arithmetic)، أو إذا تجاوز حجم الكود مئات الأسطر مع مكتبات معقدة؛ فهنا يصبح الانتقال إلى لغات متكاملة مثل Python أو Go خياراً حتمياً لتفادي كوابيس الصيانة وصعوبة تتبع الأخطاء.


