Homebrew版本信息转JSON:环境盘点与自动化实践 📅 发布时间:2026/9/7 19:04:00 👁 浏览次数: Mac 上写代码的人十个有九个绕不开 Homebrew。装软件一个brew install查依赖一个brew info升级一条brew upgrade看似简单但真要让你把这台机器上到底装了哪些包、各自是什么版本、哪些是你主动装的、哪些只是捎带手的依赖盘清楚大部分人就开始挠头了。我最近在项目里就遇到这个需求需要把几十台开发机的软件环境导出成统一格式给巡检脚本和可视化页面用。Homebrew 的默认输出是给人看的文本字段随版本变化程序解析起来非常痛苦。到最后我选定的方案只有一个——把所有版本信息转成 JSON。JSON 格式互联互通能力强Python、Shell、Go、前端、监控平台都能直接消费这才是自动化该有的样子。这篇博文就把我是怎么把 Homebrew 版本信息转成 JSON 的完整思路讲清楚包括底层命令原理、三种可直接复用的实操方案、JSON 字段怎么读以及我在过程中踩过的坑和排查思路。无论你是刚接触 Homebrew 的新手还是天天写部署脚本的老手只要需要在 Mac 上做环境管理和自动化这篇文章都能给你一份能直接抄作业的参考。1. 为什么要把 Homebrew 版本信息转成 JSON1.1 一个真实的场景开发机环境盘点先抛一个我实际遇到过的场景。项目组要求每周出一份开发机软件环境清单用来做资产盘点和依赖审计。以前的做法是让每个开发手动跑brew list --versions然后把终端输出贴到表格里结果五花八门有人贴了完整路径有人只贴包名有人漏了 casks根本没法统一处理。后来我写了个巡检脚本需要在每台机器上抓取 Homebrew 里的安装明细再汇总到中心服务器。这时候就发现一个核心问题Homebrew 文本输出根本没有稳定的机器可读格式。brew list --versions输出的每一行都类似jq 1.7.1表面看是包名 版本但一旦遇到 versioned formulae比如python3.9和python3.11同时存在、或者某些包提示是依赖安装的格式就乱了。更不用说brew list默认还会把 casks 和 formulae 混在一起它们的命名规则和输出顺序完全不一样。与其写一堆脆弱的正则去猜不如直接从源头拿结构化数据也就是 JSON。1.2 JSON 比文本强在哪很多人会问我都已经把文本导出来了转成 JSON 到底有什么实际好处我总结成三点。第一结构稳定。JSON 里有明确的字段名和层级比如versions.stable表示上游稳定版本installed[0].version表示本机实际安装的版本程序只要按字段取就行不用猜。第二跨语言、跨系统。几乎所有语言都有现成的 JSON 解析库Shell 里有 jqPython 里有 json 模块JavaScript 里直接JSON.parse前端页面、监控平台、CI 系统全部原生支持。第三可比较、可入库。JSON 可以稳定地存进数据库做历史对比也可以和另一个环境的 JSON 做差异分析这在纯文本时代几乎不可能。1.3 这篇指南适合谁这篇内容主要面向三类人第一类是需要在 Mac 上做开发环境自动化的工程师第二类是写 CI/CD 流程或环境巡检脚本的运维和 SRE第三类是刚接触 Homebrew 和 JSON、想搞明白格式化输出到底是什么的同学。我会尽量把命令背后的原理讲清楚这样即使 Homebrew 以后升级改了一些细节你也能举一反三。2. 先搞懂 Homebrew 的输出逻辑2.1 常用命令输出形态对比在动手转 JSON 之前我建议先把 Homebrew 常用的输出命令在脑子里过一遍。我整理了一张表平时查文档用这张表就很够用命令输出内容是否结构化适合场景brew --versionHomebrew 主版本、系统信息、Cellar 路径半结构化文本人工检查brew list --versions已安装包名和版本每行一个文本人工查看brew list --cask已安装 cask 列表文本人工查看brew info --jsonv2 --installed已安装 formulae/casks 的完整 JSON是自动化首选brew outdated --json可升级包的 JSON是检查更新brew info 包名 --jsonv2单个包的完整 JSON是单包详情从这张表能看出Homebrew 其实很早就提供了 JSON 输出能力只是很多人不知道。brew info支持通过--jsonv2指定输出格式配合--installed就只输出本机已安装的包这是做版本导出的核心命令。--jsonv1是旧版 API字段布局和 v2 不一样现在官方文档都推荐 v2我后面统一说 v2。2.2 brew info --jsonv2 的字段怎么读brew info --jsonv2 --installed输出的最顶层是一个 JSON 对象包含两个数组formulae和casks。formulae 是 Homebrew 官方源里的命令行工具和库casks 是图形化应用比如 Chrome、VS Code两者数据模型不同所以 Homebrew 把它们分开。每个 formula 对象里的关键字段我建议重点看这几个name和full_name包名带 tap 的包全名可能是homebrew/core/jq这种形态。versions.stable上游仓库当前稳定的版本号不一定是本机装的版本。installed本机安装信息数组里面每个元素有version、used_options等字段。注意是数组因为某些包可能同时存在多个版本。installed_as_dependency这个包是不是作为其他包的依赖被自动装进来的而不是主动安装的。installed_on_request你是不是真的手动brew install过它。dependencies依赖的其他包名列表是后续做依赖分析的重要素材。举个简化例子你运行brew info jq --jsonv2会看到类似这样的结构{ formulae: [ { name: jq, full_name: jq, desc: Command-line JSON processor, versions: { stable: 1.7.1 }, installed: [ { version: 1.7.1, used_options: [] } ], installed_as_dependency: false, installed_on_request: true, dependencies: [] } ], casks: [] }这里有一个容易误解的地方versions.stable是上游仓库现在发布的最新稳定版installed[0].version才是你机器上实际的版本。如果你很久没brew upgrade这两个值会不一样。想要做当前环境版本清单一定要取installed里的值而不是versions.stable这是我最初写脚本时被坑过的点。2.3 formulae 和 casks 要分开看很多第一次做导出的人会忽略 formulae 和 casks 的区别结果后面处理数据时一脸懵。formulae 的版本号通常是语义化版本像3.11.2而 casks 的版本号可能带着latest、beta、或者应用自己的奇怪命名习惯。另外cask 对象里没有dependencies这种字段但会有artifacts描述安装后的应用去向。所以我的建议是先分别导出、分别存盘需要合并时再统一成一套自己的字段。下面的实操方案里我会给出分别导出的具体命令。3. 三个落地方案从一条命令到脚本自动化这一章是全文的核心我按简单到复杂的顺序给出三种方案。第一种只要会敲命令就能用第二种能让你精确裁剪字段第三种适合集成到自动化流程里长期使用。3.1 方案一一条命令直接导出完整 JSON如果你只是想快速把当前机器的 Homebrew 安装信息存成一份 JSON 文件最直接的办法就是这样brew info --jsonv2 --installed brew-installed.json运行完brew-installed.json里就是当前所有已安装 formulae 和 casks 的完整 JSON 数据。这里有两个关键点要说明。第一--installed这个参数必须有。不加它brew info --jsonv2会把整个库里的全部 formula 元数据都导出来文件大小轻轻松松上几 MB而且大部分跟你本机没什么关系。加了--installed输出就只包含本机实际安装的包文件小、信息准。第二如果你只想导 formulae 或只想导 casks可以进一步细分# 只导出命令行工具类 brew info --jsonv2 --installed --formula brew-formulae.json # 只导出图形应用类 brew info --jsonv2 --installed --cask brew-casks.json这两个参数在brew info里和brew list里都能用习惯之后会非常顺手。实际项目中我一般会同时保存三份完整一份、formulae 一份、casks 一份方便后续不同的处理逻辑直接读取。3.2 方案二用 jq 裁剪出自己想要的字段一份完整的 Homebrew JSON 里每个包都带十几二十个字段很多我们用不上。第二套方案就是用 jq 把字段裁到自己需要的粒度。jq 是命令行下的 JSON 处理器在 Mac 上装它很简单brew install jq。装好之后比如我只想要包名、实际安装版本、是否主动安装这三个字段可以这样写brew info --jsonv2 --installed | jq [.formulae[] | { name: .name, version: (.installed[0].version // 未安装), installed_on_request: .installed_on_request }]这里解释几个 jq 的写法新手容易卡住。最外层[ ... ]表示把结果包成 JSON 数组.formulae[]是遍历 formulae 数组里的每一个元素{ ... }是在构造一个新的对象(.installed[0].version // 未安装)表示取 installed 数组第一个元素的 version 字段如果取不到就使用备用值未安装。如果你只想保留主动安装的包用来生成一份我的核心软件清单可以用select过滤brew info --jsonv2 --installed | jq [.formulae[] | select(.installed_on_request true) | {name: .name, version: (.installed[0].version // )}]我强烈建议把处理后的文件重定向保存下来比如jq ... brew-inventory.json这样后面脚本读起来更快也不用每次重新调用 brew。这里有一个经验brew info --json每次执行都要去读本地 Homebrew 的元数据几十上百个包时还好如果你装了上千个包响应时间会明显变长。所以一次导出、多次复用是必须养成的习惯。3.3 方案三Python 脚本生成结构化清单当你有更复杂的逻辑——比如要同时对比多台机器的版本、要生成人类可读的报表、要按包名排序并去重——命令行就有点不够用了这时候我建议用 Python 写一个小脚本。下面这个脚本是我在实际项目里用的基础版本思路是先调用brew info --jsonv2 --installed拿原始 JSON再解析成自己定义的清单结构最后落盘#!/usr/bin/env python3 导出 Homebrew 已安装包清单为 JSON。 import json import subprocess def load_brew_json(): 调用 brew 命令获取已安装包的 JSON 数据。 raw subprocess.check_output( [brew, info, --jsonv2, --installed], textTrue, ) return json.loads(raw) def build_inventory(data): 把 brew 原始 JSON 转换为自己定义的清单结构。 packages [] for item in data.get(formulae, []): installed_versions [ entry.get(version) for entry in item.get(installed, []) ] packages.append({ name: item.get(name), description: item.get(desc, ), stable_version: item.get(versions, {}).get(stable), installed_versions: installed_versions, installed_on_request: item.get(installed_on_request, False), installed_as_dependency: item.get(installed_as_dependency, False), dependencies: item.get(dependencies, []), }) # 按名字排序方便后续 diff packages.sort(keylambda p: (p[name] or ).lower()) return packages def main(): data load_brew_json() inventory build_inventory(data) with open(brew-inventory.json, w, encodingutf-8) as fp: json.dump(inventory, fp, ensure_asciiFalse, indent2) print(f导出完成共 {len(inventory)} 个 formula。) if __name__ __main__: main()这个脚本有几个细节值得说。第一我用了subprocess.check_output而不是os.popen因为前者能正确处理命令参数、错误码和返回文本后者在带管道命令时容易引入 shell 转义问题。第二installed_versions我用的是数组不是单个字符串。前面说过Homebrew 允许同一包存在多个版本记录尤其是 versioned formulae如果只取第一个版本后续做差异分析时会漏掉真实情况。第三排序放在 Python 里做不依赖sort命令的本地化行为。macOS 自带的sort对版本号的处理和 GNU 版本不太一样用 Python 内置排序更可控。如果你还要生成人类可读的报表可以在main里继续加一段把inventory格式化成 markdown 表格或者纯文本输出。关键是把导 JSON - 清洗 - 落盘这条链路跑通。4. 实战中踩过的坑问题与排查技巧这部分可能是全文最值钱的地方。我在做版本导出的过程中踩了不少坑也帮同事排查过一些问题整理成速查表加点心得你可以直接收藏。4.1 命令报错和输出格式问题现象常见原因解决办法brew info提示未知参数Homebrew 版本太老不支持--jsonv2先执行brew update必要时升级 Homebrew 自身jq: command not foundjq 未安装brew install jq报Unexpected token之类的 JSON 解析错误shell 引号嵌套错误或 jq 表达式写错用单引号包住完整 jq 表达式调试时先输出到文件再解析JSON 里版本是null该包没有安装记录或 Homebrew 元数据异常先用brew list --versions 包名人工确认导出的文件特别大没加--installed导出了全量元数据加上--installed或只导 formulae/casks 之一这里我想特别强调一下引号问题。写 jq 表达式时很多人把双引号和单引号混用结果 shell 把表达式拆成了好几段jq 收到的是残缺输入报错信息又很隐晦。最稳妥的写法是jq 表达式整体用单引号包住表达式内部的字符串字段值用双引号。比如jq [.formulae[] | {name: .name}]这种。如果你需要把外部变量传进去用--arg参数不要硬拼字符串。4.2 版本对比的坑导出的版本号是字符串直接拿去做大小比较几乎必然踩坑。举个例子字符串比较下1.10.0会小于1.9.0因为逐字符比较到第二位时1和9比大小已经决定了结果正确语义里1.10.0应该大于1.9.0。所以做版本门槛判断时一定要用语义化版本解析而不是裸比较字符串。在 Shell 里可以借助sort -V做版本排序但 macOS 自带的 BSD sort 并不支持-V参数只有 GNU coreutils 版本才支持。我实测下来的替代方案有两个一是装coreutils然后用gsort -V二是直接交给 Python 的packaging.version.Version类处理。我的项目里因为脚本本来就是 Python 写的直接用下面这种方式一行代码就解决比较问题from packaging.version import Version if Version(1.10.0) Version(1.9.0): print(新版本满足要求)注意packaging这个库在 macOS 系统自带的 Python 里不一定有需要用pip install packaging安装或者使用虚拟环境。4.3 性能与文件大小的取舍brew info --jsonv2 --installed在包数量少的时候毫秒级返回但如果你开发机装了上千个包这个命令可能要跑好几秒甚至更久因为 Homebrew 要读每个包的元数据文件。我在项目里优化过两个点。第一个点是缓存。巡检脚本不会每次运行都重新brew info而是先检查本地有没有今天生成的brew-inventory.json有就直接读没有才重新执行 brew 命令。这样日常巡检的开销几乎为零。第二个点是分片。如果只需要检查某些关键包比如 jq、python、node、git可以只对这几个包分别执行brew info 包名 --jsonv2然后合并成一个 JSON 数组而不是全量导出一遍再过滤。当包数量达到上千时这两种方式的耗时差距非常明显。5. 拿到 JSON 之后还能干什么把版本信息转成 JSON 只是第一步这一步真正值钱的是后面能接多少自动化。我分享两个我实际在用的场景给你个参考。5.1 环境快照备份与批量恢复每次做完一轮环境调整我都会把brew info --jsonv2 --installed的结果保存成带日期的快照文件比如brew-snapshot-2026-06-01.json。这样万一哪台机器环境被改坏了或者需要在新机器上复现一套类似环境就能对照快照手动逐项恢复。Homebrew 官方也有brew bundle可以用 Brewfile 管理依赖我也在用但说实话 Brewfile 记录的是你要装什么JSON 快照记录的是实际装了什么、什么版本两者互补。做故障复盘时JSON 快照的历史对比非常有用能一眼看出是哪次变更引入了问题。5.2 在 CI 里做版本门槛检查另一个我很常用的场景是在 CI 流程里检查某个工具版本是否满足要求。比如项目要求 Node 必须大于某个版本我可以在 CI 的第一步执行NODE_VERSION$(brew info node --jsonv2 | jq -r .formulae[0].installed[0].version) echo node version on runner: $NODE_VERSION然后用脚本把这个字符串和期望值做语义化比较不满足就直接 fail。这样比在文档里写请确保 Node 版本大于 xx要可靠得多因为文档会过期代码检查不会。这个模式的通用思路是任何需要机器上某软件版本信息的地方都可以用brew info --json拿到结构化数据再配合 jq 和脚本做判断。把这一步沉淀成脚本或 CI 模板以后每个项目都能复用。我在实际项目中把这个流程跑了大半年最大的体会是把 Homebrew 版本信息转成 JSON 这件事本身不难难的是想清楚拿到 JSON 之后要干什么。如果只是导出来放那儿那确实意义不大但一旦接到巡检、CI、报表这些自动化流程里它的价值就会成倍放大。最后再分享一个小技巧所有导出命令都建议先手动跑一遍把输出重定向到文件里用 jq 或 Python 验证字段确认无误后再写进自动化脚本能省掉很多在 CI 里反复试错的麻烦。