caveman:一个"原始人"级别的极简习惯打卡工具
说实话,我最初看到"caveman"这个名字的时候,脑子里冒出来的是石器时代、钻木取火、每天打猎采集的画面。但作为一个折腾了十年命令行工具的人,我几乎立刻意识到:这应该是一个追求极致简单、拒绝一切花里胡哨依赖的极简派项目。我花了几个周末自己动手写了一个叫caveman的极简习惯打卡命令行工具,全程只用 Python 标准库,零第三方依赖,整个项目就一个脚本文件加一个 JSON 数据文件。这篇文章就把整个项目的设计思路、核心实现、踩过的坑完整拆开讲一遍,适合想自己做点小工具、又不想被复杂框架绑架的人参考。
1. 项目概述:为什么做一个"原始人"级别的工具
1.1 这个工具到底是什么
caveman是一个跑在终端里的习惯打卡工具。它做的事情非常简单:你可以往里面添加想坚持的习惯,比如"跑步""阅读""喝水八杯",然后每天完成之后执行命令打个卡,它会帮你记录历史、计算连续天数、输出最近一段时间的执行情况。
整个工具的使用方式长这样:
caveman add 跑步 "每天三公里" caveman done 跑步 caveman list caveman report 7输出非常朴素,就是纯文本表格和数字,没有任何图形化界面。我第一次做出来的时候,朋友问我"你为什么不加个进度条?不加个彩色输出?"我的回答是:加了那些东西,它就不是 caveman 了。这个项目的核心哲学就四个字:原始、简单。原始到连一个第三方库都不装,简单到任何一个会点 Python 基础的人都能在半小时内读懂全部代码。
为什么叫 caveman?因为现代软件太复杂了——一个 TODO 应用要装 Electron,一个计时器要用 React Native,连一个笔记软件都要拉几百 MB 的依赖。我想做一个反方向的东西:回到最原始的形态,用一个只要几 KB 的脚本解决真实需求。这就跟原始人只用石头和木棍就能生活一样,工具的价值不在于它有多炫,而在于它能不能稳定地帮你把事办成。
1.2 解决什么问题,适合谁用
我自己的痛点很具体:我试过 Notion、滴答清单、各种习惯打卡 App,最后都死在同一件事上——打开应用的成本太高。手机解锁、找到图标、点进去、找到对应习惯、点完成,五个步骤,只要中间被打断一次,今天就忘了打卡。而命令行工具的好处是:终端我本来就开着,命令行的肌肉记忆已经形成,敲几个字母回车就完成了。
所以caveman最适合这几类人:
- 开发者:日常本来就在终端里工作,不想为一个小需求切到 GUI 应用。
- 极简主义者:受不了软件膨胀,希望工具越小越好、越透明越好。
- 数据自控派:不想让自己的习惯数据躺在别人的服务器上,所有数据就是本地一个 JSON 文件,随时可以备份、迁移、甚至用脚本分析。
如果你只是偶尔想起要记录一下,那这个工具可能不适合你——它的学习成本虽然低,但如果你没有"每天在终端里做一件事"的固定节奏,你还是会三天打鱼两天晒网。工具救不了懒,但可以给本来就想做的人减少阻力。
2. 设计思路:零依赖不是炫技,是刻意取舍
2.1 为什么选 Python 标准库
选语言的时候我纠结过一轮。Go 写命令行工具很舒服,编译出来是个静态二进制,丢到任何机器上就能跑。但 Go 有个问题:要实现同样功能,代码量会比 Python 多不少,而且我需要维护编译环境。对于我这种只是想快速解决自己问题的场景,Python 的无缝体验赢了——只要有 Python 3.6 以上版本就能跑,macOS 自带、Linux 发行版自带、Windows 装一下也就三分钟的事。
更重要的是,Python 标准库在这个场景下完全够用:
argparse处理命令行参数,不用手写参数解析。json读写数据文件,数据结构天然就是字典嵌套。os和os.path处理目录和路径。datetime处理日期计算。
一个第三方依赖都不需要,意味着用户拿到脚本就能跑。不需要pip install,不需要虚拟环境,不需要担心依赖冲突和版本地狱。这在给别人推荐工具的时候特别有说服力:你拷过去一个.py文件,python caveman.py就能用,零安装成本。
有时候我们被现代工程化的思维惯坏了,习惯性地认为"正规项目一定要有依赖管理、一定要分层架构"。但对一个个人工具来说,最稳妥的方案往往是最朴素的方案。就好比你露营只需要一把瑞士军刀,没必要背一套专业厨具。
2.2 数据存储与目录规划
数据存储我选择了最直接的方式:一个 JSON 文件,放在用户主目录下的~/.caveman/目录里。为什么不用 SQLite?因为这里的数据量撑死了也就几十个习惯、每天每个习惯一条记录,SQLite 对这种量级完全是大炮打蚊子,而且引入 SQLite 意味着要么用标准库内置的sqlite3模块(可以),要么引入额外的驱动(不行),最终我评估了一下,JSON 的读写代码量最少、调试最直观——打开文件就能看懂里面存了什么。
存储结构设计成下面这样:
{ "version": 1, "habits": { "跑步": { "created": "2025-01-06", "description": "每天三公里", "history": { "2025-01-06": "done", "2025-01-07": "done" } }, "阅读": { "created": "2025-01-06", "description": "每天至少半小时纸质书", "history": { "2025-01-06": "done" } } } }这个结构有几个隐性的设计决策:
第一,用习惯名称作为键(key)。这样查询和修改都是 O(1) 的字典操作,而且天然保证了一个习惯不会重复注册。当然代价是"名字不能中途改名",想改名就只能删掉重建,历史记录会丢失。我在 README 里明确写了这一点,把它当作一个特性而不是缺陷——改了名字就是新的习惯,旧的历史留在原处,这样反而能保持数据的一致性。
第二,history里的日期键统一用 ISO 格式YYYY-MM-DD。这样做的最大好处是字典排序就是时间排序,sorted(history.keys())得到的就是从早到晚的序列。字符串比较在日期上完全可靠,这是一个容易被忽略但很重要的细节——如果存的是时间戳或者MM-DD格式,排序和比较都会出问题。
第三,每个记录的值存的是"done"。我最初想过存打卡时间("2025-01-06 21:30:15"),这样能分析出自己每天在什么时间段最容易坚持。后来发现这会导致数据结构变得模糊:判断"今天打没打卡"要从字符串里截日期做前缀匹配,代码复杂一个量级。最终我做成只要打卡就把整天的记录标记为"done",哪天打的时间点这种信息对当前版本来说不是刚需,不需要为一个还没验证的需求把复杂度堆上去。
2.3 命令设计:一个命令只做一件事
命令设计上我遵循了 Unix 哲学:每个子命令只做一件清晰的事,组合起来形成完整的操作闭环。
| 命令 | 作用 | 等价描述 |
|---|---|---|
caveman add <名称> [描述] | 新增一个习惯 | 创建结构化记录 |
caveman done <名称> | 今日打卡 | 写入一条完成记录 |
caveman undo <名称> | 撤销今日打卡 | 删除今天的记录 |
caveman list | 查看所有习惯的今日状态 | 状态总览 |
caveman report [天数] | 查看最近 N 天汇总 | 数据统计 |
caveman remove <名称> | 删除一个习惯 | 永久删除 |
这六个命令覆盖了完整的生命周期:添加、执行、纠错、查看、统计、删除。没有export、没有sync、没有remind——那些都是"软件膨胀"的开始。想要导出数据?直接cp ~/.caveman/habits.json backup.json就完了。想要提醒?你自己设个 cron 或者用手机闹钟,工具不做自己不该做的事。
undo这个命令看似简单,其实是实操中发现问题才补上的。我用了三天之后某天早上点错了卡片(在终端里敲命令也会手滑),发现没法撤销,只能手动改 JSON 文件。那一刻我意识到:任何一个会写入数据的工具,必须有对应的撤销操作,否则用户迁就机器,体验就会很别扭。这是我在设计阶段完全没想到、用了之后才补回来的功能,也是这个项目里我印象最深的一次迭代。
3. 核心功能与实现细节
3.1 参数解析:用 argparse 做命令路由
argparse 是 Python 标准库里的参数解析模块,我用了它的子命令(subparsers)功能来做命令路由。这样写的好处是:caveman done 跑步和caveman report 7用的是不同的参数规则,每一组子命令可以定义自己的位置参数和选项,互不干扰。
核心的入口函数是这样的:
def main(): parser = argparse.ArgumentParser( prog="caveman", description="极简习惯打卡工具——像原始人一样简单" ) sub = parser.add_subparsers(dest="command", required=True) p_add = sub.add_parser("add", help="新增习惯") p_add.add_argument("name", help="习惯名称") p_add.add_argument("desc", nargs="?", default="", help="习惯描述(可选)") p_done = sub.add_parser("done", help="打卡") p_done.add_argument("name", help="习惯名称") p_undo = sub.add_parser("undo", help="撤销今日打卡") p_undo.add_argument("name", help="习惯名称") p_list = sub.add_parser("list", help="查看今日状态") sub.add_parser("list") # 实际用上面这个,这里是示意 p_report = sub.add_parser("report", help="查看汇总") p_report.add_argument("days", nargs="?", type=int, default=7, help="最近天数,默认7天") p_remove = sub.add_parser("remove", help="删除习惯") p_remove.add_argument("name", help="习惯名称") args = parser.parse_args() dispatch(args)几个细节值得展开讲:
第一,required=True是 Python 3.7 以后才可用的参数。如果没传子命令,直接执行caveman不带任何参数,程序会立即报错退出并打印帮助信息。这个行为对用户是友好的——不会出现"执行了但是什么都没发生"的困惑。
第二,desc参数用了nargs="?",意思是可有可无。加了default=""之后,即使没传描述字段,程序也能拿到一个空字符串做默认值,后续不用反复判断None。
第三,report的days参数做了类型转换type=int,同时给默认值 7。这一步虽然简单,但避免了用户在后续统计时传一个字符串导致类型错误的低级问题——argparse 会帮你把类型转换和校验在最前面拦住。
3.2 数据层:读写、备份与原子写
数据层是整个工具的地基,我在这一层花了最多心思。读文件很简单,json.load()打开就完事。真正需要思考的是写入问题:如果程序在写文件写到一半的时候崩溃了,数据文件会不会损坏?
答案是有可能。所以我没有直接往原文件写,而是采用了原子写入(atomic write)的方式:
def save_data(data): tmp_file = DATA_FILE + ".tmp" with open(tmp_file, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) os.replace(tmp_file, DATA_FILE)原理很简单:先写一个临时文件,写完之后用os.replace()把临时文件原子性地替换成目标文件。os.replace()在 POSIX 系统上是一个原子系统调用,要么替换成功,要么不替换,不存在"写了一半"的中间状态。这样即使进程在json.dump期间被杀掉,最多损失一次写入,但原始文件始终是完好的。
另外一个经验:ensure_ascii=False必须加。如果不加,JSON 里的中文会被转义成\u8dd1\u6b65这种形式,数据没错,但当你打开文件想人工查看的时候,看到的全是一堆转义字符,对排查问题极不友好。加了这个参数,写入的就是原始中文,文件可读性瞬间提升。
数据读取这边,我做了版本号和基础结构的兼容处理:
def load_data(): if not os.path.exists(DATA_FILE): return {"version": 1, "habits": {}} with open(DATA_FILE, "r", encoding="utf-8") as f: data = json.load(f) if "habits" not in data: data["habits"] = {} return data为什么要做这个容错?因为用户可能手动编辑过 JSON 文件,或者用了旧版本的数据。如果代码假设数据文件永远结构完全,一旦遇到异常结构就会 KeyError 崩溃,这种崩溃信息对用户来说是灾难——他根本不知道去哪改。一个小的if判断就能让工具在大部分情况下降级可用,这是多年踩坑学来的教训:永远不要信任持久化的数据格式,要像防贼一样防着你的历史数据。
3.3 打卡与连续天数:核心算法的实现思路
打卡逻辑本身没什么难度:检查习惯存在,然后把今天的日期作为 key 写入history字典。真正有意思的是连续天数(streak)的计算——这是所有习惯记录工具里用户最在意的指标,也是算法上最需要小心处理的地方。
先看打卡的核心代码:
def cmd_done(data, args): habits = data["habits"] if args.name not in habits: print(f"错误:习惯「{args.name}」不存在,请先 add") return 1 today = date.today().isoformat() habits[args.name]["history"][today] = "done" save_data(data) streak = calc_streak(habits[args.name]["history"]) print(f"打卡成功!「{args.name}」连续坚持 {streak} 天") return 0连续天数的计算逻辑我把它单独抽成了一个纯函数calc_streak(history),没有混在命令处理里。这样做的好处是方便单元测试——我可以构造任意奇怪的history字典来验证算法是否正确。
算法思路是这样的:
- 获取今天的日期和昨天的日期。
- 判断今天是否已打卡;如果没有,再看昨天是否打卡。这一步决定了"连续"的起算点是今天还是昨天。
- 从起算点开始往前逐日检查,一旦发现某天没有记录,就停止计数。
def calc_streak(history): if not history: return 0 today = date.today() today_str = today.isoformat() yesterday_str = (today - timedelta(days=1)).isoformat() if today_str in history: start = today elif yesterday_str in history: start = today - timedelta(days=1) else: return 0 count = 0 cur = start while cur.isoformat() in history: count += 1 cur = cur - timedelta(days=1) return count这个算法有一个反直觉的地方,但它是故意这样设计的:如果今天还没打卡,昨天打了卡,那么连续天数依然从昨天开始算。比如昨天是第 5 天,今天还没打卡,返回的连续天数就是 5,而不是 0。这样做的理由是——连续这一指标记录的是"过去一段时间的坚持",只要昨天还在坚持,连续记录就不归零。等到今天结束时如果还没打卡,明天再来算,起算点变成昨天,发现昨天不在记录里,连续天数才真正归零。
这个细节我前后想了很久。最初版本是"只从今天开始算",结果发现每天早上打开工具看连续天数永远是 0,吓得我以为自己坚持失败了。后来改成上面的逻辑,才符合人们使用习惯工具的真实心理预期。
4. 实操过程:从零写一个能用的版本
4.1 搭建项目骨架:单文件还是多文件?
写这个项目的第一步是决定代码怎么组织。我纠结过单文件和多文件的问题。多文件的结构更"工程化"——models.py、commands.py、utils.py分开,逻辑清晰。但考虑到整个工具加上注释也就四五百行,多文件反而增加了目录复杂度,用户拷贝分发时还得复制一整个目录。
最终我选择了单文件方案:全部逻辑都在caveman.py一个文件里。为了让代码可读性不下降,我用了清晰的函数命名和段落注释来分区。整个文件的结构大致是:
caveman.py ├── 头部注释(介绍工具用途和使用方式) ├── 常量定义(DATA_DIR、DATA_FILE) ├── 数据层函数(load_data、save_data) ├── 计算函数(calc_streak、calc_summary) ├── 命令处理函数(cmd_add、cmd_done、cmd_undo……) └── 入口函数(main、dispatch)一个文件、一个数据文件,整个项目的全部实体就这两个东西。这就是"原始人"的生存方式——工具极少,但每一样都可靠。
目录上,数据放在~/.caveman/。我没把它放在当前目录,因为这样不管你从哪个目录执行caveman,读到的都是同一份数据。这个决策对使用体验影响很大:如果数据放在当前目录,你在~/work目录打卡的数据和在~/home目录打卡的数据就是两份,统计就会错乱。
4.2 核心代码:命令处理函数的细节
命令处理函数是整个工具的行为主体。每个命令函数接受(data, args)两个参数,data是当前的内存数据对象,args是解析后的命令行参数。这样做有一个好处:所有写操作都先改内存里的data,最后统一调用save_data(data)落盘。内存和磁盘的一致性通过"只在命令函数里改数据、只在命令函数尾部保存"这个约定来保证。
我挑几个实现上有讲究的函数展开讲。
cmd_add的代码:
def cmd_add(data, args): habits = data["habits"] if args.name in habits: print(f"错误:习惯「{args.name}」已存在") return 1 habits[args.name] = { "created": date.today().isoformat(), "description": args.desc, "history": {} } save_data(data) print(f"已添加习惯「{args.name}」") return 0这里检查了重名。如果不检查,后面所有基于名字的操作都会出现歧义——打卡时不知道该写到哪个记录上。这是一个前置校验的典型案例:错误在入口拦住,总比让它传导到深层代码再爆雷要划算得多。
cmd_undo的实现值得多说一句:
def cmd_undo(data, args): habits = data["habits"] if args.name not in habits: print(f"错误:习惯「{args.name}」不存在") return 1 today = date.today().isoformat() history = habits[args.name]["history"] if today not in history: print(f"「{args.name}」今天还没有打卡,无需撤销") return 0 del history[today] save_data(data) print(f"已撤销「{args.name}」今日打卡") return 0注意撤销和打卡的返回值策略不一样:打卡时如果习惯不存在,是错误,返回 1;撤销时如果今天没打卡,不是错误,返回 0。因为"想撤销但今天没打卡"不是异常情况,只是用户记错了,温和提示即可。区分"错误"和"正常但无事可做"这两种情况,能让工具的退出码语义更干净——脚本调用方可以根据退出码判断是否真的出错了。
cmd_report是统计功能,我设计成输出一个简单的 ASCII 表格:
def cmd_report(data, args): habits = data["habits"] if not habits: print("还没有任何习惯,先用 add 添加吧") return 0 days = args.days today = date.today() print(f"最近 {days} 天汇总\n") print(f"{'习惯':<12}{'完成天数':<8}{'连续天数':<8}") print("-" * 30) for name, info in habits.items(): history = info["history"] completed = 0 for i in range(days): d = (today - timedelta(days=i)).isoformat() if d in history: completed += 1 streak = calc_streak(history) print(f"{name:<12}{completed:<8}{streak:<8}") return 0表格用了最原始的字符串格式化对齐方式,输出效果大概是这样:
最近 7 天汇总 习惯 完成天数 连续天数 ------------------------------ 跑步 5 3 阅读 7 7 喝水八杯 4 1你可能会问为什么不用tabulate库?因为零依赖是底线——为一个打印表格的小功能引入一个第三方库,违背了项目的根本原则。f"{name:<12}"这种格式化语法已经足够做对齐了,虽然功能比 tabulate 弱,但在这个场景下完全够用。
4.3 安装与日常使用:让工具真正跑起来
写完之后,要让caveman真正成为一个可以直接敲的命令,而不是每次都要打python3 ~/caveman.py,我做了两件事。
第一,在脚本头部加上 shebang:
#!/usr/bin/env python3这样系统就知道用 Python3 来执行这个文件,不用手动指定解释器。
第二,把脚本链接到PATH目录下。我的做法是:
chmod +x ~/tools/caveman.py ln -s ~/tools/caveman.py ~/.local/bin/caveman~/.local/bin在我的PATH环境变量里,所以现在在任何目录下直接敲caveman就能运行。很多新手在ln -s这一步会搞错源和目标的方向,我在这里给个小提示:ln -s的第一个参数是真实的源文件,第二个参数是你要创建的链接路径。反了的话,系统会创建一个指向错误位置的软链接,执行会报"文件不存在"。
日常使用的时候,我的习惯是设置了 shell 别名来缩短命令。比如在.bashrc或.zshrc里加上:
alias cm="caveman"这样每天打卡就变成了三个键:cm done 跑步。不要小看这少敲的几个字母——习惯能不能坚持,很大程度取决于执行一个动作的阻力有多小。如果你的工具能让用户用最少的心智成本完成记录,那它就已经成功了一半。
5. 常见问题与排查技巧实录
5.1 我踩过的四个真实问题的排查过程
这个项目虽然小,但我在使用和让别人试用的过程中还是遇到了不少问题。下面四个是我印象最深的,每一个的排查过程都挺有代表性。
问题一:中文习惯名在终端里显示乱码
有朋友在 macOS 的 iTerm2 上用这个工具,发现caveman list输出的中文全是乱码。一开始我怀疑是代码里编码处理的问题,检查了ensure_ascii、检查了 JSON 读写编码,都没发现问题。后来排查到终端本身的编码设置——他的 iTerm2 环境变量LANG没设置成zh_CN.UTF-8,导致终端以错误的编码解析输出。处理方法是建议他在.zshrc里显式设置export LANG=en_US.UTF-8。这个问题的教训是:跨平台工具的中文兼容性,往往不是代码的问题,而是终端环境的问题,排查时要跳出错觉。
问题二:json 文件被手动修改后格式错误,程序直接崩溃
有次我手动编辑 JSON 文件加数据,不小心多打了一个逗号,运行caveman list直接抛了 JSONDecodeError,还带着一大串 traceback,非常吓人。这个问题的根源是我的load_data()没有捕获解析异常。后面我改成了这样:
def load_data(): if not os.path.exists(DATA_FILE): return {"version": 1, "habits": {}} try: with open(DATA_FILE, "r", encoding="utf-8") as f: data = json.load(f) except json.JSONDecodeError: backup = DATA_FILE + f".broken-{datetime.now().strftime('%Y%m%d%H%M%S')}" os.rename(DATA_FILE, backup) print(f"警告:数据文件损坏,已备份到 {backup},创建新的空数据文件") return {"version": 1, "habits": {}} if "habits" not in data: data["habits"] = {} return data遇到解析错误,先把损坏的文件改名备份,再创建一个新的空数据。这样虽然历史数据"暂时"不可用,但工具能继续跑,而且备份文件还在,用户可以手工抢救数据。这比直接抛异常让工具瘫掉要好得多——我的原则是:个人工具可以丢一次数据,但不能把用户卡死在原地。
问题三:连续天数的计算在跨年时出过错
这个是我写单元测试时发现的。最初calc_streak里我用cur -= timedelta(days=1)然后cur.isoformat()去字典里查,理论上不会跨年出问题。但我在测试用例里模拟"12月31日打卡,1月1日打卡"的场景时,突然意识到 ISO 格式字符串比较是字典序,而字典序和真实日期序在特殊情况(比如2025-12-31和2026-01-01)下依然是一致的,所以代码没问题。但这让我意识到一个潜在隐患:如果有人用"2025-1-1"这种格式手动往 JSON 里写了日期,那排序和匹配就会错乱。所以我在文档里加了明确约定:所有日期必须是补零的 IS0 格式,手动编辑数据文件时不要省零。
问题四:多设备同步时数据冲突
我在公司和家里的电脑上都装了同样的工具,有时候会出现同一台电脑上数据是旧的、覆盖了新记录的情况。这是直接拷文件同步方式的天然缺陷。我这个版本的解决方案很简单——不解决。在 README 里写清楚:这个工具默认单机使用,如果要多设备同步,可以把~/.caveman/habits.json放到你自己的同步盘里,但要注意别在两台机器同时写。真正的多设备同步是另一个复杂课题,不适合塞进一个原始人工具里。工具的价值在于把边界划清楚,而不是什么都要做。
5.2 注意事项与避坑速查表
结合我自己的使用经验,整理了一份避坑清单,给打算照着做或者自己动手写类似小工具的人参考:
| 坑的类别 | 具体表现 | 避免方法 |
|---|---|---|
| 数据中心化 | 数据放在当前目录,换目录就"丢失" | 统一放在~/.xxx/用户目录 |
| 非原子写入 | 写一半崩溃导致文件损坏 | 临时文件 +os.replace() |
| 无撤销命令 | 手滑打卡只能手动改文件 | 补上undo命令 |
| 无数据容错 | JSON 手动改坏就崩 | 捕获解析异常,自动备份重建 |
| 中文字符转义 | JSON 文件里全是\u码 | ensure_ascii=False |
| 路径硬编码 | 换用户就不能用 | 用os.path.expanduser("~") |
还有一条关于 shell 别名的建议:如果你要设别名,建议设置完整的命令别名,比如alias cm="caveman",而不是alias cm="python3 ~/tools/caveman.py"。为什么?因为你可能后续会把caveman换成同名的一个编译版二进制,或者挪位置,这时候别名指向的是名字而不是路径,迁移成本就小得多。这是我一贯的偏好——别名尽量指向名字,不要指向路径,这样工具链才具备可迁移性。
6. 写在最后的一些体会
这个项目从构思到可用的版本,大概花了两个完整的周末。第一个周末写完了所有核心功能,第二个周末根据自己实际使用的感受调了很多细节——把undo加进去、优化calc_streak的起算逻辑、处理数据文件损坏的情况。这些功能没有一个是我在第一版就能想全的,全部是在真实使用中暴露出来需求后才补的。这个过程中我最大的体会是:一个工具好不好用,只有在你自己日复一日地用它之后才能知道,纸面上的完美设计永远替代不了真实场景的检验。
如果你也打算做一个类似的小工具,我给三点建议。第一,从你能承受的最小功能集开始,先让一个极简版本跑通,再谈扩展。第二,把"数据安全"放在功能优先级的最前面——数据丢失一次,用户信任就归零,这会直接杀死一个工具。第三,不要迷恋技术选型上的"高级",对个人工具来说,维护成本和使用成本往往比技术难度重要得多。caveman这种原始人级别的工具没有任何技术门槛,它存在的意义就是提醒我,也是提醒每一个耐不住浮躁的开发者:很多好工具不是靠复杂取胜的,恰恰相反,它们靠的是在取舍上的清醒和克制。