Linux技巧:在代码中设置终端字符显示颜色和移动光标位置

本篇文章介绍如何在代码中使用 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 转义码清除光标所在的整行内容,输出内容为空。

你可能感兴趣的:(linux,bash,shell)