用C语言写一个命令行待办工具:caveman的设计与实践

发布时间:2026/10/28 21:34:30
用C语言写一个命令行待办工具:caveman的设计与实践 给自己写了一个命令行待办工具取名叫caveman。起因很简单手机上的待办App换了一轮又一轮功能越来越花哨我却越来越不愿意打开它们。后来想明白一个事——我需要的不是更多的标签、提醒、看板和团队协作而是找个地方把“接下来要做的几件事”记下来做完划掉。于是就有了这个用C语言写的、名字直白到不需要解释的小工具一个可执行文件一个纯文本文件不联网不装数据库不配服务器。现在这个工具已经跟着我跑了半年周末归档、平时随手记前前后后帮我消化了大概两百多个零散任务。这篇文章就把整个项目拆开讲清楚设计取舍、存储格式、核心代码、实际踩坑全给你捋一遍。1. 项目整体设计与思路拆解1.1 为什么叫“caveman”把待办清单打回石器时代起这个名字的时候我心里想的就是穴居人的生活状态。穴居人一天的待办清单基本上只有两件事今天有没有猎到东西今天晚上有没有安全的地方睡觉。现代人类的待办清单理论上也可以简化成类似的逻辑今天要推进哪些事哪些事已经干完。其他的圈圈绕绕比如把任务分成十几个子任务、给每个任务设精确到分钟的提醒、再给任务打个标签方便以后统计这些功能对于个人日常管理来说绝大多数都是负担。我过去也用过不少主流工具最后发现一个规律整理清单本身会变成一种拖延。打开应用先看一遍项目分组再调两个标签再想想这个任务应该被归到哪个看板里等这套动作做完十分钟过去了正经事还没开始。caveman就是反着来的。它从设计上砍掉所有可以让人“玩工具”的机会只剩四个动作加一条任务、看没完成的任务、把任务标记完成、把已完成的任务归档。就是这么原始。这种刻意限制不是倒退而是把注意力还给事情本身。就像穴居人不会花一上午打磨一把新长矛磨一会儿就出去打猎了。工具够用就好剩下的时间应该花在做事情上。所以项目名caveman对我来说不是“简陋”的意思而是“清爽”的意思。1.2 技术选型为什么是C语言和纯文本可能有人会问现在写这种小工具用Python、Shell或者Go不是更省事吗为什么偏偏选了C语言。说实话选C语言有我的私心。第一当时正好想系统地补一遍C语言的字符串处理和文件IO与其做教材里的学生管理系统不如写个自己真会用的工具学习动力完全不同。第二C语言编译出来的东西真的很小、很安静。一个几百KB的静态链接二进制放到哪里都能直接跑不需要目标机器上有Python解释器或者Go运行时。第三这也是最戳我的一点C语言写出来的程序依赖只有标准库和操作系统提供的基础能力几乎不会出现“昨天还好好的今天跑不起来”的怪问题。存储方案就更简单了。任务数据直接用纯文本每一行一条任务放在~/.caveman/tasks.txt。没有SQLite没有JSON文件没有YAML。原因也很直白这种量级的个人待办数据用数据库属于杀鸡用牛刀。更重要的是纯文本是透明的任何时刻我都可以用cat直接看内容用grep、awk、sed做临时统计就算程序出了bug拿文本编辑器改两行就能修好。数据库文件一旦损坏恢复的难度比改一行文本高出好几个量级。从隐私角度看纯文本存在自己电脑的隐藏目录里不联网、不上传也让我放心很多。我不需要为“待办事项”这种隐私数据绑定任何在线账号。老实说这大概也是我最终能坚持用下来的原因。1.3 功能边界只保留四个操作第一版功能清单我写得很克制add添加一条任务list查看全部未完成任务done把某条任务标记为已完成archive把所有已完成任务移出主清单后来我自己加了两个辅助命令show查看单条任务的完整信息delete删除一条写错了的任务。除此之外优先级的支持是有意的但也只分了 P1、P2、P3 三个档位。P1 是今天必须完成P2 是计划内要做P3 是有空才做。我没有做“截止日期”“循环任务”“提醒通知”“日历视图”“多端同步”这些功能。因为对我个人来说这些东西大多数时候不是帮助而是噪音。需要截止日期的任务我直接在描述里写“周三前完成季度总结”需要周期性提示的事情交给闹钟需要多端查看的工作安排本来就是公司OA系统该管的事。caveman只管住我生活里那一小撮个人任务管住这一撮就够了。这种边界意识是整个项目最重要的设计。每次手里痒痒想加功能我就问自己这个功能能让我今天多做一件事吗如果答案不确定就不加。后来这个原则本身反而成了我用得最久的设计。2. 核心数据结构与命令解析2.1 存储格式显式ID的任务行既然要长期使用数据格式就不能拍脑袋定。我最早的一版用文件行号当任务ID因为实现起来最简单但后来发现一个问题一旦删除掉中间的某一行后面的行号全部往前挪之前记下的ID就对不上了。标记完成还好如果要精确操作某一条任务行号就会飘。所以第二版改成了显式ID每条任务都带着自己的编号格式如下T12 [done] 2025-06-14 [P1] 修复登录页闪退每个字段的含义我都写死在解析逻辑里顺序不能乱字段示例说明任务IDT12全局唯一单调递增删除后不复用状态open/done是否完成日期2025-06-14添加时记录标记完成时改成完成日期优先级P1/P2/P3决定的展示排序和你的注意力分配描述修复登录页闪退任务的自由文本唯一不能省略的部分日期字段我特意只保留到“天”没有精确到时分秒。个人待办不是日志只需要知道是哪天记下、哪天做完的精确到天足够。少一点精度解析也多省一些力气。默认数据文件路径是~/.caveman/tasks.txt同时支持环境变量CAVEMAN_FILE覆盖。我用它测试时方便换目录的时候也不用手动拷贝。在实现上只需要在启动时这样判断const char *path getenv(CAVEMAN_FILE); if (path NULL) { path DEFAULT_DATA_FILE; }2.2 命令语法从 add 到 archivecaveman的操作方式走的是Unix工具一贯的路线一个动词一个目标没有交互式对话。下面是几个最常用命令的完整说明。添加任务时描述用双引号包起来可以用-P指定优先级不指定就用默认的 P2caveman add 准备周会材料 -P 1 caveman add 整理报销单查看未完成任务输出会按优先级排序然后按ID从小到大caveman list输出大概是这样的格式T1 2025-06-14 P1 准备周会材料 T4 2025-06-15 P2 整理报销单 T7 2025-06-15 P3 给书架做一次断舍离标记完成只需要知道任务ID。比如搞定周会材料caveman done T1如果偶尔看花了眼可以用show看某条任务当时的完整记录caveman show T4archive是维护主清单清爽度的关键命令。它会扫描所有done状态的任务把它们追加到同目录的archive.txt然后从tasks.txt中移除。这样日常list看到的永远只有还没做完的事而不是一堆已经画掉的旧记录。2.3 编译安装从源码到可执行文件项目结构非常简单就两个文件caveman.c和Makefile。我使用的是 GCC完整编译参数放在Makefile里方便以后在别的机器上重新构建CCgcc CFLAGS-stdc99 -Wall -Wextra -pedantic -O2 -finput-charsetUTF-8 -fexec-charsetUTF-8 LDFLAGS caveman: caveman.c $(CC) $(CFLAGS) -o $ $^ $(LDFLAGS) install: install -m 0755 caveman ~/.local/bin/ clean: rm -f caveman这里有两个细节值得说明。一是-finput-charsetUTF-8 -fexec-charsetUTF-8用来保证源码里直接写的中文默认字符串比如错误提示、帮助信息在编译后也能以 UTF-8 正常输出。二是安装目录我用了~/.local/bin这是用户级可执行文件目录不需要 root 权限。装好之后需要把~/.local/bin加进 PATH。如果用的是 bash在~/.bashrc里加一行export PATH$HOME/.local/bin:$PATH然后重新登录终端或者手动执行source ~/.bashrc。接下来跑caveman add hello caveman就算正式上路了。3. 实操过程完整实现一个可用的 caveman3.1 源码骨架与关键数据结构整个程序不到五百行核心是由一个结构体和一个解析函数驱动的。结构体描述了一条任务的全部字段#include stdio.h #include stdlib.h #include string.h #include time.h #include locale.h #define MAX_LINE 1024 #define MAX_DESC 900 enum status { OPEN, DONE }; struct task { int id; int status; char date[16]; int prio; char desc[MAX_DESC]; };所有命令最终都会落到三个动作上读文件、写文件、按行解析。解析函数我写得有点轴但胜在直接——先用sscanf抓前面的元数据再把剩下的部分当描述static int parse_task_line(const char *line, struct task *t) { char status_buf[8], prio_buf[8]; int consumed 0; if (line[0] ! T) return -1; int scanned sscanf(line, T%d %7s %15s [%7[^]] %n, t-id, status_buf, t-date, prio_buf, consumed); if (scanned 4) return -1; t-status (strcmp(status_buf, done) 0) ? DONE : OPEN; t-prio (prio_buf[1] 1 prio_buf[1] 3) ? prio_buf[1] - 0 : 2; snprintf(t-desc, sizeof(t-desc), %s, line consumed); return 0; }这里最反直觉的是%n的使用。它不读取任何输入而是把当前已经消费掉的字符数写进consumed变量这样我就能知道任务ID、日期、优先级占掉多少字节剩下的理所当然就是描述文字。因为描述里允许有空格和中文字符用%n比用%s更稳。3.2 核心函数add_task 与 mark_doneadd_task的逻辑是读两遍文件。第一遍扫描现有行找出最大的任务ID并加一第二遍把新任务追加到文件末尾。这样写虽然多一次IO但对这个量级的数据来说时间开销完全可以忽略。真正的收益在于文件不存在时也能正常创建void add_task(const char *desc, int prio) { int new_id next_id(); FILE *fp fopen(data_path(), a); if (fp NULL) { perror(caveman); exit(2); } time_t now time(NULL); struct tm *tm localtime(now); char date[16]; strftime(date, sizeof(date), %Y-%m-%d, tm); fprintf(fp, T%d [open] %s [P%d] %s\n, new_id, date, prio, desc); fclose(fp); }next_id的实现也不复杂逐行读取文件解析每行的T后数字保留最大值。如果文件为空就返回1。注意这里不能用“最后一行加一”因为显式ID只增不减哪怕最后一条任务已经被归档或删除新ID也要继续往上走。mark_done稍微麻烦一点因为要修改文件中间的一行。最安全的方式是先读全文在内存里把对应的行改掉然后写临时文件最后用rename覆盖原文件。关键操作如下// 伪代码示意找到目标行替换 [open] 为 [done]日期改成今天 for (each line) { if (line starts with T12 ) { snprintf(new_line, %s, updated_buffer); modified 1; } fprintf(tmp_fp, %s, new_line); } if (modified) rename(tmp_path, data_path());用临时文件加rename本质上就是借鉴了原子写回的思路。就算程序在中途崩溃原始文件也基本不会被写坏。这一点在文本数据上尤其值得养成习惯。3.3 现场演示从上手到集成为了让你更直观地感受这个工具怎么用我把第一次完整试用caveman的终端记录简化了一下。从编译到跑通一共就这几步make make install caveman add 准备周会材料 -P 1 caveman add 整理报销单 caveman add 给书架做一次断舍离 -P 3 caveman listlist的输出如下T1 2025-06-14 P1 准备周会材料 T2 2025-06-14 P2 整理报销单 T3 2025-06-14 P3 给书架做一次断舍离然后下午把周会材料搞定标记完成之后再次查看caveman done T1 caveman list输出变成T2 2025-06-14 P2 整理报销单 T3 2025-06-14 P3 给书架做一次断舍离到了周末归档直接执行caveman archive已经完成的任务会进入~/.caveman/archive.txt主文件里只剩下未完成的部分。整套流程没有任何多余动作从想到要做某件事到记下来三秒内能完成这就是一个个人工具该有的速度。3.4 让任务按优先级排序的输出逻辑list命令并不是按文件里的原始顺序输出的而是先按优先级 P1、P2、P3 分桶同优先级再按ID升序。实现上我先把符合open条件的任务塞进一个数组然后写一个比较函数int task_cmp(const void *a, const void *b) { const struct task *ta a, *tb b; if (ta-prio ! tb-prio) return ta-prio - tb-prio; return ta-id - tb-id; }这个排序规则会直接影响使用习惯每天早上一打开终端最先看到的永远是当天最该啃的第一件事而不是最晚记下来的那件事。done之后任务立刻从列表里消失视觉上也是一种正反馈。4. 常见问题与排查技巧实录4.1 中文任务描述乱码从源码到终端的一整条链我自己第一次编译完就撞上了中文乱码。现象很典型caveman add 准备周会材料之后用list看到的中文变成了乱码。排查下来有两个原因。一个是源码文件本身的编码。如果编辑器把.c文件保存成 GBK 或者 GB18030GCC 默认又会按 UTF-8 读取源码字符串常量里的中文从一开始就错了。解决办法是在编辑器里统一存成 UTF-8同时给编译参数加上-finput-charsetUTF-8 -fexec-charsetUTF-8正如前面的 Makefile 里那样。另一个原因是终端环境的 locale。如果 Linux 系统的LANG不是zh_CN.UTF-8终端可能不会按 UTF-8 解码输出。我用了一个比较通用的处理在main函数开头调用setlocale(LC_ALL, )让程序主动跟随系统环境变量。代码里加上#include locale.h int main(int argc, char **argv) { setlocale(LC_ALL, ); // ... }从此之后我到哪台机器上编译都不会再为中文输出折腾了。4.2 任务ID为什么不能直接用行号初版caveman用的就是行号当ID。当时觉得反正也没多少人用文件行数也不多行号完全够用。但实际跑了两个星期就发现不对劲。某天我手工用文本编辑器打开tasks.txt删掉了一条写错的旧任务再跑caveman done T6结果发现 T6 指到了另一条任务上因为删除那条旧行之后后面的行号全部前移了。改成显式ID之后这个坑彻底消失。ID一旦分配出去永远不会给别的任务复用。即使某条任务被delete删掉了后面新任务拿到的ID也只会更大。这样就允许用户放心大胆地记住一个ID并在之后几天内继续使用。所以如果你要自己写类似工具我的第一个建议就是ID一定要持久不能依赖位置。4.3 两个终端同时写文件并发怎么办caveman正常情况下是单用户、单命令不太会碰到并发问题。但我在跑自动备份脚本时确实遇到过caveman add和另一个脚本同时写入导致部分内容错乱的情况。这个问题的原因是两个进程同时以追加模式打开同一个文件并用fprintf写入时互相覆盖很容易发生。解决办法是在写入之前给文件加一把“建议锁”。在 Linux 和 macOS 上用flock比较简单#include sys/file.h int fd open(data_path(), O_RDWR | O_CREAT, 0644); flock(fd, LOCK_EX); // 执行读取-修改-写回操作 flock(fd, LOCK_UN); close(fd);锁虽然是“建议锁”但只要所有写路径都遵守就能保证同一时刻只有一个进程改动文件。我在add_task和archive这两个写操作里都套上了这把锁之后再也没有出现过文件错乱。4.4 脚本调用与日常集成用了caveman一段时间之后我开始想让它在后台自动帮点忙而不是每次手动敲命令。比如每天早晨在 cron 里固定加一条低优先级的例行任务0 9 * * * /home/you/.local/bin/caveman add 检查服务器磁盘空间 -P 3这里有个小坑cron 默认 PATH 很短基本不包含~/.local/bin所以要么用绝对路径调用要么在脚本里先export PATH$HOME/.local/bin:$PATH。另外cron 环境下没有交互式终端环境变量CAVEMAN_FILE如果依赖.bashrc设置需要显式在 cron 脚本里也导出一次。我还写了一个简单统计命令用来计算当前未完成任务数grep -c ^T.*\[open\] ~/.caveman/tasks.txt把它绑到 shell 提示符或者 tmux 状态栏里每天一抬头就能看到积压数量。这种轻量集成方式比装一个常驻后台服务舒服得多。4.5 常见问题速查表问题可能原因处理方式中文乱码源码编码或终端locale不对源码存UTF-8编译加charset参数程序里setlocale任务ID对不上使用行号当ID / 手工删过行改用显式ID删除任务也保持ID单调递增命令找不到PATH没有包含安装目录把~/.local/bin加入~/.bashrc数据文件写乱并发写入同一文件写操作加flock锁用临时文件rename替换添加任务失败数据目录不存在或权限不对确保~/.caveman存在且当前用户可写筛选年轻的任务描述里包含空格导致解析错位用%n定位描述起点不要用%s读描述5. 实际跑了半年值得留存的体验最后聊点工具以外的东西。caveman不是一个完美的程序它的界面是黑底白字的终端它的功能少到可能让习惯看板的人觉得寒酸。但半年用下来我反而觉得这种“原始”是我能一直用下去的核心原因。第一个体会是工具一旦被设计成“用完就走”它就不会反过来消耗你的精力。以前我用那些功能繁重的App打开一次要处理完小红点、推送、同步状态才能开始记事情。现在打开终端敲一行caveman add任务已经进去了整个过程没有多余提示也没有值得分心的地方。第二个体会是约束越硬越不容易跑偏。我几次想给caveman加“截止日期提醒”和“标签系统”最后都被自己按住了。为什么因为只要加上提醒我就得为它维护一套时间判断逻辑只要加上标签我就得每次添加任务时纠结该打哪个标签。这些恰恰是我最初想逃离的东西。第三纯文本虽然笨但它真的不怕坏。我备份过很多次tasks.txt也曾经在电脑重装之后直接把它拷回来继续用没有迁移成本没有版本兼容问题。数据握在自己手里心里踏实。如果你也想动手做类似的个人工具我不建议一上来就追求完整。可以从一个最离谱的简化版开始比如只用 shell 脚本加echo 实现跑两周真实场景再根据自己手痒的地方决定要不要升级成 C 语言。工具是给人用的不是给人供着的。半年来caveman教会我的最重要的事大概就是把工具做小把事情做完。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询