SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-08

Как использовать !! и !$ в bash

Изучите шесть способов раскрытия истории 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

Этот вывод echo — ваша проверка. Прочитайте его до того, как изучать результат выполнения команды, так как это единственный шанс увидеть, что именно bash подставил вместо !$.

Цепочка работает, потому что bash сохраняет в истории раскрытую строку, а не тот !$, который вы ввели. Поэтому строка 3 берет свой последний аргумент из раскрытой строки 2, а строка 4 — из строки 3. Строка 4 также показывает, что за десигнатором может следовать текст: !$/config.env превращается в путь с /config.env в конце, так как слово-десигнатор заканчивается на /.

Еще один пример из той же сессии. После редактирования файла вы хотите вывести список содержимого каталога, в котором он находится:

ls -l !$:h

:h — это модификатор head. Он отсекает последний компонент пути, выполняя ту же задачу, что и 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 !*

Фраза «кроме первого» выполняет важную работу. !* отбрасывает только нулевое слово, поэтому опции передаются вместе с путями. После ls -l file1 file2, !* становится -l file1 file2, поэтому sudo chmod 600 !* завершается ошибкой, так как chmod получает -l в качестве аргумента. Нумеруйте слова начиная с нуля и используйте срез: !!:2* означает слова со второго и до конца.

Та же ловушка возникает при использовании sudo. После sudo chown deploy:deploy /srv/www/app нулевое слово — это sudo, а первое — 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 и ничего не запускает, что является безопасным поведением.

Исправление опечатки с помощью ^старое^новое

sudo systemctl status ngnix
Unit 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

Механизм раскрытия истории (history expansion) срабатывает до обработки кавычек, а двойные кавычки не защищают символ !. Это делают только одинарные кавычки или обратная косая черта. Поэтому 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 строк. Значения по умолчанию в Ubuntu ~/.bashrc составляют 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 добавляет новые строки после каждого приглашения командной строки, поэтому даже если сессия была завершена аварийно, а не корректно, команды останутся в файле. Значение по умолчанию в Ubuntu ~/.bashrc уже включает histappend. Это становится тем важнее, чем больше машин вы обслуживаете, так как история хранится отдельно для каждого пользователя на каждой машине, поэтому, когда вы управляете несколькими серверами с одной рабочей станции, нужная вам !$ может просто находиться на другом хосте.

Защита секретов от попадания в bash history

Переменная HISTCONTROL определяет, какие строки будут сохранены в истории.

  • Значение ignorespace отбрасывает любую строку, начинающуюся с пробела.
  • Значение ignoredups отбрасывает строку, если она идентична предыдущей.
  • Значение ignoreboth выполняет оба указанных выше действия.
  • Значение erasedups удаляет все предыдущие копии сохраняемой строки.
export HISTCONTROL=ignoreboth

При активном ignorespace один начальный пробел предотвращает попадание команды в список в оперативной памяти, поэтому она не записывается и в файл. Проверьте значение переменной перед тем, как полагаться на неё, с помощью echo "$HISTCONTROL". В Ubuntu по умолчанию для пользователя .bashrc установлено значение ignoreboth, но в оболочке root или в минималистичных образах переменная может быть не задана. В таком случае начальный пробел не сработает, и строка будет сохранена как обычно.

Трезво оценивайте уровень защиты. Строка не попадает в ~/.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) относится только к интерактивным оболочкам. Скрипт выполняется в неинтерактивной оболочке, где список истории отключен, а раскрытие не производится, поэтому !! и !$ остаются в строке как обычный текст. 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.

Почему !! и !$ не работают в моем скрипте?

Раскрытие истории включено только в интерактивных оболочках. Скрипт выполняется неинтерактивно, поэтому оболочка не ведет список истории, а !! остается в строке как обычный текст. Выполните 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