مبتدئ → تطبيقي · 10 دروس

Linux CLI — سطر الأوامر للمطورين

سطر أوامر Linux من الصفر على Ubuntu: التنقل، الملفات، الصلاحيات وumask، الأنابيب، grep وfind، العمليات وsystemd، apt وsudo، مفاتيح SSH، وسكربتات Bash آمنة — مع عادات تحميك من الحذف الخاطئ.

كيف تدرس هذا المسار؟

  1. اقرأ المفهوم — الطرفية بوابة السيرفر الحقيقي.
  2. انسخ الأوامر وجرّبها في Terminal أو WSL.
  3. نفّذ المهمة العملية قبل الاختبار.
  4. أكمل الاختبار — التقدّم يُحفظ على جهازك.

مسار مكتوب لمن يريد التعامل مع الملفات والسيرفرات من سطر الأوامر بثقة، ويفيده إنهاء مسار الويب أو Git أولاً.

1. مقدمة الطرفية — بوابة المطور

الهدف: تفهم الفرق بين الطرفية والصدفة، وتقرأ البرومبت وأجزاء الأمر، وتطلب المساعدة، وتثبّت أول برنامج بـ apt على Ubuntu.

الطرفية (Terminal) نافذة نصية تكتب فيها أمراً فينفّذه النظام ويطبع النتيجة. داخل هذه النافذة يعمل برنامج آخر اسمه الصدفة (Shell)، وهو الذي يقرأ السطر الذي كتبته ويفسّره ثم يشغّل البرنامج المطلوب. على Ubuntu تكون الصدفة الافتراضية للمستخدمين هي Bash، وتفتح الطرفية في سطح المكتب بالاختصار Ctrl+Alt+T. أما الخوادم فلا نافذة فيها أصلاً، بل جلسة نصية تصلها عبر SSH، وهو موضوع الدرس الثامن. وإن كنت على Windows فأسهل طريق للتجربة هو WSL، الذي يثبّت توزيعة Ubuntu حقيقية بالأمر wsl --install من PowerShell مفتوح بصلاحية المسؤول.

أول ما تراه هو البرومبت (Prompt)، أي السطر الذي ينتظر أمرك:

sara@laptop:~/projects$

اقرأه من اليسار: اسم المستخدم sara، ثم اسم الجهاز laptop، ثم المجلد الحالي، حيث الرمز ~ اختصار لمجلدك الرئيسي ‎/home/sara، ثم العلامة $ التي تعني أن الصدفة تنتظر أمراً من مستخدم عادي. إذا رأيت # مكان $ فأنت تعمل بحساب المدير root، وأي خطأ عندها قد يصيب النظام كله لا ملفاتك وحدك.

أجزاء الأمر. يتكوّن الأمر عادة من ثلاثة أجزاء تفصل بينها مسافات: اسم البرنامج، ثم الخيارات (Options) التي تبدأ بشرطة، ثم الوسائط (Arguments) أي الأهداف التي يعمل عليها. في ls -l --human-readable /var/log البرنامج هو ls، والخيار القصير -l يطلب عرضاً مفصلاً، والخيار الطويل --human-readable يكتب الأحجام بوحدات مقروءة، والوسيط /var/log هو المجلد المطلوب. الخيارات القصيرة يمكن دمجها، فـ ls -lh تساوي ls -l -h. والمسافة هنا جزء من المعنى: ls-l بلا مسافة اسم برنامج لا وجود له.

كيف تسأل الطرفية نفسها؟ معظم البرامج تقبل --help وتطبع ملخصاً لخياراتها. للتفاصيل الكاملة افتح صفحة الدليل بـ man ls، وتحرك فيها بالأسهم، وابحث بكتابة / ثم الكلمة، واخرج بالحرف q. وإن لم تعرف اسم الأمر أصلاً فجرّب man -k copy لتبحث في أوصاف الصفحات. بعض الأوامر ليست برامج على القرص، بل مدمجة في الصدفة نفسها، ويكشف ذلك الأمر type (مخرجات توضيحية):

$ type cd
cd is a shell builtin
$ type python3
python3 is /usr/bin/python3

مساعدة الأوامر المدمجة مثل cd تأتي من help cd لأن أغلبها بلا صفحة man مستقلة.

جلسة أولى على Ubuntu خطوة بخطوة. تريد رؤية شجرة مجلدات مشروعك فتكتب tree، فيرد النظام برسالة مثل هذه (مختصرة، وقد تقترح نسختك طريقة تثبيت إضافية):

Command 'tree' not found, but can be installed with:
sudo apt install tree

الرسالة تعني أن البرنامج غير مثبّت، وأن Ubuntu يعرف الحزمة (Package) التي توفّره. التثبيت يتم بمدير الحزم apt على خطوتين:

sudo apt update          # تحديث قوائم الحزم من المستودعات
sudo apt install tree    # تنزيل الحزمة وتثبيتها
tree -L 1                # عرض مستوى واحد فقط من الشجرة

الخطوة الأولى لا تثبّت شيئاً، فهي تنزّل أحدث قوائم الحزم المتاحة من المستودعات، ولذلك تسبق أي تثبيت على نظام لم يُحدَّث منذ مدة. الكلمة sudo تشغّل الأمر الذي يليها بصلاحيات المدير، لأن التثبيت يعدّل ملفات النظام. ستُسأل عن كلمة مرورك أنت لا كلمة مرور root، ولن يظهر أي حرف أو نجمة أثناء الكتابة؛ هذا سلوك مقصود يخفي طول كلمة المرور عمن ينظر إلى شاشتك، فاكتبها واضغط Enter.

أوامر apt التي ستحتاجها كثيراً:

apt search json          # بحث عن حزمة بكلمة في اسمها أو وصفها
apt show tree            # الوصف والحجم والاعتماديات
sudo apt upgrade         # ترقية كل الحزم المثبّتة
sudo apt remove tree     # إزالة الحزمة
apt list --installed     # قائمة بكل ما هو مثبّت

ملاحظة للسكربتات: الأمر apt مصمم للاستخدام التفاعلي، وتنبّه صفحة دليله إلى أن واجهته قد تتغير بين الإصدارات، لذلك تستعمل السكربتات عادة apt-get ذات السلوك الثابت.

قاعدتان للأمان من اليوم الأول: لا تضع sudo أمام أمر لمجرد أنه فشل، واقرأ رسالة الخطأ أولاً، فأغلب ما تفعله داخل مجلدك الرئيسي لا يحتاج صلاحيات المدير. ولا تلصق في الطرفية أمراً منسوخاً من منتدى لا تفهم كل جزء منه، خاصة الأوامر من نوع curl ... | sudo bash، لأنك تمنح بذلك كاتب السكربت حق تشغيل ما يشاء على جهازك. واختصارات تحتاجها كل يوم: Tab لإكمال الأسماء، والسهم للأعلى لاستدعاء الأوامر السابقة، وCtrl+C لإيقاف أمر عالق، وCtrl+R للبحث في سجل أوامرك.

  • الطرفية نافذة العرض، والصدفة (Bash على Ubuntu) هي التي تفسّر الأوامر وتشغّلها.
  • علامة $ في البرومبت تعني مستخدماً عادياً، و# تعني أنك root فانتبه لكل أمر.
  • الخيار --help والأمر man والأمر type ثلاث طرق لتفهم أمراً قبل تشغيله.
  • التثبيت على Ubuntu: sudo apt update ثم sudo apt install اسم_الحزمة.

مخطط

أنت
الطرفية: نافذة
الصدفة Bash: تفسّر
البرامج / النظام

مختبر: اسأل الطرفية

نفّذ type على cd وls وpython3: أيها مدمج في الصدفة وأيها برنامج على القرص؟ ثم افتح man ls وابحث عن الخيار -h بكتابة /-h واخرج بـ q.

مهمة عملية

ثبّت الحزمة tree بـ apt (أو تحقق من وجودها بـ apt list --installed tree)، ثم نفّذ tree -L 1 في مجلدك الرئيسي واكتب في ملاحظاتك عدد المجلدات التي ظهرت.

اختبر فهمك (1/3)

على Ubuntu، ما الفرق بين الطرفية (Terminal) والصدفة (Shell)؟

اختبر فهمك (2/3)

كتبت tree فظهرت رسالة Command 'tree' not found, but can be installed. ما الخطوة الصحيحة؟

اختبر فهمك (3/3)

تكتب كلمة المرور بعد sudo فلا يظهر أي حرف على الشاشة. ماذا يعني ذلك؟

للاستزادة: Ubuntu: The Linux command line for beginners · man7: صفحة دليل الأمر man · Ubuntu Manpage: apt · Ubuntu Server docs: إدارة الحزم

2. التنقل — pwd و cd و ls

الهدف: تعرف مكانك في شجرة الملفات، وتنتقل بالمسارات المطلقة والنسبية، وتقرأ مخرجات ls المفصلة.

لكل طرفية مفتوحة «مجلد عمل حالي» (Working Directory)، وهو النقطة التي تُفسَّر منها أي أسماء ملفات تكتبها دون مسار كامل. ثلاثة أوامر تكفي للتنقل: pwd يطبع المسار الكامل لمكانك الآن، وcd ينقلك إلى مجلد آخر، وls يعرض محتويات المجلد.

pwd              # أين أنا؟
ls               # ماذا يوجد هنا؟
cd Documents     # ادخل مجلداً داخل المجلد الحالي
cd ..            # اصعد إلى المجلد الأب
cd               # بلا وسيط: عودة إلى مجلدك الرئيسي
cd -             # ارجع إلى المجلد الذي كنت فيه قبل آخر cd
cd /var/log      # انتقل بمسار مطلق من الجذر

خريطة النظام قبل التنقل فيه. في Linux شجرة واحدة تبدأ من الجذر /، ولا توجد أقراص بأحرف مثل C: و D:. تنظيم المجلدات الرئيسية متقارب بين التوزيعات لأنه يتبع معيار هرمية نظام الملفات (FHS)، وتشرحه صفحة الدليل man hier. أهم ما ستزوره: /home وفيه مجلد لكل مستخدم، و/etc لملفات الإعداد النصية، و/var/log للسجلات، و/usr/bin للبرامج المثبّتة، و/tmp للملفات المؤقتة التي قد تُمسح عند إعادة التشغيل، و/root وهو المجلد الرئيسي لحساب المدير. معرفة هذه الخريطة توفر عليك البحث العشوائي: إن أردت إعداد برنامج فابدأ من /etc، وإن أردت معرفة سبب عطل فابدأ من /var/log.

مسار مطلق أم نسبي؟ المسار المطلق يبدأ بـ / ويصف الطريق من الجذر، مثل /home/sara/projects، ويعمل من أي مكان. المسار النسبي لا يبدأ بـ / ويُحسب من مجلدك الحالي، والنقطتان .. تعنيان المجلد الأب، والنقطة الواحدة . تعني المجلد الحالي نفسه. تتبّع هذا التمرين:

$ pwd
/home/sara/projects/shop
$ cd ../../Downloads
$ pwd
/home/sara/Downloads

النقطتان الأوليان رفعتاك إلى projects، والثانيتان إلى sara، ثم نزلت إلى Downloads. قاعدة عملية: في السكربتات والأوامر الخطرة استعمل المسار المطلق، وفي التنقل اليومي يكفي النسبي مع Tab.

قراءة ls المفصّلة. الأمر ls -lah يجمع العرض المفصل والملفات المخفية والأحجام المقروءة (الأسماء والتواريخ هنا توضيحية):

$ ls -lah
total 20K
drwxr-xr-x  4 sara sara 4.0K Oct  3 09:12 .
drwxr-x--- 18 sara sara 4.0K Oct  3 08:40 ..
-rw-r--r--  1 sara sara  220 Oct  1 10:05 .env.example
drwxr-xr-x  2 sara sara 4.0K Oct  3 09:12 docs
-rw-r--r--  1 sara sara 1.2K Oct  2 17:30 README.md

العمود الأول يبدأ بحرف النوع: d لمجلد و- لملف عادي، وتليه الصلاحيات التي نفصّلها في الدرس الرابع. ثم عدد الروابط، فالمالك، فالمجموعة، فالحجم، فتاريخ آخر تعديل، فالاسم. السطران . و.. يمثلان المجلد الحالي والأب. والملفات المخفية هي ببساطة ما يبدأ اسمه بنقطة، مثل .env.example و.bashrc، ولا يعرضها ls إلا مع -a. ولترتيب مختلف: ls -lt يضع الأحدث تعديلاً أولاً، وls -lS يرتب حسب الحجم من الأكبر.

سؤالان يتكرران أثناء التنقل: ما نوع هذا الملف، وكم يشغل هذا المجلد؟ الأمر file يفحص محتوى الملف نفسه بدل الاعتماد على امتداده، فيخبرك مثلاً أن logo.png صورة PNG فعلاً، أو أن ملفاً بلا امتداد نص عادي. والأمر du -sh * يعرض حجم كل عنصر في المجلد الحالي بوحدات مقروءة، بينما df -h يعرض المساحة المتبقية على كل قسم من القرص. وإذا امتلأ القرص فجأة فابدأ بـ du -sh ~/* | sort -h لتظهر أكبر المجلدات في آخر القائمة، ثم انزل داخلها مجلداً بعد مجلد بالأمر نفسه.

حالة متخيلة: طالبة اسمها منى تتدرب على Ubuntu داخل WSL، وكتبت cd Projects فظهرت الرسالة bash: cd: Projects: No such file or directory مع أن المجلد موجود أمامها. نفّذت ls فوجدت الاسم مكتوباً projects بحرف صغير؛ فأسماء الملفات في Linux حساسة لحالة الأحرف. بعدها حاولت دخول مجلد اسمه My Files فاعتبرت الصدفة الكلمتين وسيطين منفصلين، والحل علامات التنصيص cd "My Files" أو الشرطة المائلة cd My\ Files. وحين كتبت cd notes.txt ظهرت Not a directory لأنها حاولت دخول ملف. الدرس الذي خرجت به: اكتب أول حرفين ثم اضغط Tab، فإن لم يكتمل الاسم فهو غير موجود حيث تظن، وتكتشف الخطأ قبل الضغط على Enter.

  • pwd يكشف مكانك، وcd بلا وسيط يعيدك إلى مجلدك الرئيسي، وcd - يرجعك للمكان السابق.
  • المسار المطلق يبدأ بـ / ويعمل من أي مكان، والنسبي يُحسب من المجلد الحالي.
  • /etc للإعدادات و/var/log للسجلات و/home لمجلدات المستخدمين.
  • الأسماء حساسة لحالة الأحرف، والأسماء ذات المسافات تحتاج علامات تنصيص.

الهيكل

 /
 ├── etc        (إعدادات)
 ├── var/log    (سجلات)
 ├── usr/bin    (برامج)
 └── home
      └── sara  (~)
           └── projects/shop
 cd ..  ← صعود   |   cd shop → نزول

مختبر: خريطة مجلدات

من مجلدك الرئيسي زر /etc ثم /var/log ثم عد بـ cd - ونفّذ pwd بعد كل انتقال. ارسم الشجرة التي مررت بها على ورقة.

مهمة عملية

أنشئ المجلد mesue-cli-lab في مجلدك الرئيسي وادخله، ثم انتقل منه إلى /tmp بمسار نسبي واحد (مثل ../../../tmp حسب عمق مسارك) وتحقق بـ pwd أن الناتج /tmp.

اختبر فهمك (1/3)

أين تتوقع أن تجد ملفات الإعداد النصية لأغلب البرامج على Ubuntu؟

اختبر فهمك (2/3)

أنت في /home/sara/projects/shop ونفّذت cd ../../Downloads. ما ناتج pwd؟

اختبر فهمك (3/3)

تريد دخول مجلد اسمه My Files. أي أمر يعمل كما تتوقع؟

للاستزادة: GNU Coreutils: ls invocation · man7: hier — وصف هرمية نظام الملفات · Filesystem Hierarchy Standard 3.0

3. الملفات — إنشاء، نسخ، نقل، حذف

الهدف: تنشئ الملفات والمجلدات وتنسخها وتنقلها وتقرؤها، وتحذف بعادات تمنع الكوارث.

هذه أدوات العمل اليومي في الطرفية، وكلها من حزمة GNU Coreutils المثبّتة على كل نظام Ubuntu:

mkdir src                # مجلد جديد
mkdir -p app/static/css  # ينشئ السلسلة كلها دفعة واحدة
touch notes.txt          # ملف فارغ، أو تحديث تاريخ ملف موجود
cp notes.txt notes.bak   # نسخ ملف
cp -r src/ src-copy/     # نسخ مجلد بكل ما فيه
mv notes.bak old/        # نقل إلى مجلد آخر
mv draft.txt final.txt   # إعادة تسمية
rm final.txt             # حذف ملف
rmdir empty-dir          # حذف مجلد فارغ فقط

بدون -p يفشل mkdir app/static/css إن لم يكن app موجوداً، أما معه فينشئ الناقص ولا يشتكي إن وُجد المجلد مسبقاً، ولهذا تراه في السكربتات كثيراً. والأمر mv هو نفسه أمر إعادة التسمية، لأن النقل وتغيير الاسم في Linux كلاهما تغيير لمسار الملف. انتبه إلى أن cp وmv يستبدلان الملف الهدف إن كان موجوداً دون سؤال، والخيار -i يجعلهما يطلبان تأكيدك قبل الاستبدال.

قراءة الملفات دون فتح محرر. لكل حجم أداة مناسبة:

cat config.ini           # ملف قصير يُطبع كاملاً
less /var/log/syslog     # ملف طويل: تصفّح صفحة صفحة
head -n 20 data.csv      # أول 20 سطراً
tail -n 20 app.log       # آخر 20 سطراً
tail -f app.log          # متابعة الأسطر الجديدة لحظة كتابتها

داخل less: المسافة للصفحة التالية، وb للسابقة، و/error للبحث عن كلمة، وn للنتيجة التالية، وG لآخر الملف، وg لأوله، وq للخروج. وتخرج من tail -f بـ Ctrl+C. وللتعديل السريع استعمل المحرر nano: Ctrl+O للحفظ وCtrl+X للخروج.

لا سلة مهملات في الطرفية. الأمر rm يحذف نهائياً، ولا يوجد أمر تراجع. لذلك تحتاج عادات ثابتة أكثر مما تحتاج حفظ الخيارات. قبل أي حذف بنمط مثل rm *.log نفّذ ls *.log بالنمط نفسه لترى ما سيطابقه بالضبط. والخيار -I بحرف كبير يسألك مرة واحدة قبل الحذف التكراري أو قبل حذف أكثر من ثلاثة ملفات، فهو أخف من -i الذي يسأل عن كل ملف. وإن كان اسم الملف يبدأ بشرطة مثل -notes.txt فاكتب rm -- -notes.txt أو rm ./-notes.txt كي لا يُفهم الاسم كخيار.

الأنماط تتوسع قبل أن يراها الأمر. حين تكتب rm *.log فإن الصدفة هي التي تحوّل *.log إلى قائمة الأسماء المطابقة ثم تسلّمها إلى rm جاهزة؛ فالنجمة تطابق أي عدد من الأحرف، وعلامة الاستفهام ? تطابق حرفاً واحداً، و[0-9] تطابق رقماً واحداً. لهذا تنجح حيلة بسيطة للمعاينة: ضع echo قبل الأمر، فيطبع echo rm *.log الأمر كما سيُنفَّذ بعد التوسيع دون أن يحذف شيئاً. وإن لم يطابق النمط أي ملف يمرره Bash حرفياً كما هو، فترى رسالة No such file or directory تحمل النمط نفسه اسماً.

قبل وبعد: سطران خطيران في سكربت تنظيف. هذه النسخة الأصلية:

cd ~/old-sit     # خطأ إملائي: المجلد اسمه old-site
rm -rf *         # يحذف كل ما في المجلد الذي بقيت فيه

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

ls -d ~/old-site && rm -rI ~/old-site

العلامة && تعني «نفّذ ما بعدي فقط إن نجح ما قبلي»، فإن لم يوجد المجلد توقف الأمر عند ls ولم يُحذف شيء. وفي الدرس التاسع نضيف set -e لتوقيف السكربت كله عند أول فشل.

التجربة الجافة (Dry Run). أدوات كثيرة تتيح لك رؤية ما ستفعله قبل أن تفعله. أشهرها rsync للنسخ والمزامنة (ثبّته بـ apt إن لم يكن موجوداً):

rsync -av --dry-run docs/ backup/docs/   # يعرض ما سيُنسخ فقط
rsync -av docs/ backup/docs/             # التنفيذ الفعلي بعد المراجعة

لاحظ الشرطة المائلة في آخر docs/: معها يُنسخ محتوى المجلد، ومن دونها يُنسخ المجلد نفسه داخل الهدف فتحصل على backup/docs/docs. وهذا بالضبط نوع الخطأ الذي تكشفه التجربة الجافة قبل وقوعه.

أخيراً، رسائل ستقابلها كثيراً: cp: -r not specified; omitting directory تعني أنك نسخت مجلداً دون -r فتجاهله cp. وrm: cannot remove 'docs': Is a directory حاجز أمان يمنع حذف مجلد دون -r. وفي GNU rm حماية مدمجة ترفض حذف الجذر / تكرارياً ما لم تطلب ذلك صراحة، لكنها لا تحميك من حذف مجلدك الرئيسي أو مشروعك، فالمسؤولية تبقى على مراجعتك للمسار.

  • mkdir -p ينشئ المجلدات المتداخلة ولا يفشل إن كانت موجودة.
  • cp وmv يستبدلان الهدف بصمت، والخيار -i يطلب التأكيد.
  • less للملفات الطويلة، وtail -f لمتابعة سجل يكبر.
  • قبل rm بنمط نفّذ ls بالنمط نفسه، وجرّب rsync --dry-run قبل المزامنة.

مخطط

touch / mkdirملف/مجلد
cp / mvنسخة / اسم جديد
ls النمط أولاًrm -I✗ (نهائي)
rsync --dry-runمراجعةتنفيذ

مختبر: نسخ احتياطي بتجربة جافة

شغّل rsync مرة بـ docs/ ومرة بـ docs (بلا شرطة) مع --dry-run، وقارن المسارات التي يعرضها في كل مرة قبل أي نسخ فعلي.

مهمة عملية

في mesue-cli-lab أنشئ src وdocs وREADME.md، ثم انسخ docs إلى backup بـ rsync بعد تجربة جافة، وتأكد بـ ls -R أن المسار الناتج backup/docs وليس backup/docs/docs.

اختبر فهمك (1/3)

ملف سجل يكبر باستمرار وتريد متابعة الأسطر الجديدة لحظة كتابتها. أي أمر؟

اختبر فهمك (2/3)

في سكربت سطران: cd ~/old-sit ثم rm -rf * مع خطأ إملائي في اسم المجلد. ما الخطر؟

اختبر فهمك (3/3)

ما فائدة rsync -av --dry-run docs/ backup/docs/ قبل النسخ الفعلي؟

للاستزادة: GNU Coreutils: rm invocation · GNU Coreutils: cp invocation · man7: less · rsync — صفحة الدليل الرسمية

4. الصلاحيات — من يقرأ ويكتب وينفّذ؟

الهدف: تقرأ صلاحيات rwx وتعدّلها بـ chmod الرقمية والرمزية، وتغيّر المالك بـ chown، وتفهم أثر umask وعضوية المجموعات.

لكل ملف في Linux مالك (User) ومجموعة (Group)، وثلاث فئات من الصلاحيات تظهر في ls -l (أمثلة توضيحية):

-rw-r--r-- 1 sara sara 1.2K Oct  2 17:30 README.md
drwxr-x--- 2 sara devs 4.0K Oct  3 09:12 shared

بعد حرف النوع تأتي تسعة أحرف، كل ثلاثة منها لفئة: المالك، ثم المجموعة، ثم بقية المستخدمين (Others). في كل ثلاثية r للقراءة وw للكتابة وx للتنفيذ، والشرطة تعني غياب الصلاحية. في السطر الأول تقرأ سارة وتكتب، والباقون يقرؤون فقط. وفي الثاني تملك سارة كل شيء، وأعضاء مجموعة devs يدخلون المجلد ويقرؤون، والباقون ممنوعون تماماً.

الصيغة الرقمية. لكل صلاحية قيمة: r تساوي 4، وw تساوي 2، وx تساوي 1، وتُجمع القيم لكل فئة على حدة. فـ rwx تساوي 7، وrw- تساوي 6، وr-x تساوي 5، وr-- تساوي 4. مثال محلول: chmod 640 salaries.csv يعني المالك 6 أي قراءة وكتابة، والمجموعة 4 أي قراءة فقط، والآخرون 0 فلا شيء، فتصبح الصلاحيات -rw-r-----. القيم الشائعة: 644 لملف نصي عادي، و600 لملف سري لا يقرؤه غيرك، و755 لسكربت أو مجلد يدخله الجميع، و700 لمجلد خاص بك.

الصيغة الرمزية. تكتب فيها الفئة (u للمالك، g للمجموعة، o للآخرين، a للجميع)، ثم العملية (+ للإضافة، - للحذف، = للتعيين الكامل)، ثم الصلاحية:

chmod u+x deploy.sh        # أضف التنفيذ للمالك فقط
chmod g-w,o= notes.txt     # احذف كتابة المجموعة وامسح صلاحيات الآخرين
chmod u=rw,go=r report.md  # تعادل 644

قاعدة الاختيار بينهما: الرمزية حين تريد تغيير صلاحية واحدة وترك الباقي كما هو، والرقمية حين تريد فرض الحالة الكاملة دفعة واحدة.

في المجلدات تتغير المعاني: r تسمح بعرض الأسماء بـ ls، وw تسمح بإنشاء الملفات وحذفها داخله، وx تسمح بدخوله والوصول إلى ما فيه. لذلك مجلد بصلاحية r دون x يعرض الأسماء لكنه يمنعك من فتح أي ملف داخله. ولتشغيل سكربت بـ ./hello.sh يجب أن يملك x، وإلا ظهرت Permission denied، وحلها chmod u+x hello.sh.

من أنت في نظر النظام؟ الأمر id يعرض رقمك ومجموعاتك (مخرجات توضيحية):

$ id
uid=1000(sara) gid=1000(sara) groups=1000(sara),27(sudo),1001(devs)

عضوية مجموعة sudo هي ما يسمح لسارة باستخدام sudo على Ubuntu. وتغيير المالك يتم بـ sudo chown ali:devs report.csv، ويحتاج صلاحيات المدير لأن المستخدم العادي لا يستطيع إهداء ملفاته لغيره. والخيار -R يطبّق التغيير على مجلد بكل ما فيه، فراجع المسار قبل استعماله.

umask: من أين تأتي صلاحيات الملفات الجديدة؟ حين ينشئ برنامج ملفاً يطلب عادة 666 للملفات و777 للمجلدات، ثم يحذف النظام منها البتات الموجودة في قناع الإنشاء umask. نفّذ umask لترى قيمتك؛ ستجد غالباً 0022 أو 0002 حسب الإعداد. مع 0022:

$ umask
0022
$ touch a.txt && mkdir d && ls -ld a.txt d
-rw-r--r-- 1 sara sara    0 Oct  3 10:00 a.txt
drwxr-xr-x 2 sara sara 4.0K Oct  3 10:00 d

أي 644 للملف و755 للمجلد، ومع 0002 تصبحان 664 و775. والأمر umask 077 في جلسة ما يجعل كل ما تنشئه بعده خاصاً بك وحدك، وهو مفيد قبل التعامل مع ملفات حساسة.

سجل متخيل لمشكلة حقيقية الشكل: فريق صغير لثلاثة طلاب يشارك مجلد /srv/shared على جهاز مختبر. يكتب علي touch /srv/shared/plan.md فيظهر Permission denied. الخطوة الأولى ls -ld /srv/shared فتظهر drwxr-x--- root devs، أي أن المجموعة تقرأ وتدخل لكنها لا تكتب. الخطوة الثانية id ali فيتبين أنه ليس عضواً في devs أصلاً. يضيفه المسؤول بـ sudo usermod -aG devs ali، والخيار -a ضروري لأن حذفه يستبدل كل مجموعات علي الإضافية بدل الإضافة إليها. يسجّل علي خروجه ودخوله لتُقرأ العضوية الجديدة، ثم يمنح المسؤول المجموعة صلاحية الكتابة بـ sudo chmod g+w /srv/shared. انتهت المشكلة بتغييرين محددين، دون اللجوء إلى chmod 777 الذي يمنح كل مستخدم وكل برنامج مخترق على الجهاز حق التعديل.

  • r=4 وw=2 وx=1 تُجمع لكل فئة: 640 تعني rw- للمالك وr-- للمجموعة ولا شيء للآخرين.
  • chmod الرمزية مثل u+x تغيّر صلاحية واحدة، والرقمية تفرض الحالة كاملة.
  • umask يحذف بتات من 666 و777 عند الإنشاء، فـ 0022 تعطي 644 للملفات و755 للمجلدات.
  • usermod -aG يضيف مجموعة دون حذف غيرها، والعضوية تُفعَّل بعد تسجيل دخول جديد.

مخطط

 -rw-r-----
  |  |  |
  |  |  +-- others: ---  = 0
  |  +----- group : r--  = 4
  +-------- user  : rw-  = 6
 → chmod 640

مختبر: صلاحيات بالرقم والرمز

جرّب ./hello.sh قبل chmod u+x وبعده، ثم حوّل الصلاحيات إلى 640 بالصيغة الرقمية، ثم أعدها إلى 644 بالصيغة الرمزية u=rw,go=r وتحقق بـ ls -l بعد كل خطوة.

مهمة عملية

أنشئ ملفاً اسمه secret.txt واجعل صلاحياته -rw------- مرة بـ chmod الرقمية ومرة بالرمزية، ثم نفّذ umask 077 وأنشئ ملفاً جديداً وتحقق أن صلاحياته خرجت 600 تلقائياً.

اختبر فهمك (1/3)

ما صلاحيات الملف بعد تنفيذ chmod 640 salaries.csv؟

اختبر فهمك (2/3)

تريد إضافة صلاحية التنفيذ للمالك فقط دون المساس بباقي الصلاحيات. أي أمر؟

اختبر فهمك (3/3)

أُضيف ali إلى مجموعة devs بالأمر usermod -aG، لكنه ما زال يرى Permission denied في جلسته المفتوحة. ما الخطوة الأرجح؟

للاستزادة: GNU Coreutils: chmod invocation · GNU Coreutils: Symbolic Modes · GNU Coreutils: chown invocation · man7: umask · man7: usermod

5. الأنابيب وإعادة التوجيه — ربط الأوامر

الهدف: تفصل المخرجات عن الأخطاء، وتوجّهها إلى ملفات، وتبني سلسلة أدوات صغيرة تحلل بيانات حقيقية.

كل برنامج في الطرفية يتعامل مع ثلاث قنوات مرقّمة: القناة 0 هي المدخل القياسي stdin (لوحة المفاتيح افتراضياً)، والقناة 1 هي المخرج القياسي stdout للنتائج العادية، والقناة 2 هي stderr لرسائل الخطأ. القناتان 1 و2 تظهران معاً على الشاشة فتبدوان شيئاً واحداً، لكن الصدفة تستطيع فصلهما وتوجيه كل منهما إلى مكان مختلف. هذه الفكرة البسيطة هي أساس كل ما في هذا الدرس.

إعادة التوجيه (Redirection) ترسل قناة إلى ملف بدل الشاشة:

ls -l > list.txt          # stdout إلى ملف مع استبدال محتواه
date >> activity.log      # إلحاق في آخر الملف
find / -name x 2> err.log # الأخطاء فقط إلى ملف
make > build.log 2>&1     # المخرجات والأخطاء في ملف واحد
wc -l < names.txt         # قراءة المدخل من ملف
cmd 2> /dev/null          # التخلص من الأخطاء

العبارة 2>&1 تعني «أرسل القناة 2 إلى حيث تذهب القناة 1 الآن»، ولذلك يهم الترتيب: في make 2>&1 > build.log تذهب الأخطاء إلى الشاشة، لأن التوجيه قُرئ من اليسار قبل تحويل stdout إلى الملف. أما /dev/null فملف خاص يبتلع كل ما يُكتب فيه.

الأنبوب (Pipe) يأخذ stdout من الأمر الأيسر ويجعله stdin للأمر الأيمن، فتبني من أدوات صغيرة متخصصة أداة كبيرة. وهو يمرر stdout وحده: نفّذ ls nothere | wc -l فترى رسالة الخطأ على الشاشة ثم الرقم 0، لأن الخطأ خرج من القناة 2 مباشرة إليك، ولم يصل إلى wc إلا مخرج فارغ.

مثال محلول على بيانات متجر متخيل. متجر إلكتروني صغير يصدّر طلباته في ملف orders.csv (البيانات توضيحية)، والمطلوب: أكثر ثلاث مدن طلباً.

$ head -3 orders.csv
order_id,date,city,total
1001,2026-09-28,Cairo,350
1002,2026-09-28,Alexandria,120
$ tail -n +2 orders.csv | cut -d, -f3 | sort | uniq -c | sort -rn | head -3
     42 Cairo
     17 Alexandria
      9 Mansoura

اقرأ السلسلة مرحلة مرحلة: tail -n +2 يطبع الملف من السطر الثاني فيتخطى سطر العناوين. ثم cut -d, -f3 يقطع كل سطر عند الفواصل ويأخذ الحقل الثالث وهو المدينة. ثم sort يرتّب الأسماء فتتجاور المتكررة، وهذا شرط لما بعده لأن uniq -c يدمج الأسطر المتطابقة المتجاورة فقط ويكتب عددها أمامها. ثم sort -rn يرتب رقمياً من الأكبر، وhead -3 يأخذ الثلاثة الأولى. طريقة البناء الصحيحة أن تضيف مرحلة واحدة في كل مرة وتنظر إلى الناتج قبل إضافة التالية، فإن ظهر خطأ عرفت أي مرحلة سببته.

سؤال ثانٍ على الملف نفسه: ما أكبر خمسة طلبات قيمة؟ هنا لا تحتاج cut، يكفي أن تخبر sort بالفاصل والعمود: tail -n +2 orders.csv | sort -t, -k4 -rn | head -5. الخيار -t, يحدد الفاصل، و-k4 يرتب حسب الحقل الرابع، و-rn يجعل الترتيب رقمياً تنازلياً. ولعدّ الطلبات كلها tail -n +2 orders.csv | wc -l. لكن انتبه إلى حدود هذه الأدوات: هي لا تفهم ملفات CSV المعقدة التي تحتوي فواصل داخل علامات تنصيص، مثل عنوان «شارع النيل، الجيزة»؛ مع بيانات كهذه انتقل إلى أداة تفهم الصيغة مثل وحدة csv في بايثون.

الأمر tee يكتب ما يصله إلى ملف ويمرره إلى الشاشة في الوقت نفسه، مثل ping -c 3 example.com | tee ping.log. وله استخدام يحل لغزاً شائعاً: الأمر sudo echo "127.0.0.1 shop.local" >> /etc/hosts يفشل بـ Permission denied رغم وجود sudo، لأن إعادة التوجيه >> تنفّذها صدفتك العادية قبل تشغيل sudo، وصدفتك لا تملك حق الكتابة في ذلك الملف. الصيغة الصحيحة echo "127.0.0.1 shop.local" | sudo tee -a /etc/hosts، فهنا tee هو الذي يكتب وهو يعمل بصلاحيات المدير، والخيار -a يجعله يلحق بدل أن يستبدل.

فخان أخيران: الأمر sort data.txt > data.txt يترك الملف فارغاً، لأن الصدفة تفرّغ الهدف قبل أن يبدأ sort بالقراءة؛ اكتب إلى ملف جديد ثم انقله بـ mv. والثاني أن رمز خروج السلسلة كلها هو رمز آخر أمر فيها فقط، فلو فشل أمر في منتصفها لن تعرف ذلك من النتيجة النهائية. سنعالج هذا في السكربتات بالخيار set -o pipefail في الدرس التاسع.

  • القناة 1 للنتائج والقناة 2 للأخطاء، والأنبوب يمرر القناة 1 وحدها.
  • > يستبدل و>> يلحق، و> file 2>&1 يجمع القناتين بهذا الترتيب تحديداً.
  • uniq -c يحتاج sort قبله لأنه يدمج المتجاور فقط.
  • للكتابة في ملف نظام استعمل sudo tee -a بدل sudo echo مع >>.

مخطط

orders.csv
أكثر المدن طلباً

مختبر: تحليل ملف طلبات

ابنِ السلسلة مرحلة مرحلة: نفّذ tail وحده، ثم أضف cut، ثم sort، ثم uniq -c. ماذا يحدث إن حذفت sort الأولى؟

مهمة عملية

أنشئ orders.csv بعشرة أسطر لأربع مدن، واستخرج بسلسلة أنابيب واحدة المدينة الأكثر طلباً، واحفظ الناتج في top.txt مع عرضه على الشاشة باستخدام tee.

اختبر فهمك (1/3)

أي أمر يحفظ المخرجات العادية ورسائل الخطأ معاً في الملف all.log؟

اختبر فهمك (2/3)

فشل الأمر sudo echo "x" >> /etc/hosts برسالة Permission denied. لماذا؟

اختبر فهمك (3/3)

في السلسلة cut -d, -f3 | sort | uniq -c، لماذا يأتي sort قبل uniq -c؟

للاستزادة: GNU Bash: Redirections · GNU Bash: Pipelines · GNU Coreutils: uniq invocation · GNU Coreutils: tee invocation

6. grep و find — البحث داخل الملفات وعنها

الهدف: تبحث عن نص داخل الملفات بـ grep، وعن الملفات نفسها بـ find، وتحذف نتائج البحث بأمان.

قبل أن تكتب أمر بحث، حدّد نوع السؤال. إن كان السؤال «أين ذُكرت هذه الكلمة؟» فأنت تبحث داخل المحتوى، وأداتك grep. وإن كان «أين الملفات التي اسمها كذا، أو التي تجاوز حجمها كذا، أو لم تُعدَّل منذ شهر؟» فأنت تبحث عن الملفات بصفاتها، وأداتك find. الخلط بينهما يعطيك نتائج ناقصة أو بحثاً بطيئاً بلا داعٍ.

grep يقرأ النص سطراً سطراً ويطبع الأسطر التي تطابق النمط:

grep -n "TODO" app.py                    # مع رقم السطر
grep -i "error" app.log                  # تجاهل حالة الأحرف
grep -rn "fetch(" src/                   # بحث تكراري في مجلد
grep -rn "TODO" . --exclude-dir=node_modules
grep -rl "API_KEY" .                     # أسماء الملفات فقط
grep -v "DEBUG" app.log                  # استبعاد الأسطر المطابقة
grep -w "id" app.py                      # الكلمة كاملة: لا valid ولا width

النمط في grep تعبير نمطي (Regular Expression) وليس نصاً حرفياً دائماً: النقطة تطابق أي حرف، و^ بداية السطر، و$ نهايته. لذلك ls | grep ".md" يلتقط أيضاً ملفاً اسمه cmd_notes.txt، لأن النقطة طابقت الحرف c. الصيغة الدقيقة ls | grep '\.md$': الشرطة المائلة تجعل النقطة حرفية، و$ تشترط أن تأتي في آخر الاسم. وللبحث عن نص فيه رموز كثيرة دون تفسير استعمل grep -F، ولخيارات متعددة grep -E "error|warning". وضع النمط بين علامتي تنصيص دائماً كي لا تفسّر الصدفة رموزه قبل grep.

وحين تبحث في سجل لا يكفيك السطر المطابق وحده، فالخطأ يُفهم بما قبله وما بعده. الخيار -A 3 يطبع ثلاثة أسطر بعد كل تطابق، و-B 2 سطرين قبله، و-C 2 سطرين من كل جهة. فالأمر grep -n -C 2 "Traceback" app.log يعرض موضع الاستثناء مع الأسطر المحيطة وأرقامها، فتفتح الملف عند الرقم مباشرة في محررك. وإن كثرت النتائج فاحصرها في نوع ملفات محدد: grep -rn --include="*.py" "import requests" . يبحث في ملفات بايثون وحدها.

تفصيل يهمك في السكربتات: grep يخرج برمز 0 إن وجد تطابقاً، و1 إن لم يجد، و2 عند خطأ مثل ملف غير موجود. فالجملة if grep -q "ERROR" app.log; then ... fi تختبر وجود الخطأ دون طباعة شيء، والخيار -q يكتم المخرجات.

find يمشي في شجرة المجلدات ويختبر كل ملف بالشروط التي تكتبها (المسارات توضيحية):

$ find ~/projects -type f -name "*.log" -mtime +30
/home/sara/projects/shop/logs/2026-07-01.log
/home/sara/projects/shop/logs/2026-07-02.log
$ find . -type f -size +100M          # ملفات أكبر من 100 ميغابايت
$ find . -iname "readme*"             # الاسم بلا حساسية لحالة الأحرف
$ find src -name "*.py" -exec grep -l "TODO" {} +

الشرط -type f يحصر النتائج في الملفات العادية، و-mtime +30 يعني آخر تعديل قبل أكثر من ثلاثين يوماً. والسطر الأخير يجمع الأداتين: find يختار ملفات بايثون، وgrep يبحث داخلها، و{} + يمرر الأسماء إليه دفعات.

الحذف بنتائج find خطوتان دائماً. شغّل الأمر أولاً بلا أي فعل، وراجع القائمة سطراً سطراً، ثم أضف -delete في آخر الأمر: find ~/projects -type f -name "*.log" -mtime +30 -delete. الموضع مهم: find يقيّم الشروط من اليسار إلى اليمين، فلو كتبت -delete قبل -name لحذف كل ما يصادفه قبل أن يصل إلى شرط الاسم، وهو تحذير مكتوب صراحة في صفحة دليله. هذه هي فكرة التجربة الجافة نفسها التي رأيتها مع rsync، مطبقة على البحث.

وحين تكون النتائج طويلة لا تدعها تتدفق على الشاشة؛ مررها إلى less: grep -rn "TODO" src/ | less. وتستطيع فتح سجل عند أول ظهور لكلمة مباشرة بـ less +/ERROR app.log، ثم الضغط على n للانتقال إلى الظهور التالي. حالة متخيلة: مطوّر في متجر صغير يلاحظ أن القرص امتلأ، فيبدأ بـ find / -xdev -type f -size +500M 2>/dev/null ليجد الملفات الضخمة على القسم الرئيسي وحده، ويكتشف سجلاً قديماً بحجم كبير، فيراجعه بـ less قبل أن يقرر حذفه أو ضغطه. الخيار -xdev يمنع find من النزول إلى أقسام وأقراص أخرى مركّبة، و2>/dev/null يخفي رسائل المجلدات الممنوعة.

  • grep للبحث داخل المحتوى، وfind للبحث عن الملفات بالاسم والحجم والتاريخ.
  • النقطة في grep تطابق أي حرف؛ استعمل \.md$ أو grep -F للنص الحرفي.
  • grep يخرج بـ 0 عند التطابق و1 عند عدمه و2 عند الخطأ.
  • راجع نتائج find أولاً، ثم أضف -delete في آخر الأمر فقط.

مخطط

سؤالك؟
«أين ذُكرت الكلمة؟»grep -rn
«أين الملف؟ ما حجمه؟ متى عُدّل؟»find
مراجعة القائمة-delete في الآخر

مختبر: صيد الملفات القديمة

أنشئ ملفين بـ touch، وغيّر تاريخ أحدهما بـ touch -d "40 days ago" old.log، ثم اعثر عليه وحده بـ find -mtime +30 قبل أي حذف.

مهمة عملية

في مجلد mesue-cli-lab اعثر بـ find على كل ملفات ‎.log الأقدم من 30 يوماً وراجع القائمة، ثم احذفها بإضافة -delete في آخر الأمر، وتحقق بإعادة تشغيل البحث أن الناتج أصبح فارغاً.

اختبر فهمك (1/3)

تريد قائمة بملفات ‎.log الأقدم من 30 يوماً داخل مجلد المشروع. أي أمر يناسب السؤال؟

اختبر فهمك (2/3)

ما الطريقة الآمنة لحذف ملفات ‎.tmp القديمة باستخدام find؟

اختبر فهمك (3/3)

الأمر ls | grep ".md" أظهر أيضاً cmd_notes.txt. ما التصحيح الأدق؟

للاستزادة: GNU grep Manual · man7: find · man7: less

7. العمليات والخدمات — ما الذي يعمل الآن؟

الهدف: تراقب العمليات وتوقفها بالإشارة المناسبة، وتدير خدمات النظام بـ systemctl، وتقرأ سجلاتها بـ journalctl.

كل برنامج يعمل الآن هو عملية (Process) لها رقم تعريف فريد اسمه PID، وحتى الصدفة التي تكتب فيها عملية، وكل أمر تشغّله منها يصبح عملية ابنة لها. أدوات المراقبة اليومية (المخرجات توضيحية):

$ ps aux --sort=-%mem | head -3     # أكثر العمليات استهلاكاً للذاكرة
$ pgrep -a python3
2311 python3 -m http.server 8080
$ top                               # مراقبة حية، والخروج بـ q

في مخرجات ps aux تجد المستخدم وPID ونسبة المعالج والذاكرة والأمر الكامل. ويُفضَّل pgrep على ps aux | grep لأن الأخير يُظهر سطر grep نفسه ضمن النتائج.

الإيقاف يتم بإرسال إشارة (Signal). الأمر kill 2311 يرسل افتراضياً SIGTERM، وهي طلب مهذب بالتوقف يتيح للبرنامج إنهاء الكتابة وحفظ ملفاته وتحرير موارده. أما kill -9 2311 فيرسل SIGKILL التي لا يستطيع البرنامج التقاطها ولا تجاهلها، فيُنهى فوراً دون تنظيف، وقد تبقى ملفات مؤقتة أو أقفال عالقة أو بيانات لم تُحفظ؛ لذلك هي الحل الأخير بعد أن يفشل SIGTERM. وفي الطرفية نفسها يرسل Ctrl+C الإشارة SIGINT للأمر الذي في المقدمة، بينما يرسل Ctrl+Z إشارة تجمّد العملية فقط دون إنهائها.

هذا الفرق الأخير مصدر فخ شهير: تجمّد خادم تطوير بـ Ctrl+Z ثم تشغّله من جديد، فتظهر Address already in use لأن النسخة المجمّدة ما زالت تحجز المنفذ. أدوات التحكم في المهام تحل المشكلة:

python3 -m http.server 8080 &    # تشغيل في الخلفية
jobs                             # قائمة مهام هذه الصدفة
fg %1                            # إعادة المهمة 1 إلى المقدمة
kill %1                          # إيقاف المهمة 1 برقمها
ss -ltnp | grep 8080             # من يستمع على المنفذ 8080؟

مهام الخلفية التي تبدأ بـ & مرتبطة بجلسة الطرفية، فتنتهي غالباً عند إغلاق النافذة أو انقطاع اتصال SSH. للمهمة الطويلة التي يجب أن تستمر، كنسخ ملفات ضخمة أو تشغيل تجربة تستغرق ساعات، أمامك خياران: nohup command > run.log 2>&1 & يجعل الأمر يتجاهل إشارة إغلاق الجلسة SIGHUP ويكتب مخرجاته في ملف، أو أداة مثل tmux تفتح جلسة تبقى حية بعد انقطاعك فتعود إليها لاحقاً. أما البرنامج الذي يجب أن يعمل دائماً فمكانه الطبيعي خدمة يديرها systemd.

الخدمات وsystemd. البرامج التي يجب أن تعمل دائماً، مثل خادم الويب أو المجدول cron، لا تُشغَّل يدوياً من طرفية، بل يديرها systemd، وهو أول عملية تبدأ عند إقلاع Ubuntu ومدير الخدمات فيه. كل خدمة عنده «وحدة» (Unit) ينتهي اسمها بـ ‎.service، وتتحكم فيها بـ systemctl (مخرجات توضيحية):

$ systemctl status cron
● cron.service - Regular background program processing daemon
     Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
     Active: active (running) since Sat 2026-10-03 08:41:12 EEST; 1h 2min ago
   Main PID: 812 (cron)
$ sudo systemctl restart cron         # إعادة التشغيل
$ sudo systemctl enable --now cron    # تفعيل عند الإقلاع وتشغيل فوري
$ systemctl --failed                  # الوحدات التي فشلت

السطر Loaded يخبرك أين ملف الوحدة وهل هي مفعّلة (enabled)، والسطر Active يخبرك هل تعمل الآن. وهذا تمييز يخلط فيه المبتدئون كثيراً: start يشغّل الخدمة الآن فقط، وenable يجعلها تبدأ تلقائياً عند كل إقلاع دون أن يشغّلها الآن، وenable --now يجمع الأمرين. قاعدة أخرى: الخدمة التي يديرها systemd أوقفها بـ systemctl stop لا بـ kill، لأن systemd قد يعيد تشغيلها تلقائياً حسب إعداد Restart في ملف الوحدة، فتظن أن الإيقاف لم يعمل.

سجل استكشاف متخيل. طالب يشغّل واجهة برمجية تجريبية لمتجره كخدمة اسمها shop-api، وبعد تحديث توقفت. الخطوة الأولى systemctl status shop-api تُظهر Active: failed. الخطوة الثانية قراءة السجل:

$ journalctl -u shop-api -n 20 --no-pager
shop-api[2210]: PermissionError: [Errno 13] Permission denied: /var/lib/shop/data.db
systemd[1]: shop-api.service: Main process exited, code=exited, status=1/FAILURE

السبب واضح الآن: الخدمة تعمل بمستخدم مخصص لا يملك ملف قاعدة البيانات بعد نقله. أصلح الطالب الملكية بـ chown كما في الدرس الرابع، ثم نفّذ sudo systemctl restart shop-api، وتأكد بـ systemctl status أن الحالة عادت active (running).

خيارات journalctl التي تكفيك غالباً: -u لوحدة محددة، و-n 50 لآخر خمسين سطراً، و-f للمتابعة الحية مثل tail -f، و--since "1 hour ago" لفترة زمنية، و-p err -b للأخطاء فقط منذ آخر إقلاع. وإن لم تظهر لك سجلات النظام كاملة فقد تحتاج sudo أو عضوية مجموعة adm أو systemd-journal.

  • kill يرسل SIGTERM للتوقف المنظم، وkill -9 حل أخير بلا تنظيف.
  • Ctrl+Z يجمّد العملية فقط، فاستعمل jobs وfg وkill %1 لإدارتها.
  • start يشغّل الخدمة الآن، وenable يجعلها تبدأ عند الإقلاع، وenable --now يجمعهما.
  • journalctl -u اسم_الخدمة هو أول مكان تبحث فيه عن سبب توقف خدمة.

مخطط

عملية عاديةps / pgrepkill (TERM)kill -9 (أخيراً)
خدمةsystemctl status
failed?journalctl -u الخدمة
إصلاحsystemctl restartactive

مختبر: عملية وخدمة

شغّل sleep 300 & ثم أوقفه بـ kill %1 وتحقق بـ jobs. بعدها اقرأ حالة cron وآخر عشرة أسطر من سجلها، وحدد السطر الذي يخبرك هل الخدمة مفعّلة عند الإقلاع.

مهمة عملية

اكتب في ملاحظاتك ناتج systemctl is-active cron وsystemctl is-enabled cron، ثم استخرج بـ journalctl -u cron --since "1 hour ago" عدد أسطر السجل في الساعة الأخيرة باستخدام wc -l.

اختبر فهمك (1/3)

ما الفرق بين sudo systemctl start nginx و sudo systemctl enable nginx؟

اختبر فهمك (2/3)

تريد رسائل خدمة اسمها shop-api خلال الساعة الأخيرة. أي أمر؟

اختبر فهمك (3/3)

ضغطت Ctrl+Z على خادم تطوير ثم أعدت تشغيله فظهرت Address already in use. لماذا؟

للاستزادة: man7: ps · man7: signal — أنواع الإشارات وسلوكها · man7: systemctl · man7: journalctl · GNU Bash: Job Control Basics

8. مقدمة SSH — الاتصال الآمن بخادم

الهدف: تتصل بجهاز بعيد عبر SSH، وتنشئ زوج مفاتيح وتستخدمه بدل كلمة المرور، وتختصر اتصالاتك بملف ‎~/.ssh/config.

SSH (Secure Shell) يفتح لك سطر أوامر على جهاز بعيد، كخادم افتراضي أو Raspberry Pi أو جهاز مختبر في الشبكة، عبر اتصال مشفّر لا يستطيع أحد في الطريق قراءة ما يمر فيه. الصيغة الأساسية ssh user@host، ومع منفذ غير المنفذ الافتراضي 22 تضيف -p 2222. يتناول هذا الدرس جانب المستخدم اليومي: الاتصال والمفاتيح ونقل الملفات، أما ضبط إعدادات خادم SSH نفسه وتقويته فموضوع مسار الاستضافة السحابية.

أول اتصال وبصمة الخادم. عند الاتصال بخادم لأول مرة يعرض ssh بصمة (Fingerprint) مفتاح الخادم ويسألك Are you sure you want to continue connecting (yes/no/[fingerprint])?. بعد الموافقة تُحفظ البصمة في ~/.ssh/known_hosts، وفي كل اتصال لاحق يقارن ssh ما يراه بما حفظه. إن تغيّرت يوماً ظهرت رسالة بارزة تبدأ بـ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!. قد يكون السبب بريئاً كإعادة تثبيت الخادم، وقد يكون محاولة اعتراض، لذلك لا تحذف السطر القديم بـ ssh-keygen -R host إلا بعد أن تعرف السبب من مسؤول الخادم.

كلمة المرور تُخمَّن وتُعاد استخدامها في أكثر من مكان، أما المفاتيح فتقوم على زوج مترابط: مفتاح خاص يبقى على جهازك وحدك، ومفتاح عام تضعه على كل خادم تريد دخوله. يثبت جهازك هويته بالمفتاح الخاص دون أن يرسله عبر الشبكة أصلاً، ومن يطّلع على المفتاح العام لا يستطيع الدخول به.

إنشاء الزوج (المسارات والبصمة توضيحية، ونص الأسئلة قد يختلف قليلاً بين الإصدارات):

$ ssh-keygen -t ed25519 -C "sara@laptop"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/sara/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Your identification has been saved in /home/sara/.ssh/id_ed25519
Your public key has been saved in /home/sara/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:Xy3...illustrative...Q9 sara@laptop

الملف id_ed25519 هو المفتاح الخاص، وid_ed25519.pub هو العام. عبارة المرور (Passphrase) تشفّر المفتاح الخاص على القرص، فلو سُرق حاسوبك لم يُستخدم المفتاح مباشرة؛ ولكي لا تكتبها في كل اتصال يحفظها وكيل SSH بعد ssh-add طوال الجلسة.

لنقل المفتاح العام إلى الخادم نفّذ ssh-copy-id user@host، فيضيفه إلى ملف ~/.ssh/authorized_keys عند ذلك المستخدم. وإن لم تتوفر الأداة، كما في بعض بيئات Windows، فالصق محتوى ملف ‎.pub سطراً واحداً في ذلك الملف يدوياً. ويحرص ssh على الصلاحيات: المجلد ~/.ssh يُفضَّل أن يكون 700، والمفتاح الخاص 600، وإن كان المفتاح مقروءاً لغيرك رفض استخدامه برسالة WARNING: UNPROTECTED PRIVATE KEY FILE!، وحلها chmod 600 ~/.ssh/id_ed25519.

ملف الإعداد يوفر عليك الحفظ. بدل كتابة العنوان والمنفذ والمستخدم في كل مرة، أنشئ ~/.ssh/config (العنوان من نطاق مخصص للأمثلة):

Host lab
    HostName 203.0.113.10
    User sara
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

بعدها يكفي ssh lab، ويعمل الاسم المختصر مع scp أيضاً: scp report.pdf lab:~/ ينسخ ملفاً إلى مجلدك الرئيسي على الخادم، وscp lab:~/app.log . يجلب ملفاً إلى مجلدك الحالي. النقطتان بعد الاسم تعنيان «مساراً على الجهاز البعيد».

قائمة فحص حين يرفض الخادم دخولك برسالة Permission denied (publickey): هل اسم المستخدم صحيح؟ هل مفتاحك العام موجود في authorized_keys عند هذا المستخدم بالذات؟ هل صلاحيات المفتاح الخاص 600؟ ثم أعد المحاولة بـ ssh -v lab لترى بالتفصيل أي المفاتيح عرضها جهازك وأين توقف التفاوض. والمفاتيح نفسها تعمل مع GitHub: بعد إضافة المفتاح العام في إعدادات حسابك اختبر بـ ssh -T git@github.com، فيرحب بك باسمك ويخبرك أنه لا يوفر صدفة. وتذكّر أن الملف بلا ‎.pub لا يُرسل لأحد ولا يُرفع إلى أي مستودع.

  • تغيّر بصمة الخادم إنذار يستحق التحقق قبل حذف السطر من known_hosts.
  • المفتاح العام (.pub) يوضع على الخوادم، والخاص لا يغادر جهازك أبداً.
  • المفتاح الخاص يحتاج صلاحيات 600 وإلا رفضه ssh.
  • ملف ~/.ssh/config يختصر الاتصال إلى ssh lab ويعمل مع scp.

مخطط

 [جهازك]                         [الخادم]
 id_ed25519 (خاص، 600)          ~/.ssh/authorized_keys
 id_ed25519.pub ──ssh-copy-id──→ (يُضاف هنا)
 ~/.ssh/config: Host lab → ssh lab

مختبر: مفتاح وملف إعداد

تحقق من وجود زوج مفاتيح وصلاحية 600 للخاص. اكتب كتلة Host تجريبية في ~/.ssh/config لجهاز تملكه (أو localhost إن كان خادم SSH مثبّتاً) وجرّب ssh -v باسمها المختصر.

مهمة عملية

أنشئ مفتاح ed25519 بعبارة مرور إن لم يكن لديك واحد، واعرض بصمته بـ ssh-keygen -lf، ثم اشرح كتابةً لماذا يجوز نشر محتوى ملف ‎.pub ولا يجوز نشر الملف الآخر.

اختبر فهمك (1/3)

أي ملف تضعه على الخادم أو في إعدادات حسابك على GitHub؟

اختبر فهمك (2/3)

أضفت في ~/.ssh/config كتلة Host lab فيها HostName وUser وPort. كيف تتصل الآن؟

اختبر فهمك (3/3)

ظهرت WARNING: UNPROTECTED PRIVATE KEY FILE مع مفتاحك الخاص. ما الحل؟

للاستزادة: OpenSSH: ssh-keygen · OpenSSH: ssh_config · man7: ssh-copy-id · GitHub Docs: Connecting to GitHub with SSH

9. سكربتات Bash — أتمتة المهام

الهدف: تكتب سكربت Bash يتعامل مع الوسائط والأخطاء بأمان باستخدام التنصيص وset -euo pipefail، وتفحصه قبل تشغيله.

السكربت ملف نصي فيه أوامر تُنفَّذ بالترتيب كأنك كتبتها بيدك. سطره الأول يبدأ بـ #! ويسمى shebang، ويخبر النظام أي مفسّر يشغّل الملف عند تنفيذه بـ ./script.sh؛ والصيغة #!/usr/bin/env bash شائعة لأنها تبحث عن bash في مسارات النظام. تحتاج بعدها chmod u+x script.sh مرة واحدة كما رأيت في درس الصلاحيات. أفضل طريقة لتعلّم كتابة السكربتات هي إصلاح سكربت ضعيف، وهذا ما نفعله الآن.

النسخة الأولى: سكربت نسخ احتياطي يعمل في الحالة السعيدة فقط.

#!/bin/bash
SRC=$1
DEST=~/backups/$(date +%F)
mkdir $DEST
cp -r $SRC/* $DEST
echo done

ثلاث مشكلات خطيرة هنا. الأولى: إن شغّلته دون وسيط تصبح قيمة SRC فارغة، فيتحول $SRC/* إلى /*، أي يحاول نسخ كل محتويات الجذر. الثانية: إن كان اسم المجلد فيه مسافة مثل My Docs فإن غياب علامات التنصيص يقسمه إلى كلمتين، فيبحث cp عن مجلدين لا وجود لهما. الثالثة: إن فشل mkdir لأي سبب يكمل السكربت ويطبع done كأن شيئاً لم يحدث، فتظن أن لديك نسخة احتياطية وهي غير موجودة.

النسخة المصححة:

#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
  echo "الاستخدام: $0 <مجلد_المصدر>" >&2
  exit 2
fi
src=$1
dest="$HOME/backups/$(date +%F)"
[[ -d "$src" ]] || { echo "ليس مجلداً: $src" >&2; exit 1; }
mkdir -p "$dest"
cp -r -- "$src"/. "$dest"/
echo "تم النسخ إلى $dest"

السطر set -euo pipefail يجمع ثلاثة خيارات. الخيار -e يوقف السكربت عند أول أمر يفشل بدل أن يكمل فوق نتيجة خاطئة. والخيار -u يعامل استخدام متغير غير معرّف كخطأ، فيكشف الأخطاء الإملائية في أسماء المتغيرات. والخيار -o pipefail يجعل السلسلة تفشل إن فشل أي أمر فيها، بعد أن كان رمز خروجها رمز آخر أمر وحده كما رأيت في درس الأنابيب. لكن -e له استثناءات يجب أن تعرفها: لا يتوقف السكربت إن فشل أمر داخل شرط if، أو في جهة من && و|| قبل آخرها. ومن هنا تأتي مفاجأة شائعة: grep الذي لا يجد تطابقاً يخرج برمز 1 فيوقف السكربت كله، فإن كان عدم التطابق مقبولاً فاكتب grep "x" file || true.

باقي التصحيحات: $# عدد الوسائط، فنرفض التشغيل إن لم يكن وسيطاً واحداً، ونطبع رسالة الاستخدام إلى القناة 2 بـ >&2 ونخرج برمز غير الصفر ليعرف أي برنامج يستدعي السكربت أنه فشل. الاختبار [[ -d "$src" ]] يتحقق أن المسار مجلد موجود. كل متغير محاط بعلامات تنصيص مزدوجة. و"$src"/. تنسخ محتوى المجلد بما فيه الملفات المخفية التي تتجاهلها النجمة، و-- تمنع تفسير اسم يبدأ بشرطة كخيار. وإن احتجت وسيطاً اختيارياً مع -u فاكتب ${2:-default} لتعطيه قيمة افتراضية.

وحين يقبل السكربت عدة ملفات، فإن "$@" بين علامات تنصيص تعطيك كل وسيط كما كُتب حتى لو احتوى مسافات، فتكتب حلقة مثل for f in "$@"; do wc -l -- "$f"; done. أما $* بلا تنصيص فتدمج الوسائط ثم تعيد تقسيمها عند المسافات، فتكسر الأسماء نفسها التي حميتها. ولمعرفة نتيجة أمر بعينه اقرأ $? بعده مباشرة: 0 للنجاح وأي رقم آخر للفشل، وتتغير قيمته مع كل أمر جديد، فاحفظها في متغير إن احتجتها لاحقاً.

افحص السكربت قبل تشغيله بخطوتين: bash -n backup.sh يتحقق من الصياغة دون تنفيذ، وأداة shellcheck (تُثبَّت بـ sudo apt install shellcheck) تقرأ السكربت وتنبهك إلى متغيرات بلا تنصيص وأخطاء شائعة أخرى مع شرح لكل تحذير. ولتنظيف الملفات المؤقتة مهما حدث استعمل trap: بعد tmp=$(mktemp -d) اكتب trap 'rm -rf "$tmp"' EXIT فيُحذف المجلد المؤقت عند خروج السكربت بنجاح أو بفشل. وأخيراً، إن ظهرت /bin/bash^M: bad interpreter فالملف محفوظ بنهايات أسطر Windows (CRLF)؛ غيّرها في محررك إلى LF.

  • set -e يوقف عند الفشل، و-u يرفض المتغيرات غير المعرفة، وpipefail يكشف فشل منتصف السلسلة.
  • ضع كل متغير بين علامات تنصيص مزدوجة مثل "$src" لحماية الأسماء ذات المسافات.
  • تحقق من الوسائط بـ $# واطبع الأخطاء إلى >&2 واخرج برمز غير الصفر.
  • bash -n وshellcheck يكشفان الأخطاء قبل أن يكتشفها التشغيل.

مخطط

#!/usr/bin/env bash
set -euo pipefail
تحقق من $# والمسار
أوامر بمتغيرات "مُنصَّصة"
نجاحرسالةفشل>&2 + exit 1

مختبر: إصلاح سكربت ضعيف

احفظ النسخة الأولى من الدرس باسم backup-v1.sh وشغّلها على مجلد اسمه My Docs (داخل mesue-cli-lab فقط) لترى الكسر، ثم شغّل النسخة المصححة بلا وسيط ومع مجلد غير موجود ولاحظ رسائلها ورمز الخروج بـ echo $?.

مهمة عملية

اكتب backup.sh بالنسخة المصححة، ومرّره على shellcheck حتى لا يبقى تحذير، ثم أثبت بثلاثة تشغيلات أن رمز الخروج 2 بلا وسيط، و1 لمسار غير موجود، و0 عند النجاح.

اختبر فهمك (1/3)

في سكربت يبدأ بـ set -euo pipefail، ماذا يفعل الخيار -u؟

اختبر فهمك (2/3)

السلسلة grep ERROR app.log | wc -l في سكربت بلا pipefail، والملف غير موجود. ما رمز خروجها؟

اختبر فهمك (3/3)

لماذا يُكتب "$src" بين علامتي تنصيص مزدوجتين في السكربت؟

للاستزادة: GNU Bash: The Set Builtin · GNU Bash: Quoting · GNU Bash: Conditional Expressions · ShellCheck — فحص سكربتات الصدفة · LinuxCommand.org: Writing Shell Scripts

10. المشروع النهائي — تنظيم مجلد مشروع Bash

الهدف: تسلّم مجلد مشروع منظماً بسكربت organize.sh قابل لإعادة التشغيل، يدعم وضع التجربة الجافة، ويجمع مهارات المسار كلها.

المطلوب مجلد mesue-cli-project في مجلدك الرئيسي بهذا الهيكل:

mesue-cli-project/
├── README.md          (وصف عربي وطريقة التشغيل)
├── src/
├── docs/
├── scripts/
│   └── organize.sh    (يجهّز الهيكل وينظّف السجلات)
└── logs/
    └── .gitkeep

وظيفة organize.sh أربع نقاط: ينشئ المجلدات الناقصة، ويحذف ملفات السجل الأقدم من ثلاثين يوماً من logs، ويكتب سطراً بتاريخ التشغيل في logs/setup.log، ويدعم الخيار --dry-run الذي يعرض ما سيفعله دون تغيير أي شيء.

السكربت المقترح:

#!/usr/bin/env bash
set -euo pipefail
DRY_RUN=0
if [[ "${1:-}" == "--dry-run" ]]; then DRY_RUN=1; fi
run() {
  if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] $*"; else "$@"; fi
}
cd "$(dirname "$0")/.."
for d in src docs scripts logs; do
  run mkdir -p "$d"
done
run touch logs/.gitkeep
echo "سجلات أقدم من 30 يوماً:"
if [[ -d logs ]]; then find logs -type f -name "*.log" -mtime +30 -print; fi
if [[ $DRY_RUN -eq 0 ]]; then
  find logs -type f -name "*.log" -mtime +30 -delete
  date "+%F %T organize ok" >> logs/setup.log
fi
echo "جاهز: $(pwd)"

اقرأه بعين الدروس السابقة. السطر الثاني من درس السكربتات، و${1:-} تعطي الوسيط الأول قيمة فارغة إن لم يوجد كي لا يعترض الخيار -u. الدالة run هي قلب وضع التجربة الجافة: تستقبل أي أمر في "$@"، فإن كان الوضع مفعّلاً طبعته بدل تنفيذه، وإلا نفّذته كما هو. هذه الفكرة نفسها التي رأيتها في rsync --dry-run وفي مراجعة نتائج find قبل -delete، وصارت الآن جزءاً من أداتك الخاصة. والسطر cd "$(dirname "$0")/.." ينقلك إلى جذر المشروع: $0 مسار السكربت، وdirname يعطي مجلده scripts، و/.. ترفعك درجة؛ فيعمل السكربت صحيحاً سواء شغّلته من داخل المشروع أو من أي مكان آخر. أما الحذف فيمر بالخطوتين اللتين تعلمتهما: طباعة القائمة دائماً، ثم الحذف في الوضع الفعلي وحده. والشرط [[ -d logs ]] قبل find ليس زينة: في أول تجربة جافة على مشروع جديد لا يوجد logs بعد، فيفشل find، ويوقف set -e السكربت كله برمز خطأ.

قابلية إعادة التشغيل (Idempotency) تعني أن تشغيل السكربت مرة أو عشر مرات يترك المشروع في الحالة نفسها، مع سطر سجل جديد في كل تشغيل. يضمنها هنا أن mkdir -p وtouch لا يفشلان إن كان الهدف موجوداً، وأن السجل يُلحق بـ >> ولا يُستبدل. هذه الخاصية هي ما يجعلك تثق بتشغيل السكربت من مهمة مجدولة أو من زميل لا يعرف حالة المجلد.

خطة اختبار من ست خطوات، نفّذها بالترتيب وسجّل نتيجة كل خطوة في README:
1) ./scripts/organize.sh --dry-run يطبع أسطراً تبدأ بـ [dry-run] ولا يُنشئ شيئاً؛ تحقق بـ ls.
2) ./scripts/organize.sh ينشئ الهيكل ويكتب السطر الأول في setup.log.
3) شغّله مرة ثانية فوراً: لا أخطاء، والهيكل كما هو، وسطر سجل ثانٍ.
4) أنشئ سجلاً قديماً تجريبياً بـ touch -d "40 days ago" logs/old.log، ثم شغّل التجربة الجافة وتأكد أنه ظهر في القائمة وبقي موجوداً.
5) شغّل التنفيذ الفعلي وتأكد أن old.log حُذف وأن setup.log بقي.
6) من مجلد آخر نفّذ bash ~/mesue-cli-project/scripts/organize.sh وتأكد أن لا مجلدات أُنشئت في مكانك الحالي.

قبل مشاركة المشروع افحص الأسرار: grep -rniE "password|secret|token|api_key" . --exclude-dir=.git يبحث بلا حساسية لحالة الأحرف عن كلمات تدل على بيانات حساسة، وأي نتيجة تحتاج مراجعة يدوية. وإن استعملت Git فأضف logs/*.log إلى ملف ‎.gitignore كي لا ترفع السجلات، وابقِ ملف ‎.gitkeep لأن Git لا يتتبع المجلدات الفارغة. وإن فشل التشغيل فاقرأ الرسالة قبل أي تعديل: Permission denied تعني أن السكربت بلا صلاحية تنفيذ، وNo such file or directory مع المسار النسبي ./scripts/organize.sh تعني غالباً أنك لست في جذر المشروع.

توسعات اختيارية بعد التسليم: أضف الخيار --help الذي يطبع طريقة الاستخدام ويخرج برمز 0، واجعل أي خيار غير معروف يطبع رسالة إلى >&2 ويخرج برمز 2. ولتشغيله يومياً افتح crontab -e وأضف سطراً مثل 0 9 * * * "$HOME/mesue-cli-project/scripts/organize.sh" >> "$HOME/mesue-cli-project/logs/cron.log" 2>&1 ليعمل كل يوم في التاسعة صباحاً. وتذكّر أن cron يشغّل الأوامر ببيئة محدودة ودون طرفية، فالمسارات الكاملة وقابلية إعادة التشغيل اللتان بنيتهما هنا هما ما يجعل التشغيل المجدول آمناً.

معيار التسليم: السكربت يمر على shellcheck بلا تحذيرات، وخطة الاختبار الست ناجحة، وREADME يشرح بالعربية ما يفعله السكربت وكيف تشغّله بالوضعين، ولقطة من tree أو ls -R تُظهر الهيكل كاملاً، وفحص الأسرار لا يُرجع شيئاً.

  • الدالة run تحوّل أي أمر إلى طباعة في وضع --dry-run وإلى تنفيذ في الوضع العادي.
  • mkdir -p والإلحاق بـ >> يجعلان السكربت قابلاً لإعادة التشغيل بأمان.
  • cd "$(dirname "$0")/.." يجعل السكربت يعمل من جذر المشروع أياً كان مكان تشغيله.
  • افحص الأسرار بـ grep -rniE قبل مشاركة أي مجلد.

مخطط

 organize.sh [--dry-run]
        |
   cd جذر المشروع
        |
  run mkdir -p ...  →  [dry-run] طباعة | تنفيذ
        |
  find logs -mtime +30 -print → (-delete في الوضع الفعلي)
        |
  date >> logs/setup.log

مختبر: العرض النهائي

نفّذ خطة الاختبار الست بالترتيب. أهم لحظة: الخطوة الرابعة، حيث يجب أن يظهر old.log في قائمة التجربة الجافة ويبقى موجوداً بعدها.

مهمة عملية

سلّم mesue-cli-project مع organize.sh وREADME عربي يوثّق نتائج خطة الاختبار الست، ولقطة tree -a للهيكل، وناتج shellcheck scripts/organize.sh خالياً من التحذيرات.

اختبر فهمك (1/3)

ماذا يجب أن يحدث عند تشغيل ./scripts/organize.sh --dry-run؟

اختبر فهمك (2/3)

ما فائدة cd "$(dirname "$0")/.." في أول السكربت؟

اختبر فهمك (3/3)

شغّلت السكربت مرتين متتاليتين. ما النتيجة المتوقعة من سكربت قابل لإعادة التشغيل؟

للاستزادة: GNU Bash: Shell Parameters · man7: find · Git: gitignore · ShellCheck

مراجع رسمية لهذا المسار

سطر الأوامر أصبح أداة يومية

جرّب مسار السحابة لنشر موقعك على VPS، أو Git لإدارة مشروعك من الطرفية.