
每次打開終端第一眼看到的那行userhost:~$并不是 Bash 隨手畫的裝飾而是它專門用整整一節(jié)來講的特性——Bash Features 里的 Controlling the Prompt。我第一次認真啃這節(jié)內容是因為配壞了一次 PS1提示符里顏色、路徑、git 分支全混在一起結果光標像抽風一樣在屏幕上亂跳敲一個長命令就換行錯亂最后只能開另一個終端來救場。后來把文檔啃完才發(fā)現提示符根本不是“能顯示就好”的小事它是一個信息密度極高的窗口也是你每天要看好幾百遍的東西。這篇就把 PS1 到 PS4、轉義序列、顏色規(guī)則、動態(tài)命令、PROMPT_COMMAND以及一套可以直接抄作業(yè)的生產級配置全拆開講清楚。適合剛學 Bash 想提升終端體驗的朋友也適合已經用了一段時間、但從來沒細摳過提示符細節(jié)的開發(fā)者。1. 先搞清楚提示符從哪里來PS1 到 PS4 各管什么1.1 你看到的那個提示符本質是一個環(huán)境變量在 Bash 里主提示符的內容由一個叫PS1的變量決定。你可以把它理解成“提示符模板字符串”。Bash 在等你輸入命令之前會先讀取這個變量、做一輪展開然后把結果打印到終端上。也就是說你看到的userhost:~$并不是寫死的你可以隨時重新賦值。最簡單的驗證方法就是在終端里直接敲PS1hello 回車之后提示符立刻變成hello等下一次命令結束又會恢復成原來的樣子。為什么因為這種賦值只作用于當前 shell等你重新登錄或者打開新的終端窗口系統(tǒng)又回到~/.bashrc里定義的默認值。所以真正要讓配置長期生效就得把 PS1 的賦值寫進~/.bashrc。這里有個很容易忽略的點~/.bashrc是交互式 shell 啟動時執(zhí)行的腳本非交互式 shell比如直接跑bash script.sh根本不會加載它也不會顯示提示符。這一點后面排查問題時會反復用到。1.2 PS2、PS3、PS4 都是干什么的很多人以為提示符就是那個主提示符其實 Bash 一共有四個提示符變量每一個的職責都不一樣。變量默認值用途PS1\s-\v\$主提示符shell 等待你輸入命令時顯示PS2續(xù)行提示符命令沒寫完被折行時顯示PS3#?select命令的交互菜單提示符PS4bash -x調試模式下每條命令前面的前綴這里面最容易感知到的是 PS2。你沒寫完后半句引號、或者用反斜杠換行繼續(xù)寫命令時提示符會變成那就是 PS2 在提醒你“這條命令還沒結束”。我見過有人把 PS2 改成...的一眼就能看出命令處于續(xù)行狀態(tài)體驗會好一些。PS3 很少有人去動但如果你寫過帶菜單的腳本比如讓用戶從幾個選項里挑一個執(zhí)行用的就是 PS3 提示符。PS4 則是在bash -x跟蹤執(zhí)行時出現的調試前綴日常使用頻率不高。這四兄弟里面真正的重頭戲只有 PS1后面對顏色、動態(tài)內容的控制絕大多數都圍繞它展開。1.3 交互式 Shell 的加載鏈路搞清楚配置為什么生效要先知道交互式 Bash 啟動時做了什么。登錄 shell 會先讀系統(tǒng)級配置和~/.bash_profile有些發(fā)行版會間接 source~/.bashrc而新的交互式 shell 則直接讀~/.bashrc。簡單說只要你想讓提示符改動全局生效把它扔進~/.bashrc就不會錯。還有一個技巧改完.bashrc不必重新登錄直接執(zhí)行source ~/.bashrc就能在當前窗口里讓新配置生效。如果不執(zhí)行 surface當前終端的 PS1 還是舊值這種情況會讓很多人誤以為自己改錯了。2. 提示符里能放什么轉義序列與顏色控制速查2.1 高頻轉義序列速查表PS1 最強大的地方在于它支持一套反斜杠轉義序列Bash 會在顯示前把它們替換成實際內容。這就像餐廳門口的菜單牌不光能寫“今日套餐”還能把價格、營業(yè)時間、當日天氣都標上去。轉義序列含義示例輸出\u當前用戶名user\h主機名不含域名部分host\H完整主機名host.example.com\w當前目錄完整路徑$HOME縮寫為~~/projects\W當前目錄最后一段projects\d日期格式為“星期 月 日”Fri Mar 14\t24 小時制時間含秒14:32:05\A24 小時制時間不含秒14:32\T12 小時制時間含秒02:32:05\12 小時制時間不含秒02:32 PM\D{格式}自定義日期格式按strftime語法\D{%Y-%m-%d}→2025-03-14\$root 用戶顯示#普通用戶顯示$$\#當前這條命令的編號42\!當前命令的歷史記錄編號1024\n換行換行\(zhòng)\反斜杠本身\\[開始標記一段非打印字符—\]結束標記一段非打印字符—\$這個轉義序列非常實用。它自動區(qū)分 root 和普通用戶避免你開著 root 窗口卻還看到$誤以為自己是普通用戶。反過來普通用戶也一眼能看出自己的權限狀態(tài)。2.2 ANSI 顏色規(guī)則并沒有那么復雜提示符配色的底層機制是 ANSI 轉義碼。終端看到\e[31m就知道后面輸出的字符用紅色顯示看到\e[0m就恢復默認顏色。規(guī)則很簡單\e[開頭接上一個或多個用分號隔開的控制碼最后加一個m。8 種基本顏色的編號如下顏色前景色背景色黑3040紅3141綠3242黃3343藍3444品紅3545青3646白3747除了顏色本身還有幾個常用的顯示屬性0重置全部屬性1加粗4下劃線7反色。注意加粗在某些終端里會改變顏色亮度所以很多人喜歡用\e[1;32m表示“亮綠色加粗”實際效果比純32更醒目。組合規(guī)則很容易理解把屬性和顏色碼用分號拼在一起比如前景紅色加粗就是\e[1;31m藍色加粗就是\e[1;34m。如果要用背景色就再把背景色碼加進去比如\e[1;31;43m是紅字黃底。還有一點值得提現代終端還支持 256 色\e[38;5;Nm甚至真彩色\e[38;2;R;G;Bm但兼容性參差不齊。做通用配置時用 8 色加粗組合是最穩(wěn)妥的方案。2.3\[ \]這對括號是保命符這是提示符里最容易被忽略、卻最要命的一對轉義序列。它的作用是告訴 Bash括號之間的內容是不占屏幕寬度的“隱形字符”比如顏色轉義碼。為什么這么重要因為 Bash 在計算提示符寬度時默認把每個字符都按等寬字符來計算。而 ANSI 顏色碼本身并不會在屏幕上畫出任何可見字符如果不用\[ \]包住Bash 會把它們也計入寬度。一旦命令跑到終端最右側需要換行ReadlineBash 的行編輯器就會算錯位置導致光標重疊、命令行顯示混亂。打個比方你貼貼紙的時候貼紙本身有厚度但貼紙的離型紙不該算進展示尺寸。\[和\]就是提醒 Bash 把離型紙的部分忽略掉。我在第一次配帶顏色的提示符時就踩過這個坑后來養(yǎng)成一個習慣所有 ANSI 碼一律用\[\e[...m\]包裹任何例外都不接受。3. 讓提示符動態(tài)起來命令替換與 PROMPT_COMMAND3.1 在 PS1 里執(zhí)行命令的時機和代價PS1 不光支持轉義序列還支持命令替換。也就是說你可以把一條命令的輸出直接放進提示符里。寫法就是在 PS1 里嵌入$(命令)Bash 會在每顯示一次提示符之前執(zhí)行它。最常見的場景是顯示 git 分支PS1\u\h:\w$(git branch --show-current 2/dev/null)\$ 但這里有個非常隱蔽的坑如果你用雙引號給 PS1 賦值比如PS1...$(git branch...)那$(...)會在賦值那一刻就執(zhí)行一次之后提示符永遠顯示當時的舊分支。正確做法是用單引號賦值讓命令替換延遲到每次顯示提示符時才執(zhí)行。單引號里$(...)是字面量Bash 解析 PS1 時才展開。命令替換會給每條命令增加額外開銷。git branch在大倉庫里可能要到一兩百毫秒如果你每條命令間隙都卡一下用戶體驗會很糟。后面第 4 章會講怎么用緩存優(yōu)化。3.2 PROMPT_COMMAND 能做哪些 PS1 做不了的事PROMPT_COMMAND是另一個配套變量它的值是一條命令通常是函數調用Bash 會在每次顯示 PS1 之前執(zhí)行它。它和 PS1 的差別在于PS1 負責“渲染”PROMPT_COMMAND 負責“干活”。我目前最常用的場景有三個。第一個是修改終端標題欄PROMPT_COMMANDprintf \033]0;%s%s:%s\007 $USER $HOSTNAME ${PWD/#$HOME/~}這樣終端窗口的標題會實時顯示當前用戶、主機和路徑配合終端的分組管理非常好用。第二個是捕獲上一條命令的退出碼。這里有個細節(jié)$?一旦被其他命令執(zhí)行就會覆蓋所以必須在 PS1 展開之前先保存。PROMPT_COMMAND 正好在 PS1 之前執(zhí)行可以在里面做PROMPT_COMMANDret$?然后 PS1 里引用$ret這個變量。注意ret$?本身也是一條賦值命令執(zhí)行后$?會變成 0但$ret的值已經被保存住了。第三個是動態(tài)切換整個提示符的配色。比如上一條命令失敗時把整個提示符變成紅色PROMPT_COMMANDret$?; if [ $ret -ne 0 ]; then PS1\\[\\e[1;31m\\]\\u\\$ ; else PS1\\[\\e[1;32m\\]\\u\\$ ; fi這種寫法等于在每條命令后重新生成 PS1靈活度很高但開銷也大建議只在你確實需要時才用。3.3 動態(tài)顯示命令退出狀態(tài)我比較推薦的做法是在 PS1 里放一個函數調用而不是直接寫一長串命令這樣可維護性好很多。function __last_status { local code$? if [[ $code -ne 0 ]]; then printf [%d] $code fi } PS1\u\h:\w$(__last_status)\$ 注意這里$?的取值時機命令替換在 PS1 展開時發(fā)生__last_status函數開頭拿到的$?正是上一條用戶命令的退出碼。如果這個函數放的不是第一個命令替換返回值就不可靠了這也是實際配置時容易搞亂的地方。在提示符里看到一個非零的退出碼比你在命令輸出里翻半天“到底成沒成功”要高效得多。我見過很多開發(fā)者都把這個作為標配尤其是頻繁編譯、部署的人。4. 從零組裝一套生產級提示符完整配置與驗證4.1 先定需求再動手在寫配置之前我習慣先把需求列清楚。這里我想實現四個目標一眼能區(qū)分用戶名、路徑、系統(tǒng)狀態(tài)在 git 倉庫目錄里顯示當前分支上一條命令失敗時顯示退出碼普通用戶和 root 的提示符標識能自動區(qū)分。這個方案不需要第三方工具也不需要額外安裝東西純 Bash 就能實現。4.2 完整配置代碼下面這段配置建議放到~/.bashrc末尾。先定義一個 git 分支函數和一個退出狀態(tài)函數# 顯示 git 分支名如果有 function __git_ps1_branch { local b b$(git branch --show-current 2/dev/null) if [[ -n $b ]]; then printf (%s) $b fi } # 顯示上一條命令的退出碼非零時 function __last_status { local code$? if [[ $code -ne 0 ]]; then printf [%d] $code fi }然后組裝 PS1。我這里用單引號賦值保證$(...)不會在賦值時被執(zhí)行PS1\[\e[1;32m\]\u\[\e[0m\]\[\e[1;33m\]\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]\[\e[1;35m\]$(__git_ps1_branch)\[\e[0m\]\[\e[1;31m\]$(__last_status)\[\e[0m\]\[\e[1;36m\]\$\[\e[0m\] 效果大概是這樣的userhost:~/projects(my-feature)[1]$其中用戶名是綠色、主機名是黃色、路徑是藍色、分支是品紅、退出碼是紅色、結尾的$是青色。每條命令的$?為 0 時退出碼部分不顯示提示符干干凈凈。4.3 加載與驗證寫入后執(zhí)行source ~/.bashrc然后分別驗證幾個場景進入一個 git 倉庫目錄看分支是否顯示故意執(zhí)行一條失敗命令比如ls /nonexistent看退出碼是否出現執(zhí)行sudo -s成 root看提示符末尾是否變成#。如果這些場景都正常說明這套配置基本合格了。4.4 性能與可維護性優(yōu)化第 3 章提過git 分支檢測開銷不可忽略。如果倉庫很大或者你經常用提示符渲染高頻的命令比如交互式 shell 里的每次回車我建議加一層簡單緩存只在目錄變化時才去查 git。declare -g __git_cached_branch declare -g __git_cached_dir function __git_ps1_branch { local cur_dir$PWD if [[ $cur_dir ! $__git_cached_dir ]]; then __git_cached_dir$cur_dir __git_cached_branch$(git rev-parse --abbrev-ref HEAD 2/dev/null) [[ -z $__git_cached_branch ]] __git_cached_branch fi [[ -n $__git_cached_branch ]] printf (%s) $__git_cached_branch }這個版本在同目錄內不會重復執(zhí)行 git性能明顯更好。代價是如果你在同一個目錄里切換分支比如拉取了新分支提示符里的分支名不會立刻更新。這是我權衡過才接受的折中——大多數時候你在同一目錄里做完操作再按回車分支名才變這其實影響不大。如果你實在想要實時更新那就得回到每命令都執(zhí)行 git 的版本。關于可維護性我給自己的建議是如果配置開始變得很長就把顏色定義抽成變量或者直接放到單獨的文件里 source。畢竟.bashrc里還有一堆別名和函數提示符配置單獨維護出問題也好定位。5. 提示符出問題時怎么排查常見坑與速查表5.1 長命令換行錯亂、光標重疊這個問題十有八九是\[ \]沒寫對。顏色轉義碼沒有用\[ \]包住Bash 把這些隱形字符當成了可見寬度。排查方法很簡單把 PS1 里所有顏色碼檢查一遍確保每個\e[...m都完整地被\[\]包裹。如果你已經有現成配置但不想逐字看可以用一個臨時簡化版 PS1 來驗證PS1\[\e[32m\]\u\[\e[0m\]$ 如果這樣沒亂說明問題確實出在顏色碼定位上。5.2 提示符里的命令只執(zhí)行了一次這個幾乎都是賦值時引用方式的問題。雙引號賦值會讓$(...)立即執(zhí)行并固化成字符串正確姿勢是單引號賦值或者用\$()來轉義。我在配置里也經??吹接腥诉@么寫PS1\u\h:\w \$(git branch --show-current) \$雙引號里\$之后的內容會延遲到 PS1 展示時執(zhí)行。這也可以但可讀性不如單引號容易寫著寫著就漏轉義。我的習慣是統(tǒng)一用單引號。5.3 提示符拖慢每條命令遇到這種問題先檢查 PS1 里命令替換的數量和開銷。git branch --show-current、ls這類外部命令每次展示都要起一個子進程成本就在那里。優(yōu)化方向有三去掉不必要的信息、加緩存、或者把高頻信息放進 PROMPT_COMMAND 里統(tǒng)一計算。如果你發(fā)現自己配了非常復雜的提示符每個按鍵都卡頓也可以考慮用異步方案但這已經超出純 Bash 提示符的范疇了需要借助外部工具來定期刷新緩存。5.4 特殊目錄名引發(fā)的問題目錄名本身可能包含換行符或控制字符在提示符里會引發(fā)不可預料的渲染問題。雖然這種情況極少但如果你在臨時目錄里不小心建了帶特殊字符的目錄再進到里面時終端渲染就會被破壞。穩(wěn)妥的做法是在函數輸出之前做一層過濾把控制字符去掉b$(git branch --show-current 2/dev/null) b${b//[[:cntrl:]]/}這個做法不僅適合分支名也適合任何插入到 PS1 的不可信字符串。5.5 常見問題速查表現象可能原因處理辦法改動.bashrc后沒變化沒有重新加載配置執(zhí)行source ~/.bashrc或重新登錄提示符換行錯亂ANSI 碼未被\[ \]包裹把所有顏色碼完整包進\[ \]動態(tài)命令只輸出一次雙引號賦值導致立即展開改用單引號或用\$()每條命令都變卡提示符里外部命令開銷過大加緩存或者去掉低頻信息非交互式 shell 里出現奇怪顏色腳本和管道里沒有 PS1 邏輯確認是否預期行為用tput做兼容終端標題不刷新PROMPT_COMMAND 未設置參照第 3 章的 printf 寫法5.6 關于兼容性和終端差異我這里用的\e[是 ANSI 轉義碼的一種寫法在主流終端模擬器里都能正常工作。如果你想要更強的兼容性可以改用tput來生成顏色碼tput會根據當前TERM環(huán)境變量輸出該終端支持的控制碼。比如C_USER$(tput setaf 2) C_RESET$(tput sgr0)用tput的好處是省去自己記顏色編號的麻煩但輸出結果里不含\[ \]仍然需要手動包一層。很多發(fā)行版自帶的.bashrc里就有基于tput的彩提示符判斷你可以參考那個寫法來做兼容處理。我個人在實際操作中的體會是提示符配置不怕寫得花最怕不知道每一步在干什么。你把顏色、動態(tài)信息、函數結構拆清楚出問題時看一眼就能定位如果把所有東西揉成一長串魔法字符串那哪天出了問題排查成本會遠遠高于你當初省下的那幾分鐘。還有一個小技巧每次改完 PS1先不要急著做其他事敲幾條長短不一的命令測試一下?lián)Q行行為確認 Readline 正常工作再收工。改提示符這件事量不大但細節(jié)確實多值得花一點時間調成順手的狀態(tài)。