SSD Nodes Learn 🎉 VPS החל מ־$5.50/חודש
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-13

קיצורי Bash: איך להשתמש ב-!! ו-!$ ביעילות

למדו להשתמש ב-!! לחזרה על הפקודה האחרונה וב-!$ לשליפת הארגומנט האחרון. המדריך מפרט 6 הרחבות היסטוריה חיוניות ב-Bash, כולל השימוש ב-:p למניעת הרצה שגויה של פקודות.

מה עושה הרחבת היסטוריית bash

הרחבת היסטוריית bash משחזרת שורת פקודה קודמת מתוך היסטוריית ה-shell שלכם לפני ש-bash מריץ אותה. !! חוזר על הפקודה הקודמת. !$ מציב את הארגומנט האחרון של הפקודה הקודמת. ההחלפה היא טקסטואלית והיא מתבצעת ראשונה, לפני ש-bash מפצל את השורה למילים, כך שהתוצאה היא התווים המדויקים שהקלדתם בפעם הראשונה.

שישה פורמטים מכסים כמעט כל פעולה שתבצעו בשרת:

  • !! חוזר על כל השורה הקודמת, ו-sudo !! מריץ אותה שוב כ-root.
  • !$ הוא הארגומנט האחרון של השורה הקודמת.
  • !* הוא כל הארגומנטים של השורה הקודמת.
  • !n מריץ את פריט ההיסטוריה שמספרו n, ו-!-n סופר לאחור מהמיקום הנוכחי שלכם.
  • !string מריץ את הפקודה האחרונה ביותר שהתחילה ב-string.
  • ^old^new מריץ מחדש את השורה הקודמת כאשר ה-old הראשון מוחלף ב-new.

כל מה שמופיע להלן מוקלד בשורת פקודה אינטראקטיבית בשרת שלכם. הרחבת היסטוריה כבויה בתוך סקריפטים, כפי שמוסבר בסעיף האחרון.

נתיב ארוך, הקלדה אחת

זהו המקרה שמצדיק את המאמץ. אתם מכינים ספריית release, והנתיב ארוך מספיק כדי שהקלדתו בשנית תזמין שגיאת כתיב.

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

לפני הרצת כל שורה, bash מדפיס את השורה כפי שהיא נראית לאחר ה־expansion:

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

ה־echo הזה הוא הבדיקה שלכם. קראו אותו לפני שאתם קוראים את הפלט של הפקודה, שכן זו ההזדמנות היחידה שלכם לראות מה bash החליט ש־!$ מייצג.

השרשרת נשמרת מכיוון ש־bash שומר בהיסטוריה את השורה המורחבת, ולא את ה־!$ שהקלדתם. לכן שורה 3 לוקחת את הארגומנט האחרון שלה מהשורה המורחבת 2, ושורה 4 לוקחת אותו משורה 3. שורה 4 גם מראה שטקסט יכול להופיע אחרי ה־designator: ה־!$/config.env הופך לנתיב עם /config.env בסופו, כיוון שה־word designator עוצר ב־/.

עוד דוגמה מאותו session. לאחר עריכת הקובץ, אתם רוצים להציג את הספרייה שמכילה אותו:

ls -l !$:h

:h הוא ה־head modifier. הוא מסיר את הרכיב האחרון מנתיב, בדיוק כפי ש־dirname עושה. :t שומר רק על הרכיב האחרון, :r מסיר את הסיומת, ו־:e שומר רק על הסיומת.

חזרה על הפקודה האחרונה באמצעות !! ו-sudo !!

שכחת את sudo, ומנהל השירותים מדווח על כך:

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

Bash מחליף את !! בטקסט של השורה הקודמת, ולכן ה-shell מריץ את sudo systemctl restart nginx.

!! הוא בדיוק השורה הקודמת, תהיה אשר תהיה. כאן מתרחשת הטעות הקלאסית. אתם סבורים שהפקודה האחרונה היא זו שנכשלה, אך מאז הרצתם cd, או history, או שהפקודה שנכשלה הייתה שתי שורות אחורה. sudo !! מריץ אז את הפקודה השגויה בהרשאות root. הדפיסו אותה תחילה כאשר אינכם בטוחים:

sudo !!:p

:p מדפיס את השורה המורחבת ואינו מריץ אותה. השורה המודפסת מתווספת להיסטוריה שלכם, כך שאם היא נראית נכון, !! פשוט בשורה הבאה יריץ אותה.

שימוש חוזר בארגומנט האחרון באמצעות !$

!$ הוא המילה האחרונה בשורה הקודמת. זהו הפורמט שבו תשתמשו בתדירות הגבוהה ביותר, כיוון שהמילה האחרונה היא לרוב האובייקט שעליו אתם עובדים: נתיב או שם של שירות.

sudo systemctl status nginx
sudo systemctl reload !$

שני דברים מפתיעים משתמשים בהקשר זה.

ראשית, !$ הוא המילה האחרונה, לא הארגומנט האחרון שהתכוונתם אליו. אם השורה הקודמת הסתיימה בהפניה (redirection), המילה האחרונה היא יעד ההפניה:

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

במקרה הזה התוצאה תקינה. אך לאחר journalctl -u nginx > /tmp/log.txt, ה-!$ הוא /tmp/log.txt ולא nginx. קראו את השורה שמוצגת ב-echo.

שנית, ההרחבה היא טקסטואלית, לכן משתנה חוזר ללא הרחבה. לאחר 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 בלבד, כך שהאפשרויות (options) נשמרות לצד הנתיבים. לאחר ls -l file1 file2, !* הוא -l file1 file2, לכן sudo chmod 600 !* נכשל, כיוון ש-chmod מקבל את -l כארגומנט. יש למספר את המילים החל מאפס ולבצע חיתוך (slice): !!: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 שהפעלתם בלי לחשוב. מספרים מוחלטים יציבים בתוך סשן אחד, אך הם אינם אותם מספרים בסשן שני על אותו שרת, והם אינם זהים לאחר שה-login הבא שלכם קורא שוב את קובץ ה-history. מספר ששיננתם אתמול מצביע על פקודה אחרת היום.

הרצת פקודה מחדש לפי תחילית עם !string

!string מריצה את הפקודה האחרונה ביותר שמתחילה ב-string.

!ss

פעולה זו מריצה מחדש את ss -tulpn מהרשימה לעיל, בדיקת ה-socket המאזין המתוארת ב-אילו פורטים פתוחים בשרת 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 משמעו החלפה (substitute) ו-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

הרחבת היסטוריה (history expansion) מתבצעת לפני פענוח המירכאות, ומרכאות כפולות אינן מגינות על התו !. רק גרש בודד או לוכסן אחורי (backslash) מספקים הגנה כזו. לכן, echo 'deploy done!' מדפיס את התוצאה המצופה. לוכסן אחורי עוצר גם הוא את ההרחבה, אך בתוך מירכאות כפולות bash משאיר את הלוכסן בפלט, ולכן שימוש בגרש בודד הוא הפתרון הנקי.

בעיה זו מורגשת במיוחד בסיסמאות, שכן סיסמה חזקה מכילה לעיתים קרובות את התו !. פקודה כמו mysql -u app -p"S3cret!pass" תיכשל עם שגיאת event-not-found, או במקרה שקיים ערך היסטוריה תואם, היא תשלח מחרוזת שונה ללא התראה. השתמשו בגרש בודד, או עדיף מכך, אפשרו לכלי לבקש מכם את הסיסמה כך שהסוד לעולם לא יגיע לשורת הפקודה. הרגל זה הוא חלק מהיגיינת חשבון תקינה: ראו עשר הדקות הראשונות בשרת VPS חדש כדי ללמוד כיצד יש לנהל מפתחות וסיסמאות בשרת טרי.

אם בכוונתכם להדביק גוש טקסט המכיל תווים מסוג !, כבו את ההרחבה עבור הסשן הנוכחי באמצעות set +H, והפעילו אותה מחדש בעזרת set -H.

HISTSIZE, HISTFILESIZE ומיקום היסטוריית הפקודות שלך

אלו שני משתנים שאנשים נוטים לבלבל ביניהם בגלל הדמיון בשמותיהם.

  • HISTSIZE קובע כמה פקודות ה-shell הפעיל שומר בזיכרון.
  • HISTFILESIZE קובע כמה שורות יישמרו בקובץ על הדיסק, ~/.bash_history אלא אם HISTFILE מגדיר אחרת.

הקובץ נכתב בעת יציאה מה-shell, ובאותו רגע הוא נחתך ל-HISTFILESIZE שורות. ברירת המחדל של Ubuntu ב-~/.bashrc מגדירה 1000 ו-2000. בדקו מה הערכים הנוכחיים שלכם:

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

הגדילו את שני הערכים אם חיפושי !string שלכם מפספסים פקודות שאתם יודעים שהרצתם. ערכים של 10000 ו-20000 הם נפוצים ואינם גוזלים משאבים, כיוון שהקובץ הוא טקסט פשוט. ערך שלילי משמעו ללא הגבלה כלל.

חותמות זמן מסייעות כאשר חוזרים לשרת לאחר שבוע של היעדרות:

export HISTTIMEFORMAT='%F %T '

history מדפיס תאריך ושעה לפני כל רשומה, כיוון ש-bash מתחיל לכתוב שורת הערה המכילה את שניות ה-epoch לפני כל פקודה בקובץ.

תלונה נפוצה היא שהיסטוריית הפקודות נעלמת בעת שימוש ביותר מטרמינל אחד. כל shell שומר רשימה משלו בזיכרון וכותב אותה בעת היציאה, לכן ללא histappend, ה-shell האחרון שנסגר דורס את מה שהאחרים שמרו. שתי הגדרות פותרות זאת:

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

histappend גורם ל-shell להוסיף את הרשימה שלו לסוף הקובץ במקום להחליף את הקובץ כולו. history -a מצרף שורות חדשות לאחר כל prompt, כך שגם סשן שנסגר בכוח ולא בצורה מסודרת משאיר את הפקודות שלו מאחור. ברירת המחדל של Ubuntu ב-~/.bashrc כבר מגדירה histappend. עניין זה הופך למשמעותי יותר ככל שמנהלים יותר מכונות, כיוון שההיסטוריה נשמרת לפי משתמש לכל מכונה; לכן, כאשר אתם מנהלים כמה שרתים מתחנת עבודה אחת, ה-!$ שאתם מחפשים עשוי פשוט להימצא במארח אחר.

שמירה על סודות מחוץ להיסטוריית ה-bash

HISTCONTROL קובע אילו שורות יישמרו בכלל.

  • ignorespace משמיט כל שורה שמתחילה ברווח.
  • ignoredups משמיט שורה שזהה לשורה שקדמה לה.
  • ignoreboth מבצע את שתי הפעולות לעיל.
  • erasedups מסיר כל עותק ישן יותר של השורה הנשמרת.
export HISTCONTROL=ignoreboth

כאשר ignorespace פעיל, רווח בודד בתחילת השורה מונע מהפקודה להיכנס לרשימה שבזיכרון, ולכן היא לעולם לא מגיעה לקובץ. בדקו את הערך לפני שאתם מסתמכים עליו באמצעות echo "$HISTCONTROL". משתמש ברירת המחדל ב-Ubuntu מגדיר .bashrc את ignoreboth, אך shell של root או image מינימלי עשויים להשאיר אותו ללא הגדרה; במקרה כזה, הרווח בתחילת השורה לא יבצע דבר והשורה תישמר ככל שורה אחרת.

היו כנים עם עצמכם לגבי מה שזה מגן עליו. השורה נשארת מחוץ ל-~/.bash_history. היא עדיין גלויה בפלט של ps בזמן שהתהליך רץ, כך שכל משתמש אחר במכונה יכול לקרוא אותה שם. sudo מתעד את הפקודה שהריץ ביומן המערכת. קובץ ההיסטוריה הוא טקסט פשוט, לכן שמרו אותו במצב 600 וזכרו שכל מי שיכול לקרוא את תיקיית הבית שלכם יכול לקרוא את אלפי הפקודות האחרונות שלכם.

אם מידע רגיש כבר נמצא ברשימה, הסירו את הרשומה וכתבו מחדש את הקובץ:

history
history -d 517
history -w

history -d מסיר את הרשומה מהזיכרון, ו-history -w כותב את הרשימה הנוכחית על גבי הקובץ. history -c מנקה את הרשימה כולה. HISTIGNORE הוא הכפתור הרלוונטי לרעשי רקע ולא לסודות: HISTIGNORE='ls:pwd:history:clear' שומר על שורות אלו מחוץ לרשימה כדי שהחיפושים שלכם יחזירו תוצאות שימושיות.

מדוע הרחבת היסטוריה אינה פועלת בתוך סקריפט shell

הרחבת היסטוריה (history expansion) שייכת ל־shells אינטראקטיביים. סקריפט רץ בתוך shell לא-אינטראקטיבי, שבו רשימת ההיסטוריה אינה פעילה וההרחבה כבויה, לכן !! ו־!$ נשארים בשורה כטקסט רגיל. sudo !! בתוך סקריפט מורה ל־sudo להריץ פקודה ששמה המילולי הוא !!, והפעולה נכשלת.

בדקו את ה־shell שבו אתם עובדים:

echo $-

הפלט הוא קבוצת דגלי האופציות הנוכחיים, משהו בסגנון himBHs. i מציין שה־shell אינטראקטיבי ו־H מציין שהרחבת היסטוריה פעילה. הריצו את אותה שורה בתוך סקריפט, ואף אחת מהאותיות הללו לא תופיע.

זהו קו ההפרדה בין שני חלקי העבודה שלכם ב־shell. בשורת הפקודה, !$ ו־Ctrl-R חוסכים הקלדות על פקודות שאתם עדיין יכולים לראות. בתוך קובץ, עליכם לתת שמות לדברים במקום זאת: הציבו את הנתיב במשתנה, או לכדו פלט באמצעות command substitution. סקריפט שהיה מסתמך על ההיסטוריה האישית שלכם היה מבצע פעולות שונות עבור האדם הבא שיריץ אותו, וזהו ההפך מהמטרה שלשמה נועד סקריפט.

מסיבה זו, כל דוגמה בדף זה נועדה להקלדה בשורת פקודה פעילה. אף אחת מהן לא תתנהג באותו אופן אם תודבק לתוך קובץ .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. הוא לוקח את ה"מילה" האחרונה בשורה, לכן יעד של הפניה (redirection) בסוף השורה נחשב למילה האחרונה. ניתן גם לצרף טקסט אליו: !$/config.env מוסיף לנתיב, כיוון שמציין המילה נעצר ב-/. החלופה האינטראקטיבית היא Alt-., שמכניסה את אותו טקסט לשורת הפקודה שלך כדי שתוכל לקרוא אותו לפני לחיצה על Enter.

מדוע bash מציג "event not found" כשהטקסט שלי מכיל סימן קריאה?

מרכאות כפולות אינן מגנות על ! מפני הרחבת היסטוריה, לכן echo "done!" גורם ל-bash לחפש אירוע היסטוריה ולהדפיס bash: !": event not found. גרשיים בודדים כן מגנים עליו, לכן כתוב echo 'done!'. לוכסן הפוך (backslash) עוצר את ההרחבה גם הוא, אך בתוך מרכאות כפולות bash משאיר את הלוכסן בפלט. כדי להדביק בלוק ארוך המכיל !, כבה את ההרחבה עבור הסשן הנוכחי עם set +H.

מדוע !! ו-!$ לא עובדים בסקריפט ה-shell שלי?

הרחבת היסטוריה מופעלת רק ב-shells אינטראקטיביים. סקריפט רץ בצורה לא-אינטראקטיבית, לכן ה-shell לעולם לא בונה רשימת היסטוריה, ו-!! נשאר בשורה כטקסט פשוט. הרץ echo $- כדי לראות באיזה מצב אתה נמצא: shell אינטראקטיבי מדפיס דגלים הכוללים את i ו-H, בעוד סקריפט לא מדפיס אף אחד מהם. בסקריפטים, השתמש במשתנה או ב-command substitution במקום זאת.

איך שומרים סיסמה מחוץ להיסטוריית ה-bash שלי?

הגדר את HISTCONTROL=ignorespace או HISTCONTROL=ignoreboth ב-~/.bashrc, ולאחר מכן התחל את הפקודה ברווח בודד והיא לעולם לא תישמר. ודא את הערך עם echo "$HISTCONTROL" תחילה, כיוון שאם המשתנה לא מוגדר, הרווח המוביל לא עושה דבר. פעולה זו רק שומרת את השורה מחוץ ל-~/.bash_history. הפקודה עדיין גלויה ב-ps בזמן שהיא רצה, ו-sudo מתעד את מה שהיא ביצעה. אם סוד כבר נשמר, מצא את המספר שלו עם history, ולאחר מכן הרץ history -d <number> ואחריו history -w כדי לכתוב מחדש את הקובץ.