资讯动态

用Python标准库从零构建命令行待办事项工具

发布时间:2026/10/13 21:24:44 来源:尧图企业网站定制
命令行工具这种东西一旦用习惯了就很难再回到图形界面。图形界面适合复杂交互但真正高频、小粒度的操作往往还是终端里敲一条命令来得痛快。待办事项管理就是特别典型的场景——打开一个App、找到列表、点新建、输入内容这中间至少好几秒而在终端里敲一句todo add 写周报到回车一秒钟都不到。这篇文章会完整拆解一个基于命令行的待办事项应用从设计、实现到扩展的整个流程包括需求怎么拆、存储格式怎么选、命令怎么设计、代码怎么写、踩坑怎么排查。这个项目不需要任何第三方依赖只用Python标准库就能跑通非常适合想做个完整练手项目的中级开发者也适合想优化自己日常效率工具的终端爱好者参考。全部内容基于我实际开发这个工具时的真实过程和通用实践经验整理你可以直接照着做改一改就能变成自己的东西。1. 需求拆解一个待办应用到底该做什么1.1 先搞清楚目标用户和使用场景在动手写代码之前我先花了半天时间想清楚这个工具到底为谁服务。表面上看待办应用谁都能用但如果什么都想要最后只能是什么都做不好。我给自己设定的目标用户是长时间驻扎在终端里的开发者以及那些不喜欢为了一件小事反复切换窗口的效率爱好者。这类用户的使用场景有三个共同点。第一个场景是“随手记”可能正在写代码脑子里突然蹦出一个临时任务比如“给脚本补个参数校验”此时最自然的行为就是切到终端敲一条命令而不是打开一个App去点好几下。第二个场景是“快速回顾”想知道今天还剩哪些事没做一条list命令就能看到全部而不是打开日历或者便签翻来翻去。第三个场景是“可脚本化”比如想把待办任务接入自己的自动化流程里或者想在每天早上一打开终端就看到任务清单命令行工具天生就能和Shell脚本无缝配合。明确了这三个场景我立刻确定了一个设计基调这个工具不是要替代那些功能完整的大型待办软件而是要在“记录”和“查看”这两个核心动作上做到极致的轻和快。功能可以少但核心路径一定不能卡顿。1.2 功能边界与交互设计原则我把最小可用版本的核心功能收敛成了五个子命令add新增一条任务支持优先级和标签list查看任务列表支持多维度过滤done把任务标记为完成undo把误标记的任务恢复为未完成rm彻底删除一条任务没有一上来就做提醒、循环任务、多列表分组这些功能。提醒需要常驻进程循环任务涉及复杂的日期逻辑多列表分组会让命令参数急剧膨胀。这些功能会让第一个版本的复杂度翻好几倍但对“快速记录待办”这个核心目标的贡献却很有限。我宁可先做到这个范围里每一件事都做得很顺手再考虑后续扩展。交互设计上我给自己定了四条原则。第一单一职责。每个命令只做一件事add只管添加list只管展示不做任何隐藏的联动行为。第二短参数记忆成本低。常用参数只用一两个字母-p代表优先级-t代表标签-d代表只看已完成让高频操作不需要翻阅文档。第三输出即反馈。每执行一个操作终端里立刻给出直观的反馈比如删除一条任务后会显示“已删除任务 #3写周报”而不是默默执行完就没了。第四数据可移植。所有的任务数据存储在单个JSON文件中意味着可以随意用文本编辑器查看、用Git管理版本、用任何脚本处理绝不锁死在专有的数据库格式里。1.3 为什么“快”是这个工具的命根子整个设计过程中我最看重的一个指标就是“一条命令在平均一秒钟之内完成”。为什么这个指标如此重要因为待办工具的使用场景通常出现在工作流的“间隙”里一旦操作耗时超过两三秒用户就会觉得打断思路下次宁可用便签纸也不会再打开这个工具。这和IDE里打开某个配置文件的体验是同一个道理。为了做到快有两点需要在设计阶段就确定。第一启动时间必须短。Python脚本的启动开销本身就比编译型语言大一些所以这个工具在导入模块时不能有任何额外的耗时操作比如启动时就去检查更新或者加载大配置。第二数据准备必须轻。任务文件再怎么存也不会超过几百KB一次性全部加载进内存完全没问题根本不需要引入ORM那种重量级的数据访问层。2. 技术选型为什么是Python加JSON这种组合2.1 编程语言的选择与对比做命令行工具实际上可选的方案不少。我在这三个方案之间做过对比Python、Node.js、Go。方案优势短板适用场景Python标准库自带json、argparse等模块零第三方依赖就能跑通全流程跨平台代码可读性强启动速度略慢分发时需要在目标机器装有Python个人效率工具、快速原型、脚本类工具Node.js自带异步能力npm生态丰富每个小工具引入node_modules略显笨重启动速度不比Python快生态依赖较多的工具、前端开发者自用工具Go编译成单一二进制启动快如闪电分发极其简单开发迭代慢一些标准库里的JSON处理写起来比Python啰嗦需要分发给大量用户的终端工具、对性能有要求的工具最终我选了Python理由有两点。第一我准备实现的这些功能恰好都在Python标准库的覆盖范围内用json处理序列化、用argparse处理命令行参数解析、用pathlib处理跨平台路径完全不需要装任何第三方库就能实现。第二写Python脚本时迭代速度非常快改完立刻就能跑来看效果。对一个我打算长期使用的效率工具来说后续想加点功能改动成本也最低。2.2 存储格式JSON文件为什么够用数据存储方案是命令行待办应用里比较核心的技术决策。常见的方案有三种纯文本文件、JSON文件、SQLite数据库。纯文本文件的优点是人和程序都能轻松读写比如一行一条任务用分隔符切分字段。但缺点也明显任务会包含多行内容的场景很难处理字段多起来之后解析容易出各种意外情况。SQLite数据库则完全是另一个方向功能极其完善能应付复杂查询和高并发但对一个个人终端工具来说有点大炮打蚊子的感觉还需要处理数据库驱动依赖文件也做不到用文本编辑器直接查看。JSON文件恰好落在两者的中间。它的格式是标准化的任何语言都有现成解析器Python标准库里的json模块就能用。JSON文件本质是纯文本直接用cat或者任何编辑器就能查看内容排查数据问题时非常方便。JSON天然支持嵌套结构后续如果想给任务添加子任务或者备注字段不需要改动存储结构就能扩展。可能有人会担心性能问题。我们来估算一下一个重度使用者一天新增10条任务已经很多了一年下来也就两千多条每条任务序列化之后大约200字节整体文件大小不到1MB。对于这种量级JSON文件加载和保存的时间都在毫秒级别性能完全不需要考虑。2.3 数据存放位置与跨平台路径设计任务数据不能和脚本存放在同一个目录里。一方面脚本目录可能被清理或者被版本管理工具接管另一方面不同平台有各自约定的用户数据位置。更合理的方案是存放在用户主目录下的一个隐藏文件夹里例如~/.todo/tasks.jsonWindows下对应的路径是%USERPROFILE%\.todo\tasks.json。用Python的pathlib处理这块非常顺手。核心逻辑就几行from pathlib import Path home Path.home() todo_dir home / .todo data_file todo_dir / tasks.json # 确保目录存在不存在则创建 todo_dir.mkdir(exist_okTrue)Path.home()会自动适配不同操作系统返回当前用户的主目录完全不需要自己判断系统类型来拼接路径。这里有个细节值得留意如果用户想用另一个位置存放数据应该允许通过环境变量覆盖默认路径。所以我额外加了一行逻辑当设置了TODO_FILE环境变量时就使用环境变量指定的路径否则走默认路径。这样既保证了默认行为的一致性又给高级用户留了自定义空间。3. 核心功能模块与完整实现3.1 任务数据模型设计数据模型是整个应用的地基任何类型的任务、参数和业务逻辑都建立在这个模型之上。我设计的任务结构是这样的{ id: 1, content: 给脚本补参数校验, created_at: 2024-10-01T09:30:00, completed: false, completed_at: null, priority: 1, tags: [工作, 运维] }每个字段都有明确的作用我逐一说明。id是任务的唯一标识所有对任务的引用都通过这个数字完成比如done 3表示把第3号任务标记完成。content是任务本身的内容也是用户看到的最核心信息。created_at记录了创建时间格式采用ISO 8601标准这种格式既方便代码解析也方便人阅读而且按字典序排序就等于按时间排序对过滤和排序逻辑非常有帮助。completed用布尔值表示任务是否完成。completed_at存放完成时间初始为null标记完成后才写入。可能有读者会问已经有了布尔字段为什么还要时间字段因为实际使用中需要知道某件事是什么时候做完的比如月底回顾的时候很有用。priority用整数表示优先级数字越小优先级越高我规定1为最高3为最低这样按priority和created_at两个字段做排序非常直观。tags是一个字符串数组用于给任务打上分类标签比如“工作”“个人”“学习”这样list -t就能按标签筛选。数据模型设计过程中我曾经考虑过是否需要为每个任务都生成一个UUID字符串而不是自增整数。后来放弃了原因是自增整数对人类友好得多一条任务显示为“任务 #3”比显示一串36位十六进制字符串直观得多而且个人工具单文件存储不存在并发冲突的问题自增整数已经完全够用。3.2 存储层读写函数的关键实现存储层的职责非常简单粗暴把所有任务从JSON文件读到内存以及把内存里的任务写回JSON文件。但两个细节让这段代码变得不那么简单。第一个细节是写入的原子性。如果直接对tasks.json执行写操作而写入过程中程序崩溃或者机器断电文件就可能写了一半变成损坏数据。更稳妥的写法是先写入一个临时文件写入成功后原子性地把临时文件替换成目标文件。Python标准库中os.replace()就是干这个的它在Unix和Windows上都能保证替换操作是原子的。第二个细节是中文的可读性。Python的json.dump()默认会把非ASCII字符转义成\uXXXX序列这样虽然不丢数据但直接用文本编辑器打开文件时看到全是一堆转义符非常影响调试体验。加上ensure_asciiFalse参数以后中文字符就可以原样输出。存储层的核心代码简洁直观import json import os def load_tasks(data_file): if not data_file.exists(): return [] with open(data_file, r, encodingutf-8) as f: return json.load(f) def save_tasks(data_file, tasks): tmp_file data_file.with_suffix(.json.tmp) with open(tmp_file, w, encodingutf-8) as f: json.dump(tasks, f, ensure_asciiFalse, indent4) os.replace(tmp_file, data_file)这里有个细节临时文件名用data_file.with_suffix(.json.tmp)得到这样临时文件和正式文件在同一个目录下保证了os.replace()能在同一个文件系统内完成操作。如果临时文件存放在别的盘符跨文件系统的移动有可能是非原子的那就失去了原子替换的意义。3.3 命令层用argparse搭建子命令体系命令行的解析方案其实也可以考虑用标准库的argparse或者第三方的click、typer。最终我选argparse因为它不需要额外安装依赖核心功能也完全够用还能自动生成命令行帮助文本和参数校验。argparse通过subparsers支持子命令结构这个机制正好匹配我们的设计需求。下面是主要代码逻辑import argparse def build_parser(): parser argparse.ArgumentParser( progtodo, description一个轻量的命令行待办事项管理工具 ) subparsers parser.add_subparsers(destcommand, requiredTrue) add_parser subparsers.add_parser(add, help新增任务) add_parser.add_argument(content, help任务内容) add_parser.add_argument(-p, --priority, typeint, choices[1, 2, 3], default2) add_parser.add_argument(-t, --tags, nargs*, default[]) list_parser subparsers.add_parser(list, help列出任务) list_parser.add_argument(-d, --done, actionstore_true, help只看已完成) list_parser.add_argument(-u, --undone, actionstore_true, help只看未完成) list_parser.add_argument(-t, --tag, help按标签过滤) list_parser.add_argument(-p, --priority, typeint, help按优先级过滤) done_parser subparsers.add_parser(done, help标记任务完成) done_parser.add_argument(task_id, typeint, help任务ID) undo_parser subparsers.add_parser(undo, help恢复为未完成) undo_parser.add_argument(task_id, typeint, help任务ID) rm_parser subparsers.add_parser(rm, help删除任务) rm_parser.add_argument(task_id, typeint, help任务ID) return parseradd的-t参数用nargs*表示可以接收零个或多个值这样todo add 买菜 -t 生活就能同时打多个标签比如-t 生活 采购。done和rm都接收一个整数参数作为任务ID这点至关重要用户敲出的ID是紧接着上一次查看列表时看到的编号如果数据在两次操作之间发生了变化用户就可能会对错号。为避免误操作我会在执行前再次确认任务是否存在并在删除时要求用户二次确认。3.4 输出层不依赖第三方库的终端格式化终端输出是命令行工具的脸面。最简单的输出方式就是逐行打印任务信息但那样可读性太差。我做了一个不需要引入任何第三方库的格式化方案手动计算列宽对齐并通过ANSI颜色转义区分任务状态。ANSI颜色转义码的原理很简单终端会识别以\x1b[开头的特殊字符序列后续的数字表示颜色和样式。比如\x1b[32m表示绿色前景\x1b[0m表示重置为默认颜色。这样在终端中显示的效果是已完成的任务变成灰色未完成的高优先级任务用黄色标记整个列表扫一眼就知道状态。列表输出的格式大概长这样ID 内容 优先级 标签 状态 3 修复登录接口的边界条件 1 工作 未完成 2 取快递 3 生活 未完成 1 写季度总结文档 2 工作, 汇报 已完成对齐的核心算法是先扫一遍所有任务把每列的最大内容宽度算出来然后对每一行用str.ljust(width)做左对齐补空格。这样做的效果是简单实用不需要引入像tabulate这样的第三方库而且对中文等宽字符也够用。有一点必须提醒在Windows自带的旧版控制台里ANSI转义是不生效的需要开启VT处理模式在Windows Terminal和所有主流现代终端模拟器里则没有这个问题。4. 从零到可用的实操过程步步记录4.1 项目目录结构与初始化动手之前先把项目目录结构规划好这能避免后面代码纠缠成一团。一个命令行工具按职责拆分成几个模块比把所有代码塞进一个文件更利于后续维护。我采用了这个结构todo/ ├── todo.py # 入口函数装配各模块 ├── storage.py # 数据加载与保存 ├── cli.py # 命令行参数解析 ├── models.py # 任务数据结构与过滤逻辑 └── tests/ └── test_todo.py # 单元测试todo.py是唯一直接面向调用方的入口它负责读取解析出的命令把参数分发给对应模块处理再把结果交给输出层展示。models.py里定义了任务字典的默认结构和一系列处理函数例如创建任务、过滤任务、排序任务。storage.py只负责读写JSON。cli.py只负责解析命令行参数不包含任何业务逻辑。分层的主要价值在于以后想加新功能时改动范围能控制在一个模块内。4.2 模块实现的完整步骤第一步实现models.py任务创建和过滤的核心逻辑都在这里from datetime import datetime def new_task(tid, content, priority, tags): return { id: tid, content: content, created_at: datetime.now().isoformat(timespecseconds), completed: False, completed_at: None, priority: priority, tags: tags, } def generate_id(tasks): if not tasks: return 1 return max(t[id] for t in tasks) 1注意datetime.now().isoformat()取出来的是带微秒的完整时间字符串我用timespecseconds截断到秒级既能保证排序精度也不会让文件里的数据显得太啰嗦。第二步实现todo.py里的handle_add函数def handle_add(args): tasks load_tasks(data_file) tid generate_id(tasks) tasks.append(new_task(tid, args.content, args.priority, args.tags)) save_tasks(data_file, tasks) print(f已添加任务 #{tid}: {args.content})这个流程的先后顺序是固定的先加载、再生成新任务、追加、保存、反馈。任何一步骤的顺序乱了都可能导致问题。尤其要强调的是必须先load_tasks再generate_id如果跳过了当前数据直接生成ID很可能就生成一个重复ID把文件里的数据覆盖掉。第三步实现过滤和排序逻辑。列表展示时用户最常用的做法是先看未完成的、再看高优先级的。排序规则我用两层级联第一层按完成状态排序未完成的排前面第二层在同一状态内按优先级和创建时间排序优先级越高、创建时间越早排在越靠前。def filter_tasks(tasks, done, undone, tag, priority): result [] for t in tasks: if done and not t[completed]: continue if undone and t[completed]: continue if tag and tag not in t[tags]: continue if priority and t[priority] ! priority: continue result.append(t) return result def sort_tasks(tasks): return sorted( tasks, keylambda t: (t[completed], t[priority], t[created_at]) )排序时把completed布尔值作为排序主键是Python里很妙的一个写法False在比较时等于0True等于1所以未完成的任务自然就排在前面了。4.3 让todo命令在系统任意位置可用现在代码写完了剩下的工作是把启动命令变得顺手。每次都输入python3 ~/todo/todo.py add 任务显然太啰嗦。我做了两步。第一步在todo.py文件第一行加上Shebang#!/usr/bin/env python3然后给文件添加执行权限chmod x ~/todo/todo.py第二步建立一个软链接把这个脚本链接到PATH中的某个目录比如Linux和macOS上常见的~/.local/binmkdir -p ~/.local/bin ln -s ~/todo/todo.py ~/.local/bin/todo做完这一步以后在终端的任意目录下都可以直接敲todo add 写周报来运行了。Windows用户可以在同目录下建立一个todo.bat批处理文件里面写一行python C:\path\to\todo.py %*同样能达到效果。注意软链接的目标文件需要有执行权限否则系统会提示Permission denied。4.4 一整天任务管理流程演示代码和安装都搞定我用一个模拟的真实使用流程来结束这个模块。早晨打开终端敲$ todo add 修复登录接口空指针问题 -p 1 -t 工作 -t 紧急 已添加任务 #5: 修复登录接口空指针问题 $ todo add 预约下周的牙医 -p 3 -t 生活 已添加任务 #6: 预约下周的牙医 $ todo list -u ID 内容 优先级 标签 状态 5 修复登录接口空指针问题 1 工作, 紧急 未完成 3 更新项目文档 2 工作 未完成 6 预约下周的牙医 3 生活 未完成下午修完了那个接口问题标记完成$ todo done 5 任务 #5 已完成: 修复登录接口空指针问题 $ todo list -d ID 内容 优先级 标签 状态 5 修复登录接口空指针问题 1 工作, 紧急 已完成 1 写季度总结文档 2 工作, 汇报 已完成一条命令就能知道今天做了什么这个体验在终端里真的是行云流水。整套操作从打开终端到看到结果不会超过五秒。5. 常见问题与排查技巧速查5.1 中文内容显示乱码或者间距不齐中文乱码是命令行工具比较容易碰到的问题。如果发现任务内容里的中文显示成\uXXXX转义序列说明写入JSON时没有设置ensure_asciiFalse。如果发现中文显示为乱码先检测一下终端编码Linux和macOS通常是UTF-8Windows较老的控制台可能是GBK这时可以在脚本入口处加一行import sys sys.stdout.reconfigure(encodingutf-8)上面这行代码能保证Python的输出始终使用UTF-8至于终端能不能正确渲染取决于终端自身的编码设置。5.2 任务ID跳号或者误操作问题这个工具删除任务后ID是持续递增的可能出现ID从1到10然后是12、13的情况中间的11永远空着。一开始我觉得跳号很难看想做成“删除后补号”后来想通了跳号是特性而不是缺陷。如果删掉11号任务就把12号改成11号会带来两个新问题一是已经保存到某个笔记里的引用全部失效二是并发或者恢复备份时数据可能不一致。保持ID只增不减是消息队列和数据库设计里极其通用的原则。发生了误删把备份文件拷回来就行ID不会因为前后两次操作而错乱。5.3 数据文件损坏时的抢救办法我在早期版本里没有处理写入原子性问题某次测试时故意拔电模拟崩溃结果tasks.json变成了半截JSON文件整个工具直接启动失败。原因就是写入过程中断文件被写坏了。解决方案就是前面提到的临时文件加os.replace()写入让写入操作不可分割。但这只能防未来已经损坏的文件怎么救JSON文件有一个特点如果损坏只发生在文件末尾前面大部分内容很可能还是完整的。我的恢复策略是写一个一次性脚本读取文件用json.JSONDecoder.raw_decode()尝试从文件头开始尽量解析出有效数据。如果文件确实完全损坏了那就找备份。我后来在保存时加了一个简单的备份逻辑每次覆盖前把旧文件改名为tasks.json.bak这样最多只会丢一次修改的变更。5.4 Shell引号导致的参数传递问题这是每个命令行工具使用者都会遇到的坑。当任务内容里包含空格时如果不加引号Shell会把内容按空格拆分成多个参数导致严重的参数解析错误。比如# 错误写法Shell会拆成3个参数传给todo todo add 修复登录接口空指针问题 # 正确写法用引号包住整个内容 todo add 修复登录接口空指针问题典型的坑是任务内容本身包含双引号比如todo add 她说今天有空这会让Shell的解析彻底乱套。我的解决方案有两条第一在帮助文档里明确建议优先使用单引号包裹含特殊字符的内容todo add 她说今天有空这种写法就没有任何问题第二在代码层给出优雅的错误提示当参数数量不符合预期时提示用户检查引号而不是输出一段让新手摸不着头脑的报错。5.5 平台差异Windows与Unix的路径和符号链接Path.home()已经帮我们处理了用户目录的差异但在安装环节还是会有区别。在Unix环境下用软链接就能让todo命令全局可用Windows下则需要建.bat包装脚本。另外Windows的文件系统不支持os.replace()原子替换在跨目录时的行为与Unix完全一致所以临时文件必须与正式文件在同一个目录。6. 进阶扩展这个命令行工具还能怎么玩6.1 给任务增加提醒能力一个自然演化方向是给任务加上通知提醒。可以在任务数据模型里新增一个remind_at字段存放ISO格式的提醒时间。然后写一个独立的监听脚本每隔几分钟轮询一次任务文件发现当前时间已经到达提醒时间且还没提醒过就调用系统通知能力弹出一条通知。这样既能保持主工具的轻量又能获得分类通知能力。轮询间隔建议设置在30到60秒之间间隔太短会因为频繁读取文件造成不必要的IO开销太长则不够及时。6.2 数据导出与备份待办数据存在JSON文件里但从这个文件直接阅读没有大问题想要用到其他工具里就需要导出能力。我添加了两个导出子命令export csv和export markdown。CSV格式方便导入电子表格或日历应用Markdown格式方便直接粘贴到周报或者团队文档里。备份则可以通过crontab定时任务实现每天凌晨自动把tasks.json复制到备份目录也可以直接用Git仓库管理整个.todo目录每次修改都自动提交一次。6.3 与其他工具联动命令行工具的隐藏优势是能被其他程序调用。我做过一个很有用的联动方案在Shell配置文件的启动提示符里调用todo list -u --count来显示当前未完成的任务数。从此以后每次打开终端第一眼就知道还有几件事没做。类似地还可以在Git提交前检查是否有遗留的高优先级任务或者在代码部署完成后自动清理相关的部署待办。这类自动化是图形界面工具很难做到的。结合这几个方向这个原本极简的小工具可以一步步长成一个和个人工作流深度绑定的效率中枢。核心思路始终是保持数据格式的开放性和命令接口的稳定性然后在这个基础上不断做增量叠加。我自己实际用下来最大的体会是待办应用最怕的就是变成“第二个待办应用”。我们缺的从来不是一个功能繁杂、配色好看的效率软件而是一个愿意天天打开、操作不费脑子的记录入口。命令行形式恰好把记录成本压到了最低。如果你也想动手做自己的版本我的建议是先完成最小可用版本用起来哪里别扭就改哪里再往里填功能。先跑通流程再去谈什么花哨的体验别让项目死在“想做得太完美”的半路上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑