﻿---
title: Set System Env-Vars in UNIX
date: 2025-07-27
excerpt: MacOS 全局环境变量设置方法。介绍如何通过创建一个简单的启动代理（Launch Agent）和 Shell 脚本，实现真正意义上的系统级环境变量配置。
tags:
  - OS
  - Shell
  - Terminal
  - macOS
  - Process
  - Daemon
  - XDG
  - Nushell
  - Linux
cover: https://assets.vluv.space/cover/Dev/tahoe.webp
updated: 2026-05-08 22:10:51
---

## Intro

许多教程在设置 macOS 环境变量时，建议在 `~/.bashrc` 或 `$ZDOTDIR/.zshrc` 等文件内设置环境变量。这种方式是ok的，但有局限：

1. 如果需要切换使用不同Shell，(e.g. Nushell, Zsh)，需要维护多套配置
2. GUI 应用并不能读取 Shell 配置文件中的环境变量，例如在 `$ZDOTDIR/.zshrc` 中设置 `$EDITOR=nvim`，Kitty 不能读取到这个变量[^1]
3. **Bootstrapping issue**: 像 `ZDOTDIR` 这类变量，其作用是告诉 Shell **“去哪里读取配置”**。如果在配置文件**内部**才定义它，就如同把钥匙锁在了保险箱里——Shell 在启动时依然会去默认路径寻找配置，导致该变量实际上失去了引导意义。

> [!NOTE]-
>
> **Shell 变量**：这是一种只在当前 Shell 会话中有效的变量。它对该 Shell 启动子进程不可见
>
> ```shell
> (zsh)$ x=1
> (zsh)$ echo $x
> 1
> (zsh)$ bash
> (bash)$ echo $x
> [NO-OUTPUT]
> ```
>
> **环境变量**：这是一种特殊的变量，它不仅在当前 Shell 会话中有效，还会被传递给所有由该 Shell 启动的子进程；
>
> ```shell
> (zsh)$ export x=1
> (zsh)$ echo $x
> 1
> (zsh)$ bash
> (bash)$ echo $x
> 1
> ```
>
> Read More: [export command in Linux with Examples - GeeksforGeeks](https://www.geeksforgeeks.org/linux-unix/export-command-in-linux-with-examples/)

## Example Case

这里以设置 `XDG_*` 系列变量为例进行演示。关于 XDG 规范的介绍，可查阅 [XDG Base Directory - ArchWiki](https://wiki.archlinux.org/title/XDG_Base_Directory)。

> [!info]
>
> macOS 使用  `launchd`  进程来管理守护进程和代理，而你还可以用它来运行 shell 脚本。你不与  [launchd](x-man-page://launchd)  直接交互，而是使用  [launchctl](x-man-page://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`。

```bash
touch ~/.local/bin/sys_envs
```

脚本内容如下，针对 MacOS 它使用 `{shell} launchctl setenv` 来设置环境变量。

```bash sys_envs
#! /bin/bash

# Define a macro function for setting environment variables
env() {
    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_directories
env 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#Partial
env 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"
# docker
env DOCKER_CONFIG "$XDG_CONFIG_HOME/docker"
env MACHINE_STORAGE_PATH "$XDG_DATA_HOME/docker_machine"
# npm
env NPM_CONFIG_USERCONFIG "$XDG_CONFIG_HOME/npm/npmrc"
# zsh
env ZDOTDIR "$XDG_CONFIG_HOME/zsh"
env ZSH_PROFILE "$XDG_CONFIG_HOME/zsh/profile"
env HISTFILE "~/.cache/zshhistory"
# yazi
env 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`中

> [!NOTE]
>
> 针对个人跨平台需求，这里把环境变量的设置封装成一个 `env` 函数中，对于 Arch Linux 来说，系统级环境变量可以写入 `/etc/environment` 或`/etc/profile.d/*.sh`中
>
> 如需针对单用户设置，可以考虑写入`~/.pam_environment`，详见[pam_env.conf(5) - Linux man page](https://linux.die.net/man/5/pam_env.conf)

```nu sys_envs.nu
#! /usr/bin/env nu

# Define a function to set environment variables cross-platform
def --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 ~/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 支持 (中国)](https://support.apple.com/zh-cn/guide/terminal/apdc6c1077b-5d5d-4d35-9c19-60f2397b2369/mac)
- [Setting environment variables via launchd.conf no longer works in OS X Yosemite/El Capitan/macOS Sierra/Mojave? - Stack Overflow](https://stackoverflow.com/questions/25385934/setting-environment-variables-via-launchd-conf-no-longer-works-in-os-x-yosemite)
- [EnvironmentVariables - Community Help Wiki](https://help.ubuntu.com/community/EnvironmentVariables#Persistent_environment_variables)

[^1]: [Opening preferences uses vim even though \$EDITOR is set to nvim · Issue #580 · kovidgoyal/kitty](https://github.com/kovidgoyal/kitty/issues/580)
