如何在Makefile中从.env文件加载和导出变量?

34
什么是在Makefile中使用.env文件的最佳方法,即加载该文件并为子shell中的所有变量进行导出?
如果建议的解决方案仅适用于make,则会很好,例如不使用任何第三方工具。 此外,.env文件支持多行变量,如: FOO="这\n是\n一个\n多行\n变量" 这就是为什么此解决方案可能不够充分的原因。

include 起作用了。 - ArturFH
2
不完全是这样。它在大多数情况下可能会正确加载变量,但它不会自动将它们导出作为子 Shell 命令的环境对吧? - Bastian Venthur
如果你在谈论GNU make,可能有一些使用字符串函数和$(eval )来实现这个目标的非常复杂的方法,但这听起来不是一个好主意。如果你能说明通过导出这些变量想要实现什么目标,可能会有更直接的解决方案。 - user2371524
我们的想法是遵循 12 因素应用程序的理念,使用一个 .env 文件来存放所有构建/部署配置所需的环境变量。由于 make 是编排构建/部署的工具,如果我能够轻松地在不使用外部工具的情况下使用这个 .env 就好了。 - Bastian Venthur
5个回答

22

Make没有提供读取文件内容到变量的方法。因此,我认为不使用外部工具无法实现结果。但是,如果我错了,我很乐意学习一些新技巧。

所以,假设有两个文件,.env是一个技术上正确的shell文件:

FOO=bar

BAR="notfoo" # comment
   #comment
MULTILINE="This\nis\nSparta!"
# comment

script.sh

#!/bin/bash
echo FOO=${FOO}
echo BAR=${BAR}
echo -e ${MULTILINE}

一种解决方法是将.env文件包含进来,然后确保变量被导出:

include .env

$(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env))

all:
    ./script.sh

由于Shell和Make处理引号的方式不同,因此您会在输出中看到引号。

您可以通过重新对变量进行Make处理来避免这种情况:

include .env

VARS:=$(shell sed -ne 's/ *\#.*$$//; /./ s/=.*$$// p' .env )
$(foreach v,$(VARS),$(eval $(shell echo export $(v)="$($(v))")))

all:
    ./script.sh

但是多行变量会变成一行。

最后,您可以生成一个临时文件,在运行任何命令之前将其传递给Bash并进行处理:

SHELL=bash

all: .env-export
    . .env-export && ./script.sh

.env-export: .env
    sed -ne '/^export / {p;d}; /.*=/ s/^/export / p' .env > .env-export

哦,多行变量中的换行符出了问题。您需要额外引用它们。

最后,您可以使用上述sed命令向.env文件中添加export,然后执行:

SHELL=bash
%: .env-export
    . .env-export && make -f secondary "$@"

6
好的,我想你说服我使用外部工具了。感谢您详细的回答! - Bastian Venthur
在shell和make中处理引号的方式非常混乱。另一个解决方案似乎是在每个命令前加上. .env;,即. env; echo $FOO - Bastian Venthur
在你提供的例子中,$FOO 是由同一 shell 扩展的,该 shell 调用了 .env 文件。要运行 . .env; ./script.sh,变量需要已在 .env 文件中被导出,以便在 ./script.sh 中可见。 - ArturFH

20

发现这个方法非常有效:

在makefile的顶部

ifneq (,$(wildcard ./.env))
    include .env
    export
endif

那么您需要为所有的环境变量创建变量,例如将MY_VAR用作$(MY_VAR)。


3
这是有效的,但我不得不通过谷歌搜索来理解它的作用...所以对于其他人来说... ifneq + wildcard 是检查文件是否存在的典型方式;include .env.env 导入 Makefile 变量,而不带参数的 export 则导出所有变量。 - Peter McIntyre

5

您可以创建一个函数和目标来加载每个特定的.env文件,并在需要时与其他目标一起使用它。以下是示例:

define setup_env
    $(eval ENV_FILE := $(1).env)
    @echo " - setup env $(ENV_FILE)"
    $(eval include $(1).env)
    $(eval export)
endef

devEnv: 
    $(call setup_env, dev)

prodEnv: 
    $(call setup_env, prod)

clean:
    rm -rf  bin/

build: clean
    GOOS=linux GOARCH=amd64 go build -o bin/ ./cmd/...

dev: build devEnv
    cd cmd/api && ./../../bin/api
 
migrate-dev-db: devEnv
    sh +x database/migration/migrate.sh dev

migrate-prod-db: prodEnv
    sh +x database/migration/migrate.sh


deploy: prodEnv
    sh +x script/deployment/production/ec2-deploy.sh

为什么dev目标依赖于devEnv,然后立即调用$(call setup_env,dev)呢? - Mark Lakata
@MarkLakata 对不起,我的回答有误。我已经进行了编辑。 - princebillyGK

1

我曾经遇到过同样的问题,这是我解决它的方法。

  • 首先,建议检查是否存在.env文件,否则include将会引发异常。
  • 其次,可以将.env-example文件添加到您的仓库中。
    ENVFILE = .env
    ENVFILE_EXAMPLE = .env-example
    
    # Create the .env file if not exit.
    ifeq ("$(wildcard $(ENVFILE))","")
       $(shell cp $(ENVFILE_EXAMPLE) $(ENVFILE))
    endif
    # Load the environment variables from .env file
    include $(ENVFILE)
    export $(shell sed '/^\#/d; s/=.*//' $(ENVFILE)) 

顺便提一下:这种方法也可以处理注释,而这在Makefile语法中默认是不支持的。

Sanhoj - 谢谢


0
这是一个与编程有关的文本。返回翻译文本:

以下是 .env 和 makefile 的使用示例:

.env:

DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/my_db_development?sslmode=disable

makefile:

include .env

migrate:
    migrate -source file://postgres/migrations \
            -database $(DATABASE_URL) up

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接