☰
CLI-Anything:打造命令行工具,把重复劳动变成一条命令
2026/9/29 19:10:11 网站建设 项目流程

1. 先说个场景:为什么我会想给“Everything”套上命令行的壳

有一次,我需要把某个内部平台上的 40 多张报表全部导出,再按日期重命名、归类、压缩,最后附到一封邮件里发给合作方。这个流程每周都要做一次,平台本身只有网页端,没有开放 API,也没有“全选导出”的按钮。前几次我老老实实打开浏览器,一张一张点进去、点导出,然后手动挪文件,大概要花上 15 到 20 分钟。做到第三周,我实在受不了了,想着:这破流程到底能不能写成一个命令,敲一下回车就全干完?

这就是“CLI-Anything”这个想法最初的样子。它本质上不是什么高深的技术概念,而是一种工作哲学:凡是重复两次以上的操作,都值得变成命令行工具。CLI 是 Command Line Interface 的缩写,也就是命令行接口。所谓 CLI-Anything,就是试图给一切可自动化的日常事务——不管它原本是网页操作、桌面软件操作、还是数据整理——都做成一个能在终端里一行调用、随时复用的便捷命令。

这篇内容会从动机、技术路线选型、真实项目拆解、参数设计、踩坑记录到个人经验,完整分享我在构建个人命令行工具集时的全套思路。不追求大而全的理论,全部是实实在在能落地的方案。适合现在正被各种重复劳动烦扰的研发、运维、数据分析师,以及所有想在终端里获得更高效率的人。

1.1 从“偶尔手点”到“非 CLI 不可”的转折点

很多人一开始都会有这种感觉:这事儿不复杂,手动点几下就好了,花时间搭工具反而更麻烦。这个判断在“只做一次”的时候完全正确,但在“每周都做、每次都容易出错、做错了还没人知道”的时候就不成立了。

我自己的转折点是某次备份事故。当时我手动把一批文件从服务器下载到本地,因为文件名带时间戳,覆盖了上一次的版本,等我发现丢数据的时候,已经过去两天。那件事之后我意识到,手工操作不仅慢,更致命的是无法审计、无法回放、无法保持一致。同样一套操作,周一做和周五做可能因为鼠标点错一个位置就得出完全不同的结果。

而命令行天然具备几个 GUI 给不了的基本属性:

  • 可组合:一条命令的输出可以直接喂给下一条命令,中间不需要人工搬运。
  • 可重复:同一套参数执行一百次,结果一致。
  • 可记录:命令天然就是操作日志,历史记录里能看到每次做了什么。
  • 可远程:终端能连上的地方,命令就能跑,不需要远程桌面。

打个不恰当的比方,GUI 操作像是你每次都亲自去后厨跟厨师说“少放盐、多放葱”,而 CLI 是写一张精确到克的菜谱,谁拿到都能照着做出一模一样的菜。

1.2 CLI 能覆盖的“Anything”到底有多广

很多人对 CLI 的理解还停留在ls、cd、grep这种基础命令,觉得命令行只是 Linux 运维的专属玩具。其实在今天的技术环境下,CLI 能触达的范围远比想象中大。

我自己日常使用会分成这几类:

  • 本地文件批量处理:重命名、格式转换、内容替换、批量压缩解压。
  • 网络与接口请求:curl 一族的HTTP调试,封装各类 REST API 的调用脚本。
  • 音视频处理:ffmpeg 一家基本能通吃剪辑、转码、抽帧、压缩。
  • 无头浏览器操作:以前只能打开浏览器手动点的页面,如今 Playwright 等工具能在后台自动完成。
  • 数据库与数据管道:MySQL、PostgreSQL 的查询脚本,ETL 流程的编排。
  • 日常杂项:定时提醒、批量发消息、生成随机数据、管理个人待办。

每类里面都能找到成熟稳定的基础工具。CLI-Anything 要做的,不是从零发明轮子,而是用少量胶水代码把这些工具粘合起来,变成顺手的单一命令。

2. 落地路线选型:alias、胶水脚本、成熟 CLI 框架,我该用哪个

在动手写第一个命令之前,先要回答一个很实际的问题:我用什么技术方案来做?

我见过不少人一上来就选一个重量级框架,写一堆抽象类,结果一个 10 行能解决的问题写了 200 行,维护成本比手动操作还高。也见过反过来,从头到尾只用 alias,到后面 alias 定义了几百条,自己都记不住哪个是哪个。

所以我把 CLI 的落地路线大致分成三档,每档的适用边界和成本完全不一样。

2.1 路线 A:Shell 组合拳与 alias,快速但不耐折腾

最轻量的一档就是直接在 shell 里组合现有命令,然后起个别名。比如我早期处理日志文件时常用这一条:

alias todaylog='find ~/logs -name "*.log" -mtime -1 | xargs grep -l "ERROR" | sort'

一行 alias 解决“找出今天报错的日志文件”这种需求,确实快,30 秒写完,立刻生效。

这一类方案的优点是零依赖、零学习成本、改起来直接编辑配置文件。但它有明显天花板:一旦逻辑里出现条件分支、循环、需要接收用户输入,shell 脚本的可读性就会急剧下降。别跟我说 shell 也能写复杂逻辑,确实能,但维护起来真的噩梦一样——变量和引号的坑数不胜数,语法稍不留神就翻车。

所以我对 route A 的使用原则是:单条管道能完成的组合,才用 alias;凡是需要写 if/for/函数结构的,就别硬塞进 alias,立刻升级到下一档。

2.2 路线 B:Python/Node 胶水脚本,中间的过渡带

当一条命令需要超过三步以上逻辑,或者涉及字符串处理、JSON 解析、网络请求的时候,我会切换到“胶水脚本”模式。所谓胶水脚本,就是用一个通用语言把若干个外部命令拼接成一个完整的处理流程。

比如批量把 Markdown 文件里的图片链接替换成本地相对路径,用 shell 写很容易在转义上出 bug,但用一段 Python 就很从容:

#!/usr/bin/env python3 import pathlib import re for md in pathlib.Path("docs").glob("**/*.md"): content = md.read_text(encoding="utf-8") new_content = re.sub(r"https://example.com/img/(\w+)\.png", r"img/\1.png", content) if new_content != content: md.write_text(new_content, encoding="utf-8") print(f"updated: {md}")

脚本本身可以直接放在~/bin/目录下,配好可执行位后就是一条自定义命令。好处是写起来几乎没有框架负担,任何搞过脚本的人都懂;坏处是参数解析、错误提示都要自己做,稍微不注意就会做得很难用。

正因为如此,我在这个阶段踩了不少坑。直到有一次写了一个 300 行的 Python 脚本,参数全靠sys.argv硬取,结果某天传参顺序变了,脚本把输出目录当成了输入目录,直接吃掉了源文件的一个备份。痛定思痛之后,我开始转向第三档方案。

2.3 路线 C:用 Typer、Commander、Cobra 这类框架开发正式 CLI

所谓“正式 CLI”,不是指多么庞大,而是指它在结构上足够严谨:有规范的参数定义、自动生成的帮助信息、系统化的退出码和错误输出。

我目前主力用的语言是 Python,所以最常用的框架是 Typer。也用过一段时间的 Click,还有更底层的 argparse。三者的关系打个比方:argparse 是手写毛笔字,Click 是标准的钢笔字帖,Typer 则是直接给了你一个能自动排版的模板。

简单展示下 Typer 的写法:

import typer app = typer.Typer() @app.command() def export( source: str = typer.Option(..., "--source", "-s", help="源报表目录"), output: str = typer.Option("out.csv", "--output", "-o", help="输出文件"), dry_run: bool = typer.Option(False, "--dry-run", help="只演练,不真正执行") ): """导出并汇总报表。""" if dry_run: typer.echo(f"[dry-run] 将处理 {source} -> {output}") raise typer.Exit() # 真正的处理逻辑 typer.echo(f"处理完成: {output}") if __name__ == "__main__": app()

写完后框架自动帮你生成--help帮助信息、参数校验和错误类型,生产可用度一下子就上去了。Node 生态里对应的有 Commander,Go 阵营则是 Cobra 用得最多。选哪个取决于你最熟悉的语言,并不存在“唯一正确答案”。

2.4 我个人的选型原则

我的经验法则可以总结成一句话:“复杂度不超标,就不要引入框架;逻辑一复杂,就别再用 bash 硬撑。”具体到决策上:

  • 如果只是组合两三个现成命令,用 alias / shell 函数。
  • 如果涉及 5 行以上业务逻辑、解析数据、处理文件,写一个带解析参数的脚本。
  • 如果要交付给其他人用,或者自己会反复用超过一个月,直接用框架规范化整。

CLI-Anything 的核心不是一个大型项目,而是一套渐进增强的工具库,每个命令独立存在,互不干扰,该轻的轻,该重的重。

3. 实战:把一个“只能浏览器操作”的后台报表流程,变成一条 cli 命令

理论说完了,来看一个完整的实战拆解。这是我在内部做得最成功、也是复用频率最高的一个命令:把某后台网站的报表导出流程,整个封装成一条命令。

背景是这样的:某个运营数据平台只有网页端,没有开放 API。每天需要登录账号,进入报表中心,选择一个日期范围,点击“生成报表”,等页面异步加载几秒后,再点击“下载 Excel”,最后把文件移到指定目录并按日期重命名。一天一次,风雨无阻。

3.1 确定边界:哪些步骤值得自动化

在动手之前,我先列出了完整操作步骤,然后逐个打标:能自动化的、必须人工的、要不要留确认环节的。这一步非常关键,因为很多人做自动化一上来就奔着“全自动”去,结果把不该自动化的部分强行自动化,反而整出大麻烦。

我的边界划分结果:

  • 登录:自动,但首次登录需要人工介入处理验证码。
  • 设置日期:自动,取昨天日期作为默认值。
  • 点生成、等待加载:自动,轮询接口直到文件生成。
  • 下载:自动。
  • 重命名与归档:自动,但保留加--dry-run的演练模式。

技术选型我定了两条路线:能走简单requests模拟接口的就直接处理接口;接口无法直接复用的,就用 Playwright 无头浏览器模拟点击。最终这个场景是两者混合,因为登录之后就有一个带 token 的异步状态查询接口可以直接 POST 提取数据,反而不需要全程模拟点击。

3.2 无头浏览器的登录态处理与令牌复用

这个后台平台的登录流程不算复杂,但有一个验证码步骤,没法稳定自动识别。我的处理方式是:用 Playwright 打开有头浏览器,手动完成登录并保存 Cookie,之后的请求全部复用 Cookie,从而绕过无休止的验证码问题。实际步骤是:

  1. 第一次运行时启动带界面的浏览器,人工登录一次。
  2. 将 Cookie 序列化到本地文件,默认有效期设 24 小时。
  3. 后续任务直接加载本地 Cookie,写在请求头里发送。

核心代码大致长这样:

import asyncio import json from playwright.async_api import async_playwright async def login_and_save_cookie(storage_path: str): async with async_playwright() as p: browser = await p.chromium.launch(headless=False) # 第一次必须有头 context = await browser.new_context() page = await context.new_page() await page.goto("https://internal-report.example.com/login") input("请在浏览器中完成登录后,回到终端按回车继续...") await context.storage_state(path=storage_path) await browser.close() async def fetch_report(storage_path: str, date: str): async with async_playwright() as p: browser = await p.chromium.launch(headless=True) context = await browser.new_context(storage_state=storage_path) page = await context.new_page() # 进入报表页面,用页面里暴露的 API result = await page.evaluate("window.fetchReportData(...)") ...

这里最值得分享的一个经验是:能用隐蔽 API 的,就别纯靠点击模拟。点击模拟脆弱得离谱,只要页面样式一变、按钮动个位置,脚本就崩。通过 Playwright 打开页面后,直接从页面上下文里调用它内部的window方法或拦截 XHR 请求获取真实接口,稳定性和速度都会好很多。

3.3 输出设计:结构化、可管道、可继续处理

这条命令的最终输出不能只是把文件丢到目录里就算了。我的设计是:命令结束后,往标准输出写一行 JSON,包含文件路径、大小、下载耗时、报表日期等字段。这样既能人读,也能喂给下一个工具继续处理。

{"file": "reports/business_2025-01-14.xlsx", "size": 204800, "elapsed": 8.2, "date": "2025-01-14"}

为什么这么设计?因为 CLI 最强大的地方在于管道。如果我的导出命令输出的是干净可解析的 JSON,那我可以继续在后面接一个脚本做数据汇总,或者接一个消息推送命令,把“报表已生成”这个事件发到团队群。这就是 CLI-Anything 的核心价值——每个工具都是流水线上的一环,而不是孤岛。

4. 参数设计与交互细节:命令行工具是否好用的分水岭

功能做了出来,命令能跑,这只能算完成了一半。真正让一个 CLI 工具从“自己用的脚本”变成“团队里大家愿意用的工具”,取决于参数设计和交互细节。说直白点:如果你的命令帮助文本残缺、参数命名混乱、报错看不懂,那它就永远只能活在你自己终端里。

4.1 帮助信息与子命令的排布:替用户省掉读文档的时间

每写一个命令,我都会让自己跑一遍yourcmd --help。如果输出结果让一个从没读过代码的人也能看懂怎么用,那这步就过关了。

我常用的格式是:

用法: report-export [OPTIONS] COMMAND [ARGS]... 导出后台报表并归档。 选项: --date TEXT 报表日期,默认昨天 --output PATH 输出目录,默认 ./reports --dry-run 只演练,不真正执行 --verbose 显示详细日志 --help 显示此帮助信息 命令: login 登录后台并保存令牌 run 执行报表导出

这里我特别想提醒一点:子命令的名称要尽量是常用动词,而不是“process”“handle”“execute”这类看似专业实则没有信息量的词。比如login、run、cleanup就比auth、do、doWork直白得多,用户不用想就知道它干嘛的。

4.2 --dry-run 和 --verbose:两个最容易被忽略的救命开关

我给所有会“写文件”“删文件”“调外部接口”的命令至少留一个--dry-run。它做两件事:把整个流程跑一遍,但跳过所有有副作用的动作,在最后输出一份“如果正式执行,会发生什么”的清单。

留--dry-run的意义不只是防止手滑,更重要的是给了使用者一个低成本试错的路径。我自己在写脚本的时候,经常在正式执行前一小时跑一遍 dry run,看到输出完全符合预期才真正动手。没有这个开关的命令,每次执行都像闭眼开车。

--verbose则是日志分级的开关。默认只输出关键结果;开启后打印每一步的详细过程、请求耗时、中间变量、命中的文件路径等。这在排障时几乎就是救命的。下面是一个很简单但有效的分级日志写法:

import logging import sys logging.basicConfig( level=logging.DEBUG if verbose else logging.INFO, format=verbose and "%(asctime)s %(levelname)s %(message)s" or "%(message)s", stream=sys.stdout )

4.3 退出码与错误提示的“人话化”

退出码是 CLI 和自动化系统之间最底层的交互语言。约定俗成的标准是:0代表成功,非 0 代表失败。但很多自己写的脚本只有成功和失败两种状态,失败时一律exit(1),这给后续处理带来很大麻烦。

我更推荐按错误类型区分退出码,比如:

退出码含义场景
0成功正常完成
1通用错误未预期的运行时异常
2参数错误输入参数不合法
3依赖缺失系统中的某个命令或文件不存在
4外部服务失败调用的远程 API 连接不上或返回错误

除了退出码,错误提示的文案也很关键。我不止一次看到有人写ERROR: something wrong这样的提示,用户看到以后完全不知道哪里不对。好的错误信息至少应包括:错误的操作是什么、具体影响什么、下一步建议怎么做。

错误:无法读取输入目录 /data/raw。 原因:目录不存在。 已检查路径:/data/raw 请先创建该目录后重试,或通过 --source 指定其他目录。

这种提示虽然多写几行代码,但给人省下的脑力非常多。团队里其他人用你的命令,第一反应绝对不是去读源码,而是看报错信息能不能自己解决问题。

5. 翻车记录:CLI 项目最容易踩的坑

讲完设计思路,按惯例得翻一翻我实际踩过的坑。CLI 工具的坑和 Web 服务不一样,它藏得深、分布广,出了事往往是在别人的机器上、别人的环境里,用一句话概括就是“在我机器上明明可以跑”。

5.1 环境依赖造成的“在我机器上能跑”

命令行工具的执行效果极度依赖环境。同一个 Python 脚本,在 A 机器上跑得好好的,换到 B 机器上因为 Python 版本从 3.9 变 3.10,或者某个第三方库没装,立刻歇菜。

我目前的应对策略是三层:

  • 第一层:能用标准库的尽量不引第三方依赖。标准库最大的优势不是“自带功能全”,而是“无论去哪儿大概率都在”。
  • 第二层:必须用第三方库时,在命令的--version里带上版本号信息,并在启动时做一次依赖检查,缺少就明确报出。
  • 第三层:对于要交付的正式工具,直接用pipx这种方式安装成独立环境,避免和全局环境互相污染。

5.2 跨平台的文件路径与编码问题

你以为的路径是/home/user/data/file.txt,Windows 上可能是C:\Users\user\data\file.txt;你以为文件夹里全是 UTF-8 编码,本地客户给的文件却是 GBK 编码。跨平台问题可以说是 CLI 工具最常见的隐性炸弹。

解决路径问题最简单的方法:永远不要手动拼路径字符串,用pathlib或者os.path.join这类库函数生成。编码问题则是“读取时明确编码,写入时明确编码”,不要相信“默认编码”这四个字。我会在脚本开头强制指定环境变量:

import locale import sys if sys.stdout and hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8")

很多模糊的“乱码”“UnicodeDecodeError”,多半都是默认编码在作祟,提前处理能省下大量排查时间。

5.3 超时、重试与网络波动:另一个次元的问题

调用外部 API 的命令和纯本地命令有一个本质差异——前者会有随机的网络失败。不处理超时和重试的 CLI 工具,在稳定网络下一切正常,稍一抖动就各种怪异报错。

我给远程调用类命令统一套了一个装饰器式的重试逻辑:遇到超时或 5xx 状态码时,按指数退避重试 3 次,最大间隔 8 秒,超时就明确报错退出。这样表面上只是多了几行代码,实际稳定性提升是数量级的。附一个简化伪代码:

import time import requests from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=8), retry=retry_if_exception_type((requests.Timeout, requests.ConnectionError)) ) def call_api(url, **kwargs): resp = requests.get(url, timeout=10, **kwargs) resp.raise_for_status() return resp.json()

6. 沉淀自己的命令军火库:维护、命名、文档化

CLI-Anything 做到后期,考验的根本不是写代码能力,而是维护体系的能力。命令越来越多,怎么保证三个月后还能记得每条命令是干嘛的、参数是什么意思、依赖是什么?如果完全不管理,工具越多越混乱,最终只能放弃这个工作流。

6.1 统一目录与命名规范

所有自建命令统一放在~/bin/目录下(macOS 和 Linux 都默认支持这个路径下的PATH查找),然后按“动词-对象”的格式命名:

  • pdf-merge
  • report-export
  • img-compress
  • server-logs

不搞花哨缩写,不用驼峰,短横线分隔,一眼能看出来这条命令做什么。命名规范这件事越早定越好,因为命令一旦被别人记住,改名成本极高。

6.2 写文档,但不写废话

虽然我强调帮助信息要完善,但长文 README 其实没几个人会看。我更推荐在仓库或工具集根目录放一个简短的README.md,里面只有三类内容:

  1. 命令一览表,一行一个。
  2. 高频场景的速查示例。
  3. 每个命令的版本和依赖说明。

类似这样:

## report-export 后台报表导出工具。 ### 示例 # 登录并保存凭证 report-export login # 导出昨天的报表 report-export run # 导出指定日期,且先演练一遍 report-export run --date 2025-01-14 --dry-run

写文档的核心原则是:给三个月后的自己看,而不是给审核专家看。别写背景、别写意义、别写架构图,只要能让我十分钟内重新上手用这个命令就好。

6.3 让用法持续演化

最后一条经验是:CLI 工具不是写完就定死了,它应该随着使用频率和需求变化持续演化。我自己的习惯是每使用一个工具达到三次以上,就会顺手记录一下“哪里卡住了”“哪个参数顺序不对”。攒到三条优化点,就花一个下午集中改一版。

比如最开始我的img-compress命令只有一个固定压缩率,后来发现不同场景需要不同质量档位,于是进化出了--level 高/中/低参数;再后来发现有时候要保留 EXIF 信息,又加了--keep-exif开关。每次改动都很小,但日积月累,命令就从“勉强能用”变成了“离不开”。

我个人在这些年折腾 CLI 时最大的体会是:工具的复杂度应该跟着使用频率走,而不是跟着想象力走。高频操作才值得做 CLI,低频操作做了反而是负数。如果你也正在被某类重复劳动折磨,不要急着去学一个新框架、架构一套宏大方案,先把你最痛的那个流程拿脚本实现,加一个--dry-run,跑通一遍,再用起来沉淀。慢慢你就发现,这个“CLI-Anything”的工作方式会不知不觉长成你离不开的伙伴。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询