年纪越大越懒这句话放到命令行上同样成立。我用一个叫 CLI-Anything 的私人工具体系,把过去需要在图形界面里点来点去的活儿(文件批量重命名、系统状态查看、接口调试、文本清洗、git 仓库汇总)全部压成短命令。这篇文章不是来推荐某个现成轮子的,而是把我是怎么设计这套工具、用哪些方案落地、踩了哪些坑、以及最终可以怎么复现出来的完整过程记录下来,希望给同样想把日常任务统一收编到终端的你一个可抄的参考。
1. 项目整体设计与思路拆解
1.1 为什么要把一切塞进命令行
人一天能记住的操作方式非常有限,但工作里的重复操作却无限多。我自己的触发点是有一天发现自己同时装了五个桌面工具:一个用来批量改文件名、一个看系统资源、一个发 HTTP 请求、一个做文本替换、还有一个管 git 仓库。每个工具都有俩屏幕的按钮,但真正用到的功能只有两三个。最烦的是它们之间还不能串联,改完文件名想顺手看一眼磁盘占用,得切换窗口再来一遍。
CLI-Anything 想解决的问题本质上只有一个:给高频、重复、可程序化的任务一个统一入口。命令行相比图形界面的好处不是你敲得快,而是可组合、可记录、可批量。你今天手动执行了一个命令,明天就可以把它写进脚本里,后天就能挂在定时任务上。图形界面里的操作则需要你记住人肉步骤,这是完全没法扩展的。
所以这个项目的核心设计哲学是:一切命令必须短、必须稳定、必须有返回值(能接入别的脚本)。那些需要眼睛直观看图表的场景留给专业可视化工具,但日常 80% 的操作,命令行绰绰有余。
1.2 方案选型:技术栈怎么定
选型这件事我花了整整一下午,对比过三个方向:
| 方案 | 优势 | 劣势 | 我的判断 |
|---|---|---|---|
| Python + click | 生态全、代码直观、psutil/requests 现成 | 启动稍慢(约 0.3s) | 最终选择 |
| Node.js + commander | 启动快、JSON 原生 | 文件操作和系统调用的库不如 Python 顺手 | 备选 |
| Go + cobra | 单二进制、极快 | 迭代成本高,日常脚本语言不够快 | 适合发布场景 |
我最后选了 Python + click。原因不复杂:CLI-Anything 是给我自己用的私人工具,重点在快速开发和灵活修改,运行效率反而不是瓶颈。Python 生态里文件处理、系统监控、网络请求都有非常成熟的库,写起来像写自然语言一样流畅。而且 click 这个库天然支持命令分组、参数解析和帮助文档,几乎不用写命令行解析的样板代码。
如果你要分发给很多用户、或者放在低配服务器上高频调用,Go 会是更优解。但自己做工具,别为性能过早优化。
1.3 架构规划:命令树怎么组织
CLI-Anything 不是一把大铁锤,而是一个工具架。我按照任务领域把命令分成五个顶层分组:
cli-anything ├── file # 文件与目录批处理 ├── sys # 系统信息与进程监控 ├── text # 文本处理与格式化 ├── net # 网络请求与接口调试 └── git # git 工作流辅助每个分组下挂若干子命令,比如file rename、sys status、text replace。这样设计的好处有三个:第一,命令不存在命名冲突,敲前缀就知道属于哪个领域;第二,每个分组都是一个 Python 模块,方便单独维护和测试;第三,shell 的 Tab 补全体验极佳,敲cli file然后按两下 Tab,你能看到这个组下所有子命令。
模块之间的公共逻辑(比如路径校验、输出格式化、颜色高亮)抽到公共包里,避免每个模块都复制粘贴一遍。
2. 核心功能模块与工具选型解析
2.1 文件与目录批处理模块
这个模块是 CLI-Anything 用得最狠的部分。功能清单包括:批量重命名(支持正则替换和模板)、重复文件检测与清理、磁盘大文件扫描、按日期归档目录。底层的库用了 Python 标准库的 pathlib 和 os,检测重复文件时用 hashlib 计算文件的哈希。
模块设计上有一个原则:所有破坏性操作必须有 dry-run 模式。file rename与file clean都默认只打印将要执行的动作,不会真的动文件。需要真实执行时加--yes或--apply。这个设计帮我避免了两次差点把备份目录清空的灾难。
批量重命名的参数设计要注意:支持新旧命名模板的正则替换还不够,最好支持占位符,比如%Y%m%d这种日期后缀,方便做归档。这类参数用 click 的字符串参数可以轻松搞定。
2.2 系统信息与进程监控模块
系统监控我在 Windows 和 Linux 两个环境都有需求。得益于 psutil 这个库,一段代码在两个平台都能跑。sys status命令输出 CPU 逻辑核心数、负载、内存占用、磁盘使用率、关键温度(如果有传感器)。sys top命令则模仿任务管理器,按照 CPU 或内存排序展示前十个进程,实时刷新。
这里有个设计细节:数值输出必须做人性化处理。直接输出字节数会得到一串天文数字,所以我写了个human_size函数,把字节自动换算成 KB/MB/GB,保留一位小数。这个函数后面被 text、net 模块反复复用,成了公共库里的明星工具。
2.3 文本处理与格式化模块
文本处理模块覆盖了批量的全局替换、编码转换、JSON 格式化与 JSONLines 摘要。日常用得最多的是text replace,它接受一个 glob 路径模式,然后把匹配文件里的旧字符串统一替换为新字符串。和文件模块一样,它默认 dry-run,打印每个文件里会命中多少次替换。
编码问题在这个模块里最常出现。GBK、UTF-8、UTF-8-BOM 混在一起的时候,直接读文件会报 UnicodeDecodeError。我的处理方式是让用户通过--encoding参数指定输入编码,同时加一个--auto模式:先尝试 UTF-8,失败后尝试 GBK,还不行就报错并输出前几个字节的十六进制,方便排查。
2.4 网络请求与接口调试模块
过去我调接口要开 Postman,环境配置、请求头管理、导出测试用例,重得很。CLI-Anything 的net模块只保留三个命令:net get、net post和net check。net get/post就是最直接的 HTTP 请求,支持自定义 header、body、超时时间。net check则用来做批量健康检查:传一组 URL,它并发地请求并统计状态码和耗时,输出一个表格。
细节上,请求时我会给所有命令加上重试次数参数(默认 2 次),并设置一个合理超时(默认 10 秒),避免网络抖动导致命令行挂死。JSON 输出时,利用 Python 的 json.dumps 做美化输出,并设置 ensure_ascii=False,避免中文 Unicode 转义。
2.5 Git 工作流辅助模块
Git 命令本身已经够简洁,为什么还要包一层?因为高频操作里有一批“组合动作”。比如批量检查多个仓库的状态、一键清理已合并的本地分支、按照固定模板快速生成规范化的 commit message。这些动作用原生命令一条条敲太碎,而写 shell 脚本又容易在环境间迁移时碰壁。
git repos命令会读取一个配置文件(里面放了你本地所有项目的绝对路径),然后逐个执行git status --short,把结果汇总到一个列表里。有未提交变更的仓库高亮显示,完全干净的仓库显示为一行简单提示。这样我早上打开电脑,跑一次就能知道所有项目的状态,省了逐个cd的功夫。
3. 实操过程:从零搭建 CLI-Anything
3.1 环境准备与依赖安装
推荐用 Python 3.10 以上版本,虚拟环境创建我用 venv:
mkdir cli-anything && cd cli-anything python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install click psutil requests项目结构我按模块划分:
cli-anything/ ├── pyproject.toml ├── cli_anything/ │ ├── __init__.py │ ├── cli.py # 入口与分组 │ ├── common.py # 公共工具函数 │ ├── file_cmd.py # file 分组 │ ├── sys_cmd.py # sys 分组 │ ├── text_cmd.py # text 分组 │ ├── net_cmd.py # net 分组 │ └── git_cmd.py # git 分组 └── tests/pyproject.toml 里最关键的是入口点的配置,它决定cli-anything命令怎么被安装到系统里:
[project.scripts] cli-anything = "cli_anything.cli:cli"这样执行pip install -e .之后,就等于在自己机器上多了一个全局命令。修改代码立刻生效,不需要重新安装。
3.2 入口设计与命令注册
入口文件的核心任务是把所有分组挂载到同一个 Group 上。click 的写法非常直接:
import click from . import file_cmd, sys_cmd, text_cmd, net_cmd, git_cmd @click.group() @click.version_option("0.1.0", prog_name="cli-anything") def cli(): """CLI-Anything: 把日常任务统一收编到命令行的万能工具箱。""" for module in [file_cmd, sys_cmd, text_cmd, net_cmd, git_cmd]: if hasattr(module, "register"): module.register(cli)每个模块文件里提供一个register(group)函数做命令注册。比如 file 模块:
import click from .common import validate_path @click.group(name="file") def file_group(): """文件与目录批处理。""" @file_group.command(name="scan") @click.argument("path", type=click.Path(exists=True)) @click.option("--min-size", default="100M", help="只显示超过该大小的文件") def scan(path, min_size): """扫描目录下的大文件。""" ... def register(group): group.add_command(file_group)这样组织代码的原因很简单:每个模块之间不互相依赖,新增一个分组只需要新建一个 py 文件、写若干命令、实现register函数即可,其余部分不需要改动。哪怕以后拆出去给别人用,也可以整模块复制。
3.3 文件批处理命令的实现细节
以file rename为例,它的功能是:筛选出匹配正则的文件名,然后执行替换,支持 dry-run 和预览。
import re from pathlib import Path import click @click.command(name="rename") @click.argument("pattern", type=str) @click.argument("replacement", type=str) @click.option("--ext", default="*", help="文件扩展名过滤,比如 *.txt") @click.option("--recursive/--no-recursive", default=True) @click.option("--apply", is_flag=True, help="dry-run 模式下不执行实际重命名") def rename(pattern, replacement, ext, recursive, apply): """将匹配 pattern 的文件名替换为 replacement。""" base = Path.cwd() files = base.rglob(ext) if recursive else base.glob(ext) hit = 0 for f in files: if not f.is_file(): continue new_name = re.sub(pattern, replacement, f.name) if new_name != f.name: target = f.with_name(new_name) click.echo(f"{f.name} -> {new_name}") if apply: f.rename(target) hit += 1 click.echo(f"共匹配 {hit} 个文件")--apply这个变量的命名我特意跟apply动作分开,只有你确认预览结果没问题才加上它。这里有一个经验:永远不要把破坏性操作放在默认路径上。命令行工具和图形工具不一样,图形工具你还能弹窗确认,命令行里一旦回车就没有反悔按钮。
3.4 系统监控命令的实现细节
sys status的代码要兼容 Windows 和 Linux,psutil 已经抹平了大部分差异。核心输出函数如下:
import psutil import platform def human_size(num): for unit in ["B", "KB", "MB", "GB", "TB"]: if num < 1024: return f"{num:.1f} {unit}" num /= 1024 return f"{num:.1f} PB" def sys_status(): cpu_percent = psutil.cpu_percent(interval=1) virtual_mem = psutil.virtual_memory() disk_usage = psutil.disk_usage("/") boot_time = psutil.boot_time() click.echo(f"主机: {platform.node()}") click.echo(f"系统: {platform.system()} {platform.release()}") click.echo(f"CPU 使用率: {cpu_percent}%") click.echo(f"内存: {human_size(virtual_mem.used)} / {human_size(virtual_mem.total)} " f"({virtual_mem.percent}%)") click.echo(f"磁盘 /: {human_size(disk_usage.used)} / {human_size(disk_usage.total)} " f"({disk_usage.percent}%)")注意cpu_percent(interval=1)这个参数,官方文档特别说明第一次调用返回的是 0.0,因为需要采样时间差。如果要第一次就有准确值,可以程序启动时先调一次预热。这个细节在文档里不明显,实测中容易踩到,我一开始没预热,还以为是 psutil 的 bug。
3.5 打包发布与日常使用
开发时一直用pip install -e .,既注册命令又免安装。等到我把这套东西同步到另一台电脑时,发现一个问题:每个环境都要装一遍依赖,还要保证 Python 版本一致。偷懒的做法是用 requirements.txt:
pip freeze > requirements.txt但这样会把项目自身的依赖和无关包全带进去,不干净。更好的做法是在 pyproject.toml 里声明项目依赖:
[project] dependencies = [ "click>=8.1", "psutil>=5.9", "requests>=2.31", ]然后一条命令安装到新机器:
pip install -e .日常使用我再推荐一个技巧:给 bash/zsh 加命令别名。比如alias c=cli-anything,然后把c f(f 是 file 的别名)绑到 Tab 补全上。实测下来,最终我敲命令的速度基本和敲普通 Linux 命令一样快。这个阶段才是最舒服的:工具已经长在肌肉记忆里了。
4. 常见问题与排查技巧实录
4.1 命令冲突与命名空间管理
CLI-Anything 里的命令分了很多组,组多了之后最怕命令重名。比如text replace和file rename功能上有重叠,第一次实现时我在 file 组里放了个replace,又在 text 组里放了个replace,于是 CLI 一启动就报重复命令。我的解决办法是约定:file 组只处理“名字层面”的变化,text 组只处理“内容层面”的变化。如果两个命令做的事本质相同,就统一靠前缀做区分。这个约定不仅解决了冲突,也让命令的查找路径更清晰。
4.2 路径参数与转义陷阱
命令行处理路径是一块天然的雷区。最常见的坑是路径里有空格,比如~/My Documents/项目 A。如果你在 shell 里手敲cli-anything file scan ~/My Documents/项目 A,那它会解析成三个独立参数,直接报错。解决方法是使用 Tab 补全,shell 会自动给路径加转义;如果手敲,就用引号把整个路径包起来。另一个坑是 Windows 的路径里结尾的反斜杠C:\folder\在某些解析逻辑里会被当成转义符,我在 click 的 Path 类型下遇到过,最终统一用 pathlib.PurePath 做内部表示,避免字符串拼接时出问题。
4.3 跨平台兼容与编码问题
这台工具我同时在 Windows 和 Linux 上使用,编码问题首当其冲。Windows 终端默认的代码页可能是 GBK,Linux 是 UTF-8,直接用 print 输出中文在某些环境会乱码。我自己踩过的坑是:在 PowerShell 里调用sys status,磁盘使用率那行中文显示正常,但文件名带特殊字符时终端直接中断。后来我把所有控制台输出统一处理:优先用sys.stdout.reconfigure(encoding="utf-8"),并且在文件读写时显式声明编码。还有一个经验:Windows 下避免使用colorama这类 ANSI 颜色库糊得到处都是,我就只用在 Linux/macOS 检测到终端颜色支持时才启用。
4.4 性能优化与大数据量处理
日常文件扫描不是每次都要跑几千个文件,但一旦目录里面有几十万个小文件(比如 node_modules 或者 .git 目录),速度差距就出来了。标准库的os.scandir比os.listdir+os.path.join快很多,并且它能直接拿到文件类型,省去多余的系统调用。实测在 Linux 上一个包含 30 万文件的目录,用os.scandir遍历只需要几秒钟,而os.listdir可能要 20 秒以上。另外我统一用pathlib.Path.rglob做递归匹配,它内部也基于 scandir,不会重复踩坑。
4.5 常见问题速查表
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 命令安装后找不到 | 没有配置 entry_points | 检查 pyproject.toml 的 [project.scripts] 段 |
cpu_percent第一次返回 0 | psutil 需要两次采样 | 程序启动时先调用一次预热 |
| 中文输出乱码 | 终端编码与 Python 输出不一致 | stdout reconfigure 为 UTF-8 |
| 文件名带空格无法解析 | shell 参数分割 | 使用引号包裹或依赖 Tab 补全 |
| 运行慢,扫描目录卡顿 | 目录里文件过多 | 改用 os.scandir 和增量过滤 |
| 重复文件删除后误伤 | 哈希匹配精度不够或 dry-run 未启用 | 对破坏性操作强制先 dry-run |
| 不同系统的路径处理错误 | 字符串拼接路径 | 改用 pathlib.PurePath |
| 网络超时导致 CLI 挂起 | 未设置 timeout | requests 统一加 timeout=10 |
| 批量替换后文件编码异常 | 忽略原始编码 | 提供 --encoding 和 --auto 参数 |
| Tab 补全失效 | 未安装 shell 补全脚本 | click 支持生成补全脚本,按文档安装 |
写在最后:CLI-Anything 这个项目对我最大的改变不是省了多少时间,而是让我养成了一个习惯——每次发现自己重复手工操作超过两次,就会停下来想想能不能把它变成一条命令。工具本身是私人化的,每个人的高频任务不同,但设计模式是通用的:分组清晰、破坏性操作默认安全、输出可解析、参数可记忆。如果你也想做一套自己的命令行工具箱,建议从最让你烦躁的那个重复动作开始,别一上来就追求大而全。一条命令用顺手了,你自然会忍不住想加第二条。