Two direnv extensions which help ensure a project's environment variables are properly set up
Why direnv?
I prefer to use direnv in development rather than .env
files because direnv provides configuration via the environment itself, rather than via a system that has to be explicitly loaded. The application knows nothing about how its environment is set up.
My Requirements
I want any project I create to have a template for setting up development environment and checks that things stay aligned.
First, I add a note to every project's README to indicate that direnv is required in development.
Any developer who works a project configured this way will need to set it up.
Once they've got that working, the developer (who may be myself months later!) needs to know what values to set and what they mean.
I achieve this by through the combination of .envrc
and .envrc.private
and the use of two direnv functions that check that everything's been set up correctly.
.envrc
All my projects have an .envrc
file which is included in the repo and which look like this:
# SOME_REQUIRED_VARIABLE: some variable that must be set for the application to run
export SOME_REQUIRED_VARIABLE=
# SOME_OTHER_VAR: (optional) a variable you don't necessarily need to set
export SOME_OTHER_VAR=
# A_VARIABLE_WITH_A_DEFAULT: true*|false - a variable that has a default
export A_VARIABLE_WITH_A_DEFAULT=true
source_env .envrc.private
source_env .config/direnvrc
ensure_all_documented
ensure_all_set
All the environment variables that the application relies on to be set are included, possibily with default values. Each variable has documentation, which can also indicate if the variable is optional
.
The Checks
Two direnv extensions, ensure_all_documented
and ensure_all_set
, are defined in the project:
# This file provides the following direnv extensions:
# * ensure_all_documented
# * ensure_all_set
# It assumes .envrc lists all environment variables *required* by a project,
# by exporting them set equal to an empty string:
# export MYVAR=
# or, to the default value:
# export MYVAR=some-default
# .envrc.private (which should be excluded from source control) supplies the values
# for required variables.
# Usage:
# 1. In `.envrc`, document all exports:
#
# # MYVAR: a value I want to set
# export MYVAR=
# # OTHERVAR: (optional) a value I might want to set
# export OTHERVAR=
#
# source_env .envrc.private
#
# 2. In `.envrc.private`, set all required values:
#
# export MYVAR=foo
#
# 3. At the end of `.envrc`, source this file:
#
# source_env .config/direnvrc
__direnv_print_error() (
local RED="\e[1;31m"
local DEFAULT="\e[0m"
echo -e "${RED}$1${DEFAULT}"
)
all_exports() (
# Find lines with exports of environment variables,
# collect the names of the environment variables.
# To get exported environment variables as an array
# do this:
# local exports=($(all_exports))
grep -Po '(?<=^export )[A-Z0-9_]+(?==)' .envrc
)
# Ensure that all exports from .envrc are documented,
# unless they are marked as (optional).
# A comment should be as follows:
# '# VARIABLE: documentation'
ensure_all_documented() (
local exports=($(all_exports))
for (( i=0; i<${#exports[@]}; i++ ))
do
local export=${exports[$i]}
grep -P "^# $export:" .envrc >/dev/null || __direnv_print_error "direnv: export $export does not have a comment"
done
)
ensure_all_set() (
local exports=($(all_exports))
for (( i=0; i<${#exports[@]}; i++ ))
do
local export=${exports[$i]}
# Skip optional exports
if grep -P "^# $export: \(optional\)" .envrc >/dev/null; then
continue
fi
if [ -z ${!export} ]; then
__direnv_print_error "direnv: export $export is unset. Either set it in .envrc.private or mark it as optional by adding a comment '# $export: (optional) ...'"
fi
done
)
ensure_all_set
checks that all the environment variables set in .envrc
have been given a value.
ensure_all_set
is an automatic version of direnv's own env_vars_required
. Unlike env_vars_required
, it doesn't require a list of the variables to check - it checks every variable that is declared, so I believe it's less error prone. You can opt out of the check by putting (optional)
in the documentation string.
ensure_all_documented
checks that all the environment variables in .envrc
have a preceding comment matching variable name + ": " + some text
. If they don't, it prints a warning in red:

ensure_all_documented
gives a warning in red if a variable is declared without documentation[edit: previously, I relied on these functions being set in the user's ~/.config/direnv/direnvrc
, but a comment on Lobsters convinced me that the code should be added to the repo.]
.envrc.private
.envrc.private
, is excluded from the repo (via .gitignore
) and is expected to be present by .envrc
, if it's not there, direnv gives a warning:
direnv: referenced .envrc.private does not exist
So, that message will prompt the developer into creating .envrc.private
.
This file needs to set the values from .envrc
without defaults:
export SOME_REQUIRED_VARIABLE=foo
Conclusion
When a project configured in this way, setup is quite linear: README -> direnv -> create .envrc.private
.
As things change during development, the checks ensure environment variables are set properly, and that future onboarding remains well documented.