命令行提示符每天会出现几百次,却常被当成一条“换个颜色就完成”的装饰配置。真正好用的提示符应该回答几个很具体的问题:我以什么身份工作、处在哪个环境、当前目录是什么、上一条命令是否成功,以及这次按回车会不会在错误的上下文里执行。
这一章会把提示符当成 Bash 的一段交互流程来拆解。我们先弄清 PS1 到底在什么时候展开,再处理颜色、Git 分支、上一条命令状态、性能和安全,最后把配置放进可检查、可回滚的启动文件中。所有实验都限制在一次性容器的 /tmp/welearn-ch13 目录。
Bash 运行一条前台命令,等待它结束,取得退出状态,然后准备下一次输入。在绘制主提示之前,它可以先执行 PROMPT_COMMAND;随后解码 PS1 中的提示转义,再让 Readline 接管键盘输入和光标编辑。

这个顺序解释了三个常见现象。
cd 之后 \w 会立刻变成新路径,因为 PS1 不是终端启动时只计算一次。$(git ...) 放进提示符会重复运行,因为每次重新显示提示都要做命令替换。提示符不是授权系统。看到 # 只能提醒当前有效 UID 为 0,真正的访问控制仍由内核权限、能力、挂载和安全策略决定。同样,颜色只能辅助识别,不能代替文字标记。
PS1、PS2、PS3、PS4 通常只需要是当前 Bash 进程里的 Shell 变量。export 的意义是把变量放进子进程环境;外部程序不负责绘制 Bash 提示符,因此没有必要为了定制提示而 export PS1。把它们统称为“环境变量”会掩盖这一层边界。
PS1 是主提示。Bash 准备读取一条新命令的第一行时显示它。PS2 是续行提示:引号未闭合、管道还在等待右侧命令,或者 if、for、函数体还没有结束时,Bash 用它提示“语法仍未完成”。
PS1='\u@\h:\W\$ '
PS2='继续> '例如输入未结束的复合命令:
xiaohu@lab:src$ if true; then
继续> printf '完成\n'
继续> fi
完成PS2 不是错误信息。它表示解析器已经知道当前文本还不足以形成完整命令。若你是不小心漏了引号,可以按 Ctrl-C 放弃这一轮输入。
PS3 只在 Bash 的 select 结构等待选择时使用。未设置时,默认提示通常是 #? :
PS3='请选择编号> '
select target in 开发 测试 生产; do
printf '你选择了:%s\n' "$target"
break
donePS4 是 set -x 的跟踪前缀。Bash 在输出将要执行的展开后命令行前加上它;展开结果的第一个字符还可能重复,用来表达更深的间接层级。
PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:-main}: '
set -x
do_work
set +x跟踪会暴露展开后的参数。不要在含令牌、密码或私钥的流程里随意打开 set -x,也不要把秘密放进 PS4。
提示转义由 Bash 在每次绘制时解码。它们和 $USER、$(date) 不是同一类语法:前者是 Bash 专门为提示定义的反斜杠代码,后者属于后续的 Shell 展开。

路径过长时不必立刻改成外部命令。先试:
PROMPT_DIRTRIM=3
PS1='\u@\h:\w\$ 'PROMPT_DIRTRIM 大于 0 时,\w 保留指定数量的尾部目录,并用省略标记代替前面的层级。若只关心当前目录名,直接用 \W,成本更低。
时间适合在长任务后帮助判断“什么时候回到提示”,却不一定要精确到秒。秒数每次变化,会增加视觉噪声;\A 往往比 \t 更克制。若需自定义格式,Bash 还支持 \D{format},其中格式交给 strftime 处理。
历史编号和命令序号不保证相同。历史可以从历史文件恢复,也可能受 HISTCONTROL、HISTIGNORE 影响;命令序号描述当前 Shell 会话里的执行顺序。
\r 会把光标移回行首,如果后面没有清理和覆盖逻辑,旧文本仍可能留在屏幕上。\e 只是产生 ESC 字节,并不保证后续控制序列被所有终端同样解释。
Bash 的提示处理可以分成两层。第一层是你执行 PS1=... 时的普通 Shell 引用和展开;第二层发生在每次显示提示时:先解码 \u、\w 等提示转义,再根据 promptvars 选项执行参数展开、命令替换、算术展开和引用移除。

比较两条赋值:
label=早
PS1="$label \W\$ "
label=晚双引号允许 $label 在赋值时展开,所以以后仍显示“早”。\W 的反斜杠在双引号中会保留,仍由提示解码器动态处理。
label=早
PS1='$label \W\$ '
label=晚单引号保留字面 $label。promptvars 默认开启,因此每次显示提示时都会读到当前值“晚”。这就是“现在算”与“每次显示再算”的区别。
检查当前状态:
shopt -q promptvars && echo 'promptvars 已开启'关闭后,提示转义仍然会解码,但 $name、$(command)、$((1+1)) 不再作为提示的第二阶段展开:
shopt -u promptvars
PS1='$USER \w\$ '此时 $USER 会按字面显示,而 \w 仍会更新。关闭 promptvars 能显著缩小动态求值面,但也会使依赖变量和命令替换的主题失效。修改前记录状态,实验后可用 shopt -s promptvars 恢复。
下面的写法会把赋值当下的状态写死:
PS1="[$?] \w\$ "要让 $? 留到提示阶段,至少需要单引号:
PS1='[$?] \w\$ '但这仍有一个时序问题:如果 PROMPT_COMMAND 先运行了其他命令,$? 可能已不是用户刚执行命令的状态。可靠做法是在提示钩子的第一步保存它,再让 PS1 只显示保存值。下一节会完成这条链路。
最稳的设计是让 PROMPT_COMMAND 负责准备数据,让 PS1 只负责排版。这样可以明确哪一步会执行命令,也能把上一条命令状态保存在任何查询之前。

$?__prompt_update() {
local rc=$?
PROMPT_STATUS=$rc
if (( rc == 0 )); then
PROMPT_MARK='✓'
else
PROMPT_MARK="✗${rc}"
fi
}
PROMPT_COMMAND=(__prompt_update)
PS1='${PROMPT_MARK} \u@\h:\W\$ 'local rc=$? 必须是函数的第一条实质命令。若先执行 git、printf、tput 或一次成功的条件检查,再读取 $?,保存到的就是提示准备动作的状态。
提示符同时保留符号与数字:成功显示 ✓,失败显示 ✗127。即使颜色被关闭或终端不支持,状态仍然能读懂。
PROMPT_COMMAND 可以是一个字符串,也可以是数组。数组中每个已设置元素都会在主提示前当作命令执行。直接写:
PROMPT_COMMAND=__prompt_update会覆盖目录跳转器、终端集成、调试器或虚拟环境工具已有的钩子。添加前先看类型:
declare -p PROMPT_COMMAND 2>/dev/null || true需要保证自己的状态捕获先运行时,可以在启动文件加载阶段保存旧值,再把函数放到数组首位:
if declare -p PROMPT_COMMAND 2>/dev/null | grep -q 'declare -a'; then
__old_prompt_commands=("${PROMPT_COMMAND[@]}")
PROMPT_COMMAND=(__prompt_update "${__old_prompt_commands[@]}")
elif [[ -n ${PROMPT_COMMAND-} ]]; then
__old_prompt_command=$PROMPT_COMMAND
PROMPT_COMMAND=(
这段初始化只能注册一次。反复 source ~/.bashrc 会重复插入函数,所以实际配置应先检查数组中是否已有同名元素,或把提示配置写成可幂等加载的独立文件。
颜色不是 Bash 字符串自身的属性。提示符把控制字节写到终端,终端解释这些字节并改变后续字符的呈现。Bash 和 Readline 还要知道哪些字节不可见。

常见 SGR 形式是 ESC [ 参数 m。ESC 是十六进制 1b;在 Bash 中可以写成 \e、\033 或 ANSI-C 引用 $'\e'。
不同终端可能把“粗体”呈现为高亮色,也可能不支持斜体。样式会一直影响后续输出,直到收到重置。因此提示符必须在用户开始输入前恢复默认状态,不能只在“下一次提示开头”再补救。
tput 根据 TERM 和 terminfo 数据库输出当前终端的能力字符串:
if [[ -t 1 && ${TERM-} != dumb ]] && command -v tput >/dev/null 2>&1; then
C_GREEN=$(tput setaf 2 2>/dev/null || :)
C_RED=$(tput setaf 1 2>/dev/null || :)
C_BOLD=$(tput bold 2>/dev/null
-t 1 检查标准输出是否连接终端;TERM=dumb 表示不要假定有丰富光标和颜色能力。NO_COLOR 常被工具作为“不要输出颜色”的生态约定,它不是 Bash 或 ECMA-48 的强制变量。如果团队采用它,可在条件里增加 [[ -z ${NO_COLOR-} ]]。
tput sgr0 可能不只输出 ESC[0m,因为 terminfo 会按终端能力恢复更多状态。命令失败时应退化为无颜色,而不是让提示符配置阻止 Shell 启动。
\[ 与 \]正确组合:
PS1='\[$C_BOLD$C_GREEN\]\u@\h\[$C_RESET\]:\W\$ '\[ 与 \] 本身不发颜色。它们告诉 Readline,中间展开出来的控制字节不占显示列。遗漏边界时,短命令可能看不出问题;当输入跨行、调用历史、按 Ctrl-A/Ctrl-E 或连续退格时,Readline 会用错误列数重画,表现为光标跳动、字符覆盖和回退错位。
边界只包控制序列,不能把可见用户名或路径也包进去。否则 Readline 会少算宽度,同样会错。
提示符越长,留给命令编辑的空间越少。信息是否应该加入,不看“能不能显示”,而看它是否会改变下一步决策。

远程管理常用:
PS1='\u@\h:\w\$ '\u 和 \h 能减少在错误账号或错误主机执行命令的概率,\$ 用 $/# 区分普通用户与有效 UID 0。生产、测试等环境不能只靠红绿颜色,可用可信配置写入明确文本:
CONTEXT_TAG='测试'
PS1='[$CONTEXT_TAG] \u@\h:\W\$ 'CONTEXT_TAG 应来自受控配置,而不是把任意远端返回值直接拼成 PS1 原文。容器中的 root 也不等于拥有完整宿主权限;提示符只描述当前 Shell 的身份视角。
长路径有三种常见策略:
\W 只显示末级目录;PROMPT_DIRTRIM 保留少量尾部层级;\n 把上下文与输入行分开。PROMPT_DIRTRIM=3
PS1='[\u@\h \w]\n\$ '两行提示把长上下文放在上一行,使每条命令从固定列开始。代价是纵向空间增加,复制多行终端文本时也要辨认提示行。
Python 虚拟环境、容器或云上下文最好由负责它们的工具提供一个已经处理好的短变量,再由 PS1 以参数展开显示。不要每次提示都扫描进程表或整棵目录来猜环境。若用 VIRTUAL_ENV,通常只取基名:
PROMPT_VENV=${VIRTUAL_ENV##*"/"}
PS1='${PROMPT_VENV:+($PROMPT_VENV) }\W\$ '读取当前符号分支可以使用:
git symbolic-ref --quiet --short HEAD 2>/dev/null脱离分支的 HEAD 会返回非零,这时可留空或显示短提交 ID。不要用 git branch | grep 解析给人看的格式,也不要默认每次运行 git status;后者可能检查工作区和索引,在大型仓库、网络文件系统或大量未跟踪文件中明显变慢。
推荐在状态函数中读取一次,并把结果放进变量:
__prompt_update() {
local rc=$?
local branch=
PROMPT_STATUS=$rc
if command -v git >/dev/null 2>&1; then
branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || branch=
fi
PROMPT_GIT=$branch
}
这里 PS1 保留的是变量引用,不把分支值拼成下一轮要解析的原始代码。Git 查询依然有成本,应在需要的环境启用;如果每次提示都卡顿,先临时去掉这一项再测量。
提示不是后台仪表盘。命令结束后,你在等待它出现;任何慢查询、网络访问、锁等待和副作用都会直接阻塞下一次输入。
PS1='$(sleep 0.1; printf 慢) \W\$ '这不是启动时等待 0.1 秒,而是每次绘制都等待。四次提示至少增加约 0.4 秒。命令替换还可能失败、打印错误、修改缓存或继承意外环境。
评估动态字段可以用三问:
能由 Shell 转义得到的信息,不要启动外部进程。例如时间用 \A,路径用 \w,用户名用 \u。
危险模式是先取得动态文本,再用双引号把值直接拼进 PS1:
payload='$(some-command)'
PS1="$payload \W\$ "赋值这一轮只做参数展开,payload 中的 $() 不会立刻递归执行;但结果已经成为 PS1 原始字符串。下一次提示的 promptvars 阶段会把它当命令替换。
把变量引用留在配方中更安全:
payload='$(some-command)'
PS1='${payload} \W\$ '参数展开得到的变量值不会再被重新扫描成一轮命令替换。对仓库名、分支名、路径或远端标签仍要处理终端控制字符,避免它们改变标题、颜色或光标;最简单的方案是只显示受控短标签。
GNU Bash 的提示说明明确警告,提示转义解码后再展开可能产生不希望的副作用。不同 Bash 版本会对部分路径转义做保护,但配置不应依赖“当前版本恰好处理了这个输入”。
ESC 开始的序列可改颜色、移动光标;OSC 序列常用于窗口或图标标题。XTerm 约定的 OSC 0/1/2 可以改标题,但其他终端的支持、过滤和结束符处理可能不同。
不要把未知网站、日志或仓库中的控制字符原样粘进启动文件。即使它不执行 Shell 命令,也可能隐藏文本、重写标题、触发响铃或让屏幕难以恢复。需要标题时,应使用目标终端明确支持的接口,并把内容限制为可信、无控制字符的文本。
多行提示由 \n 直接支持,适合把长上下文放在上行。Bash 没有和某些其他 Shell 相同的内建右侧提示变量。用“保存光标、移动到右边、再恢复”模拟右侧状态,会遇到终端宽度变化、自动换行、中文宽字符、组合字符、多行 Readline 编辑和后台输出的共同影响。
如果右侧信息必须存在,优先交给已经处理终端重绘的成熟工具;自己实现时要订阅窗口尺寸、计算显示列而不是字节数,并覆盖跨行、粘贴和历史编辑测试。简单配置里,把信息放到第一行通常更可靠。
窗口标题常见形式是 OSC 2 ; 文本 ST。它属于终端协议,不是 PS1 的跨终端保证。标题字符串中的 BEL、ESC、换行等控制字符必须过滤,且整个控制序列要放在 \[ 与 \] 内。终端不支持时应无声退化。
当终端样式或模式被破坏,可先尝试:
printf '\e[0m'
stty sanetput reset 会做更完整的终端重置,可能清屏或恢复模式,适合确认终端状态确实异常时使用,不应每次提示都运行。
提示配置出错通常不会破坏数据,却可能让每次打开 Shell 都报错、卡住或看不到光标。正确顺序是先在当前会话试用,再备份和语法检查,最后用新 Shell 验证。

PS1_SAVED=$PS1
PS1='test:\w\$ '
: "恢复当前会话中的原提示"
PS1=$PS1_SAVED这只影响当前 Shell。若彩色版本发生错位,先换成可读的纯文本:
PS1='bash:\w\$ '
PROMPT_COMMAND=清空 PROMPT_COMMAND 会停掉所有提示钩子,只适合作为临时诊断;确认问题后再按备份恢复。保持另一个未关闭的 Shell 会话,可以在新配置无法启动时编辑回去。
~/.bashrc 中的交互配置可先加边界:
case $- in
*i*) ;;
*) return ;;
esac这样即使某个非交互流程手动 source ~/.bashrc,也不会执行 tput、Git 查询和提示注册。随后按顺序操作:
backup="$HOME/.bashrc.prompt-backup-$(date +%Y%m%d-%H%M%S)"
cp -p "$HOME/.bashrc" "$backup"
: "使用编辑器修改 ~/.bashrc 后再检查"
bash -n "$HOME/.bashrc"
bash --noprofile --rcfile "$HOME/.bashrc" -ibash -n 只检查语法,不会证明每个命令都存在,也不会发现所有运行时延迟。用新交互 Shell 验证,不要立刻在唯一可用会话中 source 未检查配置。
恢复时:
cp -p "$backup" "$HOME/.bashrc"
bash -n "$HOME/.bashrc"如果启动文件无法加载,可先运行 bash --noprofile --norc -i 得到不读配置的干净交互 Shell,再执行恢复。
下面的实操使用 Debian Bookworm 的一次性容器。script 分配 PTY,让 Bash 真正进入交互模式;所有配置、录屏和错误文件都位于 /tmp/welearn-ch13。
docker run --rm -i \
--name welearn-linux-ch13-lab \
--hostname labhost.example.test \
debian:bookworm-slim bash -s <<'LAB'
set -eu
work=/tmp/welearn-ch13
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/a/b/c"
export TERM=xterm-256color
cat >"$work/prompt.rc" <<'RC'
case $- in
*i*) ;;
*) return ;;
esac
__prompt_update() {
local rc=$?
PROMPT_STATUS=$rc
if (( rc == 0 )); then
PROMPT_MARK='OK'
else
容器不安装额外包:该镜像中的 bash、script、tput、od 足以完成验证。trap 是中途失败的兜底,末尾仍显式删除并用 test 复查。
实测环境为 Bash 5.2.15。false 结束后,下一次提示显示:
[ERR:1] root@labhost:.../b/c#这证明函数在其他提示准备动作前保存了状态 1。未完成的 if 触发:
CONT> echo multi-ok
CONT> fi
multi-okPS4 的跟踪片段会带来源、行号和函数名:
+:3:main: f
+main:1:f: :tput 的字节检查中,绿色为 1b 5b 33 32 6d;sgr0 还包含额外恢复字节,说明从终端能力取得重置比假定固定字符串更稳妥。
彩色提示还应在 40–60 列窄窗口中测试:
Ctrl-A 到行首插字;Ctrl-E 回到行尾;PTY 对照实验中,相同可见提示只有 \[\e[31m\]P>\[\e[0m\] 与未标记版本不同。正确版和错误版产生了不同的回车与重绘字节轨迹,证明问题发生在 Readline 的列计算,而不是颜色是否成功显示。
实验末尾输出:
/tmp/welearn-ch13 已清理--rm 又会删除退出后的容器可写层。容器外复查:
docker ps -a --filter name=^/welearn-linux-ch13-lab$无结果表示没有同名容器残留。镜像缓存与这次容器写入是不同对象,不应为了清理一次实验而误删其他任务正在使用的镜像。
完成这套验证后,再把需要的字段逐项加回去。每增加一项,就问它从哪里取数据、多久执行一次、失败会怎样,以及无颜色和窄窗口下是否仍然可读。这样得到的提示符不一定最花哨,却能在真实工作里一直保持可靠。