الأدلة التقنية

تمارين Kata في Linux بثلاثة مستويات مع الحلول

تمارين في سطر أوامر Linux تحلها بيدك دون ذكاء اصطناعي، من قراءة الصلاحيات إلى القرص الذي يمتلئ بسبب ملف محذوف.

تمارين Kata في Linux بثلاثة مستويات مع الحلول

في مقال لماذا تتمرن دون ذكاء اصطناعي؟ شرحنا لماذا تضعف المهارة التي لا تستخدمها، وكيف تتمرن بطريقة Kata وحدك أو مع فريقك، وهذا المقال هو أول مجال نطبق فيه هذه الطريقة: سطر الأوامر في Linux. والسبب أن كل ما يحدث على السيرفر يمر عبره في النهاية، فعندما يمتلئ القرص أو تتوقف مهمة مجدولة أو يرفض سكربت أن يعمل، فأنت تحتاج أن تكتب grep وawk وfind من ذاكرتك وتفهم ما تقرؤه في المخرج، وليس أن تنسخ أمراً لا تعرف ماذا يفعل.

وطريقة الاستخدام قصيرة: أغلق أي مساعد ذكاء اصطناعي، وافتح طرفية Terminal على المختبر الذي سوف نجهزه بعد قليل، واقرأ التمرين، واضبط مؤقتاً على الوقت المقترح، ثم اكتب إجابتك كاملة في ملف قبل أن تفتح أي شيء. المسموح لك هو ما تجده على السيرفر نفسه، أي man و--help، وإذا علقت فافتح التلميح Hint ثم حاول مرة أخرى، وبعدها فقط افتح الحل Answer وقارنه بإجابتك سطراً بسطر، وفي النهاية سجل الوقت والأخطاء في جدول المتابعة وارجع إلى التمرين بعد أسبوع.

وسوف نناقش في هذا المقال ما يلي:

  • تجهيز مختبر Lab واحد في Docker تحل فيه كل التمارين، مع ملف سجل Access Log حقيقي الشكل تعمل عليه.
  • المستوى الأول: التنقل والمسارات، وإنشاء المجلدات، وقراءة الصلاحيات، والعد في السجل، والتوجيه Redirection، ورمز الخروج Exit Code.
  • المستوى الثاني: معالجة النصوص بالأنابيب Pipes وawk وsed، وصلاحية المجلد، ومساحة القرص، والإشارات Signals.
  • المستوى الثالث: سكربت يخفي فشله، وملف محذوف يحجز القرص، وcron، وsudo لأمر واحد، وملف خدمة systemd.
  • جدول متابعة تنسخه لتسجل تقدمك، وروابط المقالات التي تشرح كل موضوع بالتفصيل.

والصورة التالية تبين التمارين السبعة عشر موزعة على المستويات الثلاثة:

ثلاثة أعمدة ملونة: في اليمين المستوى الأول Beginner باللون الأخضر من 5 إلى 10 دقائق للتمرين، وفيه التمارين 1 إلى 6: cd والمسارات النسبية وls -laht، وmkdir -p مع الأقواس، وقراءة سطر ls -l و750، والعد بالأمرين grep -c وwc، وstdout وstderr و2>&1، ورموز الخروج 0 و1 و2. وفي الوسط المستوى الثاني Intermediate بالأصفر من 15 إلى 20 دقيقة، وفيه التمارين 7 إلى 12: أكثر العناوين طلباً بالأمرين sort وuniq -c، وأخطاء 5xx لكل مسار بالأمر awk، وsed -i على ملف إعداد، وصلاحية x على المجلد، وdu وfind -size والملف المتناثر، وSIGTERM وtrap وSIGKILL. وفي اليسار المستوى الثالث Advanced بالأحمر من 20 إلى 25 دقيقة، وفيه التمارين 13 إلى 17: set -euo pipefail، وملف محذوف والقرص ما زال ممتلئاً، وأمر يعمل باليد ويفشل في cron، وsudo لأمر واحد فقط، وملف وحدة systemd وjournalctl
كل مستوى يبني على الذي قبله، فابدأ من الأول حتى لو بدا لك سهلاً

تجهيز المختبر Lab وملفات التمارين

كل التمارين تعمل في Container واحد من ال Docker Image الخاص ب PostgreSQL 18، وهو مبني على Debian 13 وفيه bash وcoreutils وprocps جاهزة، ولن نشغل PostgreSQL نفسه، وهذا نفس المختبر الذي استخدمناه في دليل سطر الأوامر في Linux. ولاحظ الخيار --init، فهو يضع عملية صغيرة في مكان العملية رقم 1 PID 1 تجمع العمليات المنتهية، وبدونه سوف ترى في تمرين الإشارات عمليات ميتة Zombies لا علاقة لها بالتمرين، وقد شرحنا السبب في قسم العمليات الميتة والعملية رقم 1:

docker run -d --init --name kata-linux --hostname kata --entrypoint sleep postgres:18 infinity
docker exec -it kata-linux bash

ثم داخل ال Container نثبت الحزم التي تحتاجها تمارين المستوى الثالث، وننشئ مستخدماً اسمه deploy، والمجلدات التي سوف نعمل فيها. ويجدر الإشارة هنا إلى أن ال Docker Image فيه مستخدم اسمه postgres، وسوف نستخدمه في تمارين الصلاحيات على أنه «مستخدم آخر» ليس المالك وليس في المجموعة:

apt-get update && apt-get install -y cron sudo systemd nodejs
useradd -m -s /bin/bash deploy
mkdir -p /srv/app/logs /srv/app/releases /srv/ops /etc/shop /srv/reports

وأهم ملف في التمارين هو سجل الوصول Access Log الخاص بسيرفر ويب، بنفس صيغة السجل المركبة Combined Log Format التي يكتبها Nginx، وفيه 20 طلباً من أربعة عناوين IP من النطاقات المحجوزة للتوثيق. أنشئه كما هو، لأن أرقام الحلول محسوبة عليه:

cat > /srv/app/logs/access.log <<'EOF'
203.0.113.7 - - [06/Oct/2026:10:00:01 +0000] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
198.51.100.23 - - [06/Oct/2026:10:00:03 +0000] "GET /login HTTP/1.1" 200 2048 "-" "Mozilla/5.0"
203.0.113.7 - - [06/Oct/2026:10:00:04 +0000] "POST /login HTTP/1.1" 302 0 "-" "Mozilla/5.0"
192.0.2.55 - - [06/Oct/2026:10:00:09 +0000] "GET /api/orders HTTP/1.1" 500 312 "-" "curl/8.14.1"
203.0.113.7 - - [06/Oct/2026:10:00:12 +0000] "GET /dashboard HTTP/1.1" 200 8192 "-" "Mozilla/5.0"
192.0.2.55 - - [06/Oct/2026:10:00:15 +0000] "GET /api/orders HTTP/1.1" 500 312 "-" "curl/8.14.1"
198.51.100.23 - - [06/Oct/2026:10:00:21 +0000] "POST /login HTTP/1.1" 401 128 "-" "Mozilla/5.0"
198.51.100.23 - - [06/Oct/2026:10:00:22 +0000] "POST /login HTTP/1.1" 401 128 "-" "Mozilla/5.0"
198.51.100.23 - - [06/Oct/2026:10:00:23 +0000] "POST /login HTTP/1.1" 401 128 "-" "Mozilla/5.0"
203.0.113.7 - - [06/Oct/2026:10:01:02 +0000] "GET /api/orders HTTP/1.1" 200 1536 "-" "Mozilla/5.0"
192.0.2.55 - - [06/Oct/2026:10:01:05 +0000] "GET /api/orders HTTP/1.1" 502 157 "-" "curl/8.14.1"
203.0.113.40 - - [06/Oct/2026:10:01:30 +0000] "GET /static/app.js HTTP/1.1" 200 40960 "-" "Mozilla/5.0"
203.0.113.40 - - [06/Oct/2026:10:01:30 +0000] "GET /static/app.css HTTP/1.1" 200 10240 "-" "Mozilla/5.0"
198.51.100.23 - - [06/Oct/2026:10:02:10 +0000] "POST /login HTTP/1.1" 401 128 "-" "Mozilla/5.0"
192.0.2.55 - - [06/Oct/2026:10:02:44 +0000] "GET /api/reports HTTP/1.1" 504 160 "-" "curl/8.14.1"
203.0.113.7 - - [06/Oct/2026:10:03:00 +0000] "GET /api/reports HTTP/1.1" 200 2560 "-" "Mozilla/5.0"
198.51.100.23 - - [06/Oct/2026:10:03:15 +0000] "GET /wp-login.php HTTP/1.1" 404 153 "-" "Mozilla/5.0"
203.0.113.40 - - [06/Oct/2026:10:04:01 +0000] "GET /api/orders HTTP/1.1" 500 312 "-" "Mozilla/5.0"
203.0.113.7 - - [06/Oct/2026:10:05:30 +0000] "GET /logout HTTP/1.1" 302 0 "-" "Mozilla/5.0"
198.51.100.23 - - [06/Oct/2026:10:06:00 +0000] "GET /.env HTTP/1.1" 404 153 "-" "Mozilla/5.0"
EOF

ولكي تقرأ السطر بسرعة في التمارين، هذه أرقام الحقول Fields كما يراها awk عندما يقسم السطر على المسافات: الحقل الأول $1 عنوان IP، والرابع $4 الوقت مع القوس المربع، والسادس $6 طريقة الطلب Method مع علامة التنصيص، والسابع $7 المسار Path، والتاسع $9 رمز الحالة Status Code، والعاشر $10 حجم الرد بالبايت.

وكل تمرين يذكر في بدايته الأوامر التي تجهز ملفاته إن احتاج ملفات إضافية، وكل مخرج في الحلول هو المخرج الحقيقي من هذا المختبر، وتغير التواريخ وأرقام العمليات PIDs عندك أمر طبيعي.

المستوى الأول: مبتدئ Beginner

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

التمرين 1: التنقل بالمسار النسبي والعودة إلى المجلد السابق

جهز الملفات أولاً بهذه الأوامر، فهي تضيف نسخاً قديمة من السجل وملفاً مخفياً Hidden File وتضبط تواريخها:

cd /srv/app/logs && touch -d "2026-10-06 00:00" .last-rotate && cp access.log access.log.1 && gzip -c access.log > access.log.2.gz
touch -d "2026-10-05 00:00" access.log.2.gz && touch -d "2026-10-06 00:00" access.log.1 && touch -d "2026-10-06 10:06" access.log
cd /srv/app/releases

أنت الآن في المجلد /srv/app/releases. اكتب الأوامر التي تنفذ ما يلي بالترتيب: انتقل إلى /srv/app/logs بمسار نسبي Relative Path واطبع مكانك في نفس السطر، ثم ارجع إلى المجلد السابق بأقصر أمر ممكن، ثم اعرض كل ملفات /srv/app/logs بما فيها المخفية مع أحجام يقرؤها الإنسان والأحدث أولاً، ثم ارجع إلى مجلد المستخدم Home Directory دون أن تكتب مساره.

الوقت المقترح: 5 دقائق.

تلميح Hint

النقطتان .. تعني المجلد الأب Parent Directory، والأمر cd يقبل شرطة واحدة تعني «المكان السابق»، وcd وحده يعيدك إلى مجلدك. وللعرض تحتاج أربعة خيارات للأمر ls: الكل، والقائمة الطويلة، والحجم المقروء، والترتيب بالوقت.

الحل Answer

الأوامر والمخرج سوف يكون كما يلي:

$ pwd
/srv/app/releases
$ cd ../logs && pwd
/srv/app/logs
$ cd -
/srv/app/releases
$ cd /srv/app/logs
$ ls -laht
total 20K
drwxr-xr-x 2 root root 4.0K Oct  7 07:45 .
drwxr-xr-x 4 root root 4.0K Oct  7 07:41 ..
-rw-r--r-- 1 root root 1.9K Oct  6 10:06 access.log
-rw-r--r-- 1 root root 1.9K Oct  6 00:00 access.log.1
-rw-r--r-- 1 root root    0 Oct  6 00:00 .last-rotate
-rw-r--r-- 1 root root  394 Oct  5 00:00 access.log.2.gz
$ cd
$ pwd
/root

في الأوامر أعلاه لاحظ التالي:

  • ../logs تعني اصعد مجلداً واحداً إلى /srv/app ثم ادخل logs، و&& تشغل pwd فقط إذا نجح الانتقال.
  • cd - تطبع المجلد الذي رجعت إليه، وهي مفيدة جداً عندما تتنقل بين مجلد السجلات ومجلد الإعداد عشرات المرات.
  • الخيار -a يظهر الملفات التي تبدأ بنقطة مثل .last-rotate، و-h يحول 1900 بايت إلى 1.9K، و-t يرتب بوقت التعديل الأحدث أولاً. وإذا كتبت -A بدل -a فسوف يختفي السطران . و...
  • الملف .last-rotate جاء قبل access.log.2.gz لأن تاريخه أحدث، وليس بسبب اسمه.

للتفصيل: المسار المطلق والمسار النسبي والتنقل بين المجلدات في دليل سطر الأوامر.

التمرين 2: شجرة مجلدات كاملة في أمر واحد

مشروع جديد اسمه shop يحتاج هذه المجلدات تحت /srv/shop: المجلد src وفيه api وweb، والمجلد tests، والمجلد docs. أنشئها كلها بأمر mkdir واحد، ثم اعرض كل المجلدات التي أنشأتها مرتبة. وبعدها أجب: ماذا يحدث إذا كتبت mkdir /srv/blog/posts والمجلد /srv/blog غير موجود؟

الوقت المقترح: 5 دقائق.

تلميح Hint

ال Shell يوسع الأقواس المعقوفة Brace Expansion قبل أن يشغل الأمر، فالنص a/{b,c} يصبح a/b a/c، ويمكن أن تضع أقواساً داخل أقواس. وتحتاج خياراً في mkdir ينشئ الآباء الناقصين، وfind مع نوع المجلد.

الحل Answer
$ mkdir -p /srv/shop/{src/{api,web},tests,docs}
$ find /srv/shop -type d | sort
/srv/shop
/srv/shop/docs
/srv/shop/src
/srv/shop/src/api
/srv/shop/src/web
/srv/shop/tests
$ echo /srv/shop/{src/{api,web},tests,docs}
/srv/shop/src/api /srv/shop/src/web /srv/shop/tests /srv/shop/docs
$ mkdir /srv/blog/posts
mkdir: cannot create directory ‘/srv/blog/posts’: No such file or directory

في الأوامر أعلاه لاحظ التالي:

  • الأمر echo مع نفس الأقواس يريك ما سوف يصل إلى mkdir فعلاً، وهي عادة جيدة قبل أي أمر فيه توسيع، خصوصاً مع rm.
  • الخيار -p ينشئ /srv/shop و/srv/shop/src تلقائياً، ولا يعطي خطأ إذا كان المجلد موجوداً، لذلك تجده في كل سكربت تجهيز.
  • بدون -p يرفض mkdir إنشاء posts لأن الأب /srv/blog غير موجود، ويخرج برمز خروج 1.

للتفصيل: العمل مع الملفات والمجلدات في دليل سطر الأوامر، وقسم Brace Expansion في دليل bash.

التمرين 3: اقرأ سطر ls -l قبل أن تشغل السكربت

جهز الملف بهذه الأوامر:

printf '#!/bin/bash\necho "backup started by $(whoami)"\n' > /srv/ops/backup.sh
chown deploy:www-data /srv/ops/backup.sh && chmod 750 /srv/ops/backup.sh
cd /srv/ops && ls -l backup.sh

والسطر الذي سوف تراه هو:

-rwxr-x--- 1 deploy www-data 47 Oct  6 10:00 backup.sh

دون أن تشغل شيئاً، أجب عن أربعة أسئلة: هل يستطيع المستخدم deploy تشغيل السكربت؟ وهل يستطيع المستخدم www-data تشغيله؟ وهل يستطيع www-data تعديله؟ وماذا يحدث إذا حاول المستخدم postgres قراءته؟ ثم اكتب الصلاحيات بالأرقام Octal، وبعدها تحقق من إجاباتك بالأمر su.

الوقت المقترح: 10 دقائق.

تلميح Hint

اقسم rwxr-x--- إلى ثلاث مجموعات من ثلاثة أحرف: المالك Owner ثم المجموعة Group ثم الآخرون Others، وr=4 وw=2 وx=1. وللتجربة: su deploy -c ./backup.sh، والمستخدم www-data ليس له Shell لذلك أضف -s /bin/bash.

الحل Answer

المالك deploy له rwx أي 7، والمجموعة www-data لها r-x أي 5، والآخرون ليس لهم شيء أي 0، فالصلاحيات 750. وهذا يعني أن deploy يقرأ ويعدل ويشغل، وwww-data يقرأ ويشغل ولا يعدل، وpostgres لا يستطيع حتى القراءة. والتحقق كما يلي:

$ ls -l backup.sh
-rwxr-x--- 1 deploy www-data 47 Oct  6 10:00 backup.sh
$ stat -c '%A %a %U %G' backup.sh
-rwxr-x--- 750 deploy www-data
$ su deploy -c ./backup.sh
backup started by deploy
$ su -s /bin/bash www-data -c ./backup.sh
backup started by www-data
$ su -s /bin/bash www-data -c 'echo "# edit" >> backup.sh'
bash: line 1: backup.sh: Permission denied
$ su postgres -c 'cat backup.sh'
cat: backup.sh: Permission denied

في المخرج أعلاه لاحظ التالي:

  • الأمر stat -c '%a' يعطيك الرقم مباشرة، فلا تحتاج أن تحسبه في كل مرة على السيرفر، ولكن احسبه بيدك في التمرين.
  • السكربت يحتاج r مع x لكي يعمل، لأن bash يقرأ الملف ليشغله، ولذلك صلاحية --x وحدها لا تكفي للسكربت وتكفي للبرنامج المترجم Binary.
  • النواة Kernel تفحص جزءاً واحداً فقط: إذا كنت المالك طبقت صلاحيات المالك، وإلا إذا كنت في المجموعة طبقت صلاحيات المجموعة، وإلا طبقت صلاحيات الآخرين.

للتفصيل: قراءة سطر ls -l وترتيب الفحص في النواة في دليل الصلاحيات.

التمرين 4: عد الطلبات في السجل

من الملف /srv/app/logs/access.log أجب بأمر واحد لكل سؤال: كم طلباً في السجل؟ وكم طلباً رجع برمز الحالة 401؟ وكم طلباً ذهب إلى المسار /login بالضبط؟ وكم عنوان IP مختلفاً أرسل طلبات؟ واكتب الرقم الذي تتوقعه لكل سؤال قبل أن تشغل الأمر.

الوقت المقترح: 10 دقائق.

تلميح Hint

wc -l يعد الأسطر، وgrep -c يعد الأسطر التي فيها النص، ولكن انتبه إلى أن grep يبحث عن النص في أي مكان من السطر. وللعناوين المختلفة اطبع الحقل الأول ثم استخدم sort -u.

الحل Answer
$ wc -l access.log
20 access.log
$ grep -c ' 401 ' access.log
4
$ awk '$9 == 401' access.log | wc -l
4
$ grep -c 'login' access.log
7
$ grep -c '/login ' access.log
6
$ awk '{print $1}' access.log | sort -u | wc -l
4

في المخرج أعلاه لاحظ التالي:

  • السؤال الثالث فيه الفخ: grep -c 'login' أعطى 7 لأنه عد أيضاً الطلب إلى /wp-login.php، وهو طلب من برنامج يبحث عن WordPress وليس محاولة دخول إلى تطبيقك. والإجابة الصحيحة 6، وتحصل عليها بنص أدق فيه المسافة بعد المسار، أو بشرط على الحقل نفسه awk '$7 == "/login"'.
  • المسافتان حول 401 في ' 401 ' تمنعان أن يطابق grep رقماً مثل 4012 في حقل الحجم، والشرط $9 == 401 في awk أدق لأنه يقارن الحقل التاسع فقط.
  • sort -u يرتب ويحذف المكرر في خطوة واحدة، وهو نفس sort | uniq.

للتفصيل: القنوات الثلاث والتوجيه والأنابيب في دليل سطر الأوامر، وإذا أردت التعمق في أنماط البحث فدليل التعبيرات النمطية Regex.

التمرين 5: افصل الأخطاء عن المخرج

الأمر ls /srv/app/logs/access.log /srv/app/missing.log يطبع سطراً ناجحاً وسطر خطأ. اكتب ثلاثة أوامر: الأول يحفظ المخرج العادي في out.txt والأخطاء في err.txt، والثاني يحفظ الاثنين معاً في all.txt، والثالث يخفي رسالة الخطأ تماماً ويطبع رمز الخروج. ثم أجب: ماذا يحدث إذا كتبت 2>&1 > wrong.txt بهذا الترتيب؟

الوقت المقترح: 10 دقائق.

تلميح Hint

المخرج العادي stdout رقمه 1 والأخطاء stderr رقمها 2، و2>&1 تعني «اجعل 2 يذهب إلى حيث يذهب 1 الآن». وال Shell يقرأ التوجيهات من اليسار إلى اليمين.

الحل Answer
$ ls /srv/app/logs/access.log /srv/app/missing.log > out.txt 2> err.txt
$ cat out.txt
/srv/app/logs/access.log
$ cat err.txt
ls: cannot access '/srv/app/missing.log': No such file or directory
$ ls /srv/app/logs/access.log /srv/app/missing.log > all.txt 2>&1
$ cat all.txt
ls: cannot access '/srv/app/missing.log': No such file or directory
/srv/app/logs/access.log
$ ls /srv/app/logs/access.log /srv/app/missing.log 2>&1 > wrong.txt
ls: cannot access '/srv/app/missing.log': No such file or directory
$ cat wrong.txt
/srv/app/logs/access.log
$ ls /srv/app/missing.log 2> /dev/null; echo "exit=$?"
exit=2

في المخرج أعلاه لاحظ التالي:

  • في > all.txt 2>&1 يتوجه 1 إلى الملف أولاً، ثم يتوجه 2 إلى نفس المكان، فيدخل الاثنان في الملف.
  • في الترتيب المعكوس 2>&1 > wrong.txt يتوجه 2 إلى حيث كان 1 في تلك اللحظة، أي الشاشة، ثم يتوجه 1 وحده إلى الملف، لذلك ظهر الخطأ على الشاشة وبقي خارج wrong.txt. وهذا خطأ شائع في سطور cron، فتظن أنك تحفظ الأخطاء وهي تضيع.
  • سطر الخطأ جاء قبل السطر الناجح في all.txt لأن ls يطبع الأخطاء فوراً ثم يطبع النتائج المرتبة في النهاية.
  • إخفاء الخطأ في /dev/null لا يغير رمز الخروج، فما زال 2، والسكربت الذي يفحص $? سوف يعرف أن الأمر فشل.

للتفصيل: القنوات الثلاث والتوجيه والأنابيب في دليل سطر الأوامر، وقسم Redirections في دليل bash الذي يشرح هذا الترتيب بنفس المثال.

التمرين 6: هل وجد grep ما تبحث عنه؟

دون أن تشغل شيئاً، اكتب رمز الخروج Exit Code الذي تتوقعه لكل أمر من الأوامر الثلاثة التالية، ثم اكتب ما يطبعه الأمران الأخيران:

cd /srv/app/logs
grep -q '/api/orders' access.log; echo $?
grep -q '/admin' access.log; echo $?
grep -q '/admin' access.lg; echo $?
grep -q ' 504 ' access.log && echo "found 504" || echo "no 504"
grep -q '/admin' access.log && echo "found admin" || echo "no admin"

الوقت المقترح: 5 دقائق.

تلميح Hint

grep له ثلاث نتائج وليس اثنتين: وجد، ولم يجد، وحدث خطأ. والخيار -q يمنع الطباعة ولا يغير رمز الخروج. وانظر جيداً إلى اسم الملف في الأمر الثالث.

الحل Answer
$ grep -q '/api/orders' access.log; echo $?
0
$ grep -q '/admin' access.log; echo $?
1
$ grep -q '/admin' access.lg; echo $?
grep: access.lg: No such file or directory
2
$ grep -q ' 504 ' access.log && echo "found 504" || echo "no 504"
found 504
$ grep -q '/admin' access.log && echo "found admin" || echo "no admin"
no admin

تقول صفحة grep في الدليل إن رمز الخروج 0 إذا وجد سطراً، و1 إذا لم يجد، و2 إذا حدث خطأ. والفرق بين 1 و2 مهم في السكربت: «لم أجد كلمة error في السجل» نتيجة جيدة، و«لم أستطع فتح السجل» مشكلة يجب أن توقف السكربت، فإذا عاملت الاثنين على أنهما «ليس صفراً» فسوف تخلط بينهما. ولاحظ أن A && B || C ليست if كاملة، فإذا فشل B نفسه فسوف يعمل C أيضاً، وهي مقبولة هنا لأن echo لا يفشل.

للتفصيل: رمز الخروج Exit Code في دليل سطر الأوامر.

المستوى الثاني: متوسط Intermediate

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

التمرين 7: أكثر ثلاثة عناوين IP طلباً

من سجل الوصول، اكتب أنبوباً Pipeline واحداً يطبع أكثر ثلاثة عناوين IP أرسلت طلبات، مع عدد طلبات كل عنوان، مرتبة من الأكثر. وبعدها اكتب أنبوباً ثانياً يطبع عدد الطلبات لكل رمز حالة Status Code. وقبل أن تكتب أي شيء أجب: لماذا لا يكفي uniq -c وحده دون sort؟

الوقت المقترح: 15 دقيقة.

تلميح Hint

الترتيب المعروف هو: اطبع الحقل، ثم sort، ثم uniq -c، ثم sort -rn، ثم head. وuniq لا يدمج إلا الأسطر المتكررة المتجاورة.

الحل Answer

أولاً لنرى ماذا يحدث دون sort، ثم الحل الصحيح:

$ awk '{print $1}' access.log | uniq -c
      1 203.0.113.7
      1 198.51.100.23
      1 203.0.113.7
      1 192.0.2.55
      1 203.0.113.7
      1 192.0.2.55
      3 198.51.100.23
      1 203.0.113.7
      1 192.0.2.55
      2 203.0.113.40
      1 198.51.100.23
      1 192.0.2.55
      1 203.0.113.7
      1 198.51.100.23
      1 203.0.113.40
      1 203.0.113.7
      1 198.51.100.23
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -3
      7 198.51.100.23
      6 203.0.113.7
      4 192.0.2.55
$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
      7 200
      4 401
      3 500
      2 404
      2 302
      1 504
      1 502

في المخرج أعلاه لاحظ التالي:

  • دون sort ظهر العنوان 198.51.100.23 في خمسة أسطر منفصلة، لأن uniq يقارن كل سطر بالسطر الذي قبله فقط، فالرقم 3 الذي ظهر هو الطلبات الثلاثة المتتالية فقط وليس المجموع.
  • sort -rn يرتب بالأرقام -n وبالعكس -r، ودون -n سوف يرتب النص حرفاً حرفاً فيضع 10 قبل 9.
  • العنوان الأول أرسل 7 طلبات، منها أربع محاولات دخول فاشلة 401 وطلب إلى /.env وطلب إلى /wp-login.php، وهذا شكل من يبحث عن ثغرات أو يجرب كلمات مرور، وهو ما تبحث عنه في السجل الحقيقي.

للتفصيل: الأنابيب Pipes في دليل سطر الأوامر، وأسئلة لا يجيب عنها السجل النصي في دليل السجلات.

التمرين 8: أخطاء 5xx، أي مسار وكم نسبتها؟

وصلك بلاغ أن الموقع يعطي أخطاء. من سجل الوصول وباستخدام awk أجب عن أربعة أسئلة: ما المسارات التي رجعت بأخطاء السيرفر 5xx، وكم مرة لكل رمز ومسار؟ وما نسبة أخطاء 5xx من كل الطلبات؟ وكم بايت أرسل السيرفر لكل عنوان IP؟ وكم طلباً وصل بين 10:01:00 و10:03:00؟

الوقت المقترح: 20 دقيقة.

تلميح Hint

في awk تكتب الشرط قبل القوس $9 >= 500 {…}، والمتغير NR عدد الأسطر المقروءة، والكتلة END تعمل بعد آخر سطر، والمصفوفة Array تجمع بمفتاح مثل bytes[$1] += $10. وللوقت قارن الحقل الرابع كنص.

الحل Answer
$ awk '$9 >= 500 {print $9, $7}' access.log | sort | uniq -c | sort -rn
      3 500 /api/orders
      1 504 /api/reports
      1 502 /api/orders
$ awk '$9 >= 500 {n++} END {printf "%d of %d = %.1f%%\n", n, NR, 100*n/NR}' access.log
5 of 20 = 25.0%
$ awk '{bytes[$1] += $10} END {for (ip in bytes) print bytes[ip], ip}' access.log | sort -rn
51512 203.0.113.40
17408 203.0.113.7
2866 198.51.100.23
941 192.0.2.55
$ awk '$4 >= "[06/Oct/2026:10:01:00" && $4 < "[06/Oct/2026:10:03:00"' access.log | wc -l
6

في الأوامر أعلاه لاحظ التالي:

  • المسار /api/orders وحده فيه 4 من 5 أخطاء، وهذا أول مكان تبحث فيه، ولاحظ أن نفس المسار نجح مرة واحدة برمز 200، فالمشكلة ليست أن المسار غير موجود.
  • %% في printf تطبع علامة النسبة نفسها، وfor (ip in bytes) لا يضمن أي ترتيب، لذلك رتبنا بعده بالأمر sort -rn.
  • مقارنة الوقت كنص تعمل هنا لأن كل الأسطر في نفس اليوم ونفس الشهر، ولكنها تخطئ إذا امتد السجل على أكثر من شهر، لأن Oct تأتي قبل Sep في ترتيب الحروف. وفي هذه الحالة حول الوقت إلى رقم، أو اكتب السجل بصيغة ISO 8601 من البداية كما شرحنا في دليل السجلات.
  • هذا التمرين بالضبط هو السبب الذي يجعلنا ننصح بالسجلات المنظمة Structured Logs: كل هذه الأسئلة تصبح استعلاماً واحداً على حقول لها أسماء.

للتفصيل: السجلات المنظمة Structured Logs في دليل السجلات، ودليل gawk الرسمي للغة awk.

التمرين 9: عدل ملف الإعداد بأمر sed دون أن تكسر سطراً آخر

جهز ملف الإعداد بهذا الأمر:

cat > /etc/shop/app.conf <<'EOF'
# shop service
port=8080
debug=true
# the payments service, do not change
payments_url=http://127.0.0.1:8080/pay
#port=9090
EOF

المطلوب بأمر sed واحد يعدل الملف في مكانه In-place: غير منفذ الخدمة إلى 8081، واجعل debug=false، واحتفظ بنسخة احتياطية من الملف الأصلي، ولا تلمس عنوان خدمة الدفع ولا السطر المعلق Comment. ثم اعرض الفرق بين النسختين.

الوقت المقترح: 15 دقيقة.

تلميح Hint

جرب sed 's/8080/8081/' app.conf دون -i وانظر أي الأسطر تغيرت. والعلامة ^ تعني بداية السطر و$ نهايته، و-i.bak يحفظ نسخة قبل التعديل، و-e تسمح بأكثر من أمر استبدال.

الحل Answer

الحل البسيط s/8080/8081/ يغير سطرين وليس سطراً واحداً، وهذا ما يكشفه diff قبل أن تكتب على الملف:

$ sed 's/8080/8081/' app.conf | diff app.conf -
2c2
< port=8080
---
> port=8081
5c5
< payments_url=http://127.0.0.1:8080/pay
---
> payments_url=http://127.0.0.1:8081/pay
$ sed -i.bak -e 's/^port=8080$/port=8081/' -e 's/^debug=true$/debug=false/' app.conf
$ diff app.conf.bak app.conf
2,3c2,3
< port=8080
< debug=true
---
> port=8081
> debug=false
$ grep -n '^port=' app.conf
2:port=8081

في الأوامر أعلاه لاحظ التالي:

  • النمط ^port=8080$ يطابق السطر كاملاً فقط، فلا يلمس payments_url الذي فيه نفس الرقم، ولا السطر #port=9090 لأنه يبدأ بالعلامة #.
  • تشغيل sed دون -i وتمرير مخرجه إلى diff هو طريقة آمنة لترى التغيير قبل تنفيذه، و- في diff تعني اقرأ من الأنبوب.
  • -i.bak في GNU sed يكتب النسخة الاحتياطية app.conf.bak، وفي sed الخاص بأنظمة BSD وmacOS تكتب -i '' بصيغة مختلفة، فانتبه إذا نسخت السكربت بين الأنظمة.

للتفصيل: دليل GNU sed الرسمي، ودليل التعبيرات النمطية Regex.

التمرين 10: الملف مسموح والمجلد يمنعك

جهز المجلد بهذه الأوامر:

echo "orders: 128" > /srv/reports/today.txt
chmod 644 /srv/reports/today.txt && chmod 744 /srv/reports

المستخدم postgres يستطيع أن يرى اسم الملف بالأمر ls /srv/reports، والملف نفسه صلاحياته 644 أي يقرؤه الجميع، ومع ذلك يحصل على Permission denied عندما يكتب cat /srv/reports/today.txt. اشرح السبب، ثم أصلحه بأقل صلاحية ممكنة، وأجب: ما الفرق بين المجلد بصلاحية 755 والمجلد بصلاحية 711 للمستخدم postgres؟

الوقت المقترح: 15 دقيقة.

تلميح Hint

على المجلد Directory الحرف r يسمح بقراءة قائمة الأسماء، والحرف x يسمح بالدخول والوصول إلى ما بداخله. وجرب su postgres -c 'ls -l /srv/reports' وانظر إلى علامات الاستفهام.

الحل Answer
$ ls -ld /srv/reports /srv/reports/today.txt
drwxr--r-- 2 root root 4096 Oct  7 07:48 /srv/reports
-rw-r--r-- 1 root root   12 Oct  7 07:48 /srv/reports/today.txt
$ su postgres -c 'ls /srv/reports'
today.txt
$ su postgres -c 'cat /srv/reports/today.txt'
cat: /srv/reports/today.txt: Permission denied
$ su postgres -c 'ls -l /srv/reports'
ls: cannot access '/srv/reports/today.txt': Permission denied
total 0
-????????? ? ? ? ?            ? today.txt
$ chmod 755 /srv/reports
$ su postgres -c 'cat /srv/reports/today.txt'
orders: 128
$ chmod 711 /srv/reports
$ su postgres -c 'ls /srv/reports'
ls: cannot open directory '/srv/reports': Permission denied
$ su postgres -c 'cat /srv/reports/today.txt'
orders: 128

في المخرج أعلاه لاحظ التالي:

  • المجلد drwxr--r-- يعطي الآخرين r فقط، فيقرؤون قائمة الأسماء ولكنهم لا يستطيعون الوصول إلى الملف نفسه ولا إلى معلوماته، لذلك ظهرت علامات الاستفهام في ls -l.
  • صلاحية الملف وحدها لا تكفي، فالنواة تفحص x على كل مجلد في المسار من / إلى الملف قبل أن تنظر إلى صلاحية الملف.
  • مع 755 يرى postgres الأسماء ويقرأ الملف، ومع 711 يقرأ الملف إذا عرف اسمه ولكنه لا يستطيع أن يعرض محتوى المجلد. والإصلاح الصحيح أن تضيف x فقط chmod o+x أو تختار 755، ولا تقم بتغيير الصلاحيات إلى 777، والسبب أنها تسمح لأي مستخدم بحذف الملفات وإضافة ملفات جديدة في المجلد.

للتفصيل: r وw وx على المجلد ولماذا لا تحل chmod 777 أي مشكلة في دليل الصلاحيات.

التمرين 11: من أكل القرص؟ وملف حجمه 2G لا يأخذ شيئاً

جهز البيانات بهذه الأوامر، وهي تنشئ نسخاً احتياطية كبيرة وملفات Cache صغيرة كثيرة وملف قرص لآلة افتراضية:

mkdir -p /srv/data/{uploads,cache,backups,vm}
fallocate -l 120M /srv/data/backups/db-2026-10-05.sql.gz
fallocate -l 80M /srv/data/backups/db-2026-10-06.sql.gz
fallocate -l 30M /srv/data/uploads/video.mp4
for i in $(seq 1 200); do head -c 20K /dev/urandom > /srv/data/cache/c$i.bin; done
truncate -s 2G /srv/data/vm/disk.img

المطلوب: اعرض حجم كل مجلد تحت /srv/data مرتباً من الأصغر إلى الأكبر، ثم اعرض كل ملف أكبر من 50M مع حجمه. وستجد ملفاً حجمه 2.0G في القائمة الثانية ومجلده لا يظهر كبيراً في القائمة الأولى، فاشرح السبب وأثبته بأمر.

الوقت المقترح: 15 دقيقة.

تلميح Hint

du -sh مع sort -h الذي يفهم K وM وG، وfind -size +50M. والفرق بين الحجم الظاهري Apparent Size والمساحة المحجوزة فعلاً على القرص هو مفتاح السؤال الأخير، وانظر في du --help.

الحل Answer
$ du -sh /srv/data/* | sort -h
4.0K	/srv/data/vm
4.0M	/srv/data/cache
31M	/srv/data/uploads
201M	/srv/data/backups
$ find /srv/data -type f -size +50M -exec ls -lh {} +
-rw-r--r-- 1 root root 120M Oct  7 07:48 /srv/data/backups/db-2026-10-05.sql.gz
-rw-r--r-- 1 root root  80M Oct  7 07:48 /srv/data/backups/db-2026-10-06.sql.gz
-rw-r--r-- 1 root root 2.0G Oct  7 07:48 /srv/data/vm/disk.img
$ du -h /srv/data/vm/disk.img
0	/srv/data/vm/disk.img
$ du -h --apparent-size /srv/data/vm/disk.img
2.0G	/srv/data/vm/disk.img

في المخرج أعلاه لاحظ التالي:

  • النسخ الاحتياطية هي التي تأخذ المساحة، وsort -h رتب 4.0M قبل 31M بشكل صحيح، بينما sort -n كان سيضع 201M قبل 31M لأنه لا يفهم الوحدات.
  • truncate -s 2G أنشأ ملفاً متناثراً Sparse File: حجمه الظاهري 2G كما يقول ls وfind، ولكن النواة لم تحجز له أي مساحة على القرص لأنه لم يكتب فيه شيء، لذلك du يقول 0. وهذا شائع في أقراص الآلات الافتراضية وملفات بعض قواعد البيانات.
  • الدرس أن ls يجيب عن سؤال «كم حجم الملف؟» وdu يجيب عن سؤال «كم يأخذ من القرص؟»، وعندما يمتلئ القرص فالسؤال الثاني هو المهم. ولكن انتبه إلى أن الملف المتناثر قد يكبر فعلاً على القرص عندما تكتب فيه الآلة الافتراضية، أو عندما تنسخه بأداة لا تحافظ على الفراغات.

للتفصيل: البحث عن الملفات ومعرفة المساحة في دليل سطر الأوامر، والمساحة ممتلئة في دليل أداء السيرفر.

التمرين 12: أوقف العملية بالطريقة الصحيحة

جهز سكربتين يعملان في الخلفية. الأول worker.sh يحفظ حالته عندما يصله SIGTERM، والثاني stuck.sh يتجاهل SIGTERM تماماً:

cat > /srv/ops/worker.sh <<'EOF'
#!/bin/bash
trap 'echo "$(date +%T) worker: got SIGTERM, saving state"; echo saved > /tmp/worker.state; exit 0' TERM
echo "$(date +%T) worker: started as PID $$"
while true; do sleep 1; done
EOF
printf '#!/bin/bash\ntrap "" TERM\nwhile true; do sleep 1; done\n' > /srv/ops/stuck.sh
chmod 755 /srv/ops/worker.sh /srv/ops/stuck.sh
cd /srv/ops && (./worker.sh > /tmp/worker.log 2>&1 &) && (./stuck.sh &)

المطلوب: اعرض رقمي العمليتين PIDs، ثم أوقف كل عملية بالطريقة التي تعطيها فرصة أن تنهي عملها، وتأكد أن worker.sh حفظ حالته. وإذا رفضت إحداهما التوقف، فاستخدم الحل الأخير وتأكد أنها اختفت. ثم أجب: لماذا لا تبدأ بالإشارة 9 مباشرة؟

الوقت المقترح: 15 دقيقة.

تلميح Hint

pgrep -af يبحث في سطر الأمر كاملاً ويطبعه، وpkill يرسل الإشارة بنفس طريقة البحث. والإشارة الافتراضية لكل من kill وpkill هي TERM رقم 15، وKILL رقم 9 لا يمكن أن تلتقطها العملية أو تتجاهلها.

الحل Answer
$ pgrep -af 'worker.sh|stuck.sh'
539 /bin/bash ./worker.sh
543 /bin/bash ./stuck.sh
$ pkill -TERM -f worker.sh; sleep 2; cat /tmp/worker.log /tmp/worker.state
08:27:27 worker: started as PID 539
08:27:37 worker: got SIGTERM, saving state
saved
$ pkill -TERM -f stuck.sh; sleep 3; pgrep -af stuck.sh
543 /bin/bash ./stuck.sh
$ pkill -KILL -f stuck.sh; sleep 1; pgrep -af stuck.sh; echo "pgrep exit=$?"
pgrep exit=1

في المخرج أعلاه لاحظ التالي:

  • worker.sh استلم SIGTERM فنفذ ال trap: كتب السطر وحفظ الحالة في /tmp/worker.state ثم خرج، وهذا بالضبط ما يفعله أي تطبيق جيد يغلق اتصالاته ويكمل الطلبات الجارية قبل أن يخرج.
  • stuck.sh بقي حياً بعد SIGTERM لأنه يتجاهله، فاستخدمنا KILL، ورمز خروج pgrep صار 1 أي لم يجد شيئاً. ولو بدأت ب kill -9 مع worker.sh لما حفظ حالته أبداً، لأن KILL لا يعطي العملية أي فرصة لتنفذ شيئاً.
  • انتبه إلى أن -f يطابق أي جزء من سطر الأمر، فإذا كان هناك Shell أو محرر مفتوح وفي سطر أمره نفس الكلمة فسوف يصله نفس الإشارة، لذلك شغل pgrep -af أولاً وانظر إلى القائمة قبل pkill.
  • وهذا نفس ما يفعله docker stop: يرسل SIGTERM وينتظر 10 ثوان افتراضياً ثم يرسل SIGKILL.

للتفصيل: الإشارات وإيقاف العمليات بالأمر kill ولماذا لا تبدأ ب kill -9؟ في دليل سطر الأوامر.

المستوى الثالث: متقدم Advanced

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

التمرين 13: سكربت النسخ الاحتياطي يقول نجحت وهو فشل

هذا سكربت نسخ احتياطي يعمل كل ليلة، ونظام المراقبة يعتبره ناجحاً إذا كان رمز خروجه 0:

mkdir -p /srv/backups
cat > /srv/ops/nightly.sh <<'EOF'
#!/bin/bash
BACKUP_DIR=/srv/backups
cat /srv/app/data/orders.db | gzip > "$BACKUP_DIR/orders-$(date +%F).db.gz"
echo "backup done: $(ls -lh $BACKUP_DIR | tail -1)"
EOF
chmod 755 /srv/ops/nightly.sh

شغله وانظر إلى رمز الخروج وإلى الملف الذي أنشأه، ثم اشرح لماذا تعتبره المراقبة ناجحاً، وأصلحه بحيث يفشل السكربت كله إذا فشل أي أمر في الأنبوب، وأثبت ذلك.

الوقت المقترح: 20 دقيقة.

تلميح Hint

رمز خروج الأنبوب Pipeline في bash هو رمز خروج آخر أمر فيه فقط. جرب false | true; echo $?، وابحث في help set عن الخيار pipefail، والمصفوفة PIPESTATUS تحفظ رمز كل أمر في الأنبوب.

الحل Answer
$ ./nightly.sh; echo "exit=$?"
cat: /srv/app/data/orders.db: No such file or directory
backup done: -rw-r--r-- 1 root root 20 Oct  7 07:55 orders-2026-10-07.db.gz
exit=0
$ ls -l /srv/backups
total 4
-rw-r--r-- 1 root root 20 Oct  7 07:55 orders-2026-10-07.db.gz
$ false | true; echo "exit=$?"
exit=0
$ cat /nope | gzip > /dev/null; echo "PIPESTATUS=${PIPESTATUS[*]}"
cat: /nope: No such file or directory
PIPESTATUS=1 0
$ sed -i '2i set -euo pipefail' nightly.sh
$ head -3 nightly.sh
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/srv/backups
$ ./nightly.sh; echo "exit=$?"
cat: /srv/app/data/orders.db: No such file or directory
exit=1

في المخرج أعلاه لاحظ التالي:

  • cat فشل لأن ملف قاعدة البيانات غير موجود، ولكن gzip نجح في ضغط «لا شيء» وكتب ملفاً حجمه 20 بايت، وهو ملف gzip صالح لا يحتوي شيئاً، ثم طبع echo رسالة النجاح، ورمز خروج السكربت هو رمز آخر أمر فيه أي 0. والنتيجة نسخة احتياطية فارغة كل ليلة والمراقبة خضراء، وأنت تكتشف ذلك يوم تحتاج النسخة.
  • pipefail تجعل رمز الأنبوب هو رمز آخر أمر فشل فيه، و-e توقف السكربت عند أول أمر يفشل، و-u تعتبر المتغير غير المعرف خطأ بدل أن يصبح نصاً فارغاً.
  • بعد الإصلاح توقف السكربت قبل رسالة النجاح وخرج ب 1، ولكن لاحظ أن الملف الفارغ ما زال يكتب، لأن ال Shell يفتح الملف للتوجيه > قبل أن يبدأ الأمر. والحل الكامل أن تكتب إلى ملف مؤقت ثم تنقله بالأمر mv بعد النجاح، وأن تفحص حجم النسخة أو تختبر استرجاعها.

للتفصيل: رمز الخروج Exit Code وrm مع متغير فارغ في دليل سطر الأوامر، وقسمي Pipelines وThe Set Builtin في دليل bash.

التمرين 14: حذفت ملف السجل والقرص لم يفرغ

تطبيق يكتب سجل Debug كبيراً ويبقي الملف مفتوحاً طوال الوقت. جهزه وشغله في الخلفية:

cat > /srv/ops/app.sh <<'EOF'
#!/bin/bash
# writes its debug log and keeps the file open
exec 3>>/srv/app/logs/debug.log
head -c 1G /dev/zero >&3
while true; do sleep 1; done
EOF
chmod 755 /srv/ops/app.sh && (/srv/ops/app.sh &)

انتظر حتى يصبح حجم debug.log 1.0G، ثم احذفه بالأمر rm وقارن المساحة المستخدمة قبل الحذف وبعده بالأمر df -m. ستجد أن المساحة لم تتغير، فاشرح السبب، وجد العملية المسؤولة دون أن تثبت lsof، ثم حرر المساحة دون أن توقف التطبيق.

الوقت المقترح: 25 دقيقة.

تلميح Hint

rm يحذف الاسم فقط، والمساحة تتحرر عندما لا يبقى أي اسم ولا أي عملية تفتح الملف. وكل عملية لها مجلد /proc/PID/fd فيه روابط لكل ملف مفتوح، والرابط لملف محذوف ينتهي بالكلمة (deleted)، وfind يبحث في هدف الرابط بالخيار -lname.

الحل Answer
$ ls -lh debug.log
-rw-r--r-- 1 root root 1.0G Oct  7 08:00 debug.log
$ df -m --output=used,avail /
 Used  Avail
88447 890128
$ rm debug.log
$ df -m --output=used,avail /
 Used  Avail
88447 890128
$ du -sh /srv/app/logs
16K	/srv/app/logs
$ find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null
/proc/116/fd/3 -> /srv/app/logs/debug.log (deleted)
/proc/84/fd/3 -> /srv/app/logs/debug.log (deleted)
$ ps -o pid,cmd -p $(pgrep -f app.sh)
  PID CMD
   84 /bin/bash /srv/ops/app.sh
$ : > /proc/$(pgrep -f app.sh)/fd/3
$ df -m --output=used,avail /
 Used  Avail
87425 891149

في المخرج أعلاه لاحظ التالي:

  • بعد rm بقي Used كما هو، بينما du يقول إن المجلد فيه 16K فقط، وهذا الفرق بين df وdu هو العلامة المعروفة لملف محذوف ما زال مفتوحاً، لأن du يعد الأسماء الموجودة وdf يعد المساحة المحجوزة في نظام الملفات.
  • ظهرت عمليتان: 84 هي السكربت نفسه، و116 هي عملية sleep 1 التي كانت تعمل في تلك اللحظة، لأن العملية الابن Child Process ترث الملفات المفتوحة من أبيها، وهذا سبب آخر لكي تنظر إلى القائمة قبل أن تقرر.
  • : > /proc/84/fd/3 يفرغ الملف من خلال الرابط الذي ما زالت العملية تملكه، فرجعت المساحة المستخدمة إلى 87425 أي تحرر قرابة 1G دون أن يتوقف التطبيق. والحل الآخر أن تعيد تشغيل التطبيق فيغلق الملف، والحل الصحيح على المدى الطويل أن تستخدم تدوير السجلات Log Rotation الذي يبلغ التطبيق بإعادة فتح الملف بدل أن تحذف الملف بيدك.
  • الأرقام في df هنا من قرص ال VM الخاص ب Docker Desktop، لذلك قد تتغير قليلاً بسبب Containers أخرى، والمهم هو الفرق.

للتفصيل: ملف محذوف ما زال يحجز المساحة في دليل أداء السيرفر، والملفات النصية وأداة التدوير logrotate في دليل السجلات.

التمرين 15: يعمل بيدك ولا يعمل في cron

جهز أداة صغيرة في /usr/local/bin وثلاث مهام مجدولة للمستخدم deploy، ثم شغل خدمة cron:

printf '#!/bin/bash\necho "$(date +%%T) report generated"\n' > /usr/local/bin/report-tool
chmod 755 /usr/local/bin/report-tool
cat > /tmp/deploy.cron <<'EOF'
* * * * * report-tool >> /tmp/report.log 2>&1
* * * * * date +%F > /tmp/today.txt 2>&1
* * * * * env > /tmp/cron-env.txt
EOF
crontab -u deploy /tmp/deploy.cron
su - deploy -c 'report-tool'
cron

الأمر report-tool يعمل عندما تشغله بيدك بحساب deploy. انتظر دقيقة كاملة ثم انظر إلى الملفات الثلاثة: لماذا فشلت المهمة الأولى؟ ولماذا لم يظهر الملف /tmp/today.txt أصلاً مع أن date +%F يعمل في الطرفية؟ أصلح الملف وأثبت أن المهمتين تعملان.

الوقت المقترح: 25 دقيقة.

تلميح Hint

المهمة الثالثة كتبت لك البيئة Environment التي يرى بها cron الأوامر، فقارن PATH فيها مع echo $PATH في طرفيتك. وللسؤال الثاني اقرأ في man 5 crontab ما يحدث للعلامة % في سطر الأمر.

الحل Answer

بعد دقيقة:

$ cat /tmp/report.log
/bin/sh: 1: report-tool: not found
$ cat /tmp/today.txt
cat: /tmp/today.txt: No such file or directory
$ grep -E '^(PATH|SHELL|HOME)=' /tmp/cron-env.txt
HOME=/home/deploy
PATH=/usr/bin:/bin
SHELL=/bin/sh

السبب الأول أن cron يشغل الأوامر ببيئة صغيرة جداً، فقيمة PATH فيها /usr/bin:/bin فقط، بينما الأداة في /usr/local/bin، والطرفية التي جربت منها لها PATH أطول. والسبب الثاني أن cron يعامل العلامة % معاملة خاصة، فكما تقول صفحة crontab(5) في Debian: علامات النسبة في الأمر تتحول إلى أسطر جديدة إلا إذا سبقتها شرطة مائلة عكسية، وكل ما بعد أول % يرسل إلى الأمر كمدخل stdin. فالأمر الذي وصل إلى ال Shell هو date + وحده بدون التوجيه إلى الملف، ولذلك لم ينشأ الملف ولم تظهر أي رسالة في أي ملف. والإصلاح كما يلي:

$ crontab -l -u deploy
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * report-tool >> /tmp/report.log 2>&1
* * * * * date +\%F > /tmp/today.txt 2>&1
$ cat /tmp/report.log
08:04:01 report generated
$ cat /tmp/today.txt
2026-10-07

في الإصلاح أعلاه لاحظ التالي:

  • السطر PATH=… في أول ملف crontab يضبط البيئة لكل المهام بعده، والبديل أن تكتب المسار الكامل /usr/local/bin/report-tool في كل مهمة، وهو الأوضح عندما تقرأ الملف بعد سنة.
  • \%F تمنع cron من تحويل العلامة، فتصل %F كما هي إلى date. والأفضل في الأوامر الطويلة أن تضعها في سكربت وتكتب في crontab اسم السكربت فقط.
  • أضف دائماً >> ملف 2>&1 لكل مهمة، فلولا التوجيه في المهمة الأولى لما رأينا رسالة not found، لأن cron يرسل المخرج بالبريد، ولا يوجد خادم بريد في أغلب السيرفرات.

للتفصيل: صفحة crontab(5) في Debian، وبطء في نفس الساعة كل يوم في دليل استكشاف الأخطاء.

التمرين 16: صلاحية sudo لأمر واحد فقط

جهز سكربت النشر:

printf '#!/bin/bash\necho "deploying as $(whoami)"\n' > /usr/local/bin/deploy-app
chmod 755 /usr/local/bin/deploy-app

المستخدم deploy يجب أن يشغل /usr/local/bin/deploy-app بصلاحيات root دون كلمة مرور، ولا يستطيع أن يشغل أي شيء آخر بالأمر sudo. اكتب القاعدة في ملف داخل /etc/sudoers.d وافحص صحتها قبل أن تضعها، ثم أثبت أن الأمر المسموح يعمل وأن cat /etc/shadow مرفوض. وأجب: لماذا يجب أن يكون مالك deploy-app هو root وليس deploy؟

الوقت المقترح: 20 دقيقة.

تلميح Hint

صيغة القاعدة: user host=(runas) TAG: command، وvisudo -cf ملف يفحص ملفاً قبل أن تضعه، وsudo -l -U deploy يعرض ما يسمح به لمستخدم، وsudo -n يفشل بدل أن يسأل عن كلمة المرور.

الحل Answer

المحاولة الأولى فيها خطأ صغير متعمد ليرى كيف يحميك visudo:

$ echo 'deploy ALL=(root) NOPASSWD /usr/local/bin/deploy-app' > /tmp/deploy.sudoers
$ visudo -cf /tmp/deploy.sudoers
/tmp/deploy.sudoers:1:53: syntax error
deploy ALL=(root) NOPASSWD /usr/local/bin/deploy-app
                                                    ^
$ echo 'deploy ALL=(root) NOPASSWD: /usr/local/bin/deploy-app' > /tmp/deploy.sudoers
$ visudo -cf /tmp/deploy.sudoers
/tmp/deploy.sudoers: parsed OK
$ install -m 0440 -o root -g root /tmp/deploy.sudoers /etc/sudoers.d/deploy
$ visudo -c
/etc/sudoers: parsed OK
/etc/sudoers.d/README: parsed OK
/etc/sudoers.d/deploy: parsed OK
$ sudo -l -U deploy
Matching Defaults entries for deploy on kata:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty

User deploy may run the following commands on kata:
    (root) NOPASSWD: /usr/local/bin/deploy-app
$ su deploy -c 'sudo -n /usr/local/bin/deploy-app'
deploying as root
$ su deploy -c 'sudo -n cat /etc/shadow'
sudo: a password is required

في المخرج أعلاه لاحظ التالي:

  • نقطتان ناقصتان بعد NOPASSWD كانتا كافيتين لخطأ في الصيغة، ولو كتبت الملف مباشرة في /etc/sudoers.d لتوقف sudo كله على السيرفر حتى تصلحه، وقد لا يكون لديك حساب root آخر تدخل به. لذلك افحص بالأمر visudo -cf أولاً، ثم انسخ الملف بالأمر install بمالك root وصلاحية 0440.
  • القاعدة تحدد المسار الكامل للأمر، فلا يستطيع deploy أن يشغل غيره، والرسالة a password is required تعني أن القاعدة لا تغطي cat وأن sudo طلب كلمة مرور، ومع -n فشل بدل أن ينتظر.
  • إذا كان deploy مالك السكربت أو يستطيع الكتابة فيه، فسوف يكتب فيه أي أمر ثم يشغله بصلاحيات root، وتصبح القاعدة «أمر واحد» بلا معنى. لذلك الأمر المسموح وكل المجلدات في مساره يجب أن يملكها root ولا يكتب فيها غيره.

للتفصيل: sudo أم الدخول بحساب root؟ في دليل الصلاحيات، وصفحتي sudoers وvisudo الرسميتين.

التمرين 17: ملف خدمة systemd فيه خطآن، وأين تقرأ سجلها؟

زميلك كتب ملف الخدمة التالي لتطبيق Node.js، والخدمة لا تعيد التشغيل بعد أن تتعطل كما كان يتوقع:

[Unit]
Description=Shop API
After=network-online.target
Wants=network-online.target

[Service]
User=deploy
WorkingDirectory=/srv/shop
ExecStart=node server.js
Restart=onfailure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

احفظه في /root/units/shop.service وافحصه بأداة من systemd دون أن تثبته، وأصلح ما تجده. ثم اكتب من ذاكرتك على سيرفر حقيقي يعمل ب systemd: الأمر الذي يعرض حالة الخدمة، والأمر الذي يعرض آخر 50 سطراً من سجلها منذ آخر إقلاع Boot، والأمر الذي يعرض أخطاءها فقط في آخر ساعة.

الوقت المقترح: 25 دقيقة.

تلميح Hint

systemd-analyze verify ./shop.service يقرأ الملف ويبلغ عن كل قيمة لا يفهمها. والقيم المسموحة للخيار Restart= مكتوبة في man systemd.service. وفي journalctl ابحث عن الخيارات -u و-b و-n و-p و--since.

الحل Answer
$ systemd-analyze verify ./shop.service; echo "exit=$?"
/root/units/shop.service:10: Failed to parse Restart=onfailure, ignoring: Invalid argument
shop.service: Command node is not executable: No such file or directory
exit=1
$ sed -i -e 's/^Restart=onfailure$/Restart=on-failure/' -e 's|^ExecStart=node server.js$|ExecStart=/usr/bin/node /srv/shop/server.js|' shop.service
$ grep -E '^(ExecStart|Restart)=' shop.service
ExecStart=/usr/bin/node /srv/shop/server.js
Restart=on-failure
$ systemd-analyze verify ./shop.service; echo "exit=$?"
exit=0

في المخرج أعلاه لاحظ التالي:

  • الخطأ الأهم هو Restart=onfailure: القيمة الصحيحة on-failure بشرطة، وsystemd لا يرفض تشغيل الخدمة بسببها، وإنما يكتب تحذيراً و«يتجاهل» السطر، فتعمل الخدمة بالقيمة الافتراضية Restart=no ولا تعود بعد أن تتعطل. وهذا أخطر من الخطأ الذي يوقف كل شيء، لأنك لن تلاحظه حتى أول عطل.
  • الخطأ الثاني أن node لم يكن مثبتاً عندما فحصنا أول مرة، فقالت الأداة إنه غير قابل للتشغيل. وتقول صفحة systemd.service إن الأمر الذي ليس مساراً كاملاً يبحث عنه systemd في مسارات ثابتة مثل /usr/local/bin و/usr/bin، ونحن نفضل المسار الكامل لأنه يجعل الملف واضحاً ولا يعتمد على مكان التثبيت. وبعد تثبيت nodejs من Debian وتصحيح السطرين رجع verify بالرمز 0. ولاحظ أن verify لا يفحص وجود /srv/shop/server.js نفسه.

أما الجزء الثاني فلا يعمل داخل Container، لأن systemd ليس العملية رقم 1 فيه، وهذا ما رأيناه عندما جربنا:

$ systemctl status shop
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to system scope bus via local transport: Host is down
$ journalctl -u shop -n 5 --no-pager
No journal files were found.
-- No entries --

لذلك الإجابة التالية من صفحتي journalctl وsystemd-analyze وليست من مختبرنا:

sudo cp shop.service /etc/systemd/system/ && sudo systemctl daemon-reload
sudo systemctl enable --now shop
systemctl status shop
journalctl -u shop -b -n 50 --no-pager
journalctl -u shop -p err --since "1 hour ago"

و-u تحدد الوحدة Unit، و-b تعني منذ آخر إقلاع، و-n 50 آخر 50 سطراً، و-p err الأخطاء وما هو أخطر منها، و--since تقبل وقتاً نسبياً مثل "1 hour ago". ولا تنس daemon-reload بعد كل تعديل على ملف الوحدة، وإلا فسوف يعمل systemd بالنسخة القديمة.

للتفصيل: في systemd: journald في دليل السجلات.

جدول المتابعة

انسخ الجدول التالي إلى ملف عندك، واكتب في كل مرة تحل فيها تمريناً التاريخ والوقت الذي أخذه وهل احتجت التلميح، وارجع إلى كل تمرين احتجت فيه التلميح أو تجاوزت الوقت بعد أسبوع، وإلى التمارين التي حللتها بسرعة بعد شهر:

| #  | المستوى  | التمرين                                  | التاريخ | الوقت | تلميح؟ | المراجعة القادمة |
|----|----------|------------------------------------------|---------|-------|--------|------------------|
| 1  | مبتدئ    | التنقل بالمسار النسبي                    |         |       |        |                  |
| 2  | مبتدئ    | شجرة مجلدات في أمر واحد                  |         |       |        |                  |
| 3  | مبتدئ    | قراءة سطر ls -l                          |         |       |        |                  |
| 4  | مبتدئ    | عد الطلبات في السجل                      |         |       |        |                  |
| 5  | مبتدئ    | فصل الأخطاء عن المخرج                    |         |       |        |                  |
| 6  | مبتدئ    | رمز خروج grep                            |         |       |        |                  |
| 7  | متوسط    | أكثر ثلاثة عناوين IP                     |         |       |        |                  |
| 8  | متوسط    | أخطاء 5xx ونسبتها                        |         |       |        |                  |
| 9  | متوسط    | sed على ملف إعداد                        |         |       |        |                  |
| 10 | متوسط    | صلاحية المجلد                            |         |       |        |                  |
| 11 | متوسط    | مساحة القرص والملف المتناثر              |         |       |        |                  |
| 12 | متوسط    | SIGTERM وSIGKILL                         |         |       |        |                  |
| 13 | متقدم    | السكربت الذي يخفي فشله                   |         |       |        |                  |
| 14 | متقدم    | ملف محذوف يحجز القرص                     |         |       |        |                  |
| 15 | متقدم    | cron وPATH والعلامة %                    |         |       |        |                  |
| 16 | متقدم    | sudo لأمر واحد                           |         |       |        |                  |
| 17 | متقدم    | ملف خدمة systemd وjournalctl             |         |       |        |                  |

وعندما تنتهي احذف المختبر:

docker rm -f kata-linux

الخلاصة

وصلنا لنهاية تمارين Linux، وأهم ما تخرج به منها:

  • اكتب الرقم أو النتيجة التي تتوقعها قبل أن تشغل الأمر، فأغلب أخطاء هذا المستوى لا تعطي رسالة خطأ وإنما رقماً خاطئاً، مثل grep login الذي عد wp-login.php وuniq -c دون sort.
  • رمز الخروج هو لغة السكربتات: 0 نجاح، وأي رقم آخر له معنى، ورمز الأنبوب هو رمز آخر أمر فيه إلا إذا استخدمت set -o pipefail.
  • الصلاحيات تفحص على كل مجلد في المسار وليس على الملف وحده، والإصلاح هو أقل صلاحية تحل المشكلة وليس 777، وsudo لأمر واحد يحتاج أمراً يملكه root.
  • ls وdu وdf تجيب عن ثلاثة أسئلة مختلفة، والفرق بينها يكشف الملف المتناثر والملف المحذوف المفتوح.
  • cron يعمل ببيئة صغيرة ويعامل % معاملة خاصة، وsystemd يتجاهل القيمة التي لا يفهمها بتحذير، فافحص الملف بالأداة قبل أن تعتمد عليه.

وهذه المقالات التي تشرح كل موضوع في التمارين بالتفصيل:

دليلسطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفركيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.دليلصلاحيات الملفات Permissions في Linux وأخطاؤها الشائعةماذا تعني الحروف rwx، ولماذا تظهر رسالة Permission denied، وكيف تصلحها دون أن تلجأ إلى chmod 777.دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.دليلالتعبيرات النمطية Regex للمبتدئين، مع النص العربيكيف تقرأ وتكتب تعبيراً نمطياً جزءاً بجزء، ولماذا يختلف سلوكه مع النص العربي بين اللغات، وكيف يوقف نمط واحد سيرفرك.

وفي بقية سلسلة تمارين Kata سوف نطبق نفس الطريقة بإذن الله على الشبكات وSQL والبرمجة والأمن وDevOps وتشخيص الأعطال.

سجل التحديثات

  • أكتوبر 2026: كتابة التمارين السبعة عشر، وتنفيذ كل الحلول في Container من postgres:18 (Debian 13.7، وGNU bash 5.2.37، وcoreutils 9.7، وgrep 3.11، وsed 4.9، وmawk 1.3.4، وprocps-ng 4.0.4) مع cron 3.0pl1-197 وsudo 1.9.16p2 وsystemd 257.13 وNode.js 20.19.2 من حزم Debian، على Docker Engine 29.8.0 (Docker Desktop). أوامر systemctl وjournalctl في التمرين 17 من الدليل الرسمي لأن systemd لا يعمل كعملية رقم 1 في ال Container.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.