We're Hiring

How to manage a project-specific PATH with direnv

Our man Andrew Peng has a look at shell extension direnv.net and how it can improve your development environment.  

From direnv.net

direnv is an extension for your shell. It augments existing shells with a new feature that can load and unload environment variables depending on the current directory.

direnv allows you to do fancy things like creating isolated development environments using tools such as Nix but in this blog post we're going to look at a simpler example that involves modifying the PATH environment variable within a particular directory.


Imagine we're working on a Dockerised project and we want to be able to easily run command-line tools (such as our old friend rubocop in the container environment from our usual shell on the host system.

We might start with something like

$ docker-compose run --rm web bash -c "bundle exec rubocop --version"
0.83.0

We can simplify this a bit by wrapping it in an executable shell script.


#!/usr/bin/env bash

docker-compose run --rm web bash -c "bundle exec rubocop $*"

We can save this as rubocop and run


chmod +x rubocop
mkdir bin
mv rubocop bin/

to make it executable and move it into a subdirectory.

$ bin/rubocop --version
0.83.0

This is much shorter but we still need to remember where we put the wrapper script whenever we want to run it. It would be nice if we could just run the command rubocop and have this automatically resolve to the wrapper script.

We could prepend the bin directory to the PATH environment variable which would make this possible but would also mean that we would end up using the wrapper script whenever we ran the command rubocop from anywhere, including from outside of this project.

$ cd ~/test-project
$ ls bin/
rubocop

$ export PATH=$PWD/bin:$PATH

$ which rubocop
/Users/andrew/test-project/bin/rubocop

# success!
$ rubocop --version
0.83.0

$ cd ~

$ which rubocop
/Users/andrew/test-project/bin/rubocop

# :(
$ rubocop --version
ERROR:
        Can't find a suitable configuration file in this directory or any
        parent. Are you in the right directory?

        Supported filenames: docker-compose.yml, docker-compose.yaml

We can solve this problem quite nicely by only prepending the bin directory to PATH when we're in the project directory; when we're in the project directory then rubocop will resolve to the wrapper script but when we're somewhere else then rubocop will resolve to whatever would be found in PATH normally.

Conveniently, this is exactly what direnv allows us to do.


direnv is able to automatically update PATH whenever we change into or out of the project directory.

From direnv.net

Before each prompt, direnv checks for the existence of a .envrc file in the current and parent directories. If the file exists (and is authorized), it is loaded into a bash sub-shell and all exported variables are then captured by direnv and then made available to the current shell.

This might look something like

$ cd ~

$ echo $PATH
/Users/andrew/bin:...

$ cd ~/test-project
direnv: loading ~/test-project/.envrc
direnv: export ~PATH

$ echo $PATH
/Users/andrew/test-project/bin:/Users/andrew/bin:...

$ cd ~
direnv: unloading

$ echo $PATH
/Users/andrew/bin:...

To configure direnv for our use case, we need to create a .envrc file in the project directory and add the following

PATH_add ~/test-project/bin

PATH_add is a helper function provided by direnv which prepends the supplied directory to PATH when changing into the project directory and removes it when changing out of the project directory.

If we change into the project directory then we should see

$ cd ~/test-project
direnv: error /Users/andrew/test-project/.envrc is blocked. Run `direnv allow` to approve its content

direnv will start managing PATH within this directory once we have authorised the .envrc file.

$ direnv allow
direnv: loading ~/test-project/.envrc
direnv: export ~PATH

$ which rubocop
/Users/andrew/test-project/bin/rubocop

# success!
$ rubocop --version
0.83.0

$ cd ~
direnv: unloading

# global installation
$ which rubocop
/Users/andrew/bin/rubocop

# phew
$ rubocop --version
0.83.0

And we're done!

In practice, my home directory looks something like

$HOME
|-- bin
|   |-- docker
|   |   |-- rubocop
|   |   |-- ... other tools
|-- docker-projects
|   |-- .envrc
|   |-- project1
|   |-- project2
|   |-- ... other projects

where .envrc contains

PATH_add ~/bin/docker

which allows me to use the same set of wrapper scripts across all Dockerised projects without needing to add any additional configuration for new projects.


How Ruby if statements can help you write better code by Dave Cocks
How do you solve a problem like caching in Progressive Web Apps? by Dave Quilter
Rails 6: Seeing Action Text in... action by Stephen Giles
A Quick Comment on Git Stash by Karen Fielding