path_helper

Simple replacement for Apple's /usr/libexec/path_helper

path_helper

Hard-coding strings is bad, yet you probably hard-code your PATH. This way is far more organised. You could even target it with your app!

Interested? Then read on!

What is it?

A replacement for Apple's /usr/libexec/path_helper.

What does that do?

Apple's path_helper helps set the PATH and MANPATH environment variables, which is good but there are some significant problems with the way they've done it. This one fixes the bad stuff and builds on the good stuff. The 3 most important features are:

  1. It has per user paths as well as system wide ones.
  2. It extends the concept to include other paths than just PATH and MANPATH.
  3. It's got some helpful output for debugging your paths.

and one more for luck

  1. It's got no side effects, you simply ask it for a path and it gives back a path, no eval or setting the PATH inside the script.

How do i get this wonderful joyful event maker into my life?

A.K.A. install instructions

It's just a script with no dependencies other than Ruby.

  • Download it (e.g. git clone or a download link, you can even just copy and paste the script)
  • Make sure it has the correct permissions (chmod +x)
  • Have a look at the help by running it with -h.
  • Run the --setup (take note of the --lib and --config and their --no- counterparts)
  • Copy and paste the bit setup tells you to, and put it in your ~/.zshenv or ~/.bashenv
  • Find your life is so much better now it's easy to manage your paths

It doesn't need to be in /usr/local/bin, or any special place, just chmod +x it and call it by the full path and it'll plop out a string for you.

See An example install for more.

How does the Apple one work?

Segments of the path are defined in text files under /etc/paths.d and in /etc/path. For example, on my machine:

$ tree /etc/paths.d
/etc/paths.d
├── 10-BitKeeper
├── 10-pkgsrc
├── 15-macports
├── 20-XCode
├── MacGPG2
├── dotnet
├── dotnet-cli-tools
├── go
├── mono-commands
└── workbooks

$ cat /etc/paths /etc/paths.d/*
/usr/local/bin
/usr/local/sbin
/usr/bin
/usr/sbin
/bin
/sbin
/Applications/GPAC.app/Contents/MacOS/
/Applications/BitKeeper.app/Contents/Resources/bitkeeper
/opt/pkg/sbin
/opt/pkg/bin
/opt/local/bin
/Library/Developer/CommandLineTools/usr/bin
/usr/local/MacGPG2/bin
/usr/local/share/dotnet
~/.dotnet/tools
/usr/local/go/bin
/Library/Frameworks/Mono.framework/Versions/Current/Commands
/Applications/Xamarin Workbooks.app/Contents/SharedSupport/path-bin
/usr/local/sbin
/usr/bin
/usr/sbin
/bin
/sbin
/Applications/GPAC.app/Contents/MacOS/

Why replace it?

Because Apple's one loads the system libraries to the front, take a look:

$ /usr/libexec/path_helper
PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/Applications/GPAC.app/Contents/MacOS/:/usr/local/go/bin:/Library/Developer/CommandLineTools/usr/bin:<snip!>

…the rest of the items are added after, which means anything you add to /etc/paths.d/ will end up after the system libraries.

Want your up-to-date OpenSSL installed via Macports to be first in the PATH? Apple says "too bad!"

Want your much newer version of LLVM installed via pkgsrc to be hit first? Apple says "too bad!"

Well, there are alternatives.

More drawbacks to Apple's way

Where the Apple path_helper falls down is:

  • It puts things in /etc, meaning you need elevated permissions to add/remove path segments.
  • Being in /etc also makes them system wide.
  • It's only for PATH and MANPATH but development and administration often need headers and libraries accessible in the same way too.
  • The string it returns is designed to be eval'd. I know that eval isn't always evil but why not just return the PATH string and allow it to be set to a variable? Maybe there's more to be added.

Do i need to be on Apple to use it?

No, it should work on any unix-like system. It has one dependency, and that is Ruby. It should work with any system running Ruby 2.3.7 or above, as that is the version that ships with a Mac.

How does path_helper know what to put in the path?

Apple has put paths in /etc/paths and further files are there for the user or apps to add under /etc/paths.d/. If you want to order them then prefixing a number works well, e.g.

$ tree /etc/paths.d
/etc/paths.d
├── 10-pkgsrc
└── MacGPG2
└── ImageMagick

The format of the file is simply a path per line, e.g.

$ cat /etc/paths.d/10-pkgsrc
/opt/pkg/bin
/opt/pkg/sbin

$ cat /etc/paths            
/usr/local/bin
/usr/local/sbin
/usr/bin
/usr/sbin
/bin
/sbin

The order within the file matters as well as the order the files are read/concatenated.

Note:

The /etc/paths file in Apple isn't set out fully or in the order I'd want so I changed mine, you may want to do the same.

Per user paths

This is the bit I like best.

Apple's path_helper doesn't help with paths that may only be applicable for a single user. This version will check the following per user directories for path info:

  • ~/Library/Paths/paths.d and
  • ~/Library/Paths/paths
  • ~/.config/paths.d/ and
  • ~/.config/paths

You can use the --setup switch to have the path_helper set up the directory layout and files, you just have to fill them!

You can also use the tilde ~ character in a path by replacing it with the HOME env variable. For example, if I install Haskell and want to put it in my path I can take the following steps.

Pre-req

path_helper --setup --no-config --no-etc

This would set up the ~/Library/Paths for you, which fits a Mac very well.

path_helper --setup --no-lib --no-etc

You might choose this way if you're on a Mac or using Linux. It's up to you.

Way 1: Use the paths, Luke

On my Mac, Haskell resides in ~/Library/Haskell.

$ echo '~/Library/Haskell/bin' > ~/Library/Paths/paths

$ tree ~/Library/Paths 
/Users/iainb/Library/Paths
├── paths
└── paths.d

$ cat ~/Library/Paths/paths                          
~/Library/Haskell/bin

That puts /Users/iainb/Library/Haskell/bin at the front of my path and will only apply to my account's PATH.

Way 2: paths.d/

$ touch ~/Library/Paths/paths.d/60-Haskell

$ tree ~/Library/Paths 
/Users/iainb/Library/Paths
├── paths
└── paths.d
    └── 60-Haskell

Why use the paths.d sub directory?

Perhaps if I show you my actual set up it'll become clearer:

$ tree ~/Library/Paths 
/Users/iainb/Library/Paths
├── paths
└── paths.d
    ├── 05-pkgsrc
    ├── 08-homebrew
    ├── 10-keybase
    ├── 30-oh-my-zshell
    ├── 50-ngrok
    ├── 55-Crystal-opt
    ├── 60-Crystal
    ├── 61-Opam
    ├── 62-Haskell
    ├── 63-Erlang
    ├── 63-Go
    ├── 64-Pyenv
    ├── 65-Rust
    └── 66-Antigen

Imagine uninstalling Haskell and wanting to remove it from the PATH - are you sure you removed all of it? All the right parts? Did you make a typo?

Imagine you've developed a tool but on install you have to get the user to manually edit their PATH, or perhaps you're going to rely on PATH="/my/obnoxious/munging:$PATH"?

Once you start installing various things it makes sense to keep their paths in their own file, it's easier to organise (and remove). It's also easy for apps to target this to easily add things to a path. Some apps already do this by adding to /etc/paths.d (although that obviously needs elevated privileges and makes things system wide, so again, per user paths are better).

Ordering

path_helper will read files in this order:

  1. ~/Library/Paths/paths.d
  2. ~/Library/Paths/paths
  3. ~/.config/paths.d
  4. ~/.config/paths
  5. /etc/paths.d
  6. /etc/paths

If you don't have any of those dirs/files, they are skipped. Files within the .d dirs are read in file system order.

Why Library/Paths/paths and not Library/paths?

Because this is such a useful pattern that it can be extended for headers and includes, so ~/Library/Paths/paths is for the PATH, ~/Library/Paths/manpaths is for the MANPATH etc.

MAN and DYLD and C_INCLUDE and PKG_CONFIG

MANPATH

Apple has already dictated that /etc/manpaths and /etc/manpaths.d/ are the default paths for setting MANPATH, so the same pattern has been followed for that as with PATH:

  • ~/Library/Paths/manpaths.d/
  • ~/Library/Paths/manpaths
  • ~/.config/manpaths.d/
  • ~/.config/manpaths
  • /etc/manpaths.d/
  • /etc/manpaths

I can tell you it's a very pleasant experience typing man blah for the thing I just installed and getting the correct man page up.

DYLD_FALLBACK_LIBRARY_PATH and DYLD_FALLBACK_FRAMEWORK_PATH

Same goes for DYLD_FALLBACK_LIBRARY_PATH and DYLD_FALLBACK_FRAMEWORK_PATH:

  • ~/Library/Paths/dyld_library_paths.d/
  • ~/Library/Paths/dyld_library_paths
  • ~/.config/dyld_library_paths.d/
  • ~/.config/dyld_library_paths
  • /etc/dyld_library_paths.d/
  • /etc/dyld_library_paths

and:

  • ~/Library/Paths/dyld_framework_paths.d/
  • ~/Library/Paths/dyld_framework_paths
  • ~/.config/dyld_framework_paths.d/
  • ~/.config/dyld_framework_paths
  • /etc/dyld_framework_paths.d/
  • /etc/dyld_framework_paths

C_INCLUDE_PATH

Same again for C_INCLUDE_PATH:

  • ~/Library/Paths/include_paths.d/
  • ~/Library/Paths/include_paths
  • ~/.config/include_paths.d/
  • ~/.config/include_paths
  • /etc/include_paths.d/
  • /etc/include_paths

PKG_CONFIG_PATH

Did you know that there's a PKG_CONFIG_PATH? There is, check the man page, it's very helpful.

  • ~/Library/Paths/pkg_config_paths.d/
  • ~/Library/Paths/pkg_config_paths
  • ~/.config/pkg_config_paths.d/
  • ~/.config/pkg_config_paths
  • /etc/pkg_config_paths.d/
  • /etc/pkg_config_paths

An example install:

You could put the path_helper in /usr/local/libexec and mirror the Apple set up, so that other accounts to be able to access its goodness, but you can put it anywhere you like.

sudo mkdir -p /usr/local/libexec

Currently I run one from ~/bin so I don't bother with that.

mkdir ~/bin

Download the file then make sure it has the correct permissions:

chmod +x ~/bin/path_helper

Look at the help because you're not like everyone else, you read instructions ;-)

~/bin/path_helper --help

You need sudo to add the folders in /etc, see the --help if you don't want that. I don't want that, and let's say I prefer using ~/.config to ~/Library because I'm on a Linux system:

~/bin/path_helper --setup --no-etc --no-lib

See what's already there and why:

~/bin/path_helper --path --debug

Note: Apple's path_helper is in /usr/libexec, this install won't touch it, you can always use it or return to it if you wish.

And checking its output (debug shows you that too):

$ ~/bin/path_helper --path
    /opt/pkg/sbin:/opt/pkg/bin:/opt/X11/bin:/opt/ImageMagick/bin:/usr/local/MacGPG2/bin:/usr/local/git/bin:/opt/puppetlabs/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin"

To put it into the PATH via the command line:

$ PATH=$(~/bin/path_helper -p)
$ export PATH

but you'll probably use the helpful instructions --setup provides at the end of setting up:

# Put this in your ~/.bashrc or your ~/.zshenv
if [ -x /Users/$USER/Projects/path_helper/exe/path_helper ]; then
  C_INCLUDE_PATH=$(ruby /Users/$USER/Projects/path_helper/exe/path_helper -c)
  DYLD_FALLBACK_FRAMEWORK_PATH=$(ruby /Users/$USER/Projects/path_helper/exe/path_helper --dyld-fram)
  DYLD_FALLBACK_LIBRARY_PATH=$(ruby /Users/$USER/Projects/path_helper/exe/path_helper --dyld-lib)
  MANPATH=$(ruby /Users/$USER/Projects/path_helper/exe/path_helper -m)
  PKG_CONFIG_PATH=$(ruby /Users/$USER/Projects/path_helper/exe/path_helper -pc)
  PATH=$(ruby /Users/$USER/Projects/path_helper/exe/path_helper -p)
fi

export C_INCLUDE_PATH
export DYLD_FALLBACK_FRAMEWORK_PATH
export DYLD_FALLBACK_LIBRARY_PATH
export MANPATH
export PKG_CONFIG_PATH
export PATH

NOTE!

Remember, it won't set the PATH, it returns a path, you have to set the path with it e.g. PATH=$(/usr/local/libexec/path_helper.rb -p). Call /usr/local/libexec/path_helper -h to see all the options.

Another NOTE!

The because the Ruby team decided to spam us with warnings about everything so quite often recently I get a lot of unhelpful stuff filling up my terminal on open. Thanks, Ruby core team!

To quieten it down change:

PATH=$(ruby /path/to/path_helper -p)

to:

PATH=$(ruby /path/to/path_helper -p 2>/dev/null)

The ability to debug your paths

The --debug flag is really helpful. For example:

$ exe/path_helper -p --debug           
Name: PATH
Options: {:name=>"PATH", :current_path=>nil, :debug=>true, :verbose=>true}
Search order: [:config, :etc]
  /root/.config/paths/paths.d
  /root/.config/paths/paths
  /etc/paths.d
  /etc/paths

Results: (duplicates marked by ✗)

/root/.config/paths/paths.d/03-libiconv
 └── ~/Library/Frameworks/Libiconv.framework/Versions/Current/bin
/root/.config/paths/paths.d/04-llvm
 ├── /opt/local/libexec/llvm-11/bin
 ├── /opt/pkg/bin
 └── ~/Library/Frameworks/LLVM.framework/Programs
/root/.config/paths/paths.d/05-pkgsrc
 ├── /opt/pkg/bin ✗
 ├── /opt/pkg/sbin
 └── /opt/pkg/gnu/bin
/root/.config/paths/paths.d/10-keybase
 ├── $HOME/gopath
 └── $HOME/gopath/bin
/root/.config/paths/paths.d/30-oh-my-zshell
 └── ~/.oh-my-zsh/custom/plugins/fzf/bin
/root/.config/paths/paths.d/50-ngrok
 └── ~/Applications/ngrok
/root/.config/paths/paths.d/55-Crystal-opt
 ├── /opt/crystal/bin
 └── /opt/crystal/embedded/bin
/root/.config/paths/paths.d/60-Crystal
 ├── ~/Library/Frameworks/Crystal.framework/Versions/Current/bin
 └── ~/Library/Frameworks/Crystal.framework/Versions/Current/embedded/bin
/root/.config/paths/paths.d/61-Opam-and-OCaml
 ├── ~/Library/Frameworks/Opam.framework/Programs
 ├── ~/.opam/4.10.0/bin
 └── ~/.opam/4.10.0/sbin
/root/.config/paths/paths.d/62-Haskell
 └── ~/Library/Haskell/bin
/root/.config/paths/paths.d/63-Erlang
 └── ~/Library/Frameworks/Erlang.framework/Programs
/root/.config/paths/paths.d/63-Go
 └── ~/go/bin
/root/.config/paths/paths.d/64-Pyenv
 └── ~/.pyenv/bin
/root/.config/paths/paths.d/65-Rust
 └── ~/.cargo/bin
/root/.config/paths/paths.d/66-Antigen
 └── ~/bin
/root/.config/paths/paths.d/67-Lua
 └── ~/.lua/bin
/root/.config/paths/paths.d/68-Zig
 └── ~/Library/Frameworks/Zig.framework/Programs
/root/.config/paths/paths.d/docker-scripts
 └── ~/Projects/ThePrintedBird/scripts/docker
/root/.config/paths/paths.d/gcc
 ├── /opt/pkg/gcc7/bin
 └── /opt/pkg/gcc48/bin
/root/.config/paths/paths
 └── /opt/local/sbin
/etc/paths

Env var:
/root/Library/Frameworks/Libiconv.framework/Versions/Current/bin:/opt/local/libexec/llvm-11/bin:/opt/pkg/bin:/root/Library/Frameworks/LLVM.framework/Programs:/opt/pkg/sbin:/opt/pkg/gnu/bin:$HOME/gopath:$HOME/gopath/bin:/root/.oh-my-zsh/custom/plugins/fzf/bin:/root/Applications/ngrok:/opt/crystal/bin:/opt/crystal/embedded/bin:/root/Library/Frameworks/Crystal.framework/Versions/Current/bin:/root/Library/Frameworks/Crystal.framework/Versions/Current/embedded/bin:/root/Library/Frameworks/Opam.framework/Programs:/root/.opam/4.10.0/bin:/root/.opam/4.10.0/sbin:/root/Library/Haskell/bin:/root/Library/Frameworks/Erlang.framework/Programs:/root/go/bin:/root/.pyenv/bin:/root/.cargo/bin:/root/bin:/root/.lua/bin:/root/Library/Frameworks/Zig.framework/Programs:/root/Projects/ThePrintedBird/scripts/docker:/opt/pkg/gcc7/bin:/opt/pkg/gcc48/bin:/opt/local/sbin

Everything you need to know! Very useful for working out when other things are manipulating the path too.

Development

I'm happy to hear from you, email me or open an issue. Pull requests are fine too, try to bring me a spec or an example if you want a feature or find a bug.

To get set up for development

The project supports both Ruby and Crystal implementations. A Makefile manages Docker/Podman builds and testing across multiple versions of both languages.

Build images for all Ruby versions:

make build-all

Build images for all Crystal versions:

make build-crystal-all

Build and test everything (Ruby + Crystal):

make all

This uses git information for version tagging during development. For a release build with an explicit version:

VERSION=5.0.0 make all

See all available commands:

make help

To run the specs

Run tests for all Ruby versions:

make test-all

Run tests for all Crystal versions:

make test-crystal-all

Run tests for a specific version:

make test RUBY_VER=2.7
make test-crystal CRYSTAL_VER=1.14.0

List available images:

make list

Shell in and have a play

Open an interactive shell in a container:

make shell RUBY_VER=3.3
make shell-crystal CRYSTAL_VER=latest

Or use docker/podman directly:

# Ruby version
podman run --rm -ti --entrypoint sh path_helper:latest-ruby3.3

# Crystal version
podman run --rm -ti --entrypoint sh path_helper:latest-crystallatest

Run some tests yourself:

podman run --rm -ti --entrypoint sh path_helper:latest-ruby3.3
./spec/shell_spec.sh

# Or test the Crystal binary directly
podman run --rm -ti --entrypoint sh path_helper:latest-crystallatest
./bin/path_helper --help
./bin/path_helper -p --debug

Set up some paths using the test fixtures:

./exe/path_helper --setup --no-lib
cp -R spec/fixtures/moredirs/* ~/.config/paths

Have a look at the output by running through the available paths:

./exe/path_helper -p
./exe/path_helper -c
./exe/path_helper -f
./exe/path_helper -l
./exe/path_helper -m
./exe/path_helper --pc
./exe/path_helper -p --debug

Add colour support to the terminal so you can see the prettiness:

apk add ncurses
./exe/path_helper -p --debug

You may want to have the env vars set. Run:

source ~/.ashenv
echo $PATH
echo $C_INCLUDE_PATH
# etc

Modify some of the path files

apk add vim
vim ~/.config/paths/paths.d/03-libiconv
vim ~/.config/paths/paths.d/01-Nim
./exe/path_helper -p
# ...
exit

CI/CD

The project uses GitHub Actions for continuous integration. The workflow runs on pushes and pull requests to the master and dev branches.

Workflow Features

  • Ruby Version Matrix: Tests run against multiple Ruby versions (2.3.7, 2.7, 3.2, 3.3)
  • Manual Triggers: Workflow can be manually triggered via workflow_dispatch
  • Concurrency Control: Duplicate runs are cancelled when new commits are pushed
  • APT Caching: Dependencies are cached to speed up builds
  • Test Summaries: Results are displayed in the GitHub Actions UI
  • Artifact Retention: Test results are kept for 7 days

Workflow Structure

The main workflow file is located at .github/workflows/path_helper_tests.yml. It:

  1. Checks out the code
  2. Sets up the specified Ruby version
  3. Installs dependencies (alpine-pbuilder)
  4. Configures the test environment
  5. Runs the shell-based test suite
  6. Generates test summaries and uploads artifacts

Contributing to CI/CD

When making changes to the GitHub Actions workflow:

  1. Test locally first: Use act to test workflow changes locally before pushing
  2. Use a feature branch: Make workflow changes on a separate branch and verify they pass
  3. Update documentation: If adding new features, update this README section
  4. Maintain backwards compatibility: Ensure changes don't break existing test patterns
  5. Follow security best practices: Use minimal permissions, pin action versions, and avoid secrets in logs

Key files:

  • .github/workflows/path_helper_tests.yml - Main test workflow
  • spec/shell_spec.sh - Shell-based test suite
  • spec/fixtures/ - Test fixtures and expected results

Running Tests Locally vs CI

Local Testing (Docker)

The recommended way to run tests locally is using Docker, which provides an isolated environment:

# Build the Docker image
PATH_HELPER_VERSION=$(./exe/path_helper --version 2>&1)
packer build -var="ph_version=$PATH_HELPER_VERSION" docker/docker.pkr.hcl

# Run tests for specific Ruby versions
docker run --rm path_helper:$PATH_HELPER_VERSION-ph-r237
docker run --rm path_helper:$PATH_HELPER_VERSION-ph-r270

# Interactive shell for debugging
docker run --rm -ti --entrypoint="" path_helper sh

Local Testing (act)

To simulate the GitHub Actions environment locally:

# Install act (https://github.com/nektos/act)
# Then run the workflow
act push

# Run with specific Ruby version
act push --matrix ruby-version:3.2

CI Testing

Tests automatically run on GitHub Actions when:

  • Pushing to master or dev branches
  • Opening/updating pull requests to those branches
  • Manually triggering via the Actions tab (workflow_dispatch)

Key Differences

Aspect Local (Docker) CI (GitHub Actions)
Environment Alpine Linux Ubuntu
Ruby setup Pre-built in image ruby/setup-ruby action
Test output Console only Artifacts + Summary
Speed Fast (cached image) Depends on cache hits

Licence

See the LICENCE file.

Repository

path_helper

Owner
Statistic
  • 44
  • 3
  • 0
  • 0
  • 0
  • 44 minutes ago
  • December 30, 2016
License

Other

Links
Synced at

Sun, 23 Nov 2025 05:42:55 GMT

Languages