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 知识体系总览

下面这张图,是我梳理的控制台跨平台开发的核心脉络。你一看就明白了:

控制台跨平台开发知识体系 控制台输出 ANSI转义序列 ESC[参数m 格式 颜色/样式/光标控制 Win10+ / Linux / macOS Windows控制台API SetConsoleTextAttribute 16色 / 句柄操作 Win7+ 兼容 Unix termios tcgetattr / tcsetattr ECHO / ICANON / ISIG 非阻塞 / 原始模式 跨平台封装层(#ifdef _WIN32)

14.7 总结与避坑

控制台开发,说白了就三件事:

  1. 选对方案:新平台用ANSI,老Windows用API,需要输入控制用termios。
  2. 做好封装:用 #ifdef _WIN32 隔离平台差异,别到处写条件编译。
  3. 记得清理:修改了终端设置,退出时一定要恢复。这是血泪教训。

我曾经踩过的坑:有一次在Linux下写了一个进度条程序,用了ANSI光标移动序列。在本地终端上跑得好好的,结果通过SSH远程执行时,因为SSH客户端不支持某些控制序列,进度条直接变成了乱码。后来我加了一个 isatty(fileno(stdout)) 判断,非交互式终端就禁用ANSI序列。

嗯,控制台这块儿,说难不难,说简单也不简单。关键是要理解底层原理,然后做好封装。这样你的代码才能在Windows、Linux、macOS上都能跑得漂亮。


公众号:蓝海资料掘金营,微信deep3321