上周三下午我在三个仓库之间来回切。一个是 Node 18 的老项目,一个是 Node 20,还有一个 Python 服务要 3.11 加一个专门的 venv。以前每个仓库根目录放一个 activate.sh,切过去先 source 一下——大部分时候会忘。
忘了不一定当场出事。有一次我跑本地迁移脚本,脚本读 DATABASE_URL,而那会儿 shell 里还留着上一个项目 export 的值,脚本连到了另一个库上,多建了两张表。没丢数据,但我花了一晚上确认它没波及别的东西。之后我把这套东西交给了 direnv。
它做的事其实很少
direnv 挂在 shell 钩子上。cd 进一个目录,它沿路往上找 .envrc,找到就执行,把里面 export 的变量注入当前 shell;离开时再撤回去。
装完在 rc 文件里挂一行:
1 | # zsh |
.envrc 是个 bash 脚本,这是它比 .env 文件顺手的地方——能写逻辑,能用函数:
1 | export DATABASE_URL=postgres://localhost:5432/app_dev |
PATH_add 处理的是相对路径,还会去重。离开目录时它加的东西自动摘掉,不会在 PATH 里越积越长——这是我放弃手写 export PATH=$PWD/bin:$PATH 的主要原因,那种写法在一天里 cd 几十次之后,PATH 会长到 direnv status 都不想看。
团队里已经在用 .env 的不用迁移,一行就够:
1 | dotenv_if_exists .env.local |
watch_file 也常用,指定某个文件变了就重新加载,比如 watch_file package-lock.json。还有个人覆盖:.envrc 进 git,我机器上的差异放 .envrc.local,主文件里写 source_env_if_exists .envrc.local,再把后者加进 .gitignore。这个组合比让每个人改同一份文件省事很多。
真正省事的是跟着仓库走的身份
环境变量里最危险的那几个不是配置,是身份:连哪个数据库、用哪个云账号、打哪个集群。
我以前靠脑子记。kubectl config use-context 切过去,开个新终端又回到默认;AWS 更麻烦,几个 profile 名字长得像。现在 kubeconfig 按仓库放(不进 git),.envrc 指过去,cd 进去就是对的集群。凭证还是从 ~/.aws 和系统钥匙串来,仓库里只放指向,不放假钥匙。
Python 项目我用 layout python3,direnv 自己在 .direnv/ 下管一个 venv,进目录就激活,出目录就退出,提示符我都懒得改。其他语言在 stdlib 里也有对应的 layout,node、ruby 这些,具体行为我是直接翻 stdlib 源码看的,比文档清楚。
allow 那一步是设计,不是麻烦
第一次进一个带 .envrc 的目录,direnv 会拒绝执行,提示 blocked,要求 direnv allow。
因为它本质上是在你的交互式 shell 里跑任意代码。clone 一个仓库就等于拿到一份等待执行的脚本,这跟 npm install 跑 postinstall 是同一类风险,只是更直接、更隐蔽。
allow 按内容哈希记录,.envrc 改一个字符就要重新确认。我现在的习惯是:克隆下来的仓库先 cat .envrc 再 allow,跟看 package.json 的 scripts 一样。自己的仓库改完顺手 direnv allow,direnv status 能看到当前目录加载了什么、哪些文件在被 watch。
两个我没绕过去的坑
慢。 .envrc 里跑重活——起 nix develop、跑一次依赖安装——进目录就卡一下。direnv 有缓存,只在相关文件变化时重算,但第一次和每次改完仍然要等。我现在把重活挪出去,.envrc 里只留 export 和 PATH,超过两百毫秒的操作都不放进去。
它只管交互式 shell。 VS Code 里开的终端一般能继承(走的是同一个 hook),但 GUI 启动的进程、launchd 里的定时任务、IDE 的调试器拿不到。这些我现在显式在脚本里 source 一份,不指望 direnv 兜底。CI 里我也不用 direnv,直接读 .env——本地和 CI 怎么共用同一份变量来源,这件事我还没完全解决,目前是 CI 脚本里显式 export 一遍。
什么情况下不值得装
只有一个项目、一套环境的时候,它是净增一个要维护的文件。direnv 解决的是上下文切换,切得不频繁,收益接近零。
我现在的判断标准很土:一个项目的 README 里如果有超过两行 export,就给它一个 .envrc;只有一行的话,写在 README 里就够了。下次你翻到自己项目的安装说明,数一下那几行 export。
评论