实战案例7:代码语法高亮:识别关键字、识别字符串、识别注释、识别数字
代码语法高亮,说白了就是给代码“上色”。你写代码的时候,编辑器里关键字是蓝色的,字符串是绿色的,注释是灰色的——这就是语法高亮。今天我们就用C语言和正则表达式,亲手实现一个迷你版的语法高亮器。
我个人习惯把这类工具叫做“代码染色器”。它不改变代码内容,只给不同词法单元打上标签。嗯,听起来简单,但坑不少。我曾经在一个嵌入式项目里,因为注释匹配写错了,导致整个文件高亮全乱掉,排查了整整一个下午。
核心思路:分而治之
语法高亮的核心逻辑其实就四个字:识别 + 标记。我们不需要一次性匹配所有东西,而是分类型逐个处理。我建议你按这个顺序来:
- 先处理注释——注释优先级最高,一旦匹配,里面的内容全部忽略
- 再处理字符串——字符串里的关键字不能高亮
- 然后处理关键字——只匹配不在注释和字符串里的单词
- 最后处理数字——整数、浮点数、十六进制数
为什么会这样?因为注释和字符串是“保护区”,里面的内容不能被当作关键字或数字处理。你想想看,如果先匹配关键字,结果把字符串里的 int 也染成蓝色,那就闹笑话了。
重要原则:正则匹配的顺序决定了高亮的优先级。注释 > 字符串 > 关键字 > 数字。这个顺序一旦搞反,高亮结果必然出错。
识别注释:单行和多行
C语言有两种注释:// 单行注释和 /* */ 多行注释。正则表达式怎么写?
// 单行注释:从 // 到行尾
//\/\/.*$
// 多行注释:从 /* 到 */,中间任意字符
//\/\*[\s\S]*?\*\/
注意那个 [\s\S],它匹配任意字符包括换行。而 *? 是非贪婪匹配,防止匹配到第一个 */ 就停下。我在项目中遇到过一个问题:如果不用非贪婪,两个注释块之间的代码会被吞掉。比如:
/* 注释1 */
int a = 1;
/* 注释2 */
贪婪匹配会把从第一个 /* 到最后一个 */ 全部吃掉,中间的 int a = 1; 就没了。非贪婪匹配就能正确识别两个独立的注释。
识别字符串:双引号和单引号
字符串的匹配看起来简单,但坑很多。最基本的:
// 双引号字符串
//"[^"\\]*(?:\\.[^"\\]*)*"
// 单引号字符
//'[^'\\]*(?:\\.[^'\\]*)*'
这个正则为什么这么复杂?因为要处理转义字符。比如 "hello \"world\"",里面的 \" 是转义的双引号,不能当作字符串结束。我曾经在写一个代码分析工具时,忘了处理转义字符,结果用户代码里有个 printf("他说:\"你好\"");,整个高亮直接崩了。
小技巧:匹配字符串时,先匹配转义序列 \\.,再匹配普通字符。这样就能正确处理所有转义情况。
识别关键字:精确匹配
C语言关键字大概有30多个。我们不需要全部列出来,但至少要覆盖最常用的:
// 关键字列表(部分)
auto, break, case, char, const, continue, default, do,
double, else, enum, extern, float, for, goto, if,
int, long, register, return, short, signed, sizeof,
static, struct, switch, typedef, union, unsigned, void,
volatile, while
正则匹配时,要用 \b 单词边界,防止匹配到变量名的一部分。比如 int 是关键字,但 intPtr 不是。用 \bint\b 就能区分。
// 匹配关键字
//\b(int|char|float|double|if|else|for|while|return|void|...)\b
我个人习惯把关键字按类型分组:类型关键字、控制流关键字、存储类关键字。这样在后续处理时,可以给不同类型的关键字分配不同颜色。
识别数字:整数、浮点数、十六进制
C语言的数字格式比较多:十进制整数、八进制(0开头)、十六进制(0x开头)、浮点数(带小数点或指数)。正则表达式要覆盖这些情况:
// 十进制整数
//\b\d+\b
// 十六进制整数
//\b0[xX][0-9a-fA-F]+\b
// 浮点数(带小数点或指数)
//\b\d+\.\d*([eE][+-]?\d+)?\b
//\b\.\d+([eE][+-]?\d+)?\b
//\b\d+[eE][+-]?\d+\b
注意顺序:先匹配十六进制,再匹配浮点数,最后匹配十进制整数。否则 0xFF 会被先匹配成 0 和 FF 两个数字。
避坑指南:我曾经在匹配数字时,忘了处理 0x 前缀。结果 0xFF 被匹配成 0(十进制)和 FF(标识符)。十六进制数必须优先匹配,且要放在浮点数之前。
完整实现:组合起来
下面是一个完整的C语言语法高亮函数。它读取一行代码,依次匹配注释、字符串、关键字、数字,并输出带HTML标签的结果。
#include <stdio.h>
#include <string.h>
#include <regex.h>
void highlight_line(const char *line) {
regex_t regex;
regmatch_t match;
const char *p = line;
// 定义正则表达式数组(按优先级排序)
const char *patterns[] = {
"//[^\n]*", // 单行注释
"/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/", // 多行注释
"\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"", // 双引号字符串
"'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'", // 单引号字符
"\\b(0[xX][0-9a-fA-F]+)\\b", // 十六进制数
"\\b(\\d+\\.\\d*([eE][+-]?\\d+)?)\\b", // 浮点数
"\\b(\\d+)\\b", // 十进制整数
"\\b(int|char|float|double|if|else|for|while|return|void)\\b" // 关键字
};
// 颜色映射
const char *colors[] = {
"gray", "gray", "green", "green",
"blue", "blue", "blue", "purple"
};
while (*p) {
int matched = 0;
for (int i = 0; i < 8; i++) {
regcomp(®ex, patterns[i], REG_EXTENDED);
if (regexec(®ex, p, 1, &match, 0) == 0 && match.rm_so == 0) {
// 输出匹配的内容,加上颜色标签
printf("<span style='color:%s'>", colors[i]);
printf("%.*s", (int)(match.rm_eo - match.rm_so), p);
printf("</span>");
p += match.rm_eo;
matched = 1;
regfree(®ex);
break;
}
regfree(®ex);
}
if (!matched) {
// 普通字符直接输出
putchar(*p);
p++;
}
}
putchar('\n');
}
这段代码的核心逻辑是:从行首开始,依次尝试匹配8种模式。一旦匹配成功,就输出带颜色的HTML标签,然后移动指针继续。如果都不匹配,就当作普通字符输出。
知识体系图
下面这张图展示了语法高亮器的整体架构。你可以看到,输入代码经过四个阶段的处理,最终输出带颜色标签的HTML。
实际应用中的注意事项
在实际项目中,语法高亮器还需要考虑几个问题:
- 性能问题:如果文件很大(几千行),逐行编译正则表达式会很慢。我建议把正则表达式编译一次,重复使用。
- 多行注释的状态:注释可能跨行,需要记录当前是否在注释块内。可以用一个全局状态变量来跟踪。
- 字符串拼接:C语言允许相邻字符串自动拼接,比如
"hello" "world"。高亮时最好把它们当作一个整体。 - 预处理指令:
#include、#define等指令也需要特殊处理,通常用单独的颜色。
我的经验:如果你只是做一个简单的代码展示工具,用正则表达式完全够用。但如果你要做真正的IDE级语法高亮,建议用词法分析器生成工具(如flex/lex),它们处理边界情况更可靠。
好了,这就是语法高亮器的核心实现。你可以在自己的项目里试试,把这段代码嵌入到网页生成工具中,就能实现代码的彩色展示。记住那个优先级顺序,这是整个高亮器不出错的关键。
公众号:蓝海资料掘金营,微信deep3321