SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-13

Bash 歷史擴充:!! 與 !$ 用法

了解 Bash 中 6 種實用的歷史擴充,使用 !! 重複上一個命令、!$ 取出最後一個引數,並先用 :p 列印確認內容再執行。

Bash 歷史擴充的作用

Bash 歷史擴充會在 bash 執行命令前,從 shell 歷史記錄重建較早的命令列。!! 會重複上一個命令。!$ 會插入上一個命令的最後一個引數。這項替換是文字層級的,並且會先於 bash 將命令列拆分成單字,因此替換後的內容會保留你第一次輸入時的確切字元。

以下 6 種形式幾乎涵蓋你在伺服器上會使用的所有情況:

  • !! 會重複上一整行,sudo !! 會以 root 身分再次執行該命令。
  • !$ 代表上一行的最後一個引數。
  • !* 代表上一行的所有引數。
  • !n 會執行編號為 n 的歷史記錄項目,!-n 會從目前位置向前倒數。
  • !string 會執行最近一個以 string 開頭的命令。
  • ^old^new 會重新執行上一行,並將第一個 old 替換為 new

以下所有內容都是在你自己的伺服器上,於互動式提示字元中輸入。指令碼中會關閉歷史擴充;最後一節會說明此設定。

只需輸入一次的長路徑

這是最能立即發揮效益的情境。你正在準備 release 目錄,而路徑很長,再輸入一次很容易打錯字。

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

這段回顯就是檢查依據。先查看它,再查看命令輸出,因為這是你唯一能看到 bash 如何解讀 !$ 的機會。

這個串接方式之所以成立,是因為 bash 會將展開後的內容儲存到 history,而不是你輸入的 !$。因此,第 3 行會從展開後的第 2 行取得最後一個引數,第 4 行則從第 3 行取得最後一個引數。第 4 行也顯示,designator 後面可以接文字:!$/config.env 會變成末尾加上 /config.env 的路徑,因為 word designator 會在 / 處停止。

以下是同一個工作階段中的另一個例子。編輯檔案後,你想列出包含該檔案的目錄:

ls -l !$:h

:h 是 head modifier。它會移除路徑的最後一個元件,作用與 dirname 相同。:t 只保留最後一個元件,:r 移除副檔名,而 :e 只保留副檔名。

使用 !! 與 sudo !! 重複執行上一個指令

你忘記了 sudo,服務管理器也會明確指出這一點:

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

Bash 會將 !! 替換成上一行文字,因此 shell 會執行 sudo systemctl restart nginx

!! 就是上一行內容,無論該行原本是什麼。經典的意外通常發生在這裡。你以為上一個指令是失敗的指令,但之後可能又執行了 cdhistory,也可能是失敗的指令位於前兩行。此時 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 會將其再次作為一般參數展開。請記住這個順序:history expansion 會在 parameter expansion 以及 使用 $( ) 的 command substitution 之前執行,因此它看不到值,只會看到文字。

相鄰的形式也值得了解。!^ 代表第一個引數,!:2 代表第二個引數,而 !:2-4 代表一個範圍。!!:$!$ 的完整寫法。

如果你想在確認執行前先查看文字,請按 Alt-.(或先按 Esc,再按 .)。Readline 會直接將上一個指令的最後引數插入提示字元,讓你編輯。再次按下該按鍵,可返回再前一個指令的最後引數。在按下 Enter 前,不會執行任何內容。

將所有引數透過 !* 傳遞

!* 是上一行中除了第一個字以外的所有文字。

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

「except the first one」這個條件很重要。!* 只會移除第 0 個字,不會移除其他內容,因此選項也會一併傳遞給路徑。執行 ls -l file1 file2 後,!*-l file1 file2,所以 sudo chmod 600 !* 會失敗,因為 chmod 會將 -l 當成引數接收。將文字從 0 開始編號,再改用切片:!!:2* 代表從第 2 個字到結尾。

sudo 也有相同的陷阱。執行 sudo chown deploy:deploy /srv/www/app 後,第 0 個字是 sudo,第 1 個字是 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。絕對編號只在同一個工作階段內固定;在同一台主機的第二個工作階段中,編號會不同。下次登入並重新讀入 history 檔案後,編號也可能不同。你昨天記住的編號,今天可能會指向另一個命令。

使用 !string 前置字串重新執行命令

!string 會執行最近一個以 string 開頭的命令。

!ss

這會重新執行上方清單中的 ss -tulpn,也就是 Linux 伺服器上開啟哪些連接埠 中說明的 listening socket 檢查。!?string? 會在整行的任何位置比對,而不只是在開頭比對。當你記得某個引數,但忘記命令名稱時,這項功能很有幫助。

前置字串應盡量寫長。!s 可能會比對到 sssudosystemctlshutdown,並執行其中最近執行的一個。你必須等到命令執行後,才知道實際比對到哪一個。!string:p 只會顯示比對結果,不會執行命令。若沒有任何相符項目,bash 會顯示 bash: !ss: event not found,且不執行任何命令,這是較安全的結果。

使用 ^old^new 修正一個拼字錯誤

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

Shell 會重新執行上一行,並將第一個 ngnix 替換為 nginx。這只會變更第一個符合項目。若要變更所有符合項目,請使用長格式 !!:gs/ngnix/nginx/;其中 s 表示 substitute,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

歷史記錄展開會在引號解析前執行,而雙引號無法保護 ! 字元。只有單引號和反斜線可以保護它。因此,echo 'deploy done!' 會輸出預期的內容。反斜線也能停止展開,但在雙引號內,bash 會將反斜線保留在輸出中,因此使用單引號是最簡潔的修正方式。

這個問題在密碼中最容易造成影響,因為強密碼通常包含 !。像 mysql -u app -p"S3cret!pass" 這樣的命令,可能會因 event not found 錯誤而失敗;如果剛好有符合的歷史記錄,也可能在沒有任何提示的情況下傳送不同的字串。請使用單引號;更好的做法是讓工具提示輸入密碼,這樣密碼就不會出現在命令列中。這項習慣也是帳號安全管理的一部分:請參閱 新 VPS 啟用後的前 10 分鐘,瞭解如何在新伺服器上處理金鑰與密碼。

如果準備貼上包含大量 ! 字元的文字區塊,請使用 set +H 關閉本次工作階段的展開功能,再使用 set -H 重新開啟。

HISTSIZE、HISTFILESIZE 以及歷史記錄的儲存位置

這兩個變數的名稱很接近,因此經常被混淆。

  • HISTSIZE 是執行中的 shell 保留在記憶體中的命令數量。
  • HISTFILESIZE 是磁碟上的檔案保留的行數,檔案位置為 ~/.bash_history,除非 HISTFILE 另有指定。

shell 結束時會寫入檔案,並在該時刻將檔案截短為 HISTFILESIZE 行。Ubuntu 的預設 ~/.bashrc 會將兩者分別設為 1000 和 2000。請檢查目前實際的設定:

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

如果 !string 搜尋經常找不到你確定曾執行過的命令,請提高這兩個值。10000 和 20000 很常見,幾乎不會增加負擔,因為該檔案是純文字。負值表示不設上限。

時間戳記有助於你離開伺服器一週後回來查看歷史記錄:

export HISTTIMEFORMAT='%F %T '

之後 history 會在每筆記錄前顯示日期和時間,因為 bash 會在檔案中每個命令前寫入一行包含 epoch 秒數的註解。

常見的問題是同時使用多個終端機時,歷史記錄會遺失。每個 shell 都在記憶體中保留自己的清單,並在結束時寫入檔案。因此,若未設定 histappend,最後關閉的 shell 會覆寫其他 shell 儲存的內容。以下兩項設定可解決此問題:

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

histappend 會讓 shell 將清單附加到檔案末端,而不是取代整個檔案。history -a 會在每次提示字元出現後附加新行,因此即使工作階段遭到終止而未正常關閉,仍會留下已執行的命令。Ubuntu 的預設 ~/.bashrc 已經設定 histappend。你管理的機器越多,這點就越重要,因為歷史記錄是依每位使用者、每台機器分別儲存的。因此,當你從一台工作站管理多台伺服器時,需要的 !$ 可能只存在於另一台主機上。

避免將秘密資訊留在 bash 歷史紀錄中

HISTCONTROL決定哪些行會被儲存。

  • ignorespace捨棄所有以空格開頭的行。
  • ignoredups捨棄與前一行完全相同的行。
  • ignoreboth同時執行上述兩項行為。
  • erasedups移除目前要儲存之行的所有較舊副本。
export HISTCONTROL=ignoreboth

啟用 ignorespace後,在命令前加一個空格即可避免命令進入記憶體中的歷史清單,因此也不會寫入檔案。依賴這項設定前,請先使用 echo "$HISTCONTROL"確認其值。Ubuntu 的預設使用者 .bashrc會設定 ignoreboth,但 root shell 或精簡映像檔可能未設定該變數。此時開頭的空格不會產生作用,該行會如同其他命令一樣被儲存。

請如實評估這項機制能提供的保護。該行不會出現在 ~/.bash_history中。程序執行期間,該命令仍會顯示在 ps輸出中,因此主機上的其他使用者可以從該處讀取。sudo會將執行過的命令記錄在系統日誌中。歷史紀錄檔是純文字檔,請將其權限設為 mode 600,並記住,任何能讀取 home directory 的人,都能讀取你最近幾千筆命令。

如果敏感資訊已經存在清單中,請移除該項目並重新寫入檔案:

history
history -d 517
history -w

history -d會從記憶體中移除該項目,history -w則會將目前的清單覆寫到檔案。history -c會清除整份清單。HISTIGNORE是與雜訊而非秘密資訊相關的設定:HISTIGNORE='ls:pwd:history:clear'會讓這些行不進入清單,使搜尋結果保留有用的命中項目。

Shell script 中的 history expansion 為何沒有作用

History expansion 僅適用於互動式 shell。指令碼會在非互動式 shell 中執行,此時不會啟用 history list,expansion 也處於關閉狀態,因此 !!!$ 會以一般文字留在該行中。指令碼中的 sudo !! 會要求 sudo 執行名稱恰好為 !! 的指令,因此執行會失敗。

請在目前使用的 shell 中執行以下指令:

echo $-

輸出內容是目前的選項旗標集合,可能類似 himBHsi 表示 shell 具備互動模式,H 表示已啟用 history expansion。在指令碼中執行相同的行時,這兩個字母都不會出現。

這就是 shell 工作中兩種情境的差異。在提示字元中,!$ 和 Ctrl-R 可協助重複使用仍看得到的指令,減少重新輸入。在檔案中,則應明確指定內容:將路徑放入變數,或使用 指令替換 擷取輸出。若指令碼依賴個人的 history,其他人執行時就會產生不同結果,這與指令碼的用途相反。

因此,本頁的所有範例都必須在正在使用的提示字元中輸入。將內容貼入 .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 關閉此工作階段的展開功能。

為什麼 !! 和 !$ 在 shell script 中沒有作用?

歷史展開只會在互動式 shell 中啟用。script 以非互動模式執行,因此 shell 不會建立歷史清單,而 !! 會以純文字形式保留在命令列中。執行 echo $- 可確認目前的 shell 類型:互動式 shell 會顯示包含 iH 的旗標,script 則兩者都不會顯示。在 script 中,請改用變數或命令替換。

如何避免將密碼寫入 bash 歷史記錄?

~/.bashrc 中設定 HISTCONTROL=ignorespaceHISTCONTROL=ignoreboth,接著讓命令以單一空格開頭,該命令就不會被儲存。請先使用 echo "$HISTCONTROL" 確認設定值,因為若該設定未設定,開頭的空格不會產生作用。這只能讓該命令不出現在 ~/.bash_history 中。命令執行期間仍可在 ps 中看到,而 sudo 會記錄其執行內容。若密密已經儲存,請使用 history 找出其編號,然後執行 history -d <number>,再執行 history -w 以改寫檔案。