转自大神博客:
http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html

1. ANSI escape codes

ANSI转义序列被定义来让用户可以通过输出某些特殊字符来控制终端的行为。这包括改变终端的字体颜色、改变背景颜色以及改变光标位置等等。不同的终端对ANSI转义序列的支持程度不同,不过现代终端(包括XShell)基本上都能完美的支持大多数特性。下面开始用ANSI转义序列来打造你的炫酷应用吧!

2. 富文本

颜色

8色

现在先来尝试最基本的功能,改变颜色。改变颜色的关键字是形如下列的字符串:

// Red Text
"\x1B[31m"

// Reset all
"\x1B[0m"

"\x1B" 字符串是ANSI转义序列中非常特殊的一串控制字符(你也可以写成文章最上面的链接中的"\u001b"),大部分ANSI转义序列都以这串字符开头,而且大多数语言都支持用这种语法表示它是ANSI转义序列中的特殊字符,这意味着你可以用你最常用的语言来完成下面我用C语言完成的全部工作。

接下来就是用法,以输出红色的Hello World为例:

printf("\x1B[31mHello World\n");

效果:

如约打印出了红色的字符,上面的语法中 \x1B[31m 中的 [31m 表示告诉终端将输出字符的颜色改变为红色,则终端将从这串转义序列之后的所有字符都改变成了红色,而且从上图也能看出来,这个变化不是针对于当前的程序的,而是针对与当前的终端的,所以从今往后所有的字符都变成了红色(好吧这个黑配红真够丑的)。为了解决这种尴尬的局面就需要另一串转义序列来将终端的配置恢复成默认状态

printf("\x1B[31mHello World!\x1B[0m\n");

"\x[0m" 表示告诉终端从这串转义序列之后的所有设置恢复成默认,效果:

好的,基本用法介绍完了,现在列出最基本的8色的转义序列字符串:

  • 黑色:\x1B[30m
  • 红色:\x1B[31m
  • 绿色:\x1B[32m
  • 黄色:\x1B[33m
  • 蓝色:\x1B[34m
  • 品红:\x1B[35m
  • 蓝绿:\x1B[36m
  • 白色:\x1B[37m

上面的是控制字符颜色的,下面是控制背景颜色的8色:

  • 黑色:\x1B[40m
  • 红色:\x1B[41m
  • 绿色:\x1B[42m
  • 黄色:\x1B[43m
  • 蓝色:\x1B[44m
  • 品红:\x1B[45m
  • 蓝绿:\x1B[46m
  • 白色:\x1B[47m

下面程序打印出我们的所学:

#define RSET "\x1B[0m"

#define TBLK "\x1B[30m"
#define TRED "\x1B[31m"
#define TGRN "\x1B[32m"
#define TYEL "\x1B[33m"
#define TBLU "\x1B[34m"
#define TMAG "\x1B[35m"
#define TCYA "\x1B[36m"
#define TWHI "\x1B[37m"

#define BBLK "\x1B[40m"
#define BRED "\x1B[41m"
#define BGRN "\x1B[42m"
#define BYEL "\x1B[43m"
#define BBLU "\x1B[44m"
#define BMAG "\x1B[45m"
#define BCYA "\x1B[46m"
#define BWHI "\x1B[47m"

char* TArr[8] = {TBLK, TRED, TGRN, TYEL, TBLU, TMAG, TCYA, TWHI};
char* BArr[8] = {BBLK, BRED, BGRN, BYEL, BBLU, BMAG, BCYA, BWHI};

int main()
{
    for(int i = 0; i < 8; i++) {
        for(int j = 0; j < 8; j++) {
            printf("%s%sHello World\t%s", BArr[i], TArr[j], RSET);
        }
        printf("\n");
    }
    return 0;
}

效果:

16色

比8色更进一步,16色多出来的8色定义了比之前的8色更亮、更粗的字体,个人觉得这8种还是比较符合审美的,语法是在之前表示颜色的数字后面加上 ";1" 就ok了(同样可以改变文字和背景):

#define RSET "\x1B[0m"

#define TBLK "\x1B[30;1m"
#define TRED "\x1B[31;1m"
#define TGRN "\x1B[32;1m"
#define TYEL "\x1B[33;1m"
#define TBLU "\x1B[34;1m"
#define TMAG "\x1B[35;1m"
#define TCYA "\x1B[36;1m"
#define TWHI "\x1B[37;1m"

#define BBLK "\x1B[40;1m"
#define BRED "\x1B[41;1m"
#define BGRN "\x1B[42;1m"
#define BYEL "\x1B[43;1m"
#define BBLU "\x1B[44;1m"
#define BMAG "\x1B[45;1m"
#define BCYA "\x1B[46;1m"
#define BWHI "\x1B[47;1m"

char* TArr[8] = {TBLK, TRED, TGRN, TYEL, TBLU, TMAG, TCYA, TWHI};
char* BArr[8] = {BBLK, BRED, BGRN, BYEL, BBLU, BMAG, BCYA, BWHI};

int main()
{
    for(int i = 0; i < 8; i++) {
        for(int j = 0; j < 8; j++) {
            printf("%s%sHello World%s", TArr[i], BArr[j], RSET);
        }
        printf("\n");
    }

    return 0;
}

256色

终极版256色来了。语法:

"\x1B[38;5;${ID}m"  // Text
"\x1B[48;5;${ID}m"  // Background

将其中的 ${ID} 换成 0 ~ 255 中的任一整数来表示一个颜色:

int main()
{
    for(int i = 0; i < 16; i++) {
        for(int j = 0; j < 16; j++) {
            printf("\x1B[38;5;%dmHello World!\x1B[0m\t", i * 16 + j);
            if(j == 7 || j == 15) {
                printf("\n");
            }
        }
    }

    for(int i = 0; i < 16; i++) {
        for(int j = 0; j < 16; j++) {
            printf("\x1B[48;5;%dm%d\t\x1B[0m", i * 16 + j, i * 16 + j);
        }
        printf("\n");
    }
    return 0;
}

更多装饰符

加粗,下划线,反色都可以做到:

  • 加粗:\x1B[1m
  • 下划线:\x1B[4m
  • 反色:\x1B[7m
int main()
{
    printf("\x1B[1m BOLD \x1B[0m\x1B[4m Underline \x1B[0m\x1B[7m Reversed \x1B[0m\n");
    return 0;
}

3. 光标移动

基本用法

ANSI转义序列定义了比改变颜色更加复杂的东西:它定义了一组可以让你把光标移动到终端任意位置的指令。

其中最基本的当属上移、下移、左移、右移了:

  • 上移:\x1B[{n}A
  • 下移:\x1B[{n}B
  • 右移:\x1B[{n}C
  • 左移:\x1B[{n}D

可以用这一组转义序列所能完成的最基本的程序当属一个加载进度程序了:

int main()
{
    printf("Loading...\n");

    for(int i = 0; i <= 100; i++) {
        usleep(100000);
        printf("\x1B[1000D%d%%", i);
        fflush(stdout);
    }
    printf("\n");

    return 0;
}

可以看到数字只在同一行打印,而不会另起一行或者接在后面打印。因为 \x1B[1000D 表示将光标向左移动1000各单位,所以每次打印的时候光标都会移动到最左端从而覆盖上一次的打印(转义字符 \r 也能做到这点)。为了更加清晰的展示这种”移动“,思考下面的程序:

int main()
{
    printf("Loading...\n");

    for(int i = 0; i <= 100; i++) {
        usleep(1000000);
        printf("\x1B[1000D");
        fflush(stdout);
        usleep(1000000);
        printf("%d%%", i);
        fflush(stdout);
    }
    printf("\n");

    return 0;
}

进度条

运用上面所学的知识,首先来完成一个简单的进度条程序吧

#include <stdio.h>
#include <unistd.h>

#define KNRM  "\x1B[0m"
#define KGRN  "\x1B[32m"
#define BWHT  "\x1B[47m"

int main()
{
    int i = 0;
    char bar[101] = { 0 };
    const char* label = "|/-\\";

    while(i <= 100) {
        printf("\x1B[1000D%s%s[%-100s][%d%%][%c]%s", KGRN, BWHT, bar, i, label[i % 4], KNRM);
        fflush(stdout);
        bar[i++] = '#';
        usleep(100000);
    }
    printf("\n");

    return 0;
}

每打印完一行,就把光标左移1000个单位,这样下一次打印的时候就会重新覆盖当前行,则有了单行进度条的效果。

多行进度条

应该很多人知道,C语言中的转义字符 '\r' 回车符和上面的光标左移功能差不多,而上面的程序中替换成 '\r' 貌似也能达到预期的效果,那么现在能干什么更加不可替代的工作呢?当然,综合运用光标左移和上移,完成一个多行进度条程序!

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

void draw(int* arr, int size) {  // print process bars
    for(int i = 0; i < size; i++) {
        printf("[");
        int j;
        for(j = 0; j < arr[i]; j++) {
            printf("#");
        }
        for(; j < 50; j++) {
            printf(" ");
        }
        printf("]%d%%\n", arr[i] * 2);
    }

}

int main(int argc, char* argv[])
{
    if(argc == 1) {
        exit(1);
    }
    
    srand((unsigned int)time(NULL));

    int count = atoi(argv[1]);  // 总共需要多少个进度条 
    int total = 0;  // 记录是否所有进度条都走完了
    int arr[count];  // 用count个int记录每个进度条走了多少

    // initialize process bars
    for(int i = 0; i < count; i++) {
        arr[i] = 0;
    }

    draw(arr, count);
    usleep(50000);

    while(total != 50 * count) {
        // move up count rows
        printf("\x1B[%dA", count);
        fflush(stdout);

        // pick a random process bar
        int pick;
        while(1) {
            pick = rand() % count;
            if(arr[pick] != 50) {  // if picked bar is not full
                arr[pick]++;
                total++;
                break;
            }
        }
        draw(arr, count);

        usleep(50000);
    }

    return 0;
}

每次打印完所有进度条后,将光标上移到第一行进度条,以便下一次打印覆盖当前的进度条,效果如下:

最后修改:2020 年 03 月 26 日
如果觉得我的文章对你有用,请随意赞赏