placeholderSet System Env-Vars in UNIX

Set System Env-Vars in UNIX

How to set true system-wide environment variables on macOS with a simple Launch Agent and shell script, so GUI apps and every shell see them.

Intro

Many tutorials on setting macOS environment variables suggest putting them in files like ~/.bashrc or $ZDOTDIR/.zshrc. That works, but it has limits:

  1. If you switch between different shells (e.g. Nushell, Zsh), you have to maintain multiple sets of configuration
  2. GUI apps cannot read environment variables from shell config files. For example, if you set $EDITOR=nvim in $ZDOTDIR/.zshrc, Kitty won’t see it[1]
  3. Bootstrapping issue: variables like ZDOTDIR exist to tell the shell “where to read its configuration from”. Defining it inside the config file is like locking the key inside the safe — the shell still looks in the default location at startup, so the variable loses its bootstrapping purpose.
Note

Shell variable: a variable that only exists in the current shell session. It is invisible to child processes started by that shell

(zsh)$ x=1(zsh)$ echo $x1(zsh)$ bash(bash)$ echo $x[NO-OUTPUT]

Environment variable: a special kind of variable that is not only valid in the current shell session, but is also passed to every child process the shell starts;

(zsh)$ export x=1(zsh)$ echo $x1(zsh)$ bash(bash)$ echo $x1

Read More: export command in Linux with Examples - GeeksforGeeks

Example Case

As a demonstration, let’s set the XDG_* family of variables. For an introduction to the XDG specification, see XDG Base Directory - ArchWiki.

Info

macOS uses the launchd process to manage daemons and agents, and you can also use it to run shell scripts. You don’t interact with launchd directly; instead, you use the launchctl command to load or unload launchd daemons and agents.

During system boot, launchd is the first process the kernel runs to set up the computer. If you want a shell script to run as a daemon, it should be started by launchd. Other mechanisms for starting daemons and agents may be removed by Apple at its discretion.

You can learn about the various daemons and agents managed by launchd by looking at the configuration files in these folders:

FolderPurpose
/System/Library/LaunchDaemonsSystem daemons provided by Apple
/System/Library/LaunchAgentsPer-user agents provided by Apple for all users
/Library/LaunchDaemonsThird-party system daemons
/Library/LaunchAgentsThird-party per-user agents that apply to all users
~/Library/LaunchAgentsThird-party agents that apply only to the logged-in user

Step 1: Create the Environment Setup Script

First, create a shell script anywhere you like, e.g. ~/.local/bin/sys_envs.

touch ~/.local/bin/sys_envs

The script is shown below. On macOS it uses launchctl setenv to set environment variables.

sys_envs
#! /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

The Bash script above writes to /etc/environment, which only supports plain key-value pairs. /etc/profile.d/*.sh files, on the other hand, are shell scripts, so values can reference other variables, which is a bit more flexible. Here is a Nushell version of the script that writes Linux environment variables to /etc/profile.d/gnix-env.sh

Note

For my own cross-platform needs, setting environment variables is wrapped in an env function. On Arch Linux, system-wide environment variables can go into /etc/environment or /etc/profile.d/*.sh

For per-user settings, consider writing to ~/.pam_environment; see pam_env.conf(5) - Linux man page

sys_envs.nu
#! /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

Create and edit the ~/Library/LaunchAgents/env.plist file

~/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>      <!-- Replace this with the path to your own script -->      <string>/Users/gjx/.local/bin/sys_envs</string>    </array>    <key>RunAtLoad</key>    <true />    <key>KeepAlive</key>    <false />  </dict></plist>

Once these steps are done, launchd will run the script automatically at your next login, setting all the environment variables.

Limitations

Apple does not guarantee the exact order in which launchd loads services. Your terminal might restart before env.plist gets loaded, leaving the terminal without the new environment variables.

Workarounds:

  • Log in again: Log Out (shortcut ⌘ + ⇧ + Q), then Log back in
  • Manually restart the app: exactly what it says
  • Disable session restore: in System Settings, turn off “Reopen windows when logging back”.

Also, PATH cannot be modified via launchctl. To set PATH at the system-wide level, edit /etc/paths.

Ref


  1. Opening preferences uses vim even though $EDITOR is set to nvim · Issue #580 · kovidgoyal/kitty ↩︎