Last active
February 4, 2024 12:30
-
-
Save onecrayon/deec0fc13128be7eb0b5c19f767f732c to your computer and use it in GitHub Desktop.
Load dotenv files into Make environment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This comment shouldn't affect anything | |
SINGLE_QUOTED='just a string' | |
DOUBLE_QUOTED="another string" | |
UNQUOTED=don't strip my quote! | |
DOLLARS=$3.50 | |
MULTILINE_SINGLE_QUOTE='[ | |
"hello", | |
"world" | |
]' | |
MULTILINE_DOUBLE_QUOTE="{ | |
\"look_ma": \"it's JSON\" | |
}" | |
# RUH-ROH A TAB! | |
# EOF |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is an example Makefile stub that demonstrates how to load variables from a dotenv file into | |
# your Make environment. Note that it isn't perfect; for instance, the double-quoted JSON string | |
# in the example file will probably not parse correctly because the escape characters will remain | |
# as part of the string when it is passed through Make, non-JSON multiline strings will cause the | |
# file to fail to parse with an obscure Make error, and some advanced features such as interpolation | |
# are not supported. | |
# I always start my Makefiles with this command, because then they're effectively self-documenting | |
help: cmd-exists-grep cmd-exists-sed | |
@grep -F -h "##" $(MAKEFILE_LIST) | grep -F -v @grep | sed -e 's/\\$$//' | sed -e 's/: [a-zA-Z0-9_][ a-zA-Z0-9_-]*[a-zA-Z0-9]\([[:space:]]*\)##/:\1##/' | sed -e 's/## //' -e 's/##//' | |
# This snazzy little target allows dynamically defining a variable that is required by a given target. | |
# For instance, `some-target: guard-ENV` would ensure that the ENV variable is defined before executing | |
# the `some-target` make target. Source: https://lithic.tech/blog/2020-05/makefile-wildcards | |
guard-%: | |
@if [ -z '${${*}}' ]; then echo 'ERROR: environment variable $* not set' && exit 1; fi | |
# Similar to the above (and with the same source), although in this instance we ensure a particular | |
# command exists in the user's PATH. | |
cmd-exists-%: | |
@hash $(*) > /dev/null 2>&1 || \ | |
(echo "ERROR: '$(*)' must be installed and available on your PATH."; exit 1) | |
# This pair of targets relies on sed and awk, but allows loading dotenv environment files into | |
# environment variables by passing through Make. This is necessary for mixing specific variables | |
# from one environment into local execution or alternately for grabbing specific variables for | |
# use in deployment-based Make targets. | |
# | |
# This logic assumes that you have environment-specific files like `dev.env` or `prod.env`. | |
# You'll need to adjust paths in the targets to match your actual usage, as necessary. | |
# | |
# This logic could also be used to parse an actual `.env` file with a little tweaking. | |
# | |
# Thanks to its reliance on sed, the logic is completely opaque so let's break it down: | |
# | |
# 1. `prep-tmp-env`: this target uses sed to generate a file like ENV.env.tmp which | |
# can be safely parsed as Make variables. This is an unusual step, but is necessary because | |
# our environment files might rely on quoted strings, have dollar signs in values, or include | |
# multi-line strings. We must route through sed three times; the first, we handle changes that | |
# apply within a single line. The second and third we handle multiline strings (have to do it | |
# twice to grab both quotation types). Aside from sed's opaque syntax, this is complicated by | |
# the need to double escape all dollar signs lest Make transform them into variable lookups. | |
# | |
# Sed commands explained: | |
# | |
# * 's/\$$/$$$$/g' (actually 's/\$/$$/g'): replace all single dollar signs with double dollar signs | |
# * 's/="\(.*\)"$$/=\1/': strip double quotes from values (on a single line). Capture group | |
# parentheses must be marked with a backslash. `\1` is a back-reference. | |
# * 's/='"'"'\(.*\)'"'"'$$/=\1/': strip single quotes from values (on a single line). This is | |
# complicated by the fact that we can't include an escaped single quote without shell string | |
# concatenation (e.g. we close the single quoted string, open a double quoted string with a | |
# single quote in it, and then re-open the single quoted string leading to '"'"') | |
# * '/^[[:space:]]*$$/d': delete all lines that are nothing but whitespace. Not technically | |
# necessary, but it ensures that stray tab characters don't screw with us. | |
# * -e :a -e N -e '$$!ba': this based on a recipe from https://stackoverflow.com/a/1252191/38666 | |
# which effectively concatenates every line in the file so that we can run replacements based | |
# on start and end delimiters. Uses multiple `-e` commands to run on macOS. | |
# * 's/\([a-zA-Z0-9_]*\)="\[\([^]]*\)\]"/define \1\n[\2]\nendef/g' (and the other similar variants): | |
# These find multi-line JSON-like strings and replaces them with Makefile `define VAR_NAME ... endef` | |
# | |
# 2. `load-env`: this target includes the temporary file as a nested Makefile (effectively | |
# loading everything in it into local variables), parses the file with sed to grab | |
# the variable names, exports those as environment variables, and then cleans up the | |
# temporary file. | |
# | |
# Sed and awk commands explained: | |
# | |
# * '/^\(define [a-zA-Z0-9_]\|[a-zA-Z0-9_]*=\)/!d': this deletes all lines that don't start | |
# with either `define VARIABLE` or `VARIABLE=`. | |
# * 's/=.*//': this removes everything after an equals sign, leaving the variable names | |
# * 's/^define //': this strips out the `define ` keyword | |
# * awk 1 ORS=' ': sourced from https://stackoverflow.com/a/14853319/38666 this strips | |
# all line breaks from the output, leaving a space-delimited list of variable names | |
prep-tmp-env: guard-ENV cmd-exists-sed | |
@sed -e 's/\$$/$$$$/g' -e 's/="\(.*\)"$$/=\1/' -e 's/='"'"'\(.*\)'"'"'$$/=\1/' -e '/^[[:space:]]*$$/d' $(ENV).env \ | |
| sed -e :a -e N -e '$$!ba' \ | |
-e 's/\([a-zA-Z0-9_]*\)='"'"'\[\([^]]*\)\]'"'"'/define \1\n[\2]\nendef/g' \ | |
-e 's/\([a-zA-Z0-9_]*\)='"'"'{\([^}]*\)}'"'"'/define \1\n[\2]\nendef/g' \ | |
-e 's/\([a-zA-Z0-9_]*\)="\[\([^]]*\)\]"/define \1\n[\2]\nendef/g' \ | |
-e 's/\([a-zA-Z0-9_]*\)="{\([^}]*\)}"/define \1\n[\2]\nendef/g' \ | |
> $(ENV).env.tmp | |
load-env: prep-tmp-env guard-ENV cmd-exists-sed cmd-exists-awk cmd-exists-rm | |
@echo "Using env: $(ENV).env" | |
$(eval include $(ENV).env.tmp) | |
$(eval export $(shell sed -e '/^\(define [a-zA-Z0-9_]\|[a-zA-Z0-9_]*=\)/!d' -e 's/=.*//' -e 's/^define //' $(ENV).env.tmp | awk 1 ORS=' ')) | |
@rm $(ENV).env.tmp | |
test-env: load-env ## Verify those environment variables! Try `make test-env ENV=example` | |
@echo SINGLE_QUOTED="$$SINGLE_QUOTED" | |
@echo DOUBLE_QUOTED="$$DOUBLE_QUOTED" | |
@echo UNQUOTED="$$UNQUOTED" | |
@echo DOLLARS="$$DOLLARS" | |
@echo MULTILINE_SINGLE_QUOTE="$$MULTILINE_SINGLE_QUOTE" | |
@echo MULTILINE_DOUBLE_QUOTE="$$MULTILINE_DOUBLE_QUOTE" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment