
最近在整理自己的项目清单时我又一次把平时那些反复操作的东西翻了出来然后做了一个叫CLI-Anything的个人工具箱。这个项目的核心想法很简单能不能把几乎所有的日常操作都收拢到命令行里用一个统一的命令入口搞定。它不是一个具体的软件产品更像是一套“用终端统一日常杂事”的实践思路——文件批量处理、日志过滤、系统状态汇总、待办管理、甚至临时速记全部揉进同一个 CLI 框架里。我平时进终端敲一条命令就能完成不需要再打开各种图形界面来回点击。这篇文章适合两类人。一类是天天泡在终端里想把手头重复劳动脚本化的开发者、运维或者数据分析师另一类是刚接触命令行不久但对“效率工具”有执念愿意花时间折腾的实践型用户。我会从设计思路、技术选型、核心实现到踩坑记录把整个过程拆开讲清楚你完全可以照着这套思路搭一套自己的 CLI-Anything。1. CLI-Anything 到底解决什么问题1.1 我为什么对“全命令行”着迷说实话图形界面的工具并不差Windows、macOS、各种桌面应用已经把大部分操作做得足够直观。但问题在于图形界面按下的每一个按钮都是不可复现的一次性动作。举个我自己的例子。以前写周报的时候我总要处理一堆服务器上的日志文件先把几十个.log文件批量重命名加入日期后缀再把里面 ERROR、WARN 级别的关键行筛出来整理成表格最后还要按模块统计一下错误频率。如果用 GUI 做每一次都要打开文件管理器、挨个右键重命名、再用文本编辑器打开搜索过滤。大概十分钟动作固定纯粹的体力活。而命令行做同样的事情本质上就是把“操作意图”写下来让机器按规则执行。七八行脚本、一条循环命令几秒钟就跑完。更关键的是——这次跑完下次还能跑还能改一个参数跑。CLI-Anything 就是在做这件事把高频、重复、规则明确的动作收集到一个统一的命令行入口里让它们可以被调用、被组合、被定时触发。我并不是说命令行要取代所有 GUI。文件预览看图片、视频剪辑调色、复杂的表格透视分析这些场景 GUI 确实更强。但凡是规则明确、有固定流程、需要反复执行的批量操作命令行天生就是最优解。CLI-Anything 的核心价值不是炫技而是把效率工具的门槛降下来用统一的学习成本换回长期的时间收益。1.2 这套思路适合谁CLI-Anything 不适合完全没有命令行基础的用户。如果连cd、ls都要查手册那么建议先花几天时间熟悉基本的 Shell 操作再来看这个项目。它更适合下面这几类人开发者和运维常见场景是检查服务状态、拉取日志、批量处理配置文件、发布脚本统一管理。数据分析师经常做数据清洗、格式转换、批量重命名导出的 CSV 文件可以用命令行一步完成。喜欢折腾的效率党想让终端变成自己的个人控制台管理待办、快速记录临时想法、一键生成周报草稿。这套思路的门槛其实比想象中低。你不需要会写多复杂的代码只要掌握基础的脚本语言能写一点文件读写和字符串处理就可以搭出够用的版本。我用的是 Python 快速迭代原型生产环境里用的是 Go原因后面会细说。2. 整体架构与技术选型拆解2.1 语言选型与项目结构我在做 CLI-Anything 的技术选型时在 Python 和 Go 之间纠结了很久。最终生产版本选择了 Go原因有三个单二进制分发编译完就是一个可执行文件拷到任何 Linux 服务器上直接能跑不需要安装 Python 解释器也不需要管理依赖环境。跨平台交叉编译Go 可以很轻松地编译出 Windows、macOS、Linux 三个平台的二进制一条命令搞定。并发性能好批量操作大量文件时Go 的 goroutine 可以方便地做并发控制。但我做原型和写文章示例的时候还是习惯用 Python。原因也很实在Python 写起来足够短标准库就覆盖了文件操作、正则、进程调用等绝大多数需求任何人都能快速读懂。CLI-Anything 本身的架构和语言关系不大核心是一个稳定的入口分发机制。一个最小可运行的项目结构大概是这样的cli-anything/ ├── main.go // 入口读取参数分发到对应命令 ├── cmd/ │ ├── file.go // 文件类命令重命名、编码转换、批量复制 │ ├── log.go // 日志类命令过滤、聚合、统计 │ ├── sys.go // 系统类命令磁盘、内存、网络状态 │ └── todo.go // 日常类命令待办、速记、提醒 ├── internal/ │ ├── config.go // 配置加载 │ └── output.go // 统一的输出格式处理 └── plugins/ // 外部插件目录运行时自动注册主入口做的事并不多解析命令名找到对应的 Handler 函数然后把剩余参数传进去。真正重要的是后面这几层设计。2.2 子命令设计规范命令行工具最容易做乱的地方就是子命令命名。我见过很多工具的命令语法五花八门有的先写动作再写对象有的先写对象再写动作最后用起来全靠猜。CLI-Anything 里我定了一个统一的命令格式clia group action [target] [flags]group表示领域比如文件相关是file日志是log系统是sys。action一定是动词比如rename、filter、status。target是操作对象一般是路径、文件模式或者任务 ID。flags是参数比如--dry-run、--format、--from、--to。实际用起来是什么感觉clia file rename --from *.log --to *.bak --dry-run clia log filter appsrv/error.log --level ERROR --module auth clia sys health --format table clia todo add 写周报 --due 2025-01-10这套规范带来的好处是可猜测性。你不需要背命令只要记住“动词 对象”的基本逻辑就能猜出七八成。配合 shell 的自动补全实际使用时的记忆负担会降到很低。2.3 配置与插件机制CLI-Anything 的配置管理遵循一个经典原则简单的不进配置复杂的才进配置。配置文件放在~/.clia/config.yaml里面保存用户级偏好比如默认输出格式、编辑器路径、待办文件位置、日志目录等。一套很轻的加载优先级是命令行参数 环境变量 配置文件。举个例子如果配置文件里设置了默认输出格式是文本而你单次执行想用 JSON 输出那么加一个--format json就够了改不动配置文件。这种覆盖机制在各种工具链里都很常见用户体验统一。插件系统是整个项目里我比较得意的部分。实现思路不复杂在~/.clia/plugins/目录下放任意可执行文件CLI-Anything 在启动时会扫描这个目录把这些可执行文件注册为外部子命令。比如我想加一个“速记”功能只需要写一个note.py脚本放到插件目录然后给它一个可执行权限再去~/.clia/config.yaml里声明一行别名plugins: note: ~/.clia/plugins/note.py之后就可以直接执行clia note add 这个想法要记下来 clia note list --last 10插件的价值在于把整个框架的扩展能力开放给了调用者。你不需要修改主程序的任何代码就能不断增加新命令。这也是 CLI-Anything 能长期用下去的关键——它是一个可以让用户自己持续生长的工具而不会只在项目刚建好的头两周有点新鲜感。3. 从零搭建一个最小可运行版本3.1 目录骨架与核心入口说再多架构不如直接看代码。这里我展示一个 Python 版的最小实现方便你理解核心分发逻辑。真正的 Go 版本逻辑也一样只是语法不同。入口文件clia.py#!/usr/bin/env python3 import sys import importlib import pkgutil def load_commands(): 扫描 cmd 目录下的每个模块收集子命令 import cmd as cmd_pkg commands {} for mod_info in pkgutil.iter_modules(cmd_pkg.__path__): module importlib.import_module(fcmd.{mod_info.name}) if hasattr(module, register): module.register(commands) return commands def main(): if len(sys.argv) 2 or sys.argv[1] in (-h, --help, help): print(用法: clia group action [target] [flags]) print(可用命令:) for name in sorted(load_commands().keys()): print(f {name}) return 0 group_action sys.argv[1] # 例如 file action sys.argv[2] if len(sys.argv) 2 else help rest sys.argv[3:] commands load_commands() key f{group_action} {action} if key not in commands: print(f未找到命令: {key}) return 2 func commands[key] return func(rest) if __name__ __main__: sys.exit(main())这段代码做的事情非常简单扫描cmd目录下的所有模块每个模块通过register函数把命令名和实现绑定起来主程序拿到用户输入后去查表分发。这种设计让新增一个命令只需要新建一个文件再写一个register函数别的什么都不用动。3.2 第一个实用命令批量重命名批量重命名是文件操作里最高频的场景之一也是理解 CLI-Anything 实际工作流的好例子。我在cmd/file.py里实现了这样一段逻辑import argparse import glob import os import re def register(commands): commands[file rename] handle_rename def handle_rename(argv): parser argparse.ArgumentParser(progclia file rename) parser.add_argument(--from, destpattern, requiredTrue) parser.add_argument(--to, destreplacement, requiredTrue) parser.add_argument(--dry-run, actionstore_true) parser.add_argument(--reverse, actionstore_true) parser.add_argument(--root, default.) args parser.parse_args(argv) pattern os.path.join(args.root, args.pattern) files sorted(glob.glob(pattern)) if not files: print(没有匹配到任何文件) return 1 log [] for path in files: dirname, basename os.path.split(path) new_name re.sub(args.pattern.replace(*, .*), args.replacement, basename) if new_name basename: continue new_path os.path.join(dirname, new_name) log.append((path, new_path)) if not args.dry_run: if os.path.exists(new_path): print(f跳过: {new_path} 已存在) continue os.rename(path, new_path) for old, new in log: status 将重命名 if args.dry_run else 已重命名 print(f[{status}] {old} - {new}) print(f共处理 {len(log)} 个文件) return 0我实际使用中每次重命名都会先跑一次--dry-run看清楚效果再执行。加上--reverse参数后还能根据自己的重命名规则做回退算是在破坏性操作上补了一层保险。这里有几个值得注意的地方覆盖保护重命名之前检查目标路径是否已存在如果存在则跳过防止误覆盖正式文件。glob 与正则混用用户输入的是*.log这类 glob 通配符但re.sub需要正则语法所以要把模式里的*转成.*。这个细节最容易翻车。返回非零退出码如果没有文件匹配返回1中断调用链方便后续脚本判断。3.3 第二个实用命令日志错误聚合日志分析是运维和开发最常做的事。CLI-Anything 里的日志聚合命令本质上就是把grep、awk、sort、uniq这类经典工具组合起来但输出格式更规整参数更容易记。import argparse import collections import glob import re def register(commands): commands[log filter] handle_log_filter def handle_log_filter(argv): parser argparse.ArgumentParser(progclia log filter) parser.add_argument(pattern, help日志文件路径或 glob 模式) parser.add_argument(--level, defaultERROR, choices[DEBUG, INFO, WARN, ERROR]) parser.add_argument(--module, defaultNone) parser.add_argument(--max-line, typeint, default200) parser.add_argument(--format, defaulttext, choices[text, json, table]) args parser.parse_args(argv) counter collections.Counter() lines [] for path in glob.glob(args.pattern): with open(path, r, encodingutf-8, errorsignore) as f: for line in f: if f[{args.level}] not in line: continue if args.module and args.module not in line: continue m re.search(rmodule(\S), line) if m: counter[m.group(1)] 1 lines.append(line.strip()) if len(lines) args.max_line: break比如执行clia log filter logs/app*.log --level ERROR --module auth --format table输出就是一张按模块统计错误数量的表格。配合定时任务每天早上九点自动跑一次把前一天的错误摘要输出到一个文件里值班时间能省下一半。4. 实战经验与排坑手册4.1 参数解析最容易翻车的地方命令行工具的坑十个里有六个在参数解析。我拿 Python 的argparse说几个典型问题第一个坑是负号参数。如果命令的某个参数值是负数比如clia file rename --offset -1argparse可能会把-1当成未知的 flag从而报错。解决方法是参数值前加--offset-1或者在解析器里设置allow_abbrevFalse之类的选项。这类边界情况平时很少遇到但一旦遇到会花掉不少时间。第二个坑是子命令嵌套时的 flag 错位。CLI-Anything 的格式是clia group action [flags]如果每个 Handler 都自己创建ArgumentParser解析器只会看到 action 之后的参数这是合理的。但如果你在入口层也放了一个 parser就会导致前面的全局参数和后面的局部参数互相抢。我在早期版本里就把全局参数--verbose和局部参数--level混在一起解析结果在命令执行时经常报错。教训是全局参数和局部参数必须在不同层解析别写到一个 ArgumentParser 里。第三个坑是参数的多样性。有的参数是一个值有的是多个值有的本身是列表。我在做文件重命名的时候--from和--to都是单值但做多文件合并操作时目标可能是多个文件拼一个输出文件。这种情况下用nargs配合读数整体上比把所有参数强行塞进一个字符串再手动 split 要稳得多。用 Go 写的时候我选的cobraviper组合就省心很多。cobra 天然支持子命令嵌套viper 能做配置加载和参数绑定。如果你准备把 CLI-Anything 真正做成一个长期使用的工具我非常推荐直接用 Go cobra不要再折腾手动解析。4.2 输出格式与终端交互CLI-Anything 的输出我渐渐摸索出一套自己的约定这套约定对使用者来说非常重要stdout 只放正经结果stderr 放日志和警告。默认输出格式保持纯文本。彩色输出只在终端是 TTY 时开启如果检测到输出被重定向到文件或者管道自动改成纯文本。提供--no-colorflag。有些用户的终端配色很特殊强制彩色输出会很辣眼睛。提供--format json这类结构化输出选项。方便后续被其他脚本消费。如果用户用的是管道clia log filter logs/app.log --level ERROR --module auth | clia todo add 处理这些错误那么前面命令的输出必须足够干净不能混入进度条这类视觉元素。我曾经在某次给命令加动画加载效果时发现管道输出被污染了结果下游脚本解析失败排查了很久。从那以后我对任何“装饰性输出”都保持警惕——CLI 工具的职责是传递数据UI 美化是 GUI 做的事情。4.3 路径与跨平台问题CLI-Anything 要长期使用跨平台几乎是躲不开的问题。我自己主要是在 macOS 和 Linux 上跑但偶尔也会在 Windows 上使用于是遇到了一些很有意思的坑最直接的是路径分隔符。在 Windows 里是\在 Unix 系里是/。如果你在代码里硬编码了/来拼路径在 Windows 上运行时会得到不存在的路径。我建议永远用os.path.join或者pathlib不要手动拼字符串。然后是 shell 差异。Linux 上是 bash、zshWindows 上是 cmd 和 PowerShell。如果你在命令行工具里调用了rm、grep或者awk在 Windows 环境下可能直接失败。我的建议是不要用shellTrue这种方式去调用系统命令能用内置库实现的文件删除、文本匹配、流处理就用内置库。这样你的 CLI 工具才能做到真正跨平台。编码问题也值得一提。中文环境下Windows 的日志文件可能是 GBK 编码而 Linux 和 macOS 默认 UTF-8。读取日志文件时如果强行用 UTF-8 解码直接抛异常。我在log filter命令里加了errorsignore并且支持--encoding参数可以在特殊情况下手动指定编码。这个看起来很微不足道的细节在实际使用中帮我避免了很多次中断。4.4 退出码与错误处理命令行工具的退出码约定很多人不在意但它在脚本化和自动化场景里非常重要。我的约定是退出码含义0执行成功1一般执行错误比如文件不存在、匹配失败2参数解析错误比如用户传入了不认识的 flag有了这个约定下面的逻辑才能正常运作if clia sys health --format json; then echo 系统状态正常 else echo 系统状态异常需要人工检查 fi在 Go 版本里我习惯在每一个 Handler 里尽量用errors传递错误信息而不是直接panic或者说 print 完就返回 0。最顶层的main统一负责接收错误、打印到 stderr、设置退出码。这个习惯也是从服务端编程那边学来的——一个应用的错误处理路径越单一排错就越轻松。5. 把 CLI-Anything 扩展成个人效率中枢5.1 接入日常任务待办、速记和提醒当 CLI 框架稳定下来之后最有趣的部分开始了把日常琐碎的效率工具全部接入进来。待办功能我实现得很简单不需要任何外部服务。todo add命令往一个本地 JSON 文件里追加一条记录todo list读取并排序输出。核心代码可能不到五十行# cmd/todo.py import argparse import json import os import sys from datetime import datetime from pathlib import Path STORE_PATH Path.home() / .clia / store / todo.json def register(commands): commands[todo add] add commands[todo list] list_tasksclia todo add 整理周报素材 --due 2025-01-10 --tag weekly clia todo list --sort due --format table速记功能更简单本质上就是往一个 markdown 文件里追加一行时间戳和内容。但它的价值在于我不用再打开手机备忘录或者桌面便签在终端里顺手就记下来了。配合--tag把内容归个类一周结束可以很轻松地导出成周报素材。提醒功能严格来说不算强提醒它是配合系统和定时任务一起去消费待办列表的。5.2 与定时任务和终端别名联动CLI-Anything 最有用的一点就是它给了各种自动化操作一个统一的“关节”。我把它挂到了 cron 和 macOS 的 launchd 上每天早上和中午各跑一次定时检查。cron 配置举例0 9 * * 1-5 /usr/local/bin/clia todo list --sort due --format text ~/.clia/log/todo_morning.txt 30 9 * * 1-5 /usr/local/bin/clia log filter /var/log/app/*.log --level ERROR --module auth --format table ~/.clia/log/error_digest.txt同时在 shell 配置文件zsh 的~/.zshrc或 bash 的~/.bashrc里给最常用的几个命令设了别名alias statclia sys health --format text alias todosclia todo list --format table alias noteclia note add alias errsclia log filter logs/*.log --level ERROR --format table这么一改终端的使用习惯开始变化了早上到工位先敲一个stat看服务器负载和磁盘余量敲一个todos看今天要做什么。整套流程虽然都是最朴素的工具组合但组合到一起之后效率提升是很明显的。5.3 命名与心智负担管理CLI 工具越用越顺手的时候容易出现一个问题命令越来越多最后连自己都记不住有哪些。我的建议是设置一些准则来控制这种膨胀命令总数控制在 30 个以内。超过这个数量可以考虑做子命令分组整合。经常用clia list查看全部命令。这是 CLI-Anything 自带的一个内建命令会列出所有已注册的 group 和 action。定期删除没用的子命令。如果某个命令三个月都没用过一次说明它对应的场景要么太冷门要么本来就该用别的工具解决删掉不心疼。还有一个个人体会不要为了 CLI 化而 CLI 化。如果某个任务只需要偶尔做一次而且图形界面操作很快那它就完全没必要进 CLI-Anything。真正值得收录进去的永远是那些你重复了五遍以上的动作或者可以在脚本链条里自动触发的动作。换句话说CLI-Anything 的边界就是你的重复劳动清单。6. 结语之外一点实际体会写到这里整个 CLI-Anything 的设计、实现和扩展路径都已经讲清楚了。如果你问我对这套思路有什么想强调的经验我的回答是不要把 CLI 工具仅仅当成命令的集合而要把它看作你个人工作流的“控制面板”。每收录一个新命令就是给这个面板增加一个按钮每统一一次输出格式就是给面板提供一条更顺滑的接线。最后再分享一个小技巧任何破坏性操作一定要先提供--dry-run预览再提供真实执行并在可能冲突时自动跳过。这个习惯让我避免了很多次因为批量重命名、批量删除或者批量覆盖导致的意外损失。命令行工具是效率放大器但同时也是误操作放大器——好的工具设计要能保护用户自己。CLI-Anything 这个项目本身会继续发展每隔一段时间我就会把最近新出现的高频操作收进去。对于你来说也不一定非要把它当成一个完整框架来用哪怕只是从中选出两三个命令的思路改造成自己工作流里的脚本这篇文章的目的也就达到了。