
1. 项目概述为什么我不做App反而折腾一个命令行工具做了这么多年开发我电脑里的待办事项工具换了一茬又一茬手机自带备忘录、各种花里胡哨的GTD应用、带番茄钟的桌面神器……最后留下的是一个我自己用Python写的命令行待办事项工具。听起来很返祖但真用起来这东西比绝大多数GUI工具都趁手。先说说这个项目到底是什么。它是一个运行在终端里的待办事项应用通过几条简单的命令来添加任务、查看列表、标记完成、删除清理。没有窗口、没有按钮、没有弹窗提醒所有操作都是敲键盘。输入todo add 给老板发周报任务就存进去了输入todo list所有未完成事项按优先级排好队打印出来。这个东西适合谁首先是我这样的命令行重度用户——每天90%的时间泡在终端里切出去开个App反而打断工作流。其次是刚开始学编程的朋友它是个绝佳的练手项目数据持久化、参数解析、文件操作、测试一个命令行工具能把基础编程技能串个遍。再就是有特殊工作习惯的人比如喜欢将任务列表导出、想对任务做脚本化批量处理的效率党。我在设计这个工具时给自己定了几个硬性原则极简——没有配置文件、没有数据库、没有图形界面一条命令就干一件事可靠——数据存储在本地纯文本文件不会因为App下架、云端同步出错而丢失可组合——所有命令走标准输入输出能被shell脚本调用能跟其他命令行工具自由组合。这篇文章会把完整的实现过程拆开来讲从命令设计、存储格式、核心代码到踩过的坑全部记录。做出来的工具不依赖网络不藏着掖着原理一眼看完Rust / Node / Shell拿任何语言都能照这个思路做同样的东西。2. 工具选型与技术方案为什么是Python、JSON、命令行交互2.1 语言选型可读性优先别跟脚本较劲命令行待办事项应用业内常见的实现方案有三种Shell脚本、Python、Node.js / Go等编译型工具。我最终选了Python出发点很简单——这个工具的意义不在性能而在把功能逻辑写清楚。纯Shell脚本的优点是零依赖几乎每个系统都有但处理用户输入、特殊情况分支时Shell的语法可读性会逐渐失控。做简单的“加一行文字到文件”还行一旦涉及日期解析、优先级排序、状态变更脚本会越写越绕。Node.js和Go在性能上碾压Python但考虑这个工具的使用频率任务量顶天也就几百条区别在启动速度上只是一眨眼的事。Go需要编译别人想复现这个工具就得先装Go工具链Node则需要npm环境。Python则不一样——绝大多数系统预装了读代码的人不用额外折腾环境这就大大降低了这个工具的分发、修改门槛。我还考虑了Lua、Ruby这些冷门选项看起来轻量实际落地时周边库和资料都要少得多。对个人工具来说生态和可读性远比语言本身的优雅重要。所以最终方案定为Python 3 标准库不加任何第三方依赖。2.2 数据存储格式JSON比CSV、SQLite更适合这个场景任务数据存在哪里是我最先纠结的部分。候选列表里有CSV、JSON、SQLite哪一种更适合这个工具CSV文件结构扁平适合给Excel批量导入导出但它对嵌套结构支持极差——多条备注、子任务、标签这类结构非常难表达而且CSV没有明确的类型系统日期和布尔值进去都成了字符串每次读取都要手动处理。SQLite是嵌入式数据库功能强大支持索引、事务为几百条任务建一个数据库多少有些杀鸡用牛刀。而且SQLite的数据文件是二进制的不方便用户直接查看、修改、备份这违背了“数据掌握在自己手里”的设计初衷。JSON是无结构文件中的最优解对象数组表达任务的结构化字段非常自然缩进打印以后人能直接读改起来也可以用文本编辑器随意操作工具本身只是做了层封装。对于个人待办数量级几百条以内JSON的线性读取完全够用。存储方案的最终结构设计{ last_id: 5, tasks: [ { id: 1, content: 完成项目周报, priority: 1, due_date: 2025-02-28, done: false, created_at: 2025-02-20T10:30:00 } ] }last_id用于自增ID避免每次读取数组最后一个元素来确定下一个ID——删除任务后ID不会乱跳历史引用仍然有效。priority用1/2/3表示高/中/低排序时直接排序数字字段。done用布尔值区分完成状态。created_at是ISO格式时间字符串方便排序和展示。选JSON还有一个隐性优势——未来想给工具加Web界面或同步功能时Python自带的json模块可以直接对接API不用写额外的数据转换层。如果是SQLite方案数据到JSON表现层之间还得做一层映射。2.3 命令行交互方式子命令风格符合Unix传统命令行工具的交互设计业内主要有两种流派一种是单命令加参数比如todo --add 写文章另一种是子命令风格比如git commit -m 信息。我选择了后者原因很实在待办事项的核心动作有五个左右增、查、改、完成、删单命令加参数的模式让指令混在一起比如todo --list --done和todo --list --all含义容易混。子命令风格天然将动作拆分每个子命令只做一件确定的事结构更清晰。这与git、docker等主流工具保持了一致也方便以后扩展新子命令而不破坏现有指令。最终确定的命令集命令别名功能示例adda添加任务todo add 给客户回电话 -p 1listls查看任务列表todo list --donedoned标记任务完成todo done 3removerm删除任务todo rm 5clear无清空已完成任务todo clearhelp无查看所有命令帮助todo help为什么给常用命令设置别名因为在终端里快速输入时todo a 写文档比todo add 写文档少敲两个字母。这个体验差别在一天输入几十次时体感非常明显。这也是命令行工具和GUI的一大区别——命令行工具的“用户体验”很大程度取决于手部动作的频次。每个动作少按几次键效率就上来了。3. 核心代码实现从任务载体到命令分发一步步搭起来3.1 项目目录结构与基础模块划分好的命令行应用即便是个小工具也值得规整目录。我做项目时习惯从一开始就把“主程序”和“核心逻辑”分开而不是把所有代码写进一个文件里否则改一行代码就得翻阅全局变量。这个项目最终做成单文件todo.py因为逻辑确实足够简单但为了示范我把代码内的模块边界分得很清楚todo/ todo.py # 命令行入口与命令分发 storage.py # 数据读写与存储层 actions.py # 具体命令的业务逻辑 utils.py # 日期解析、终端输出格式化实际运行时代码量大约600行左右。这个拆分适合进一步扩展。当然如果你只想快速用把四个文件合并成一个也能跑关键要保证每个函数职责单一。3.2 存储层先把落盘逻辑做对命令行工具最核心的保障就是数据不丢。我先写存储模块。这里最容易被忽视的点是读写时要处理好任务文件不存在、内容为空、JSON损坏三种情况。import json import os import sys DATA_DIR os.path.expanduser(~/.todo) DATA_FILE os.path.join(DATA_DIR, tasks.json) DEFAULT_DATA {last_id: 0, tasks: []} def load_data(): if not os.path.exists(DATA_FILE): return json.loads(json.dumps(DEFAULT_DATA)) try: with open(DATA_FILE, r, encodingutf-8) as f: data json.load(f) except json.JSONDecodeError: backup_file DATA_FILE .bak os.rename(DATA_FILE, backup_file) print(f任务文件损坏原文件已备份至 {backup_file}) return json.loads(json.dumps(DEFAULT_DATA)) return data def save_data(data): os.makedirs(DATA_DIR, exist_okTrue) tmp_file DATA_FILE .tmp with open(tmp_file, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) os.replace(tmp_file, DATA_FILE)数据文件损坏的恢复策略我用了“自动备份”方案一旦检测到JSON解析失败立刻把原文件重命名成.bak再给用户一个新的空数据文件而不是直接覆盖。这个小细节帮我避免过惨痛的数据丢失——有一次电脑异常断电文件写了一半启动工具时直接崩掉如果当时没有备份逻辑全部任务记录就没了。写入时先用临时文件再os.replace保证写入的原子性这个做法在Linux下很关键直接打开原文件写入如果中途进程被中断文件就处于半截状态。而先写tasks.json.tmp再通过操作系统级别的rename操作替换旧文件就能确保要么旧文件完好要么新文件完整不会出现中间状态。3.3 命令分发与参数解析不用argparse我直接手写很多人第一反应是用argparse但对这个工具来说有点过重。我需要的是“子命令 少量参数”的解析手写只需要十行代码还可以完全控制错误信息。看一下核心分发逻辑import sys from actions import add_task, list_tasks, mark_done, remove_task, clear_done from utils import print_usage COMMANDS { add: add_task, a: add_task, list: list_tasks, ls: list_tasks, done: mark_done, d: mark_done, remove: remove_task, rm: remove_task, clear: clear_done, } def main(): if len(sys.argv) 2: print(输入 todo help 查看使用方法) return cmd sys.argv[1] args sys.argv[2:] if cmd in (help, -h, --help): print_usage() return handler COMMANDS.get(cmd) if not handler: print(f未知命令: {cmd}) print(输入 todo help 查看使用方法) sys.exit(1) handler(args) if __name__ __main__: main()这段代码的核心优势在于扩展性——以后想加一个stats命令统计一周完成任务数只需要在actions.py里实现stats_task(args)然后在COMMANDS字典里注册一下即可main逻辑一行不用改。这种“注册表模式”是命令行工具设计的惯用技巧跟click等框架的做法思想相通只是实现得更简洁。3.4 核心业务逻辑添加任务时的日期解析优先级处理add命令是使用频率最高的因此它的用户体验必须打磨到位。我的设计目标是让用户用自然语言写下任务同时可以附加优先级和截止日期但两者都不影响任务内容的灵活性。def add_task(args): if not args: print(错误任务内容不能为空) print(用法todo add 任务内容 [-p 1|2|3] [-d 2025-02-28]) sys.exit(1) priority 2 due_date None content_parts [] i 0 while i len(args): if args[i] -p and i 1 len(args): try: priority int(args[i1]) if priority not in (1, 2, 3): raise ValueError except ValueError: print(优先级必须是 1、2 或 3) sys.exit(1) i 2 elif args[i] -d and i 1 len(args): due_date args[i1] # 验证日期格式 try: datetime.strptime(due_date, %Y-%m-%d) except ValueError: print(f日期格式错误{due_date}应为 YYYY-MM-DD) sys.exit(1) i 2 else: content_parts.append(args[i]) i 1 content .join(content_parts).strip() if not content: print(错误任务内容不能为空) sys.exit(1) data load_data() data[last_id] 1 now datetime.now().isoformat() task { id: data[last_id], content: content, priority: priority, due_date: due_date, done: False, created_at: now } data[tasks].append(task) save_data(data) print(f已添加任务 #{task[id]}: {content} (优先级: {priority}))这里有个细节-p和-d的解析用的是while循环而非argparse的parse_known_args原因是我希望用户任务内容里出现-p开头的词也不会被“吃掉”。比如用户想写“购买-P10手机壳”如果用argparse会被错误解析成优先级参数。while循环逐个扫描遇到-p且后一个参数存在时才当作优先级否则归入内容部分。这个处理逻辑不算难但能在真实使用中减少很多挫败感。优先级默认设为2中等符合常规习惯——大多数任务是普通等级高优和低优是少数。如果默认1列表里全是最高优先级优先级标记就失去了区分意义。3.5 任务列表展示排序逻辑、终端宽度和视觉层级list命令直接决定这个工具的日常使用体验。我的目标是一眼扫过去就能分清“该做什么、什么先做”。列表排序规则先按完成状态分组未完成的排前、完成的排后未完成的再按优先级1高、2中、3低排序相同优先级下有截止日期的按日期升序无截止日期的排在日期任务之后。这个排序组合并不复杂但对决策效率的提升很高。def list_tasks(args): show_all --all in args or -a in args data load_data() tasks data[tasks] if not tasks: print(当前没有任务输入 todo add 任务 来添加) return pending [t for t in tasks if not t[done]] completed [t for t in tasks if t[done]] pending.sort(keylambda t: (t[priority], t[due_date] or 9999-99-99)) completed.sort(keylambda t: t[created_at], reverseTrue) if not show_all: print(f待办任务共 {len(pending)} 项) print(─ * 50) for t in pending: display_time t[due_date] or 无截止日期 print(f #{t[id]:4} [P{t[priority]}] {t[content]:30} 截止: {display_time}) if completed: print(f\n已完成 {len(completed)} 项使用 todo list --all 查看) else: print(f全部任务共 {len(tasks)} 项待办 {len(pending)}已完成 {len(completed)}) print(─ * 50) for t in pending: print(f #{t[id]:4} [P{t[priority]}] {t[content]:30} 截止: {t[due_date] or 无}) print( 已完成) for t in completed: print(f #{t[id]:4} [完成] {t[content]:30} 完成时间: {t[created_at]})在实际使用中还想让它更醒目可以让高优先级任务用红色显示普通任务用默认色完成的任务用灰色并加删除线。但要依赖颜色控制字符为此我们得判断当前终端是否支持颜色。简单做法是利用sys.stdout.isatty()检测是否是真正的终端是才输出ANSI转义码否则输出纯文本。这个检测对管道输出非常关键——当你在脚本里调用todo list | grep 周报时任务内容里混入颜色转义码会导致grep匹配不准。所以我的实现原则是交互终端显示颜色管道场景一律纯文本。3.6 完成、删除与清空不可逆操作的统一确认机制标记完成是高频操作删除则相对低频但它们是同一类“状态变更”操作。唯一需要警惕的是删除——一旦执行就找不回来所以我给remove命令设计了确认机制。def remove_task(args): if len(args) 1: print(用法todo remove 任务ID) sys.exit(1) try: task_id int(args[0]) except ValueError: print(f无效的任务ID{args[0]}) sys.exit(1) data load_data() for i, task in enumerate(data[tasks]): if task[id] task_id: confirm input(f确定要删除任务 #{task_id}{task[content]}吗[y/N] ) if confirm.lower() in (y, yes): del data[tasks][i] save_data(data) print(f已删除任务 #{task_id}) else: print(操作已取消) return print(f错误找不到任务 #{task_id})这里关于权限有个教训我在第一个版本里没有做确认直接删。结果某次手滑本想输入todo done 12标记12号任务完成却误输成todo remove 12任务彻底消失。从那以后所有不可逆的删除操作我都加了确认。相比之下done操作是可逆的我提供undo命令可以恢复完成状态所以它不需要确认保证高效率。清空已完成任务的clear命令因为可能一次删掉几十条同样加了确认。而且限制只能清除donetrue的任务给误操作留了一线余地。4. 实操过程复盘从零到能用包含每一步的执行方式4.1 实现顺序建议先跑通最小区块链再逐块优化如果是自己从零开始实现不建议按我上面代码的顺序来写否则会“抄都抄不会”。更好的顺序是第一步约10分钟先什么都不做只实现add和list。把任务存进去、能列出来这个工具就算有了生命。不要一开始就设计完美的日期解析和颜色显示那会让第一版迟迟无法交付。第二步约15分钟补上done和remove。这两个命令的代码与add类似都是读取数据、找到任务、修改字段、保存数据。此时核心CRUD全部完成工具已经能覆盖日常80%的使用。第三步约20分钟加入排序、日期、优先级、完成状态等展示层面的增幅再补clear和help。这一步是决定工具好用的关键也是和普通demo拉开差距的地方。第四步可选加入颜色输出、别名支持、undo命令、bash补全脚本。这些属于锦上添花但做起来很有乐趣。我自己在实际操作中花了大约一个完整的下午前两步比较快第三步花了大功夫因为排序和展示逻辑需要在真实数据上反复试DEBUG的体验非常直观——在终端里敲一条命令立刻看输出结果这个反馈周期比任何GUI调试都快。这也是命令行工具的另一个隐性优点开发和调优的效率极高。4.2 整个运行流程的现场实录为了让读者感受真实使用我从终端里截取了一段操作记录完整流程如下$ todo add 给客户发报价单 -p 1 已添加任务 #1: 给客户发报价单 (优先级: 1) $ todo add 买零食 -p 3 已添加任务 #2: 买零食 (优先级: 3) $ todo add 整理报销单据 -d 2025-03-01 已添加任务 #3: 整理报销单据 (优先级: 2) $ todo add 预约牙医 已添加任务 #4: 预约牙医 (优先级: 2) $ todo ls 待办任务共 4 项 ──────────────────────────────────── #1 [P1] 给客户发报价单 截止: 无截止日期 #3 [P2] 整理报销单据 截止: 2025-03-01 #4 [P2] 预约牙医 截止: 无截止日期 #2 [P3] 买零食 截止: 无截止日期 $ todo done 1 已完成任务 #1: 给客户发报价单 $ todo ls 待办任务共 3 项 ──────────────────────────────────── #3 [P2] 整理报销单据 截止: 2025-03-01 #4 [P2] 预约牙医 截止: 无截止日期 #2 [P3] 买零食 截止: 无截止日期 已完成 1 项使用 todo list --all 查看 $ todo clear 确定要清除所有已完成任务吗[y/N] y 已清除 1 项已完成任务 $ todo ls --all 全部任务共 3 项待办 3已完成 0 ──────────────────────────────────── #3 [P2] 整理报销单据 截止: 2025-03-01 #4 [P2] 预约牙医 截止: 无截止日期 #2 [P3] 买零食 截止: 无截止日期注意几个细节list展示未完成任务时相同优先级的按截止日期的先后排#3在#4前done 1之后list默认不再展示已经完成的任务clear只清除已完成未完成的保留完整。这整段操作大约用了30秒比任何GUI应用都要直接。4.3 将这个工具与shell工作流结合让它变成效率放大器命令行待办事项最被低估的能力是它的可组合性。它不像GUI工具那样自成一统而是可以自由嵌入到已有的shell工作流里。举几个我日常实际在用的例子这些场景会彻底改变你对待办事项工具的看法。场景一启动终端时自动显示今日待办。在~/.bashrc或~/.zshrc末尾加上echo 今日待办 todo ls --all 2/dev/null | head -20每次打开终端第一眼看到的就是待办清单提醒自己今天最重要的事是什么而无需打开任何额外App。场景二正则过滤任务配合管道统计# 查看所有含客户的任务 todo ls --all | grep 客户 # 统计有多少项高优先级的活 todo ls | grep -c \[P1\]这对纯命令行使用者来说是灵魂功能。GUI里要筛选一个关键词得打开搜索框命令行里一条管道就能搞定还能跟awk、sed等任意处理工具链式组合。场景三用cron定时提醒。不依赖手机推送用系统自带的定时任务30 9 * * * todo ls | grep \[P1\] | mail -s 今天的高优先级任务 youremail.com每天早上9:30自动把高优先级任务发到邮箱比任何App的推送都稳定可靠。不用担心服务商关停只要电脑开着就跑。场景四从Markdown批量导入任务。我习惯在写周报时用markdown列计划然后一行命令把计划转成待办cat plan.md | grep ^- \[ \] | sed s/^- \[ \] // | while read task; do todo add $task; done实际上自动化脚本的价值远超想象。一次性的任务汇总、批量添加、备份恢复都可以用几行shell脚本搞定这是GUI永远做不到的。5. 常见问题与排查技巧实录开发和使用这个工具的过程中我遇到过不少问题有的是兼容性坑有的是逻辑漏洞有的是真实的业务冲突。下面把这些记录下来按“能坑死你”的顺序排列帮你节省踩坑时间。5.1 任务内容包含特殊字符导致Shell解析出错这是新手最常遇到、也最困惑的坑。运行todo add 买牛奶注意买脱脂的 买面包时命令会异常有可能直接卡住因为是Shell的后台执行符它把命令行拆成了两个部分。排查思路Shell在把命令交给程序之前会先做词法分析。被空格分开的、|、;、都会被当作控制字符处理。所以所有命令行工具在传参时都必须遵守同一规则内容有空格必须用引号包裹有特殊字符优先用单引号。正确做法todo add 买牛奶注意买脱脂的 买面包单引号内所有字符都会原样传给程序不会被Shell解释。双引号也有一定保护作用但内部$、、\依然会被展开所以全文字段尽量用单引号。提示如果你经常需要记录带特殊符号的任务可以在add命令的解析逻辑里做一层“安全提示”当检测到任务内容包含从参数拼接后的、|等字符时打印一行提醒“内容包含特殊字符建议用单引号包裹任务内容”这也是提升工具亲和力的一个小细节。5.2 JSON文件损坏断电、误编辑、编码问题我前面在存储层写了自动备份逻辑就是应对这个问题。它的触发场景有三个程序写入过程中进程被暴力杀死如断电、kill -9文件处于半写入状态。用户用编辑器手动改了tasks.json但不小心漏了一个括号。多次切换工具版本数据结构不一致导致读取异常例如旧版本没有last_id字段。排查思路首先看~/.todo/tasks.json是否存在其次用python3 -m json.tool ~/.todo/tasks.json手动验证JSON格式。如果工具已经跑起来了直接看它的提示——它会自动备份损坏文件并重建。规避建议大改动之前先手动复制一份备份cp ~/.todo/tasks.json ~/.todo/tasks.json.bak。同步到网盘或者git仓库是更好的方案——我甚至为它专门建了一个私有git仓库每次改动后自动commit历史版本一目了然。5.3 Windows与Linux的兼容性差异实现时我用到了os.replace在Windows和Linux下都能用。差别在于路径分隔符和换行符Windows下默认路径分隔符是\Python的os.path.join会自动处理。Windows的open默认读写是文本模式写入时会自动把\n转成\r\n而JSON读取时不影响。但如果程序在Windows和Linux之间通过网盘同步tasks.json跨平台读取时要注意文件可能带有BOM头utf-8-sig解决方式是打开文件时用encodingutf-8-sig它能兼容带BOM和不带BOM的两种格式。我的统一方案所有文件读写操作都显式加encodingutf-8且进行数据操作时不依赖路径默认分隔符。这样在Windows的PowerShell里和Linux终端里跑完全一样兼容性没有问题。5.4 ID永远只增不减会不会太大当我删除了大量任务后list里显示的ID是 1、3、25、78 这样跳跃的。有人会问ID一直增长会不会爆炸能不能复用分配答案ID就是不应该复用。它不仅是数组的索引更是任务在时间轴上的“身份证”。如果今天删掉了任务5明天新增任务又变成5那你之前任何地方引用“任务5”的历史记录都会错乱。所以我的实现里ID用last_id自增删除不回收任务ID永远不重复。这个设计从底层避免了一类“引用指向错误任务”的bug。不过确实有个副作用——list展示时如果ID跨度过大原本的“#1”下一行变成“#78”对齐效果会差。这个可以用格式化控制比如#{id:4}左对齐固定占位。等ID真的大到8位数时那至少积累了千万条任务对个人使用场景来说是不可能的。5.5 颜色输出时终端兼容性和管道问题给任务列表加上颜色是我做完基础功能后第一个想加的“视觉优化”。但颜色控制符ANSI escape code在不同终端上表现不一样而且一旦输出被重定向到文件或管道就会留下乱码。排查与解决方案用sys.stdout.isatty()判断当前标准输出是否是真正的终端。比如import sys def supports_color(): if not hasattr(sys.stdout, isatty): return False if not sys.stdout.isatty(): return False return True在list_tasks里如果是终端则输出带颜色的格式化字符串否则输出纯文本。这样保住了可脚本化的能力又满足了交互时的视觉需求。搞定了这个加颜色就不再是风险而是纯增益。常用颜色码ANSI标准不需要第三方库效果代码说明红色\033[31m高优先级任务绿色\033[32m已完成项目恢复提示黄色\033[33m截止日期接近的警告灰色删除线\033[90m\033[9m已完成任务重置\033[0m恢复默认颜色5.6 待办事项类的“时间陷阱”过期任务如何展现实际用了一个月后我发现一个真实业务问题——任务如果有截止日期过期了怎么办。最初的版本只管按日期排序过期任务和普通任务混在一起。后来我用一个优先级叠加逻辑处理过期任务自动视为最高优先级并在展示时加上“已过期”的警示标记。import datetime def get_effective_priority(task): if task[due_date]: due datetime.datetime.strptime(task[due_date], %Y-%m-%d).date() if due datetime.date.today(): return 0 # 已过期优先级最高 return task[priority]在排序时用get_effective_priority代替priority字段展示时如果任务过期会在截止日期位置加上“已过期N天”的说明。这个改动虽然只多了5行代码但整个工具的实用性提升了一个档次——过期的任务再也藏不住每天开工第一眼看列表就知道哪些事已经措手不及。5.7 别名映射的坑优先级参数与内容混在一起前面代码里提到用while循环手动解析参数就是为了防止-p开头的单词混入任务内容导致误解析。实际用下来还有一个更隐蔽的坑用户可能这么输入todo add -p 写代码本意是想把“写代码”当作任务但按解析规则它被识别成“优先级参数-p值为写”然后“代码”变成内容最终结果为内容代码, 优先级写非法。修正方案当确认-p的取值不是数字1/2/3时把它和后续参数都当作内容的一部分处理而不是报错。或者把“合法优先级参数后面必须紧跟数字”的检查前置一旦发现不是数字立即把-p塞回内容列表。if args[i] -p and i 1 len(args) and args[i1] in (1, 2, 3): priority int(args[i1]) i 2 else: content_parts.append(args[i]) i 1这样-p再也不会和内容抢位置虽然不能让“写代码”变成优先级参数但至少不会丢数据。6. 进阶扩展这个工具还能长出哪些能力做完基础版本工具完全可以满足常用需求了。但如果你和我一样有“折腾病”下面这些扩展方向都很值得尝试而且每个都可以完全基于当前的数据格式往后做。6.1 增加撤销能力undo机制我第一版做的done操作是不可逆的后来补了一个undo命令执行方式很简单——给done的任务重新把done设为false即可。但因为误标记的不一定是最近一条任务所以undo不接受done命令的历史记录而是按IDtodo undo 12 # 把12号任务重新标记为未完成这个命令的核心价值是降低误操作成本。比如不小心在todo done 1时本想标记“老板的任务”却手滑标完了“日程安排”一条undo 1就能恢复。更进一步可以用collections.deque记录最近20条操作日志实现真正的“撤销上一步”。但考虑到数据文件是独立的我保留了这个念头未实现完整版本因为从语义上“撤销”对任务数据的修改是危险的——如果你撤销的是删除数据已经没了只能靠备份恢复。6.2 对接第三方服务CalDAV同步或git仓库备份数据是JSON格式天然的接口协议。未来想同步手机和电脑任何一种方式都可以把~/.todo/tasks.json放进坚果云、Dropbox等网盘目录靠网盘自动同步。用git管理每次命令结尾自动commit这样电脑本地有全量历史版本。写一个简单的REST API服务端让多个机器都能访问同一个JSON文件。我在第二步git方案上做过尝试实现并不困难。在save_data中保存完JSON后通过subprocess调用git add、git commit。但这里有个性能问题——每条命令都触发一次git操作会稍慢大约0.2-0.5秒对命令行交互来说体感还能接受。不过个人使用频率不高时手动在需要时执行一次备份就足够了。6.3 增加统计功能周报自动生成命令行工具都是最强统计工具。给工具加一个stats子命令统计当前任务数量、已完成数量、今日完成数量和逾期任务数然后输出成Markdown格式可以直接粘到周报里。这个功能我用Python的datetime模块写了100行左右效果很好——每周五发周报时todo stats一键出数。6.4 终端用户界面增强引入fzf交互选择如果你像我一样有时不想记ID可以配合fzf这个终端模糊查找工具使用。比如todo ls | fzf可以模糊搜索并选择任务拿到ID后再执行todo done ID。这个组合等于是给命令行工具加了一个“半图形化”的选择界面但底层依然是纯文本管道交互不破坏可脚本性。这种“弱界面、强组合”的思路我认为才是命令行工具的正确演进方向——不需要自己造界面靠Unix哲学的组合能力借力已有的终端生态。7. 我的使用心得与最终建议7.1 坚持用了三个月它改变了我的工作习惯工具本身很小但真的改变了我的习惯。以前手机上的待办App每次新增任务都要点亮屏幕 → 解锁 → 点开App → 输入 → 保存一步都不能跳过。现在我在终端里输入todo a 回复邮件只需要几秒而且不需要中断当前工作流。更关键的是——它和我在终端里的其他操作处于同一上下文。写代码的时候遇到“需要给同事发文件”这个临时事项直接敲一行命令记录不用切换到手机。三个月用下来我的待办列表养成一个习惯每天早上到工位第一件事todo ls只看高优先级和已过期的一天结束后todo done ID把完成的划掉。这套流程没有神奇的时间管理理论但它足够轻、足够快坚持的门槛反而低了。7.2 从基础到高级的三个建议如果你是完全新手我的建议是别一上来就复刻我的所有设计先从add和list开始然后根据自己使用中觉得“缺了什么”的特性去补。一个工具只有是你按自己需求一点一点长出来的才会真正好用。如果你想改造这个工具优先考虑给add命令加一个“所属项目/分类”字段。任务多了之后刷的欲望更强了用项目维度筛选比任何优先级逻辑都管用。做法很简单在JSON任务对象里加一个project字段然后list支持--project 名称过滤即可。如果你想生产化把我的Python脚本打包成单文件二进制用pyinstaller或nuitka然后放到PATH里。这样其他机器不必安装Python也能跑。另外把帮助文档做成man手册风格输出用--help触发挂到man todo的路径下。7.3 这个项目给我的技术启发这个项目虽然小但我它提醒我一行工具的最终形态不是由技术选型决定的而是由使用方式决定的。GUI有App的优雅和便利命令行有脚本化的自由和组合性两者没有高下之分只有匹配不匹配。对我来说一个能被我塞进cron、能被我grep、能被版本控制工具管理的待办事项应用比一个界面华丽但封闭的应用要操控得顺手得多。这也是命令行应用到今天依然生机勃勃的根本原因——它永远为自动化保留了一扇门。