A better way to manage your projects’ configuration

Daniel Haek
5 min readFeb 10, 2021

--

There are many ways to make configuration available to your application, but none of them is so versatile and easy to use as direnv.

What’s direnv?

direnvis an extension of your shell, enabling you to switch between your contexts easily by loading and unloading environment variables depending on your location within the directory tree.

It does that by combining 2 items:

  • an .envrc file created by you, the user, that contains the environment variables that are specific to the current directory/context
  • a shell hook that makes sure that the direnv binary is called before the current shell displays a prompt.

Why would I use direnv?

  • it’s written in Go and compiled into a single binary which is fast enough to have no visible impact while using the shell
  • it’s language agnostic and hooks at the shell level rather than process level as other solutions (dotenv or similar)
  • supports most of the popular shells (bash, zsh, tcsh, fish, elvish)
  • has a set of useful builtin utility functions which can be extended by the user
  • it has no external dependencies which makes it highly portable

Installation

The installation topics are covered in the official page, but some of the usual ones are:

# debian
apt install direnv
# mac - https://formulae.brew.sh/formula/direnv
brew install direnv
# suse / opensuse
zypper in direnv
# snap - https://snapcraft.io/direnv
snap install direnv

After installation, you need to make sure that your current shell loads the direnv binary. Once again some of the most common examples are:

# bash
echo eval "$(direnv hook bash)" >> ~/.bashrc
# zsh
echo eval "$(direnv hook zsh)" >> ~/.zshrc
# fish
echo 'direnv hook fish | source' >> ~/.config/fish/config.fish

After that, simply run source <rc-file> in order to load the changes.

At the time being the Windows support is pretty basic, but it should work under WSL.

Ok, now what …

Now let’s see direnv in action.

Let’s create a test directory using mkdir test-project , move inside the directory with cd test-project and create an .envrc file

echo 'export DIRENV_ENV=hello-from-direnv' > .envrc

You should see a message like this

direnv: error /test-project/.envrc is blocked. Run `direnv allow` to approve its content

You should allow the changes with

direnv allow

in order to tell direnv that you are aware of the changes that were made to .envrc file and you want to apply them.

This is a security measure that prevents unwanted runs of the .envrc file (when cloning and browsing projects or other type of content from remote sources)

Right now, there will be a DIRENV_ENV environment available to your shell. You can check it’s presence with echo $DIRENV_ENV. Now if you will navigate out of your current directory with cd .. the environment variable will magically vanish, since direnv will restore the original environment (echoing environment variable again will return an empty line).

Please note that the setup inside .envrc files is available in all subdirectories under the root of the file.

Testing direnv with Docker

If you wish to simply test everything in a safe environment before doing modifications to your local one, you can do that with the help of docker and a Dockerfile. Open you favorite editor, paste

FROM alpine:3.13ENV EDITOR=viRUN apk add bash \
&& apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing direnv \
&& echo eval "$(direnv hook bash)" >> ~/.bashrc

and save the file under the name Dockerfile. Now, open the location of the Dockerfile in your shell and build the image:

docker build --tag direnv-demo .

and finally, run the docker container:

docker run --interactive --tty direnv-demo bash

Editing direnv files

direnv comes with a handy command to edit its files, namely direnv edit . Its behavior is based on the EDITOR environment variable, meaning that it will open the file with whatever is defined in the EDITOR variable. Examples:

export EDITOR=idea # to open with Jetbrains IDEAorexport EDITOR="code --wait" # to open with vscode

You can also directly edit the file and allow the changes (with direnv allow).

Utility functions

direnv comes with some utility functions that can be called inside the .envrc file (more can be added by the user by modifying the ~/.config/direnv/direnvrc file). The complete list is present in the official documentation.

Some of the most used ones are:

  • PATH_add <argument>— this adds the path contained by argument to the PATH environment variable
  • watch_file <file> — this will reload the environment every time <file> changes
  • strict_env — this enables strict evaluation over the evaluated shell contents, meaning that it will halt execution if any command exits with a code different from 0, or if it encounters any unused variables
  • use <program> <version> — A semantic command dispatch intended for loading external dependencies into the environment. The list of out-the-box usages is presented here
  • layout <type> — A semantic dispatch used to describe common project layouts. Type can be go/julia/node/php/perl/python
  • dotenv/dotenv_if_exists <path> — loads thedotenv file specified by <path>, optionally checking it exists when used in second form
  • has <command> — returns true if the command exists, false otherwise
  • source_env/source_env_if_exists <path> — sources another .envrc file specified by <path>

A real world example

Let’s try to create an .envrc file that will create a node.js environment using nvm (Node Version Manager), will also export 2 environment variables, namely PORT and LOG_LEVEL and will add the local binfolder to our PATH since we have some utility functions there.

Given that direnv does not support nvm out of the box, we will need 2 additional steps:

  • install nvm following the instructions given here
  • create a user defined function in ~/.config/direnv/direnvrc with these contents (this will make it available for usage in any .envrc file)
use_nodeenv() {
NODE_VERSION=${1}
type nvm >/dev/null 2>&1 || . ~/.nvm/nvm.sh
nvm use "$NODE_VERSION" # you can use install instead of use if you want to dynamically install version if it not exists
}

Now, you will add these lines to .envrc file

use nodeenv 15.5.1
export PORT=3333
export LOG_LEVEL=info
PATH_add bin

Typing direnv allow in your shell will yield something similar to

direnv: loading /test/.envrc
direnv: using nodeenv 15.5.1
Now using node v15.5.1 (npm v7.3.0)
direnv: export +LOG_LEVEL +PORT ~NVM_BIN ~NVM_INC ~PATH

Also typing echo $LOG_LEVEL $PORT will yield the values that we exported

info 3333

Also any files having executable flag under the bin folder should be available to your shell without specifying the whole path.

One last note

If you wish git to ignore the direnv files you can do that:

  • globally using touch ~/.gitignore $ git config --global core.excludesFile ~/.gitignore and using echo -e ".envrc\n.direnv" >> ~/.gitignore
  • locally by simply adding it to the .gitignore file echo -e ".envrc\n.direnv" >> .gitignore

The .gitignore file should look similar to

... other content ....direnv                       
.envrc

In the end I really hope that this will help you unclutter your local setups as it did it for me.

Credits

Unsplash Image : https://unsplash.com/photos/W_ZYCEUapF0

--

--