ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

用Python自建命令行待办工具:存储、状态机与安全实践

用Python自建命令行待办工具:存储、状态机与安全实践 做了十几年开发我电脑里最离不开的东西不是什么花哨的图形便签反而是终端里那几行命令。今天想聊的是我用 Python 自建一个命令行待办事项应用的完整过程从为什么选命令行到数据怎么存、命令怎么设计再到中间踩过的坑和最后沉淀下来的实用技巧。这类工具听起来简单仿佛写个增删改查就能交差但真正要把它做成一个自己愿意每天打开的东西需要考虑的细节远比想象中多。1. 为什么图形界面解决不了“终端前随手记一笔”的需求你第一反应可能是待办事项手机备忘录、各种日历应用、桌面小组件哪个不能做确实能但那是另一类使用场景。我自己的情况是大部分工作时间都在终端里很多待办不是“下午四点开会”这种带强时间属性的事件而是“发版前记得改版本号”“修完这个 bug 后把日志加上”“测试环境更新之前先备份数据库”这类和工作上下文强绑定的短任务。它们不需要闹钟提醒不需要分类成“个人”“工作”只需要在我扫一眼终端时立刻想起来哦手上还有这件事。这类场景放进图形应用里会产生一个摩擦你得先切走注意力打开另一个软件再等它加载完。而命令行工具没有这个过程。你就在终端里敲一行命令任务已经记下来了。这种“零切换”的体验恰恰是图形界面替代不了的地方。1.1 这套工具真正要应对的使用场景我最初触发这个念头是在一次线上问题排查之后。当时连续几个小时都在命令行里查日志、改配置、重试操作中间接到几件临时任务确认某个服务的新版本号、让同事提交一份接口变更说明、给某个目录写个清理脚本。如果用图形待办工具就意味着每来一件任务我都要从终端切出去记录在那种争分夺秒的现场这是不可接受的。类似场景还有很多。SSH 登录远程服务器做维护时你身边往往没有自己常用的图形工具随身带着手机又觉得在服务器上做事却要在手机上翻待办很滑稽。再比如深夜处理告警大脑本来就在高速运转如果记录任务还要经过“进入应用、点击新建、输入标题、设置提醒”这套流程那和没记录没有区别。还有一些使用人群不太一样——不是每个人都每天打开十几个图形应用。无论是写脚本的运维、维护 CI/CD 的开发者还是长期待在终端里的数据分析师都需要一个“不会主动打扰、但随叫随到”的任务清单。命令行工具的优势就在这里它安静地待在你的数据文件里你来了它就在你不来它也不刷存在感。1.2 命令行应用不可替代的三个原因如果把这套工具和图形待办应用放在一起对比命令行至少有三个地方是真正差异化的。第一是启动速度和资源占用。图形应用启动往往要经历窗口加载、界面渲染动辄一两秒甚至更久很多还会常驻后台进程没事就在托盘里占着十几 M 内存。命令行工具是真正用完即走的每次执行只存在几百毫秒执行完就退出不给你桌面环境平添负担。第二是可脚本化。这是命令行工具“降维打击”一般的能力。我可以写一个定时任务每天早上自动把今天到期且未完成的任务列出来可以在发布流程里加一步弹出一个“记得更新 changelog”的提示甚至可以把任务输出和 shell 脚本串联做完一件事顺手把对应任务标记为完成。图形应用做得再精致也很难提供这种可编程接口。第三是数据透明。图形待办工具的数据往往存在各自的应用沙盒里想看一眼还要专门找导出功能。命令行工具的数据就是普通文件你可以直接打开可以用其他脚本处理可以放进版本管理工具。这种透明性对个人数据来说是一种安全感——“我知道我的任务躺在哪里也知道它长什么样子”。2. 技术选型Python 配 JSON 文件是怎样权衡出来的工具定位决定了技术路线。它需要跨平台、容易改、没有太复杂的外部依赖同时因为我独用不需要考虑多人并发和复杂权限。带着这几点约束我做了下面几轮取舍。2.1 用 Python 而不是其他语言这里边有哪些妥协最初的备选项其实不少Go、Rust、Node.js、Python 都写过命令行工具我也并不排斥其中任何一个。最终选择 Python 核心原因是标准库足够用。argparse、json、pathlib、time、tempfile、fcntl这些已经覆盖了 CLI 参数解析、数据读写、备份、锁文件等几乎全部需求不需要额外安装第三方库也就意味着在任何一台装有标准 Python 解释器的机器上都能直接跑。这里当然有妥协。Python 的启动速度比编译型语言慢我实际测过进程从敲下回车到输出结果大约 300 到 600 毫秒取决于系统环境。图形应用动辄几秒的启动时间让我们习惯了等待但命令行工具连半秒都显得拖沓。我曾在一次发布窗口里连续使用十几个操作感受到那种“几乎无感”的瞬时反馈有多重要。为了进一步压缩启动时间我也想过换成 Go但后来评估发现待办事项应用每次操作本来就少半秒的延迟换成开发效率和跨平台免编译的好处完全值得。如果哪天我真觉得这半秒不能忍了用 Go 重写一遍也是同一个设计方案数据格式和命令接口不变迁移成本很低。另一点让我对 Python 更放心的原因是它无需编译。改一行字段校验逻辑保存后马上就能跑调试路径足够短。很多个人工具修改频率高每改一次都要编译部署的话会显著降低你优化它的意愿。2.2 存储方案JSON 文件还是嵌入式数据库这是做工具类应用时绕不开的决策。下表是我在两种方案之间反复对比的依据维度JSON 文件嵌入式数据库查询能力需要自己写过滤逻辑但简单任务足够支持 SQL复杂联表自然并发安全多进程同时写会相互覆盖需要锁自带事务与并发控制数据可读性文本直接查看可用任何编辑器修改需要专门工具访问依赖复杂度标准库即可内置为一个库文件体积略大适合场景单用户、任务量小、重可读性多进程、数据量大、复杂查询我选 JSON 文件的理由很直接待办事项的数据结构化程度低、数据量有限常见的过滤无非按状态、按标签、按优先级这类操作用一次线性扫描就能轻松处理。而且 JSON 的透明性是无可替代的——如果哪天工具本身出了问题我还可以直接用任意编辑器把数据打开来修复这种“裸奔式”的可靠感对命令行用户很重要。嵌入式数据库固然更规范但对我这个项目来说明显过重了。我不需要 LIMIT 分页、不需要索引优化、不需要跨进程事务硬要上一个数据库反而破坏了“所见即所得”的直觉。选型原则应该是用最简单的能解决问题的方案而不是用功能最全的方案。后面加功能时如果发现单文件 JSON 撑不住了再换数据库不迟。2.3 任务字段怎么设计才能兼顾简单与好用数据结构是这类应用的地基我在第一次设计时没想全后来付出了改脚本的代价。最终沉淀下来的任务字段大致如下{ id: a3f9c2e1, title: 发版前确认版本号已更新, description: , status: pending, priority: high, due_date: 2025-06-10, tags: [release, ops], created_at: 2025-06-08T09:30:00, completed_at: null }字段解释一下id 是唯一标识用来精确操作某一条任务title 是任务标题description 是可选描述status 是状态pending、in_progress、completed 三种后面细说priority 是高、中、低三档due_date 是截止日期只存日期不存时间tags 是标签数组created_at 和 completed_at 是时间戳。有几个字段是后来才加的。description 原本没有后来发现一些任务需要写点背景比如“这个配置改过之后要回滚回滚步骤见某某链接”否则过两周再看标题已经想不起来原委。tags 也是实用主义的结果工作里的任务天然分成“项目”“运维”“个人”等维度光靠平铺列表很难快速筛选。这些字段的教训是第一版不必贪多但至少要预留类型意识否则后面加字段就要写迁移逻辑。3. 命令设计把常用操作收敛为一屏能看懂的命令集命令行应用最忌讳的是命令名长得像绕口令或者每个操作都要求记住一长串参数。我设计命令时的原则很简单高频操作要短危险操作要多一步确认整个命令集一眼看过去不会超过一屏。3.1 完整命令集常用的 10 条操作下面是最终沉淀下来的命令总览命令作用todo add 任务标题添加任务可加 --priority、--due、--tagtodo list列出全部任务可加 --status、--tag、--due-beforetodo show id查看单条任务的完整信息todo start id把任务状态从 pending 改为 in_progresstodo done id标记为完成todo reopen id重新打开已完成任务todo update id --priority high更新任务属性todo delete id删除任务需要 --yes 确认todo stats统计未完成、已完成、过期任务数量todo backup手动备份数据文件这样一个周期里最常用的操作除了删除其余命令都在三个单词以内。我刻意没有把“edit”做成全屏文本编辑器因为update 加参数的方式更可控也更容易在 shell 历史里留下操作轨迹。3.2 参数解析里最容易出错的地方讲几个真实的“坑”新手很容易踩。第一个是任务标题带空格的问题。命令是todo add 这是一个任务如果用户忘记加引号add后的位置参数只能拿到第一个单词。我的解决方案是用argparse的REMAINDER模式让add命令把剩余参数全部当作标题再配合--区隔选项这样即使标题里夹着引号、特殊字符也不容易崩。第二个是日期格式的统一。这个极其重要。用户今天输入--due 明天明天输入--due 2025-06-20如果每个别名都支持解析逻辑会越写越复杂而且容易出现歧义。最终我把日期格式收敛为YYYY-MM-DD并在解析入口做一个纯函数统一校验。不支持的格式直接报错绝不默认当成今天或任意猜测。原因是“猜错了用户可能完全注意不到”导致一个任务在错误的日期下被静默过滤掉。第三个是危险操作的确认。删除任务这种不可逆操作我在命令层做了二次确认不传--yes就进入交互式确认如果确认发现传了一个不存在的 id也要给出提示而不是默默忽略。CLI 工具做得越顺手越要防“顺手把重要数据删了”的意外。3.3 核心命令的代码实现骨架下面是核心数据层和 add、list 两个命令的实现骨架其它命令逻辑大同小异。import argparse import datetime import json import os import tempfile import uuid DATA_FILE os.path.expanduser(~/.todo.json) def load_data(): try: with open(DATA_FILE, r, encodingutf-8) as f: data json.load(f) except FileNotFoundError: data {schema_version: 1, tasks: []} return data def save_data(data): dir_name os.path.dirname(os.path.abspath(DATA_FILE)) fd, tmp_path tempfile.mkstemp(dirdir_name) with os.fdopen(fd, w, encodingutf-8) as f: json.dump(data, f, indent2, ensure_asciiFalse) os.replace(tmp_path, DATA_FILE) def cmd_add(args, data): task { id: str(uuid.uuid4())[:8], title: args.title, description: args.description or , status: pending, priority: args.priority, due_date: args.due, tags: args.tags.split(,) if args.tags else [], created_at: datetime.datetime.now().isoformat(timespecseconds), completed_at: None, } data[tasks].append(task) save_data(data) print(f已添加任务ID: {task[id]}) def cmd_list(args, data): tasks data[tasks] priority_rank {high: 0, medium: 1, low: 2} tasks.sort(keylambda t: ( priority_rank.get(t.get(priority, medium), 1), t.get(due_date) or 9999-12-31 )) for t in tasks: status t[status] mark [完成] if status completed else [进行] if status in_progress else [待办] print(f{t[id]} {mark} {t[title]} 优先级:{t.get(priority)} 截止:{t.get(due_date)} 标签:{,.join(t.get(tags, []))})代码里最值得说的是排序逻辑。due_date可能为空排序时就把它映射成9999-12-31让没有截止日期的任务排到最后优先级先于日期排列因为“重要且紧急”永远应该优先于“单纯明天到期”。粗看这像是数据细节其实它决定了列表的可用性。4. 让任务真正“动”起来状态机、过滤与视图待办事项应用如果只做到增删改查那本质上就是个带命令行外壳的文本编辑器。让它成为“工具”的关键是把任务放进工作流里这就离不开状态设计和过滤视图。4.1 状态机设计三个状态背后的工作流思考最初的版本只有两个状态未完成和已完成。用了两周之后发现不够有些任务已经开始处理了比如“修完 bug 后加日志”这件事我已经改了两行代码但还没完成下次打开列表时希望知道“这件事已经进行中”而不想看到它和其他没开始的杂事混在一起。于是加了in_progress状态。加上之后任务的生命周期变得清晰当前状态允许执行的操作目标状态pendingstartin_progresspendingdonecompletedin_progressdonecompletedin_progressreopenpendingcompletedreopenpending任意状态delete删除之所以可以通过pending直接done是因为现实中经常出现“还没来得及开始就完成了”的任务比如一个同事顺手把事件处理掉了。强行要求必须经过中间状态会显得死板。实现上我维护了一个合法的状态转换表非法操作直接报错ALLOWED_TRANSITIONS { pending: [in_progress, completed], in_progress: [completed, pending], completed: [pending], } def transition(status, action): target {start: in_progress, done: completed, reopen: pending}[action] if target in ALLOWED_TRANSITIONS[status]: return target raise ValueError(f无法将状态从 {status} 通过 {action} 变更为 {target})不要小看这一步它让任务的流转有了“语义”。当别人追问“这个任务现在到哪一步了”你能通过状态一目了然地回答。没有状态机的数据仓库只会在列表里堆出一堆永不更新的旧记录。4.2 过滤、排序与视图一个待办清单的灵魂命令行的待办清单最怕什么最怕输出一堆让人眼花缭乱的任务。任务一旦超过三十条不做过滤就是灾难。所以我给list命令加了几个常用过滤参数todo list --status pending todo list --tag release todo list --due-before 2025-06-30 todo list --status in_progress --priority high过滤逻辑用一段朴素的循环实现各个条件之间是“与”的关系同时满足才会保留def apply_filters(tasks, statusNone, tagNone, due_beforeNone): result [] for t in tasks: if status and t.get(status) ! status: continue if tag and tag not in t.get(tags, []): continue if due_before and (t.get(due_date) or 9999-12-31) due_before: continue result.append(t) return result过滤之外我还有一个个人偏好列表输出不要超过 20 行。任务一多我就只会在意“今天最要紧的几件事”所以把--limit 10直接设为隐含参数不传也默认只显示前 10 条想看全部再显式加--limit 0。命令行应用讲究一个“克制”和“清晰”宁可少一些多余输出也不要让信息淹没重点。这里有一个实用技巧把todo list的输出格式处理成“ID 状态标记 标题”的紧凑行每行不超过 80 个字符。这样即使不做任何美化放在任何终端窗口都不会折行混乱。复杂展示放在todo show id里需要看完整信息时再展开。4.3 批量操作与安全护栏任务基础少时逐条操作尚可任务积累多了以后批量操作几乎是刚需。最常用的两个是“一次性完成全部工作”和“清理已完成任务”todo done --all --status in_progress todo clean --done批量操作很容易误伤所以我把安全护栏作为默认行为不支持“直接全部搞定”的语法必须加--status参数缩小范围涉及批量删除的命令第一次执行时先输出将受影响的任务数量再要求输入y确认。有些人可能嫌这一步多余但我在一次误操作后意识到批量操作是“手滑高危区”宁可多花一秒确认也不能用恢复数据的痛苦去冒险。还值得一提的细节是增加了--dry-run选项。让批量操作先走一遍完整逻辑只打印将执行的动作不真正改动数据。这让我可以在执行前快速扫一眼结果确认无误后再去掉--dry-run重新执行。命令行工具想做得好用“安全”和“快捷”本来就要这样反复平衡。5. 数据安全与格式兼容命令行工具更该怕丢数据图形应用丢数据你会怪它自己写的命令行工具丢数据就没处甩锅了。数据文件是单文件 JSON结构简单但简单文件的脆弱性也正是它的软肋。写入一半断电、多终端并发写、格式版本变化这些都有可能造成灾难。我在这个项目上做的安全措施对任何单文件数据存储都值得借鉴。5.1 原子写入避免一次崩溃毁掉全部任务最常见的损坏场景是程序打开数据文件写入了一部分此时突然断电或进程被杀文件停留在半截状态。直接后果是 JSON 解析失败整个任务列表打不开。使用with open(DATA_FILE, w)这种直接覆盖写法风险敞口非常大。正确做法是“原子写入”先写到同目录下的临时文件再通过os.replace原子替换旧文件。同目录很重要因为跨文件系统移动不一定具备原子性而在同一文件系统内os.replace是原子的。代码就像前面 save_data 展示的那样先mkstemp再os.replace这样任何时刻目标路径要么是旧文件、要么是新文件绝不会是半截状态。我把崩溃恢复也考虑进去了即使写入临时文件后、替换前进程被杀旧文件也不会损坏唯一残留只是一个.tmp文件下次启动时清理掉即可。这段保护逻辑代码量不多却让数据的可靠性提升了一个档次。5.2 多终端同时打开时的并发与锁使用场景绕不开“多个终端同时操作”。比如工作机上开着两个 shell或者用 SSH 从另一台机器访问两边同时各跑一条todo done。如果两边都读到旧数据、各自修改后再写回会出现经典的“后写覆盖先写”问题——一个任务标记完成了另一个终端又把它还原了。解决办法也不复杂在 Linux/macOS 上用fcntl对数据文件加锁锁持有期间才允许做加载-修改-写回操作在 Windows 上则退化为朴素的“先清临时文件再直接写”因为fcntl不是标准库。简单起见我给工具加了一个启动锁文件机制核心代码如下import fcntl LOCK_FILE os.path.expanduser(~/.todo.lock) def acquire_lock(): f open(LOCK_FILE, w) fcntl.flock(f, fcntl.LOCK_EX) return f def main(): lock acquire_lock() data load_data() # 执行命令逻辑 save_data(data) fcntl.flock(lock, fcntl.LOCK_UN)当然锁只能约束使用同一个工具的进程。如果你用编辑器直接修改 JSON 文件又正好和工具写回并发操作依然可能冲突这个只能靠使用习惯回避。但工具之间的相互覆盖问题用锁已经可以杜绝了。5.3 数据迁移、备份与向前兼容我在load_data里留了schema_version字段这个字段到现在已经救过我一两次了。每版脚本如果字段结构发生变化就在数据文件加版本号加载的时候根据版本号走对应的迁移函数而不是直接按当前代码期望的结构解析。字段一旦不匹配程序要么报错要么悄悄产生空值都会造成数据污染。备份则是一个独立的todo backup命令它的实现是复制当前数据文件为带时间戳的.bak文件todo backup # 生成 ~/.todo.20250608-1530.bak说实话这个命令我平时很少主动想起来用所以后来干脆在每次“批量删除”之前自动跑一次备份。这样即便确认时手滑选中了不该删的任务也还有上次操作前的完整快照。命令行工具的意义不是让你更胆大而是让你在犯错之后有退路。6. 连续使用一段时间后的真实体验与扩展方向工具做完以后我把它装进了自己的日常工作流现在几乎每天都在用。这一章不写教程式的收尾只记录几段真实使用感受以及这个项目未来可能延伸出的方向。6.1 日常使用方式与真实感受我现在最典型的使用节奏是早上到工位先打开终端跑一遍todo list --status pending --due-before today看一眼今天必须处理的事。然后整天的杂事都往里面塞有的任务连一个描述都没有只有一句话标题比如“恢复某服务自动更新”。处理到一半的会在状态上标成进行中靠todo list --status in_progress把握手头节奏。每次进入代码库之前我也会用todo add把脑子里想到的收尾工作记下来避免写着写着把“记得补充注释”这条细节彻底忘掉。这种“写到一半随时记一笔”的场景图形应用做不到这么好因为切走的瞬间注意力就断了。真实感受中最重要的一点是这个工具因为“太简陋”反而没有心理负担。图形待办应用功能多但正经事没记住多少而命令行工具只记任务本身连打开它都觉得轻快所以记录黏性反而很高。这也印证了我最初的观点工具不应该用功能数量来评价而要看它能不能融入你的使用习惯。6.2 现有方案的明显短板必须诚实地说这个工具目前还有几个短板。第一是没有真正的“提醒”能力我只能靠每次list看到过期任务被动提醒无法在任务到期时主动通知。解决方案是外面套一个 crontab 任务定期运行但这样也没法做到像手机推送那样实时。第二是移动端场景完全空白如果出门在外想添加一个任务只能通过远程终端这对没有随身带电脑习惯的人很不方便。第三是交互式编辑缺失想改一长串描述时update的参数方式远不如一个编辑器来的高效。这几块短板让我意识到命令行工具适合做“个人快速记录和信息维持”但不适合做“全天候提醒中心”。做好定位比追求功能大而全更实际。6.3 再往下走这个项目可以长成什么样如果继续迭代第一批我会做提醒与过期扫描。在list输出中把过期的待办任务前置并且用显眼的字符标记“已过期”再加一个todo today命令只显示今天到期或已过期的任务。这能提高每天开始的效率。第二批是子任务和重复任务。子任务满足“一个大任务拆成几个小步骤”的天然分解需求重复任务解决“每天早上更新环境”这类周期性事项。这两个功能都会让任务模型复杂化需要小心控制扩展范围。第三批是统计与导出。比如每周自动输出一份任务完成率报告或者导出成 iCal 格式与日历应用打通。这块是锦上添花把一个至少“能记录”的工具变成“能复盘”的数据源。在我个人的实践里命令行待办事项应用最值得推荐的部分其实不是某个具体功能而是一种“以小事撬动日常效率”的做法。先动手做一个最小可用的版本放进真实使用中再让它沿着自己的习惯慢慢成长。所谓效率工具从来不是安装一个别人的完美作品而是长成你自己用得最顺手的模样。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表