كل مستخدم لينكس وصل إلى مرحلة معينة وجد نفسه يكتب الأوامر نفسها كل يوم: نسخ ملف، ضغط مجلد، فحص قرص، تشغيل نسخة احتياطية. في البداية يكررها يدويًا. ثم يكتبها في ملف نصي. ثم ينسى أمرًا أو يخطئ في ترتيب الوسائط، فيقرر أن يتعلم Bash scripting بشكل صحيح.

المشكلة أن معظم أدلة Bash إما تبدأ من الصفر المطلق فتُطيل في الأساسيات دون الوصول إلى شيء عملي، أو تفترض خبرة سابقة فتقفز مباشرة إلى أنماط إنتاجية معقدة. هذا الدليل يتبع مسارًا ثالثًا: يبني سكريبتك الأول خطوة بخطوة، ويشرح لماذا كل سطر يُكتب كما هو، ثم يعرض الأخطاء التي يقع فيها المبتدئون فعلًا، وكيف تتجنبها بأدوات بسيطة مثل ShellCheck و set -euo pipefail. الهدف ليس أن تحفظ الصياغة، بل أن تكتب سكريبتًا يمكنك الاعتماد عليه بعد شهر من الآن.

نظرة سريعة: خريطة مفاهيم Bash الأساسية

الجدول التالي يلخص المفاهيم التي سيغطيها الدليل، مع مثال سريع لكل منها:

المفهوم الوظيفة مثال
Shebang تحديد المفسّر #!/usr/bin/env bash
Strict Mode إيقاف التنفيذ عند أي خطأ set -euo pipefail
الاقتباسات منع تقسيم الكلمات "$var"
الشروط اتخاذ قرارات if [[ -f "$file" ]]
الحلقات تكرار العمليات for f in *.log
الدوال إعادة استخدام الكود my_func() { ... }
المصفوفات قوائم آمنة files=(a b c)
التحقق الساكن اكتشاف الأخطاء قبل التشغيل shellcheck script.sh

لماذا Bash؟ ومتى لا تكون الخيار الصحيح؟

لينكس باش

Bash موجودة على كل نظام لينكس تقريبًا، وعلى macOS افتراضيًا، وحتى على ويندوز عبر WSL. هذا يعني أن السكريبت الذي تكتبه اليوم سيعمل على خادمك، وعلى حاسوبك المحمول، وعلى خادم زميلك، دون تثبيت أي شيء إضافي. هذه الجاهزية الفورية هي الميزة الأولى.

الميزة الثانية هي أن Bash ليست لغة منفصلة عن بيئة العمل. كل أمر تستخدمه في الطرفية يمكن أن يصبح سطرًا في سكريبت. لا تحتاج إلى جسر بين ما تفعله يدويًا وما يفعله الكود. وهذا يجعلها الأداة الطبيعية لـأتمتة المهام اليومية: الملفات، والعمليات، والنسخ الاحتياطي، وجدولة المهام.

لكن Bash ليست لغة عامة الغرض، ولا تحاول أن تكون. إذا كنت تبني تطبيقًا منطقيًا معقدًا، أو تحتاج إلى هياكل بيانات متقدمة، أو اختبارات كثيفة، فـPython أو Go أنسب. القاعدة العملية: استخدم Bash للأتمتة والربط بين الأدوات، لا لبناء التطبيقات. المشكلة أن كثيرًا من المبتدئين يبدأون بسكريبت صغير، ثم ينتهي بهم الأمر إلى بناء نظام كامل بلغة لم تُصمم لذلك.

الخطوة 1: إنشاء أول سكريبت وتشغيله

أنشئ ملفًا باسم hello.sh، وضع فيه الأسطر التالية:

#!/usr/bin/env bash
set -euo pipefail

echo "Hello from Bash!"

السطر الأول هو Shebang. يخبر النظام أي مفسّر يستخدم لتشغيل الملف. استخدام /usr/bin/env bash أفضل من /bin/bash المباشر، لأنه يبحث عن Bash في PATH ويعمل على أنظمة تضع Bash في مواقع مختلفة.

السطر الثاني هو Strict Mode. سنشرحه بالتفصيل لاحقًا، لكنه باختصار يجعل السكريبت يتوقف عند أول خطأ بدلًا من المتابعة بصمت.

لتشغيل السكريبت، تحتاج إلى صلاحية التنفيذ:

chmod +x hello.sh
./hello.sh

إذا لم تضع ./ قبل الاسم، فلن يجد النظام الملف، لأن المجلد الحالي ليس ضمن PATH افتراضيًا. هذه أول نقطة يتعثر فيها المبتدئ.

المتغيرات والاقتباسات: مصدر معظم الأخطاء

المتغيرات في Bash لا تحتاج إلى نوع. تُعرَّف بالإسناد المباشر، دون مسافات حول علامة يساوي:

name="Ahmed"
age=30

# خطأ شائع: المسافات حول = تجعل Bash تحاول تنفيذ أمر باسم name
# name = "Ahmed"   # هذا خطأ

للوصول إلى قيمة المتغير، استخدم $ أو ${}. الشكل الثاني أفضل عندما تلتصق المتغيرات بنص آخر:

greeting="مرحبا"
echo "$greeting بالعالم"
echo "${greeting}بك"   # بدون  قد تبحث Bash عن متغير باسم greetingبك

الاقتباسات: الفرق الذي يحدد صحة السكريبت

هذا هو الموضوع الذي يفسر معظم أخطاء Bash الخفية. الجدول التالي يوضح الفروق:

النوع السلوك مثال
بدون اقتباس تقسيم الكلمات وتوسيع النطاقات $file
اقتباس مزدوج "..." يمنع التقسيم، يسمح بتوسيع المتغيرات "$file"
اقتباس مفرد '...' يمنع كل التوسيع، نص حرفي '$file'
Backticks `...` تنفيذ أمر (قديم، غير مفضل) `date`
Command Substitution $(...) تنفيذ أمر (حديث، مفضل) $(date)

القاعدة الذهبية التي تختصر كل شيء: اقتبس كل متغير ما لم يكن لديك سبب واضح لعدم اقتباسه. الملف الذي يحتوي على مسافة في اسمه سيكسر السكريبت إذا لم تقتبس. والنطاق * قد يتوسع إلى قائمة ملفات غير متوقعة. معظم أخطاء Bash الدقيقة مصدرها اقتباس ناقص.

المدخلات والمخرجات: printf والوسائط وقراءة المستخدم

printf بدلًا من echo

echo مريح، لكن سلوكه يختلف بين الأنظمة، خصوصًا مع الخيارات والسلاسل التي تبدأ بشرطة. للتنسيق المتوقع، استخدم printf:

printf "%-10s %s\n" "الاسم:" "$name"
printf "خطأ: لم يتم العثور على الملف\n" >&2

لاحظ >&2: يرسل الرسالة إلى stderr بدل stdout. هذا يسمح للمستخدم بفصل رسائل الخطأ عن المخرجات العادية عند التوجيه أو الأنابيب.

الوسائط (Arguments)

السكريبت يستقبل قيمًا من سطر الأوامر عبر $1 و$2 وهكذا. $0 هو اسم السكريبت نفسه، و $# عدد الوسائط، و$@ قائمة الوسائط جميعها.

#!/usr/bin/env bash
set -euo pipefail

if [[ $# -lt 1 ]]; then
    printf "الاستخدام: %s <اسم الملف>\n" "$0" >&2
    exit 1
fi

file="$1"
printf "الملف المطلوب: %s\n" "$file"

قراءة مدخلات المستخدم

read تقرأ سطرًا من الإدخال القياسي. استخدم دائمًا -r لمنع معالجة الشرطة العكسية، و -p لعرض رسالة:

read -r -p "أدخل اسم المجلد: " dir
printf "سأعمل على: %s\n" "$dir"

الشروط: if و[[ ]] وcase

الشروط في Bash تُبنى على حالة الخروج للأوامر: 0 يعني نجاح، وأي رقم آخر يعني فشلًا. هذا يعني أن أي أمر يمكن أن يكون شرطًا.

if [[ -f "$file" ]]; then
    printf "الملف موجود\n"
elif [[ -d "$file" ]]; then
    printf "هذا مجلد\n"
else
    printf "غير موجود\n" >&2
    exit 1
fi

[[ ]] هي الصيغة الحديثة، وتتفوق على [ ] لأنها تمنع تقسيم الكلمات وتوسيع النطاقات داخل الاختبار. بعض الاختبارات الشائعة:

  • -f "$path": ملف عادي موجود
  • -d "$path": مجلد موجود
  • -z "$var": سلسلة فارغة
  • -n "$var": سلسلة غير فارغة
  • -v var: متغير معرّف (حتى لو فارغًا)

للاختيار بين قيم متعددة، case أوضح من سلسلة طويلة من elif:

case "$1" in
    start)   printf "بدء التشغيل\n" ;;
    stop)    printf "إيقاف\n" ;;
    restart) printf "إعادة تشغيل\n" ;;
    *)       printf "أمر غير معروف\n" >&2; exit 1 ;;
esac

الحلقات: for وwhile وuntil

for هي الأكثر استخدامًا للمرور على الملفات أو القوائم:

for file in *.log; do
    printf "معالجة: %s\n" "$file"
done

لكن الحلقة الأكثر أمانًا للملفات التي قد تحتوي على مسافات أو أحرف خاصة هي استخدام find مع -print0 وread -d '':

while IFS= read -r -d '' file; do
    printf "معالجة: %s\n" "$file"
done < <(find . -type f -name '*.log' -print0)

while تُستخدم عندما تعتمد الحلقة على شرط يتغير. until عكسها: تستمر حتى يصبح الشرط صحيحًا.

count=0
while [[ $count -lt 3 ]]; do
    printf "العدد: %d\n" "$count"
    ((count++))
done

الدوال: تنظيم الكود وإعادة استخدامه

الدوال في Bash ليست مثل الدوال في اللغات الأخرى. لا تُرجع قيمًا عبر return (فهي تُرجع حالة خروج فقط). لإرجاع نص، استخدم echo مع command substitution. ولعزل المتغيرات، استخدم local:

log_info() {
    printf "[INFO] %s\n" "$*"
}

get_name() {
    echo "Ahmed"
}

name="$(get_name)"
log_info "الاسم: $name"

بدون local، كل متغير تعرّفه داخل الدالة يصبح متاحًا في النطاق العام، وقد يلوث متغيرات أخرى تحمل الاسم نفسه. هذا خطأ شائع عندما يكبر السكريبت.

معالجة الأخطاء: set -euo pipefail وtrap

هذا هو الجزء الذي يفصل السكريبت الهاوي عن السكريبت الإنتاجي. افتراضيًا، Bash تتجاهل الأخطاء وتكمل التنفيذ. إذا فشل أمر، تستمر إلى السطر التالي وكأن شيئًا لم يكن. هذا خطير في سكريبت نسخ احتياطي أو نشر.

Strict Mode يغير هذا السلوك:

  • set -e: إيقاف التنفيذ عند أول أمر يفشل.
  • set -u: اعتبار استخدام متغير غير معرّف خطأً. هذا يكتشف الأخطاء المطبعية في أسماء المتغيرات.
  • set -o pipefail: فشل الأنبوب إذا فشل أي أمر داخله، لا آخر أمر فقط. بدونها، false | true ينجح لأن آخر أمر نجح.
  • set -E: نقل معالج ERR إلى الدوال والعمليات الفرعية.
set -Eeuo pipefail

trap 'printf "فشل في السطر %d\n" "$LINENO" >&2' ERR

trap ... ERR يسمح لك بتسجيل سياق الخطأ (السطر، الأمر، أحيانًا دالة) بدلًا من خروج صامت. هذا يختصر وقت التصحيح كثيرًا.

تحفظ مهم: set -e له استثناءات. الأوامر داخل if أو while أو بعد || لا توقف السكريبت، لأن فشلها متوقع ضمن التحقق من الشرط. هذا مقصود، لكنه يفاجئ من لا يعرفه.

المصفوفات: التعامل الآمن مع القوائم

المصفوفات في Bash تحل مشكلة تقسيم الكلمات التي تسببها المتغيرات العادية. لتعريف مصفوفة:

files=("report 2026.txt" "backup.zip" "notes.md")

للوصول إلى عنصر واحد، استخدم الفهرس: "${files[0]}". للمرور على كل العناصر، استخدم "${files[@]}" مع الاقتباس. وبدون الاقتباس، ستنقسم العناصر التي تحتوي على مسافات:

for f in "${files[@]}"; do
    printf "العنصر: %s\n" "$f"
done

لمعرفة عدد العناصر: "${#files[@]}". لإضافة عنصر: files+=("new.txt"). المصفوفات الترابطية (associative arrays) تحتاج declare -A، وتُستخدم عندما تريد مفاتيح نصية بدل أرقام.

الأخطاء الشائعة التي توقع المبتدئين

هذه الأخطاء لا تأتي من قلة الذكاء، بل من افتراضات خاطئة عن كيفية عمل Bash. الجدول التالي يلخص أكثرها شيوعًا:

الخطأ لماذا يفشل الإصلاح
عدم اقتباس $file تقسيم الكلمات عند وجود مسافات "$file"
for f in $(ls) يفشل مع المسافات والأحرف الخاصة استخدم *.ext أو find -print0
كتابة name = "x" المسافات حول = تفسد الإسناد name="x"
عدم التحقق من الوسائط $1 فارغ يسبب خطأ في الاختبار تحقق من $# قبل الاستخدام
استخدام متغير داخل دالة بدون local تلوث النطاق العام أضف local لكل متغير داخلي
مقارنة نصية بـ-eq -eq للأرقام فقط استخدم == للنصوص
الاعتماد على echo لتنسيق حساس سلوكه يختلف بين الأنظمة استخدم printf

أدوات لا غنى عنها: ShellCheck وshfmt وBats

ShellCheck هو أهم أداة لمن يكتب Bash. إنه محلل ساكن يقرأ السكريبت ويشير إلى الأخطاء والممارسات السيئة قبل التشغيل، مع رابط شرح لكل تحذير. يمكنك تشغيله من الطرفية أو استخدامه عبر الإنترنت:

shellcheck hello.sh

shfmt ينسق السكريبت تلقائيًا، فيجعل المسافات البادئة موحدة ويقلل فروق المراجعة. Bats إطار اختبار لسكريبتات Bash، وليس ضروريًا للمبتدئ، لكنه يصبح مهمًا عندما يبدأ السكريبت بالقيام بمنطق يحتاج تحققًا.

أضف هذه الأدوات إلى سير عملك مبكرًا. تصحيح عادة سيئة أسهل من تصحيح سكريبت يعمل “بالخطأ” لشهور.

مثال عملي: سكريبت نسخ احتياطي كامل

يجمع المثال التالي معظم ما سبق: Strict Mode، والتحقق من الوسائط، والدوال، والاقتباسات، وtrap للتنظيف:

#!/usr/bin/env bash
set -Eeuo pipefail

usage() {
    printf "الاستخدام: %s <المجلد المصدر> <مجلد النسخ>\\n" "$0" >&2
}

cleanup() {
    if [[ -n "${tmp_file:-}" && -f "$tmp_file" ]]; then
        rm -f -- "$tmp_file"
    fi
}
trap cleanup EXIT

if [[ $# -ne 2 ]]; then
    usage
    exit 64
fi

src="$1"
dest="$2"

if [[ ! -d "$src" ]]; then
    printf "المصدر ليس مجلدًا: %s\\n" "$src" >&2
    exit 1
fi

mkdir -p -- "$dest"
tmp_file="$(mktemp)"
stamp="$(date +%Y%m%d-%H%M%S)"
archive="$dest/backup-$stamp.tar.gz"

tar -czf "$tmp_file" -C "$src" .
mv -- "$tmp_file" "$archive"

printf "تم إنشاء النسخة: %s\\n" "$archive"

لاحظ -- قبل المسارات في الأوامر. هذه العادة تمنع معاملة اسم يبدأ بشرطة كخيار. ولاحظ trap cleanup EXIT، الذي يضمن حذف الملف المؤقت حتى لو فشل السكريبت.

قائمة تحقق نهائية

  • هل يبدأ السكريبت بـ#!/usr/bin/env bash؟ ← أضفه دائمًا
  • هل يستخدم set -Eeuo pipefail؟ ← أضفه في أول عشرة أسطر
  • هل كل متغير مقتبس بـ"$var"؟ ← اقتبس ما لم يكن لديك سبب
  • هل تتحقق من $# قبل استخدام $1؟ ← أضف تحققًا واضحًا
  • هل تستخدم local داخل الدوال؟ ← أضفه لكل متغير محلي
  • هل تستخدم printf بدل echo في التنسيق؟ ← نعم إن كنت تحتاج تحكمًا
  • هل تمر على الملفات بطريقة آمنة؟ ← تجنب for f in $(ls)
  • هل شغّلت shellcheck؟ ← شغّله قبل كل التزام
  • هل لديك trap لتنظيف الموارد؟ ← خاصة مع الملفات المؤقتة

فيما يلي روابط رسمية وأدوات يمكنك الرجوع إليها لتعميق فهمك:

أسئلة شائعة

هل يجب أن أتعلم Bash قبل Python؟

إذا كنت تعمل على لينكس، فـBash أساسية لفهم الطرفية والملفات والعمليات. يمكنك تعلمها بالتوازي مع Python: Bash للمهام القصيرة والربط بين الأدوات، وPython للمنطق الأكثر تعقيدًا.

هل set -e كافٍ لمعالجة الأخطاء؟

لا. set -e وحده لا يوقف التنفيذ في كل السياقات (مثل الأوامر داخل if). الأفضل استخدام set -Eeuo pipefail مع trap، وفهم الاستثناءات.

ما الفرق بين [ ] و[[ ]]؟

[ ] متوافق مع POSIX لكنه أكثر عرضة لتقسيم الكلمات. [[ ]] خاص بـBash، وأكثر أمانًا (يمنع التقسيم والتوسيع)،ويدعم أنماطًا مثل && و|| داخل الأقواس.

لماذا لا تعمل المتغيرات المعدلة داخل while بعد الأنبوب؟

لأن كل جزء من الأنبوب يعمل في عملية فرعية (subshell)، والمتغيرات المعدلة في العملية الفرعية لا تعود إلى الأب. استخدم process substitution: while ...; done < <(command).

هل يمكنني استخدام Bash على ويندوز؟

نعم، عبر WSL (Windows Subsystem for Linux) أو Git Bash. لكن السكريبتات التي تعتمد على أدوات لينكس (مثل sed وgrep) قد تختلف في السلوك.