الدرس 1 من 12 · اللغة بعمق
المتغيرات والتوسيعات وعلامات الاقتباس: التقطيع والاستبدال والحساب دون كسر اسم ملف أبدًا
الهدف
معالجة السلاسل النصية داخل 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:
nom-archive.sh: اسم أرشيف دون مجلد ولا امتداد، ودون أمر خارجي؛court.sh: الاسم القصير لآلة، بالأحرف الكبيرة؛conf.sh: قيم افتراضية تصمد أمام المتغيرات الفارغة؛pourcent.sh: نسبة مئوية صحيحة مقرَّبة بشكل سليم؛compte-args.sh: المعاملات المستلمة، سليمة بفضل"$@"؛cmd-distante.sh: أمرsshآمن باستعمالprintf %q.
اختبار قصير
- مع
f=/srv/www/index.old.html، ماذا تعطي${f##*/}و${f%.*}و${f%%.*}؟ - ما الفرق بين
${PORT-80}و${PORT:-80}؟ - بأي ترتيب يطبّق bash توسيع المتغيرات وتقطيع الكلمات وأحرف البدل؟
- ما الفرق بين
"$@"و"$*"؟ - لماذا يكون
ssh hote rm -f "$f"خطيرًا إذا كان$fقادمًا من مستخدم؟
الأجوبة
index.old.html؛/srv/www/index.old؛/srv/www/index.- الأول لا يستبدل إلا إذا كان
PORTغائبًا؛ والثاني يستبدل أيضًا عندما يكونPORTفارغًا. - أولًا التوسيعات (
$vو$( )و$(( )))، ثم تقطيع الكلمات حسبIFS، ثم أحرف البدل. علامات الاقتباس المزدوجة تمنع الخطوتين الأخيرتين. "$@"يعطي كلمة لكل معامل، سليمة؛ و"$*"يجمع كل المعاملات في سلسلة واحدة يفصل بينها أول حرف منIFS.- يجمع ssh المعاملات ويجعل الصدفة البعيدة تفسّرها من جديد: اسم يحتوي على
;أو$( )يصبح أمرًا هناك. يجب حماية القيمة بـprintf %q.