14、控制台与终端:ANSI转义序列支持、Windows控制台API与Unix termios
做跨平台C语言开发,控制台这块儿,说实话,是绕不开的坑。你写个命令行工具,在Linux下跑得好好的,五彩斑斓的日志输出,换到Windows上一看——全是纯文本,连个颜色都没有。用户第一反应就是:这程序是不是坏了?
我早年做嵌入式工具链的时候,就吃过这个亏。一个日志高亮功能,在macOS终端上调试了三天,结果交付时客户在Windows Server上一跑,输出全是乱码。嗯,从那以后,我再也不敢轻视控制台兼容性了。
14.1 控制台输出的本质:字符流与转义
说白了,控制台就是个字符设备。你往stdout写什么,它就显示什么。但为什么有些程序能显示颜色、光标移动、甚至进度条?
关键在于转义序列。终端模拟器会解析输出流中的特殊字符序列,把它们解释为控制指令,而不是普通文本。
核心概念:控制台输出 = 普通文本 + 控制序列。控制序列以转义字符 ESC(\x1B)开头,后面跟着特定的命令格式。
14.2 ANSI转义序列:跨平台的“通用语言”
ANSI转义序列,最早是VT100终端的标准。现在几乎所有现代终端模拟器都支持它——包括Windows Terminal、VS Code内置终端、iTerm2等。
我个人习惯,只要目标平台是Windows 10及以上,优先用ANSI序列。为什么?因为代码写起来清爽,可读性好。
14.2.1 基本格式
// ESC[参数m —— 设置图形模式
// 示例:设置前景色为红色
printf("\x1B[31m"); // 或者 \033[31m
printf("这是红色文字\n");
printf("\x1B[0m"); // 重置所有属性
14.2.2 常用ANSI码速查表
| 类别 | 代码 | 效果 |
|---|---|---|
| 重置 | 0 | 所有属性恢复默认 |
| 加粗 | 1 | 高亮/加粗显示 |
| 下划线 | 4 | 文字加下划线 |
| 闪烁 | 5 | 文字闪烁(部分终端不支持) |
| 前景黑 | 30 | 黑色文字 |
| 前景红 | 31 | 红色文字 |
| 前景绿 | 32 | 绿色文字 |
| 前景黄 | 33 | 黄色文字 |
| 前景蓝 | 34 | 蓝色文字 |
| 前景紫 | 35 | 紫色文字 |
| 前景青 | 36 | 青色文字 |
| 前景白 | 37 | 白色文字 |
| 背景色 | 40-47 | 对应前景色+10 |
14.2.3 一个实用的颜色封装
// color.h —— 跨平台颜色宏
#ifndef COLOR_H
#define COLOR_H
#define RESET "\x1B[0m"
#define RED "\x1B[31m"
#define GREEN "\x1B[32m"
#define YELLOW "\x1B[33m"
#define BLUE "\x1B[34m"
#define BOLD "\x1B[1m"
// 使用示例
#define LOG_INFO(msg) printf(GREEN "[INFO] " RESET "%s\n", msg)
#define LOG_WARN(msg) printf(YELLOW "[WARN] " RESET "%s\n", msg)
#define LOG_ERR(msg) printf(RED "[ERROR] " RESET "%s\n", msg)
#endif
小技巧:在Windows上,如果使用传统的cmd.exe,需要先调用 SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_PROCESSING) 来启用ANSI支持。Windows Terminal和VS Code终端默认就支持,不用额外设置。
14.3 Windows控制台API:老平台的“硬核方案”
如果你需要支持Windows 7、Windows Server 2008这些老系统,或者需要更精细的控制(比如设置任意RGB颜色),那就得用Windows控制台API了。
我记得有一次给银行做运维工具,他们的服务器还是Windows Server 2008 R2,终端根本不认ANSI序列。没办法,只能用 SetConsoleTextAttribute 硬写。
14.3.1 核心API
#include <windows.h>
// 获取控制台输出句柄
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
// 设置文字颜色(前景色 + 背景色)
// 颜色值:0-15,对应16种标准色
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
printf("这是黄色加亮文字\n");
// 恢复默认颜色
SetConsoleTextAttribute(hConsole, 7); // 7 = 浅灰色
14.3.2 颜色常量表
| 常量名 | 值 | 说明 |
|---|---|---|
| FOREGROUND_BLUE | 0x01 | 蓝色前景 |
| FOREGROUND_GREEN | 0x02 | 绿色前景 |
| FOREGROUND_RED | 0x04 | 红色前景 |
| FOREGROUND_INTENSITY | 0x08 | 加亮(高亮度) |
| BACKGROUND_BLUE | 0x10 | 蓝色背景 |
| BACKGROUND_GREEN | 0x20 | 绿色背景 |
| BACKGROUND_RED | 0x40 | 红色背景 |
| BACKGROUND_INTENSITY | 0x80 | 背景加亮 |
注意:Windows控制台API只能设置16种标准色。如果你想要256色或真彩色,还是得用ANSI序列(需要Windows 10及以上版本)。另外,SetConsoleTextAttribute 设置的是全局状态,不像ANSI序列那样可以精确控制每个字符。
14.4 Unix termios:控制台的“底层控制”
在Linux/macOS上,如果你需要控制终端的输入模式(比如关闭回显、设置非阻塞输入),就得用 termios 了。这东西比ANSI序列更底层,直接操作终端驱动。
我做串口调试工具时,就大量用了termios。你想想看,读取串口数据时,肯定不希望终端把特殊字符(比如Ctrl+C)拦截掉吧?
14.4.1 核心结构体与函数
#include <termios.h>
#include <unistd.h>
struct termios old_tio, new_tio;
// 获取当前终端属性
tcgetattr(STDIN_FILENO, &old_tio);
// 复制一份,开始修改
new_tio = old_tio;
// 关闭回显和规范模式(行缓冲)
new_tio.c_lflag &= ~(ECHO | ICANON);
// 设置新属性,立即生效
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
// 此时输入不再回显,也不需要按回车
char ch = getchar();
// 恢复原始设置
tcsetattr(STDIN_FILENO, TCSANOW, &old_tio);
14.4.2 常用模式设置
| 标志位 | 作用 | 典型场景 |
|---|---|---|
| ECHO | 关闭回显 | 密码输入 |
| ICANON | 关闭规范模式(行缓冲) | 逐字符读取 |
| ISIG | 关闭信号生成(Ctrl+C等) | 串口/网络数据读取 |
| VMIN | 设置最少读取字节数 | 非阻塞读取 |
| VTIME | 设置超时时间(0.1秒单位) | 超时读取 |
避坑指南:我曾经在写一个交互式命令行工具时,忘记在程序退出前恢复终端设置。结果用户按Ctrl+C强制退出后,终端一直处于无回显状态,打什么字都看不见。后来我养成了一个习惯:用 atexit() 注册一个清理函数,确保终端设置一定能恢复。
14.5 跨平台封装:一个统一的颜色输出接口
说了这么多,实际项目中怎么用?我的建议是:写一个抽象层,把平台差异封装起来。
// console.h —— 跨平台控制台颜色接口
#ifndef CONSOLE_H
#define CONSOLE_H
#ifdef _WIN32
#include <windows.h>
#define CONSOLE_COLOR_RED 12
#define CONSOLE_COLOR_GREEN 10
#define CONSOLE_COLOR_YELLOW 14
#define CONSOLE_COLOR_RESET 7
#else
#define CONSOLE_COLOR_RED "\x1B[31m"
#define CONSOLE_COLOR_GREEN "\x1B[32m"
#define CONSOLE_COLOR_YELLOW "\x1B[33m"
#define CONSOLE_COLOR_RESET "\x1B[0m"
#endif
// 初始化控制台(Windows下启用ANSI支持)
void console_init(void);
// 设置文字颜色
void console_set_color(int color);
// 重置颜色
void console_reset_color(void);
#endif
// console.c —— 实现
#include "console.h"
void console_init(void) {
#ifdef _WIN32
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwMode = 0;
GetConsoleMode(hOut, &dwMode);
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
#endif
}
void console_set_color(int color) {
#ifdef _WIN32
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, color);
#else
printf("%s", color);
#endif
}
void console_reset_color(void) {
#ifdef _WIN32
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, CONSOLE_COLOR_RESET);
#else
printf("%s", CONSOLE_COLOR_RESET);
#endif
}
14.6 知识体系总览
下面这张图,是我梳理的控制台跨平台开发的核心脉络。你一看就明白了:
14.7 总结与避坑
控制台开发,说白了就三件事:
- 选对方案:新平台用ANSI,老Windows用API,需要输入控制用termios。
- 做好封装:用
#ifdef _WIN32隔离平台差异,别到处写条件编译。 - 记得清理:修改了终端设置,退出时一定要恢复。这是血泪教训。
我曾经踩过的坑:有一次在Linux下写了一个进度条程序,用了ANSI光标移动序列。在本地终端上跑得好好的,结果通过SSH远程执行时,因为SSH客户端不支持某些控制序列,进度条直接变成了乱码。后来我加了一个 isatty(fileno(stdout)) 判断,非交互式终端就禁用ANSI序列。
嗯,控制台这块儿,说难不难,说简单也不简单。关键是要理解底层原理,然后做好封装。这样你的代码才能在Windows、Linux、macOS上都能跑得漂亮。
公众号:蓝海资料掘金营,微信deep3321