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"
}