Working Effectively with .env Files in Emacs

ยท

4 min read

Emacs and environment variables

Environment variables are global key-value associations for a user account on a computer. On *nix systems we define them in certain files such as ~/.profile for a login profile, ~/.bash_profile or ~/.zshenv for a shell. On Windows, we define them using a graphical wizard. A popular environment variable is the PATH variable that specifies where command binaries are found.

When we start up Emacs, the environment already defined are available within Emacs. We can access them using the Emacs Lisp function getenv.

(getenv "GO111MODULE")

Any process run within Emacs, such as a compilation or a terminal, can read the environment variables too. However, environment variables defined elsewhere, such as in a .env file, are not available to Emacs.

Working with .env files

It is common in web development to define environment variables used by the application in a .env file at the root of the project. Variables like database credentials, NODE_ENV, etc. JetBrains IDEs allow us to set the location of a .env file that will be read and added to the environment variables for the project. Alternatively we can use a package like dotenv (in Node.js) that looks for a .env file at the root of your project and loads it at run time.

Unlike JetBrains IDEs, Emacs doesn't have an inbuilt configuration for loading a .env file. But it has a programmable interface that allow us implement that neatly. Let's do it!

Locating the .env file

We don't want to limit ourselves by assuming that our .env file will always be at our project root; it can be in a parent folder. I make use of git worktrees frequently because it lets me have various concurrent development trees without swinging branches and hopping stashes. For such a use case we would would need a .env file in every git worktree. It would be better to place a common .env file at the parent directory and use it for all worktrees. Let's create a function that locates a .env file at the nearest parent directory, starting at the current directory.

(defvar @-dotenv-file-name ".env"
  "The name of the .env file."
  )

(defun @-find-env-file ()
  "Find the closest .env file in the directory hierarchy."

  (let* ((env-file-directory (locate-dominating-file "." @-dotenv-file-name))
        (file-name (concat env-file-directory @-dotenv-file-name)))
    (when (file-exists-p file-name)
        file-name))
  )

locate-dominating-file is the Emacs function that does the heavy lifting here. You should check its documentation, it's very useful.

Reading .env files

To load a .env file into your Emacs environment, we have to read and parse it. Luckily there's an Emacs package that does that. It reads the file we pass to it and sets the environment variables. Let's install the package using use-package.

(use-package load-env-vars)

Now define a function that sets the environment variables using load-env-vars.

(defun @-set-project-env ()
  "Export all environment variables in the closest .env file."

  (let ((env-file (@-find-env-file)))
    (when env-file
      (load-env-vars env-file)))
  )

Two functions, one package, and we're almost done. All that's left is to call the @-set-project-env function when we're in a project.

Hook all the things ๐Ÿช

In Emacs we can attach functions to be run at customisation points called hooks. Hooks are lists of functions that will be run at particular moments, such as when entering a major mode. I have a few modes where I need variables from my .env files to be available.

  1. When in a project (managed by projectile)
  2. When switching from one project to another (also managed by projectile)
  3. When running a compilation (comint mode)
  4. When starting a terminal from within Emacs (I use vterm)
  5. When lsp-mode is on.

Let's add our @-set-project-env function as a hook on each of these modes.

(defun @-set-env-vars-hooks ()

  (use-package load-env-vars)
  (add-hook 'projectile-mode-hook #'@-set-project-env)
  (add-hook 'projectile-after-switch-project-hook #'@-set-project-env)
  (add-hook 'comint-exec-hook #'@-set-project-env)
  (add-hook 'lsp-mode-hook #'@-set-project-env)
  (add-hook 'vterm-mode-hook #'@-set-project-env)
  )

Finally, ensure to call (@-set-env-vars-hooks) in your init.el file (or in .spacemacs if you use Spacemacs).

If you've set it up correctly so far, go ahead and try it in any of the modes we defined. Place a .env file at the root of your project, then open vterm and inspect the environment variable in your shell. It should be set.

If you found this article helpful, leave a ๐Ÿ‘. Share your favourite Emacs tricks in the comments and spread the love of Emacs. Until next time. ๐Ÿ‘‹