SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-13

Bash history expansion: hoe werken !! en !$?

Gebruik !! voor de vorige opdracht en !$ voor het laatste argument. Leer de zes essentiële bash history expansies en gebruik :p om opdrachten veilig te printen voor uitvoering.

Wat bash history expansion doet

Bash history expansion reconstrueert een eerdere opdrachtregel uit uw shell-geschiedenis voordat bash deze uitvoert. !! herhaalt de vorige opdracht. !$ voegt het laatste argument van de vorige opdracht in. De vervanging is tekstueel en vindt als eerste plaats, nog voordat bash de regel in woorden opsplitst. Wat u terugkrijgt, zijn exact de tekens die u de eerste keer hebt getypt.

Zes vormen dekken bijna alles wat u op een server zult doen:

  • !! herhaalt de volledige vorige regel en sudo !! voert deze opnieuw uit als root.
  • !$ is het laatste argument van de vorige regel.
  • !* bevat elk argument van de vorige regel.
  • !n voert geschiedenisitem nummer n uit en !-n telt terug vanaf uw huidige positie.
  • !string voert de meest recente opdracht uit die begon met string.
  • ^old^new voert de vorige regel opnieuw uit waarbij de eerste old is vervangen door new.

Alles hieronder wordt getypt bij een interactieve prompt op uw eigen server. History expansion is uitgeschakeld in scripts, wat in de laatste sectie wordt uitgelegd.

Eén lang pad, slechts eenmaal getypt

Hier is het scenario dat zichzelf terugbetaalt. U bereidt een release-directory voor en het pad is lang genoeg dat het een tweede keer typen van het pad uitnodigt tot typefouten.

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

Voordat elke regel wordt uitgevoerd, print bash de regel die de expansie heeft opgeleverd:

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

Die echo is uw controle. Lees deze voordat u de output van het commando leest, omdat dit de enige kans is om te zien wat bash interpreteerde als !$.

De keten blijft intact omdat bash de geëxpandeerde regel opslaat in de history, niet de !$ die u heeft getypt. Dus regel 3 haalt zijn laatste argument uit de geëxpandeerde regel 2, en regel 4 haalt het uit regel 3. Regel 4 laat ook zien dat tekst een designator kan volgen: !$/config.env wordt het pad met /config.env aan het einde, omdat de woord-designator stopt bij de /.

Nog een voorbeeld uit dezelfde sessie. Na het bewerken van het bestand wilt u de directory weergeven die het bestand bevat:

ls -l !$:h

:h is de head-modifier. Deze verwijdert het laatste onderdeel van een pad, dezelfde taak die dirname uitvoert. :t behoudt alleen het laatste onderdeel, :r verwijdert de extensie en :e behoudt alleen de extensie.

Het laatste commando herhalen met !! en sudo !!

U vergeet sudo, en de service manager meldt dit:

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

Bash vervangt !! door de tekst van de vorige regel, waardoor de shell sudo systemctl restart nginx uitvoert.

!! is exact de vorige regel, ongeacht wat die regel was. Hier gebeurt het klassieke ongeluk. U denkt dat het laatste commando het commando was dat faalde, maar in de tussentijd heeft u een cd of een history uitgevoerd, of het gefaalde commando stond twee regels terug. sudo !! voert dan het verkeerde commando uit met root-rechten. Print het eerst als u niet zeker bent:

sudo !!:p

:p print de uitgebreide regel en voert deze niet uit. De geprinte regel wordt toegevoegd aan uw geschiedenis, dus als deze correct is, voert een eenvoudige !! op de volgende regel het commando uit.

Het laatste argument hergebruiken met !$

!$ is het laatste woord van de vorige regel. Dit is de vorm die u het meest zult gebruiken, omdat het laatste woord meestal het object is waar u aan werkt: een pad of een servicenaam.

sudo systemctl status nginx
sudo systemctl reload !$

Twee zaken verrassen gebruikers hier vaak.

Ten eerste is !$ het laatste woord, niet het laatste argument dat u in gedachten had. Als de vorige regel eindigde met een redirectie, dan is het laatste woord het doel van de redirectie:

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

Dat werkt in dit geval goed. Maar na journalctl -u nginx > /tmp/log.txt is !$ gelijk aan /tmp/log.txt en niet aan nginx. Lees de getoonde regel goed.

Ten tweede is de expansie tekstueel, waardoor een variabele onuitgebreid terugkomt. Na ls $HOME/backups geeft !$ u de tekens $HOME/backups, die bash vervolgens opnieuw uitbreidt als een normaal parameter. Onthoud deze volgorde: history expansion wordt uitgevoerd vóór parameter expansion en vóór command substitution met $( ), waardoor het nooit waarden ziet, enkel tekst.

De aanverwante vormen zijn ook nuttig om te kennen. !^ is het eerste argument, !:2 is het tweede en !:2-4 is een bereik. !!:$ is de uitgebreide schrijfwijze van !$.

Als u de tekst liever eerst wilt zien voordat u deze bevestigt, druk dan op Alt-. (of Esc en daarna .). Readline voegt het laatste argument van het vorige commando direct in uw prompt in, waar u het kunt bewerken. Druk nogmaals om terug te gaan naar het laatste argument van het commando daarvoor. Er wordt niets uitgevoerd totdat u op Enter drukt.

Geef elk argument door met !*

!* is elk woord van de voorgaande regel, behalve het eerste.

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

De zinsnede "behalve het eerste" is hier cruciaal. !* verwijdert alleen woord 0, waardoor opties samen met de paden worden doorgegeven. Na ls -l file1 file2 is !* gelijk aan -l file1 file2, waardoor sudo chmod 600 !* faalt, omdat chmod het argument -l ontvangt. Nummer de woorden vanaf nul en gebruik een slice: !!:2* staat voor woord 2 tot en met het einde.

Hetzelfde probleem doet zich voor bij sudo. Na sudo chown deploy:deploy /srv/www/app is woord 0 gelijk aan sudo en woord 1 gelijk aan chown, waardoor !* het volgende commando chown deploy:deploy /srv/www/app meegeeft. Dat is zelden de bedoeling.

Een commando kiezen op positie met !n en !-n

history toont de lijst met een nummer voor elk item.

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 voert item 513 opnieuw uit. !-2 voert het item uit dat twee regels terug staat, geteld vanaf de regel die u nu typt, waardoor !-1 en !! hetzelfde betekenen.

Controleer de nummers direct voordat u ze gebruikt. !-2 verwijst elke keer naar een andere plek wanneer u iets uitvoert, inclusief een ls die u ondoordacht heeft afgevuurd. Absolute nummers zijn stabiel binnen één sessie, maar het zijn niet dezelfde nummers in een tweede sessie op dezelfde machine, en ze zijn niet hetzelfde nadat uw volgende login het geschiedenisbestand opnieuw heeft ingelezen. Een nummer dat u gisteren uit uw hoofd heeft geleerd, verwijst vandaag naar een ander commando.

Een commando opnieuw uitvoeren op basis van een prefix met !string

!string voert het meest recente commando uit dat begint met string.

!ss

Dit voert ss -tulpn uit de bovenstaande lijst opnieuw uit, de controle op luisterende sockets zoals beschreven in welke poorten openstaan op een Linux-server. !?string? zoekt naar een overeenkomst in de gehele regel in plaats van alleen aan het begin; dit is nuttig wanneer u zich een argument herinnert maar niet de naam van het commando.

Houd de prefix lang. !s komt overeen met ss, sudo, systemctl of shutdown, afhankelijk van welke als laatste is uitgevoerd, en u weet niet welke dit is totdat het commando draait. !string:p toont de overeenkomst zonder deze uit te voeren. Als er geen overeenkomst wordt gevonden, toont bash bash: !ss: event not found en voert het niets uit; dit is de veilige uitkomst.

Eén typefout corrigeren met ^oud^nieuw

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

De shell voert de vorige regel opnieuw uit waarbij de eerste ngnix is vervangen door nginx. Alleen de eerste overeenkomst wordt gewijzigd. Gebruik de lange vorm !!:gs/ngnix/nginx/ om elke overeenkomst te wijzigen, waarbij s staat voor substitutie en g betekent dat dit over de gehele regel moet worden toegepast.

Toon de expansie voordat u deze uitvoert

Twee gewoonten voorkomen het ongeluk waarbij een expansie iets uitvoert wat u niet had bedoeld.

De eerste is :p, die u al heeft gezien. Voeg dit toe aan een expansie en bash toont het resultaat in plaats van het uit te voeren: !!:p, of !systemctl:p. De getoonde regel wordt in uw geschiedenis opgenomen, dus !! voert daarna het commando uit dat u zojuist heeft gelezen.

De tweede is krachtiger, omdat deze van toepassing is op elke expansie zonder dat u iets hoeft te onthouden:

shopt -s histverify

Plaats deze regel in ~/.bashrc. Met histverify ingesteld, wordt een expansie niet uitgevoerd wanneer u op Enter drukt. Bash schrijft de geëxpandeerde regel terug in uw prompt zodat u deze kunt lezen en bewerken, en u drukt een tweede keer op Enter om het uit te voeren. Het kost één extra toetsaanslag en het elimineert een hele categorie ongelukken. Het vereist readline, dus het werkt alleen bij een interactieve prompt en nergens anders.

Ctrl-R is de andere manier van werken, en deze expandeert nooit iets. Druk op Ctrl-R en typ een deel van een commando. De prompt verandert in een reverse-i-search-prompt en toont de meest recente overeenkomst terwijl u typt. Druk nogmaals op Ctrl-R om terug te bladeren naar oudere overeenkomsten. Enter voert de getoonde regel uit. Ctrl-G annuleert de zoekopdracht en geeft uw oorspronkelijke prompt terug. De pijl-links-toets beëindigt de zoekopdracht en laat de gevonden regel in uw prompt staan voor bewerking.

Gebruik Ctrl-R wanneer u het commando eerst wilt zien. Gebruik !! en !$ wanneer het commando één regel oud is en u het nog op het scherm kunt lezen.

Waarom geeft echo "done!" de melding event not found

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

History expansion wordt uitgevoerd voordat quotes worden verwerkt, en dubbele quotes beschermen het !-teken niet. Alleen enkele quotes en een backslash doen dit. Daarom print echo 'deploy done!' wat u verwacht. Een backslash stopt de expansie ook, maar binnen dubbele quotes laat bash de backslash in de output staan, dus enkele quotes zijn de nette oplossing.

Dit is vooral problematisch bij wachtwoorden, omdat een sterk wachtwoord vaak een ! bevat. Een commando zoals mysql -u app -p"S3cret!pass" faalt ofwel met een event-not-found-foutmelding, of, als een geschiedenisitem toevallig overeenkomt, verstuurt het stilletjes een andere string. Gebruik enkele quotes, of beter nog, laat de tool u om invoer vragen zodat het geheim nooit op de command line terechtkomt. Die gewoonte hoort bij de rest van uw accountbeveiliging: zie de eerste tien minuten op een nieuwe VPS voor hoe met sleutels en wachtwoorden moet worden omgegaan op een nieuwe server.

Als u een tekstblok vol met !-tekens wilt plakken, schakel expansie dan uit voor de sessie met set +H, en weer in met set -H.

HISTSIZE, HISTFILESIZE en waar uw geschiedenis wordt bewaard

Er zijn twee variabelen, en mensen halen ze vaak door elkaar omdat de namen op elkaar lijken.

  • HISTSIZE bepaalt hoeveel commando's de actieve shell in het geheugen houdt.
  • HISTFILESIZE bepaalt hoeveel regels er in het bestand op de schijf worden bewaard, ~/.bash_history tenzij HISTFILE anders aangeeft.

Het bestand wordt weggeschreven wanneer de shell wordt afgesloten en wordt op dat moment ingekort tot HISTFILESIZE regels. De standaardinstelling ~/.bashrc van Ubuntu stelt deze in op 1000 en 2000. Controleer wat uw huidige waarden zijn:

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

Verhoog beide waarden als uw !string zoekopdrachten commando's missen waarvan u weet dat u ze heeft uitgevoerd. Waarden van 10000 en 20000 zijn gebruikelijk en kosten vrijwel niets, omdat het bestand uit platte tekst bestaat. Een negatieve waarde betekent dat er helemaal geen limiet is.

Tijdstempels helpen wanneer u na een week afwezigheid terugkeert naar een server:

export HISTTIMEFORMAT='%F %T '

history plaatst vervolgens een datum en tijd voor elk item, omdat bash voor elk commando in het bestand een commentaarregel schrijft die de epoch-seconden bevat.

Een veelgehoorde klacht is dat de geschiedenis verloren gaat wanneer u meer dan één terminal gebruikt. Elke shell houdt zijn eigen lijst in het geheugen bij en schrijft deze weg bij het afsluiten; zonder histappend overschrijft de laatst afgesloten shell de gegevens die de andere sessies hebben opgeslagen. Twee instellingen lossen dit op:

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

histappend zorgt ervoor dat de shell zijn lijst toevoegt aan het einde van het bestand in plaats van het bestand te vervangen. history -a voegt na elke prompt nieuwe regels toe, zodat een sessie die wordt beëindigd in plaats van netjes wordt afgesloten, zijn commando's alsnog achterlaat. De standaardinstelling ~/.bashrc van Ubuntu stelt histappend al in. Dit is belangrijker naarmate u meer machines beheert, omdat de geschiedenis per gebruiker per machine wordt opgeslagen. Wanneer u dus meerdere servers vanaf één werkstation beheert, kan het !$ dat u zoekt zich simpelweg op een andere host bevinden.

Geheimen buiten uw bash history houden

HISTCONTROL bepaalt welke regels überhaupt worden opgeslagen.

  • ignorespace negeert elke regel die begint met een spatie.
  • ignoredups negeert een regel die identiek is aan de voorgaande regel.
  • ignoreboth voert beide bovenstaande acties uit.
  • erasedups verwijdert elke oudere kopie van de regel die wordt opgeslagen.
export HISTCONTROL=ignoreboth

Met ignorespace actief zorgt één spatie aan het begin ervoor dat een commando niet in de in-memory lijst terechtkomt, waardoor het ook nooit naar het bestand wordt geschreven. Controleer de waarde met echo "$HISTCONTROL" voordat u hierop vertrouwt. De standaardgebruiker .bashrc in Ubuntu stelt ignoreboth in, maar een root-shell of een minimale image laat dit mogelijk oningesteld. In dat geval doet de spatie niets en wordt de regel gewoon opgeslagen.

Wees realistisch over wat dit beschermt. De regel blijft buiten ~/.bash_history. Het commando is nog steeds zichtbaar in de uitvoer van ps terwijl het proces draait, dus elke andere gebruiker op de server kan het daar uitlezen. sudo registreert het uitgevoerde commando in het systeemlogboek. Het history-bestand is platte tekst; zorg daarom dat de rechten op 600 staan en onthoud dat iedereen die uw home-directory kan lezen, ook uw laatste paar duizend commando's kan inzien.

Als er al gevoelige informatie in de lijst staat, verwijder het item dan en overschrijf het bestand:

history
history -d 517
history -w

history -d verwijdert dat item uit het geheugen en history -w overschrijft het bestand met de huidige lijst. history -c wist de volledige lijst. HISTIGNORE is de gerelateerde instelling voor ruis in plaats van geheimen: HISTIGNORE='ls:pwd:history:clear' houdt die regels uit de lijst, zodat uw zoekopdrachten relevante resultaten opleveren.

Waarom history expansion niet werkt in een shell script

History expansion is een functie van interactieve shells. Een script wordt uitgevoerd in een niet-interactieve shell, waar de geschiedenislijst niet is ingeschakeld en expansie is uitgeschakeld. Hierdoor blijven !! en !$ als gewone tekst in de regel staan. sudo !! in een script vraagt aan sudo om een commando uit te voeren dat letterlijk !! heet, wat resulteert in een foutmelding.

Controleer de shell waarin u werkt:

echo $-

De uitvoer toont de huidige optievlaggen, bijvoorbeeld himBHs. i betekent dat de shell interactief is en H betekent dat history expansion is ingeschakeld. Voer dezelfde regel uit in een script en geen van beide letters zal aanwezig zijn.

Dit is het onderscheid tussen de twee manieren waarop u met de shell werkt. Bij de prompt besparen !$ en Ctrl-R toetsaanslagen voor commando's die u nog kunt zien. In een bestand benoemt u zaken expliciet: plaats het pad in een variabele of vang de uitvoer op met command substitution. Een script dat afhankelijk is van uw persoonlijke geschiedenis zou voor een andere gebruiker anders werken, wat het tegenovergestelde is van het doel van een script.

Om die reden zijn alle voorbeelden op deze pagina bedoeld om bij een actieve prompt te worden getypt. Niets hiervan gedraagt zich op dezelfde manier wanneer het in een .sh bestand wordt geplakt.

FAQ

Wat doet !! in bash?

!! expandeert naar de volledige tekst van de vorige opdrachtregel, dus sudo !! voert uw laatste opdracht opnieuw uit als root. De expansie is tekstueel en vindt plaats voordat bash de regel parseert; bash toont de voltooide regel vlak voordat deze wordt uitgevoerd. Als u niet zeker weet wat de vorige regel was, typ dan eerst sudo !!:p. :p toont de expansie zonder deze uit te voeren en voegt de getoonde regel toe aan uw geschiedenis, zodat een daaropvolgende !! deze uitvoert.

Hoe hergebruik ik het laatste argument van de vorige opdracht?

Gebruik !$. Na sudo mkdir -p /srv/www/app/releases wordt de regel ls -ld !$ gelijk aan ls -ld /srv/www/app/releases. Het neemt het laatste woord van de regel; een redirectie-doel aan het einde telt dus als het laatste woord. U kunt er ook tekst aan toevoegen: !$/config.env voegt tekst toe aan het pad, omdat de woord-designator stopt bij de /. Het interactieve alternatief is Alt-., die dezelfde tekst in uw prompt invoegt zodat u deze kunt lezen voordat u op Enter drukt.

Waarom zegt bash "event not found" als mijn tekst een uitroepteken bevat?

Dubbele aanhalingstekens beschermen ! niet tegen geschiedenis-expansie, dus echo "done!" zorgt ervoor dat bash zoekt naar een geschiedenis-event en bash: !": event not found toont. Enkele aanhalingstekens beschermen het wel, dus schrijf echo 'done!'. Een backslash stopt de expansie ook, maar binnen dubbele aanhalingstekens laat bash de backslash in de uitvoer staan. Om een lang blok met ! te plakken, schakelt u expansie voor de sessie uit met set +H.

Waarom werken !! en !$ niet in mijn shell-script?

Geschiedenis-expansie is alleen ingeschakeld in interactieve shells. Een script wordt niet-interactief uitgevoerd, waardoor de shell nooit een geschiedenislijst opbouwt en !! als platte tekst in de regel blijft staan. Voer echo $- uit om te zien in welke modus u zich bevindt: een interactieve shell toont vlaggen waaronder i en H, terwijl een script geen van beide toont. Gebruik in scripts in plaats daarvan een variabele of command substitution.

Hoe houd ik een wachtwoord uit mijn bash-geschiedenis?

Stel HISTCONTROL=ignorespace of HISTCONTROL=ignoreboth in via ~/.bashrc, en begin de opdracht vervolgens met een spatie; de opdracht wordt dan niet opgeslagen. Controleer eerst de waarde met echo "$HISTCONTROL", want als deze niet is ingesteld, heeft de spatie aan het begin geen effect. Dit houdt de regel alleen uit ~/.bash_history. De opdracht is nog steeds zichtbaar in ps terwijl deze draait, en sudo logt wat er is uitgevoerd. Als een geheim al is opgeslagen, zoek dan het nummer op met history en voer vervolgens history -d <number> uit, gevolgd door history -w om het bestand te herschrijven.