Skip to content

环境变量与 PATH

💡 学习指南:每次你在终端输入 gitpython,系统都要去找这个程序在哪里。每次你的代码调用大模型 API,程序要知道用哪个密钥。这两件事背后都是同一套机制——环境变量


0. 每个程序身边都带着一组配置

运行中的每个程序,都持有一组「键=值」配置,叫做环境变量。程序可以随时读取这些配置,用来了解当前的运行环境。

点击下方列表里的任意变量,在终端里"查看"它的值:

环境变量浏览器点击任意变量行,在终端中查看它的值和作用
变量名示例值
HOME/Users/alice
USERalice
SHELL/bin/zsh
PATH/usr/local/bin:/usr/bin:/bin
PWD/Users/alice/projects
LANGzh_CN.UTF-8
NODE_ENVdevelopment
OPENAI_API_KEYsk-••••••••••••••••
bash
← 点击左侧任意变量行来查看它
$
核心概念:环境变量是每个进程持有的一组「键=值」配置。程序启动时自动从父进程继承一份,可随时通过 echo $变量名 查看,用 export KEY=value 设置。

1. PATH:Shell 怎么找到你输入的命令

PATH 是一个特殊的环境变量,存着一串目录路径(用冒号分隔)。你输入 git 时,Shell 就按这串目录的顺序,一个一个地进去找名叫 git 的可执行文件——找到第一个就立刻停止。

bash
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

选择一个命令,观察 Shell 逐目录搜索的过程:

PATH 搜索过程输入命令名,看 Shell 是如何逐目录查找的
选择命令:
当前 PATH:
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
/usr/local/bin
待查找
/usr/bin
待查找
/bin
待查找
/usr/sbin
待查找
/sbin
待查找
核心机制:Shell 拿到命令名后,按 PATH 里目录的顺序依次查找。找到第一个匹配就立即使用,停止继续搜索。所以 PATH 中目录的顺序非常重要——先出现的目录优先级更高。

三个关键规律

  • 目录在 PATH 里越靠前,优先级越高
  • 找到第一个就停止,不会继续搜索
  • 所有目录都没有 → command not found

2. 为什么安装工具后要重启终端?

安装 nvm、Homebrew、conda 这类工具时,安装脚本会自动在 ~/.zshrc 里追加一行,把自己的目录加入 PATH:

bash
# 安装脚本自动写入的内容(示例)
export PATH="/usr/local/opt/python@3.12/bin:$PATH"

这行代码只在新 Shell 启动时才执行。已经打开的终端窗口不受影响,所以:

bash
# 不重启也能立刻生效
source ~/.zshrc

AI 开发工具常见情况

bash
# Ollama / pipx 装完报 command not found
which ollama          # 查实际安装位置

# pip 安装的 CLI 工具路径(加入 PATH)
# macOS:~/Library/Python/3.x/bin
# Linux:~/.local/bin
export PATH="$PATH:$HOME/.local/bin"

# 推荐用 pipx 安装命令行工具,自动管理 PATH
pipx install aider-chat

3. 变量的作用域:谁能看见这个变量?

环境变量不是广播给所有程序的——每个进程持有自己的一份副本,从父进程继承而来,修改自己的副本不会影响父进程。

下图展示三个层级。在「用户级」里 export 一个新变量,看它是否出现在「进程级」:

环境变量的三个层级变量从外到内单向传递,子进程继承父进程的副本
🖥️
系统级 /etc/environment
所有用户、所有进程都能看到,由管理员配置
PATH=/usr/local/bin:/usr/bin:/bin
LANG=zh_CN.UTF-8
TZ=Asia/Shanghai
▼ 子进程继承父进程环境
👤
用户级 ~/.zshrc
只影响当前用户,登录 Shell 启动时自动加载
HOME=/Users/alice
SHELL=/bin/zsh
NVM_DIR=$HOME/.nvm
=
▼ 启动子进程(如 node app.js)
⚙️
进程级(当前运行的程序)
继承所有上层变量,退出后消失,修改不影响父进程
PATH=/usr/local/bin:/usr/bin:/bin
LANG=zh_CN.UTF-8
TZ=Asia/Shanghai
HOME=/Users/alice
SHELL=/bin/zsh
NVM_DIR=$HOME/.nvm
NODE_ENV=development
PORT=3000
单向传递:变量只能向下继承,子进程修改变量值不会影响父进程。关闭终端后,直接 export 的变量也会消失。

4. export:决定子进程能不能读到这个变量

设置变量时,加不加 export 是完全不同的两件事:

export 决定子进程能不能"看见"变量切换开关,观察子进程是否能读到父进程设置的变量
父进程(Shell)
$MY_VAR="hello"
$echo $MY_VAR
hello
$bash -c 'echo $MY_VAR'
启动子进程
变量未继承
子进程(bash -c ...)
$echo $MY_VAR
(空,什么都没有)
#子进程无法修改父进程的变量
没有 export: 变量只存在于当前 Shell,子进程读到的是空字符串。

要让变量跨会话永久存在,把 export 写入配置文件:

bash
# macOS (zsh)
echo 'export MY_VAR="value"' >> ~/.zshrc
source ~/.zshrc       # 立刻生效,不用重开终端

# Linux (bash)
echo 'export MY_VAR="value"' >> ~/.bashrc
source ~/.bashrc

5. API 密钥:绝对不能写进代码

调用 OpenAI、Anthropic、DeepSeek 等 API 时,密钥就是你的「身份证 + 信用卡」。泄露了,别人可以用你的额度消费,费用由你承担。

最常见的错误是把密钥直接写在代码里:

硬编码密钥 vs 用环境变量同样的功能,两种写法,安全性天壤之别
危险写法:密钥写在代码里
# Python
import openai
 
client = openai.OpenAI(
api_key="sk-proj-abc123..."
)
💀git push 后,密钥就公开在 GitHub 上
💀爬虫秒级扫描,密钥被盗用并产生费用
💀GitHub Secret Scanner 自动吊销密钥
💀删除提交也没用,Git 历史仍保留
正确写法:从环境变量读取
# Python
import openai, os
 
client = openai.OpenAI(
api_key=os.environ.get("OPENAI_API_KEY")
)
代码里没有任何密钥信息,可以安全开源
不同环境(开发/测试/生产)用不同密钥
密钥泄露时只需重新生成,不用改代码
团队成员各用各的密钥,互不影响
黄金法则:代码里出现密钥字符串 = 密钥已泄露。GitHub 的 Secret Scanner 会在推送后秒级扫描,发现 sk- 等前缀就通知厂商吊销。即使立刻删除提交,Git 历史里仍然保存着。

6. 本地开发:用 .env 文件管密钥

本地开发时,把密钥放在项目根目录的 .env 文件里,代码通过 dotenv 库读取。.env 必须加入 .gitignore,不能提交到 Git。

左边写配置,右边读取——切换语言看两种写法:

.env 文件 + 代码读取左边写配置,右边读取——两者之间只有变量名这一条线
📄 .env 不提交 Git
# 本地开发配置,不提交到 Git
OPENAI_API_KEY=sk-proj-abc123...
DATABASE_URL=postgresql://localhost/dev
PORT=3000
NODE_ENV=development
📋 .env.example 可以提交 Git
# 复制为 .env,填入真实值
OPENAI_API_KEY=(值留空)
DATABASE_URL=(值留空)
PORT=(值留空)
NODE_ENV=(值留空)
💻 main.py
# pip install python-dotenv openai
from dotenv import load_dotenv
import os, openai
 
load_dotenv() # 读取 .env 文件
 
client = openai.OpenAI(
api_key=os.environ.get("OPENAI_API_KEY")
)
 
db = os.environ.get("DATABASE_URL")
port = int(os.environ.get("PORT", 8000))
程序实际读到的值
OPENAI_API_KEYsk-proj-abc123...
DATABASE_URLpostgresql://localhost/dev
PORT3000
工作流程:load_dotenv() / import 'dotenv/config' 在启动时读取 .env 文件,把里面的键值注入到进程环境变量中,代码里再用 os.environprocess.env 读取,两端只靠变量名连接。

7. 生产环境:让运行平台注入密钥

.env 是开发阶段的便利工具。服务器和云平台上,应该由运行环境负责注入密钥,代码本身完全不感知密钥放在哪里:

生产环境如何注入密钥.env 是开发工具,服务器上不能靠它
/etc/systemd/system/myapp.service
# 推荐:用独立密钥文件,权限可控
[Service]
EnvironmentFile=/etc/myapp/secrets.env
ExecStart=/usr/bin/node /app/index.js
# 设置文件权限:只有所有者可读
sudo chmod 600 /etc/myapp/secrets.env
sudo chown deploy:deploy /etc/myapp/secrets.env
# 应用配置后重启服务
sudo systemctl daemon-reload
sudo systemctl restart myapp
密钥文件 chmod 600 后,只有 deploy 用户可读,其他账号无法访问
密钥和代码完全分离,更新密钥不需要重新部署代码
不要直接在 systemd 文件里写 Environment="KEY=val"——改动需要 reload,且明文在配置里
原则:.env 文件是本地开发便利工具,生产环境应由运行平台负责注入环境变量——代码完全不感知密钥存在哪、怎么来的。

8. 实战排错

command not found

bash
# 第一步:确认是否在 PATH 里
which python3         # 有输出说明找到了

# 第二步:找到程序实际位置(macOS)
brew list python | grep bin

# 第三步:把目录加入 PATH
export PATH="/找到的路径:$PATH"
source ~/.zshrc       # 写入配置文件后记得 source

装了两个版本,用的不是我想要的

bash
which python
# /usr/bin/python ← 系统旧版,在 PATH 靠前

# 把新版目录放到 PATH 最前面
export PATH="/usr/local/bin:$PATH"

which python
# /usr/local/bin/python ← 新版,现在优先了

变量明明设置了,程序却读不到

原因解决
忘了 export加上 export 再试
改了 ~/.zshrc 没生效source ~/.zshrc
用了 .env 但没装 dotenvpip install python-dotenv / npm install dotenv
服务器上只在 SSH 会话有效改用 systemd EnvironmentFile

名词速查

术语含义
PATH存储 Shell 搜索可执行文件的目录列表,冒号分隔,顺序决定优先级
export将变量标记为可继承,子进程启动时自动获得副本
source在当前 Shell 重新执行配置文件,使修改立即生效
which显示某命令对应的可执行文件路径(PATH 搜索的结果)
.env项目本地配置文件,存开发用密钥,必须加入 .gitignore
.env.example变量名完整、值留空的模板,可以安全提交到 Git
chmod 600文件权限:只有所有者可读写,适合保护密钥文件
Secret ScannerGitHub 等平台自动扫描密钥泄露,发现后通知厂商吊销