用 Rust 写的 Homebrew Python 版本切换器:在 macOS 上,把 python3 指向你通过 brew install [email protected] 装的那一个版本,而不是系统自带的那一个。
它只做一件事——管理 PATH 里的那一个 <prefix>/opt/[email protected]/libexec/bin 目录。不创建虚拟环境,不接管 pip,不修改 Homebrew 的链接。
Homebrew 把 [email protected] 的可执行文件放在 libexec/bin 下,并且默认不把它写进 PATH(这是 Homebrew 有意为之,避免覆盖系统 Python)。结果是:
brew install [email protected]之后敲python3,拿到的可能还是 3.9 或者别的版本;- 手动往
~/.zshrc里加export PATH=...容易越加越多,切换版本时还得自己记得删掉旧的那条; [email protected]、[email protected]这些公式名字里也有python,用公式名做黑名单过滤迟早出错;- 预发布版(比如
[email protected])只提供python不提供python3,把它加进 PATH 也不会生效——需要明确告诉你,而不是静默失败。
brewpy 就是为这几件事准备的。
git clone https://github.com/Xwoder/Brewpy.git
cd Brewpy
cargo build --release
install -m 755 target/release/brewpy /usr/local/bin/brewpy需要 Rust(edition 2024)和 Homebrew。
在 ~/.zshrc(bash 用户用 ~/.bashrc)末尾加一行:
eval "$(brewpy init zsh)"重开一个终端即生效。这一步会定义三个 shell 函数,并把配置的默认版本注入 PATH——不会动你 .zshrc 里的其它内容。
brewpy list # 列出 Homebrew 已装的可用版本
brewpy current # 当前终端实际生效的是哪个 python3
brewpy default 3.14 # 设置持久化默认版本(影响新开的终端)
brewpy use 3.12 # 只切换当前终端
brewpy reset # 撤销 brewpy 对本终端 PATH 的修改brewpy list 的真实输出:
Homebrew prefix: /opt/homebrew
3.11 /opt/homebrew/opt/[email protected]/libexec/bin
3.12 /opt/homebrew/opt/[email protected]/libexec/bin [tk]
* 3.13 /opt/homebrew/opt/[email protected]/libexec/bin [default, tk]
3.14 /opt/homebrew/opt/[email protected]/libexec/bin [tk]
* = 默认版本;用 `brewpy use 3.12` 切换当前终端,`brewpy default 3.12` 设置持久化默认。
已忽略:3.15(缺少 libexec/bin/python3,加入 PATH 也不会生效)
brewpy current:
python3: /opt/homebrew/opt/[email protected]/libexec/bin/python3
version: 3.13
source: Homebrew [email protected]
default: 3.13
active: 3.13
| 命令 | 作用 |
|---|---|
brewpy list |
列出所有可用的解释器,标注 default / tk,并单独列出被忽略的版本 |
brewpy current |
当前终端的 python3 路径、版本、来源(是否为 Homebrew)、配置默认、本次激活版本 |
brewpy default <X.Y> |
写入持久化默认版本。不影响已经打开的终端 |
brewpy default --clear |
清除默认版本,新终端不再被注入 PATH |
brewpy default-show |
打印配置里的默认版本及配置文件路径 |
brewpy use <X.Y> |
只切换当前终端(需要 shell 集成) |
brewpy reset |
移除 brewpy 注入过的所有路径,清空 BREWPY_ACTIVE_DIR |
brewpy init <zsh|bash> |
输出 shell 初始化代码,供 eval 使用 |
brewpy activate <X.Y> |
输出切换代码,供 eval 使用(shell 集成内部调用) |
版本号接受 3.14 和 3.14.2 两种写法,一律规范化为 MAJOR.MINOR。
子进程无法修改父 shell 的环境变量,所以 brewpy 把职责拆开:
- Rust 侧负责发现、校验、配置:跑
brew --prefix,扫描<prefix>/opt,校验libexec/bin/python3是否真的存在且可执行,读写配置文件,然后把一段 shell 代码打到 stdout; - shell 侧负责改环境:
eval这段代码,把目标目录前置插入PATH,export BREWPY_ACTIVE_DIR,hash -r清掉命令缓存。
终端启动
└─ zshrc 执行 eval "$(brewpy init zsh)"
├─ 读 ~/.config/brewpy/config.toml → default = 3.14
├─ brew --prefix → /opt/homebrew
├─ 扫描 opt,校验 libexec/bin/python3 可执行
│ └─ 找不到或不合法 → stderr 警告,跳过激活,不注入 PATH
└─ 打印 activate 代码 → 定义辅助函数 → PATH 前置插入 → export BREWPY_ACTIVE_DIR → hash -r
brewpy() 这个包装函数会把 use / reset 转发成 eval "$(brewpy activate ...)",其余子命令直接交给真正的可执行文件。如果没做 shell 集成就敲 brewpy use,会直接报错并提示你该执行什么。
- 只保存
opt/路径,绝不保存Cellar/路径。Cellar目录名带补丁版本号,brew upgrade之后就失效了;opt/[email protected]是稳定符号链接。 - 判据是「有没有可执行的
python3」,不是公式名黑名单。 这样[email protected]、[email protected]会被自动排除,将来 Homebrew 新增同类公式也不需要改代码。检测不到python3的版本会被列进已忽略并说明原因。 - 激活前先清理。 PATH 前置插入之前会先剔除所有已注入的
*/opt/python@*/libexec/bin,切换版本不会留下历史残留,brew upgrade也不用担心旧路径堆积。 activate的输出是自包含的。 即使没跑过init,eval "$(brewpy activate 3.12)"也能工作;辅助函数运行时自探测 shell(zsh 用path数组,bash 用IFS=:切分),不依赖可能不可靠的$SHELL。- 配置文件原子写入:先写临时文件(权限
0600)再rename,中断时不会留下半截 TOML。
配置文件在 ~/.config/brewpy/config.toml(遵循 XDG_CONFIG_HOME),内容就一行:
default = "3.13"也可以直接用 brewpy default <版本> / brewpy default --clear 来改,不用手动编辑。
cargo build # 构建
cargo test # 单元测试(版本号规范化、扫描判据、shell 代码片段等)
cargo run -- list # 直接跑代码结构:
src/
main.rs 子命令实现与输出
cli.rs clap 命令行定义
config.rs config.toml 的读写(原子写入)
homebrew.rs brew --prefix、opt 扫描、版本规范化、当前 python3 探测
shell.rs 生成 zsh/bash 的辅助函数与包装函数
会话内状态只有一个环境变量:BREWPY_ACTIVE_DIR,记录当前终端被注入的目录。
- 只支持 zsh 和 bash(fish 等非 POSIX shell 暂不支持)。
- 只处理 Homebrew 安装的 Python:pyenv、conda、系统 Python 不在管理范围内,
brewpy current会把它们标成「非 Homebrew」。 - 只切
python3的 PATH,不创建或管理虚拟环境——python -m venv该怎么做还是怎么做。