A better way to manage your projects’ configuration
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?
direnv
is 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 variablewatch_file <file>
— this will reload the environment every time <file> changesstrict_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 variablesuse <program> <version>
— A semantic command dispatch intended for loading external dependencies into the environment. The list of out-the-box usages is presented herelayout <type>
— A semantic dispatch used to describe common project layouts. Type can be go/julia/node/php/perl/pythondotenv/dotenv_if_exists <path>
— loads thedotenv file specified by <path>, optionally checking it exists when used in second formhas <command>
— returns true if the command exists, false otherwisesource_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 bin
folder 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 usingecho -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.