Quicktip: Setting paths on macOS

Setting the PATH environment variable should be a simple thing. But, for whatever reason, various flavours of *nix seem to have made it wilfully complex.

I recently tidied up how paths are set on my iMac, running macOS 10.14, aka Mojave. And as this blog is basically my lab notebook, I thought I’d share.

First, I use Bash as my preferred shell. If you use something else, your mileage may vary. Second, things work slightly differently on Linux machines, so I may do a separate post on that sometime.

So where does PATH get set on macOS (or OS X if you still want to cling to that)? The answer is: lots of places. But this is how it’s done on my machine. Although not directly related to PATH, we’ll also look at a couple of other environment variables.

Where it’s not set is in /etc/environment. Linux often uses this for system-wide environment variables, but that file doesn’t exist on either of my Macs (both running macOS Mojave).

In the /etc/ directory is a file called paths and a sub-directory called paths.d. When the system boots or when you run a shell, the contents of /etc/paths gets processed and each item in it gets added to PATH. The same happens with the contents of all files in /etc/paths.d/. The contents of my /etc/paths file look like this:

/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin

In other words, it sets the essential paths for various system and application executables. It’s probably best to leave this file alone and not mess with it. These are the files I have in /etc/paths.d/:

40-XQuartz
MacGPG2
com.vmware.fusion.public
50-at.obdev.CrossPack-AVR
Wireshark
go

You can probably work out that these are path elements required by various applications and frameworks. The go file, for example, contains the following:

/usr/local/go/bin

Note that there is no mention of PATH itself. It’s not like .bashrc where you might have something like:

export PATH=”$PATH:/some/other/path/element”

The entries in /etc/paths and the files in /etc/paths.d/ just list the elements you want added to the PATH variable.

All of those settings were created automatically during installation of the packages – I didn’t create any manually. There’s nothing to stop you editing the files yourself (using sudo), but I’d advise against it. The packages are expecting the paths that they’ve defined in those files. You could add to them, but the best way to do this, if you want system-wide path elements, is to put additional files in /etc/paths.d/. Unless you’re really sure what you’re doing, though, leave these alone.

Better with Bash

So far so good. Now we come to files that have some commonality across macOS and Linux – the profile and Bash files. These reside in two places – /etc/ and the home directory (designated with the tilde symbol, ~) for each user. The ones in the home directory are so-called ‘hidden’ files in that they start with a dot (eg, .profile) and so don’t normally show up in Finder or the Terminal when you use ls. Use ls -a to see them.

The ones in /etc/ are loaded first and are common to all users, while the home directory ones are individual to each user. If you’re the only user on the system – a common enough situation – then the difference seems moot. But think of it this way: if you change files in /etc/, you’re messing with the system. It’s usually better not to do that.

The profile files – /etc/profile and ~/.profile are usually for things not directly related to a specific shell. So this might seem a good place to put environment variables. But the profile files are usually associated with login shells – for example, shells created when you remotely SSH into a system. If you’re using Terminal locally to do stuff on the command line, the profile files may not get loaded. Besides, I’ve decided that it’s easier to do everything I want in one place, so I leave the profile files alone. The /etc/profile file is as Apple created it, untouched. And my local ~/.profile file is empty.

There are three Bash-related files that we need to concern ourselves with: /etc/bashrc, ~/.bash_profile and ~/.bashrc. These get loaded in that order.

Again, I’ve left /etc/bashrc in its virgin state. It sets a prompt (which I reset later) and also loads a Bash config file for the specific terminal program you’re using. This file is /etc/bashrc_Apple_Terminal and you should definitely not mess with that.

Like /etc/profile, the ~/.bash_profile file is loaded by login shells, while ~/.bashrc is loaded by interactive, non-login shells. In my case, ~/.bash_profile is nearly empty. All it does is print the date and ensure that ~/.bashrc get loaded:

date
if [ -f ~/.bashrc ]; then
    source ~/.bashrc
fi

What this means is that all the real action takes place in ~/.bashrc. Keeping (nearly) everything in one location makes life simpler.

Dotfiles

But okay, I lied. Because I don’t set path stuff in ~/.bashrc. Or I do, but indirectly. Following a tip by Corey Schafer, my home directory contains a directory called .dotfiles. In there are three files: path, aliases and prompt. These contain Bash settings who purposes should be evident from the filenames. As our concern here is setting PATH, let’s focus on that. Here’s the contents of the path file:

# PATH

# PYTHON
PATH=”$PATH:$HOME/Library/Python/3.7/bin”

# DEV
PATH=”$PATH:$HOME/scripts:$HOME/code:$HOME/Dev/lib”

# GO
# /usr/local/go/bin is set in /etc/paths.d/go
PATH=”$PATH:$HOME/Dev/Dev-Go/bin”
export GOPATH=$HOME/Dev/Dev-Go

export PATH

So that’s our PATH environment variable set (and also the GOPATH variable for good measure). How does the ~/.dotfiles/path file get loaded? Here’s the contents of my ~/.bashrc file:

for file in ~/.dotfiles/{path,prompt,aliases}; do
[ -r “$file” ] && [ -f “$file” ] && source “$file”;
done;
unset file;

if [[ $- == *i* ]]; then          # only if we’re in an interactive shell
   uname -nsmpr                 # otherwise using echo causes probs with scp
   localIP=$(ipconfig getifaddr en0);
   echo “Local IP: $localIP”;
   unset localIP;
   echo ‘————————————————————‘;
   # Make bash check its window size after a process completes
   shopt -s checkwinsize
fi

export EDITOR=nano

At the start you’ll see we loop over an array containing the names of the files within ~/.dotfiles/ and load each in turn. Then we print a welcome message of sorts and set the default editor.

And that’s it. Except…

The plist way

There is another way to set environment variables on macOS. This isn’t so useful for PATH. But, for example, I do need to set the environment variable PYTHONPATH to help Python find my user-created libraries. This is done using the launchd system. (I have a vague memory of reading something that said this method is deprecated now. I can’t recall the details and I may have that wrong. But this still works, so whatevs).

There is a plist file called ~/Library/LaunchAgents/environment.plist. To edit this, first ensure the associated agent is unloaded using:

launchctl unload ~/Library/LaunchAgents/environment.plist

Then edit the file. You can use a fancy plist editor, but I just use Nano. Here’s what my file looks like:

<?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>setenv.environment</string>
  <key>ProgramArguments</key>
  <array>
    <string>sh</string>
    <string>-c</string>
    <string>
    launchctl setenv CLASSPATH /usr/local/lib/cr/java
    launchctl setenv PYTHONPATH /usr/local/lib/cr/python
    </string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>

Two environment variables are being set: CLASSPATH, which is used by Java, and PYTHONPATH. You can add what you want here, with each entry being on its own line. Then reload the agent with:

launchctl load ~/Library/LaunchAgents/environment.plist

In summary

To sum up, PATH gets set (or could be set) by the following files in the following order:

/etc/paths # main system path elements
/etc/paths.d/* # app- and framework-specific path elements
/etc/profile # for login shells – I don’t touch this
/etc/bashrc # for interactive shells – I don’t touch this either
~/.profile # for login shells – empty on my Mac
~/.bash_profile # for login Bash shells – just loads ~/.bashrc on my Mac
~/.bashrc # where all the real action happens

In my case, system and application-specific parts of PATH are handled by /etc/paths and /etc/paths.d/* and the rest of the PATH elements are set by ~/.bashrc. And ~/Library/LaunchAgents/environment.plist takes care of a couple of other environment variables.

Works for me.

Never miss a post

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Leave a Reply

Your e-mail address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.