SSD Nodes Learn 🎉 VPS ab $4.99/Monat
Anleitungen Matt ConnorVon Matt Connor

Bash-History-Expansion: !! und !$ richtig nutzen

!! wiederholt den letzten Befehl, !$ übernimmt sein letztes Argument. Lernen Sie sechs wichtige Bash-Erweiterungen und :p zum Prüfen vor der Ausführung kennen.

Was die Bash-History-Expansion bewirkt

Die Bash-History-Expansion erstellt vor der Ausführung durch Bash eine frühere Befehlszeile aus der Shell-Historie neu. !! wiederholt den vorherigen Befehl. !$ fügt das letzte Argument des vorherigen Befehls ein. Die Ersetzung erfolgt als Text und zuerst, bevor Bash die Zeile in Wörter aufteilt. Daher werden genau die Zeichen verwendet, die Sie beim ersten Mal eingegeben haben.

Sechs Formen decken fast alle Vorgänge ab, die Sie auf einem Server ausführen:

  • !! wiederholt die gesamte vorherige Zeile, und sudo !! führt sie erneut als root aus.
  • !$ bezeichnet das letzte Argument der vorherigen Zeile.
  • !* bezeichnet alle Argumente der vorherigen Zeile.
  • !n führt den Verlaufseintrag mit der Nummer n aus, und !-n zählt von der aktuellen Position aus rückwärts.
  • !string führt den letzten Befehl aus, der mit string begonnen hat.
  • ^old^new führt die vorherige Zeile erneut aus und ersetzt dabei das erste old durch new.

Alle folgenden Eingaben erfolgen an einer interaktiven Eingabeaufforderung auf Ihrem eigenen Server. In Skripten ist die History-Expansion deaktiviert. Der letzte Abschnitt erläutert dies.

Ein langer Pfad, einmal eingegeben

Hier zeigt sich der praktische Nutzen unmittelbar. Sie bereiten ein Release-Verzeichnis vor, und der Pfad ist so lang, dass eine erneute Eingabe leicht zu einem Tippfehler führt.

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

Vor jeder Zeile gibt bash die durch die Expansion erzeugte Zeile aus:

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

Diese Ausgabe dient als Kontrolle. Lesen Sie sie, bevor Sie die Ausgabe des Befehls prüfen. Nur dort sehen Sie, was bash unter !$ verstanden hat.

Die Kette funktioniert, weil bash die expandierte Zeile in der History speichert, nicht das eingegebene !$. Daher übernimmt Zeile 3 ihr letztes Argument aus der expandierten Zeile 2, und Zeile 4 übernimmt es aus Zeile 3. Zeile 4 zeigt außerdem, dass nach einem Designator weiterer Text stehen kann: !$/config.env wird zum Pfad mit /config.env am Ende, weil der Wort-Designator beim / endet.

Noch ein Beispiel aus derselben Sitzung. Nach dem Bearbeiten der Datei möchten Sie das Verzeichnis auflisten, in dem sie liegt:

ls -l !$:h

:h ist der Kopfmodifikator. Er entfernt die letzte Komponente aus einem Pfad. Das entspricht der Funktion von dirname. :t behält nur die letzte Komponente, :r entfernt die Dateierweiterung, und :e behält nur die Dateierweiterung.

Den letzten Befehl mit !! und sudo !! wiederholen

Sie vergessen sudo, und der Dienstmanager weist Sie darauf hin:

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

Bash ersetzt !! durch den Text der vorherigen Zeile. Dadurch führt die Shell sudo systemctl restart nginx aus.

!! ist genau die vorherige Zeile, unabhängig davon, welcher Befehl dort stand. Hier passiert der klassische Fehler. Sie gehen davon aus, dass der letzte Befehl der fehlgeschlagene war. Danach haben Sie jedoch möglicherweise noch einen cd oder einen history ausgeführt, oder der fehlgeschlagene Befehl stand zwei Zeilen weiter zurück. sudo !! führt dann den falschen Befehl mit Root-Rechten aus. Geben Sie den Befehl zuerst aus, wenn Sie nicht sicher sind:

sudo !!:p

:p gibt die erweiterte Zeile aus, führt sie aber nicht aus. Die ausgegebene Zeile wird in Ihre Historie aufgenommen. Wenn sie korrekt ist, führt ein einfaches !! in der nächsten Zeile den Befehl aus.

Das letzte Argument mit !$ wiederverwenden

!$ ist das letzte Wort der vorherigen Zeile. Diese Form verwenden Sie am häufigsten, weil das letzte Wort normalerweise das Objekt Ihrer aktuellen Arbeit ist: ein Pfad oder ein Dienstname.

sudo systemctl status nginx
sudo systemctl reload !$

Hier sorgen zwei Dinge häufig für Überraschung.

Erstens ist !$ das letzte Wort, nicht das letzte Argument, das Sie im Sinn hatten. Wenn die vorherige Zeile mit einer Umleitung endete, ist das letzte Wort das Ziel der Umleitung:

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

In diesem Fall ist das praktisch. Nach journalctl -u nginx > /tmp/log.txt ist !$ jedoch /tmp/log.txt und nicht nginx. Lesen Sie die ausgegebene Zeile.

Zweitens wird die Erweiterung als Text durchgeführt. Deshalb wird eine Variable ohne Auflösung erneut eingesetzt. Nach ls $HOME/backups liefert !$ die Zeichen $HOME/backups, die bash anschließend erneut als normalen Parameter erweitert. Merken Sie sich diese Reihenfolge: Die History-Erweiterung läuft vor der Parametererweiterung und vor der Befehlsersetzung mit $( ). Sie sieht daher nie Werte, sondern nur Text.

Die benachbarten Formen sind ebenfalls wichtig. !^ bezeichnet das erste Argument, !:2 das zweite, und !:2-4 einen Bereich. !!:$ ist die ausführliche Schreibweise für !$.

Wenn Sie den Text vor der Ausführung prüfen möchten, drücken Sie Alt-. (oder Esc und anschließend .). Readline fügt das letzte Argument des vorherigen Befehls direkt in die Eingabeaufforderung ein. Dort können Sie es bearbeiten. Drücken Sie die Tastenkombination erneut, um zum letzten Argument des davor ausgeführten Befehls zurückzugehen. Nichts wird ausgeführt, bevor Sie Enter drücken.

Alle Argumente mit !* weitergeben

!* enthält jedes Wort der vorherigen Zeile außer dem ersten.

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

Die Formulierung „außer dem ersten“ ist entscheidend. !* lässt Wort 0 und kein anderes weg, sodass Optionen zusammen mit den Pfaden weitergegeben werden. Nach ls -l file1 file2 ist !* gleich -l file1 file2. Daher schlägt sudo chmod 600 !* fehl, weil chmod -l als Argument erhält. Nummerieren Sie die Wörter ab 0 und verwenden Sie stattdessen einen Ausschnitt: !!:2* bedeutet „Wort 2 bis zum Ende“.

Dieselbe Falle tritt bei sudo auf. Nach sudo chown deploy:deploy /srv/www/app ist Wort 0 sudo und Wort 1 chown. Daher übergibt !* dem nächsten Befehl chown deploy:deploy /srv/www/app. Das war nur selten beabsichtigt.

Befehl anhand seiner Position mit !n und !-n auswählen

history gibt die Liste aus und setzt vor jeden Eintrag eine Nummer.

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 führt Eintrag 513 erneut aus. !-2 führt den Eintrag zwei Zeilen vor der aktuell eingegebenen Zeile aus. Daher bedeuten !-1 und !! dasselbe.

Prüfen Sie die Nummern unmittelbar vor ihrer Verwendung. !-2 verweist nach jeder Ausführung auf eine andere Stelle, auch nach einem ls, das Sie unüberlegt ausgeführt haben. Absolute Nummern bleiben innerhalb einer Sitzung stabil. In einer zweiten Sitzung auf demselben System gelten jedoch andere Nummern. Das gilt auch nach der nächsten Anmeldung, wenn die History-Datei erneut eingelesen wird. Eine Nummer, die Sie sich gestern gemerkt haben, verweist heute auf einen anderen Befehl.

Einen Befehl anhand eines Präfixes mit !string erneut ausführen

!string führt den zuletzt ausgeführten Befehl aus, der mit string beginnt.

!ss

Damit wird ss -tulpn aus der obigen Liste erneut ausgeführt, die in welche Ports auf einem Linux-Server geöffnet sind beschriebene Prüfung auf Listening-Sockets. !?string? sucht die Zeichenfolge an beliebiger Stelle in der Zeile statt nur am Anfang. Das hilft, wenn Sie sich an ein Argument, aber nicht an den Befehlsnamen erinnern.

Halten Sie das Präfix möglichst lang. !s kann ss, sudo, systemctl oder shutdown finden, je nachdem, welcher Befehl zuletzt ausgeführt wurde. Welcher Befehl ausgewählt wird, ist erst bei der Ausführung erkennbar. !string:p gibt den Treffer aus, ohne ihn auszuführen. Wenn kein Treffer gefunden wird, gibt bash bash: !ss: event not found aus und führt nichts aus. Das ist das sichere Verhalten.

Einen Tippfehler mit ^old^new korrigieren

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

Die Shell führt die vorherige Zeile erneut aus und ersetzt dabei das erste ngnix durch nginx. Nur das erste Vorkommen wird geändert. Um jedes Vorkommen zu ändern, verwenden Sie die Langform !!:gs/ngnix/nginx/. Dabei steht s für „ersetzen“ und g dafür, die Ersetzung in der gesamten Zeile auszuführen.

Expansion vor der Ausführung anzeigen

Zwei Gewohnheiten verhindern, dass eine Expansion etwas ausführt, das Sie nicht beabsichtigt haben.

Die erste ist :p, das Sie bereits kennengelernt haben. Hängen Sie es an eine Expansion an, gibt bash das Ergebnis aus, statt es auszuführen: !!:p oder !systemctl:p. Die ausgegebene Zeile wird in Ihre History übernommen. Mit !! führen Sie anschließend genau den Befehl aus, den Sie gerade gelesen haben.

Die zweite Methode ist zuverlässiger, weil sie für jede Expansion gilt, ohne dass Sie sich an etwas erinnern müssen:

shopt -s histverify

Fügen Sie diese Zeile in ~/.bashrc ein. Wenn histverify gesetzt ist, wird eine Expansion nicht ausgeführt, wenn Sie Enter drücken. Bash schreibt die expandierte Zeile zurück in Ihren Prompt, damit Sie sie lesen und bearbeiten können. Zum Ausführen drücken Sie ein zweites Mal Enter. Das kostet einen Tastendruck und verhindert die gesamte Klasse solcher Unfälle. Die Funktion benötigt readline. Sie funktioniert daher an einem interaktiven Prompt und sonst nirgends.

Ctrl-R ist die andere Arbeitsweise. Dabei wird niemals etwas expandiert. Drücken Sie Ctrl-R und geben Sie einen Teil eines Befehls ein. Der Prompt wechselt zu einem reverse-i-search-Prompt und zeigt während der Eingabe den neuesten Treffer an. Drücken Sie erneut Ctrl-R, um zu älteren Treffern zu wechseln. Mit Enter führen Sie die angezeigte Zeile aus. Ctrl-G bricht die Suche ab und stellt Ihren ursprünglichen Prompt wieder her. Mit der Pfeiltaste nach links beenden Sie die Suche. Die gefundene Zeile bleibt dann zur Bearbeitung im Prompt.

Verwenden Sie Ctrl-R, wenn Sie den Befehl zuerst anzeigen möchten. Verwenden Sie !! und !$, wenn der Befehl gerade erst eingegeben wurde und noch auf dem Bildschirm lesbar ist.

Warum meldet echo "done!" den Fehler „event not found“?

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

Die History-Expansion wird ausgeführt, bevor die Auswertung der Anführungszeichen erfolgt. Doppelte Anführungszeichen schützen das Zeichen ! nicht. Nur einfache Anführungszeichen und ein Backslash tun das. Daher gibt echo 'deploy done!' das erwartete Ergebnis aus. Ein Backslash verhindert die Expansion ebenfalls. Innerhalb doppelter Anführungszeichen übernimmt bash den Backslash jedoch in die Ausgabe. Deshalb sind einfache Anführungszeichen die saubere Lösung.

Das tritt besonders häufig bei Passwörtern auf, weil ein sicheres Passwort oft ! enthält. Ein Befehl wie mysql -u app -p"S3cret!pass" schlägt entweder mit dem Fehler „event not found“ fehl oder sendet, falls zufällig ein passender History-Eintrag vorhanden ist, unbemerkt eine andere Zeichenfolge. Verwenden Sie einfache Anführungszeichen. Noch besser ist es, das Tool zur Eingabe aufzufordern, damit das Geheimnis überhaupt nicht in der Befehlszeile erscheint. Diese Vorgehensweise gehört zur grundlegenden Kontohygiene. Unter die ersten zehn Minuten auf einem neuen VPS erfahren Sie, wie Schlüssel und Passwörter auf einem neuen Server behandelt werden sollten.

Wenn Sie gleich einen Textblock mit vielen Zeichen ! einfügen möchten, schalten Sie die Expansion für die Sitzung mit set +H aus. Mit set -H schalten Sie sie wieder ein.

HISTSIZE, HISTFILESIZE und der Speicherort Ihrer History

Zwei Variablen werden häufig verwechselt, weil ihre Namen ähnlich sind.

  • HISTSIZE gibt an, wie viele Befehle die laufende Shell im Arbeitsspeicher behält.
  • HISTFILESIZE gibt an, wie viele Zeilen in der Datei auf der Festplatte gespeichert werden, ~/.bash_history sofern HISTFILE nichts anderes festlegt.

Die Datei wird beim Beenden der Shell geschrieben und dabei auf HISTFILESIZE Zeilen gekürzt. Ubuntu setzt ~/.bashrc standardmäßig auf 1000 und 2000. Prüfen Sie, welche Werte bei Ihnen tatsächlich gesetzt sind:

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

Erhöhen Sie beide Werte, wenn Ihre !string-Suchen häufig Befehle nicht finden, die Sie nachweislich ausgeführt haben. Werte von 10000 und 20000 sind üblich und verursachen kaum zusätzlichen Aufwand, da die Datei einfachen Text enthält. Ein negativer Wert bedeutet, dass es keine Begrenzung gibt.

Zeitstempel helfen, wenn Sie nach einer Woche wieder auf einen Server zugreifen:

export HISTTIMEFORMAT='%F %T '

history gibt dann vor jedem Eintrag Datum und Uhrzeit aus, weil bash vor jedem Befehl in der Datei eine Kommentarzeile mit den Epochensekunden schreibt.

Häufig fehlt die History, wenn Sie mehr als ein Terminal verwenden. Jede Shell führt ihre eigene Liste im Arbeitsspeicher und schreibt sie beim Beenden in die Datei. Ohne histappend überschreibt daher die zuletzt geschlossene Shell die von den anderen Shells gespeicherten Einträge. Zwei Einstellungen beheben dieses Verhalten:

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

histappend sorgt dafür, dass die Shell ihre Liste an das Ende der Datei anhängt, statt die Datei zu ersetzen. history -a hängt nach jeder Eingabeaufforderung neue Zeilen an. Dadurch bleiben die Befehle einer Sitzung auch dann erhalten, wenn sie beendet wird, ohne ordnungsgemäß geschlossen zu werden. Ubuntu setzt ~/.bashrc standardmäßig bereits auf histappend. Das ist besonders relevant, wenn Sie mehrere Rechner verwalten, da die History pro Benutzer und pro Rechner gespeichert wird. Wenn Sie mehrere Server von einer Workstation aus verwalten, befindet sich die gesuchte !$ möglicherweise einfach auf einem anderen Host.

Geheimnisse aus der Bash-History heraushalten

HISTCONTROL legt fest, welche Zeilen überhaupt gespeichert werden.

  • ignorespace verwirft jede Zeile, die mit einem Leerzeichen beginnt.
  • ignoredups verwirft eine Zeile, die mit der vorherigen identisch ist.
  • ignoreboth führt beides aus.
  • erasedups entfernt alle älteren Kopien der zu speichernden Zeile.
export HISTCONTROL=ignoreboth

Wenn ignorespace aktiviert ist, hält ein führendes Leerzeichen einen Befehl aus der Liste im Arbeitsspeicher heraus. Dadurch gelangt er auch nicht in die Datei. Prüfen Sie den Wert mit echo "$HISTCONTROL", bevor Sie sich darauf verlassen. Der Standardbenutzer von Ubuntu .bashrc setzt ignoreboth. Eine Root-Shell oder ein minimales Image kann die Variable jedoch ungesetzt lassen. Dann hat das führende Leerzeichen keine Wirkung, und die Zeile wird wie jede andere gespeichert.

Seien Sie sich darüber im Klaren, was dieser Schutz leistet. Die Zeile bleibt aus ~/.bash_history heraus. Solange der Prozess läuft, ist sie weiterhin in der Ausgabe von ps sichtbar. Jeder andere Benutzer auf dem Server kann sie dort lesen. sudo protokolliert den ausgeführten Befehl im System-Log. Die History-Datei ist Klartext. Setzen Sie daher den Modus auf 600. Denken Sie außerdem daran, dass jeder Benutzer, der Ihr Home-Verzeichnis lesen kann, auch Ihre letzten tausend Befehle lesen kann.

Wenn bereits ein vertraulicher Eintrag in der Liste steht, entfernen Sie ihn und schreiben Sie die Datei neu:

history
history -d 517
history -w

history -d entfernt diesen Eintrag aus dem Arbeitsspeicher. history -w schreibt die aktuelle Liste über die Datei. history -c leert die gesamte Liste. HISTIGNORE ist die entsprechende Einstellung für weniger Rauschen statt für Geheimnisse: HISTIGNORE='ls:pwd:history:clear' hält diese Zeilen aus der Liste heraus, damit Ihre Suchen relevante Treffer liefern.

Warum die History-Erweiterung in einem Shell-Skript nichts bewirkt

Die History-Erweiterung gehört zu interaktiven Shells. Ein Skript läuft in einer nicht interaktiven Shell. Dort ist die History-Liste nicht aktiviert und die Erweiterung ist ausgeschaltet. Deshalb bleiben !! und !$ als gewöhnlicher Text in der Zeile stehen. sudo !! fordert sudo innerhalb eines Skripts auf, einen Befehl mit dem wörtlichen Namen !! auszuführen. Das schlägt fehl.

Prüfen Sie die Shell, in der Sie gerade arbeiten:

echo $-

Die Ausgabe enthält die aktuell gesetzten Optionsflags, zum Beispiel himBHs. i bedeutet, dass die Shell interaktiv ist. H bedeutet, dass die History-Erweiterung aktiviert ist. Führen Sie dieselbe Zeile innerhalb eines Skripts aus, ist keiner der beiden Buchstaben vorhanden.

Das ist die Grenze zwischen den beiden Bereichen Ihrer Shell-Arbeit. An der Eingabeaufforderung sparen !$ und Ctrl-R bei Befehlen Tipparbeit, deren Verlauf Sie noch sehen können. In einer Datei benennen Sie Dinge stattdessen direkt: Speichern Sie den Pfad in einer Variablen oder erfassen Sie die Ausgabe mit Befehlsersetzung. Ein Skript, das von Ihrer persönlichen History abhinge, würde sich bei der nächsten Person, die es ausführt, anders verhalten. Das wäre das Gegenteil dessen, wofür ein Skript gedacht ist.

Aus diesem Grund sind alle Beispiele auf dieser Seite für die Eingabe an einer aktiven Eingabeaufforderung vorgesehen. Nichts davon verhält sich beim Einfügen in eine .sh-Datei auf dieselbe Weise.

FAQ

Was bewirkt !! in bash?

!! wird zum vollständigen Text der vorherigen Befehlszeile erweitert. Daher führt sudo !! den letzten Befehl erneut als root aus. Die Erweiterung erfolgt als Text, bevor bash die Zeile analysiert. bash gibt die fertige Zeile unmittelbar vor ihrer Ausführung aus. Wenn Sie nicht sicher sind, wie die vorherige Zeile lautete, geben Sie zuerst sudo !!:p ein. :p gibt die Erweiterung aus, ohne sie auszuführen, und fügt die ausgegebene Zeile Ihrer History hinzu. Ein nachfolgendes !! führt sie dann aus.

Wie verwende ich das letzte Argument des vorherigen Befehls erneut?

Verwenden Sie !$. Nach sudo mkdir -p /srv/www/app/releases wird die Zeile ls -ld !$ zu ls -ld /srv/www/app/releases. Dabei wird das letzte Wort der Zeile verwendet. Ein Ziel einer Umleitung am Ende zählt daher als letztes Wort. Sie können Text anhängen: !$/config.env fügt Text an den Pfad an, weil der Wortbezeichner beim / endet. Die interaktive Alternative ist Alt-.. Sie fügt denselben Text in Ihre Eingabeaufforderung ein, damit Sie ihn vor dem Drücken der Eingabetaste prüfen können.

Warum gibt bash "event not found" aus, wenn mein Text ein Ausrufezeichen enthält?

Doppelte Anführungszeichen schützen ! nicht vor der History-Erweiterung. Daher sucht echo "done!" nach einem History-Ereignis und gibt bash: !": event not found aus. Einfache Anführungszeichen schützen das Zeichen. Schreiben Sie daher echo 'done!'. Ein Backslash verhindert die Erweiterung ebenfalls. Innerhalb doppelter Anführungszeichen lässt bash den Backslash jedoch in der Ausgabe stehen. Um einen längeren Block einzufügen, der ! enthält, deaktivieren Sie die Erweiterung für die Sitzung mit set +H.

Warum funktionieren !! und !$ nicht in meinem Shell-Skript?

Die History-Erweiterung ist nur in interaktiven Shells aktiviert. Ein Skript läuft nicht interaktiv. Daher erstellt die Shell keine History-Liste, und !! bleibt als einfacher Text in der Zeile stehen. Führen Sie echo $- aus, um den Shell-Typ zu prüfen. Eine interaktive Shell gibt Flags einschließlich i und H aus. Ein Skript gibt keines dieser Flags aus. Verwenden Sie in Skripten stattdessen eine Variable oder eine Befehlsersetzung.

Wie halte ich ein Passwort aus meiner bash-History heraus?

Setzen Sie HISTCONTROL=ignorespace oder HISTCONTROL=ignoreboth in ~/.bashrc. Wenn Sie den Befehl anschließend mit einem einzelnen Leerzeichen beginnen, wird er nicht gespeichert. Prüfen Sie den Wert zuerst mit echo "$HISTCONTROL". Wenn er nicht gesetzt ist, bewirkt das führende Leerzeichen nichts. Dadurch wird die Zeile nur aus ~/.bash_history ferngehalten. Während der Ausführung ist der Befehl weiterhin in ps sichtbar, und sudo protokolliert, was ausgeführt wurde. Wenn ein Secret bereits gespeichert ist, ermitteln Sie seine Nummer mit history. Führen Sie anschließend history -d <number> gefolgt von history -w aus, um die Datei neu zu schreiben.

#bash#shell#history#productivity#cli