你有没有过这样的时刻手机里堆了四五个笔记应用每一个都号称重新定义效率最后你连它们的数据存在哪都不清楚。我就在这种状态里耗了大半年笔记越记越少应用越换越勤。直到某一天我在旧电脑的桌面上翻出一个三年前的 txt 文件里面乱七八糟地记着当时的想法、待办、地址和密码提示——我居然一口气读完了还找到了两条早就忘掉的灵感。于是我做了一个叫 caveman 的小工具。它是一个单文件的 Python 脚本不依赖任何第三方库数据就是一行行纯文本装在终端里用。caveman 这个名字有两层意思一是自嘲承认自己在工具选择上退回了原始人水平二是提醒自己先用最笨、最透明的手段解决问题再考虑要不要上复杂度。这篇文章是这个项目的完整复盘。我会讲清楚我怎么把需求砍到只剩骨架核心代码怎么设计日常怎么把它用起来以及在 Windows、macOS、Linux 上实测时踩过的具体坑。如果你也在被各种全家桶型工具裹挟想回到一个命令记一条笔记的简单状态这篇应该对你有用。1. 工具焦虑和退化成原始人的冲动1.1 一年换了五款笔记工具最终输给了一个 txt先说背景。我做项目有个毛病喜欢换工具。Notion 出来换 NotionObsidian 出来换 Obsidian中间还用过 Bear、flomo、OneNote手机上装过一堆速记类 App。每次换工具都有一百个理由这个支持双向链接那个支持每日回顾再一个支持微信剪藏。但真实情况是换完之后我几乎不会回去翻旧笔记。去年年底我做了一次笔记大扫除统计了一下五个应用里加起来有 700 多条笔记真正在近三个月被翻出来用过的不到 20 条。大部分内容我已经彻底忘了写过因为它们的搜索逻辑、目录结构、标签体系各不相同我根本想不起来该去哪个应用里找。反而是桌面那个 txt我每周都会打开一次。原因很简单它就躺在那里双击就能看CtrlF 就能搜改完一存完事。没有任何加载动画没有同步冲突没有此功能需要升级会员。这个对比让我意识到一件事我不缺功能我缺的是低摩擦。功能再多如果每次记录要经过打开应用→新建条目→选分类→排版→保存我宁愿不记。txt 之所以赢不是因为它强大而是因为它几乎不存在——它像一个原始人的石板不提供任何多余的东西。1.2 caveman的命名由来向最原始的手段致敬caveman这个词在程序员圈子里其实是个褒贬参半的梗。最常见的关联是caveman debugging——指不用调试器、全靠 print 输出慢慢定位问题的原始调试法。很多人觉得这是菜鸟行为但实际上在面对环境复杂、断点不生效、调试器配置要折腾半天的问题时print 往往是最快出结果的方案。我特别吃这一套。所以给项目起名的时候我几乎是下意识地敲了 caveman。我想表达的是这个工具不追求先进追求的是一眼能看穿。它的全部数据是可见的文本全部逻辑就是几十行顺序代码任何人拿到手十分钟就能看懂它在干什么改起来也没有心理负担。另外一个原因和近来网络上 caveman 这个词的流行语境有关——它经常被拿来形容断网很久、跟不上潮流的人。我做这个工具时的心态恰好就是我主动选择断掉对工具的追逐先像个原始人一样把记下来、找得到这两件事做好。起名还有个实际的考虑caveman 在各类开源社区里撞名的概率很低搜一下不会有几百个同名仓库免得以后跟别人的项目混淆。如果你也要给自己的小工具起名冷门词、好记、和项目气质搭这三个条件至少占两个别用一堆通用词拼出来的名字。2. 需求收敛设计哪些功能被我亲手砍掉2.1 三条设计红线零依赖、纯文本、单文件动手之前我先给自己立了三条红线零第三方依赖只用 Python 标准库。理由很简单我不想在任何一台新机器上先折腾 pip install 才能用工具。哪怕是 requests 这种几乎人人都有的库我也列为禁用对象。数据必须是纯文本所有笔记存成一个文本文件每行一条。理由也很直接纯文本是未来几十年都能打开的文件格式不需要任何数据库迁移和版本升级。必须是一个单文件脚本整个工具就是一个 caveman.py拷到哪台机器都能跑不用装包、不用配环境变量最多加个别名。这三条红线本质上是在防止功能蔓延。我给自己设了一个很朴素的问题如果一个功能必须破坏以上任何一条红线那它就不配存在。基于这三条红线我最终确定的功能只有四个add 内容快速记录一行文字自动带上时间戳list [数量]列出最近的 N 条记录find 关键词全文子串搜索done 序号把一条记录标记为已完成移入 done.txt够了真够了。我把自己过去半年真正高频使用的动作列了一遍发现无非就是记和找偶尔需要标完成。编辑历史标签云思维导图多人协作通通不是核心需求反而是摩擦来源。2.2 砍掉的功能清单与理由这里有一张我当时的实际需求 vs 幻想需求对比表功能传统笔记应用caveman 的处理砍掉理由标签分类支持需要维护标签体系不提供全靠搜索记的时候想标签是负担富文本格式支持加粗、标题、引用纯文本让内容可被任何工具读取附件/图片支持拖入不支持文件另存笔记里只写路径云同步各家私有协议不内置交给 git 或网盘每日回顾支持插件用 cron list 实现系统能力可以替代特别想聊一下标签这个功能。我一开始的理想设计是给每条笔记分三类工作、生活、灵感。但用了两周就发现最消耗意志力的根本不是记录而是这条该放哪类。很多时候一条笔记既属于工作又算灵感我纠结十秒钟干脆不记了。后来我把分类字段彻底删掉换成纯粹的关键词搜索记录成本降到了零使用频率反而翻倍。实际上我连编辑功能都考虑砍掉。最初我还写了caveman edit 序号去替换某一行后来想通了改一条记录的场景远远少于重记一条新记录。与其实现一个容易出错的读取-定位-替换-写回流程不如直接告诉用户错了就再 add 一条新的。这个决定让脚本少了几十行也少了一个潜在 bug 源。这个思路可以叫用追加对抗修改——数据只增不改就不存在并发冲突和定位偏移的问题。这个教训后来被我写进了项目说明里功能不是加法是减法每一个需要用户决策的设计点都在消耗用户的使用意愿。提示如果你也希望记录成本低于五秒记住一个原则——任何需要你思考怎么存的设计选分类、打标签、选笔记本都是多余的。把记录动作压成一行命令这个工具才能真正活下来。3. 200 行核心代码逐块拆给你看3.1 存储层一行一条记录文件即数据库整个工具的存储设计简单到可能有些人会觉得这也能叫设计所有数据都追加写入~/.caveman/notes.txt一行一条格式是时间 | 内容。2025-01-12 09:31 | 给客户回复邮件重点说明上线时间 2025-01-12 14:05 | 备忘周五前提交报销单 2025-01-13 08:22 | 灵感把筛选逻辑拆成独立模块这个格式的妙处在于文件本身就是人可读的不需要任何工具就能打开编辑同时每行开头的时间戳保证字典序和真实时间顺序一致前提是补齐两位数所以就算什么排序逻辑都不写按行读出来的顺序就是时间顺序。对应的代码很短DATA_FILE os.path.expanduser(~/.caveman/notes.txt) def add(content: str) - None: os.makedirs(os.path.dirname(DATA_FILE), exist_okTrue) ts datetime.now().strftime(%Y-%m-%d %H:%M) with open(DATA_FILE, a, encodingutf-8) as f: f.write(f{ts} | {content}\n)a模式是追加写天然支持反复打开不互踩。exist_okTrue保证第一次使用时自动创建目录不需要任何初始化步骤。为什么用~/.caveman而不是项目目录因为这是一个面向用户日常的工具数据必须放在用户主目录下才符合直觉也方便备份。如果你想把数据放到自己的工作目录里把DATA_FILE改成一个环境变量CAVEMAN_FILE就行这点灵活性值得留。3.2 命令层放弃 argparse 的手写解析最初我打算用标准库的argparse来做命令解析写了一半果断放弃。原因有两个。第一argparse的默认行为是面向复杂命令行程序的它会生成--help格式、校验参数、报错这些对于一个四条命令的小工具来说完全是噪音。第二也是更关键的一点我的内容参数里会包含中文、空格、标点argparse的子命令解析在这种场景下经常把我绕晕而不是帮我。于是我直接手动解析sys.argvdef main() - None: args sys.argv[1:] if not args or args[0] list: list_notes(int(args[1]) if len(args) 1 else 10) elif args[0] add: add( .join(args[1:])) elif args[0] find: find(args[1]) elif args[0] done: done(int(args[1])) else: print(HELP)add用 .join(args[1:])把剩余参数拼成一行这样执行caveman add 记得买牛奶和面包不需要额外加引号也能正常工作。这是一个很小的细节但体验差异很大如果强制要求用户caveman add 记得买牛奶和面包懒惰的我大概率会在某天少打一对引号导致内容被截断。代价是如果内容里本身包含多个空格或者特殊字符手动解析会丢失信息。我的处理是接受这个代价——对于一条临时备忘来说多个空格被压缩成单个空格损失几乎为零但少打引号就翻车的挫败感是致命的。3.3 查找与状态流转子串匹配和移动行find的实现是我整段代码里最没营养的但它够用def find(keyword: str) - None: if not os.path.exists(DATA_FILE): return with open(DATA_FILE, r, encodingutf-8) as f: for line in f: if keyword in line: print(line, end)不建索引、不做模糊匹配、不考虑大小写纯子串包含判断。对于几百行甚至几千行的文本文件这种线性扫描的性能都完全够用。真到了几万行那也是 grep 该管的事轮不到我重新发明轮子。done稍微有点意思。我的做法是把已完成的行从notes.txt挪到done.txt而不是在原行上加标记。原因很实际我不想在list的默认输出里看到一堆已完成的历史噪音。如果只是在行尾加[done]标记用户每次列出最近十条会有三条是已做完的事干扰真正待办的信息密度。def done(index: int) - None: with open(DATA_FILE, r, encodingutf-8) as f: lines f.readlines() if not (0 index len(lines)): print(f序号 {index} 超出范围总共有 {len(lines)} 条) return target lines.pop(index) with open(DATA_FILE, w, encodingutf-8) as f: f.writelines(lines) with open(DONE_FILE, a, encodingutf-8) as f: f.write(target) print(f已完成{target.rstrip()})这里有一个容易忽略的点list显示的是最近 N 条也就是文件后 N 行但done接收的序号是列表中的相对位置。如果你先list 20看到第 5 条要标完成done 5操作的是文件当前的第 5 行那其实不是同一条。我的解决办法是在list输出时直接显示相对序号从 0 开始并约定先list再done让用户在同一个会话里完成操作。这种简化是有代价的但对个人工具来说清晰的约定比全面防错更重要。3.4 把它打扮成系统命令单文件脚本还需要最后一步才能融入日常让它像一个真正的系统命令一样可以被调用。我写了一个三行的小脚本放在/usr/local/bin/caveman#!/bin/bash exec python3 $HOME/.caveman/caveman.py $这样在任何目录下敲caveman都能直接运行。macOS 和 Linux 下这个方案通用Windows 下我在 PowerShell 里加了一个函数别名本质是一样的。数据目录和脚本目录分开还有个额外好处脚本文件可以被 git 管理方便我同步不同机器上的版本数据文件则单独同步互不干扰。这是我在其他项目里吃过亏之后养成的习惯——代码和用户数据永远要分开放。4. 把脚本接进日常生活4.1 别名、引号习惯和五秒内完成一次记录工具本身写完之后真正的考验是它能不能改变我每天的动作。我做了三件事让记录成本降到最低。第一配置别名。在.bashrc或.zshrc里加alias ncaveman add alias nlcaveman list alias nfcaveman find从此记一条笔记只需要n 明天十点开会加上一次回车三秒内完成。我给自己定的标准是从想到一件事到它被记录下来不能超过五秒。超过五秒这个工具就会被我自然遗弃。第二训练自己的引号习惯。虽然脚本内部做了join处理但内容里如果包含$、、;、*这类 shell 特殊字符时依然要加引号否则会被 shell 先吞掉。比如记一条bose sony 耳机对比必须写成n bose sony 耳机对比。我踩过一次坑那次我以为记的是 bose sony实际文件里只剩 bose因为把命令切成了后台执行。这个教训让我养成了凡是带符号统一加引号的肌肉记忆。注意caveman add的内容如果包含空格以外的特殊符号$、、;、*记得用单引号包起来否则 shell 会抢先解析。第三把它绑进收件箱习惯。我的做法是任何场合开会、走路、看书一旦冒出一个想法立刻在脑子里判断它是不是需要在未来被找到。是就打开终端敲n ...不是就放掉。这个二选一的判断比存入哪个笔记本要简单得多所以执行率很高。还有一个小技巧不要把记录动作设计成需要打开某个应用再点新建。我见过很多人的失败案例是手机里装了五个 App结果灵感来时正开着 IDE根本不想切窗口。caveman 的优势在这里体现得很明显终端永远开着一条命令的事。我甚至在 IDE 的集成终端里也能直接敲n xxx不用切任何窗口。4.2 定时汇总、git 同步、手机端配合单机脚本最大的短板是不在身边。我做了三件低成本的事来补。一是用 cron 每天 23:55 生成一份当天的记录汇总内容就是caveman list 50的输出送到一个固定的文件里。这样即使当天没开过几次终端晚上也能扫一眼今天到底记了什么。# crontab -e 55 23 * * * /home/user/.caveman/summary.shsummary.sh里就三行调用caveman list 50把结果重定向到~/.caveman/daily.txt再顺便统计一下今天的条数。没有复杂的依赖也没有通知推送——推送这种功能本质上又是在造一个需要维护的中间层。二是用 git 管理.caveman数据目录每次操作后自动 commit。我加了一个非常短的脚本在add和done之后执行git add -A git commit -m update配合远程仓库等于有了一个聊胜于无的同步和历史版本。回头翻旧账的时候我能看到某条笔记是什么时候被标记完成的。三是手机端我没有做 App只在手机浏览器里放了一个指向 git 仓库文件的快捷方式。因为数据是纯文本随便一个带浏览功能的 Git 托管平台网页端直接就能看和搜这已经覆盖了我 90% 的人在外面想查一条笔记的场景。这套组合拳不优雅但非常可靠。cron 是系统自带git 是日常标配浏览器是每个手机都有——我没有引入任何一个需要额外维护的中间层。5. 踩过的坑编码、排序和分类伪需求5.1 同一个脚本在三个操作系统上的编码问题caveman 是我第一次认认真真在 Windows、macOS、Linux 三端都跑过的个人项目编码问题排名第一。Linux/macOS 的终端默认 UTF-8基本没问题。Windows 则不一样老版本 PowerShell 和 cmd 的默认编码是 GBK中文系统Python 在重定向输出到管道时如果print里出现了 UTF-8 字符而系统编码不匹配直接抛UnicodeEncodeError。我当时的第一个版本在 Windows 上跑caveman list一半情况直接红字崩溃一半情况中文乱码。排查之后发现两个修复点。一是脚本开头强制 UTF-8 输入输出。在main()入口处加了一行sys.stdout.reconfigure(encodingutf-8, errorsreplace)注意reconfigure是 Python 3.7 之后的io.TextIOWrapper方法老版本可以用sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)替代。二是 Windows 终端本身的问题。即使用chcp 65001切到 UTF-8 代码页Windows Terminal 和传统 conhost 的表现也不完全一致。我的建议很现实Windows 用户直接装 Windows Terminal然后在启动设置里把默认代码页固定为 UTF-8比在脚本里做一堆 hacks 靠谱得多。这个坑给我的教训是如果一个工具宣称跨平台编码问题必须在第一周就处理完不然后面每个功能都会带着这个隐患。5.2 时间排序里藏着的边界问题第二个坑藏得比较深是我用了两周之后才发现的最后一页问题。我的list实现是lines[-limit:]——取文件最后 N 行。这在文件只追加、顺序即时间的前提下是正确的。但有一天我手动用编辑器修改了notes.txt把一条旧笔记移到前面并加了个前缀之后list的顺序就乱了——准确说不是乱是最后 N 行不再等价于最近 N 条。我最后的处理方案很朴素恢复文件只追加纪律手动编辑旧笔记时只允许修改该行内容本身不允许移动行序或删除行真要归档走done命令。同时把list改为可选地按时间戳字段重新排序def list_notes(limit: int 10, sort: bool True) - None: with open(DATA_FILE, r, encodingutf-8) as f: lines f.readlines() if sort and lines: lines.sort(keylambda x: x.split(|)[0].strip()) for line in lines[-limit:]: print(line, end)split(|)取行首的时间戳字符串参与排序因为格式统一是YYYY-MM-DD HH:MM字符串比较的结果和真实时间顺序完全一致。这个问题的本质是我相信了一个隐含假设。任何依赖文件行序的程序一旦用户手工介入假设就会被打破。要么用代码强制约束比如启动时校验行序要么在文档里明确纪律。我个人选了后者因为个人工具的首要原则是别太像软件。5.3 分类这个伪需求我花了整整一周才想通这个坑在前面提过但值得展开说。我最早的设计带一个分类字段命令是caveman add -t 工作 给客户回邮件。听起来很合理对吧直到我统计自己一周的记录发现 42 条笔记里有 9 条被我归到了错误的分类还有 5 条在归类时犹豫超过十秒。我反复问自己分类到底给我带来了什么答案是列表可以按类别过滤。但真实使用中我找一条旧笔记的方式永远是关键词搜索从来不是打开工作类目逐条翻。既然过滤这个动作一辈子用不上几次那分类就是在为想象中的需求付出真实的成本。删掉分类字段之后工具的命令行从caveman add -t 工作 xxx变成了caveman add xxx记录成本肉眼可见地降低了。这个被我称为分类伪需求的观察后来成了我判断一切功能的标准如果一个功能今天不加也能完成核心任务那它今天就不该加。6. 项目做完之后我对工具这件事的认知刷新6.1 工具不是越强越好而是越透明越好做 caveman 之前我选工具的逻辑是功能越多越安心。做完之后我的标准变成了我能看穿它多少。caveman 的所有逻辑摊开不到 300 行数据文件打开就能读出了问题不用等版本更新自己改两行就完事。这种透明感带来的安全感是任何黑盒软件给不了的。这个项目还改变了我选型的一个习惯凡是别人给我推荐XX 全能工具我的第一反应已经开始变成——它能导出我的数据吗我能手工读文件吗答案是否定的我就很难信任它。不是功能不好而是把长期数据交给一个我看不懂结构的黑盒等于在跟时间打赌。顺带说一句我并没有卸载其他笔记应用。caveman 做的事其实非常窄它是我的临时脑外存储只承载那些现在不记下来十分钟后就会忘的内容。长文写作、资料整理、项目文档该用正经工具还是用。关键是每个工具都待在它该待的位置而不是一个工具试图吞下所有场景。6.2 caveman 接下来的三个扩展方向工具写完了但我列了几个可能的方向全都在不破坏三条红线的范围内给add加一个-d参数指定未来某天提醒自己。实现上不需要复杂的调度器只要在list和 cron 汇总时多查一条是否有到期未读触发一次通知即可。支持caveman export导出为 JSON 或 CSV方便以后万一要迁移数据。纯文本文件本身就很容易转这个功能优先级不高但值得留接口。做一个caveman stats统计最近一周的记录条数和完成率。这个对个人复盘有用实现的成本也很低只是多扫一遍文件做计数。我大概率会在下一个假期挑其中一个做掉。但即便什么都不做现在的 caveman 已经解决了我的核心问题五秒内完成记录五秒内找到某条旧记录零依赖、零学习成本、数据永远在自己手里。最后分享一个我实际用下来的小心得别再给记录排版了。给内容加一堆格式、分类、标签本质是在给未来的自己增加阅读负担。原始人刻在石板上的符号不标日期也能被后人读懂因为信息量本身足够。你的临时笔记也是同样——记的时候尽量快内容尽量完整至于整理那是一个可以永远拖延下去的问题。