> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# تمارين Kata في Linux بثلاثة مستويات مع الحلول
- URL: https://arabroot.io/articles/تمارين-kata-في-linux/
- Published: 2026-10-06T13:15:00.000Z
- Updated: 2026-10-09T09:44:07.000Z
- Description: تمارين في سطر أوامر Linux تحلها بيدك دون ذكاء اصطناعي، من قراءة الصلاحيات إلى القرص الذي يمتلئ بسبب ملف محذوف.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, لينكس والسيرفرات, أساسيات, الاستضافة الذاتية

في مقال [لماذا تتمرن دون ذكاء اصطناعي؟](https://arabroot.io/articles/%D8%AA%D9%85%D8%A7%D8%B1%D9%8A%D9%86-kata-%D8%AD%D8%A7%D9%81%D8%B8-%D8%B9%D9%84%D9%89-%D9%85%D9%87%D8%A7%D8%B1%D8%AA%D9%83-%D8%AF%D9%88%D9%86-%D8%B0%D9%83%D8%A7%D8%A1-%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A/) شرحنا لماذا تضعف المهارة التي لا تستخدمها، وكيف تتمرن بطريقة Kata وحدك أو مع فريقك، وهذا المقال هو أول مجال نطبق فيه هذه الطريقة: سطر الأوامر في Linux. والسبب أن كل ما يحدث على السيرفر يمر عبره في النهاية، فعندما يمتلئ القرص أو تتوقف مهمة مجدولة أو يرفض سكربت أن يعمل، فأنت تحتاج أن تكتب `grep` و`awk` و`find` من ذاكرتك وتفهم ما تقرؤه في المخرج، وليس أن تنسخ أمراً لا تعرف ماذا يفعل.

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

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

- تجهيز مختبر 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](https://arabroot.io/content/images/2026/10/katas-linux-01-levels-map.webp)

كل مستوى يبني على الذي قبله، فابدأ من الأول حتى لو بدا لك سهلاً

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

كل التمارين تعمل في Container واحد من ال Docker Image الخاص ب PostgreSQL 18، وهو مبني على Debian 13 وفيه bash وcoreutils وprocps جاهزة، ولن نشغل PostgreSQL نفسه، وهذا نفس المختبر الذي استخدمناه في [دليل سطر الأوامر في Linux](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#lab). ولاحظ الخيار `--init`، فهو يضع عملية صغيرة في مكان العملية رقم 1 PID 1 تجمع العمليات المنتهية، وبدونه سوف ترى في تمرين الإشارات عمليات ميتة Zombies لا علاقة لها بالتمرين، وقد شرحنا السبب في قسم [العمليات الميتة والعملية رقم 1](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#zombies):

```bash
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، وسوف نستخدمه في تمارين الصلاحيات على أنه «مستخدم آخر» ليس المالك وليس في المجموعة:

```bash
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 من النطاقات المحجوزة للتوثيق. أنشئه كما هو، لأن أرقام الحلول محسوبة عليه:

```bash
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 وتضبط تواريخها:

```bash
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 

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

```text
$ 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` لأن تاريخه أحدث، وليس بسبب اسمه.

للتفصيل: [المسار المطلق والمسار النسبي](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#paths) و[التنقل بين المجلدات](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#navigation) في دليل سطر الأوامر.

### التمرين 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 

```text
$ 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.

للتفصيل: [العمل مع الملفات والمجلدات](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#files) في دليل سطر الأوامر، وقسم [Brace Expansion](https://www.gnu.org/software/bash/manual/html%5Fnode/Brace-Expansion.html?ref=arabroot.io) في دليل bash.

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

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

```bash
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

```

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

```text
-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 لا يستطيع حتى القراءة. والتحقق كما يلي:

```text
$ 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](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/#ls-l) و[ترتيب الفحص في النواة](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/#check-order) في دليل الصلاحيات.

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

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

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

تلميح Hint

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

الحل Answer 

```text
$ 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`.

للتفصيل: [القنوات الثلاث والتوجيه والأنابيب](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#redirection) في دليل سطر الأوامر، وإذا أردت التعمق في أنماط البحث ف[دليل التعبيرات النمطية Regex](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%A8%D9%8A%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D9%85%D8%B7%D9%8A%D8%A9-regex-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

### التمرين 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 

```text
$ 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، والسكربت الذي يفحص `$?` سوف يعرف أن الأمر فشل.

للتفصيل: [القنوات الثلاث والتوجيه والأنابيب](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#redirection) في دليل سطر الأوامر، وقسم [Redirections](https://www.gnu.org/software/bash/manual/html%5Fnode/Redirections.html?ref=arabroot.io) في دليل bash الذي يشرح هذا الترتيب بنفس المثال.

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

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

```bash
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 

```text
$ 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 في الدليل](https://man7.org/linux/man-pages/man1/grep.1.html?ref=arabroot.io) إن رمز الخروج 0 إذا وجد سطراً، و1 إذا لم يجد، و2 إذا حدث خطأ. والفرق بين 1 و2 مهم في السكربت: «لم أجد كلمة error في السجل» نتيجة جيدة، و«لم أستطع فتح السجل» مشكلة يجب أن توقف السكربت، فإذا عاملت الاثنين على أنهما «ليس صفراً» فسوف تخلط بينهما. ولاحظ أن `A && B || C` ليست if كاملة، فإذا فشل `B` نفسه فسوف يعمل `C` أيضاً، وهي مقبولة هنا لأن `echo` لا يفشل.

للتفصيل: [رمز الخروج Exit Code](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#exit-codes) في دليل سطر الأوامر.

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

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

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

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

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

تلميح Hint

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

الحل Answer 

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

```text
$ 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](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#redirection) في دليل سطر الأوامر، و[أسئلة لا يجيب عنها السجل النصي](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#lab-questions) في دليل السجلات.

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

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

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

تلميح Hint

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

الحل Answer 

```text
$ 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](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#structured-logs) في دليل السجلات، و[دليل gawk](https://www.gnu.org/software/gawk/manual/gawk.html?ref=arabroot.io) الرسمي للغة awk.

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

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

```bash
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` قبل أن تكتب على الملف:

```text
$ 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](https://www.gnu.org/software/sed/manual/sed.html?ref=arabroot.io) الرسمي، و[دليل التعبيرات النمطية Regex](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%A8%D9%8A%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D9%85%D8%B7%D9%8A%D8%A9-regex-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/).

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

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

```bash
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 

```text
$ 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 على المجلد](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/#dir-bits) و[لماذا لا تحل chmod 777 أي مشكلة](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/#chmod-777) في دليل الصلاحيات.

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

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

```bash
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 

```text
$ 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` يجيب عن سؤال «كم يأخذ من القرص؟»، وعندما يمتلئ القرص فالسؤال الثاني هو المهم. ولكن انتبه إلى أن الملف المتناثر قد يكبر فعلاً على القرص عندما تكتب فيه الآلة الافتراضية، أو عندما تنسخه بأداة لا تحافظ على الفراغات.

للتفصيل: [البحث عن الملفات ومعرفة المساحة](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#find-du-df) في دليل سطر الأوامر، و[المساحة ممتلئة](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#disk-full) في دليل أداء السيرفر.

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

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

```bash
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 

```text
$ 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](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#signals) و[لماذا لا تبدأ ب kill -9؟](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#sigterm-sigkill) في دليل سطر الأوامر.

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

تمارين هذا المستوى تشبه ما يحدث على السيرفر فعلاً: شيء يفشل دون رسالة واضحة، والمطلوب أن تجد السبب ثم تصلحه وتثبت أن الإصلاح عمل. واكتب في ملفك فرضيتك Hypothesis قبل أي أمر، كما شرحنا في [دليل استكشاف الأخطاء](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#hypothesis).

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

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

```bash
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 

```text
$ ./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](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#exit-codes) و[rm مع متغير فارغ](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#empty-variable) في دليل سطر الأوامر، وقسمي [Pipelines](https://www.gnu.org/software/bash/manual/html%5Fnode/Pipelines.html?ref=arabroot.io) و[The Set Builtin](https://www.gnu.org/software/bash/manual/html%5Fnode/The-Set-Builtin.html?ref=arabroot.io) في دليل bash.

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

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

```bash
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 

```text
$ 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 أخرى، والمهم هو الفرق.

للتفصيل: [ملف محذوف ما زال يحجز المساحة](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/#deleted-open) في دليل أداء السيرفر، و[الملفات النصية وأداة التدوير logrotate](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#files-logrotate) في دليل السجلات.

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

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

```bash
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 

بعد دقيقة:

```text
$ 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)](https://manpages.debian.org/trixie/cron/crontab.5.en.html?ref=arabroot.io) في Debian: علامات النسبة في الأمر تتحول إلى أسطر جديدة إلا إذا سبقتها شرطة مائلة عكسية، وكل ما بعد أول `%` يرسل إلى الأمر كمدخل stdin. فالأمر الذي وصل إلى ال Shell هو `date +` وحده بدون التوجيه إلى الملف، ولذلك لم ينشأ الملف ولم تظهر أي رسالة في أي ملف. والإصلاح كما يلي:

```text
$ 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)](https://manpages.debian.org/trixie/cron/crontab.5.en.html?ref=arabroot.io) في Debian، و[بطء في نفس الساعة كل يوم](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#case-scheduled) في دليل استكشاف الأخطاء.

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

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

```bash
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`:

```text
$ 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؟](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/#sudo-root) في دليل الصلاحيات، وصفحتي [sudoers](https://www.sudo.ws/docs/man/sudoers.man/?ref=arabroot.io) و[visudo](https://www.sudo.ws/docs/man/visudo.man/?ref=arabroot.io) الرسميتين.

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

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

```text
[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 

```text
$ 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](https://man7.org/linux/man-pages/man5/systemd.service.5.html?ref=arabroot.io) إن الأمر الذي ليس مساراً كاملاً يبحث عنه systemd في مسارات ثابتة مثل `/usr/local/bin` و`/usr/bin`، ونحن نفضل المسار الكامل لأنه يجعل الملف واضحاً ولا يعتمد على مكان التثبيت. وبعد تثبيت `nodejs` من Debian وتصحيح السطرين رجع `verify` بالرمز 0\. ولاحظ أن `verify` لا يفحص وجود `/srv/shop/server.js` نفسه.

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

```text
$ 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](https://man7.org/linux/man-pages/man1/journalctl.1.html?ref=arabroot.io) و[systemd-analyze](https://man7.org/linux/man-pages/man1/systemd-analyze.1.html?ref=arabroot.io) وليست من مختبرنا:

```bash
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](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#journald) في دليل السجلات.

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

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

```text
| #  | المستوى  | التمرين                                  | التاريخ | الوقت | تلميح؟ | المراجعة القادمة |
|----|----------|------------------------------------------|---------|-------|--------|------------------|
| 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             |         |       |        |                  |

```

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

```bash
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 يتجاهل القيمة التي لا يفهمها بتحذير، فافحص الملف بالأداة قبل أن تعتمد عليه.

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

[![](https://arabroot.io/content/images/2026/10/linux-cli-cover-cover.webp)دليلسطر الأوامر في Linux للمبتدئين: أول ما تحتاجه على السيرفركيف تتنقل بين الملفات على السيرفر، وتربط الأوامر ببعضها، وتتحكم في العمليات Processes دون أن تحذف شيئاً بالخطأ.](https://arabroot.io/articles/%D8%B3%D8%B7%D8%B1-%D8%A7%D9%84%D8%A7%D9%88%D8%A7%D9%85%D8%B1-%D9%81%D9%8A-linux-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/linux-permissions-cover-cover.webp)دليلصلاحيات الملفات Permissions في Linux وأخطاؤها الشائعةماذا تعني الحروف rwx، ولماذا تظهر رسالة Permission denied، وكيف تصلحها دون أن تلجأ إلى chmod 777.](https://arabroot.io/articles/%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D9%81%D9%8A-linux-%D9%88%D8%A7%D8%AE%D8%B7%D8%A7%D8%A4%D9%87%D8%A7/)[![](https://arabroot.io/content/images/2026/10/linux-performance-cover-cover.webp)دليلتحليل أداء سيرفر Linux في 60 ثانية: ماذا تكتب أولاً؟ماذا تكتب أول ما تدخل إلى سيرفر بطيء، وكيف تعرف خلال دقيقة هل المشكلة في المعالج أم الذاكرة أم القرص أم الشبكة.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D8%AF%D8%A7%D8%A1-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1-linux-%D9%81%D9%8A-60-%D8%AB%D8%A7%D9%86%D9%8A%D8%A9/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/troubleshooting-guide-cover-cover.webp)دليلاستكشاف الأخطاء وتحليل الأداء للمبتدئين: من العرض إلى السببعندما يبطئ الموقع أو يعطي 502، هذه طريقة منظمة توصلك من العرض إلى السبب قبل أن تخمن أو تشتري سيرفراً أكبر.](https://arabroot.io/articles/%D8%A7%D8%B3%D8%AA%D9%83%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1-%D9%88%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D8%A7%D8%AF%D8%A7%D8%A1-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/regex-cover-cover.webp)دليلالتعبيرات النمطية Regex للمبتدئين، مع النص العربيكيف تقرأ وتكتب تعبيراً نمطياً جزءاً بجزء، ولماذا يختلف سلوكه مع النص العربي بين اللغات، وكيف يوقف نمط واحد سيرفرك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B9%D8%A8%D9%8A%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D9%86%D9%85%D8%B7%D9%8A%D8%A9-regex-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)

وفي بقية سلسلة [تمارين Kata](https://arabroot.io/start/#stage-katas) سوف نطبق نفس الطريقة بإذن الله على الشبكات و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.