在 FreeBSD sh 中将当前目录显示为终端标题

同时打开多个终端时,如果能直接从标题栏判断每个终端所在的目录,会方便 很多。

例如当前目录为:

/home/hodong/projects/nimfsoft.com

终端标题可以显示为:

~/projects/nimfsoft.com

如果当前目录不在主目录下,则显示完整路径:

/usr/local/src

本文以 FreeBSD 的 /bin/sh~/.shrc 为例。

修改终端标题的原理

兼容 xterm 的终端模拟器可以通过 OSC(Operating System Command)转义序列 修改终端标题。

printf '\033]0;%s\007' '新的终端标题'

各部分含义如下:

\033    ESC 字符
]0;     设置图标名称和窗口标题
%s      要显示的字符串
\007    使用 BEL 结束 OSC 序列

因此,可以直接将当前目录用作标题:

printf '\033]0;%s\007' "$PWD"

是否支持该功能取决于终端模拟器及其配置。

将主目录缩写为 ~

如果 $PWD 等于 $HOME,或者位于 $HOME 之下,就将主目录前缀替换为 ~

set_terminal_title() {
    if [ -n "${HOME-}" ]; then
        case "$PWD" in
            "$HOME")
                _terminal_title_path='~'
                ;;
            "$HOME"/*)
                _terminal_title_path="~${PWD#"$HOME"}"
                ;;
            *)
                _terminal_title_path=$PWD
                ;;
        esac
    else
        _terminal_title_path=$PWD
    fi

    printf '\033]0;%s\007' "$_terminal_title_path"
    unset _terminal_title_path
}

HOME 未设置时,${HOME-} 会展开为空字符串,从而避免把空值当作有效的 主目录前缀。

当主目录已正确设置时,使用 case 语句对当前路径进行分类。

case "$PWD" in
    "$HOME")
        _terminal_title_path='~'
        ;;
    "$HOME"/*)
        _terminal_title_path="~${PWD#"$HOME"}"
        ;;
    *)
        _terminal_title_path=$PWD
        ;;
esac

如果当前目录与主目录完全相同,标题为 ~

/home/hodong -> ~

如果当前目录位于主目录之下,则删除 $HOME 部分,并在剩余路径前加上 ~

/home/hodong/projects/nimfsoft.com
-> ~/projects/nimfsoft.com

${PWD#"$HOME"} 会从 $PWD 开头删除与 $HOME 匹配的最短前缀。

例如,在以下环境中:

HOME=/home/hodong
PWD=/home/hodong/projects/nimfsoft.com

其结果为:

/projects/nimfsoft.com

在前面加上 ~ 后,最终标题为:

~/projects/nimfsoft.com

单独使用 "$HOME"/* 模式,可以避免将 /home/hodong2/projects 错误地 视为 /home/hodong 的子目录。

/home/hodong2/projects
-> /home/hodong2/projects

_terminal_title_path 是临时变量。shell 函数中的变量默认会保留在当前 shell 中,因此使用完后通过 unset 删除,以降低命名冲突的可能性。

在执行 cd 后更新标题

只在 shell 启动时设置一次标题还不够。工作目录变化后,也需要重新设置。

下面的函数先调用原始的 shell 内建 cd,并且只在切换目录成功后更新标题:

cd() {
    command cd "$@" || return $?
    set_terminal_title
}

如果在函数内部直接执行 cd "$@",会再次调用同名函数并形成递归。

cd "$@"

使用 command cd "$@" 会绕过同名函数,调用 shell 内建的 cd

command cd "$@"

"$@" 会原样保留传给 cd 的所有参数:

cd
cd -
cd /usr/local/src

如果目录切换失败,则返回原来的失败状态,并保持标题不变。

command cd "$@" || return $?

因此,即使尝试进入一个不存在的目录,当前目录和终端标题也都会保持 不变。

最终 .shrc 补丁

将下面的补丁应用到 ~/.shrc

--- a/.shrc	2026-07-15 19:28:53.358286000 +0900
+++ b/.shrc	2026-07-15 19:47:56.461994000 +0900
@@ -46,6 +46,37 @@
 bind ^[[5~ ed-move-to-beg
 bind ^[[6~ ed-move-to-end

+# Update terminal title to the current working directory.
+set_terminal_title() {
+    if [ -n "${HOME-}" ]; then
+        case "$PWD" in
+            "$HOME")
+                _terminal_title_path='~'
+                ;;
+            "$HOME"/*)
+                _terminal_title_path="~${PWD#"$HOME"}"
+                ;;
+            *)
+                _terminal_title_path=$PWD
+                ;;
+        esac
+    else
+        _terminal_title_path=$PWD
+    fi
+
+    printf '\033]0;%s\007' "$_terminal_title_path"
+    unset _terminal_title_path
+}
+
+# Change directory and update the terminal title.
+cd() {
+    command cd "$@" || return $?
+    set_terminal_title
+}
+
+# Set the initial terminal title.
+set_terminal_title
+
 # set prompt: ``username@hostname:directory $ ''
 PS1="\u@\h:\w \\$ "

最初调用一次 set_terminal_title,是为了在读取 .shrc 时立即反映当前 目录。

set_terminal_title

之后,每次 cd 成功时都会再次更新标题。

重新加载配置:

. ~/.shrc

进入项目目录:

cd ~/projects/nimfsoft.com

标题将显示为:

~/projects/nimfsoft.com

进入主目录之外的路径时,则显示完整路径:

cd /usr/local/src
/usr/local/src

为什么不在 PS1 中调用函数

一种看似可行的写法是:

PS1='$(set_terminal_title)\u@\h:\w \$ '

但是在本文使用的 FreeBSD sh 环境中,该写法没有按预期工作。提示符显示 异常,终端标题也没有更新。

因此保留原有的提示符:

PS1="\u@\h:\w \\$ "

标题更新不依赖提示符的重新求值行为,而是在 cd 成功之后立即完成。

注意事项

该配置要求终端模拟器支持 OSC 标题序列,并允许应用程序修改标题。

本文假设目录名是普通文本。如果目录名可能包含 ESC、BEL 等终端控制字符, 则在将路径插入 OSC 序列之前应先进行过滤。

标题会在通过这里定义的 cd 函数切换目录时更新。shell 启动时则由显式的 初始调用设置标题。

总结

最终的显示规则如下:

/home/hodong                       -> ~
/home/hodong/projects              -> ~/projects
/home/hodong/projects/nimfsoft.com -> ~/projects/nimfsoft.com
/home/hodong2/projects             -> /home/hodong2/projects
/usr/local/src                     -> /usr/local/src

这个小设置可以在同时处理多个项目时,更容易区分不同的终端窗口。