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