Friday, January 26, 2024

[SOLVED] Reading dotenv file within a target in a Makefile

Issue

I have a Makefile such that one of the targets creates a .env file, which is read in another target. So, since the file is created dynamically, I can't include it at the top of the Makefile. I tried the method using export and xargs, as described here, but it doesn't work. Here's my Makefile:

create_dotenv_file:
  echo "FOO=BAR" > .env

test_env: create_dotenv_file
  export $(cat .env | xargs) && \
  echo $(FOO)

And this is what I get when executing $ make test_env:

/home# make test_env
echo "FOO=BAR" > .env
export  && \
echo

What am I missing?


Solution

There are several problems here:

  1. the expression $(cat .env | xargs) isn't doing what you intend. The make command itself uses $ to variable expansion; this means that make is interpreting that expression, rather than the shell. To make it looks like a reference to an invalid variable so the replacement is the empty string.

    Similarly, $(FOO) is referring to the make variable FOO, which doesn't exist.

    You will need to replace $ with $$ in order for the shell to see a single $.

  2. Each line in a Makefile target executes in a new shell. Even if your export $(cat .env|xargs) statement works, it's a no-op -- changes to environment variables only impact the current process and its children. You are effectively starting a new shell, setting an environment variable, and then exiting the shell; your environment variables are lost.


You can rewrite your Makefile so that it "works" by doing something like this:

create_dotenv_file:
    echo "FOO=1" > .env
    echo "BAR=2" >> .env

test_env: create_dotenv_file
    export $$(xargs < .env); \
    echo $${FOO}

But this is still problematic. The above sets two variables and works as expected, but what happens if one of your variables contains whitespace, like:

create_dotenv_file:
    echo "FOO=\"This is \"" > .env
    echo "BAR=\"a test\"" >> .env

You'll end up with the shell expression:

export FOO=This is  BAR=a test

So you will have FOO=This, BAR=a, and a couple of empty environment variables named is and test.

A more effective solution might look something like this:

create_dotenv_file:
    echo "FOO=\"This is \"" > .env
    echo "BAR=\"a test\"" >> .env

test_env: create_dotenv_file
    set -a; \
    . ./.env; \
    set +a; \
    echo $${FOO}

This uses set -a to enable the export of all variables that we create or modify; then . to read in the .env file, and then set +a to disable the set -a. The output of running make test_env with the above Makefile is:

echo "FOO=\"This is \"" > .env
echo "BAR=\"a test\"" >> .env
set -a; \
. ./.env; \
set +a; \
echo ${FOO}
This is

But even with that solution, it probably still doesn't do what you want...variables sourced in the test_env target won't be available anywhere else, because setting environment variables only impacts the current process and its children. Exporting variables in shell scripts simply cannot make them available elsewhere in the Makefile.



Answered By - larsks
Answer Checked By - Candace Johnson (WPSolving Volunteer)