你有没有过这样的时刻:手机里堆了四五个笔记应用,每一个都号称重新定义效率,最后你连它们的数据存在哪都不清楚。我就在这种状态里耗了大半年,笔记越记越少,应用越换越勤。直到某一天,我在旧电脑的桌面上翻出一个三年前的 txt 文件,里面乱七八糟地记着当时的想法、待办、地址和密码提示——我居然一口气读完了,还找到了两条早就忘掉的灵感。
于是我做了一个叫 caveman 的小工具。它是一个单文件的 Python 脚本,不依赖任何第三方库,数据就是一行行纯文本,装在终端里用。caveman 这个名字有两层意思:一是自嘲,承认自己在工具选择上退回了原始人水平;二是提醒自己,先用最笨、最透明的手段解决问题,再考虑要不要上复杂度。
这篇文章是这个项目的完整复盘。我会讲清楚我怎么把需求砍到只剩骨架,核心代码怎么设计,日常怎么把它用起来,以及在 Windows、macOS、Linux 上实测时踩过的具体坑。如果你也在被各种"全家桶"型工具裹挟,想回到"一个命令记一条笔记"的简单状态,这篇应该对你有用。
1. 工具焦虑和"退化"成原始人的冲动
1.1 一年换了五款笔记工具,最终输给了一个 txt
先说背景。我做项目有个毛病:喜欢换工具。Notion 出来换 Notion,Obsidian 出来换 Obsidian,中间还用过 Bear、flomo、OneNote,手机上装过一堆速记类 App。每次换工具都有一百个理由:这个支持双向链接,那个支持每日回顾,再一个支持微信剪藏。但真实情况是,换完之后我几乎不会回去翻旧笔记。
去年年底我做了一次"笔记大扫除",统计了一下:五个应用里加起来有 700 多条笔记,真正在近三个月被翻出来用过的,不到 20 条。大部分内容我已经彻底忘了写过,因为它们的搜索逻辑、目录结构、标签体系各不相同,我根本想不起来该去哪个应用里找。
反而是桌面那个 txt,我每周都会打开一次。原因很简单:它就躺在那里,双击就能看,Ctrl+F 就能搜,改完一存完事。没有任何加载动画,没有同步冲突,没有"此功能需要升级会员"。
这个对比让我意识到一件事:我不缺功能,我缺的是"低摩擦"。功能再多,如果每次记录要经过"打开应用→新建条目→选分类→排版→保存",我宁愿不记。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_ok=True) ts = datetime.now().strftime("%Y-%m-%d %H:%M") with open(DATA_FILE, "a", encoding="utf-8") as f: f.write(f"{ts} | {content}\n")a模式是"追加写",天然支持反复打开不互踩。exist_ok=True保证第一次使用时自动创建目录,不需要任何初始化步骤。为什么用~/.caveman而不是项目目录?因为这是一个面向用户日常的工具,数据必须放在用户主目录下才符合直觉,也方便备份。如果你想把数据放到自己的工作目录里,把DATA_FILE改成一个环境变量CAVEMAN_FILE就行,这点灵活性值得留。
3.2 命令层:放弃 argparse 的手写解析
最初我打算用标准库的argparse来做命令解析,写了一半果断放弃。原因有两个。
第一,argparse的默认行为是面向"复杂命令行程序"的,它会生成--help格式、校验参数、报错,这些对于一个四条命令的小工具来说完全是噪音。第二,也是更关键的一点:我的内容参数里会包含中文、空格、标点,argparse的子命令解析在这种场景下经常把我绕晕,而不是帮我。
于是我直接手动解析sys.argv:
def 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", encoding="utf-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", encoding="utf-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", encoding="utf-8") as f: f.writelines(lines) with open(DONE_FILE, "a", encoding="utf-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 n='caveman add' alias nl='caveman list' alias nf='caveman 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(encoding="utf-8", errors="replace")注意reconfigure是 Python 3.7 之后的io.TextIOWrapper方法,老版本可以用sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-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", encoding="utf-8") as f: lines = f.readlines() if sort and lines: lines.sort(key=lambda 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 已经解决了我的核心问题:五秒内完成记录,五秒内找到某条旧记录,零依赖、零学习成本、数据永远在自己手里。
最后分享一个我实际用下来的小心得:别再给记录"排版"了。给内容加一堆格式、分类、标签,本质是在给未来的自己增加阅读负担。原始人刻在石板上的符号不标日期也能被后人读懂,因为信息量本身足够。你的临时笔记也是同样——记的时候尽量快,内容尽量完整,至于"整理",那是一个可以永远拖延下去的问题。