SSD Nodes Learn 🎉 VPS من $4.99/شهر
الأدلة Matt Connorبقلم Matt Connor

توسيع سجل Bash: استخدام !! و !$

يعيد !! تنفيذ آخر أمر، بينما يلتقط !$ وسيطه الأخير. تعرّف إلى 6 توسعات مفيدة في Bash، ومنها :p لطباعة الأمر قبل تنفيذه.

وظيفة توسيع سجل Bash

يعيد توسيع سجل Bash بناء سطر أوامر سابق من سجل الصدفة قبل أن ينفّذه bash. يكرّر !! الأمر السابق. ويُدرج !$ الوسيط الأخير من الأمر السابق. هذا الاستبدال نصّي ويحدث أولاً، قبل أن يقسّم bash السطر إلى كلمات؛ لذلك تكون النتيجة هي الأحرف نفسها التي كتبتها في المرة الأولى.

تغطي الأشكال الستة التالية معظم ما ستنفّذه على خادم:

  • يكرّر !! السطر السابق بالكامل، وينفّذه sudo !! مرة أخرى بصفة root.
  • يمثّل !$ الوسيط الأخير في السطر السابق.
  • يمثّل !* جميع وسائط السطر السابق.
  • ينفّذ !n إدخال السجل ذي الرقم n، ويعدّ !-n تنازلياً بدءاً من موضعك الحالي.
  • ينفّذ !string أحدث أمر بدأ بـ string.
  • يعيد ^old^new تنفيذ السطر السابق مع استبدال أول old بـ new.

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

مسار طويل، يُكتب مرة واحدة

إليك الحالة التي تبرر استخدام هذه الطريقة. أنت تجهّز دليلاً لإصدار، والمسار طويل بما يكفي لأن إعادة كتابته تفتح المجال لخطأ مطبعي.

sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.env

قبل تنفيذ كل سطر، يطبع bash السطر الذي نتج عن التوسعة:

sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07

هذا السطر المطبوع هو نقطة التحقق. اقرأه قبل قراءة مخرجات الأمر، لأنه فرصتك الوحيدة لرؤية ما قرره bash بشأن معنى !$.

تعمل هذه السلسلة لأن bash يخزّن السطر بعد التوسعة في السجل، وليس !$ الذي كتبته. لذلك يأخذ السطر 3 وسيطَه الأخير من السطر 2 بعد توسعته، ويأخذه السطر 4 من السطر 3. يوضح السطر 4 أيضاً أن النص يمكن أن يأتي بعد محدِّد: يتحول !$/config.env إلى المسار مع إضافة /config.env في نهايته، لأن محدِّد الكلمة يتوقف عند /.

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

ls -l !$:h

:h هو معدِّل الرأس. يزيل آخر مكوّن من المسار، وهي الوظيفة نفسها التي ينفذها dirname. يحتفظ :t بآخر مكوّن فقط، ويزيل :r الامتداد، بينما يحتفظ :e بالامتداد فقط.

كرّر الأمر الأخير باستخدام !! وsudo !!

لقد نسيت sudo، ويؤكد مدير الخدمة ذلك:

systemctl restart nginx
Failed to restart nginx.service: Interactive authentication required.
sudo !!

يستبدل Bash الرمز !! بنص السطر السابق، لذلك تنفّذ الصدفة sudo systemctl restart nginx.

يمثّل !! السطر السابق حرفياً، بصرف النظر عن محتواه. هنا يحدث الخطأ الشائع. قد تظن أن الأمر الأخير هو الأمر الذي فشل، لكنك نفّذت بعده cd، أو history، أو كان الأمر الفاشل قبل سطرين. عندئذٍ ينفّذ sudo !! الأمر الخطأ بامتيازات root. اعرض الأمر أولاً عندما لا تكون متأكداً:

sudo !!:p

يعرض :p السطر بعد توسيعه ولا ينفّذه. يُضاف السطر المعروض إلى سجل الأوامر، لذلك إذا كان صحيحاً، فإن تنفيذ !! في السطر التالي يشغّله.

إعادة استخدام الوسيط الأخير باستخدام !$

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

sudo systemctl status nginx
sudo systemctl reload !$

هناك أمران يفاجئان المستخدمين هنا.

أولاً، !$ هي الكلمة الأخيرة، وليست الوسيط الأخير الذي تفكر فيه. إذا انتهى السطر السابق بإعادة توجيه، فستكون الكلمة الأخيرة هي هدف إعادة التوجيه:

sudo nginx -T > /tmp/nginx-dump.conf
less !$

ينجح ذلك في هذه الحالة. لكن بعد journalctl -u nginx > /tmp/log.txt، تكون !$ هي /tmp/log.txt وليست nginx. اقرأ السطر الذي تمت طباعته.

ثانياً، التوسيع نصي، لذلك يعود المتغير من دون توسيع. بعد ls $HOME/backups، تعطيك !$ الأحرف $HOME/backups، ثم يوسّعها bash مرة أخرى كمعامل عادي. تذكّر هذا الترتيب: يحدث توسيع سجل الأوامر قبل توسيع المعاملات وقبل استبدال الأوامر باستخدام $( )، لذلك لا يرى القيم، بل النص فقط.

تجدر معرفة الصيغ المجاورة أيضاً. !^ هو الوسيط الأول، و!:2 هو الوسيط الثاني، و!:2-4 هو نطاق من الوسائط. أما !!:$ فهو الطريقة المطوّلة لكتابة !$.

إذا أردت رؤية النص قبل اعتماده، فاضغط Alt-.، أو Esc ثم .. يُدرج Readline الوسيط الأخير للأمر السابق مباشرةً في موجه الأوامر، حيث يمكنك تعديله. اضغط عليه مرة أخرى للانتقال إلى الوسيط الأخير للأمر الذي سبقه. لا يُنفَّذ أي شيء حتى تضغط Enter.

مرِّر كل الوسائط باستخدام !*

!* هي كل كلمة في السطر السابق باستثناء الكلمة الأولى.

stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*

تؤدي عبارة «باستثناء الكلمة الأولى» وظيفة فعلية هنا. يحذف !* الكلمة 0 فقط ولا يحذف شيئاً آخر، لذلك تنتقل الخيارات مع المسارات. بعد ls -l file1 file2، تصبح !* هي -l file1 file2، ولذلك يفشل sudo chmod 600 !* لأن chmod يتلقى -l كوسيط. رقّم الكلمات بدءاً من الصفر، ثم استخدم مقطعاً بدلاً من ذلك: يعني !!:2* الكلمة 2 حتى نهاية السطر.

يظهر الخطأ نفسه مع sudo. بعد sudo chown deploy:deploy /srv/www/app، تكون الكلمة 0 هي sudo والكلمة 1 هي chown، ولذلك يمرّر !* إلى الأمر التالي chown deploy:deploy /srv/www/app. نادراً ما يكون هذا هو ما تريده.

اختيار أمر حسب موضعه باستخدام !n و!-n

يعرض history القائمة مع رقم أمام كل إدخال.

history 5
  512  sudo nginx -t
  513  sudo systemctl reload nginx
  514  ss -tulpn
  515  sudo tail -f /var/log/nginx/error.log
  516  history 5

يشغّل !513 الإدخال 513 مرة أخرى. يشغّل !-2 الإدخال الموجود قبل سطرين، مع احتساب ذلك من السطر الذي تكتبه الآن، لذلك يعني !-1 و!! الشيء نفسه.

تحقق من الأرقام مباشرة قبل استخدامها. يشير !-2 إلى موضع جديد في كل مرة تشغّل فيها أي شيء، بما في ذلك ls الذي شغّلته دون تفكير. تكون الأرقام المطلقة ثابتة ضمن الجلسة نفسها، لكنها لا تكون الأرقام نفسها في جلسة ثانية على الخادم نفسه، كما أنها لا تبقى نفسها بعد أن تقرأ جلستك التالية ملف المحفوظات. الرقم الذي حفظته عن ظهر قلب أمس يشير إلى أمر مختلف اليوم.

إعادة تشغيل أمر باستخدام بادئة ‎!string

يشغّل !string أحدث أمر يبدأ بـ string.

!ss

يعيد ذلك تشغيل ss -tulpn من القائمة أعلاه، وهو فحص المقابس التي تستمع، والمشروح في المنافذ المفتوحة على خادم Linux. يطابق !?string? أي موضع في السطر، بدلاً من مطابقة بدايته فقط. يفيد ذلك عندما تتذكر وسيطاً، لكنك لا تتذكر اسم الأمر.

اجعل البادئة طويلة. سيطابق !s واحداً من ss أو sudo أو systemctl أو shutdown، أيّها نُفِّذ مؤخراً. لن تعرف أي أمر سيُشغَّل قبل تنفيذه. يطبع !string:p المطابقة من دون تشغيلها. إذا لم توجد مطابقة، يطبع bash bash: !ss: event not found ولا يشغّل أي أمر. هذه هي النتيجة الآمنة.

إصلاح خطأ إملائي واحد باستخدام ‎^old^new‎

sudo systemctl status ngnix
Unit ngnix.service could not be found.
^ngnix^nginx

يعيد shell تنفيذ السطر السابق بعد استبدال أول ngnix بـ nginx. يتغير التطابق الأول فقط. لتغيير كل التطابقات، استخدم الصيغة الطويلة !!:gs/ngnix/nginx/، حيث يعني s الاستبدال، ويعني g تنفيذه على السطر بأكمله.

اطبع التوسعة قبل تشغيلها

تمنع عادتان وقوع الخطأ الذي يؤدي إلى تشغيل توسعة لأمر لم تقصده.

الأولى هي :p، التي رأيتها من قبل. أضفها إلى التوسعة، وسيطبع bash النتيجة بدلاً من تشغيلها: !!:p أو !systemctl:p. يُحفظ السطر المطبوع في سجل الأوامر، لذلك يؤدي تشغيل !! بعد ذلك إلى تنفيذ ما قرأته للتو.

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

shopt -s histverify

ضع هذا السطر في ~/.bashrc. عند ضبط histverify، لا تُشغَّل التوسعة عند الضغط على Enter. يكتب Bash السطر بعد توسعته مرة أخرى في موجّه الأوامر، حتى تتمكن من قراءته وتحريره، ثم تضغط على Enter مرة ثانية لتشغيله. لا يتطلب ذلك سوى ضغطة مفتاح واحدة، ويزيل هذه الفئة كاملة من الأخطاء. يحتاج هذا الخيار إلى readline، لذلك يعمل في موجّه تفاعلي، ولا يعمل في أي مكان آخر.

تُعد Ctrl-R طريقة أخرى للعمل، وهي لا توسّع أي شيء مطلقاً. اضغط على Ctrl-R واكتب جزءاً من أمر. يتغير الموجّه إلى موجّه reverse-i-search ويعرض أحدث تطابق أثناء الكتابة. اضغط على Ctrl-R مرة أخرى للانتقال إلى التطابقات الأقدم. يؤدي الضغط على Enter إلى تشغيل السطر المعروض. يلغي Ctrl-G البحث ويعيد موجّه الأوامر الأصلي. ينهي مفتاح السهم الأيسر البحث ويترك السطر المطابق في موجّه الأوامر حتى تحرره.

استخدم Ctrl-R عندما تريد رؤية الأمر أولاً. استخدم !! و!$ عندما يكون الأمر في السطر السابق ويمكنك ما زلت قراءته على الشاشة.

لماذا تعرض echo "done!" رسالة event not found

echo "deploy done!"
bash: !": event not found

يُنفَّذ توسيع سجل الأوامر قبل حسم الاقتباس، ولا تحمي علامات الاقتباس المزدوجة المحرف !. تحميه علامات الاقتباس المفردة والشرطة المائلة العكسية فقط. لذلك تطبع echo 'deploy done!' القيمة المتوقعة. تمنع الشرطة المائلة العكسية التوسيع أيضاً، لكن Bash يبقيها في الناتج داخل علامات الاقتباس المزدوجة، لذا تُعد علامات الاقتباس المفردة الحل الأنظف.

تظهر هذه المشكلة بوضوح أكبر مع كلمات المرور، لأن كلمة المرور القوية غالباً ما تحتوي على !. قد يفشل أمر مثل mysql -u app -p"S3cret!pass" مع خطأ event-not-found، أو قد يرسل بصمت سلسلة مختلفة إذا تطابق النص مع أحد إدخالات السجل. استخدم علامات الاقتباس المفردة، أو الأفضل من ذلك، اترك للأداة مهمة طلب كلمة المرور حتى لا يصل السر إلى سطر الأوامر إطلاقاً. وينبغي أن تكون هذه العادة جزءاً من ممارسات حماية الحسابات الأخرى. راجع الدقائق العشر الأولى على VPS جديد لمعرفة كيفية التعامل مع المفاتيح وكلمات المرور على خادم جديد.

إذا كنت على وشك لصق كتلة نصية تحتوي على عدد كبير من محارف !، فعطّل التوسيع لهذه الجلسة باستخدام set +H، ثم أعد تفعيله باستخدام set -H.

HISTSIZE وHISTFILESIZE ومكان حفظ سجل الأوامر

متغيران يخلط الناس بينهما لأن اسميهما متشابهان.

  • يحدد HISTSIZE عدد الأوامر التي تحتفظ بها الصدفة قيد التشغيل في الذاكرة.
  • يحدد HISTFILESIZE عدد الأسطر المحفوظة في الملف على القرص، ~/.bash_history ما لم يحدّد HISTFILE خلاف ذلك.

يُكتب الملف عند خروج الصدفة، ويُختصر إلى HISTFILESIZE أسطر في تلك اللحظة. يضبط ~/.bashrc الافتراضي في Ubuntu القيمتين على 1000 و2000. تحقّق من القيمة الفعلية في نظامك:

grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"

ارفع القيمتين إذا كانت عمليات البحث باستخدام !string لا تعثر على أوامر تعرف أنك نفّذتها. تُعد القيمتان 10000 و20000 شائعتين، ولا تستهلكان موارد تُذكر لأن الملف نص عادي. تعني القيمة السالبة عدم فرض أي حد.

تساعد الطوابع الزمنية عندما تعود إلى خادم بعد غياب أسبوع:

export HISTTIMEFORMAT='%F %T '

بعد ذلك يطبع history التاريخ والوقت أمام كل إدخال، لأن bash يبدأ بكتابة سطر تعليق يحتوي على ثواني epoch قبل كل أمر في الملف.

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

shopt -s histappend
export PROMPT_COMMAND='history -a'

يجعل histappend الصدفة تضيف قائمتها إلى نهاية الملف بدلاً من استبدال الملف. ويضيف history -a الأسطر الجديدة بعد كل مطالبة، لذلك تظل أوامر الجلسة محفوظة حتى إذا أُوقفت الجلسة بدلاً من إغلاقها بطريقة سليمة. يضبط ~/.bashrc الافتراضي في Ubuntu histappend مسبقاً. تزداد أهمية ذلك كلما زاد عدد الأجهزة التي تديرها، لأن السجل يُخزَّن لكل مستخدم على كل جهاز. لذلك، عندما تكون تدير عدة خوادم من محطة عمل واحدة، قد يكون !$ الذي تبحث عنه موجوداً ببساطة على مضيف آخر.

إبقاء الأسرار خارج سجل Bash

يحدد HISTCONTROL الأسطر التي تُحفظ أصلاً.

  • ignorespace يحذف أي سطر يبدأ بمسافة.
  • ignoredups يحذف السطر المطابق للسطر السابق له.
  • ignoreboth يطبّق الشرطين السابقين.
  • erasedups يزيل كل نسخة أقدم من السطر الجاري حفظه.
export HISTCONTROL=ignoreboth

عند تفعيل ignorespace، تمنع المسافة الأولى الأمر من الظهور في القائمة الموجودة في الذاكرة، ولذلك لا يصل إلى الملف أيضاً. تحقّق من القيمة قبل الاعتماد عليها باستخدام echo "$HISTCONTROL". يضبط مستخدم Ubuntu الافتراضي .bashrc القيمة ignoreboth، لكن قد تتركها جلسة root أو صورة نظام مصغّرة غير مضبوطة. عندئذٍ لا تفعل المسافة الأولى شيئاً، ويُحفظ السطر كأي سطر آخر.

كن واضحاً بشأن ما تحميه هذه الطريقة. يبقى السطر خارج ~/.bash_history. لكنه يظل ظاهراً في خرج ps أثناء تشغيل العملية، ولذلك يستطيع أي مستخدم آخر على الخادم قراءته هناك. يسجل sudo الأمر الذي شغّلته في سجل النظام. ملف السجل نص عادي، لذا اضبط نمط صلاحياته على 600، وتذكّر أن أي شخص يستطيع قراءة مجلد home الخاص بك يستطيع قراءة آخر بضعة آلاف من أوامرك.

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

history
history -d 517
history -w

يزيل history -d ذلك الإدخال من الذاكرة، ويكتب history -w القائمة الحالية فوق الملف. يمسح history -c القائمة بأكملها. أما HISTIGNORE فهو الإعداد المرتبط بالضوضاء بدلاً من الأسرار: إذ يُبقي HISTIGNORE='ls:pwd:history:clear' تلك الأسطر خارج القائمة كي تُرجع عمليات البحث نتائج مفيدة.

لماذا لا يحدث توسيع السجل في البرنامج النصي للصدفة

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

تحقق من أي صدفة تعمل فيها حالياً:

echo $-

يكون الناتج مجموعة أعلام الخيارات الحالية، مثل himBHs. يعني i أن الصدفة تفاعلية، ويعني H أن توسيع السجل مفعّل. نفّذ السطر نفسه داخل برنامج نصي، ولن يظهر أي من الحرفين.

هذا هو الحد الفاصل بين جانبي عملك في الصدفة. عند موجّه الأوامر، يحفظ !$ وCtrl-R ضغطات المفاتيح عند تنفيذ أوامر ما زلت تراها. أما في ملف، فتسمّي الأشياء بدلاً من ذلك: ضع المسار في متغير، أو التقط الناتج باستخدام استبدال الأوامر. إذا اعتمد برنامج نصي على سجلك الشخصي، فسيتصرف بطريقة مختلفة مع الشخص التالي الذي يشغّله، وهذا عكس الغرض من البرامج النصية.

لذلك، يُفترض أن تكتب كل الأمثلة في هذه الصفحة عند موجّه أوامر فعلي. ولا يعمل أي منها بالطريقة نفسها عند لصقه في ملف .sh.

FAQ

ماذا يفعل !! في bash؟

يوسّع !! إلى النص الكامل لسطر الأوامر السابق، ولذلك يعيد sudo !! تنفيذ آخر أمر بصلاحيات root. يحدث التوسيع نصياً قبل أن يحلّل bash السطر، ويطبع bash السطر المكتمل مباشرة قبل تنفيذه. إذا لم تكن متأكداً من محتوى السطر السابق، فاكتب sudo !!:p أولاً. يطبع :p التوسيع من دون تنفيذه، ويضيف السطر المطبوع إلى سجل الأوامر، ولذلك ينفّذه !! عند استخدامه لاحقاً.

كيف أعيد استخدام الوسيط الأخير للأمر السابق؟

استخدم !$. بعد sudo mkdir -p /srv/www/app/releases، يصبح السطر ls -ld !$ هو ls -ld /srv/www/app/releases. يأخذ هذا التوسيع آخر كلمة في السطر، ولذلك يُعد هدف إعادة التوجيه في نهاية السطر آخر كلمة. يمكنك أيضاً إلحاق نص به: يضيف !$/config.env نصاً إلى المسار، لأن محدِّد الكلمة يتوقف عند /. والبديل التفاعلي هو Alt-.، إذ يُدرج النص نفسه في موجه الأوامر حتى تتمكن من قراءته قبل الضغط على Enter.

لماذا يقول bash "event not found" عندما يحتوي النص على علامة تعجب؟

لا تحمي علامات الاقتباس المزدوجة ! من توسعة سجل الأوامر، ولذلك يجعل echo "done!" bash يبحث عن حدث في السجل ويطبع bash: !": event not found. تحميه علامات الاقتباس المفردة، ولذلك اكتب echo 'done!'. ويوقف الخط المائل العكسي التوسعة أيضاً، لكن bash يترك الخط المائل العكسي في الناتج داخل علامات الاقتباس المزدوجة. للصق كتلة طويلة تحتوي على !، عطّل التوسعة لهذه الجلسة باستخدام set +H.

لماذا لا يعمل !! و !$ في برنامج shell النصي؟

تُفعّل توسعة سجل الأوامر في الصدَفات التفاعلية فقط. يعمل البرنامج النصي بطريقة غير تفاعلية، ولذلك لا تنشئ الصدفة قائمة سجل، ويظل !! في السطر كنص عادي. شغّل echo $- لمعرفة نوع الصدفة: تطبع الصدفة التفاعلية أعلاماً تتضمن i وH، بينما لا يطبع البرنامج النصي أياً منهما. استخدم في البرامج النصية متغيراً أو استبدال الأوامر بدلاً من ذلك.

كيف أمنع حفظ كلمة مرور في سجل bash؟

اضبط HISTCONTROL=ignorespace أو HISTCONTROL=ignoreboth في ~/.bashrc، ثم ابدأ الأمر بمسافة واحدة، ولن يُحفظ الأمر. تحقّق من القيمة باستخدام echo "$HISTCONTROL" أولاً، لأن المسافة البادئة لا تفعل شيئاً إذا كان المتغير غير مضبوط. يمنع هذا السطر من الظهور في ~/.bash_history فقط. يظل الأمر ظاهراً في ps أثناء تشغيله، ويسجل sudo ما نفّذه. إذا كانت قيمة سرية محفوظة مسبقاً، فاعثر على رقمها باستخدام history، ثم شغّل history -d <number> متبوعاً بـhistory -w لإعادة كتابة الملف.

#bash#shell#history#productivity#cli