Search code examples
ubuntumakefileenvironment-variablesgnu-make

Reading dotenv file within a target in a Makefile


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.