使用ANSI转义码增强控制台输出

在图形界面已经十分发达的今天,许多程序(特别是开发工具)依然采用文本形式的命令行操作。在终端使用命令行和程序交互的时候,有些程序的输出会使用颜色甚至文本装饰来标识输出文本的不同片段以及不同的重要性,就像这样:

可能有读者认为这种效果需要用到终端甚至操作系统提供的底层 API 才能实现。但其实不然,现代的终端都允许使用 ANSI 转义序列,任何能在终端输出字符串的编程语言都可以以一种通用的方式控制终端行为,而无需接触底层 API 。

例如,使用任意编程语言运行类似如下的代码,可以在终端输出字体颜色为红色的 Hello World :

print("\033[31mHello World!\033[0m")

ANSI 转义序列是标准规定的内容,任何终端都有理由支持这样做。接下来系统地介绍 ANSI 转义序列的历史与内容。

ANSI转义码的历史

ANSI 转义码的历史可以追溯到 20 世纪中叶计算机技术刚开始发展的时期。彼时的计算机使用电传打字机(Teletypewriter, TTY)实现远程文本交流。不过当时各种不同的公司和组织的电传打字机使用各自的编码系统来表示字符,这导致了互操作性的问题。

1963 年,美国国家标准协会(ANSI)发布了 ASCII (American Standard Code for Information Interchange),这是一个 7 位字符集,能够编码 128 个字符。ASCII 包含大小写英文字母、数字、常用标点符号以及一组控制字符(称为 C0 控制码,范围为 0x00 到 0x1F ),包括熟悉的换行符 LF 、回车符 CR 以及制表符 HT 等。

在 20 世纪 70 年代,随着视频显示技术的发展,出现了视频显示终端。视频显示终端可以实现更复杂的功能(例如使用不同颜色、定位光标、清除屏幕等)。由于视频显示终端依旧使用 ASCII 字符来传输文本,为了实现这些复杂的功能,厂商在设计视频显示终端时,允许在输出中使用特定的字符序列控制终端的行为。但是,不同的厂商在设计终端时采用的控制序列也不尽相同,又导致了互操作性的问题。

1976 年,欧洲信息处理系统标准化组织(ECMA)通过了 ECMA-48 标准,在扩展的 8 位字符集中定义了一系列具有控制功能的字符标准,包括一组扩展的控制字符(称为 C1 控制码,范围为 0x80 到 0x9F),支持更复杂的文本和设备控制功能。同时,为了兼容 7 位字符集的情况,ECMA-48 允许使用 ESC (Escape, 位号为 0x1B ,是 C0 控制码中的一个字符,用作转义功能)加上另一个 7 位字符来表示这组扩展的控制字符。

1978年,数字设备公司(Digital Equipment Corporation, DEC)推出了 VT100 终端,这是一种功能强大的视频显示终端。VT100 在 ECMA-48 的标准上设计了一套字符序列来实现复杂的指令。VT100 的设计非常成功,以至于后续其它厂商的设计的终端也基本兼容它所采用的控制字符序列。随着 VT100 和类似终端的普及,它的控制字符序列成为事实上的行业标准。

VT-100 终端的外观 图片来源于 terminals wiki

受 VT100 的启发,美国国家标准协会(ANSI)在 1979 年发布了 ANSI X3.64 标准,这个标准定义了一套标准化的转义字符序列,是所有终端共享的指令集,并要求采用 ASCII 的数字字符传递所有的信息,也就是现在所说的“ANSI 转义序列”。后来,ECMA-48 和 ANSI X3.64 在 1992 年被合并为 ISO 6429 国际标准,在国际上统一了视频终端和类似设备的操作方法。

在后来的几十年中,尽管物理终端逐渐被淘汰,但它们的标准被带到了现代的终端仿真器中。现代操作系统自带的终端仿真器(如 Unix/Linux 的 xterm,macOS 的 Terminal,Windows 的 PowerShell 和 Windows Terminal 等)都在不同程度上支持这些标准。

ANSI转义码的内容

ANSI控制字符

在介绍 ANSI 控制序列前,先简单回顾一下 ANSI 中的控制字符。

在 ASCII 规定的 7 位字符中,一些控制字符(即 C0 控制字符)也可以控制终端的行为,现在还比较常用的有以下几个:

  • 响铃符 BEL (Bell):位号 0x07 ,可以产生铃声(或者在不能发声的设备上显示相应图案)
  • 退格符 BS (Backspace):位号 0x08 ,用于将当前光标回退一个位置(虽然标准规定只是回退而不删除任何内容,但实际上终端执行时都会删除前一个字符)
  • 水平制表符 HT (Horizontal Tab):位号 0x09 ,将光标移动到下一个水平制表位
  • 换行符 LF (Line Feed):位号 0x0A ,可以使光标向下移动一行。
  • 回车符 CR (Carriage Return):位号 0x0D ,这里换行符的功能仅仅只是向下移动一行,并不会使光标回到行首的位置,回车符才用于将光标移动到行首的位置(但是在 Linux系统中 LF 同时具有 CR 的作用)
  • 转义符 ESC (Escape):位号 0x1B ,用于得到控制序列,也是本

换行符和回车符的名字和功能在今天看来都有些奇怪,但要注意当时它们是用于打字机的,感兴趣的可以搜索早期打字机的结构。

0x80 位号以后还有一些终端可用的 C1 控制字符。C1 控制字符最早是设计成 8 位的,但是为了兼容 7 位的情况,它允许使用 ESC 字符外加一个其它的 7 位字符来表示它。现代的很多终端(包括 xterm 和 Windows Terminal )普遍采用 ESC 开头的 7 位形式来使用它们,而不是直接使用原生的 8 位字符。

在大多数编程语言中,可以在字符串内使用 \033\x1b 表示 ESC 字符,有些编程语言还支持使用 \e 表示它

以下列举了常用的 C1 控制字符:

  • IND (Index):位号 0x84 ,通常使用 ESC 'D' 表示,用于将光标向下移动一行,并且保持当前列的位置不变。它和 C0 控制字符中的 LF 很像,但是由于 LF 在一些系统中同时具有别的功能,所以 IND 用于严格定义这种下移的行为。
  • NEL (Next Line):位号 0x85 ,通常使用 ESC 'E' 表示,用于将光标移动到下一行的起始位置,相当于 LFCR 的结合。
  • RI (Reverse Index):位号 0x8D ,通常使用 ESC 'M' 表示,用于将光标向上移动一行,并保持当前列的位置不变,可以看作是 IND 的反向操作。
  • HTS (Horizontal Tabulation Set):位号 0x88 ,通常使用 ESC 'H' 表示,用于将当前光标位置设置为制表位,这样在前面位置使用的 HT 就会来到这个位置。
  • CSI (Control Sequence Introducer):位号 0x9B ,通常使用 ESC '[' 表示,用于引入控制序列,这是实现文本格式化和光标移动的核心控制字符,下文将详细介绍。
  • OSC (Operating System Command):位号 0x9D,通常使用 ESC ] 表示,一般用于实现特定操作系统支持的功能,兼容性不好,用的比较少。

控制序列

以上介绍的控制字符也实现了一些终端控制能力,但控制字符相对终端的功能来说还是太少,远远不够控制终端的各种属性。因此 ANSI 提出使用使用一组特定的字符序列来控制终端,也就是这里说的 ANSI 转义序列(ANSI escape sequence)。

所有的转义序列都以 C1 控制字符 CSI(位号 0x9B ,但一般使用 ESC '[' 表示)开头。后面接上一组普通字符作为具体的控制命令。终端在接受到 CSI 字符时,会把接下来属于转义序列的字符解释为相应的指令,而不是普通的显示字符。在读取完转义序列后,之后接收到的字符将仍然作为可显示字符显示在屏幕上,除非它们也是控制字符或者属于转义序列的字符。因此转义序列可以方便地嵌入在任何用于输出的字符串内,可以被任何编程语言天然地支持。

转义序列具有不同的长度,但它们都满足以下格式:

所有序列都以 CSI 开头,中间是若干个参数字节(parameter bytes),参数字节的取值范围是 ASCII 0x30-0x3F 的字符(即字符 0–9:;<=>? ),不同的参数使用中间字节(intermediate bytes) 隔开,中间字节是 0x20–0x2F 的 ASCII 字符(即字符 !"#$%&'()*+,-./ ,注意包括空格符),最后以一个最终字节(final byte)结束,最终字节范围是 0x40–0x7E(即字符 @A–Z[\]^_`a–z{|}~ )。这样即便有些终端不支持该转义序列,它也能从最终字节判断出转义序列在什么位置结束,从而不会影响正常的输出。

下面列举常见终端都支持的一些转义序列。

光标相关控制序列

现代终端都支持通过控制终端的光标来自由决定文本输出的位置。除了控制字符就带有的光标控制字符外,还有以下更强大的光标控制序列:

  • CSI "nA" :光标上移(Cursor Up)
  • CSI "nB" :光标下移(Cursor Down)
  • CSI "nC" :光标前移(Cursor Forward)
  • CSI "nD" :光标后移(Cursor Back)

将光标向指定位置移动 n 格,如果光标已在屏幕边缘,则指令无效。

  • CSI "nE" :光标行下移(Cursor Next Line),将光标移动到下 n 行的开头
  • CSI "nF" :光标行上移Cursor Previous Line),将光标移动到上 n 行的开头
  • CSI "nG" :光标水平移动(Cursor Horizontal Absolute),将光标移动到第 n
  • CSI "n;mH" :设置光标位置(Cursor Position):将光标移动到第 n 行第 m

注意:如果是绝对移动,那么第 1 行第 1 列相当于终端左上角的位置。另外,以上序列的参数 n 全部可以省略,省略的结果相当于取 1 。

CSI "nJ" 清除屏幕内容(Erase in Display),具体清除方式和传入的参数有关:

  • 如果 n0 或省略,清除屏幕位于光标以后的所有内容
  • 如果 n1 ,清除屏幕位于光标之前的所有内容
  • 如果 n2 ,清除屏幕所有内容
  • 如果 n3 ,清除屏幕包括之前输出的所有内容

CSI "nK" 清除行内容(Erase in Line),只影响本行内容,除此之外和清屏类似:

  • 如果 n0 或省略,清除本行位于光标以后的所有内容
  • 如果 n1 ,清除本行位于光标之前的所有内容
  • 如果 n2 ,清除本行所有内容

最后需要说明的是,这些清除指令都不改变光标位置。

下面列出的是和制表相关的控制序列:

  • CSI "nI" 制表命令(Cursor Horizontal/Forward Tab):将光标移到本行的下 n 个制表位,或者在没有制表位时移到最后一列,又或者已经在最后一列时移到下一行的起始位置。
  • CSI "nZ" 制表回退命令(Cursor Backwards Tab):将光标移到本行的上 n 个制表位,或者没有制表位时移到本行的开头。
  • CSI "0g" 清除制表位(Tab Clear):清除当前光标所处位置的制表位。
  • CSI "3g" 清除制表位(|Tab Clear),并且是清除当前存在的所有制表位。

最后是一些其它的控制序列:

  • CSI "nS" 向上滚动(Scroll Up):将所有内容向上滚动 n
  • CSI "nT" 向下滚动(Scroll Down):将所有内容向下滚动 n
  • CSI 's' 保存当前光标位置(Save Current Cursor Position)
  • CSI 'u' 恢复保存的光标位置(Restore Saved Cursor Position)

选择图形再现

在所有控制序列中,CSI "nm" 称为“选择图形再现(Select Graphic Rendition, SGR)”,用于设置与显示相关的属性,例如文本颜色、文本装饰、背景颜色等。根据参数 n 的不同,序列的作用也有所不同,下面介绍列举常用以及广泛支持的 SGR 参数:

0~9 范围内的参数可以设置文本的基本样式:

n作用
0将所有属性重设为默认值
1设置粗体
2显示变暗,一般来说终端会将文本颜色设置得更贴近背景颜色
3设置斜体,不过相当部分终端不支持
4为文本设置下划线
7反转文本和背景颜色
8隐藏输出,支持程度不佳
9为文本设置删除线

如果省略了参数 n ,相当于取默认值 0 ,即重设所有样式。

10~20 范围内的参数用于设置文本字体,现代的终端基本也不再支持了。21 ~ 29 范围内的参数用于取消 1~9 参数的效果:

n作用
21标准允许该参数可用于为文本设置双下划线,也可以用于取消 1 的效果
xterm 和 Windows Terminal 采用的是前一种实现
22取消 12 的效果
23取消 3 的效果
24取消 4 的效果
27取消 7 的效果
28取消 8 的效果
29取消 9 的效果

还有几个比较特殊的:

n作用
53为文本设置上划线
55取消文本的上划线

这里需要特别注意一下,一旦使用了 SGR 命令,它会彻底改变终端接下来所有文本显示的效果,甚至当前程序退出之后也是如此。因此最好在输出完成后立刻清除使用的样式,防止对后续输出造成干扰。

以上介绍的 SRG 相关属性,允许在一个 SRG 序列内组合多个属性,各个属性之间使用分号 ; 隔开。例如,如果想要同时得到粗体、斜体、下划线的文本,可以使用这样的转义序列:

print("\033[1;3;4mSample Text\033[0m")

控制显示颜色

在所有 SRG 序列内,有许多和颜色相关的参数。

n 取值为 30-37 时,可以为文本设置不同的颜色。这个范围一共只有 8 种颜色,因此称为 3 位颜色。不同参数对应的颜色如下:

30
31
32
33
34
35
36
37
black
red
green
yellow
blue
magenta
cyan
white

标准只给出了颜色名,具体显示的是什么样的颜色随终端不同而有所不同。维基百科给出了所有常见终端颜色名对应的颜色代码,感兴趣的可以进一步了解差异。

现代的终端还支持在 90-97 范围内设置相应颜色“更亮”的版本,这样可以得到 4 位颜色。不支持这些颜色的终端会采用和 30-37 相同的颜色。在 Windows Terminal 内,这些颜色的表现效果为:

40-47 参数范围内,可以设置背景色,它们和 30-37 所使用的颜色是一一对应的。同样地,可以在 100-107 范围内设置“更亮”颜色的背景颜色。例如,要指定黄色的文本以及蓝色的背景,可以使用以下转义序列:

print("\033[33;44mYellow Foreground & Blue Background\033[0m")

许多终端在上世纪 90 年代扩展了对 256 色(即所谓的 8 位颜色)的支持。SGR 序列的第 38 和 48 号参数用于扩展更丰富的颜色,可以使用 CSI "38;5;nm" 根据 n 来选择这 256 色,具体的颜色表可以参照下图:

  • 0-7 范围内是和 CSI 30–37 m 相同的标准色
  • 8-15 范围内是和 CSI 90–97 m 相同的“更亮”版颜色
  • 16-231 范围内是 6 × 6 × 6 的 RGB 颜色,共 216 种彩色
  • 232-255 是 24 级的灰度颜色

相应地,CSI "48;5;nm" 用于设置 8 位背景颜色。

现代的终端大多都支持 24 位 RGB 真彩色,可以使用 \x1b[38;2;r;g;bm 选择 RGB 前景色;相应地以使用 \x1b[48;2;r;g;bm 选择 RGB 背景色。真彩色的引入丰富了终端的视觉效果,使得现代终端也可以创建非常美观的文本界面。

参考资料/延伸阅读

C0 and C1 control codes - Wikipedia

ANSI escape code - Wikipedia

Control characters in ASCII and Unicode - aivosto.com

Programmer Information - Digital VT100 User Guide

XTerm Control Sequences - Invisible Island

Control Sequences (Screen User’s Manual) - GNU

Console Virtual Terminal Sequences - Windows Console | Microsoft Learn

ANSI Escape Codes - GitHub Gist

文本的所有内容使用任意编程语言都可以实现,这里给出了一个 C 语言实现的示例。

京ICP备2021034974号
contact me by hello@frozencandles.fun