SSD Nodes Learn 🎉 VPS mula $5.50/buwan
Mga Gabay Matt ConnorNi Matt Connor · Na-update 2026-08-13

Bash history expansion: gamit ng !! at !$

Alamin ang anim na Bash history expansion, kasama ang !! para ulitin ang huling command, !$ para sa huling argument, at :p para i-print muna ito.

Ano ang ginagawa ng bash history expansion

Binubuo muli ng Bash history expansion ang isang naunang command line mula sa shell history bago ito patakbuhin ng bash. Inuulit ng !! ang nakaraang command. Ipinapasok ng !$ ang huling argument ng nakaraang command. Textual ang substitution at ginagawa muna ito bago hatiin ng bash ang line sa mga salita, kaya ang lumalabas ay eksaktong mga character na unang nai-type.

Anim na anyo ang sumasaklaw sa halos lahat ng gagawin mo sa isang server:

  • Inuulit ng !! ang buong nakaraang line, at pinapatakbo itong muli ng sudo !! bilang root.
  • Ang !$ ang huling argument ng nakaraang line.
  • Ang !* ang lahat ng argument ng nakaraang line.
  • Pinapatakbo ng !n ang history entry number n, at bumibilang paatras ang !-n mula sa kasalukuyan mong posisyon.
  • Pinapatakbo ng !string ang pinakahuling command na nagsimula sa string.
  • Pinapatakbo muli ng ^old^new ang nakaraang line at pinapalitan ang unang old ng new.

Lahat ng nasa ibaba ay tina-type sa isang interactive prompt sa sarili mong server. Naka-off ang history expansion sa mga script; ipinapaliwanag ito sa huling section.

Isang mahabang path, isang beses lang ita-type

Ito ang sitwasyong sulit gamitin ang feature na ito. Naghahanda ka ng release directory, at sapat ang haba ng path para maging madaling magkamali kapag muli mo itong tina-type.

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

Bago patakbuhin ang bawat linya, ipinapakita ng bash ang linyang ginawa ng expansion:

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

Ang echo na iyon ang iyong pagsusuri. Basahin muna ito bago ang output ng command, dahil iyon lang ang pagkakataon mong makita kung ano ang itinuring ng bash na ibig sabihin ng !$.

Gumagana ang chain dahil iniimbak ng bash sa history ang linyang may expanded values, hindi ang !$ na tina-type mo. Kaya kinukuha ng line 3 ang huling argument nito mula sa expanded line 2, at kinukuha naman ito ng line 4 mula sa line 3. Ipinapakita rin ng line 4 na maaaring may text pagkatapos ng designator: nagiging path na may /config.env sa dulo ang !$/config.env, dahil humihinto ang word designator sa /.

Isa pa mula sa parehong session. Pagkatapos mong i-edit ang file, gusto mong ilista ang directory na kinalalagyan nito:

ls -l !$:h

Ang :h ang head modifier. Tinatanggal nito ang huling component ng path, katulad ng ginagawa ng dirname. Ang :t ay iniiwan lamang ang huling component, inaalis ng :r ang extension, at iniiwan lamang ng :e ang extension.

Ulitin ang huling command gamit ang !! at sudo !!

Nakalimutan mong sudo, at ipinapaalam ito ng service manager:

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

Pinapalitan ng Bash ang !! ng text ng nakaraang line, kaya pinapatakbo ng shell ang sudo systemctl restart nginx.

Ang !! ay eksaktong nakaraang line, anuman ang laman ng line na iyon. Dito nangyayari ang klasikong aksidente. Akala mo ang huling command ang nag-fail, pero pagkatapos noon ay nagpatakbo ka ng cd, o history, o dalawang line ang pagitan ng failed command. Pagkatapos, pinapatakbo ng sudo !! ang maling command na may root privileges. I-print muna ito kapag hindi ka sigurado:

sudo !!:p

Ini-print ng :p ang expanded line at hindi ito pinapatakbo. Idinaragdag ang naka-print na line sa history mo, kaya kung tama ang nakasaad dito, patatakbuhin ito ng plain !! sa susunod na line.

Gamitin muli ang huling argument gamit ang !$

Ang !$ ang huling salita sa naunang linya. Ito ang form na pinakamadalas mong gagamitin dahil karaniwang ang huling salita ang pinagtatrabahuhan mo: isang path o pangalan ng service.

sudo systemctl status nginx
sudo systemctl reload !$

Dalawang bagay ang kadalasang nakapagtataka rito.

Una, ang !$ ay ang huling salita, hindi ang huling argument na nasa isip mo. Kung nagtapos ang naunang linya sa isang redirection, ang huling salita ay ang target ng redirect:

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

Maayos ang resulta nito. Pero pagkatapos ng journalctl -u nginx > /tmp/log.txt, ang !$ ay /tmp/log.txt at hindi nginx. Basahin ang line na naka-echo.

Ikalawa, text-based ang expansion, kaya bumabalik ang variable nang hindi pa nae-expand. Pagkatapos ng ls $HOME/backups, ibinibigay sa iyo ng !$ ang mga character na $HOME/backups, na muli namang ine-expand ng bash bilang normal na parameter. Tandaan ang pagkakasunod-sunod na ito: nauuna ang history expansion sa parameter expansion at sa command substitution gamit ang $( ), kaya text lamang ang nakikita nito, hindi ang mga value.

Mahalagang malaman ang mga katabing form. Ang !^ ang unang argument, ang !:2 ang ikalawa, at ang !:2-4 ay isang range. Ang !!:$ ang mahabang paraan ng pagsulat sa !$.

Kung gusto mong makita muna ang text bago mo ito isagawa, pindutin ang Alt-. (o ang Esc at pagkatapos ay .). Direktang inilalagay ng Readline sa prompt mo ang huling argument ng naunang command, kung saan maaari mo itong i-edit. Pindutin itong muli upang bumalik sa huling argument ng command bago iyon. Walang isasagawa hanggang sa pindutin mo ang Enter.

Ipasa ang bawat argument gamit ang !*

!* ay bawat salita sa naunang linya maliban sa una.

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

Mahalaga ang pariralang “maliban sa una.” Inaalis ng !* ang word 0 at wala nang iba, kaya naipapasa rin ang mga option kasama ng mga path. Pagkatapos ng ls -l file1 file2, ang !* ay -l file1 file2, kaya nabibigo ang sudo chmod 600 !* dahil tinatanggap ng chmod ang -l bilang isang argument. Bilangin ang mga salita mula sa zero at gumamit ng slice sa halip: ang !!:2* ay nangangahulugang word 2 hanggang sa huli.

Nangyayari rin ang parehong problema sa sudo. Pagkatapos ng sudo chown deploy:deploy /srv/www/app, ang word 0 ay sudo at ang word 1 ay chown, kaya ipinapasa ng !* sa susunod na command ang chown deploy:deploy /srv/www/app. Bihira itong maging gusto mong resulta.

Pumili ng command ayon sa posisyon gamit ang !n at !-n

history ipinapakita ang listahan na may numero sa unahan ng bawat entry.

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

Muling pinapatakbo ng !513 ang entry 513. Pinapatakbo naman ng !-2 ang entry na dalawang linya ang layo, na binibilang mula sa linyang kasalukuyan mong tina-type, kaya pareho ang ibig sabihin ng !-1 at !!.

Suriin ang mga numerong nasa unahan bago gamitin ang mga ito. Tumutukoy ang !-2 sa bagong entry sa tuwing nagpapatakbo ka ng kahit ano, kasama ang ls na pinatakbo mo nang hindi pinag-isipan. Stable ang absolute number sa loob ng isang session, pero iba ang mga ito sa mga numero sa ikalawang session sa parehong box. Hindi rin pareho ang mga ito pagkatapos basahin muli ng susunod mong login ang history file. Ang numerong minemorya mo kahapon ay maaaring tumukoy sa ibang command ngayon.

Muling magpatakbo ng command batay sa prefix gamit ang !string

!string pinapatakbo nito ang pinakabagong command na nagsisimula sa string.

!ss

Muli nitong pinapatakbo ang ss -tulpn mula sa listahan sa itaas, ang listening socket check na inilalarawan sa kung aling mga port ang bukas sa Linux server. Tumatapat ang !?string? kahit saan sa linya, sa halip na sa simula lamang. Nakakatulong ito kapag naaalala mo ang isang argument pero hindi ang pangalan ng command.

Panatilihing mahaba ang prefix. Tatapatan ng !s ang ss, sudo, systemctl, o shutdown, alinman ang pinakahuling pinatakbo. Hindi mo malalaman kung alin ito hanggang sa mapatakbo ang command. Ipi-print ng !string:p ang tumugmang command nang hindi ito pinapatakbo. Kung walang tumugma, ipi-print ng bash ang bash: !ss: event not found at walang tatakbong command. Ito ang ligtas na resulta.

Ayusin ang isang typo gamit ang ^old^new

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

Muling pinapatakbo ng shell ang nakaraang linya, at pinapalitan ang unang ngnix ng nginx. Ang unang tugma lamang ang binabago. Para palitan ang lahat ng tugma, gamitin ang mahabang anyong !!:gs/ngnix/nginx/, kung saan ang s ay nangangahulugang magsubstitute at ang g ay nangangahulugang gawin ito sa buong linya.

I-print muna ang expansion bago ito patakbuhin

May dalawang paraan para maiwasan ang aksidenteng pagtakbo ng expansion na hindi mo naman nilayon.

Ang una ay :p, na nakita mo na. Idagdag ito sa isang expansion at ipi-print ng bash ang resulta sa halip na patakbuhin ito: !!:p, o !systemctl:p. Mapupunta sa history ang na-print na linya, kaya kapag ginamit mo ang !! pagkatapos, patatakbuhin nito ang kababasa mo lang.

Mas matibay ang ikalawang paraan dahil awtomatiko itong nalalapat sa bawat expansion:

shopt -s histverify

Ilagay ang linyang iyon sa ~/.bashrc. Kapag naka-set ang histverify, hindi tatakbo ang expansion kapag pinindot mo ang Enter. Ibabalik ng Bash sa prompt ang expanded line para mabasa at ma-edit mo ito, at kailangan mong pindutin muli ang Enter para patakbuhin ito. Isang keystroke lang ang dagdag nito, at inaalis nito ang buong klase ng aksidenteng pagtakbo. Kailangan nito ng readline, kaya gumagana lamang ito sa interactive prompt at hindi sa ibang lugar.

Ang Ctrl-R ang isa pang paraan ng paghahanap, at wala itong ine-expand. Pindutin ang Ctrl-R at mag-type ng bahagi ng command. Magiging reverse-i-search prompt ang prompt at ipapakita nito ang pinakabagong tugma habang nagta-type ka. Pindutin muli ang Ctrl-R para lumipat sa mas lumang mga tugma. Kapag pinindot ang Enter, tatakbo ang linyang ipinapakita nito. Kinakansela ng Ctrl-G ang paghahanap at ibinabalik ang orihinal mong prompt. Tinatapos ng left arrow key ang paghahanap at iniiwan ang tumugmang linya sa prompt para ma-edit mo.

Gamitin ang Ctrl-R kapag gusto mong makita muna ang command. Gamitin ang !! at !$ kapag isang linya pa lang ang layo ng command at nababasa mo pa ito sa screen.

Bakit sinasabi ng echo "done!" na event not found

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

Nauuna ang history expansion bago malutas ang quoting, at hindi pinoprotektahan ng double quotes ang character na !. Single quotes at backslash lamang ang gumagawa nito. Kaya ipinapakita ng echo 'deploy done!' ang inaasahan mong output. Pinipigilan din ng backslash ang expansion, pero sa loob ng double quotes, iniiwan ng bash ang backslash sa output. Kaya single quotes ang mas malinis na solusyon.

Pinakamadalas itong magdulot ng problema sa mga password dahil karaniwang may ! ang isang malakas na password. Maaaring mabigo ang command na tulad ng mysql -u app -p"S3cret!pass" at maglabas ng event-not-found error. Maaari rin itong tahimik na magpadala ng ibang string kung may history entry na tumutugma. Gumamit ng single quotes. Mas mabuti, hayaan ang tool na humingi ng input upang hindi mapunta ang secret sa command line. Bahagi ito ng wastong account hygiene: tingnan ang unang sampung minuto sa bagong VPS para malaman kung paano dapat pangasiwaan ang keys at passwords sa bagong server.

Kung magpe-paste ka ng text block na maraming character na !, patayin ang expansion para sa session gamit ang set +H. I-on itong muli gamit ang set -H.

HISTSIZE, HISTFILESIZE at kung saan iniimbak ang iyong history

Dalawang variable ito, at napagkakamalan ang mga ito dahil magkahawig ang mga pangalan.

  • HISTSIZE ang bilang ng mga command na iniingatan ng kasalukuyang shell sa memory.
  • HISTFILESIZE ang bilang ng mga linyang iniingatan sa file sa disk, ~/.bash_history maliban kung iba ang itinakda ng HISTFILE.

Isinusulat ang file kapag lumabas ang shell, at tina-truncate ito sa HISTFILESIZE linya sa sandaling iyon. Itinatakda ng default na ~/.bashrc ng Ubuntu ang mga value na 1000 at 2000. Tingnan kung ano talaga ang laman ng iyong configuration:

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

Taasan ang dalawang value kung madalas hindi makita ng iyong !string searches ang mga command na alam mong pinatakbo mo. Karaniwan ang mga value na 10000 at 20000, at halos walang dagdag na gastos dahil plain text ang file. Nangangahulugan ang negatibong value na walang limitasyon.

Makakatulong ang timestamps kapag bumalik ka sa isang server makalipas ang isang linggo:

export HISTTIMEFORMAT='%F %T '

Pagkatapos, nagpi-print ang history ng petsa at oras bago ang bawat entry dahil nagsisimulang magsulat ang bash ng comment line na naglalaman ng epoch seconds bago ang bawat command sa file.

Karaniwang reklamo na nawawala ang history kapag gumagamit ka ng higit sa isang terminal. Sariling listahan sa memory ang iniingatan ng bawat shell at isinusulat ito kapag lumalabas, kaya kung wala ang histappend, ino-overwrite ng huling shell na nagsara ang mga na-save ng ibang shell. Dalawang setting ang kailangan:

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

Dahil sa histappend, idinadagdag ng shell ang listahan nito sa dulo ng file sa halip na palitan ang file. Idinadagdag ng history -a ang mga bagong linya pagkatapos ng bawat prompt, kaya kahit ma-kill ang session sa halip na maisara nang maayos, naiiwan pa rin sa file ang mga command nito. Itinatakda na ng default na ~/.bashrc ng Ubuntu ang histappend. Mas mahalaga ito kapag mas marami kang pinapatakbong machine dahil per user at per machine iniimbak ang history. Kaya kapag nagma-manage ka ng ilang server mula sa isang workstation, maaaring nasa ibang host lang ang !$ na hinahanap mo.

Pag-iwas na mapasama ang secrets sa bash history

HISTCONTROL ang nagtatakda kung aling mga line ang ise-save.

  • ignorespace inaalis ang anumang line na nagsisimula sa space.
  • ignoredups inaalis ang line na kapareho ng naunang line.
  • ignoreboth ginagawa ang dalawang nabanggit sa itaas.
  • erasedups inaalis ang lahat ng mas lumang kopya ng ise-save na line.
export HISTCONTROL=ignoreboth

Kapag active ang ignorespace, ang isang space sa unahan ay nag-iiwan sa command sa in-memory list, kaya hindi rin ito nakakarating sa file. Suriin ang value bago mo ito asahan gamit ang echo "$HISTCONTROL". Itinatakda ng default user ng Ubuntu na .bashrc ang ignoreboth, pero maaaring hindi ito naka-set sa root shell o minimal image. Kapag ganoon, walang epekto ang space sa unahan at mase-save ang line gaya ng iba.

Maging malinaw sa sarili kung ano ang napoprotektahan nito. Hindi mapupunta ang line sa ~/.bash_history. Makikita pa rin ito sa output ng ps habang tumatakbo ang process, kaya mababasa ito roon ng sinumang ibang user sa server. Itinatala ng sudo ang command na pinatakbo nito sa system log. Plain text ang history file, kaya panatilihin ito sa mode 600 at tandaan na mababasa ito ng sinumang may access sa home directory mo, kasama ang huling ilang libong command.

Kung may sensitibong entry na sa list, alisin ang entry at isulat muli ang file:

history
history -d 517
history -w

Inaalis ng history -d ang entry na iyon sa memory, at isinusulat ng history -w ang kasalukuyang list sa ibabaw ng file. Nililinis ng history -c ang buong list. Ang HISTIGNORE ang kaugnay na setting para sa noise sa halip na secrets: iniiwas ng HISTIGNORE='ls:pwd:history:clear' na mapasama ang mga line na iyon sa list para kapaki-pakinabang ang mga resulta ng iyong searches.

Bakit walang ginagawa ang history expansion sa isang shell script

Ang history expansion ay para sa mga interactive shell. Tumatakbo ang isang script sa non-interactive shell, kung saan hindi naka-enable ang history list at naka-off ang expansion. Kaya nananatili sa linya bilang ordinaryong text ang !! at !$. Sa loob ng script, inuutusan ng sudo !! ang sudo na patakbuhin ang command na literal na pinangalanang !!, kaya nabibigo ito.

Suriin ang anumang shell na kasalukuyan mong ginagamit:

echo $-

Ang output ay ang set ng kasalukuyang option flag, gaya ng himBHs. Ibig sabihin ng i ay interactive ang shell, at ibig sabihin ng H ay naka-enable ang history expansion. Patakbuhin ang parehong linya sa loob ng isang script at wala roon ang alinmang letra.

Iyan ang hangganan sa pagitan ng dalawang bahagi ng iyong paggamit ng shell. Sa prompt, nakatutulong ang !$ at Ctrl-R na makatipid ng mga keystroke para sa mga command na nakikita mo pa. Sa isang file, tahasan mong tinutukoy ang mga bagay: ilagay ang path sa isang variable, o kunin ang output gamit ang command substitution. Kung nakadepende ang isang script sa personal mong history, iba ang magiging resulta nito para sa susunod na taong magpapatakbo nito. Kabaligtaran iyon ng layunin ng isang script.

Dahil dito, ang bawat halimbawa sa page na ito ay para i-type sa isang aktibong prompt. Hindi pareho ang magiging gawi ng mga ito kapag ini-paste sa isang .sh file.

FAQ

Ano ang ginagawa ng !! sa bash?

Ang !! ay nag-e-expand sa buong text ng naunang command line, kaya inuulit ng sudo !! ang huli mong command bilang root. Textual ang expansion at nangyayari ito bago i-parse ng bash ang line. Ipinapakita rin ng bash ang nabuong line bago ito patakbuhin. Kung hindi ka sigurado kung ano ang naunang line, i-type muna ang sudo !!:p. Ipi-print ng :p ang expansion nang hindi ito pinapatakbo, at idaragdag nito ang na-print na line sa history, kaya mapapatakbo ito ng kasunod na !!.

Paano ko magagamit muli ang huling argument ng naunang command?

Gamitin ang !$. Pagkatapos ng sudo mkdir -p /srv/www/app/releases, nagiging ls -ld /srv/www/app/releases ang line na ls -ld !$. Kinukuha nito ang huling word ng line, kaya itinuturing na huling word ang redirection target kung nasa dulo ito. Maaari ka ring magdagdag ng text dito: dinaragdagan ng !$/config.env ang path, dahil humihinto ang word designator sa /. Ang interactive na alternatibo ay Alt-.. Inilalagay nito ang parehong text sa prompt para mabasa mo muna bago pindutin ang Enter.

Bakit sinasabi ng bash na "event not found" kapag may exclamation mark ang text ko?

Hindi pinoprotektahan ng double quotes ang ! laban sa history expansion, kaya hinahanap ng echo "done!" ang isang history event at ipinapakita ang bash: !": event not found. Pinoprotektahan ito ng single quotes, kaya isulat ang echo 'done!'. Pinipigilan din ng backslash ang expansion, pero sa loob ng double quotes ay iniiwan ng bash ang backslash sa output. Para mag-paste ng mahabang block na naglalaman ng !, i-off ang expansion para sa session gamit ang set +H.

Bakit hindi gumagana ang !! at !$ sa shell script ko?

Naka-enable lamang ang history expansion sa interactive shells. Non-interactive ang pagpapatakbo ng script, kaya hindi bumubuo ng history list ang shell at iniiwan ang !! sa line bilang plain text. Patakbuhin ang echo $- para makita kung alin ang ginagamit mo: nagpi-print ang interactive shell ng mga flag na kabilang ang i at H, samantalang wala ang alinman sa mga ito sa output ng script. Sa mga script, gumamit na lang ng variable o command substitution.

Paano ko maiiwasang mapasama sa bash history ang password?

Itakda ang HISTCONTROL=ignorespace o HISTCONTROL=ignoreboth sa ~/.bashrc, pagkatapos ay simulan ang command gamit ang isang space at hindi ito mase-save. Kumpirmahin muna ang value gamit ang echo "$HISTCONTROL", dahil walang epekto ang leading space kapag unset ito. Inilalayo lamang nito ang line sa ~/.bash_history. Nakikita pa rin ang command sa ps habang tumatakbo ito, at nilolog ng sudo ang mga command na ipinatupad nito. Kung naka-save na ang isang secret, hanapin ang number nito gamit ang history, pagkatapos ay patakbuhin ang history -d <number> na sinusundan ng history -w upang muling isulat ang file.