Using direnv to improve your workflow

2024/05/01

Introduction

For the uninitiated, direnv is a tool that can load and unload environment variables depending on your current working directory. On it’s surface, this seems like a fairly trivial thing but if you take a closer look, direnv packs some really neat features which can really improve your development workflow.

Take for example the “simple” task of including an extra directory in your $PATH while working inside your project directory. Without direnv you’re looking at something like this:

$ cd project_dir/
$ export OLD_PATH=$PATH
$ export PATH=./bin:$PATH

<do some stuff>

$ cd ..
$ export PATH=$OLD_PATH

Envrc

Now, we could certainly write two scripts to do this for us and run them whenever we work in our project environment but we still have to remember to run them when we enter and exit a project environment. What if we had something to handle that for us; enter .envrc.

Instead of having a set of scripts to set up and tear down your environment manually, let’s put the necessary bits in to a file named .envrc within the root of our project directory:

export PATH=./bin:$PATH

Now tell direnv it’s ok to load the file with direnv allow and that’s it! Whenever you enter this directory in the future, direnv will change the value of $PATH for you. But wait, IT GETS BETTER!

The above example works but direnv has some extra tricks up it’s sleeve to make this process even easier and less error prone.

DIRENV-STDLIB

Direnv provides a stdlib of common functions that make working with direnv easier and less prone to common mistakes. Using the stdlib, let’s improve our example above. Replace the contents of the .envrc with the following:

PATH_add bin

You will need to re-allow direnv with direnv allow after any modifications to a .envrc file. Once you’ve done this, you can leave and re-enter the directory and your path will be updated to include /my/project/bin. Using PATH_add has several advantages over directly setting your $PATH. Primary among them being that it avoids common mistakes such as export PATH=bin which would remove all other values of $PATH (effectively breaking your environment).

Advanced Trickery

We have seen how direnv can modify environment variables and we’ve learned that direnv stdlib has some builtin functionality to make our lives either. Now, let’s look at some of the more interesting aspects of direnv and how they can be used to improve your development environment.

Nested Environments

Let’s say we have a project with some subdirectories, maybe it looks something like this:

project
├── bin
│   └── file2.sh
└── foo
    └── bin
        ├── file.sh
        └── file2.sh

We want to add project/bin to our path but if we go in to project/foo/ we want to also add project/foo/bin as well. If we create a .envrc inside of project/ and inside of project/foo/ each containing PATH_add bin we are going to have a problem. In this scenario, direnv will load project/foo/.envrc when we navigate to project/foo/ but in doing so, it is going to unload the rc file from project/. If you guessed the stdlib has a solution for this, you’re absolutely right; it has two options, in fact: source_up .envrc and source_up_if_exists .envrc. Including one of these options in the rc file for project/foo/ will enable us to include both files. The following is an example of the project/foo/.envrc:

source_up .envrc
PATH_add bin

It’s important to remember that these rc files are read from top to bottom in this case. If you had project/bin/my_app and project/foo/bin/my_app and wanted the most specific to be used when running my_app, you need to ensure you include PATH_add bin after sourcing the previous directories rc since doing so is equivalent to:

PATH_add ../bin
PATH_add bin

and PATH_add prepends the specified directory to the front of $PATH.

Custom functions

In addition to the stdlib, direnv provides a way to create custom functions inside of ~/.config/direnv/direnvrc. Inside this file you can define standard bash functions that you want to have available to any .envrc file, allowing you to extend the functionality of direnv. Let’s now take look at one such use case for custom functionality within direnv.

Per Directory Command Aliases (sort of)

Unfortunately, as of today, there is no native method for loading additional shell aliases inside of a .envrc file. However, using custom functions we can kind of fake it. While this does not, strictly speaking, create shell aliases, it does provide a method for creating alias-like commands inside of your .envrc file. The one downside to this approach is that it will create and leave files behind within your project directory. These are easy enough to ignore with a .gitignore file and I’ve seen some examples of ZSH scripts which can handle the load/unload events outside of direnv but I will not be looking at those today. If you want to read more, check out the comments on this PR.

The meat of this is in the custom function. You’ll place the following function inside of your ~/.config/direnv/direnvrc file:

export_alias() {
  local name=$1
  shift
  local alias_dir=$PWD/.direnv/aliases
  local target="$alias_dir/$name"
  local oldpath="$PATH"
  mkdir -p "$alias_dir"
  if ! [[ ":$PATH:" == *":$alias_dir:"* ]]; then
    PATH_add "$alias_dir"
  fi

  echo "#!/usr/bin/env bash" > "$target"
  echo "PATH=\"$oldpath\"" >> "$target"
  echo "$@" >> "$target"
  chmod +x "$target"
}
<< Using Terraform with libvirt in 2022 Portable Dev Environments with Devpods >>