SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-13

Jak używać !! oraz !$ w bash history expansion

Składnia !! powtarza ostatnie polecenie, a !$ wstawia jego ostatni argument. Poznaj sześć kluczowych operacji rozwijania historii oraz modyfikator :p do podglądu przed wykonaniem.

Na czym polega rozwijanie historii w bash

Rozwijanie historii w bash odtwarza wcześniejszą linię poleceń z historii powłoki przed jej uruchomieniem. !! powtarza poprzednie polecenie. !$ wstawia ostatni argument poprzedniego polecenia. Podstawienie ma charakter tekstowy i odbywa się jako pierwsze, jeszcze przed podziałem linii na słowa przez bash, dlatego zwracane są dokładnie te znaki, które wpisano za pierwszym razem.

Sześć form obejmuje niemal wszystkie operacje wykonywane na serwerze:

  • !! powtarza całą poprzednią linię, a sudo !! uruchamia ją ponownie z uprawnieniami root.
  • !$ to ostatni argument poprzedniej linii.
  • !* to wszystkie argumenty poprzedniej linii.
  • !n uruchamia wpis historii o numerze n, a !-n odlicza wstecz od bieżącej pozycji.
  • !string uruchamia najnowsze polecenie, które rozpoczynało się od string.
  • ^old^new uruchamia ponownie poprzednią linię, zastępując pierwsze wystąpienie old ciągiem new.

Wszystkie poniższe przykłady wpisuje się w interaktywnym wierszu poleceń na własnym serwerze. Rozwijanie historii jest wyłączone w skryptach, co wyjaśnia ostatnia sekcja.

Długa ścieżka wpisana tylko raz

Oto przypadek, który zwraca się z nawiązką. Przygotowujesz katalog dla release, a ścieżka jest na tyle długa, że wpisanie jej po raz drugi zwiększa ryzyko literówki.

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

Przed wykonaniem każdej linii, bash wyświetla linię wynikającą z rozwinięcia:

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

To echo stanowi weryfikację. Przeczytaj je przed analizą wyjścia polecenia, ponieważ jest to jedyna szansa, aby zobaczyć, co bash podstawił pod !$.

Łańcuch działa, ponieważ bash przechowuje w historii rozwiniętą linię, a nie wpisany przez użytkownika !$. Dlatego linia 3 pobiera swój ostatni argument z rozwiniętej linii 2, a linia 4 pobiera go z linii 3. Linia 4 pokazuje również, że po desygnatorze może wystąpić tekst: !$/config.env staje się ścieżką z dopisanym na końcu /config.env, ponieważ desygnator słowa kończy się na /.

Jeszcze jeden przykład z tej samej sesji. Po edycji pliku chcesz wyświetlić zawartość katalogu, w którym się znajduje:

ls -l !$:h

:h to modyfikator części nadrzędnej (head). Usuwa on ostatni element ścieżki, wykonując to samo zadanie co dirname. :t zachowuje tylko ostatni element, :r usuwa rozszerzenie, a :e zachowuje tylko rozszerzenie.

Powtarzanie ostatniego polecenia za pomocą !! oraz sudo !!

Zapominasz o sudo, a menedżer usług zgłasza błąd:

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

Bash zastępuje !! tekstem poprzedniej linii, więc powłoka wykonuje sudo systemctl restart nginx.

!! to dokładnie poprzednia linia, niezależnie od tego, co nią było. W tym miejscu dochodzi do typowej pomyłki. Zakładasz, że ostatnim poleceniem było to, które zakończyło się niepowodzeniem, ale w międzyczasie uruchomiłeś cd, history lub nieudane polecenie było dwie linie wcześniej. sudo !! wykonuje wtedy niewłaściwe polecenie z uprawnieniami root. Wyświetl je najpierw, jeśli nie masz pewności:

sudo !!:p

:p wypisuje rozwiniętą linię i nie wykonuje jej. Wypisana linia jest dodawana do historii, więc jeśli wygląda poprawnie, zwykłe !! w następnej linii ją uruchomi.

Ponowne użycie ostatniego argumentu za pomocą !$

!$ to ostatnie słowo poprzedniej linii. Jest to forma używana najczęściej, ponieważ ostatnim słowem jest zazwyczaj obiekt, na którym aktualnie pracujesz: ścieżka lub nazwa usługi.

sudo systemctl status nginx
sudo systemctl reload !$

Dwie kwestie są w tym przypadku zaskakujące.

Po pierwsze, !$ to ostatnie słowo, a nie ostatni argument, o którym mogłeś myśleć. Jeśli poprzednia linia kończyła się przekierowaniem, ostatnim słowem jest cel przekierowania:

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

W tym przypadku działa to poprawnie. Jednak po journalctl -u nginx > /tmp/log.txt, !$ to /tmp/log.txt, a nie nginx. Przeczytaj wyświetloną linię.

Po drugie, rozwinięcie ma charakter tekstowy, więc zmienna powraca w formie nierozwiniętej. Po ls $HOME/backups, !$ zwraca znaki $HOME/backups, które bash następnie rozwija ponownie jako zwykły parametr. Pamiętaj o tej kolejności: rozwinięcie historii następuje przed rozwinięciem parametrów oraz przed podstawieniem polecenia za pomocą $( ), więc mechanizm ten nigdy nie widzi wartości, a jedynie tekst.

Warto znać również pokrewne formy. !^ to pierwszy argument, !:2 to drugi, a !:2-4 to zakres. !!:$ to pełny zapis !$.

Jeśli wolisz zobaczyć tekst przed jego zatwierdzeniem, naciśnij Alt-. (lub Esc, a następnie .). Readline wstawi ostatni argument poprzedniego polecenia bezpośrednio do wiersza poleceń, gdzie możesz go edytować. Naciśnij ponownie, aby cofnąć się do ostatniego argumentu polecenia sprzed tego. Nic nie zostanie uruchomione, dopóki nie naciśniesz Enter.

Przekazywanie wszystkich argumentów za pomocą !*

!* to każde słowo z poprzedniej linii poza pierwszym.

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

Fraza "poza pierwszym" ma kluczowe znaczenie. !* usuwa tylko słowo 0, dzięki czemu opcje są przekazywane wraz ze ścieżkami. Po ls -l file1 file2, !* to -l file1 file2, więc sudo chmod 600 !* kończy się niepowodzeniem, ponieważ chmod otrzymuje -l jako argument. Należy numerować słowa od zera i użyć wycinka: !!:2* oznacza słowa od 2 do końca.

Ten sam problem występuje w przypadku sudo. Po sudo chown deploy:deploy /srv/www/app, słowo 0 to sudo, a słowo 1 to chown, więc !* przekazuje następnemu poleceniu chown deploy:deploy /srv/www/app. Rzadko jest to pożądany rezultat.

Wybieranie polecenia według pozycji za pomocą !n oraz !-n

history wyświetla listę z numerem przed każdym wpisem.

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 uruchamia ponownie wpis 513. !-2 uruchamia wpis znajdujący się dwie linie wstecz, licząc od linii, w której obecnie piszesz, więc !-1 oraz !! oznaczają to samo.

Sprawdź numery bezpośrednio przed ich użyciem. !-2 wskazuje na nowe miejsce za każdym razem, gdy uruchomisz cokolwiek, w tym ls, które zostało wywołane bez zastanowienia. Numery bezwzględne są stabilne w obrębie jednej sesji, ale nie są tymi samymi numerami w drugiej sesji na tej samej maszynie i nie są takie same po tym, jak kolejne logowanie wczyta plik historii. Numer zapamiętany wczoraj wskazuje dzisiaj na inne polecenie.

Ponowne uruchomienie polecenia za pomocą prefiksu !string

!string uruchamia ostatnio wykonane polecenie, które rozpoczyna się od string.

!ss

To polecenie uruchamia ponownie ss -tulpn z powyższej listy, czyli sprawdzenie gniazd nasłuchujących opisane w które porty są otwarte na serwerze Linux. !?string? dopasowuje ciąg znaków w dowolnym miejscu linii, a nie tylko na jej początku, co jest przydatne, gdy pamiętany jest argument, a nie nazwa polecenia.

Należy stosować długie prefiksy. !s dopasuje ss, sudo, systemctl lub shutdown, zależnie od tego, które z nich zostało uruchomione jako ostatnie, co może prowadzić do niepewności co do wyniku przed wykonaniem polecenia. !string:p wyświetla dopasowane polecenie bez jego uruchamiania. Jeśli nie znaleziono dopasowania, bash wyświetla bash: !ss: event not found i nie wykonuje żadnej akcji, co jest bezpiecznym zachowaniem.

Poprawianie literówki za pomocą ^stary^nowy

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

Powłoka uruchamia ponownie poprzednią linię, zastępując pierwsze wystąpienie ngnix przez nginx. Zmieniane jest tylko pierwsze dopasowanie. Aby zmienić każde wystąpienie, należy użyć pełnej formy !!:gs/ngnix/nginx/, gdzie s oznacza podstawienie, a g oznacza wykonanie operacji w całej linii.

Wyświetlanie rozwinięcia przed wykonaniem

Dwa nawyki zapobiegają przypadkom, w których rozwinięcie uruchamia niepożądane polecenie.

Pierwszym z nich jest :p, który już znasz. Dodaj go do rozwinięcia, a bash wyświetli wynik zamiast go uruchamiać: !!:p lub !systemctl:p. Wyświetlona linia trafia do historii, więc późniejsze !! uruchamia to, co właśnie sprawdziłeś.

Drugi sposób jest skuteczniejszy, ponieważ dotyczy każdego rozwinięcia bez konieczności pamiętania o czymkolwiek:

shopt -s histverify

Umieść tę linię w ~/.bashrc. Gdy opcja histverify jest ustawiona, rozwinięcie nie uruchamia się po naciśnięciu klawisza Enter. Bash zapisuje rozwiniętą linię w wierszu poleceń, co pozwala ją przeczytać i edytować, a drugie naciśnięcie klawisza Enter uruchamia polecenie. Kosztuje to jedno naciśnięcie klawisza i eliminuje całą klasę błędów. Wymaga to readline, więc działa tylko w interaktywnym wierszu poleceń.

Ctrl-R to alternatywny sposób pracy, który nigdy nie wykonuje rozwinięć. Naciśnij Ctrl-R i wpisz fragment polecenia. Wiersz poleceń zmieni się w monit reverse-i-search i pokaże ostatnie dopasowanie w miarę pisania. Naciśnij ponownie Ctrl-R, aby cofnąć się do starszych dopasowań. Klawisz Enter uruchamia wyświetlaną linię. Ctrl-G anuluje wyszukiwanie i przywraca oryginalny wiersz poleceń. Klawisz strzałki w lewo kończy wyszukiwanie i pozostawia dopasowaną linię w wierszu poleceń do edycji.

Używaj Ctrl-R, gdy chcesz najpierw zobaczyć polecenie. Używaj !! oraz !$, gdy polecenie pochodzi sprzed jednej linii i nadal widzisz je na ekranie.

Dlaczego polecenie echo "done!" zwraca błąd event not found

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

Mechanizm rozwijania historii (history expansion) uruchamia się przed interpretacją cudzysłowów, a podwójny cudzysłów nie chroni znaku !. Robią to jedynie pojedynczy cudzysłów oraz znak odwrotnego ukośnika. Dlatego echo 'deploy done!' wyświetla oczekiwany wynik. Odwrotny ukośnik również blokuje rozwijanie, jednak wewnątrz podwójnych cudzysłowów bash pozostawia go w wyjściu, więc pojedynczy cudzysłów jest czystszym rozwiązaniem.

Problem ten jest najbardziej dotkliwy w przypadku haseł, ponieważ silne hasło często zawiera znak !. Polecenie typu mysql -u app -p"S3cret!pass" kończy się błędem event-not-found lub, jeśli w historii istnieje pasujący wpis, cicho przesyła inny ciąg znaków. Należy używać pojedynczych cudzysłowów lub, co jest lepszą praktyką, pozwolić narzędziu na wyświetlenie monitu o hasło, aby sekret nigdy nie trafił do linii poleceń. Ten nawyk powinien stanowić element higieny pracy na koncie: zobacz pierwsze dziesięć minut na nowym VPS, aby dowiedzieć się, jak zarządzać kluczami i hasłami na świeżo skonfigurowanej maszynie.

Jeśli planujesz wkleić blok tekstu zawierający wiele znaków !, wyłącz rozwijanie historii dla bieżącej sesji za pomocą set +H, a po zakończeniu włącz je ponownie poleceniem set -H.

HISTSIZE, HISTFILESIZE oraz miejsce przechowywania historii

Dwie zmienne, które są często mylone ze względu na podobne nazwy.

  • HISTSIZE określa liczbę poleceń przechowywanych w pamięci przez aktywną powłokę.
  • HISTFILESIZE określa liczbę wierszy przechowywanych w pliku na dysku, ~/.bash_history chyba że HISTFILE stanowi inaczej.

Plik jest zapisywany w momencie zakończenia pracy powłoki i w tym samym czasie przycinany do HISTFILESIZE wierszy. Domyślna konfiguracja ~/.bashrc w systemie Ubuntu ustawia wartości 1000 oraz 2000. Sprawdź aktualne ustawienia:

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

Zwiększ obie wartości, jeśli wyszukiwanie w !string nie zwraca poleceń, które zostały wykonane. Wartości 10000 oraz 20000 są powszechnie stosowane i nie obciążają systemu, ponieważ plik jest zwykłym tekstem. Wartość ujemna oznacza brak limitu.

Znaczniki czasu ułatwiają pracę po powrocie do serwera po dłuższej przerwie:

export HISTTIMEFORMAT='%F %T '

history powoduje wyświetlenie daty i godziny przed każdym wpisem, ponieważ bash zapisuje w pliku linię komentarza z czasem w formacie epoch przed każdym poleceniem.

Częstym problemem jest znikanie historii podczas korzystania z wielu terminali. Każda powłoka przechowuje własną listę w pamięci i zapisuje ją przy wyjściu, więc bez histappend ostatnia zamknięta powłoka nadpisuje dane zapisane przez pozostałe. Dwa ustawienia rozwiązują ten problem:

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

histappend sprawia, że powłoka dopisuje swoją listę na końcu pliku zamiast go zastępować. history -a dopisuje nowe linie po każdym znaku zachęty, dzięki czemu sesja, która została przerwana, a nie zamknięta poprawnie, również zachowuje swoje polecenia. Domyślna konfiguracja ~/.bashrc w systemie Ubuntu ustawia już histappend. Ma to większe znaczenie przy obsłudze wielu maszyn, ponieważ historia jest przechowywana dla każdego użytkownika na każdym hoście, więc podczas zarządzania kilkoma serwerami z jednej stacji roboczej poszukiwane !$ może znajdować się na innym serwerze.

Ochrona sekretów przed zapisem w historii bash

HISTCONTROL określa, które linie są w ogóle zapisywane.

  • ignorespace odrzuca każdą linię rozpoczynającą się od spacji.
  • ignoredups odrzuca linię identyczną z poprzednią.
  • ignoreboth wykonuje obie powyższe operacje.
  • erasedups usuwa każdą starszą kopię zapisywanej linii.
export HISTCONTROL=ignoreboth

Przy aktywnej zmiennej ignorespace, jedna spacja na początku linii zapobiega zapisaniu polecenia na liście w pamięci, dzięki czemu nie trafia ono do pliku. Przed poleganiem na tym mechanizmie należy sprawdzić wartość zmiennej za pomocą echo "$HISTCONTROL". Domyślny plik .bashrc w systemie Ubuntu ustawia ignoreboth, jednak powłoka root lub minimalny obraz systemu mogą pozostawić tę zmienną nieustawioną. W takim przypadku spacja na początku linii nie przyniesie efektu, a polecenie zostanie zapisane jak każde inne.

Należy mieć świadomość zakresu ochrony. Linia nie trafia do ~/.bash_history. Pozostaje ona jednak widoczna w danych wyjściowych ps podczas działania procesu, więc każdy inny użytkownik systemu może ją tam odczytać. sudo rejestruje uruchomione polecenie w dzienniku systemowym. Plik historii jest zwykłym plikiem tekstowym, dlatego należy ustawić dla niego uprawnienia 600 i pamiętać, że każdy użytkownik z dostępem do katalogu domowego może odczytać ostatnie kilka tysięcy poleceń.

Jeśli poufne dane znajdują się już na liście, należy usunąć wpis i nadpisać plik:

history
history -d 517
history -w

history -d usuwa dany wpis z pamięci, a history -w zapisuje bieżącą listę do pliku. history -c czyści całą listę. HISTIGNORE to powiązany parametr służący do eliminacji szumu, a nie ochrony sekretów: HISTIGNORE='ls:pwd:history:clear' wyklucza te linie z listy, dzięki czemu wyszukiwanie zwraca użyteczne wyniki.

Dlaczego rozwijanie historii nie działa w skrypcie powłoki

Rozwijanie historii jest funkcją powłok interaktywnych. Skrypt uruchamia się w powłoce nieinteraktywnej, w której lista historii jest wyłączona, a rozwijanie nieaktywne, dlatego !! oraz !$ pozostają w linii jako zwykły tekst. sudo !! wewnątrz skryptu nakazuje sudo uruchomienie polecenia o dosłownej nazwie !!, co kończy się niepowodzeniem.

Sprawdź parametry powłoki, w której aktualnie pracujesz:

echo $-

Wynikiem jest zestaw aktualnych flag opcji, na przykład himBHs. i oznacza, że powłoka jest interaktywna, a H oznacza, że rozwijanie historii jest włączone. Uruchomienie tej samej linii wewnątrz skryptu spowoduje, że żadna z tych liter nie pojawi się w wyniku.

To jest granica między dwiema połowami pracy z powłoką. W wierszu poleceń !$ oraz Ctrl-R oszczędzają wpisywanie komend, które wciąż widzisz. W pliku należy nazywać rzeczy wprost: umieść ścieżkę w zmiennej lub przechwyć wynik za pomocą podstawienia polecenia. Skrypt zależny od osobistej historii użytkownika zachowywałby się inaczej u każdej kolejnej osoby, co jest przeciwieństwem celu, któremu służą skrypty.

Z tego powodu każdy przykład na tej stronie jest przeznaczony do wpisania w aktywnym wierszu poleceń. Żaden z nich nie zadziała w ten sam sposób po wklejeniu do pliku .sh.

FAQ

Co oznacza !! w bash?

!! rozwija się do pełnej treści poprzedniego wiersza poleceń, więc sudo !! uruchamia ostatnie polecenie z uprawnieniami root. Rozwinięcie jest tekstowe i następuje przed analizą wiersza przez bash, a powłoka wyświetla gotowy wiersz tuż przed jego wykonaniem. Jeśli nie masz pewności, co zawierał poprzedni wiersz, wpisz najpierw sudo !!:p. :p wyświetla rozwinięcie bez jego uruchamiania i dodaje wiersz do historii, dzięki czemu późniejsze !! wykona to polecenie.

Jak ponownie użyć ostatniego argumentu poprzedniego polecenia?

Użyj !$. Po wykonaniu sudo mkdir -p /srv/www/app/releases, wiersz ls -ld !$ staje się ls -ld /srv/www/app/releases. Pobiera on ostatnie słowo wiersza, więc celem przekierowania na końcu wiersza jest traktowany jako ostatnie słowo. Możesz również dopisać do niego tekst: !$/config.env rozszerza ścieżkę, ponieważ desygnator słowa kończy się na /. Interaktywną alternatywą jest Alt-., który wstawia ten sam tekst do wiersza poleceń, co pozwala na jego weryfikację przed naciśnięciem Enter.

Dlaczego bash zgłasza "event not found", gdy tekst zawiera wykrzyknik?

Cudzysłowy nie chronią ! przed rozwinięciem historii, więc echo "done!" powoduje, że bash szuka zdarzenia w historii i wyświetla bash: !": event not found. Pojedyncze cudzysłowy chronią przed tym, więc należy zapisać echo 'done!'. Ukośnik odwrotny (backslash) również zatrzymuje rozwinięcie, ale wewnątrz cudzysłowów bash pozostawia ukośnik w danych wyjściowych. Aby wkleić długi blok zawierający !, wyłącz rozwinięcie dla bieżącej sesji za pomocą set +H.

Dlaczego !! i !$ nie działają w skrypcie powłoki?

Rozwinięcie historii jest włączone tylko w powłokach interaktywnych. Skrypt działa w trybie nieinteraktywnym, więc powłoka nie tworzy listy historii, a !! pozostaje w wierszu jako zwykły tekst. Uruchom echo $-, aby sprawdzić tryb pracy: powłoka interaktywna wyświetla flagi zawierające i oraz H, a skrypt nie wyświetla żadnej z nich. W skryptach używaj zmiennych lub podstawienia poleceń.

Jak uniknąć zapisywania hasła w historii bash?

Ustaw HISTCONTROL=ignorespace lub HISTCONTROL=ignoreboth w ~/.bashrc, a następnie rozpocznij polecenie pojedynczą spacją, aby nie zostało zapisane. Najpierw potwierdź wartość za pomocą echo "$HISTCONTROL", ponieważ jeśli zmienna jest nieustawiona, wiodąca spacja nie zadziała. Chroni to jedynie przed zapisem w ~/.bash_history. Polecenie pozostaje widoczne w ps podczas wykonywania, a sudo rejestruje jego uruchomienie. Jeśli hasło zostało już zapisane, znajdź jego numer za pomocą history, a następnie wykonaj history -d <number> wraz z history -w, aby nadpisać plik.