Розгортання історії Bash: як працюють !! та !$
Дізнайтеся про шість форм розгортання історії Bash: !!, !$, !*, !n, !string і ^old^new^. Окремо показано :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 також показує, що після designator може стояти текст: !$/config.env перетворюється на шлях із /config.env у кінці, оскільки word designator закінчується на /.
Ще один приклад із того самого сеансу. Після редагування файлу ви хочете вивести каталог, у якому він міститься:
ls -l !$:h:h — це head modifier. Він видаляє останній компонент зі шляху; так само працює dirname. :t залишає лише останній компонент, :r видаляє розширення, а :e залишає лише розширення.
Повторіть останню команду за допомогою !! і sudo !!
Ви забули sudo, і менеджер служб повідомляє про це:
systemctl restart nginxFailed 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 зі списку вище — перевірку listening socket, описану в розділі які порти відкриті на Linux-сервері. !?string? шукає збіг у будь-якому місці рядка, а не лише на початку. Це зручно, якщо ви пам’ятаєте аргумент, але не назву команди.
Використовуйте довгий префікс. !s може збігтися з ss, sudo, systemctl або shutdown — залежно від того, яка з них виконувалася останньою. Ви не знатимете, яку саме команду буде виконано, доки це не станеться. !string:p виводить знайдений збіг, не виконуючи його. Якщо збігів немає, bash виводить bash: !ss: event not found і нічого не виконує. Це безпечний результат.
Виправлення однієї помилки за допомогою ^old^new
sudo systemctl status ngnixUnit ngnix.service could not be found.^ngnix^nginxОболонка повторно виконує попередній рядок, замінюючи перше 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 seconds.
Поширена проблема полягає в тому, що історія зникає під час роботи з кількома терміналами. Кожна оболонка зберігає власний список у пам’яті та записує його під час завершення роботи. Тому без 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, але shell root або мінімальний образ можуть залишити цю змінну невстановленою. У такому разі початковий пробіл нічого не змінює, і рядок зберігається як будь-який інший.
Реалістично оцінюйте, від чого це вас захищає. Рядок не потрапляє до ~/.bash_history. Поки процес виконується, його все одно видно у виведенні ps, тому будь-який інший користувач сервера може прочитати його там. sudo записує виконану команду в системний журнал. Файл історії зберігається у відкритому текстовому форматі, тому встановіть для нього режим 600 і пам’ятайте: будь-хто, хто може читати ваш домашній каталог, може прочитати кілька тисяч останніх команд.
Якщо конфіденційні дані вже потрапили до списку, видаліть відповідний запис і перезапишіть файл:
history
history -d 517
history -whistory -d видаляє цей запис із пам’яті, а history -w записує поточний список поверх файлу. history -c очищає весь список. HISTIGNORE — це пов’язаний параметр для зайвих рядків, а не секретів: HISTIGNORE='ls:pwd:history:clear' не додає такі рядки до списку, щоб результати пошуку містили корисні збіги.
Чому розширення історії не працює у shell-скрипті
Розширення історії призначене для інтерактивних shell. Скрипт виконується в неінтерактивному shell, де список історії не ввімкнений, а розширення вимкнене, тому !! і !$ залишаються в рядку як звичайний текст. sudo !! у скрипті передає sudo команду з буквальним ім’ям !!, і вона завершується помилкою.
Перевірте shell, у якому ви зараз працюєте:
echo $-У виведенні буде набір поточних прапорців параметрів, наприклад himBHs. i означає, що shell інтерактивний, а H — що розширення історії ввімкнене. Виконайте той самий рядок у скрипті — там не буде жодної з цих літер.
Це межа між двома способами роботи із shell. У командному рядку !$ і 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 додає текст до шляху, оскільки designator слова зупиняється на /. Інший варіант для інтерактивної роботи — Alt-.. Він вставляє такий самий текст у prompt, щоб ви могли перевірити його перед натисканням 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, щоб перезаписати файл.