写在前面
Virtual terminal sequences are control character sequences that can control cursor movement, console color, and other operations when written to the output stream.
—— Microsoft Learn
简介
ANSI 转义序列(ANSI Escape Sequences),也被微软称作虚拟终端序列(Virtual Terminal Sequences,也作 VT 序列),还被一些人称作 VT100 终端控制码/VT100 控制码/终端控制码。
正如上文引用所言,这一类序列用于控制视频文本终端(Video Text Terminals)及终端模拟器(Terminal Emulators)上的光标位置及颜色等。终端应该把这些序列解释为指令,而不是解释为普通的字符编码/输出它们。绝对没有内涵某些微软的老“终端模拟器”。
历史
以下历史主要整理自维基百科:
https://zh.wikipedia.org/wiki/ANSI转义序列,
https://en.wikipedia.org/wiki/ANSI_escape_code。
相关标准
最初,几乎每个视频终端制造商都各自添加了特定的转义序列用于执行一些特殊操作,比如把光标置于屏幕上的某一个位置。
由于这些序列各自为政,很多序列还要求借助字符的二进制发送数字(而某些编程语言做到这些是有困难甚至做不到的);因此人们曾开发一些复杂的库(例如 termcap (terminal capabilities))和一些实用工具(例如 tput),这样可以用同一套 API 应对各种终端序列。(条件编译、交叉编译的苦,我已经尝到了……)
ANSI 标准试图解决这个问题。标准制订了一种所有终端共享的指令集,并要求用 ASCII 的数字字符传递所有数值信息。
1976 年,该系列的第一个标准 ECMA-48 发布。
1979 年,ANSI X3.64 标准(这里的 ANSI 是指 American National Standards Institute,美国国家标准学会)发布,ANSI 转义序列就是出自这里。
1983 年,以上两个标准合并为 ISO 6429(最新修改的标准为 1992 年的 ISO/IEC 6429;中国国家标准馆页面)。
1994 年,ANSI 取消了其标准,以支持国际标准。
终端普及
第一个支持 ANSI X3.64 标准的流行视频终端是 1978 年推出的 Digital VT100,它在市场上非常成功,引发了各种各样的仿制品。(这大概就是有些人称“ANSI 转义序列”为“VT100 控制码”的原因)
此后,许多其他品牌的视频终端都兼容可选 VT 100, VT 103 或 ANSI 模式。
越来越多的软件依靠 ANSI 转义序列起作用,几乎所有新的终端和终端模拟器都支持了该标准。
类 Unix 系统中,到 20 世纪 80 年代中期,ANSI 转义序列几乎得到了全平台支持。
尽管许多操作系统在标准文本输出中越来越多地支持 ANSI,但大多数情况下 ANSI 支持是由终端模拟器完成的(例如 xterm, GNOME Terminal, Konsole)。
2016 年前的 Windows 中(Windows 10 Threshold 2, 即 Windows 10 1511 之前),Win32 控制台(conhost.exe)完全不支持 ANSI 转义序列。(先进的微软大概晚了 40 年才跟上标准吧。)
2019 年,微软推出了 Windows Terminal,并且正在用其替换掉 Win32 控制台。事实是,老电脑用不了 WT,而老电脑比比皆是。
Windows 下附赠的准备条件
我再也不用 Windows 了,所以可以跳过这段内容不写。
正如上面的历史所言,Windows 的 conhost 对于 ANSI 转义序列支持很晚。
除此之外,想要启用 conhost 中的该功能和正常输出中文,还要有一定手段。
因此,我们暂且按下“怎么使用 ANSI 转义序列”不表,先来看怎么
例如以下代码(UTF-8 编码保存,这是现代编辑器【Dev-C++ 不算,它只能以 GB 2312 的编码保存】的惯例):
#include <cstdio>
using namespace std;
int main()
{
printf("\033[1;31mred fg 红色 前景\n");
return 0;
}
编译后直接放到比较新的 Windows 10 的 conhost 里运行,你会惊喜(xia)地发现输出了以下东西:
□[1;31mred fg 绾㈣壊 鍓嶆櫙
原因有下:
- 中文系统中,conhost 默认的代码页是 936(简体中文 - GBK),而现代的代码通常以 UTF-8 保存,输出时自然有乱码;
- conhost 默认没有启用“虚拟终端序列”的功能,你只有用 Console API 手动启用,才能正常使用 ANSI 转义序列。
根据 官方示例,加上切换代码页,我曾写了以下函数(你肯定要加上 windows.h 头文件):
函数
/**
* @brief 在 Windows 下启用虚拟终端序列。
* @return `true` 代表成功,`false` 代表失败。
*
* @details
* 输出模式设定 `ENABLE_VIRTUAL_TERMINAL_PROCESSING` `DISABLE_NEWLINE_AUTO_RETURN`;
* 输入模式设定 `ENABLE_VIRTUAL_TERMINAL_PROCESSING`;
* 还设置代码页为 `65001`(`CP_UTF8`)。
*
* @see https://learn.microsoft.com/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
* @see https://learn.microsoft.com/windows/console/classic-vs-vt
*/
bool win_vt_setup() {
// 设置输入、输出代码页为 65001
SetConsoleCP(CP_UTF8); // 65001
SetConsoleOutputCP(CP_UTF8);
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 输出句柄
if (hOut == INVALID_HANDLE_VALUE)
{
return false;
}
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); // 输入句柄
if (hIn == INVALID_HANDLE_VALUE)
{
return false;
}
DWORD dwOriginalOutMode = 0;
DWORD dwOriginalInMode = 0;
if (!GetConsoleMode(hOut, &dwOriginalOutMode))
{
return false;
}
if (!GetConsoleMode(hIn, &dwOriginalInMode))
{
return false;
}
DWORD dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
DWORD dwRequestedInModes = ENABLE_VIRTUAL_TERMINAL_INPUT;
DWORD dwOutMode = dwOriginalOutMode | dwRequestedOutModes; // 合并原有终端模式与新的终端模式
if (!SetConsoleMode(hOut, dwOutMode))
{
// we failed to set both modes, try to step down mode gracefully.
dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
dwOutMode = dwOriginalOutMode | dwRequestedOutModes;
if (!SetConsoleMode(hOut, dwOutMode))
{
// Failed to set any VT mode, can't do anything here.
return false;
}
}
DWORD dwInMode = dwOriginalInMode | dwRequestedInModes;
if (!SetConsoleMode(hIn, dwInMode))
{
// Failed to set VT input mode, can't do anything here.
return false;
}
return true;
}当然啦,如果你使用 Windows Terminal 或 Visual Studio Code 自带的 Xterm.js 或 ConEmu,就不会有类似的问题了。
所以你为什么不去下载现代的终端(模拟器)?
正常输出(为了还原 VSC 里的 xterm 终端,粗体均使用亮色):
red fg 红色 前景
C0 控制码(也就是定义在 ASCII 里的特殊字符)
这些都是单个字节(单个字符),最开始定义在 ASCII 中。
事实上,这些单个控制字符(对于普通文本来说)除了换行符、制表符(\r,\n,\t),并不常用。
也就是说,你通常不会用这单个字符来达到控制终端的目的,而是达到输出文本的目的。
以下是较常用(不常用中的较常用)的 C0 控制码。
| 名称 | 缩写 | OCT | HEX | 转义字符 | ^(脱出字符) | 描述(在终端中的效果) |
|---|---|---|---|---|---|---|
| Bell | BEL | 007 | 0x07 | \a | ^G | 终端响铃。 |
| BackSpace | BS | 010 | 0x08 | \b | ^H | 退格,将光标左移一格。 |
| Horizontal Tab | HT | 011 | 0x09 | \t | ^I | 将光标向右移一个制表符的长度。 |
| Line Feed | LF | 012 | 0x0A | \n | ^J | 移动到下一行。如果已在最后一行,则滚动显示。 |
| Form Feed | FF | 014 | 0x0C | \f | ^L | 换页。 |
| Carriage Return | CR | 015 | 0x0D | \r | ^M | (回车)将光标移动到第零列。 |
| Escape | ESC | 033 | 0x1B | \e | ^[ | 所有转义字符的开始。 |
通常而言,在某种语言中,你可以用转义字符(即反斜杠 \)加上 0+数(八进制)/x+数(十六进制)来通过其对应的 ASCII 码位输出。
例如 ESC,可用 \033 或 \x1b 来输出。
基于本人习惯,以下通常使用八进制表示法。
ANSI 转义序列
ESC 之后的第二个字节接上 @A–Z[\]^_ 的字符,即可变成 ANSI 转义序列。
这里只列举两个通常会用到的。
\033[(ESC [) 控制序列导入器(Control Sequence Introducer),简称CSI。\033](ESC ]) 操作系统命令(Operating System Command),简称OSC。
以下的内容,尖括号(< >)中的内容表示是某种参数。
当然,这种参数的名字是我自己起的。
CSI 序列
CSI 序列应该是人们最常用,也最通用的。
组成形式为 \033[<params><operation>。
其中:
<operation>是指令,为一个任意字母,大小写敏感。<params>是该指令的参数集,由若干数字和分号组成。
以分号分隔每一个参数。若没有给定数字(对应的参数),默认为0。
例如,\033[m等同于\033[0m;\033[;3H等同于\033[0;3H。
光标
指令码有 A~H, n, s, u。
| 序列 | 名称 | 说明 |
|---|---|---|
\033[<n>A |
Cursor Up (CUU) —— 光标上移 | 光标向指定方向移动 <n>。若已到屏幕边缘,则无效。 |
\033[<n>B |
Cursor Down (CUD) —— 光标下移 | |
\033[<n>C |
Cursor Forward (CUF) —— 光标右移 | |
\033[<n>D |
Cursor Back (CUB) —— 光标左移 | |
\033[<n>E |
Cursor Next Line (CNL) —— 光标到下一行 | 光标向对应方向移 <n> 行,并回到开头。若已到屏幕边缘,则无效。 |
\033[<n>F |
Cursor Previous Line (CPL) —— 光标到上一行 | |
\033[<col>G |
Cursor Horizontal Absolute (CHA) —— 光标水平绝对定位 | 光标移到第 <ln> 行(CHA 就是当前行),第 <col> 列。行列从 1 开始。 |
\033[<ln>;<col>H |
Cursor Position (CUP) —— 光标定位 | |
\033[s |
Save Cursor Position (SCP) —— 保存光标位置 | 没有存进栈,因此只能存一个位置,恢复后“储存的”也是原来的位置。 |
\033[u |
Restore Cursor Position (RCP) —— 恢复光标位置 | |
\033[6n |
Device Status Report (DSR) —— 设备状态报告 | 以 \033[<ln>;<col>R 的形式报告光标位置。(这个会重定向到到标准输入[stdin],就像在键盘上输入) 建议去看看项目 rich.hpp 的 cursor_coord() 函数。 |
清除
指令码有 J, K。
\033[<n>JErase in Display(ED) —— 擦除显示;\033[<n>KErase in Line (EL) —— 擦除行。
参数效果如下。
| 序列 | 说明 |
|---|---|
\033[0J | 从光标处开始清除,直到屏尾。 |
\033[1J | 从屏首处开始清除,直到光标。 |
\033[2J | 清屏。 |
\033[0K | 从光标处开始清除,直到行尾。 |
\033[1K | 从行首处开始清除,直到光标。 |
\033[2K | 清除光标所在的行的内容。 |
颜色与样式(选择图形再现)
如果你想控制文本颜色样式,就会用到选择图形再现(Select Graphic Rendition, 缩写为 SGR)。
指令字符为 m。
可以有多个参数,效果会叠加。(例如 \033[1;3m 会同时启用粗体和斜体)
参数规律总结
- —— 重置所有 SGR 属性;
- —— 多种图形样式;
- —— 重置对应的图形样式;
- —— 多种前景色;
- —— 对应背景色;
- —— 对应亮色版本前景色;(非标准,但大多数支持)
- —— 对应亮色版本背景色。(非标准,但大多数支持)
特殊
| 序列 | 说明 |
|---|---|
\033[0m | 重置所有 SGR 属性。 |
\033[21m | 双下划线(有些未支持)。若要关闭粗体,请用 \033[22m。 |
\033[39m | 默认前景色。 |
\033[49m | 默认背景色。 |
\033[53m | 上划线(不常用,有些未支持)。 |
图形模式/样式
用 2<对应的图形样式码> 作为参数可以消除对应的图形样式(粗体例外,只能用 22)。
例如 \033[23m 可以清除斜体。
正常文字
\033[1m 粗体 有些终端可能为亮色效果,或粗体+亮色效果
\033[2m 虚化(黯淡)效果
\033[3m 斜体
\033[4m 下划线
\033[5m 闪烁(**部分未支持,这里就演示不出**)
\033[7m 反显(交换前景色、背景色)
\033[8m 隐形(**这里无法演示**)
\033[9m 删除线
颜色
8 色(3/4 位)
有一个色盘,数字对应相应的颜色。
| 黑 | 红 | 绿 | 黄 | 蓝 | 品红(MAGENTA) | 青 | 白 |
指定上方的 为 color 参数,可以组合而成的 SGR 参数有:
3<color>前景色4<color>背景色9<color>前景亮色(非标准,但大多数支持)10<color>背景亮色(非标准,但大多数支持)
黑色 \033[30m:前景 \033[40m:背景 \033[90m:前景亮色 \033[100m:背景亮色
红色 \033[31m:前景 \033[41m:背景 \033[91m:前景亮色 \033[101m:背景亮色
绿色 \033[32m:前景 \033[42m:背景 \033[92m:前景亮色 \033[102m:背景亮色
黄色 \033[33m:前景 \033[43m:背景 \033[93m:前景亮色 \033[103m:背景亮色
蓝色 \033[34m:前景 \033[44m:背景 \033[94m:前景亮色 \033[104m:背景亮色
品红 \033[35m:前景 \033[45m:背景 \033[95m:前景亮色 \033[105m:背景亮色
青色 \033[36m:前景 \033[46m:背景 \033[96m:前景亮色 \033[106m:背景亮色
白色 \033[37m:前景 \033[47m:背景 \033[97m:前景亮色 \033[107m:背景亮色
上述输出可通过以下脚本得到
#! /usr/bin/bash
COLORS=("黑色" "红色" "绿色" "黄色" "蓝色" "品红" "青色" "白色")
CONTROLS=([3]="前景" [4]="背景" [9]="前景亮色" [10]="背景亮色")
for color_id in "${!COLORS[@]}"; do
for control_id in "${!CONTROLS[@]}"; do
escape_seq="\033[${control_id}${color_id}m"
escape_seq_output="\\\\033[${control_id}${color_id}m"
if [ $control_id -eq 3 ]; then
echo -ne "\033[1m${COLORS[$color_id]}\033[0m"
fi
echo -ne " ${escape_seq_output}:${escape_seq}${CONTROLS[$control_id]}\033[0m"
done
echo
done256 色(8 位)
前景色格式为 \033[38;5;<id>m;背景色格式为 \033[48;5;<id>m。
其中,<id> 是 256 色表中的一个,值为 0~255。
规律:
- 标准色,同
\033[<30~37>m; - 标准色亮色版本,同
\033[<90~97>m; - 立方(216 色),;
232~255从黑到白的 24 阶灰度色。
0 1 2 3 4 5 6 7
8 9 10 11 12 13 14 15
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
生成色盘的屎山脚本
#! /usr/bin/bash
SEQ_PREFIX="\033[48;5;"
SEQ_SUFFIX="m"
for (( i=0; i<=15; i+=1 )); do
addition=""
if (( i == 3 || i >= 7 )); then addition="\033[30m"; fi # 变成黑色文字
printf "${SEQ_PREFIX}${i}${SEQ_SUFFIX}${addition}%4d\033[0m" $i
if (( (i + 1) % 8 == 0 )); then echo; fi
done
echo
for (( i=16; i<=231; i+=1 )); do
addition=""
if (( (i >= 34 && i <= 51) || (i >= 70 && i <= 87) || (i >= 100 && i <= 123) || (i >= 136 && i <= 159) || (i >= 136 && i <= 155) || (i >= 166 && i <= 195) || (i >= 202 && i <= 231) )); then
addition="\033[30m"
fi
printf "${SEQ_PREFIX}${i}${SEQ_SUFFIX}${addition}%4d\033[0m" $i
if (( (i - 15) % 36 == 0 )); then echo; fi
done
echo
for (( i=232; i<=255; i+=1 )); do
addition=""
if (( i >= 244 )); then addition="\033[30m"; fi
printf "${SEQ_PREFIX}${i}${SEQ_SUFFIX}${addition}%4d\033[0m" $i
done
echo真彩色(truecolor,24 位,16777216 色)
我认为这个相比 256 色更实用。现代终端(模拟器)基本上均支持它。
前景色格式为 \033[38;2;<r>;<g>;<b>m;背景色格式为 \033[48;2;<r>;<g>;<b>m。
其中,<r>, <g>, <b> 值为 0~255。
例如:
\033[38;2;241;137;19m
\033[38;2;106;153;85m
\033[48;2;181;206;168m
OSC 序列
大部分 OSC 序列由 Xterm 首先定义,但是几乎所有终端模拟器都支持它们。
以下介绍两个常用的。
- 设置标题:格式为
\033]0;<title>\007。<title>即为你要设置的标题。 - 超链接:格式为
\033]8;;<uri>\007<text>\033]8;;\007。
<uri>为链接,<text>为显示的文字。
通常显示为带虚线下划线的文字形式;通常按住 Ctrl 再单击才能访问链接。
项目
我为同学写过一个初中毕业礼物,写了处理输出样式的头文件(utils)。
这就是用 ANSI 转义序列堆出来的。
Here rich.hpp.
小学毕业时,我也写过类似的头文件,但是使用 Windows Console API。
没有写小学毕业礼物,只写了这个头文件,也没送出去。
我甚至把这个头文件发到洛谷上了。
由于历史部分主要修改、整理自 Wikipedia,百科使用 CC BY-SA 4.0 许可,要求使用“相同方式共享”。
因此,本文许可为 CC BY-SA 4.0。