☰
CLI-Anything:用Go打造统一命令行工具,终结日常琐碎命令
2026/9/29 18:51:16 网站建设 项目流程

1. 为什么我会做“CLI-Anything”这个项目

1.1 痛点:日常命令七零八落

先说说这项目的起因。我自己是重度终端用户,每天的工作流里有大量零散的小需求:查一个端口被谁占用、批量改文件名、把一个JSON里的某个字段抠出来、把时间戳转成可读日期、快速生成一堆测试密码、对两个配置文件做差异对比……这些事单独看都不难,但问题是它们分布在完全不同的命令和工具里,有的要装单独的软件包,有的要写一次性Python脚本,有的干脆记不住参数只能现场翻文档。

时间一长你会发现,真正消耗精力的不是任务本身,而是“在不同工具之间来回切换”的上下文成本。比如你本来在改一个服务端配置,忽然要验证一段JSON结构是否合法,于是你打开浏览器查在线工具,把数据粘进去复制结果,再切回终端;隔半个小时又要给一批图片做尺寸调整,你又要回忆ImageMagick的参数,或者去搜一条python脚本。这些琐碎操作相互之间没有关系,却频繁打断主线工作。

CLI-Anything就是冲着这个问题去的。它的核心想法很简单:把日常开发里那些“高频但分散”的小任务,收敛到同一个命令行入口里,用一套统一的规则和风格来操作。不需要记住几百个分散的参数,不需要安装一堆各自为政的小工具,一行命令解决问题,然后回到主线工作。

1.2 定位对比:为什么不做现成工具

动手之前我其实认真评估过现成方案。像fzf、jq、yq、ripgrep、fd这些单点工具确实都很优秀,每个单拎出来都是各自领域的标杆。但它们解决的是“某个具体场景”的问题,不会管你的时间戳转换、密码生成、端口占用检查。另一个方向是那些“全家桶”式的终端工具集,比如有些开发者把一堆shell脚本攒到一起做成自己的toolbox,这个思路很对,但问题是个人脚本往往标准化不足,换台机器就得重新适配,给同事推荐也不好意思拿出手。

还有一个考虑是跨平台一致性。公司里有人用macOS,有人用Windows WSL,有人用Linux,如果依赖bash脚本加Linux专用命令,Windows那边基本废掉。CLI-Anything从设计第一天就用跨平台语言实现,核心逻辑不依赖操作系统特定命令,这才有可能让团队里所有人在同一个工具语言下工作。

当然,我也很清楚这个项目的边界:它不打算替代jq处理复杂的JSON管道流,不打算替代ImageMagick做专业图像处理,更不打算成为代码编辑器的替代品。它的定位是“高频轻量任务的统一入口”,把80%场景下大家重复在做的琐事,用统一命令解决掉。剩下的20%复杂需求,它把基础能力给你,然后你自己组合。

注意:我这里说的“统一入口”,不是要把几十个工具塞进一个包,那样只会变成另一个臃肿的怪物。克制,是这类工具最重要的品质。

2. 整体设计与核心模块拆解

2.1 架构设计:一个入口,N个子命令

CLI-Anything的架构可以用一句话概括:“一个可执行文件,统一命令分发,模块化子命令。”整体上模仿了git的经典设计——主命令后面跟子命令,每个子命令是一个相对独立的模块。这样的好处显而易见:新加功能不影响已有命令的稳定性,用户的记忆负担也被限制在“cli + 动作”这个最小模式。

cli anything文件中 # 文件差异对比 cli http GET https://api.example.com # HTTP接口测试 cli json extract '$.user.name' input.json # JSON字段提取 cli ts 1780000000 # 时间戳转日期 cli pass 16 # 生成16位随机密码 cli net port 8080 # 查端口占用

所有子命令的返回格式也做了统一约定:普通模式下直接输出人眼可读的结果,--json模式下输出结构化数据,方便在脚本里接管道继续处理。这套约定从项目最开始就定下来,后面扩展的所有模块都遵守,避免了“每个命令一种风格”的混乱。

2.2 核心技术选型:为什么用Go

技术栈的选择我想了很久。最开始用Python写过一版原型,开发速度确实快,几天就攒了十几个模块。但打包分发是个问题:同事用起来要么装Python环境,要么用pip安装,依赖一多就开始闹情绪。而且Python的启动时间虽然不至于不可接受,但对一个“高频小命令”工具来说,每次多花几百毫秒都是很真实的使用成本。

最后我改用Go重写。原因很直接:交叉编译一个静态二进制文件,丢到服务器、Mac、Windows上都能直接跑,不用装任何运行时;编译产物启动快,体感上和原生命令没有差别。加上Go的标准库本身覆盖了文件处理、JSON解析、网络请求这些核心需求,第三方依赖可以控制得很少,维护起来省心很多。

这里顺便说下项目结构。我自己习惯把每个子命令拆成一个独立的package,入口文件只负责注册路由:

cli-anything/ main.go cmd/ root.go # 入口与全局参数 file.go json.go http.go time.go pass.go net.go internal/ render.go # 输出格式统一处理 validate.go # 参数校验公共逻辑

这种结构的价值在你新增模块时会非常明显:新写一个package,在入口挂载一行路由,编译就能用,完全不需要碰其他模块的代码。对一个人维护的工具接着说“重构”,这句话是我切身体会。

2.3 命令设计的原则

CLI-Anything的命令参数遵循几个硬性约定:

第一,短参数用于最高频的操作。比如cli json extract '$.user.name' file.json,其中'$.user.name'是必选参数,因为它就是这个命令存在的理由。第二,所有可选项提供长参数,不做隐式魔法。第三,所有输入文件支持-作为标准输入占位符,这样就能跟其他命令无缝管道衔接。

举个管道衔接的例子:

cat app.log | cli json extract '$.level' -

这在排查线上问题时特别好用,整个处理链不需要中间文件。没有这套约定,命令与命令没办法配合,工具的价值就会大打折扣。

3. 实操实现:从零搭出CLI-Anything

3.1 命令路由与全局配置

入口部分我用的Go标准库flag配合手工路由,刻意避开了重量级command框架。当初这么选不是因为不会用框架,而是担心框架带来的子命令惯例、help格式化、全局Flag覆盖等等机制,会把工具复杂度抬高。一个内部工具的宿命常常就是“因为太重而没人用”,与其预支复杂度,不如轻装上阵。

核心路由代码大概长这样:

func main() { if len(os.Args) < 2 { usage() os.Exit(1) } switch os.Args[1] { case "file": RunFile(os.Args[2:]) case "json": RunJSON(os.Args[2:]) case "http": RunHTTP(os.Args[2:]) case "ts": RunTime(os.Args[2:]) case "pass": RunPass(os.Args[2:]) case "net": RunNet(os.Args[2:]) default: fmt.Printf("unknown command: %s\n", os.Args[1]) usage() os.Exit(1) } }

注意这个实现的边界:每个子命令的RunXxx函数只负责解析自己的参数,不写任何跨模块的共享状态。全局唯一的输出约定是返回码——0代表成功,1代表业务失败,2代表参数错误。规范化返回码这件事,对将来写shell脚本或者接自动化流程特别重要。

3.2 高频模块实战一:文件差异对比

第一个实用模块是文件对比。我日常工作里经常需要确认两个配置文件改了什么、两段日志差异在哪、发布前后的构建产物有什么变化。直接diff当然也行,但diff的输出在终端里不够直观,而且对大文件性能一般。

CLI-Anything的file diff采用“分块匹配+行级对比”策略,输出分成三块:只出现在A文件的行、只出现在B文件的行、两文件共有但顺序不同的区域。对于配置文件的对比,用不着LCS级别的精细diff,逐行对比加分组展示就足够定位问题。

cli file diff old.conf new.conf

输出示例(简化):

[old.conf only] - max_connections = 200 - enable_cache = false [new.conf only] + max_connections = 500 + enable_cache = true [unchanged] timeout = 30

在做这个模块时,我补了一个很多人会忽略的细节:文件编码处理。Go的字符串默认假设UTF-8,但实际线上很多配置文件或者Windows导出的文本是GBK编码,直接读会得到一堆乱码。我在读取文件时做了编码探测,GBK编码自动转成UTF-8再比较,否则这个工具在中文环境里基本没法用。

3.3 高频模块实战二:JSON字段提取与校验

json模块可能是CLI-Anything里用得最多的部分。它内置了三个子命令:extract(提取字段)、validate(校验格式)、walk(遍历结构)。其中extract支持一个简化的路径语法,不需要完整实现JSONPath那么复杂的东西:

cli json extract '$.user.name' user.json cli json extract '$.items[0].id' list.json cli json extract '$.users[*].name' list.json

路径解析逻辑并不难,关键在于容错:路径中任意一层不存在时,默认输出空并返回0还是不存在的错误?我最后选择的做法是加一个--strict参数,默认宽松模式输出空值,方便在管道里快速筛查;运维脚本里建议加上--strict,避免静默吞掉结构异常。这个取舍在实际使用中反复被证明是对的——不同场景对错误容忍度的要求是完全不同的。

validate子命令则很简单,读入文件或标准输入,尝试解析成合法的JSON结构,能解析就输出 “valid JSON object/array”,不能解析就输出错误位置和原因。它非常适合在提交数据前做快速校验,或者检查API响应是不是合法JSON。

3.4 高频模块实战三:批量文件重命名

批量重命名这个需求看上去简单,真正做起来有不少细节。CLI-Anything的file rename使用的是“匹配规则+替换模板”的模式,而不是那种要手写循环的脚本:

cli file rename '*.jpg' --from 'IMG_{n}' --to 'photo_{n}.jpg' --start 1

具体行为是:扫描当前目录下所有符合*.jpg的文件,提取原始文件名中的编号,按对应模板生成新名字,批量执行。所有变更先做干跑(dry-run)打印预览,加--commit参数才真正落盘。这个设计极大避免了手滑批量改名后找不回来的惨剧。

提示:任何批量写操作,务必先不加--commit跑一次,确认预览结果符合预期,再加提交参数。这个习惯让我至少避开了三次灾难性操作。

3.5 高频模块实战四:HTTP接口测试

HTTP模块解决的是终端里快速验证API的场景。不做成Postman那样复杂的GUI,只做四件事:发送请求、自定义Header和Body、查看响应头和状态码、格式化输出JSON响应体。

cli http POST https://api.example.com/users \ --header "Authorization: Bearer xxx" \ --body '{"name":"alice","age":30}' \ --json

在实现这个模块时,我特别处理了几个痛点:自动跟随重定向并打印重定向链(排查接口跳转问题很有用)、当响应头里没有Content-Type时自动嗅探格式、连接超时统一控制为5秒防止命令卡死。这台工具在实际排查本地服务时,几乎替代了我手动开浏览器的习惯。

3.6 高频模块实战五:时间戳转换与随机密码

这两个小模块看着不起眼,但完美诠释了CLI-Anything“高频、轻量、统一”的目标。

时间戳转换提供了三种模式:时间戳转本地时间、时间戳转UTC时间、本地时间转时间戳。我特意让输出默认同时显示ISO格式和相对时间(“3小时前”),因为实际追日志时这两种格式都经常需要:

cli ts 1780000000 cli ts now cli ts "2026-06-01 12:00:00" --from-format "2006-01-02 15:04:05"

密码生成则使用Go的crypto/rand,绝不使用伪随机函数。默认输出16位混合字符,支持--no-symbols、--length等参数。安全性上有一个小细节:生成后不在终端回显默认值,只有显式配合--show才会打印,这个决定是出于防止密码意外出现在终端日志里的考虑。

4. 常见问题与排查技巧实录

4.1 参数解析的坑:子命令与全局参数混淆

早期版本犯过一个典型的参数解析错误:全局参数--json和子命令参数混在一起时,解析逻辑会乱。比如cli file diff --json a.conf b.conf能不能识别出--json是全局输出格式,而不是子命令的参数?

我的解决方案很“笨”但很有效:在入口处先轮询一次全局参数并全部过滤掉,再传给子命令解析函数。这不优雅,但能保证兼容性,而且实现完全透明。这个问题也说明了一个道理:小工具的设计宁可简单、可预期,也别引入太多黑魔法。

4.2 编码问题:Windows上的隐藏坑

跨平台最大的坑不在逻辑,在文件编码与行尾符。Windows上常见的\r\n与Unix的\n,会让文件对比和行号定位全部错位。我在所有读文件逻辑里统一做行尾符归一化,同时保留--raw参数用于那些确实需要原始字节的场景。

在Windows使用场景下还有一个环境变量坑:用户目录的路径区分C:\Users\name和/home/name,所有默认路径相关的逻辑都要检测操作系统,不能写硬编码。

4.3 性能优化心得:编译产物大小与启动时间

很多人忽略CLI工具的性能体感,但恰恰是“快”决定了你会不会在关键时刻想起它。Go程序启动通常在几十毫秒以内,但如果导入过多重量级依赖,启动会变慢,产物体积也会膨胀到几十MB。CLI-Anything的优化策略很简单:

  • 尽量只用标准库;
  • JSON路径解析这种轻量逻辑手写,不引入完整JSONPath库;
  • 使用-ldflags="-s -w"交叉编译时去掉调试信息,产物体积能减小约30%。

实际编译性测试中,CLI-Anything当前二进制大小约9MB,启动时间稳定在20–30毫秒,在交互体感上和系统自带命令已经很难区分。

4.4 使用中容易踩的坑速查表

现象原因解决方案
json extract输出为空但无报错路径写错或数据结构不符加--strict强制报错,或用json walk查看实际结构
file diff显示乱码源文件编码不为UTF-8确保读取时编码探测生效,或手动转码后再比较
HTTP请求超时目标服务未启动或网络不通加上--verbose查看详细错误,但注意不含敏感信息
批量重命名执行后没生效忘记加--commit这是设计行为,先看预览,确认后再提交
端口查询显示结果为空权限不足或协议类型不对检查当前用户权限,部分端口查询在非root下受限

5. 使用姿势与扩展思路

5.1 日常效率场景组合拳

单独使用每个子命令只能解决零散问题,把这些命令组合起来,效率才能真正体现。

一个非常常见的排查链路是:查看服务日志里某个时间段的错误、提取JSON字段中的用户ID、批量查询这些ID对应的状态。用CLI-Anything做这套组合时,管道写法非常顺畅:

grep "ERROR" app.log | cli json extract '$.user_id' - | sort -u | while read id; do cli http GET "https://api.example.com/users/$id/status" --json | cli json extract '$.status' - done

另一个典型场景是发布前的文件对比与备档。我用一个两行脚本把发布产物的哈希值、文件大小、差异摘要一并生成,打包成发布备注。以前这些字段散落在不同的命令和工具里,现在统一走CLI-Anything,出错的概率小了很多,排查也方便。

5.2 插件化扩展思路:新增一个子命令的成本

CLI-Anything从一开始就刻意保持“主程序+子命令包”的结构,新增子命令的成本被压到很低。比如我想加一个hash子命令,只需要新建cmd/hash.go,实现参数解析和核心逻辑,然后在入口的switch里增加一行:

case "hash": RunHash(os.Args[2:])

这其实给未来的扩展留下了一个很自然的路径。如果哪天需要团队共享,可以为它加一套简单的配置系统,让某些场景参数写入配置文件,而不是每次敲一长串命令。也可以考虑提供--completion参数自动生成shell补全脚本,降低大家的使用门槛。

但这些扩展有一个共同前提——必须守住设计原则:命令风格统一、返回码统一、输出格式统一。任何为了某个单一场景破坏统一性的改动,都是在透支这个工具的长期价值。

5.3 写在最后的一个小技巧

用了CLI-Anything半年之后,我最大的体会是:命令行的效率不取决于单个命令有多强,而取决于多个命令之间的组合能力有多顺。单纯把“功能做出来”只完成了一半,另一半是让每个命令的参数风格、输出格式、返回码保持一致,让它们可以自由地像积木一样拼在一起。这套一致性设计带来的收益,远比多写几个子命令大得多。

如果你也想动手做类似的个人工具,我的建议是从自己最常用的三个场景出发,做一个最小可用版本跑起来。不需要一上来就规划几十个模块,先把三个命令做扎实,用顺手之后自然知道该扩展什么。这比一开始设计一个宏大架构,最后瘫在维护里要实用得多。

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

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

立即咨询