I commonly need environment variables for personal or work projects. In many cases, they are loaded automatically from a .env file: Docker/Podman can take a --env-file .env flag and many programming languages have a dotenv library that reads this file automatically. But when I want to run one-off commands in the terminal, I need to parse this file manually.

The pier

When it is just one environment variable, I just export FOO=bar and call it a day. But when there are more variables, I get lazy. Also, these export commands get saved in my shell history file which is very handy and very scary: to get a variable I exported in the past I just hit CTRL-R and start typing it until I recover the full command. But I don’t want a command with a secret saved in plain text in any shell history file (or any file at all).

Here’s a little trick to load all env vars from a .env file:

$ export $(<.env)

This will read all lines of the .env file, and feed each word to the export command.

For example, if you have this .env file:

CAT=Bob
OTHERCAT=Dimitri

The $(<.env) magic will produce CAT=Bob OTHERCAT=Dimitri, which is then passed to export. This exports all those variables.

But note: this trick is not a general-purpose .env parser, it only works for simple KEY=VALUE lines. Spaces, quotes, special characters, and “comments” kind of break this command. For example, this .env file:

CAT=Bob
OTHERCAT=Dimitri
# What about turtles?
TURTLE=Sabrina

Leads to this: export CAT=Bob OTHERCAT=Dimitri # What about turtles? TURTLE=Sabrina. Each word is sent to export as an argument, which gives some errors:

$ export $(<.env)
-bash: export: `#': not a valid identifier
-bash: export: `turtles?': not a valid identifier

But we still get the TURTLE variable exported.

Another way to load a .env file in Bash is to enable the allexport option and then source the file:

$ set -o allexport
$ source .env
$ set +o allexport

Disabling allexport afterwards is good to keep our sanity: allexport controls whether new or modified variables get automatically exported. Turning it off restores the default state.

Another advantage of allexport + source is that it allows us to define “dynamic” environment variables: we can use shell expansions and run commands to, for example, query a secrets store. A .env file is now a shell script, much more powerful than a mere data file:

CAT=Bob
OTHERCAT=Dimitri
# What about turtles?
TURTLE=Sabrina

LUCKY_NUMBER=${RANDOM}

API_KEY=$(keepassxc-cli show -sa Password ~/.secrets/personal.kdbx '/AWS/groot-key')

The disadvantage of this more powerful file (i.e. a Bash script) is that it breaks the export $(<.env) trick, and tools (e.g. docker run --env-file .env or a dotenv() function) that expect a simple KEY=VALUE file may misbehave or crash completely. And because this executes the script, you should only source files you trust.

PS: set -a is short for set -o allexport.