ANSI 转义码标准

大家好!今天我想谈谈 ANSI 转义码。

很长一段时间里,我只是模糊地知道 ANSI 转义码(就是那种可以在终端让文本变红之类的东西),但我并不真正了解它们应该在哪里定义,也不清楚是否有相关标准。我只是对它们有种模糊的 “这里有未知风险” 的感觉。而在今年学习终端相关知识时,我了解到:

  1. ANSI 转义码为终端的可用性带来了很多改进(你知道通过 SSH 连接到远程机器时,有办法可以把内容复制到系统剪贴板吗?这是一个名为 OSC 52 的转义码!)。

  2. 它们并没有完全标准化,正因如此,其工作效果并不总是可靠。而且由于它们是不可见的,所以排查转义码问题时会极其令人沮丧。

所以我想为自己整理一份有关转义码现有标准的清单,因为我想知道,使用它们是否就得面临不可靠和令人沮丧的情况,还是说未来我们能够更放心地使用它们。

什么是转义码?

你是否曾在终端中按下左箭头键,然后看到过 ^[[D?这就是一个转义码!它被称作 “转义码” 是因为第一个字符是 “转义” 字符,通常写作 ESC\x1b\E\033 或者 ^[

转义码是终端模拟器与在终端中运行的程序来传递各类信息(颜色、鼠标移动等)的方式。转义码有两种类型:

  1. 输入码:终端模拟器针对无法用 Unicode 表示的按键操作或鼠标移动所发送的码。例如,“左箭头键” 对应的是 ESC[D,“Ctrl + 左箭头” 可能是 ESC[1;5D,而鼠标点击可能是像 ESC[M:3 这样的内容。
  2. 输出码:程序可以输出这些代码来对文本上色、移动光标、清屏、隐藏光标、将文本复制到剪贴板、启用鼠标点击报告、设置窗口标题等。

现在我们来聊聊标准吧!

ECMA-48

我找到的与转义码相关的首个标准是 ECMA-48,它最初发布于 1976 年。

ECMA-48 做了两件事:

  1. 为转义码定义了一些通用格式(比如 “CSI” 码,格式是 ESC[ + 某些内容,以及 “OSC” 码,格式是 ESC] + 某些内容)。

  2. 定义了一些特定的转义码,例如 “将光标向左移动” 是ESC[D,“将文本变为红色” 是 ESC[31m。在该规范中,“光标左移” 的这个码被称为 CURSOR LEFT,而用于更改颜色的被叫做 SELECT GRAPHIC RENDITION

这些格式是可扩展的,所以未来其他人有空间去定义更多转义码。如今很多流行的转义码在 ECMA-48 中并未定义,例如,终端应用程序(如 vim、htop 或 tmux)支持使用鼠标是很常见的,但 ECMA-48 没有为鼠标操作定义转义码。

xterm 控制序列

有很多转义码在 ECMA-48 中未做定义,例如: 启用鼠标点击报告(你在终端的哪里点击了?) 带括号粘贴(你输入的文本是粘贴的还是自己敲的?) OSC 52(终端应用程序可以用它将文本复制到系统剪贴板) 我认为(如果我说错了请纠正我!),上述这些以及其他一些转义码源自 xterm,在《XTerm 控制序列》中有记载,并且已被其他终端模拟器广泛采用。 这份 “xterm 所支持内容” 的清单严格来说不算一个标准,但 xterm 极具影响力,所以它似乎是一份重要的文档。

terminfo

在 20 世纪 80 年代(在某种程度上现在也是,但据我理解,在 80 年代这种情况要严重得多),终端实际支持的转义码存在大量差异。

为了解决这个问题,有一个针对各种终端转义码的数据库,叫做 “terminfo”。

看起来 terminfo 的标准被称为 X/Open Curses,不过由于某些原因,你需要创建一个账户才能查看该标准。它定义了数据库格式,以及用于访问该数据库的 C 库接口(“curses”)。

例如,你可以运行以下 bash 代码片段,来查看系统所知的所有不同终端针对 “清屏” 这一操作的所有可能转义码:

bash 复制
for term in $(toe -a | awk '{print $1}')
do
  echo $term
  infocmp -1 -T "$term" 2>/dev/null | grep 'clear=' | sed's/clear=//g;s/,//g'
done

在我的系统上(可能在我使用过的每个系统上都是如此?),terminfo 数据库是由 ncurses 管理的。

程序应该使用 terminfo 吗?

我觉得有意思的是,应用程序在处理 ANSI 转义码时有两种主要方式:

  1. 根据 TERM 环境变量的内容,使用 terminfo 数据库来确定要使用哪些转义码。例如 fish 就是这么做的。

  2. 确定一组 “单一通用的” 转义码,这些码在 “足够多” 的终端模拟器中都能生效,然后直接将它们硬编码进去。

采用第二种方式(“不使用 terminfo”)的程序 / 库的一些例子包括:

我很好奇为什么人们可能会不再使用 terminfo,然后我发现了一篇由 fish 的一位维护者所写的关于 terminfo 的非常有趣且极为详细的吐槽文章,其观点是:

terminfo 的作者们 做了大量在当时极其重要且有帮助的工作。

但我的观点是,如今它不再那么重要了。” 我无法公正地概括这篇文章内容,所以就不总结了,我觉得它值得一读。

是否存在 “单一通用的一组” 转义码?

我刚才谈到了可以使用一组 “通用的” 转义码,让它们能在大多数人使用的环境下起作用这个想法。但这组转义码是什么呢?是否有共识呢?

我其实完全不知道这个问题的答案,但通过阅读相关资料,似乎它是以下内容的某种组合:

  • VT100 所支持的代码(尽管其中一些在现代终端上已不相关)。
  • ECMA-48 中的内容(我觉得这里面也有些东西现在不再相关了)。
  • xterm 所支持的内容(不过我猜其中并非所有内容都真正得到了广泛支持)。

并且也许最终是 “确定你认为用户最常使用的终端模拟器,然后在这些模拟器上进行测试”,就如同网页开发者决定可以使用哪些 CSS 功能时所做的那样。

不过我认为对于终端而言,并不存在像 Can I use…? 或者 Baseline 这样的资源。(理论上 terminfo 应该是终端领域的 “caniuse”,但似乎新的终端功能被发明出来后,往往要花上十多年时间才会被加入到 terminfo 中,这让它的作用非常有限)。

使用 terminfo 的一些原因

我还在 Mastodon 上询问了为什么在 2025 年人们仍觉得 terminfo 有价值,得到了几个让我觉得有道理的原因:

  • 有些人期望能够使用 TERM 环境变量来控制程序的行为(例如设置 TERM=dumb 时),而且在没有 terminfo 的情况下,对于这应该如何实现是没有标准的。

  • 尽管现在终端模拟器之间的差异比 80 年代小了,但远非没有差异:有图形化终端、Linux 帧缓冲控制台、通过串行控制台连接到服务器时的情况、Emacs shell 模式,可能还有更多我没提到的情况。

  • 并不存在一个关于 “单一通用的一组” 转义码的标准,而且有时程序使用的转义码实际上并没有得到足够广泛的支持。

terminfo 与用户代理检测

ncurses 使用 TERM 环境变量来决定使用哪些转义码的这种方式,让我想起了过去 Web 服务器有时会根据浏览器用户代理来决定提供哪个版本的网站。

这似乎也产生了一些相同的结果 ——iTerm2 将自己报告为 “xterm - 256color” 的方式,类似于 Safari 的用户代理是 “Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15”。在这两种情况下,终端模拟器 / 浏览器最终都更改了自己的用户代理,以避开效果不佳的用户代理检测。

在 Web 领域,我们最终认定用户代理检测不是一种好做法,而是应该专注于标准化,这样我们就能向所有浏览器提供相同的 HTML/CSS。不过我不知道终端领域未来是否也会采取相同的方法 —— 我认为如今的终端生态比 Web 曾经的状况要更加碎片化,而且资金支持也少得多。

更多文档 / 标准

我认为这很有趣的原因

我有时会看到有人说 Unix 终端 “过时了”,而因为我非常喜欢终端,所以我总是很好奇哪些渐进式的改变可以让它看起来不那么 “过时”。

也许如果我们有一个更清晰的标准环境(就像 Web 领域那样!),终端模拟器开发者开发新功能就会更容易,终端应用程序的开发者也能更有信心地采用这些功能,这样我们都能从中受益,并且在终端中有更丰富的体验。

显然,对 ANSI 转义码进行标准化并非易事(ECMA-48 首次发布差不多是 50 年前了,可我们至今都还没完全实现标准化!)。我甚至都不知道所有的挑战是什么。但 HTML/CSS/JS 曾经的情况也极其糟糕,而现在已经好多了,所以也许还是有希望的。