阶段二 · Shell 入门

环境变量,PATH 与配置文件

1. 环境变量是什么

环境变量是操作系统维护的一组 KEY=VALUE 键值对. 每个进程出生时都会从父进程继承一份环境变量的拷贝. 它们是进程间传递配置信息的最基本方式.

┌──────────────────────────────────────────┐
│ launchd (PID 1, macOS 的 init) │
│ HOME=/Users/yutong │
│ PATH=/usr/bin:/bin │
│ LANG=en_US.UTF-8 │
└──────────────┬───────────────────────────┘

┌────────────────────────────────────────┐
│ iTerm2 (fork 自 launchd, 继承所有环境变量) │
└──────────────┬─────────────────────────┘

┌────────────────────────────────────────────┐
│ zsh (iTerm2 fork + exec, 继承并可能修改) │
└──────────────┬─────────────────────────────┘

┌─────────────────────────────────────────────┐
│ ls / cat / git ... (zsh fork + exec, 继承) │
└─────────────────────────────────────────────┘

每个新进程得到的是一份拷贝--子进程改了自己的环境变量后,父进程不受影响. 反过来也一样: 父进程改了,已经存在的子进程看不见.

环境变量就像遗传基因.
每个孩子出生时获得父母基因的复制.
孩子长大后锻炼出肌肉(改了自己的环境变量),父母的基因不会变.
父母后来得了什么病(父进程改了环境变量),已出生的孩子也不会突然获得这个特性.

2. 环境变量 vs Shell 变量

这是一个最常见的困惑. Shell 中有两种变量:

Shell 变量 环境变量
定义方式 NAME=value export NAME=value
存储位置 Shell 进程的内存中 内核为每个进程维护的环境块
子进程能否看到 不能 能(子进程继承拷贝)
查看方式 set(列出所有 Shell 变量) env / printenv
生命周期 Shell 退出就没了 Shell 退出就没了(但可以写入配置文件持久化)
# Shell 变量:只在当前 Shell 内可见
$ MY_NAME=makoto
$ echo $MY_NAME
makoto
$ bash -c 'echo $MY_NAME' # 子进程看不到
# (空输出)

# 环境变量:子进程继承
$ export MY_NAME=makoto
$ bash -c 'echo $MY_NAME'
makoto

# 一次性:只对这个子进程设置,不影响当前 Shell
$ MY_NAME=makoto bash -c 'echo $MY_NAME'
makoto
$ echo $MY_NAME # 当前 Shell 里没有
# (空输出)

最常用的两件事--查看所有环境变量和查看单个环境变量:

$ env            # 或 printenv--列出所有环境变量
$ echo $HOME # 查看单个
$ printenv HOME # 另一种查看方式

3. 关键环境变量速览

变量 值示例 含义
HOME /Users/yutong 用户的家目录. Shell 启动时的默认工作目录
PATH /usr/local/bin:/usr/bin:/bin 命令搜索路径,冒号分隔. 最重要的环境变量
SHELL /bin/zsh 当前用户的默认登录 Shell
USER yutong 当前用户名
PWD /Users/yutong/note/mine 当前工作目录(Shell 每次 cd 都会更新它)
OLDPWD /Users/yutong 上一次的 PWD(cd - 回到的目录)
LANG en_US.UTF-8 系统的语言和编码
EDITOR vimcode 默认文本编辑器(git commit 等会用到)
PS1 那一长串提示符模板 Shell 提示符(仅 Shell 变量,通常不 export)

4. PATH: 命令查找的 "地图"

这是最重要的环境变量,没有之一. 它决定了 "当敲 ls 时,Shell 去哪找 ls 这个可执行文件".

4.1 命令解析全过程

在 Shell 中输入 git status 并按 Enter 时,Shell 按以下顺序查找 git:

Shell 收到 "git status"

是否是 Shell 函数? → 是 → 执行 Shell 函数
↓ 否
是否是内置命令? → 是 → 执行内置命令
↓ 否
是否包含 / (路径)? → 是 → 直接执行该路径
↓ 否
在 PATH 中依次查找:
/opt/homebrew/bin/git → 找到 → 执行(Homebrew 安装)
/usr/bin/git → 找到 → 执行(系统自带)
...
全部找完都没有 → command not found
# 查看 PATH
$ echo $PATH
/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

# 某个命令在 PATH 的哪个目录?
$ which git
/opt/homebrew/bin/git

# 某个命令有多个版本?列出所有匹配
$ which -a python3
/opt/homebrew/bin/python3 ← Homebrew 的(先被找到,实际运行的)
/usr/bin/python3 ← 系统自带的

# 直接看 PATH 里所有能找到的命令
$ echo $PATH | tr ':' '\n' | xargs -I{} ls {}/git 2>/dev/null
/opt/homebrew/bin/git
/usr/bin/git

4.2 PATH 的目录顺序为什么重要

Shell 按 PATH 中的目录顺序查找,找到第一个匹配的就停. 这意味着:

# 如果 PATH 是:
# /usr/local/bin:/usr/bin:/bin

# 而 /usr/local/bin/python3 和 /usr/bin/python3 都存在
# → which python3 指向 /usr/local/bin/python3(Homebrew 版)
# → /usr/bin/python3 永远不会被用到

# 如果把顺序倒了:
# /usr/bin:/usr/local/bin
# → which python3 指向 /usr/bin/python3(系统版)
# → Homebrew 装的 python3 被 "屏蔽" 了

这就是为什么安装 Homebrew 后,安装脚本会把 /opt/homebrew/bin 追加到 PATH 最前面--确保 Homebrew 安装的工具优先于系统自带的同名工具.

4.3 PATH 与安全: 不要把 . 放进 PATH

一个经典的安全漏洞:

# 危险配置(永远不要这样做)
$ export PATH=".:$PATH"
# 效果:当前目录下的可执行文件优先级最高

# 假设在 /tmp/attack/ 下:
$ cd /tmp/attack
$ ls
# 如果 /tmp/attack/ 下有一个叫 "ls" 的恶意脚本,
# Shell 会执行它而不是 /bin/ls!
# 这个恶意 ls 可以偷走所有文件,而毫无察觉

永远不要把 . 或空目录放进 PATH

如果需要执行当前目录下的脚本,显式加上 ./ 前缀: ./myscript.sh. Shell 看到路径中有 / 就不会去 PATH 里找了.

5. export: 让变量被子进程继承

export 做的事很简单: 把这个变量从 Shell 变量提升为环境变量,标记为 "子进程应该继承".

# 写法一:两步
$ MY_VAR=hello
$ export MY_VAR

# 写法二:一步
$ export MY_VAR=hello

# 写法三:只对这个子进程有效(最常用的调试技巧)
$ MY_VAR=hello ./my_program # my_program 看到 MY_VAR=hello
$ echo $MY_VAR # 当前 Shell 没有 MY_VAR

# 查看哪些变量被 export 了
$ export # 列出所有环境变量(相当于 env)
$ export -p # 同上,格式更适合重新 source

哪些变量应该 export?

应该 export: 子进程需要的配置(PATH, HOME, LANG, EDITOR, 数据库连接串...)
不应该 export: 只给当前 Shell 用的(PS1, 历史设置, alias, Shell 选项)

6. 配置文件全景

环境变量只在内存中--Shell 退出就消失. 要让它们每次启动 Shell 时自动生效,需要写进配置文件.

6.1 Zsh 的启动文件加载顺序

不同场景下 zsh 读取不同的文件. 理解它就不需要每次改了配置再 source ~/.zshrc 后猜 "为什么没生效".

场景 加载顺序 放什么
登录 Shell /etc/zprofile~/.zprofile 登录时执行一次的命令(设置环境变量, 启动 ssh-agent)
交互 Shell /etc/zshrc~/.zshrc 每次打开终端都要生效的(alias, PS1, 补全配置)
登录 + 交互 .zprofile,再 .zshrc macOS 终端默认走这条
脚本(非交互) 什么都不读 -
始终(任何场景) /etc/zshenv~/.zshenv 极少数情况(通常避免使用,因为脚本也会读)
iTerm2 启动

读取 iTerm2 配置 (Preferences → Profiles → General → Command)
默认值: Login shell (/bin/zsh -l)

读取系统环境 (launchd 继承来的: HOME, PATH, LANG ...)

① /etc/zshenv → ~/.zshenv

② /etc/zprofile → ~/.zprofile

③ /etc/zshrc → ~/.zshrc

④ /etc/zlogin → ~/.zlogin

看到提示符 ➜

6.2 Bash 的启动文件加载顺序

Bash 的逻辑略有不同--登录 Shell 只读 profile,交互非登录只读 bashrc:

场景 Bash 加载
登录 Shell /etc/profile~/.bash_profile(或 ~/.bash_login~/.profile,只读第一个)
交互非登录 /etc/bashrc~/.bashrc

常见的做法: 在 ~/.bash_profile 里写一行 source ~/.bashrc,这样无论哪种启动都最终读到 bashrc.

6.3 实际配置建议

# ~/.zprofile -- 登录时执行一次
# 放环境变量(因为子进程会继承,不需要每次开终端都设)
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$PATH"
export EDITOR="vim"
export LANG="en_US.UTF-8"

# 启动 ssh-agent(只需要一次)
eval "$(ssh-agent -s)" 2>/dev/null

# ~/.zshrc -- 每次打开终端都要生效
# 放 Shell 专属配置
alias ll="ls -lh"
alias gs="git status"
export PS1="..." # 提示符(其实不需要 export,Shell 变量就够了)

# 加载 oh-my-zsh(如果装了)
source $ZSH/oh-my-zsh.sh

# 加载本地/私密配置(API keys 等)
[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local

改了 ~/.zshrc 后为什么要 source? source 到底做了什么?

source ~/.zshrc(等价于 . ~/.zshrc)不是 "重启 Shell"--它让当前 Shell 进程直接执行文件中的每一行命令. 所以改了 PATH 后 source 一下,当前 Shell 的 PATH 就更新了,不用关窗口重开.

它的本质和 bash script.sh 完全不同:

# bash script.sh → 启动一个子 bash,在子进程里执行脚本
# → 脚本里 export 的变量只在子进程里生效

# source script.sh → 在当前 Shell 里逐行执行
# → export 的变量影响当前 Shell(和后续子进程)

7. alias: 命令的快捷方式

alias 是 Shell 的纯文本替换机制--在命令解析的第一步就完成替换,不做任何语法检查.

# 基本语法
$ alias ll="ls -lh"
$ alias gs="git status"
$ alias ..="cd .."

# 查看所有 alias
$ alias
ll='ls -lh'
gs='git status'
..='cd ..'

# 查看单个
$ alias ll
ll='ls -lh'

# 取消 alias
$ unalias ll

# 绕过 alias:如果 ll 是个 alias,但这次想运行真正的 ll 命令
$ \ll # 反斜杠前缀 → 跳过 alias 查找
$ command ll # 另一种写法

alias 的局限

alias 是纯文本替换,不能传参数. 如果需要参数,用 Shell 函数:

# alias 做不到这个:
$ alias mkcd="mkdir $1 && cd $1" # 行不通,alias 不理解 $1

# 用函数:
$ mkcd() { mkdir -p "$1" && cd "$1" }
$ mkcd new-project # 创建目录并进入

8. 常用配置模式

以下是几个在实际配置中反复出现的模式:

在 PATH 前追加目录

# 追加到最前面(优先级最高)
export PATH="/new/dir:$PATH"

# 追加到最后面(优先级最低)
export PATH="$PATH:/new/dir"

# 避免重复追加(每次 source 都加一遍会变很长)
# 写法一:先检查再追加
if [[ ":$PATH:" != *":/new/dir:"* ]]; then
export PATH="/new/dir:$PATH"
fi

# 写法二:用 typeset -U(zsh 专有,自动去重)
typeset -U PATH
export PATH="/new/dir:$PATH"

根据操作系统做不同配置

# 同一个 .zshrc 在 macOS 和 Linux 上共用
case "$(uname -s)" in
Darwin)
export PATH="/opt/homebrew/bin:$PATH" # macOS Homebrew
alias lock="/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend"
;;
Linux)
export PATH="$HOME/.linuxbrew/bin:$PATH" # Linux Homebrew
alias lock="systemctl suspend"
;;
esac

加载私密配置

# 在 ~/.zshrc 末尾
# 私密配置(API keys, 数据库密码)放在单独文件,不提交到 git
[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local

快速回顾

  • 环境变量 = 进程间传递配置的 KEY=VALUE 键值对,子进程继承父进程的拷贝
  • Shell 变量只在当前 Shell 内可见; 加 export 后子进程才能继承
  • PATH = 命令查找的目录列表,冒号分隔,按顺序搜索,找到即停
  • 不要把 . 放进 PATH--执行当前目录的命令用 ./command
  • 登录 Shell~/.zprofile(放环境变量),交互 Shell~/.zshrc(放 alias, PS1 等 Shell 专属配置)
  • macOS 终端默认是登录 Shell--所以 ~/.zprofile~/.zshrc 都会被加载
  • source 让当前 Shell 逐行执行文件内容--不是重启,是 "就地执行"
  • alias 是纯文本替换,不能传参; 需要参数用 Shell 函数

动手练习

  1. 查看环境变量: 运行 env,找到 HOME, PATH, SHELL, USER 的值
  2. PATH 搜索: 运行 which gitwhich -a git,确认 git 在 PATH 的哪个位置. 对比 echo $PATH 中各目录的顺序
  3. 环境变量继承: 运行 export MY_TEST="hello from parent"; bash -c 'echo $MY_TEST',验证子进程继承了环境变量
  4. Shell 变量不继承: 运行 MY_LOCAL="only here"; bash -c 'echo $MY_LOCAL',验证不带 export 的变量子进程看不到
  5. 阅读配置文件: 阅读 ~/.zshrc~/.zprofile--找出所有 export 的变量和 alias 定义
  6. 临时 alias: 创建一个临时 alias: alias today="date +%Y-%m-%d",然后运行 today. 关掉终端重开,today 还在吗?