برمجة Bash النصية: كتابة سكربتات متينة وسهلة الصيانة
دليل شامل لبرمجة Bash النصية: المتغيرات، والاقتباس (quoting)، وset -euo pipefail، والدوال، وgetopts، وtrap، والمصفوفات، وshellcheck للحصول على سكربتات موثوقة.
InSkillCoach
برمجة Bash النصية: كتابة سكربتات متينة وسهلة الصيانة
يستطيع أي شخص كتابة سكربت Bash يعمل مرة واحدة، على جهازه، في يوم مثالي. أما كتابة سكربت يصمد أمام المسافات في أسماء الملفات، والأوامر التي تفشل بصمت، والمقاطعات في منتصف المعالجة، فهذه مسألة مختلفة تمامًا. في هذا الدليل نستعرض التقنيات التي تفصل السكربت المؤقت عن سكربت الإنتاج: الوضع الصارم، والاقتباس الدقيق، والدوال، وإدارة الوسائط، والتنظيف باستخدام trap، والتحقق باستخدام shellcheck. ونختم بسكربت نسخ احتياطي كامل مع شروحات.
1. Shebang والتنفيذ
السطر الأول مهم
يحدد shebang للنظام المفسّر الذي سيُستخدم. هناك صيغتان شائعتان:
#!/bin/bash
# مسار مباشر: بسيط ويعمل على معظم التوزيعات تقريبًا
#!/usr/bin/env bash
# يبحث عن bash في PATH: الأفضل للتوافقية (macOS وNix وغيرهما)
لا تستخدم #!/bin/sh إذا كان سكربتك يحتوي على بنى خاصة بـ Bash ([[ ]] أو المصفوفات أو local): ففي Debian وUbuntu يشير /bin/sh إلى dash الذي لا يفهمها.
جعل السكربت قابلًا للتنفيذ
# منح صلاحية التنفيذ
chmod +x mon_script.sh
# التنفيذ بالمسار النسبي
./mon_script.sh
# التنفيذ دون صلاحية تنفيذ (عبر المفسّر)
bash mon_script.sh
# التحقق من الصياغة دون تنفيذ
bash -n mon_script.sh
# التنفيذ في وضع التتبع للتنقيح (يعرض كل أمر)
bash -x mon_script.sh
2. الوضع الصارم: set -euo pipefail
هذا هو السطر الأول الذي ينبغي كتابته بعد shebang في أي سكربت جاد:
#!/usr/bin/env bash
set -euo pipefail
تفصيل كل خيار:
| الخيار | التأثير | بدونه |
|---|---|---|
set -e | يوقف السكربت عند أول أمر يفشل | يواصل السكربت عمله بعد الخطأ ويزيد الضرر |
set -u | خطأ عند استخدام متغير غير معرّف | خطأ مطبعي يُنتج سلسلة فارغة بصمت |
set -o pipefail | يفشل خط الأنابيب إذا فشل أي أمر فيه | commande_cassee | grep x ينجح إذا نجح grep |
مثال ملموس على الخطر عند غياب set -u:
# كارثة محتملة: إذا لم يكن REPERTOIRE معرّفًا،
# يصبح هذا السطر "rm -rf /" مع بعض القيم
rm -rf "${REPERTOIRE}/cache"
# مع set -u، يتوقف السكربت فورًا مع:
# ligne N : REPERTOIRE : variable sans liaison
عندما يُسمح لأمر ما بالفشل، صرّح بذلك بوضوح:
# السماح بفشل أمر محدد رغم set -e
grep "motif" fichier.log || true
# أو التقاط رمز الإرجاع
if ! systemctl is-active --quiet nginx; then
echo "nginx est arrêté"
fi
3. المتغيرات والاقتباس (Quoting)
تعريف المتغيرات واستخدامها
# الإسناد: لا مسافات حول علامة المساواة
nom="serveur-web-01"
# أحِط المتغيرات دائمًا بعلامات اقتباس مزدوجة
echo "Machine : ${nom}"
# استبدال الأوامر: فضّل $() على backticks
date_jour=$(date +%Y-%m-%d)
# قيمة افتراضية إذا كان المتغير فارغًا أو غير موجود
port="${PORT:-8080}"
# خطأ صريح إذا كان المتغير غير موجود
fichier="${1:?Usage : $0 <fichier>}"
# متغيرات للقراءة فقط للثوابت
readonly VERSION="2.1.0"
الاقتباس: مصدر الخطأ الأول
المتغير غير المقتبس يخضع لتقسيم الكلمات وتوسيع رموز الاستبدال (globs). وهذا هو سبب معظم أخطاء Bash.
fichier="rapport final.pdf"
# خطأ: يرى Bash وسيطين، "rapport" و"final.pdf"
cp $fichier /backup/
# صحيح: وسيط واحد مهما كان المحتوى
cp "$fichier" /backup/
# خطأ: إذا كان المجلد يحتوي على *.txt فسيتم توسيع الرمز
message="Fichiers *.txt trouvés"
echo $message
# صحيح
echo "$message"
قاعدة بسيطة: اقتبس متغيراتك دائمًا ("$var")، إلا إذا كنت تعرف بالضبط لماذا لا تفعل ذلك.
4. الاختبارات باستخدام [[ ]]
زوج الأقواس المزدوج [[ ]] خاص بـ Bash وأكثر أمانًا من [ ]: لا تقسيم للكلمات داخله، وعوامل منطقية مقروءة، ومطابقة أنماط مدمجة.
# اختبارات على السلاسل النصية
if [[ -z "$variable" ]]; then echo "vide"; fi
if [[ -n "$variable" ]]; then echo "non vide"; fi
if [[ "$reponse" == "oui" ]]; then echo "confirmé"; fi
# مطابقة نمط (glob)
if [[ "$fichier" == *.log ]]; then echo "fichier de log"; fi
# تعبير نمطي
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ressemble à une adresse IPv4"
fi
# اختبارات على الملفات
if [[ -f "/etc/nginx/nginx.conf" ]]; then echo "fichier existe"; fi
if [[ -d "/var/www" ]]; then echo "répertoire existe"; fi
if [[ -r "$fichier" && -w "$fichier" ]]; then echo "lecture et écriture"; fi
# مقارنات رقمية (أو استخدم (( )) للحساب)
if [[ "$nombre" -gt 100 ]]; then echo "supérieur à 100"; fi
if (( nombre > 100 )); then echo "idem, syntaxe arithmétique"; fi
5. الحلقات
# حلقة على قائمة صريحة
for service in nginx postgresql redis; do
systemctl status "$service" --no-pager
done
# حلقة على الملفات: يتعامل glob مع المسافات بشكل صحيح
for fichier in /var/log/*.log; do
echo "Traitement de $fichier"
done
# حلقة رقمية بأسلوب C
for (( i = 1; i <= 5; i++ )); do
echo "Tentative $i"
done
# حلقة while مع عدّاد
tentatives=0
while (( tentatives < 3 )); do
(( tentatives++ )) || true
echo "Essai numéro $tentatives"
done
# حلقة until: انتظار استجابة خدمة
until curl -sf http://localhost:8080/health > /dev/null; do
echo "En attente du service..."
sleep 2
done
لا تكرّر أبدًا على مخرجات ls: استخدم globs أو find مع -print0.
6. الدوال
# التعريف: المتغيرات المحلية إلزامية لتجنب الآثار الجانبية
log() {
local niveau="$1"
local message="$2"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${niveau}] ${message}" >&2
}
# دالة تُرجع قيمتها عبر stdout
espace_libre_mo() {
local point_montage="$1"
df --output=avail -m "$point_montage" | tail -1 | tr -d ' '
}
# الاستخدام
log "INFO" "Démarrage du traitement"
libre=$(espace_libre_mo /backup)
if (( libre < 1024 )); then
log "ERREUR" "Moins de 1 Go disponible sur /backup"
exit 1
fi
قاعدتان ذهبيتان: عرّف جميع متغيرات الدالة باستخدام local، واكتب رسائل التشخيص على stderr (>&2) حتى لا تلوّث القيم المُرجعة عبر stdout.
7. الوسائط وgetopts
المتغيرات الخاصة
echo "Nom du script : $0"
echo "Premier argument : $1"
echo "Nombre d'arguments : $#"
echo "Tous les arguments : $*"
# "$@" يحافظ على الوسائط الفردية (فضّل هذه الصيغة دائمًا)
for arg in "$@"; do
echo "Argument : $arg"
done
تحليل الخيارات باستخدام getopts
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage : $0 [-v] [-o fichier_sortie] -s source" >&2
exit 1
}
verbeux=false
sortie="/tmp/resultat.txt"
source_dir=""
while getopts ":vo:s:" option; do
case "$option" in
v) verbeux=true ;;
o) sortie="$OPTARG" ;;
s) source_dir="$OPTARG" ;;
:) echo "L'option -$OPTARG requiert un argument" >&2; usage ;;
\?) echo "Option inconnue : -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
# التحقق من الوسائط الإلزامية
[[ -n "$source_dir" ]] || usage
"$verbeux" && echo "Source : $source_dir, sortie : $sortie"
8. trap والتنظيف
قد يُقاطَع السكربت في أي لحظة (Ctrl+C أو kill أو خطأ مع set -e). يضمن trap أن التنظيف يتم مهما حدث.
#!/usr/bin/env bash
set -euo pipefail
# إنشاء مجلد عمل مؤقت
travail=$(mktemp -d)
# تُنفَّذ دالة التنظيف عند خروج السكربت،
# سواء كان الخروج عاديًا أو بسبب خطأ أو مقاطعة
nettoyage() {
local code=$?
rm -rf "$travail"
echo "Nettoyage effectué (code de sortie : $code)" >&2
}
trap nettoyage EXIT
# عرض السطر المسبب للخطأ عند حدوثه: مفيد جدًا للتنقيح
trap 'echo "Erreur à la ligne $LINENO" >&2' ERR
# يمكن لبقية السكربت استخدام $travail باطمئنان
cp /etc/hosts "$travail/"
يغطي trap على EXIT تقريبًا جميع الحالات: الخروج العادي، وexit الصريح، والخطأ مع set -e، والإشارتين SIGINT/SIGTERM (ينفّذ Bash الـ trap الخاص بـ EXIT بعد استلام الإشارة).
9. المصفوفات
# مصفوفة مفهرسة
serveurs=("web-01" "web-02" "db-01")
# إضافة عنصر
serveurs+=("cache-01")
# عدد العناصر
echo "Nombre de serveurs : ${#serveurs[@]}"
# التكرار: استخدم دائمًا "${tableau[@]}" بعلامات اقتباس
for serveur in "${serveurs[@]}"; do
echo "Vérification de $serveur"
done
# مصفوفة ترابطية (Bash 4 وما فوق)
declare -A ports
ports[http]=80
ports[https]=443
ports[ssh]=22
for service in "${!ports[@]}"; do
echo "$service écoute sur le port ${ports[$service]}"
done
10. قراءة الملفات
الطريقة القياسية لقراءة ملف سطرًا بسطر:
# IFS= يحافظ على المسافات في بداية السطر ونهايته
# -r يمنع تفسير الشرطات المائلة العكسية
while IFS= read -r ligne; do
echo "Lu : $ligne"
done < /etc/hostname
# قراءة ملف CSV مع تقسيم الحقول
while IFS=, read -r nom ip role; do
echo "Serveur $nom ($ip) : rôle $role"
done < serveurs.csv
# تحميل ملف كامل في مصفوفة
mapfile -t lignes < /etc/hosts
echo "Le fichier contient ${#lignes[@]} lignes"
تجنّب الفخ الكلاسيكي cat fichier | while read: ينشئ الـ pipe صدفة فرعية (subshell) وتضيع المتغيرات المعدَّلة داخل الحلقة عند الخروج منها.
11. shellcheck: مراجعك الآلي
يحلّل shellcheck سكربتاتك ويكشف الأخطاء الشائعة: المتغيرات غير المقتبسة، والمقارنات المشبوهة، والبنى غير المتوافقة بين الأنظمة.
# التثبيت
sudo apt install shellcheck # Debian/Ubuntu
sudo dnf install ShellCheck # Fedora/Rocky
# تحليل سكربت
shellcheck mon_script.sh
# تجاهل قاعدة محددة في سطر واحد (مع تبرير في تعليق)
# shellcheck disable=SC2086 # تقسيم متعمد للخيارات
commande $options
أدمج shellcheck في CI الخاص بك: السكربت الذي لا يجتاز shellcheck لا ينبغي دمجه.
12. مثال كامل: سكربت نسخ احتياطي
يجمع هذا السكربت كل التقنيات التي استعرضناها سابقًا.
#!/usr/bin/env bash
#
# backup.sh — نسخ احتياطي مضغوط لمجلد مع تدوير النسخ.
# الاستخدام : backup.sh [-k nombre_de_copies] -s /repertoire/source -d /repertoire/destination
#
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
# --- الدوال ----------------------------------------------------------------
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
usage() {
echo "Usage : $SCRIPT_NAME [-k copies] -s source -d destination" >&2
exit 1
}
nettoyage() {
# حذف الأرشيف الجزئي إذا قوطع السكربت
if [[ -n "${archive_temp:-}" && -f "$archive_temp" ]]; then
rm -f "$archive_temp"
log "Archive partielle supprimée : $archive_temp"
fi
}
trap nettoyage EXIT
# --- تحليل الوسائط ---------------------------------------------------------
copies=7
source_dir=""
dest_dir=""
while getopts ":k:s:d:" option; do
case "$option" in
k) copies="$OPTARG" ;;
s) source_dir="$OPTARG" ;;
d) dest_dir="$OPTARG" ;;
*) usage ;;
esac
done
[[ -n "$source_dir" && -n "$dest_dir" ]] || usage
[[ -d "$source_dir" ]] || { log "ERREUR : source introuvable : $source_dir"; exit 1; }
mkdir -p "$dest_dir"
# --- النسخ الاحتياطي -------------------------------------------------------
horodatage=$(date +%Y%m%d-%H%M%S)
archive="${dest_dir}/backup-${horodatage}.tar.gz"
archive_temp="${archive}.part"
log "Sauvegarde de $source_dir vers $archive"
# الكتابة في ملف مؤقت ثم إعادة التسمية: لا يوجد الأرشيف النهائي
# إلا إذا كان مكتملًا
tar -czf "$archive_temp" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
mv "$archive_temp" "$archive"
archive_temp=""
log "Archive créée : $(du -h "$archive" | cut -f1)"
# --- التدوير: الاحتفاظ بأحدث N أرشيفًا -------------------------------------
mapfile -t anciennes < <(ls -1t "${dest_dir}"/backup-*.tar.gz 2>/dev/null | tail -n +"$((copies + 1))")
for ancienne in "${anciennes[@]}"; do
rm -f "$ancienne"
log "Ancienne archive supprimée : $ancienne"
done
log "Terminé : $(ls -1 "${dest_dir}"/backup-*.tar.gz | wc -l) archive(s) conservée(s)"
أفضل الممارسات
- ابدأ كل سكربت بـ
set -euo pipefailوبرّر كل استثناء باستخدام|| true. - اقتبس متغيراتك بشكل منهجي:
"$var"و"${tableau[@]}"و"$@". - عرّف متغيرات الدوال باستخدام
localوالثوابت باستخدامreadonly. - اكتب السجلات (logs) على stderr لإبقاء stdout متاحًا للبيانات.
- استخدم
mktempللملفات المؤقتة وtrap EXITلتنظيفها. - اكتب في ملف مؤقت ثم أعد التسمية باستخدام
mv: العملية الذرية (atomic) تتفادى الملفات المكتوبة جزئيًا. - مرّر كل سكربت عبر shellcheck قبل نشره.
- إذا تجاوز السكربت بضع مئات من الأسطر أو تعامل مع بنى بيانات معقدة، ففكّر في Python: يتفوق Bash في تنسيق الأوامر (orchestration)، لا في منطق الأعمال.
الخلاصة
لا تأتي متانة سكربت Bash من طوله بل من انضباطه: الوضع الصارم الذي يحوّل الأخطاء الصامتة إلى توقفات صريحة، والاقتباس الذي يُبطل مفعول أسماء الملفات الفخّية، وtrap الذي يضمن التنظيف، وshellcheck الذي يراجع كل سطر نيابةً عنك. تكلّفك هذه العادات بضع ثوانٍ أثناء الكتابة وتوفّر عليك ساعات من التنقيح في بيئة الإنتاج.
اتخذ سكربت النسخ الاحتياطي في القسم 12 هيكلًا لأدواتك الخاصة: فبنية التسجيل (logging) والوسائط وtrap والعمليات الذرية تناسب تقريبًا جميع سكربتات الإدارة.
نبذة عن InSkillCoach
خبير في التدريب والتقنيات
مدرّب متخصص في التقنيات المتقدمة والذكاء الاصطناعي، تدعمه شركة GNeurone Inc.
الشهادات:
- AWS Certified Solutions Architect – Professional
- شهادات Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش