
这两年里我把越来越多日常操作往终端里挪从查天气、算时差、批量改文件到调公司的内部接口、定时跑数据清洗慢慢攒出来一个自己的命令行工具箱。今天想聊的CLI-Anything就是我基于这个需求搭起来的一个小项目。它不是大框架、不是云平台甚至连“框架”都算不上它是一套把任意脚本、任意API、任意重复操作统一封装成命令行命令的轻量方案。一句话命令背后替你干一堆事一个叫anything的入口装着你自己定义的几十个命令。CLI-Anything能做三件事第一把散落在各个目录的脚本归拢成一个命令入口第二把复杂、长串、容易记错的命令参数固化成可复用、可组合的CLI指令第三让非程序员同事也能在终端里安全地跑起来。适合谁适合所有平时要跟终端打交道的人——后端、运维、数据分析师甚至是用命令行处理素材的设计师只要你手里有“重复输入”的痕迹这套思路就能帮你省时间。1. 为什么“Anything”都想变成CLI1.1 终端才是效率最高的“收纳盒”过去我写过不少脚本散落在家里和公司的两台电脑上放在~/scripts/里的、放在项目tools/目录下的、还有临时扔在/tmp的。时间一长印象中“我好像写过一个处理日志去重的小工具”真要找的时候翻半天。CLI-Anything解决的第一个问题不是“减少打字”而是把散落的东西收进同一个抽屉。终端作为收纳盒有天然优势它几乎不需要启动时间可以写进定时任务可以串进管道pipe可以被其他脚本调用而且文本界面天然适合远程服务器操作。相比之下GUI工具没法方便地“记住上次操作”、不好自动化、也不利于分享给其他人。CLI-Anything就是把“我需要自己记忆和查找的东西”转化为“机器可执行、可检索的清单”。1.2 它解决的三个真实痛点真实工作里重复的痛点来来回回就那么几个。第一个是“长命令记不住”。比如连接远程数据库执行一段查询命令通常是这样mysql -h 10.20.30.40 -P 3306 -u root -p --default-character-setutf8 -e select * from user where create_time 2024-01-01每天敲一遍不现实写成脚本再改参数也麻烦。CLI-Anything的做法是固化成一个命令参数只暴露你真正关心的部分anything mysql-query --host prod --days 90第二个是“脚本之间互相调用靠粘贴复制”。A脚本输出的结果B脚本用的时候又重新实现一遍解析最后各管各的逻辑也会漂移。CLI-Anything把每个操作变成独立的命令命令之间可以通过标准输出互相衔接。第三个是“同事/朋友的电脑上总缺环境”。给同事一个脚本他可能缺Python库、缺JDK、缺某个工具。CLI-Anything把所有依赖写在一个地方安装的时候一条命令拉齐。1.3 和Makefile、Shell脚本、Ansible有什么不同有人会问Makefile不是也能定义命令吗Shell脚本不是更直接吗Ansible不是更强吗确实这些工具都沾边但侧重点不一样。我做了个简单对比方案适合的场景短板Makefile项目内部、和代码构建强相关语法诡异参数传递别扭离开项目目录就没法用Shell脚本一次性自动化、依赖系统命令跨平台差参数解析要自己写逻辑复杂后难维护Ansible配置管理、批量服务器操作重光理解playbook概念就有门槛不适合个人日常CLI-Anything个人/团队的命令行工具箱规模大了需要自己维护命令注册和文档CLI-Anything的定位不是替代上面任何一个它更像“抽屉”Makefile管项目构建Shell脚本管系统级操作Ansible管机器集群而CLI-Anything管“你这个人日常要用的几十个杂事”。它的核心优势是低门槛、统一入口、方便扩展。你可以把一个Shell脚本、一段Python代码、一个curl请求全部包装成子命令然后用完全一致的方式去调用它们。2. 核心设计拆解把“Anything”封装成命令行2.1 入口注册机制函数即命令脚本即命令CLI-Anything的设计核心是“注册”。拿Python版本来说它借鉴了Click和Typer的思路一个装饰器、一个函数就是一个子命令。# anything/cli.py from cli_anything import Cli app Cli(anything) app.command(hello) def hello(name: str world): 跟命令行世界打个招呼 print(fHello, {name}!)这个装饰器背后做三件事把函数名或自定义名注册进命令表解析函数的签名自动生成参数定义把函数的docstring变成--help里面的说明。这样写的好处是——定义命令的成本被压到了极致你只需要专注于函数内部逻辑命令怎么被调用、参数怎么解析、帮助怎么写全部由框架搞定。不是所有命令都需要写Python。CLI-Anything也支持“透传命令”即直接包装一条外部命令app.external(ip-location) def ip_location(ip: str): return [curl, -s, fhttps://ipinfo.io/{ip}]注册的时候只是告诉CLI-Anything“运行这条系统命令”真正执行时才发起调用。这种方式适合快速接入已经有现成CLI的工具避免重复实现。2.2 参数解析与校验命令行工具的脸面参数解析是命令行工具最容易做烂的部分。新手写CLI喜欢自己用sys.argv切片取值然后写着写着就变成一锅粥位置参数、可选参数、标志参数混在一起报错信息还看不懂。CLI-Anything里的参数解析封装了三层能力。第一层是类型自动转换。你声明count: int 1传进来的字符串就会被自动转成整数声明verbose: bool False命令行里出现--verbose就置为True。不用自己写int(sys.argv[2])这种脆弱代码。第二层是必填与默认值的校验。缺了必填参数直接报错提示是哪条命令、缺哪个参数、期望什么类型而不是Python解释器那种一大段traceback。第三层是参数来源的扩展。参数不一定只在命令行里给也可以来自环境变量、来自配置文件或者来自一个交互式提示。CLI-Anything把参数来源串成一个“回退链”命令行参数 环境变量 配置文件 交互式提问 默认值这个设计帮了大忙。比如数据库密码直接写在命令行里容易被shell历史记录泄露写成配置文件又有泄露风险从环境变量读取既安全又统一。把“从哪取值”从业务函数中独立出来函数里只写db_password调用方只需要关心优先级。2.3 配置管理不同环境、不同机器同一套命令日常脚本最容易踩的坑就是“环境写死在代码里”。一个人的脚本测试环境地址、线上环境地址、本地路径全写在文件顶部换台机器就要改一遍源码。CLI-Anything做了一个分层配置的方案。anything --env dev deploy service-a anything --env prod deploy service-a核心是环境变量和配置文件的分层全局配置放~/.config/anything/config.toml项目配置放./.anything.toml启动参数里的--env决定加载哪个环境块。[dev] api_base http://localhost:8080 timeout 5 [prod] api_base https://api.internal.example.com timeout 30这个机制的价值在于业务逻辑完全不感知环境信息函数里面对api_base时CLI-Anything已经把正确值注入了。新同事加入只需要复制一份配置模板改掉私有的密钥部分就行。配置模板本身也可以放进代码仓库用.toml.example的方式管理。2.4 输出格式让结果可读、可解析命令行工具的输出有真有R问题——不是打印不出来而是打印出来的东西没人能直接用。print(json.dumps(data))确实是JSON但print(查询成功!)也是输出混在一起就是灾难。CLI-Anything强制了一个约定配合使用标准输出和标准错误流。正常的业务数据写标准输出stdout日志、调试信息、进度提示写标准错误流stderr。这样至少有两个好处终端里看输出不会被日志打断想要管道处理时anything xxx | jq .field拿到的是纯粹的数据。app.command(user-info) def user_info(user_id: str, output: str text): user fetch_user(user_id) if output json: print(json.dumps(user, ensure_asciiFalse, indent2)) elif output table: print_table([user]) else: console.print(f用户名: {user[name]}, stylebold green)输出格式不做死但默认会同时支持--output json和--output text两种模式。JSON模式方便机器读text模式方便人读。命令行工具绝不是越复杂越好“能进管道、能看明白”才是底线。3. 实操20分钟搭一个CLI-Anything工具箱这一节是我的实操过程。这个方案我在本地跑得很顺你可以直接照着搭一套也可以在它的逻辑上做裁剪。3.1 项目目录结构与依赖先说目录结构。不管底层语言是什么CLI工具项目都要把“入口”“命令集合”“配置模板”分开。我用的结构是这样的anything/ ├── pyproject.toml ├── config.example.toml ├── anything/ │ ├── __init__.py │ ├── core/ │ │ ├── cli.py # 入口与注册逻辑 │ │ ├── config.py # 配置加载 │ │ └── executor.py # 命令执行与透传 │ ├── commands/ │ │ ├── devops.py # 我把“运维”类命令放在这里 │ │ ├── data.py # 数据处理类命令 │ │ └── misc.py # 日常杂项命令 │ └── settings.py # 版本号、元信息入口文件不复杂核心就是建一个Cli实例把各命令模块里的注册函数全部加载进来然后启动。# anything/__init__.py from cli_anything import Cli from commands import devops, data, misc app Cli(anything, version0.1.0) app.register_module(devops) app.register_module(data) app.register_module(misc) def main(): app.run() if __name__ __main__: main()3.2 命令注册与实现的完整代码我挑一个实际例子把一个内部服务的数据查询封装成命令。先说参数设计——不做任何过度设计只暴露三个必选项和两个可选开关。# anything/commands/devops.py from cli_anything import command, opt import requests command(query-users) def query_users( service: str, days: int 7, limit: int 50, output: str table, verbose: bool False, ): 查询某服务下的活跃用户列表。 适用于日常数据核对、报表导出前的快速预览。 cfg get_config() # 环境配置注入 url f{cfg.api_base}/api/{service}/active-users params {days: days, limit: limit} if verbose: stderr(f请求 {url}参数 {params}) resp requests.get(url, paramsparams, timeoutcfg.timeout) resp.raise_for_status() users resp.json()[data] if output json: print(json.dumps(users, ensure_asciiFalse, indent2)) else: print(f共 {len(users)} 个活跃用户) for u in users[:limit]: print(f- {u[name]} 最近登录: {u[last_login]})注册的过程由register_module自动完成——扫描模块里带command装饰器的函数把它们挂到app下。不需要手写一行注册代码。command(summarize-log) def summarize_log(log_path: str, top_k: int 10): 快速统计访问日志里的Top IP和Top页面。 from collections import Counter ips, pages Counter(), Counter() with open(log_path, r, encodingutf-8, errorsignore) as f: for line in f: parts line.split() if len(parts) 7: ips[parts[0]] 1 pages[parts[6]] 1 print( Top IP ) for ip, cnt in ips.most_common(top_k): print(f{ip:20s} {cnt}) print( Top Page ) for page, cnt in pages.most_common(top_k): print(f{page:40s} {cnt})这个命令看起来简单但在实际工作中替我省了大量时间。以前要看日志要么grep | sort | uniq -c | sort -rn一串流水要么写个一次性脚本。现在直接anything summarize-log /var/log/nginx/access.log --top-k 20结果一目了然。3.3 把CLI安装成全局命令并按Tab补全命令写好了不安装成全局命令等于白写。pyproject.toml里的配置决定命令行工具怎么暴露# pyproject.toml [project.scripts] anything anything:main [project.dependencies] click 8.1 requests 2.31 toml 0.10开发时用pip install -e .生产环境用pip install .。之后在任何目录敲anything都能启动。Shell自动补全是个很多人忽略但体验提升巨大的功能。CLI-Anything提供一个生成补全脚本的入口anything completion bash ~/.bash_completion.d/anything.bash anything completion zsh ~/.zsh/completions/_anything补全的是子命令名、已注册的参数名甚至是枚举值。比如anything query-users --output按两下Tab系统会自动弹出table和json不用翻源码去猜“这个参数是叫output还是format”。3.4 跑起来的效果我当时做得差不多就把所有命令列出来看了一下$ anything Usage: anything [OPTIONS] COMMAND [ARGS]... Commands: query-users 查询某服务下的活跃用户列表 summarize-log 快速统计访问日志里的Top IP和Top页面 hello 跟命令行世界打个招呼执行一个带参命令$ anything query-users user-biz --days 3 --output json输出的JSON我能直接用脚本拉取来做日报。最妙的是它不依赖任何图形界面SSH进服务器、跳出公司网络只要装了这个包就能跑。4. 常见问题与排查技巧实录用了一段时间把踩过的坑集中记录在这里。这些内容不大可能写在官方文档里应该能帮你省点时间。4.1 子命令重名和模块加载冲突CLI工具最常见的问题是命令多了以后重名。两个不同的模块里都定义了query装饰器后注册的会把前一个覆盖掉而且不会报错——这个问题一开始我根本没意识到直到某天执行命令发现结果和代码不一致。排查思路是先看命令表anything commands --verbose这个命令会把每个子命令对应的源码文件路径打出来。如果发现两个query指向不同文件说明有重复注册。我的建议是每个命令的命名前缀尽量带上业务分组比如db-query、api-query、log-query别用宽泛的query。命令是给人敲的稍微多个词不会增加多少记忆成本。4.2 配置加载顺序造成的迷之故障有一阵子命令“偶尔会连错环境”。查了好半天才发现问题出在配置优先级上。CLI-Anything的设计是“命令行参数 环境变量 配置文件”但我在代码里用了自定义的get_config()它自己又去读了一遍.anything.toml两套逻辑互相干扰。后来吸取教训配置加载统一走框架提供的接口业务侧不要自己二次读取配置。如果你的命令确实需要新的配置项先在框架层定义字段再在命令里通过注入拿值。4.3 跨平台执行Shell命令的坑external透传命令在macOS/Linux上工作正常换到Windows就出问题。不是这个项目特有的毛病是跨平台从来都难。比如rm -rf在Windows里不存在curl默认也不是系统自带新版本PowerShell里的curl是Invoke-WebRequest的别名。如果只是团队内部用最简单的是锁死平台统一跑在Linux容器或服务器上。如果你确实需要跨平台那就注意三点不用shell的专有语法执行外部命令时用数组形式传参避免被shell误解路径拼接全部用pathlib。# 不要这样 subprocess.run(frm -rf {target}, shellTrue) # 这样更稳妥 subprocess.run([rm, -rf, target])4.4 中文输出乱码和编码问题CLI工具中文乱码主要有两个来源源码文件编码没指定、终端编码不匹配。我遇到过一次在Windows终端上输出的中文全是???。这不是CLI-Anything的问题是Windows终端默认字符集导致。两个解决办法第一文件里所有字符串都统一做UTF-8声明写文件时encodingutf-8第二Windows上在命令前设置chcp 65001或者在代码里强制重新配置标准输出编码。Python 3里我更喜欢直接在入口做一层处理import sys if sys.platform win32: sys.stdout.reconfigure(encodingutf-8)4.5 命令执行太慢卡住没反馈网络请求类型的命令最容易出现“卡住没反馈”。你在终端里干等也不知道是网络慢、参数错了、还是远端服务挂了。CLI-Anything的做法是在框架层加默认超时和--verbose开关。实践下来“加超时、加进度提示”效果很好具体值参考你命令的实际情况内部服务给10秒外部API给30秒批量任务就分块打印进度。现象大概率原因处理方式命令无法执行提示command not found包没有安装成全局命令检查pyproject.toml的[project.scripts]配置参数校验报错类型不对声明类型和传入值不匹配查看函数签名确认int、bool等类型标注有部分命令能跑有部分报ImportError命令模块里有依赖缺失把依赖统一写到pyproject.toml不要散在各处输出中文乱码终端编码问题入口强制UTF-8或设置终端代码页65001同一条命令在不同环境结果不同配置优先级混乱用配置分层不要自己二次加载配置文件5. 进阶玩法把CLI变成你自己的工作流5.1 与定时任务和通知结合CLI-Anything的命令本身就是标准化命令所以可以无缝接进cron、systemd timer或者CI流水线。我自己做了两个场景早上九点自动跑一次数据汇总生成Markdown日报推到群里凌晨扫描磁盘占用超过阈值时发一条通知。底层原理很简单——命令既然可以直接执行定时任务不需要任何额外适配# crontab 0 9 * * * cd /path/to/anything anything daily-report --output json 2 /var/log/anything.log */30 * * * * cd /path/to/anything anything disk-watch --threshold 85 /var/log/diskwatch.log把CLI命令用进定时任务有一个额外要求是命令必须支持非交互式运行也就是所有参数都能通过命令行参数、环境变量、配置文件拿到不能有“命令中途弹一个选择”的逻辑。如果你之前把交互式提问当成默认值回退链的一环那定时任务执行时会因为拿不到输入而卡住。要专门加一个--non-interactive开关。5.2 做成团队共享的“内部工具箱”CLI-Anything这个项目后来被我整理成了团队内部的一个公共包。每个成员安装后不用看文档就能用anything命令做日常操作。团队场景比个人场景多几个要求第一个是权限。团队成员用的命令不能都走管理员权限CLI-Anything在注册命令时可以声明执行级别调用时检查当前用户是否匹配。实现不复杂本质是执行前创建一个“能力上下文”命令声明需要的角色框架判断角色够不够。第二个是命令的审计。审计的目标不是人盯人而是“出了异常有据可查”。CLI-Anything在非交互模式下会把执行命令、传入参数、执行人、执行时间写入本地SQLite文件版本号带上调试线上问题的时候能定位是哪个版本、哪个参数组合触发的坑。第三个是文档自动生成。只要函数写了docstring命令说明就能自动汇总成一份Markdown文档发布到内部Wiki。省去“代码更新了文档没更新”的经典烦恼。命令一旦多了手工维护文档必然滞后让文档从代码中生成才是可持续的。5.3 还能扩展的插件化方向目前CLI-Anything对“Anything”的支撑按类型分是三类Python函数命令、外部命令透传、以及Shell脚本包装。其实还可以继续扩展HTTP请求模板化把常见的API调用保存成模板参数动态填类似Postman但走CLI远程执行连上跳板机再执行目标机器上的命令交互式向导没有命令行基础的使用者首次运行命令时通过问答方式一步步生成参数内部配置好之后就不需要再交互。我个人的体会是CLI工具做到“非交互可用、交互可引导”这条线就足够覆盖绝大多数场景了。操作上大家各自发挥。写在最后的几点心得把这个项目从一团乱麻整理成CLI-Anything踩了不少坑也留下几个比较大的体会。第一CLI工具最忌讳过度设计。不要一上来就搞分布式、搞插件市场、搞Web管理界面。真正用的还是那十几个高频命令先把它们做好、做稳定再说扩展。用户包括你自己对你的CLI的印象很大程度上取决于第一次执行时的反馈——帮助文档清晰、参数及时校验、输出干净这些比很多花哨功能重要得多。第二做命令行工具本质上是在做“API设计”那个API的名字叫终端。对用户来说命令名字代表他记住的成本参数名代表他每次敲击的成本输出格式代表他理解结果的时间。CLI-Anything能取得比较好的效果很大程度是因为命名遵循“动词-宾语”结构参数名尽量和业务语言一致输出保证人能看懂、机器能解析。第三也是我最想分享的一个小技巧把CLI命令帮到你自己的“懒人时刻”。不要等到写复杂脚本才想起ClI下次你打开浏览器查个什么东西或者手动处理一个重复文件先停下来想想“这个动作能不能命令化”。哪怕是最简单的两行脚本只要跑过一次、命令化过一次后续每次使用都是在赚钱。这些被节省的时间长期积累下来是非常可观的也是命令行这个老工具依然能带来幸福感的原因。如果你手里也有一堆脚本或者重复操作可以试试搭一套自己的CLI-Anything不用照搬我的实现抓住“统一入口、参数校验、配置分层、输出规范”这几个基本原则你的工具库也会变得好用起来的。