Set System Env-Vars in UNIX
Intro
许多教程在设置 macOS 环境变量时,建议在 ~/.bashrc 或 $ZDOTDIR/.zshrc 等文件内设置环境变量。这种方式是ok的,但有局限:
- 如果需要切换使用不同Shell,(e.g. Nushell, Zsh),需要维护多套配置
- GUI 应用并不能读取 Shell 配置文件中的环境变量,例如在
$ZDOTDIR/.zshrc中设置$EDITOR=nvim,Kitty 不能读取到这个变量[1] - Bootstrapping issue: 像
ZDOTDIR这类变量,其作用是告诉 Shell “去哪里读取配置”。如果在配置文件内部才定义它,就如同把钥匙锁在了保险箱里——Shell 在启动时依然会去默认路径寻找配置,导致该变量实际上失去了引导意义。
Note
Shell 变量:这是一种只在当前 Shell 会话中有效的变量。它对该 Shell 启动子进程不可见(zsh)$ x=1(zsh)$ echo $x1(zsh)$ bash(bash)$ echo $x[NO-OUTPUT]
环境变量:这是一种特殊的变量,它不仅在当前 Shell 会话中有效,还会被传递给所有由该 Shell 启动的子进程;(zsh)$ export x=1(zsh)$ echo $x1(zsh)$ bash(bash)$ echo $x1
Read More: export command in Linux with Examples - GeeksforGeeks
Example Case
这里以设置 XDG_* 系列变量为例进行演示。关于 XDG 规范的介绍,可查阅 XDG Base Directory - ArchWiki。
macOS 使用 launchd 进程来管理守护进程和代理,而你还可以用它来运行 shell 脚本。你不与 launchd 直接交互,而是使用 launchctl 命令来载入或卸载 launchd 守护进程和代理。
在系统启动期间,launchd 是内核在设置电脑时首先运行的进程。若你想要 shell 脚本作为守护进程运行,应由 launchd 来启动它。其他用于启动守护进程和代理的机制可能会被 Apple 酌情移除。
你可以通过在以下文件夹中查看配置文件来了解由 launchd 管理的各种守护进程和代理:
| 文件夹 | 用途 |
|---|---|
/System/Library/LaunchDaemons | Apple 提供的系统守护进程 |
/System/Library/LaunchAgents | Apple 提供的基于每个用户且所有用户适用的代理 |
/Library/LaunchDaemons | 第三方系统守护进程 |
/Library/LaunchAgents | 基于每个用户且所有用户适用的第三方代理 |
~/Library/LaunchAgents | 仅适用于登录用户的第三方代理 |
步骤 1:创建环境设置脚本
首先,在任意位置创建一个 Shell 脚本,例如 ~/.local/bin/sys_envs。touch ~/.local/bin/sys_envs
脚本内容如下,针对 MacOS 它使用 launchctl setenv 来设置环境变量。#! /bin/bash# Define a macro function for setting environment variablesenv() { os_name=$(uname -s) case "$os_name" in Darwin) # For macOS, use launchctl to set environment variables launchctl setenv "$1" "$2" # launchctl setenv does not work for current shell session(which executes this script), # for example, when set `XDG_CONFIG_HOME/bat`, the XDG_CONFIG_HOME will be expanded as empty string. export "$1=$2" ;; Linux) export "$1=$2" # Replace $HOME with ~ for /etc/environment env_value="${2//$HOME/~}" # Append to /etc/environment with sudo echo "$1=$env_value" | sudo tee -a /etc/environment > /dev/null ;; *) echo "Unsupported OS: $os_name" exit 1 ;; esac}env XDG_BIN_HOME "$HOME/.local/bin"env XDG_CACHE_HOME "$HOME/Library/Caches"env XDG_CONFIG_HOME "$HOME/.config"env XDG_CONFIG_DIRS "/etc/xdg"env XDG_DATA_HOME "$HOME/.local/share"env XDG_DATA_DIRS "/usr/local/share/:/usr/share/"env XDG_STATE_HOME "$HOME/.local/state"# https://wiki.archlinux.org/title/XDG_user_directoriesenv XDG_DESKTOP_DIR "$HOME/Desktop"env XDG_DOCUMENTS_DIR "$HOME/Documents"env XDG_DOWNLOAD_DIR "$HOME/Downloads"env XDG_MUSIC_DIR "$HOME/Music"env XDG_PICTURES_DIR "$HOME/Pictures"env XDG_PUBLICSHARE_DIR "$HOME/Public"env XDG_VIDEOS_DIR "$HOME/Movies"# Define paths for common programs with partial XDG support# https://wiki.archlinux.org/title/XDG_Base_Directory#Partialenv CARGO_HOME "$XDG_DATA_HOME/cargo"env FFMPEG_DATADIR "$XDG_CONFIG_HOME/ffmpeg"env LESSHISTFILE "$XDG_STATE_HOME/less_history"env MYPY_CACHE_DIR "$XDG_CACHE_HOME/mypy"env NODE_REPL_HISTORY "$XDG_STATE_HOME/node_repl_history"env PYENV_ROOT "$XDG_DATA_HOME/pyenv"env PYTHONPYCACHEPREFIX "$XDG_CACHE_HOME/python"env PYTHONUSERBASE "$XDG_DATA_HOME/python"env PYTHON_HISTORY "$XDG_STATE_HOME/python_history"env RIPGREP_CONFIG_PATH "$XDG_CONFIG_HOME/ripgrep/config"env RUSTUP_HOME "$XDG_DATA_HOME/rustup"env WORKON_HOME "$XDG_DATA_HOME/virtualenvs"# dockerenv DOCKER_CONFIG "$XDG_CONFIG_HOME/docker"env MACHINE_STORAGE_PATH "$XDG_DATA_HOME/docker_machine"# npmenv NPM_CONFIG_USERCONFIG "$XDG_CONFIG_HOME/npm/npmrc"# zshenv ZDOTDIR "$XDG_CONFIG_HOME/zsh"env ZSH_PROFILE "$XDG_CONFIG_HOME/zsh/profile"env HISTFILE "~/.cache/zshhistory"# yazienv YAZI_CONFIG_HOME "$XDG_CONFIG_HOME/yazi"
NuShell Rewrite-Version
前面 Bash 版本的脚本是写入到/etc/environment中,只支持普通键值对。而/etc/profile.d/*.sh为 shell script,可以在 value 中引用变量,相对灵活一点。这里给出一个 nushell 版本的脚本,将 Linux 环境变量写入到/etc/profile.d/gnix-env.sh中
针对个人跨平台需求,这里把环境变量的设置封装成一个 env 函数中,对于 Arch Linux 来说,系统级环境变量可以写入 /etc/environment 或/etc/profile.d/*.sh中
如需针对单用户设置,可以考虑写入~/.pam_environment,详见pam_env.conf(5) - Linux man page
#! /usr/bin/env nu# Define a function to set environment variables cross-platformdef --env env [name: string value: string] { # Set env var for current session load-env {$name: $value} match $nu.os-info.name { "macos" => { launchctl setenv $name $value } "linux" => { let env_file = "/etc/profile.d/gnix-env.sh" touch $env_file let lines = (open $env_file | lines | where not ($it | str starts-with $"export ($name)=")) let env_value = ($value | str replace $nu.home-path "$HOME") let updated = ($lines | append $"export ($name)=\"($env_value)\"") $updated | str join (char nl) | save --force $env_file } "windows" => { setx $name $value } }}Step 2. Load the Script at Login/StartUp
创建并编辑 ~/Library/LaunchAgents/env.plist 文件<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"> <dict> <key>Label</key> <string>com.user.environment-vars</string> <key>ProgramArguments</key> <array> <!-- 这里需要替换成你自己的脚本路径 --> <string>/Users/gjx/.local/bin/sys_envs</string> </array> <key>RunAtLoad</key> <true /> <key>KeepAlive</key> <false /> </dict></plist>
完成以上步骤后,下次登录时,launchd 进程就会自动执行该脚本,从而设置好所有环境变量。
Limitations
Apple 不保证 launchd 加载服务的确切顺序,也就是可能先重启了 Terminal,而后env.plist 才被加载,这会导致 Terminal 无法获取到新的环境变量。
解决方法如下:
- 重新登录:当前用户先 Log Out(快捷键
⌘ + ⇧ + Q),再重新 Login - 手动重启应用:字面意思
- 禁用会话恢复:在系统设置中,关闭“Reopen windows when logging back”的功能。
另外就是无法通过 launchctl 修改 PATH 环境变量,想在 System-Wide 层级设置 PATH,需要在 /etc/paths 中修改。
Ref
- 在 Mac 上的“终端”中使用 launchd 管理脚本 - 官方 Apple 支持 (中国)
- Setting environment variables via launchd.conf no longer works in OS X Yosemite/El Capitan/macOS Sierra/Mojave? - Stack Overflow
- EnvironmentVariables - Community Help Wiki
Set System Env-Vars in UNIX

