文章目录
- 前言
- 一、项目整体架构与主函数设计
- 1.1 命令行参数 argc/argv
- 1.2 main 函数分发逻辑
- 二、标准 IO vs 系统调用:为什么 fgets 简化了行读取
- 2.1 原生 read 的痛点:半行截断问题
- 2.2 fgets 的优势:原生支持按行读取
- 2.3 两套 API 对应关系
- 三、核心字符串函数详解
- 3.1 strcmp:字符串相等判断
- 3.2 strstr:子串查找
- 3.3 strtok:字符串分割
- 四、三大功能模块完整实现
- 4.1 日志列表展示 log_list
- 4.2 按等级筛选日志 log_find
- 4.3 日志等级统计 log_count
- 五、高频踩坑点总结
- 六、运行效果演示
- 总结
前言
在上一篇中我们基于 open/write 系统调用完成了日志写入功能,本篇我们继续完善日志系统的查询、筛选、统计三大核心功能。 很多同学会困惑:为什么手写 read 读取日志要写复杂的 leftover 残留缓冲区?换成 C 标准 IO 的 fgets 为什么就能大幅简化代码?本文会从底层原理到代码实现一步步拆解,同时梳理 strtok/strcmp/strstr 等高频字符串函数的用法与坑点。
一、项目整体架构与主函数设计
整个日志程序通过命令行参数驱动,支持 write/list/find/count/help 五个子命令,主函数负责参数校验与功能分发,是整个程序的入口调度中心。
1.1 命令行参数 argc/argv
C 语言主函数的两个参数是命令行编程的基础:
int main(int argc, char* argv[])
- argc:参数总个数,程序名本身算第 0 个参数
- argv:字符串指针数组,每个元素对应一个命令行参数
- argv[0]:程序本身路径,如 ./minilog
- argv[1]:子命令,如 write/list/find
- argv[2]/argv[3]:子命令的附加参数
举个例子,执行 ./minilog write INFO "system start":
- argc = 4
- argv[0] = "./minilog",argv[1] = "write",argv[2] = "INFO",argv[3] = "system start"
1.2 main 函数分发逻辑
主函数采用「先校验参数,再分支匹配命令」的结构,用 strcmp 匹配用户输入的子命令,分发到对应功能函数:
int main(int argc, char* argv[])
{
// 参数过少:打印帮助退出
if (argc < 2) {
fprintf(stderr, "Error: missing command\\n");
print_help();
return 1;
}
const char* cmd = argv[1];
// 写日志命令
if (0 == strcmp(cmd, "write")) {
if (argc != 4) {
fprintf(stderr, "Error: bad arguments for write\\n");
return 1;
}
return do_write(argv) == 0 ? 0 : 1;
}
// 查看全部日志
else if (0 == strcmp(cmd, "list")) {
return log_list(FILENAME) == 0 ? 0 : 1;
}
// 按等级筛选日志
else if (0 == strcmp(cmd, "find")) {
if (argc != 3) {
fprintf(stderr, "Error: missing level for find\\n");
return 1;
}
return log_find(FILENAME, argv[2]) == 0 ? 0 : 1;
}
// 统计日志数量
else if (0 == strcmp(cmd, "count")) {
return log_count(FILENAME) == 0 ? 0 : 1;
}
// 打印帮助
else if (0 == strcmp(cmd, "help")) {
print_help();
return 0;
}
// 未知命令
else {
fprintf(stderr, "Error: unknown command '%s'\\n", cmd);
print_help();
return 1;
}
}
设计细节:
- 常量左写 0 == strcmp(…):避免手误写成 = 赋值导致的隐蔽 bug
- 统一返回值规范:函数返回 0 代表成功,非 0 代表失败,主函数透传返回值给操作系统
- 提前校验参数:每个子命令先检查参数个数是否合法,不合法直接报错退出
二、标准 IO vs 系统调用:为什么 fgets 简化了行读取
2.1 原生 read 的痛点:半行截断问题
read 是 POSIX 系统调用,只认字节、不认换行符,每次按固定大小的字节块读取数据。 如果一行日志刚好跨越两次 read 的边界,就会出现「一行日志被切成两半」的情况,必须手动维护 leftover 残留缓冲区保存半行数据,下一次读取再拼接成完整行,代码复杂且容易出 bug。 典型场景:
- 文件末尾最后一行没有换行符,会直接丢失
- 单次读取包含多行后,末尾剩余的半行会被截断丢弃
2.2 fgets 的优势:原生支持按行读取
fgets 是 C 标准库 <stdio.h> 提供的函数,底层依然调用 read,但库内部已经帮我们实现了缓冲区和半行拼接逻辑,我们直接就能拿到完整的一行:
char *fgets(char *s, int size, FILE *stream);
- 遇到 \\n 或读到文件末尾就停止,自动把换行符存入缓冲区
- 自动在末尾补 \\0,得到合法的 C 字符串
- 返回 NULL 代表读到文件末尾或读取失败
2.3 两套 API 对应关系
| 打开文件 | open() | fopen() |
| 读取数据 | read() | fgets()/fread() |
| 写入数据 | write() | fwrite()/printf() |
| 关闭文件 | close() | fclose() |
| 句柄类型 | int fd 文件描述符 | FILE* 文件流指针 |
注意:两套 API 绝对不能混用,不能用 fopen 打开却用 close 关闭。
三、核心字符串函数详解
日志解析的核心是字符串处理,三个函数是高频必用工具,我们逐个拆解用法和避坑点。
3.1 strcmp:字符串相等判断
int strcmp(const char *s1, const char *s2);
- 功能:逐字节比较两个字符串,返回值为 0 代表完全相等
- 用途:判断命令是否匹配、判断日志等级是否匹配
- 注意:只能比较以 \\0 结尾的合法 C 字符串
3.2 strstr:子串查找
char *strstr(const char *haystack, const char *needle);
- 功能:在大字符串中查找是否包含目标子串
- 返回值:找到返回子串起始地址,找不到返回 NULL
- 用途:快速判断一行日志中是否包含某个等级标签
// 判断line中是否包含[INFO]
if (strstr(line, "[INFO]") != NULL) {
printf("%s", line);
}
3.3 strtok:字符串分割
char *strtok(char *str, const char *delim);
- 功能:按指定分隔符切割字符串,第一次传原串,后续传 NULL
- 用途:把一行日志按空格拆分成「日期、时间、等级、内容」多个字段
- ⚠️ 重大坑点:strtok 会修改原字符串,把分隔符替换成 \\0,切割后原串会被破坏
示例:拆分一行日志
char line[] = "[2026-08-24 10:30:15] [INFO] system start";
char *args[5] = {NULL};
args[0] = strtok(line, " "); // 第一次调用传原串
for (int i = 1; i < 5; i++) {
args[i] = strtok(NULL, " "); // 后续调用传NULL
}
// args[0] = "[2026-08-24"
// args[1] = "10:30:15]"
// args[2] = "[INFO]"
// args[3] = "system"
四、三大功能模块完整实现
我们统一采用 fopen + fgets + 死循环while(1) 的写法,对齐标准代码风格。
4.1 日志列表展示 log_list
最简单的功能,逐行读取文件直接输出即可:
int log_list(const char* filename)
{
FILE *fp = fopen(filename, "r");
if (NULL == fp) {
perror("log_list fopen error");
return -1;
}
char line_buf[512];
while (1) {
if (NULL == fgets(line_buf, sizeof(line_buf), fp)) {
break;
}
// fgets自带换行符,不用额外加\\n
printf("%s", line_buf);
}
fclose(fp);
return 0;
}
4.2 按等级筛选日志 log_find
这里有个关键细节:不能直接对原行用 strtok 拆分,否则原字符串被破坏,打印会残缺。我们用临时副本做拆分判断,原行保留完整内容用于输出。
int log_find(const char* filename, const char* level)
{
char target[32] = {0};
sprintf(target, "[%s]", level);
// 等级合法性校验
if (0 != strcmp(target, "[DEBUG]") &&
0 != strcmp(target, "[INFO]") &&
0 != strcmp(target, "[WARN]") &&
0 != strcmp(target, "[ERROR]"))
{
fprintf(stderr, "Error: invalid log level '%s'\\n", level);
return -1;
}
FILE *fp = fopen(filename, "r");
if (NULL == fp) {
perror("log_find fopen error");
return -1;
}
int i = 0;
while (1) {
char *args[5] = {NULL};
char line_buf[512] = {0};
char tmp_buf[512] = {0}; // 副本:专门用于strtok拆分
if (NULL == fgets(line_buf, sizeof(line_buf), fp)) {
break;
}
// 拷贝副本,拆分不影响原行
strcpy(tmp_buf, line_buf);
args[0] = strtok(tmp_buf, " ");
for (i = 1; i < 5; i++) {
args[i] = strtok(NULL, " ");
}
// 等级匹配则打印原完整行
if (0 == strcmp(args[2], target)) {
printf("%s", line_buf);
}
}
fclose(fp);
return 0;
}
4.3 日志等级统计 log_count
统计功能只需要提取等级,不需要保留原行内容,所以可以直接拆分原字符串:
int log_count(const char* filename)
{
FILE *fp = fopen(filename, "r");
if (NULL == fp) {
perror("log_count fopen error");
return -1;
}
int count[4] = {0}; // 0:DEBUG 1:INFO 2:WARN 3:ERROR
int i = 0;
while (1) {
char *args[5] = {NULL};
char line_buf[512] = {0};
if (NULL == fgets(line_buf, sizeof(line_buf), fp)) {
break;
}
// 按空格拆分字段
args[0] = strtok(line_buf, " ");
for (i = 1; i < 5; i++) {
args[i] = strtok(NULL, " ");
}
// 匹配等级并计数
if (0 == strcmp(args[2], "[DEBUG]")) count[0]++;
else if (0 == strcmp(args[2], "[INFO]")) count[1]++;
else if (0 == strcmp(args[2], "[WARN]")) count[2]++;
else if (0 == strcmp(args[2], "[ERROR]")) count[3]++;
}
fclose(fp);
// 格式化输出
printf("Debug:%d\\n", count[0]);
printf("INFO:%d\\n", count[1]);
printf("WARN:%d\\n", count[2]);
printf("ERROR:%d\\n", count[3]);
printf("Total:%d\\n", count[0] + count[1] + count[2] + count[3]);
return 0;
}
五、高频踩坑点总结
六、运行效果演示
# 写入几条测试日志
./minilog write INFO "system start"
./minilog write DEBUG "init config"
./minilog write ERROR "disk full"
./minilog write WARN "memory low"
查看全部日志
./minilog list
只看ERROR等级日志
./minilog find ERROR
统计各等级数量
./minilog count
总结
本篇我们从主函数命令行解析入手,对比了系统调用与标准 IO 的读取差异,完整实现了日志的查看、筛选、统计三大功能,核心收获的知识点:
网硕互联帮助中心






评论前必须登录!
注册