البرمجة الاحترافية بـ Bash: أتمتة إدارة أنظمة Linux
خروج

الدرس 1 من 12 · اللغة بعمق

المتغيرات والتوسيعات وعلامات الاقتباس: التقطيع والاستبدال والحساب دون كسر اسم ملف أبدًا

1 س Rocky Linux 9

الهدف

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

أهم النقاط

  • #/## يحذفان نمطًا من البداية (الأقصر / الأطول)، و%/%% من النهاية؛ ${v/a/b} يستبدل، و${v^^} و${v,,} يغيّران حالة الأحرف، و${#v} يعطي الطول.
  • ${v:-défaut} و${v:=défaut} و${v:?message} و${v:+autre}: مع النقطتين، يُعامَل المتغير الفارغ كأنه غائب.
  • $(( … )) يحسب بالأعداد الصحيحة؛ وprintf -v var ينسّق داخل متغير دون صدفة فرعية (subshell).
  • يحوّل bash السطر بترتيب ثابت: التوسيعات، ثم تقطيع الكلمات، ثم أحرف البدل (globs). علامات الاقتباس المزدوجة تمنع الخطوتين الأخيرتين.
  • "$@" يمرّر كل معامل سليمًا؛ وprintf %q يحمي سلسلة ستُقرأ من جديد في صدفة أخرى (ssh، سجل قابل لإعادة التنفيذ).

يفترض هذا الدرس أنك تعرف المتغيرات البسيطة و${1:-défaut} وأحرف البدل التي رأيتها في دورة «الأساسيات» (الدروس 1.11 و1.13 و1.17). سنذهب أبعد بكثير.


1. لماذا نستغني عن basename أو cut أو sed؟

في حلقة تعالج 20 000 ملف سجل، كل $(basename "$f") يطلق عملية جديدة. أما التوسيع ${f##*/} فيؤدي العمل نفسه داخل bash، وأسرع بمئات المرات.

f=/var/backups/ml-web-01-2026-10-20.tar.gz

echo "${#f}"            # 40           longueur en caractères
echo "${f:13:9}"        # ml-web-01    9 caractères à partir de la position 13 (on compte depuis 0)
echo "${f: -6}"         # tar.gz       les 6 derniers (l'espace avant - est obligatoire)

2. حذف نمط من البداية أو من النهاية

الأنماط هنا هي أحرف البدل الخاصة بالصدفة (* و? و[…])، وليست تعابير نمطية.

التوسيع يحذف… وسيلة للتذكّر
${v#motif} أقصر بادئة مطابقة # على يسار $ في لوحة المفاتيح
${v##motif} أطول بادئة
${v%motif} أقصر لاحقة % على اليمين
${v%%motif} أطول لاحقة
f=/var/backups/ml-web-01-2026-10-20.tar.gz
echo "${f##*/}"         # ml-web-01-2026-10-20.tar.gz   (= basename)
echo "${f%/*}"          # /var/backups                  (= dirname)
nom=${f##*/}
echo "${nom%.tar.gz}"   # ml-web-01-2026-10-20          extension précise retirée
echo "${nom#*.}"        # tar.gz                        l'extension « complète »
echo "${nom##*.}"       # gz                            la dernière extension seulement

fqdn=ml-db-01.masterlinux.lab
echo "${fqdn%%.*}"      # ml-db-01                      nom court
echo "${fqdn#*.}"       # masterlinux.lab               domaine

for f in /srv/exports/*.CSV; do
    mv -- "$f" "${f%.CSV}.csv"       # renommer en masse, sans rename ni sed
done

3. الاستبدال وتغيير حالة الأحرف

ligne="agence de fes;agence de rabat;agence de oujda"
echo "${ligne/agence/AGENCE}"     # AGENCE de fes;agence de rabat;...   première seulement
echo "${ligne//;/ | }"            # agence de fes | agence de rabat | ...  toutes
echo "${ligne//agence de /}"      # fes;rabat;oujda                     supprimer = remplacer par rien

chemin=/srv/site/
echo "${chemin/#\/srv/\/data}"    # /data/site/     # = ancré au DÉBUT
echo "${chemin/%\//}"             # /srv/site       % = ancré à la FIN

role=web ; ENV=PROD
echo "${role^} ${role^^} ${ENV,,}"   # Web WEB prod

تغيير حالة الأحرف خاص بـ bash 4 فما فوق: كل التوزيعات الحالية تتوفر عليه، لكن ليس sh الخاص بـ Debian (أي dash).

4. القيم الافتراضية والمتغيرات الإلزامية

التوسيع إذا كان v غائبًا إذا كان v فارغًا إذا كانت لـ v قيمة
${v-d} d فارغ القيمة
${v:-d} d d القيمة
${v:=d} d، ويأخذ v القيمة d d، ويأخذ v القيمة d القيمة
${v:?msg} خطأ، يتوقف السكربت خطأ القيمة
${v:+x} فارغ فارغ x
#!/bin/bash
# /usr/local/bin/ml-purge — configuration par variables d'environnement
: "${RETENTION_JOURS:=14}"           # « : » ne fait rien : seule l'affectation compte
CIBLE=${1:?Usage : ml-purge HOTE (exemple : ml-app-01)}
echo "Purge sur $CIBLE au-delà de $RETENTION_JOURS jours${DRY_RUN:+ (simulation)}"

يضيف ${v:+…} خيارًا فقط عندما يكون له معنى: rsync -a ${VERBOSE:+-v} …. أما الإحالة غير المباشرة ${!nom} فتقرأ المتغير الذي يوجد اسمه في nom (nom=seuil_db; echo "${!nom}")؛ وعندما تتكاثر الحالات، تكون المصفوفة الترابطية (الدرس 2.2) أنظف.

5. الحساب: $(( )) وprintf -v

echo $(( 45 * 100 / 60 ))                      # 75    division ENTIÈRE (tronquée)
echo $(( 2 * 100 / 3 ))                        # 66    alors que 66,67 s'arrondit à 67
echo $(( (2 * 100 + 3 / 2) / 3 ))              # 67    arrondi : on ajoute la moitié du diviseur
echo $(( 17 % 5 )) $(( 2 ** 10 ))              # 2 1024
(( compteur += 1 ))                            # pas besoin de $ dans (( ))

printf -v jour '%(%F)T' -1            # date du jour, sans lancer la commande date
printf -v hote 'ml-web-%02d' 3        # hote=ml-web-03

لا يعرف bash إلا الأعداد الصحيحة: بالنسبة للمبالغ، احسب بالسنتيمات (12350 لـ 123,50 درهم) أو أسند الحساب إلى awk.

6. ما يفعله bash بسطر ما

لنأخذ متغيرًا يحتوي على مسافة ونجمة، في مجلد يحتوي على rapport de juin.txt وsimple.txt:

motif="ventes  *"
echo $motif         # ventes rapport de juin.txt simple.txt     ← découpée PUIS l'étoile remplacée
echo "$motif"       # ventes  *                                 ← intacte

دون علامات اقتباس، قام bash بما يلي: 1) استبدل $motif بقيمته؛ 2) قطّعها على المسافات (اختفت المسافتان)؛ 3) استبدل * بقائمة الملفات. علامات الاقتباس المزدوجة تمنع الخطوتين 2 و3. القاعدة الذهبية: كل توسيع بين علامات اقتباس مزدوجة، إلا إذا كنت تريد التقطيع بالضبط.

f="rapport de juin.txt"
wc -c $f          # wc: rapport: No such file or directory  (3 fichiers demandés)
wc -c "$f"        # 4 rapport de juin.txt

7. الأنواع الثلاثة من علامات الاقتباس

hote=ml-web-01
echo 'Serveur : $hote'          # Serveur : $hote          rien n'est interprété
echo "Serveur : $hote"          # Serveur : ml-web-01      $ est interprété
echo "Prix : 120 \$"            # Prix : 120 $             \ protège un caractère dans "…"
echo $'Colonne1\tColonne2'      # $'…' comprend \t, \n, \e
echo 'l'\''agence'              # l'agence   (fermer, \' , rouvrir : seule façon d'avoir ' entre '…')
الكتابة $v و$( ) \n الاستعمال المعتاد
'…' لا لا التعابير النمطية، برامج awk، أنماط sed
"…" نعم لا كل ما تبقّى تقريبًا
$'…' لا نعم الأحرف الخاصة (IFS=$'\n'، علامات الجدولة)

8. "$@" و"$*" وIFS

# Le script reçoit trois arguments : "Rabat centre" Fes Agadir
for a in "$@"; do echo "[$a]"; done     # [Rabat centre] [Fes] [Agadir]     3 tours
for a in "$*"; do echo "[$a]"; done     # [Rabat centre Fes Agadir]         1 tour
for a in $@;   do echo "[$a]"; done     # [Rabat] [centre] [Fes] [Agadir]   4 tours : cassé

السكربت المغلِّف (wrapper) الذي يضيف خيارات ثم يسلّم المهمة يكتب exec rsync -a --exclude='*.tmp' "$@": المعاملات سليمة، ورمز الخروج هو مباشرة رمز rsync.

يُستعمل IFS (المسافة وعلامة الجدولة والسطر الجديد افتراضيًا) لتقطيع الكلمات ولـ read. غيّره لأمر واحد فقط:

IFS=: read -r nom ip ville <<< "ml-db-01:10.0.0.20:casablanca"
echo "$nom / $ip / $ville"                  # ml-db-01 / 10.0.0.20 / casablanca

9. أحرف البدل تحت السيطرة، والأسماء التي تبدأ بـ -

افتراضيًا، يبقى حرف البدل الذي لا يجد شيئًا كما هو: for f in /srv/vide/* تدور مرة واحدة بالقيمة الحرفية /srv/vide/*.

الخيار الأثر
shopt -s nullglob يختفي حرف البدل الذي لا نتيجة له: لا تدور الحلقة
shopt -s failglob حرف البدل الذي لا نتيجة له يصبح خطأ
shopt -s dotglob يشمل * الملفات المخفية
shopt -s globstar **/*.log ينزل في كل المجلدات الفرعية
shopt -s extglob أنماط موسّعة: !(*.gz) و`@(web

ملف اسمه -rf يأخذه rm * على أنه خيار. الحلول: rm -- * (-- = نهاية الخيارات) أو rm ./*.

10. printf %q: إعادة حقن سلسلة دون خطر

يجمع ssh معاملاته ويجعل الصدفة البعيدة تعيد قراءتها:

chemin="/srv/partage/compta 2026;reboot"
printf '%q\n' "$chemin"                             # /srv/partage/compta\ 2026\;reboot
ssh ml-web-01 rm -f "$chemin"                       # DANGER : le shell distant exécute « reboot »
ssh ml-web-01 "rm -f $(printf '%q' "$chemin")"      # sûr

اسم ملف يقدّمه مستخدم ويُمرَّر كما هو إلى ssh يسمح بتنفيذ أمر على الخادم البعيد: إنها ثغرة حقيقية.

11. حالة واقعية: سكربت التنظيف الذي أفرغ المجلد الخطأ

على خادم ملفات تابع لـ MasterLinux، كان سكربت تنظيف يحتوي على rm -rf $DOSSIER/*. وذات يوم أُطلق دون معامل بواسطة مهمة مجدولة مكتوبة بشكل سيئ: كان $DOSSIER فارغًا، فأصبح الأمر rm -rf /*. ثلاث حمايات، تكفي كل واحدة منها:

DOSSIER=${1:?Usage : purge.sh DOSSIER}     # 1. argument obligatoire
[ -d "$DOSSIER" ] || exit 1                # 2. il doit exister
rm -rf -- "${DOSSIER:?}"/*                 # 3. ${v:?} refuse une valeur vide, guillemets partout

أخطاء شائعة

  • الخلط بين # و%، أو نسيان النقطتين: ${PORT-8080} يترك PORT="" يمرّ.
  • ${v: -3} دون مسافة: ${v:-3} قيمة افتراضية، وليس «آخر ثلاثة أحرف».
  • متغيرات دون علامات اقتباس: [ $v = oui ] مع v فارغ يصبح [ = oui ]؛ وfor f in $(ls) يكسر الأسماء. دائمًا "$v"، وfor f in ./*.
  • علامات اقتباس مخزّنة داخل متغير لا تفيد شيئًا: opts='-a "mon dossier"'; cmd $opts يمرّر "mon وdossier". لقائمة معاملات، استعمل مصفوفة (الدرس 2.2).

التطبيق العملي (Lab)

على Rocky Linux 9، ستكتب ستة سكربتات صغيرة في /root/bash:

  1. nom-archive.sh: اسم أرشيف دون مجلد ولا امتداد، ودون أمر خارجي؛
  2. court.sh: الاسم القصير لآلة، بالأحرف الكبيرة؛
  3. conf.sh: قيم افتراضية تصمد أمام المتغيرات الفارغة؛
  4. pourcent.sh: نسبة مئوية صحيحة مقرَّبة بشكل سليم؛
  5. compte-args.sh: المعاملات المستلمة، سليمة بفضل "$@"؛
  6. cmd-distante.sh: أمر ssh آمن باستعمال printf %q.

اختبار قصير

  1. مع f=/srv/www/index.old.html، ماذا تعطي ${f##*/} و${f%.*} و${f%%.*}؟
  2. ما الفرق بين ${PORT-80} و${PORT:-80}؟
  3. بأي ترتيب يطبّق bash توسيع المتغيرات وتقطيع الكلمات وأحرف البدل؟
  4. ما الفرق بين "$@" و"$*"؟
  5. لماذا يكون ssh hote rm -f "$f" خطيرًا إذا كان $f قادمًا من مستخدم؟
الأجوبة
  1. index.old.html؛ /srv/www/index.old؛ /srv/www/index.
  2. الأول لا يستبدل إلا إذا كان PORT غائبًا؛ والثاني يستبدل أيضًا عندما يكون PORT فارغًا.
  3. أولًا التوسيعات ($v و$( ) و$(( )))، ثم تقطيع الكلمات حسب IFS، ثم أحرف البدل. علامات الاقتباس المزدوجة تمنع الخطوتين الأخيرتين.
  4. "$@" يعطي كلمة لكل معامل، سليمة؛ و"$*" يجمع كل المعاملات في سلسلة واحدة يفصل بينها أول حرف من IFS.
  5. يجمع ssh المعاملات ويجعل الصدفة البعيدة تفسّرها من جديد: اسم يحتوي على ; أو $( ) يصبح أمرًا هناك. يجب حماية القيمة بـ printf %q.

تمارين تطبيقية

تُنجَز على Rocky Linux 9

أنجز هذا التطبيق العملي على جهازك

الطرفية طرفيتك: آلة افتراضية بنظام لينكس (VirtualBox أو VMware أو Hyper-V أو WSL…) أو Docker/Podman. أداة ml تحضّر التمرين وتصحّح عملك، ويُؤكَّد الدرس هنا تلقائيًا.

أنشئ حسابًا مجانيًا للحصول على أمرك الشخصي وتصحيح تطبيقاتك العملية تلقائيًا.

إنشاء حسابي

المهام المطلوبة

  1. أنشئ السكربت القابل للتنفيذ /root/bash/nom-archive.sh CHEMIN: يعرض اسم الأرشيف دون المجلد ودون الامتداد .tar.gz. مثال: /var/backups/ml-web-01-2026-10-20.tar.gz يعطي ml-web-01-2026-10-20. ممنوع استعمال: basename وdirname وsed وawk وcut.
    عرض تلميحخطوتان: f=${1##*/} يحذف كل شيء حتى آخر /، ثم ${f%.tar.gz} يحذف الامتداد.
    عرض الحل
    mkdir -p /root/bash && printf '%s\n' '#!/bin/bash' 'f=${1##*/}' 'echo "${f%.tar.gz}"' > /root/bash/nom-archive.sh && chmod +x /root/bash/nom-archive.sh

    التحقق : test "$(/root/bash/nom-archive.sh /var/backups/ml-web-01-2026-10-20.tar.gz 2>/dev/null)" = ml-web-01-2026-10-20 && test "$(/root/bash/nom-archive.sh archive.tar.gz 2>/dev/null)" = archive && ! grep -Eq "basename|dirname|sed|awk|cut" /root/bash/nom-archive.sh

  2. أنشئ السكربت القابل للتنفيذ /root/bash/court.sh NOM: يعرض الاسم القصير لآلة بالأحرف الكبيرة. ml-db-01.masterlinux.lab يعطي ML-DB-01؛ وml-web-01 (دون نطاق) يعطي ML-WEB-01.
    عرض تلميح${1%%.*} يحتفظ بما يسبق أول نقطة؛ و${h^^} يحوّل إلى أحرف كبيرة.
    عرض الحل
    mkdir -p /root/bash && printf '%s\n' '#!/bin/bash' 'h=${1%%.*}' 'echo "${h^^}"' > /root/bash/court.sh && chmod +x /root/bash/court.sh

    التحقق : test "$(/root/bash/court.sh ml-db-01.masterlinux.lab 2>/dev/null)" = ML-DB-01 && test "$(/root/bash/court.sh ml-web-01 2>/dev/null)" = ML-WEB-01

  3. أنشئ السكربت القابل للتنفيذ /root/bash/conf.sh: يعرض port=P env=E. تأتي P من متغير البيئة PORT (القيمة 8080 إذا كان غائبًا أو فارغًا)؛ وتأتي E من ENV_ML (القيمة dev إذا كان غائبًا أو فارغًا). مثال: PORT=9090 ENV_ML=prod /root/bash/conf.sh يعرض port=9090 env=prod.
    عرض تلميحمع النقطتين، يعالج ${PORT:-8080} المتغير الفارغ أيضًا. لتثبيت ENV_ML: : "${ENV_ML:=dev}"
    عرض الحل
    mkdir -p /root/bash && printf '%s\n' '#!/bin/bash' 'port=${PORT:-8080}' ': "${ENV_ML:=dev}"' 'echo "port=$port env=$ENV_ML"' > /root/bash/conf.sh && chmod +x /root/bash/conf.sh

    التحقق : test "$(env -u PORT -u ENV_ML /root/bash/conf.sh 2>/dev/null)" = "port=8080 env=dev" && test "$(PORT=9090 ENV_ML=prod /root/bash/conf.sh 2>/dev/null)" = "port=9090 env=prod" && test "$(PORT= ENV_ML= /root/bash/conf.sh 2>/dev/null)" = "port=8080 env=dev"

  4. أنشئ السكربت القابل للتنفيذ /root/bash/pourcent.sh UTILISE TOTAL: يعرض النسبة المئوية الصحيحة المقرَّبة (1 3 تعطي 33، و2 3 تعطي 67، و45 60 تعطي 75). إذا كانت TOTAL تساوي 0: رسالة خطأ على مخرج الأخطاء والرمز 1.
    عرض تلميحالقسمة الصحيحة تبتر الكسور: أضف نصف المقسوم عليه قبل القسمة، $(( (100 * u + t / 2) / t )).
    عرض الحل
    mkdir -p /root/bash && printf '%s\n' '#!/bin/bash' 'u=$1 t=$2' 'if (( t == 0 )); then' '    echo "Erreur : TOTAL ne peut pas valoir 0" >&2' '    exit 1' 'fi' 'echo $(( (100 * u + t / 2) / t ))' > /root/bash/pourcent.sh && chmod +x /root/bash/pourcent.sh

    التحقق : test "$(/root/bash/pourcent.sh 1 3 2>/dev/null)" = 33 && test "$(/root/bash/pourcent.sh 2 3 2>/dev/null)" = 67 && test "$(/root/bash/pourcent.sh 45 60 2>/dev/null)" = 75 && ! /root/bash/pourcent.sh 5 0 >/dev/null 2>&1

  5. أنشئ السكربت القابل للتنفيذ /root/bash/compte-args.sh: يعرض عدد المعاملات المستلمة، ثم كل معامل في سطر خاص بين معقوفين، دون تغييره. مثال: compte-args.sh "*" "x y" "" يعرض 3 و[*] و[x y] و[] (لا تُستبدل النجمة بأسماء ملفات، وتبقى المسافتان).
    عرض تلميحecho "$#" ثم for a in "$@"; do echo "[$a]"; done — علامات الاقتباس حول $@ تحافظ على كل معامل كاملًا.
    عرض الحل
    mkdir -p /root/bash && printf '%s\n' '#!/bin/bash' 'echo "$#"' 'for a in "$@"; do' '    echo "[$a]"' 'done' > /root/bash/compte-args.sh && chmod +x /root/bash/compte-args.sh

    التحقق : test "$(cd / && /root/bash/compte-args.sh "*" "x y" "" 2>/dev/null)" = "$(printf "3\n[*]\n[x y]\n[]")" && test "$(/root/bash/compte-args.sh 2>/dev/null)" = 0

  6. أنشئ السكربت القابل للتنفيذ /root/bash/cmd-distante.sh CHEMIN: يعرض (دون تنفيذ) الأمر ssh ml-web-01 rm -f CHEMIN، حيث CHEMIN محمي بـ printf %q. مثال: "/tmp/a b" يعطي ssh ml-web-01 rm -f /tmp/a\ b.
    عرض تلميحprintf "ssh ml-web-01 rm -f %q\n" "$1"
    عرض الحل
    mkdir -p /root/bash && printf '%s\n' '#!/bin/bash' 'printf "ssh ml-web-01 rm -f %q\n" "$1"' > /root/bash/cmd-distante.sh && chmod +x /root/bash/cmd-distante.sh

    التحقق : test "$(/root/bash/cmd-distante.sh "/tmp/a b" 2>/dev/null)" = "ssh ml-web-01 rm -f /tmp/a\ b" && test "$(/root/bash/cmd-distante.sh "/tmp/x;reboot" 2>/dev/null)" = "ssh ml-web-01 rm -f /tmp/x\;reboot" && grep -q "%q" /root/bash/cmd-distante.sh