`export VAR=value`与`VAR=value`有何不同?一次性理解环境变量的传递范围

在终端中输入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中环境变量可见或不可见的原因。

参考资料


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注