本篇文章介绍如何在代码中使用 ANSI 转义码来设置终端的字符显示颜色、移动光标位置等,并实现一个进度条百分比跳变的效果。
ANSI 转义码
在 Linux 中,可以使用 ANSI 转义码(ANSI escape codes)设置终端的字符显示颜色、移动光标位置、清除字符显示等。
ANSI 转义码是由终端自身支持,独立于编程语言之外,可以在 C 语言、Java、Python、或者 Shell 中使用。
下面以 bash shell 为例来说明如何使用 ANSI 转义码。
ANSI 转义码格式
ANSI 转义码由一串 ASCII 编码的字符串组成,要求以 ASCII 编码的 Escape 字符和 [ 字符开头,后面跟着具体的转义码,指定相应的操作。基本格式如下:
Esc[escape code
Escape 字符也就是 Esc 键对应的字符。
由于按 Esc 键,不会得到一个可显示的字符,需要用具体的编码值来表示这个字符。
在不同编程语言中,表示字符编码值的写法可能不一样。一般常用 \e
转义字符来表示 Esc 字符。
使用 echo 命令测试 ANSI 转义码
在 bash shell 中,可以使用 echo
命令的 -e
选项来测试 ANSI 转义码。
查看 man echo 对 -e
选项说明如下:
-e
enable interpretation of backslash escapes.
If -e is in effect, the following sequences are recognized:
e
escape
0NNN
byte with octal value NNN (1 to 3 digits).
即,在 echo
命令中,-e
选项可以指定处理转义字符。
\e
转义字符表示 escape 字符。
\0NNN
转义字符使用八进制来获取 NNN 编码值对应的字符。
在 ASCII 编码中,Escape 字符对应的八进制值是 033。
则在 echo
命令中,\033
表示 escape 字符。
使用 echo
命令测试 ANSI 转义码时,可以写为 echo -e "\033[31m"
。
这里的 31m 转义码表示要把终端字符的前景色设成红色。
Linux 的 printf 命令也可以输出 ANSI 转义码,而且不需要加 -e
选项,例如写为 `printf "e[31m"。
注意:这里需要用双引号、或者单引号把 033[31m 括起来,避免 bash 自身对 \
进行转义,会去掉 \
字符,导致 echo
命令收不到 \
字符,无法处理转义字符。
也可以写为 echo -e "\e[31m"
,\e
也表示 escape 字符。
后面测试的时候,统一使用 \e
的形式,少输入一些字符。
具体测试如下:
$ echo -e "e[31m"$ echo -e "e[0m"
$
执行 echo -e "\e[31m"
命令后,终端的提示字符会变成红色,之后输入的字符也都会变成红色。
即,终端的默认字符颜色变成了红色。
执行 echo -e "\e[0m"
命令重置终端属性,让终端的字符颜色变成原来的默认颜色。
这里的 0m 转义码表示重置字符显示属性。
一般来说,为了不影响终端自身的显示,使用 ANSI 转义码设置某个字符串的显示颜色后,建议随后使用 0m 转义码来重置为原来的颜色。
举例说明如下:
$ echo -e "e[31mThis is a red string.e[0m"
This is a red string.
$
在上面命令中,\e[31m
是一个 ANSI 转义码,表示设置终端字符颜色为红色。
\e[0m
也是一个 ANSI 转义码,表示重置终端的颜色属性,会恢复成原来的颜色。
在这两个转义码中间的字符串会显示在终端上。
执行该命令后,终端的提示符会显示为原来的颜色。
设置终端字符颜色的 ANSI 转义码
下面详细说明设置终端字符颜色的 ANSI 转义码,其基本格式如下:
Esc[Value;...;Valuem
这里的 Value 可以提供多个值,不同值之间用分号 ‘;’ 隔开。
这些值可以分别指定字符的前景色、背景色、字符属性(粗体、下划线、反转)。它们之间的顺序不限。
转义码最后以 m
字符结尾。
设置字符前景色的值如下:
颜色值 | 颜色 |
---|---|
30 | 黑色 |
31 | 红色 |
32 | 绿色 |
33 | 黄色 |
34 | 蓝色 |
35 | 紫色 |
36 | 青色 |
37 | 白色 |
设置字符背景色的值如下:
颜色值 | 颜色 |
---|---|
40 | 黑色 |
41 | 红色 |
42 | 绿色 |
43 | 黄色 |
44 | 蓝色 |
45 | 紫色 |
46 | 青色 |
47 | 白色 |
设置字符属性的值如下:
属性值 | 属性含义 |
---|---|
0 | 重置所有属性,包含字符颜色 |
1 | 设成粗体 |
4 | 添加下划线 |
5 | 打开闪烁 |
7 | 颜色反转 |
8 | 显示不可见的文本 |
具体举例如下:
$ echo -e "e[31;44mFg color: Red. Bg color: Blue.e[0m"
Fg color: Red. Bg color: Blue.
$ echo -e "e[44;31mFg color: Red. Bg color: Blue.e[0m"
Fg color: Red. Bg color: Blue.
可以看到,\e[31;44m
、\e[44;31m
这两个转义码设置的字符颜色效果是一样的。
所给的前景色、背景色没有要求先后顺序。
目前的大部分终端都支持 256 色,可以使用 Esc[38;5;Valuem
来设置终端字符为 256 色。
这里的 Value 取值是 0-255。
例如,echo -e "\e[38;5;111mAAAAAA\e[0m"
命令设置为 111 对应的颜色。
具体的颜色取值可以查看 256 色的颜色表。网上的很多文章都有说明。这里不再列举。
使用 ANSI 转义码移动终端光标
ANSI 转义码可以用来移动终端的光标位置,从而改变字符的输出位置。
具体举例如下:
$ echo -e "123456789\e[4Dabc"
12345abc9
在这个命令中,\e[4D
转义码表示把光标往左移动 4 列。
可以看到,光标移动 4 列后,位于字符 6 所在的位置,重新输出 abc,覆盖了原来的 678 三个字符。
移动光标的具体转义码说明如下:
转义码 | 含义 |
---|---|
Esc[nA | 光标上移 n 行,列数不变。移动到终端最上边后不再移动 |
Esc[nB | 光标下移 n 行,列数不变。移动到终端最下边后不再移动 |
Esc[nC | 光标右移 n 列,行数不变。移动到终端最右边后不再移动 |
Esc[nD | 光标左移 n 列,行数不变。移动到终端最左边后不再移动 |
Esc[nE | 光标下移 n 行,列数变到行首 |
Esc[nF | 光标上移 n 行,列数变到行首 |
Esc[Line;ColumnH | 把光标移动到指定的行数和列数。如果不提供值,默认值为 0 |
Esc[ColumnG | 把光标移动到第 Column 列,当前行数保持不变 |
Esc[s | 保存当前光标位置,后续可以用 Esc[u 跳到保存的位置 |
Esc[u | 跳转到 Esc[s 所保存的光标位置 |
Esc[?25l | 隐藏光标(在 25 后面是小写字母 l) |
Esc[?25h | 显示光标 |
上面所说的终端位置指的是终端可见的窗口位置,不包括缓冲区位置。
即,窗口显示不会发生滚动,只在当前可见的窗口区域跳转光标。
注意:由于 echo
命令默认会输出换行符,导致移动光标后再次换行,会对光标移动效果造成干扰。
在测试移动光标的转义码时,建议用 printf
命令测试。该命令默认不会输出换行符。
由于 bash 里面需要按下回车才执行命令,会影响光标的左右移动效果,建议在 printf
自身输出的内容中左右移动光标。
实际测试发现,光标右移 n 列,光标会位于第 n 列的后面,之后输出的字符串会从 n+1 列开始。
Esc[C、Esc[0C、和 Esc[1C 的效果相同,都是光标右移 1 列。
类似的,Esc[D、Esc[0D、和 Esc[1D 的效果相同,都是光标左移 1 列。
使用 printf
命令测试如下:
$ printf "123456789\e[1Da\n"
12345678a
$ printf "123456789\e[0Da\n"
12345678a
$ printf "123456789\e[Da\n"
12345678a
可以看到,使用 \e[D
、\e[0D
、\e[1D
往左移动光标,然后输出字符 a,都是覆盖同一个字符 9。
这三个转义码的光标移动效果相同。
$ printf "123456789\e[4Da\n"
12345a789
$ printf "123456789\e[4D\e[Ca\n"
123456a89
$ printf "123456789\e[4D\e[0Ca\n"
123456a89
$ printf "123456789\e[4D\e[1Ca\n"
123456a89
\e[4D
把光标左移 4 列,移动到字符 6 的位置。
\e[C
、\e[0C
、\e[1C
都是往右移动光标到下一列,到字符 7 的位置,输出字符 a,覆盖了字符 7。
通过移动光标实现进度百分比的效果
我们可以通过移动光标实现进度百分比的效果。假设有一个 progress.sh
脚本,内容如下:
#!/bin/bash
for ((i = 0; i <= 100; ++i)); do
printf "\e[5D%3d%%" $i
sleep 0.1s
done
echo
这里使用 printf
命令进行输出,以便格式化字符串。
printf
命令也是使用 \e
来表示 escape 字符。
\e[5D
转义码表示把光标左移 5 列。
由于所输出的字符不超过 5 个字符,每次光标左移 5 列,都会移动到最左边,从第一列开始输出。
那么后面输出的内容会覆盖前面输出的内容,达到在同一行重复输出的效果。
sleep 0.1s
命令表示暂停 0.1 秒。添加这个语句,以便清楚地看到进度百分比跳变。否则执行过快,百分比很快就跳到 100%。
执行 progress.sh
脚本的结果如下:
$ ./progress.sh
100%
这里不是动图,看不到进度百分比跳变。实际执行就能看到。
从结果来看,在 for 循环中多次打印信息,这些信息都打印在同一行,并覆盖前面的输出。而不是换行打印。
通过移动光标实现进度条的效果
下面通过移动光标实现进度条的效果。假设有一个 progressbar.sh
脚本,内容如下:
#!/bin/bash
function print_chars()
{
# 传入的第一个参数指定要打印的字符串
local char="$1"
# 传入的第二个参数指定要打印多少次指定的字符串
local number="$2"
local c
for ((c = 0; c < number; ++c)); do
printf "$char"
done
}
declare -i end=50
for ((i = 1; i <= end; ++i)); do
printf "\e[80D["
print_chars "#" $i
print_chars " " $((end - i))
printf "] %3d%%" $((i * 2))
sleep 0.1s
done
echo
这个脚本定义了一个 print_chars 函数,可以多次打印同一个字符。
printf "\e[80D["
语句把光标左移 80 列。由于这个进度条的字符总长度小于 80,会移动最左边,总是从第一列开始输出。
在 ‘80D’ 后面的 ‘[’ 字符是进度条的开头第一个字符。
print_chars "#" $i
语句递增打印多个 # 字符,形成进度条往前移动的效果。
print_chars " " $((end - i))
语句打印多个空格,填充到指定的最后一列,让进度条的结束字符总是打印在同一列。
printf "] %3d%%" $((i * 2))
语句打印进度条的结束字符 ]、以及进度条百分比。
sleep 0.1s
语句暂停 0.1 秒,避免执行过快,看不到进度条的移动效果。
执行 progressbar.sh
脚本的结果如下:
$ ./progressbar.sh
[##################################################] 100%
这里不是动图,看不到进度百分比跳变。实际执行就能看到。
使用 ANSI 转义码清屏、清除字符
下面的 ANSI 转义码可以用于清屏、清除光标往后的字符。
转义码 | 含义 |
---|---|
Esc[2J | 清除屏幕显示的内容。在 Ubuntu 上测试,光标位置会保持不变 |
Esc[K | 清除从光标位置到行尾的所有字符(包括光标下的字符) |
Esc[1K | 清除从光标位置到行首的所有字符(包括光标下的字符) |
Esc[2K | 清除光标所在的整行内容 |
注意:上面的 J、K 都是大写字母。
具体举例说明如下:
$ printf "123456789\e[5D\e[K\n"
1234
$ printf "123456789\e[5D\e[1K\n"
6789
$ printf "123456789\e[5D\e[2K\n"
printf "123456789\e[5D\e[K\n"
命令先光标左移 5 列,停在字符 5 的位置,然后用 \e[K
转义码从光标位置往后清除所有字符,只保留了前面的 1234 字符串。
printf "123456789\e[5D\e[1K\n"
命令使用 \e[1K
转义码从光标位置往前清除所有字符,只保留了后面的 6789 字符串。
printf "123456789\e[5D\e[2K\n"
命令使用 \e[2K
转义码清除光标所在的整行内容,输出内容为空。