在终端中输入NAME=value会创建一个变量。然而,在接下来执行的程序中,该值可能不可见。相反,如果使用export NAME=value声明,程序也能读取该值。两者的区别不在于值的形式,而在于是否将该值传递给子进程。
首先要记住的句子是:
VAR=value是仅供当前shell使用的shell变量,而export VAR=value是会传递给后续子进程的环境变量。
这里,shell(外壳)是像Bash或Zsh这样解释命令的程序,而process(进程)指的是一个正在运行的程序。当你在终端的shell中执行python、node、docker等命令时,这些程序会作为shell的子进程启动。

仅保留在当前shell中的变量和传递给子进程的环境变量
变量相同,但传递标记不同
在Bash中,如果认为shell变量和环境变量完全独立地存在于不同的存储空间中,反而会让人感到困惑。实际上,更准确的理解是:创建一个shell变量,然后为其附加export属性,它就会传递给子进程。
PROJECT_MODE=development
此命令在当前shell中创建一个名为PROJECT_MODE的变量。在同一个shell中可以读取该值。
printf '%s
' "$PROJECT_MODE"
# development
但是,如果将新的Bash作为子进程运行,该值将不会被传递。
bash -c 'printf "%s
" "${PROJECT_MODE-unset}"'
# unset
现在,让我们为现有变量附加export属性。
export PROJECT_MODE
bash -c 'printf "%s
" "$PROJECT_MODE"'
# development
export PROJECT_MODE与其说是复制一个新值,不如说是指示将此变量包含在后续要执行命令的环境中。如果像下面这样将值指定和export合并到一行,结果也是一样的。
export PROJECT_MODE=development
GNU Bash官方文档也解释说,执行的命令会继承父shell的环境,并且export标记变量以包含在后续命令的环境中。POSIX shell标准也定义了带有export属性的变量会进入后续执行命令的环境。
将父shell和子进程视为家庭关系更容易理解
让我们将当前终端的Bash或Zsh视为父级。如果在这个shell中运行Python,那么Python就是子级。
터미널의 셸
├── python app.py
├── node server.js
└── bash script.sh
普通的shell变量是父级只记录在自己备忘录中的值。而导出的变量则包含在子级诞生时被赋予的环境列表中。这里有一个重要的比喻限制:即使子级改变了接收到的值,父级的值也不会反向改变。
export COLOR=blue
bash -c 'COLOR=red; printf "child=%s
" "$COLOR"'
printf 'parent=%s
' "$COLOR"
# child=red
# parent=blue
进程环境基本上是从父级传递到子级。子进程不是直接修改父shell变量存储的通道。
四种声明方式比较
| 声明方式 | 在当前Shell中保留 | 传递给子进程 | 主要用途 |
| — | — | — | — |
VAR=value |
是 | 否 | 当前shell内的计算·设置 |
export VAR=value |
是 | 是 | 后续执行的多个程序的设置 |
VAR=value command |
否 | 仅对该command是 | 仅向一个命令传递临时设置 |
env VAR=value command |
否 | 仅对该command是 | 显式的临时环境配置 |
第三种方式在部署和测试中特别有用。
APP_ENV=test python app.py
python app.py会接收APPENV=test,但在命令结束后,APPENV不会在当前shell中留下。
APP_ENV=test bash -c 'printf "inside=%s
" "$APP_ENV"'
printf 'after=%s
' "${APP_ENV-unset}"
# inside=test
# after=unset
GNU Bash手册解释说,命令前的变量赋值仅在命令执行期间添加到环境中,并且不影响当前shell环境。对于shell函数和特殊内置命令,具体规则可能会有所不同,因此最好先理解执行外部程序的常见情况。
为什么使用source执行时,变量无需export也能可见?
脚本的执行方式也会影响变量的可见性。
TOKEN=demo
./check.sh
./check.sh作为独立的子进程执行。如果TOKEN没有被export,脚本通常不会接收到这个值。
另一方面,以下命令则不同:
TOKEN=demo
source ./check.sh
source或点命令. ./check.sh不会启动新进程,而是在当前shell中执行文件中的命令。因此,它可以读取当前shell变量,并且脚本更改的任何变量也会保留在当前shell中。
正因为这个区别,在激活Python虚拟环境时,我们使用source,而不是./venv/bin/activate,如下所示:
source .venv/bin/activate
这是因为激活脚本需要修改当前shell的PATH。
export并不意味着永久存储
很容易误解export为“永久环境变量注册”,但export仅适用于当前shell进程及其子进程。当终端窗口关闭,shell终止时,这些设置也会消失。
如果想在每次打开新shell时都进行设置,则必须在使用中的shell的启动文件中声明。
# Bash交互式shell中常用的文件
export APP_HOME="$HOME/apps/example"
- 在Bash中,根据情况使用
~/.bashrc、~/.bash_profile、~/.profile等。 - Zsh的交互式配置通常使用
~/.zshrc。 - 根据是登录shell、交互式shell还是脚本执行,读取的文件会有所不同。
修改配置文件后,可以通过source该文件来立即将其更改应用到当前shell。
source ~/.zshrc
但是,如果在多个文件中重复声明设置,将很难追踪哪个文件最后覆盖了值。最好将设置放在一个符合其目的的单一文件中。
.env文件不会自动成为环境变量
假设你创建了这样一个.env文件:
DATABASE_HOST=localhost
DATABASE_PORT=5432
仅仅因为文件存在,操作系统或shell并不会自动读取它。应用程序的dotenv库、Docker Compose、开发工具或单独的shell代码必须读取该文件。
如果你想在shell中导出包含赋值语句的文件中的所有变量,你可能会在Bash中看到以下模式:
set -a
source .env
set +a
set -a会自动为之后创建或修改的变量附加export属性。然而,不能保证.env文件一定使用安全的shell语法。source一个不可信的文件是危险的,因为其中的命令也会在当前shell中执行。对于简单的数据文件,使用你所用工具提供的专用.env解析器会更安全。
检查值的命令也各不相同
如果你想广泛查看当前shell变量,可以使用Bash的set命令。由于输出可能很多,并且可能包含函数,因此通常最好通过已知名称进行查询。
printf '%s
' "$PROJECT_MODE"
declare -p PROJECT_MODE
导出的变量列表可以按如下方式查看:
export -p
外部程序接收的环境可以通过env或printenv来查看。
printenv PROJECT_MODE
env | grep '^PROJECT_MODE='
如果只想移除变量的export属性,而将其值保留在当前shell中,可以在Bash中使用export -n。
export -n PROJECT_MODE
要删除值本身,请使用unset。
unset PROJECT_MODE
导出密码和API密钥安全吗?
环境变量不是秘密存储。虽然它们比直接将密钥嵌入源代码更容易分离,但它们可能会暴露给以相同权限运行的进程、调试工具、错误收集、进程转储、CI日志或编写不当的诊断代码。子进程会继承导出的秘密,因此最好不要将其传递得比必要范围更广。
如果只有一个程序需要某个值,可以缩小其范围,而不是长时间全局导出。
API_TOKEN='temporary-value' ./deploy.sh
在生产环境中,应考虑使用专门的方法,例如云秘密管理服务、Vault、Kubernetes Secret和短期凭证。直接在命令行中写入秘密也可能留在shell历史记录中,因此必须使用安全的注入路径。
此外,sudo可能会根据其安全策略移除或限制环境变量。你不应该假设通过sudo执行的命令会完全接收父shell中导出的所有值。
四个常见误解
1. “大写就是环境变量”
不是。大写只是一种惯例。MY_VALUE=test即使是大写,如果不export,它仍然是一个普通的shell变量。
2. “export是复制值的命令”
关键是为变量附加export属性。如果之后更改了值,新值将传递给下一个子进程的环境。
export LEVEL=one
LEVEL=two
bash -c 'printf "%s
" "$LEVEL"'
# two
3. “子进程改变,父进程也改变”
不是。继承是从父级到子级的方向。子进程的更改不会自动反向传播到父shell。
4. “export后重启仍保留”
不是。export在当前shell的生命周期内生效。永久设置必须单独记录在shell启动文件、服务管理器、容器配置或部署平台的环境设置中。
实践中如何选择
- 仅用于shell脚本内部计算:
VAR=value - 如果后续多个程序需要读取:
export VAR=value - 仅临时提供给一个命令:
VAR=value command - 如果脚本更改当前shell设置:
source script.sh - 如果重新连接后仍需要:记录在正确的shell启动文件或服务配置中
- 如果是秘密值:最小化传递范围并考虑专用秘密管理工具
最终,export的含义并非“更强地声明变量”。它的意思是将仅存在于当前shell中的名称和值,也传递给后续执行的子进程的环境。记住这一句话,将使你更容易追踪.env、Docker、Python、Node.js、CI/CD中环境变量可见或不可见的原因。
发表回复