Linux终端调整输出色彩和光标位置的简单示例

本文详细介绍了如何在Linux终端使用ANSI颜色控制序列实现文本的彩色输出、光标位置调整以及屏幕清屏等功能。通过示例代码演示了在Raspberry Pi系统中如何在一行中循环输出特定字符,形成视觉上的旋转效果。文章还解释了选择使用stderr而非stdout的原因,以及ANSI序列在不同环境下的应用和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

相关的知识

实现的原理并不复杂,借助了Linux的 ANSI 颜色控制序列,只要在打印输出的过程中使用这些序列,就能做到彩色的字符、背景色输出,以及控制光标在打印时的位置。

简单的代码示例

以下代码都在树莓派的Raspbian系统中编译运行过。

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

int main(void)
{
    // str 字符串里包含了调整字体为红色以及光标隐藏的序列
    char str[] = "\033[31mplease wait...\033[?25l";
    char ch[3][5] = {"\\","-","/"};
    int  times = 20;

    fputs(str, stderr);
    while (times --)
    {
        fputs(ch[0], stderr);
        // 由于上面打印的字符串仅仅只占一个字符宽
        // 那么这里就只用将光标向左移动1格即可
        fputs("\033[1D", stderr);
        // 暂停 0.2 秒
        usleep(200000);
        // 调整好光标的位置后,直接打印内容,就能覆盖
        // 屏幕上之前的输出文本
        fputs(ch[1], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);
        fputs(ch[2], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);

    }
    // 这里建议打印一个换行,并且让光标否则程序运行结束之后,
    // 退回到shell,shell的提示符会紧跟着“please wait”字符串
    // 显示出来。
    // 此外,如果不让隐藏的光标显现,回到shell中后光标依然是
    // 隐藏状态,但此时直接按下回车键,键入空命令,在下一
    // 行命令中光标又会自动显现。
    fputs("\n\033[?25h", stderr);

    return 0;
}

上面代码的运行效果,是想在同一行行末的固定一个字符的位置,依次输出“\”“-”“/”并快速循环,给人以旋转的感觉。如果有读者在Kali Linux中启动“msfconsole”,则会看到程序启动中,在某一行提示信息的行末就使用了这个效果。

输出效果示意:

please wait...\
    ↓
please wait...-
    ↓
please wait.../

如果我们是用 fputc 函数打印单个字符,那么这光标位置调整仍然有效:

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

int main(void)
{
    char str[] = "\033[31mplease wait...\033[?25l";
    char ch[3] = {'\\', '-', '/'};
    int  times = 20;

    fputs(str, stderr);
    while (times --)
    {
        fputc(ch[0], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);
        fputc(ch[1], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);
        fputc(ch[2], stderr);
        fputs("\033[1D", stderr);
        usleep(200000);  
    }
    fputs("\n\033[?25h", stderr);

    return 0;
}
ANSI 色彩控制序列的简介
\033[0m 关闭所有属性
\033[1m 设置高亮度
\033[4m 下划线
\033[5m 闪烁
\033[7m 反显
\033[8m 消隐
\033[30m -- \33[37m 设置前景色
\033[40m -- \33[47m 设置背景色
字背景颜色:40----------49
40:黑
41:深红
42:绿
43:黄色
44:蓝色
45:紫色
46:深绿
47:白色
字颜色:30-----------39
30:黑
31:红
32:绿
33:黄
34:蓝色
35:紫色
36:深绿
37:白色

\033[nA 光标上移n行
\033[nB 光标下移n行
\033[nC 光标右移n行
\033[nD 光标左移n行
\033[y;xH设置光标位置
\033[2J 清屏
\033[K 清除从光标到行尾的内容
\033[s 保存光标位置
\033[u 恢复光标位置
\033[?25l 隐藏光标
\033[?25h 显示光标

其中,示例代码里面用到的“\033[31m”表示的是字体颜色设置为红色。详细的使用说明可用 man 命令进行查看:

$ man console_codes

注意,这些控制序列仅仅在Linux下有效,在windows的控制台程序中无效。而windows的控制台程序则可以通过“gotoxy”函数来调整光标的位置。

一些要注意的地方
  • 为什么是用stderr而非stdout作为输出流?

我在阅读其他人介绍 ANSI 序列的相关博客中,看到很多作者都是直接使用 printf 函数打印输出的,我将他们的代码在树莓派上进行了编译运行,跟文中的描述并无二致,颜色、光标的移动、清屏等效果都复现出来了。

但是,需要注意,printf函数是将数据写到标准输出,而Linux遵循了一个标准I/O缓冲的一个惯例——标准错误不带缓冲,打开至终端设备的的流是行缓冲的,其他流是全缓冲的。显示器作为标准输出设备,这对 printf 写到标准输出流(stdout)的影响就是,stdout 流是带有行缓冲的。

进一步的,行缓冲的特点:

行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。

对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O库要求从 (a)一个不带缓冲的流,或者 (b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在 (b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读取数据。很明显,从一个不带缓冲的流中输入(即 (a) 项)需要从内核获得数据。
……
当一个进程正常终止时(直接调用 exit 函数,或从 main 函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。

————摘自《UNIX环境高级编程(第3版)》

如果我们写下如下代码来测试ANSI序列:

#include <stdio.h>
#include <unistd.h>
int main(void)
{
    char  str = "\033[31mplease wait...\033[?25l";
    char  ch[3] = {'\\', '-', '/'};

    printf("%s", str);
    printf("%c\033[1D", ch[0]);
    sleep(2);
    printf("%c\033[1D", ch[1]);
    sleep(2);
    printf("%c\033[1D", ch[2]);
    sleep(2);
    printf("\n");

    return 0;
}

编译运行之后,发现代码是先等待6秒,然后一下子就打印出了“please wait…/”(字体为红色)并退出。究其原因,这3条printf语句将数据送入 stdout 的行缓冲中,行缓冲区既没有写满,也没有写入换行符,导致 stdout 中的这些序列数据一直留在了行缓冲区,而并非我们的程序没有按顺序执行,直到进程结束,程序退出,标准I/O流都被冲洗,这才在屏幕上打印出来。

在其他网站上看到下面一段代码,注意“right 19”那里就没有换行符,结果导致它和“left 10”的结果是一起打印出来(因为“left 10”打印了换行符)而不是先后打印,可以在打印“right 19”之后再调用一个fflush(stdout)冲洗缓冲区,就能看到“right 19”被打印出来。

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

int main(void)
{
    printf("\033[31mThe color,%s!\033[1m\n", "haha");
    printf("\033[31mThe color,%s!\033[4m\n", "haha");
    printf("\033[31mThe color,%s!\033[5m\n", "haha");
    printf("\033[31mThe color,%s!\033[7m\n", "haha");
    printf("\033[31mThe color,%s!\033[8m\n", "haha");
    printf("\033[31mThe color,%s!\033[0m\n", "haha");
    printf("\033[47;31mThe color,%s!\033[0m\n", "haha");
    printf("\033[47mThe color,%s!\033[0m\n", "haha");
    sleep(2);
    printf("\033[44m%s!\033[15A\n", "up 15");
    sleep(2);
    printf("\033[43m%s!\033[9B\n", "down 9");
    sleep(2);
    // 注意这里是没有打印换行符的!
    printf("\033[42m%s!\033[19C", "right 19");
    printf("right 19");
    sleep(2);
    printf("\033[41m%s!\033[10D", "left 10");
    printf("left 10\n");
    sleep(2);
    printf("\033[45m%s!\033[50;20H\n", "move to 50, x 20");
    printf("y50 x20");
    sleep(2);
    printf("\033[46m%s!\033[?25l\n", "hide cursor");
    sleep(2);
    printf("\033[42m%s!\033[?25h\n", "show cursor");
    sleep(2);
    printf("\033[43m%s!\033[2J\n", "clear screen");
    return 0;
}

由于标准错误流 stderr 不带缓冲,我们把输出写到它里面就不受换行符触发或者填满缓冲的限制,只要往里面写入数据,就会立即打印到屏幕上。

  • 色彩控制序列的作用范围

只要我们在打印序列中使用了这些色彩控制序列,如果不主动取消,那么它将会一直作用在程序后续的输出打印上,比如那个字体红色属性,在代码里嵌在“please wait…”字符串中,但同样会影响后面“\”“-”“/”的色彩。而且前面也谈到,那个光标隐藏属性即使在程序结束后都还能影响到shell的光标显示。

  • 打印控制的窗口自适应性

简而言之,我们在GUI的非登陆式shell打印的时候,程序是以当前窗体的界面大小来自适应光标的移动范围,清屏的时候色彩覆盖的范围。它并非按照我们实际屏幕的物理尺寸来调整输出。

Linux下使用C语言修改打印光标位置,通常可以通过ANSI转义序列来实现。ANSI转义序列是一组控制字符,用于控制终端的行为,包括光标位置、文本颜色等。 以下是一些常用的ANSI转义序列,用于控制光标位置: 1. `ESC[` + 行号 + `;` + 列号 + `H`:将光标移动到指定行列。 2. `ESC[` + 行号 + `A`:将光标向上移动指定的行数。 3. `ESC[` + 行号 + `B`:将光标向下移动指定的行数。 4. `ESC[` + 列号 + `C`:将光标向右移动指定的列数。 5. `ESC[` + 列号 + `D`:将光标向左移动指定的列数。 下面是一个示例代码,演示如何使用ANSI转义序列来修改光标位置: ```c #include <stdio.h> // 定义ANSI转义序列的转义字符 #define ESC "\x1b" // 将光标移动到指定行列 void move_cursor(int row, int col) { printf(ESC "[%d;%dH", row, col); } // 将光标向上移动指定的行数 void move_cursor_up(int rows) { printf(ESC "[%dA", rows); } // 将光标向下移动指定的行数 void move_cursor_down(int rows) { printf(ESC "[%dB", rows); } // 将光标向右移动指定的列数 void move_cursor_right(int cols) { printf(ESC "[%dC", cols); } // 将光标向左移动指定的列数 void move_cursor_left(int cols) { printf(ESC "[%dD", cols); } int main() { // 清屏 printf(ESC "[2J"); // 将光标移动到第10行,第20列 move_cursor(10, 20); printf("Hello, World!"); // 将光标向上移动5行 move_cursor_up(5); // 将光标向下移动3行 move_cursor_down(3); // 将光标向右移动10列 move_cursor_right(10); // 将光标向左移动5列 move_cursor_left(5); // 刷新输出缓冲区 fflush(stdout); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值