Makefile变量初始化和导出

44
somevar := apple
export somevar
update := $(shell echo "v=$$somevar")

all:
    @echo $(update)

我希望命令的输出结果是apple,但是它是空的,这让我觉得export和:=变量扩展发生在不同的阶段。如何解决这个问题?

4个回答

45
问题在于export将变量导出到命令使用的子shell中,而不能在其他赋值中扩展。因此,不要尝试从规则外部的环境中获取它。
somevar := apple
export somevar

update1 := $(shell perl -e 'print "method 1 $$ENV{somevar}\n"')
# Make runs the shell command, the shell does not know somevar, so update1 is "method 1 ".

update2 := perl -e 'print "method 2 $$ENV{somevar}\n"'
# Now update2 is perl -e 'print "method 2 $$ENV{somevar}\n"'

# Lest we forget:
update3 := method 3 $(somevar)

all:
    echo $(update1)
    $(update2)
    echo $(update3)
    perl -e 'print "method 4 $$ENV{somevar}\n"'
输出结果为:
echo method 1 
method 1
perl -e 'print "method 2 $ENV{somevar}\n"'
method 2 apple
echo method 3 apple
method 3 apple
perl -e 'print "method 4 $ENV{somevar}\n"'
method 4 apple


1
实际上,这是一个未被修复且可能永远不会被修复的错误[http://savannah.gnu.org/bugs/?10593]。谢谢,我会尽量避免用奇怪的makefile问题打扰您。 - Pablo
@Michael:从我阅读的GNUMake手册来看,这不是一个bug。(你也不需要在我的回答前面加上@Beta的注释。) - Beta
这并不奇怪。我需要这个。为什么我需要这个?因为环境变量是将任意字符串传递给外部脚本的唯一防弹方式。例如,假设我有一个包含各种引号、重要的 shell 元字符(如美元符号、反斜杠转义等)的变量 V。我想让一个 shell 脚本使用变量 $V。 - Kaz
@Kaz:听起来很危险,但可行。上面的方法有一个能解决问题吗? - Beta
@Beta 过时的注释。我通过使用$(subst ..)将单引号转义为'\''来解决了这个问题。然后将结果在单引号之间插入到shell脚本中。 - Kaz

27

@Beta 的回答 包含了关键指针:在 GNU make 中,被标记为 export 的变量仅对 [作为规则一部分的] 配方命令(命令)可用,遗憾的是 不能用于 $(shell ...) 的调用(它们只能看到 make 启动时所看到的环境)。

但是有一个解决方法:将导出的变量显式作为环境变量传递给 shell 函数:

update := $(shell somevar='$(somevar)' perl -e 'print "$$ENV{somevar}"')

在shell命令之前加上<var>=<val>,这将把该定义作为环境变量添加到命令所看到的环境中-这是一个通用的shell功能。

注意:@Kaz在评论中指出,如果$(somevar)包含某些字符,则此方法会出现问题,因为变量扩展是文字直接复制(没有转义),这可能会破坏生成的shell命令,并建议使用以下变体来处理嵌入式'实例(将输入值拆分为带有引号'的单引号子字符串):

update := $(shell somevar='$(subst ','\'',$(somevar))' perl -e 'print "$$ENV{somevar}"')

除了多行值(这种情况比较少见,目前没有解决方法),此方法适用于所有值。

另外需要注意的是,在值中出现的文字 $ 必须表示为 $$,否则 make 会将其视为引用其自身变量。

请注意,我故意没有选择 OP 的原始语句 update := $(shell echo "v=$$somevar") 进行演示,因为它包含一个混淆问题:由于 shell 如何评估命令行,somevar=apple echo v=$somevar 不会计算为 v=apple,因为 $somevar 引用在 somevar=apple 生效之前被扩展。在这种情况下实现所需的效果,您必须使用两个语句:update := $(shell export somevar="$(somevar)"; echo "v=$$somevar")


至于错误与功能之争:

虽然可以认为 shell 函数应该看到与配方命令相同的环境,但文档没有做出这样的承诺 - 请参阅 http://www.gnu.org/software/make/manual/make.html#Shell-Function。相反,http://www.gnu.org/software/make/manual/make.html#Variables_002fRecursion 仅提到将导出的变量提供给配方命令。


这个解决方法并不是一个解决方法。通过环境传递材料已经是解决引用和转义问题的一种解决方法了。通过构建“VAR=val command…”语法,我们重新引入了这些问题。 - Kaz
@Kaz:感谢您指出这个问题-我已经更新了答案并加上了一个警告。通用解决方案并不容易,但如果可以接受一些限制(例如在分配约束值时,如$(PATH)),那么手头的方法仍然可能是一个选择。 - mklement0
最简单的解决方法是使用 $(subst ...)' 的出现替换为 '\''。然后,在 shell 端在单引号之间插入结果数据。 (我今天就是这样解决的。) - Kaz
@Kaz:这很有前途,因为它可以正确转义嵌入的 ' 字符,但无法解决文字 $ 字符的问题:make 会不可避免地将其自己扩展(作为对其自身变量的引用),在应用 $(subst …) 之前 - 请参见我的更新。 - mklement0
我认为这不是问题。如果我有 FOO := $(BAR),然后在某个地方将 $(FOO) 插入到 shell 脚本中,我希望 $(BAR) 被扩展。这个扩展是在分配 FOO 时完成的,因为我使用了一个立即变量 (:=)。如果 $(BAR) 产生一个美元符号,那么这个美元符号现在只是 FOO 中的数据,不会再引起任何问题。如果我们在 shell 脚本中用单引号括起来得到 $(FOO),那么这个美元符号也不会引起问题。在我们考虑与 shell 通信之前,I am $HOME 已经出现了问题。 - Kaz
@Kaz:谢谢,你说得对。我已经相应更新了答案。 - mklement0

14

运行makefile

foo:=apple
export foo
all:
        @echo ">"$(shell echo "$$foo")
        @echo ">""$$foo"

在foo未定义的情况下,对我来说会产生什么结果

$ make
>
>apple

$ make foo=bar
>
>apple

$ export foo=bar; make
>bar
>apple

$ export foo=bar; make foo=bar
>bar
>bar

尝试使用带引号的形式(update := "v=$$somevar"),并让shell在运行命令时处理扩展(你仍然需要导出变量)


我需要保持shell函数不干扰目标命令的方式,因为在实际情况下它具有条件语句。 - Pablo
2
+1表示显示变化;简而言之:(a) $(shell ...) 只能看到在 make 启动时已经存在于 make 自己环境中的变量;(b) 在 make 命令行上定义变量会覆盖 makefile 中同名的定义。 - mklement0
第一个表达式的输出是什么,不要在makefile之外做任何事情? - holms

1
尽管export不能与$(shell ...)良好配合,但有一种简单的解决方法。我们可以通过命令行将数据传递给shell脚本。
现在,当然,环境传递对于转义和引用的问题是强大的。然而,shell语言有一种单引号引用方法'...',它可以处理一切。唯一的问题是没有办法在其中插入单引号;但当然这可以通过终止引号、反斜杠转义所需的单引号并开始一个新引号来解决:换句话说:
ab'cd -> 'ab'\''cd'

在由$(shell ...)执行的shell脚本中,我们只是生成一个形式为var='$(...)' 的变量赋值,其中$(...)是一些适当转义的make表达式插值。因此,Makefile:
somevar := apple with 'quoted' "stuff" and dollar $$signs

shell_escape = $(subst ','\'',$(1))

update := $(shell v='$(call shell_escape,$(somevar))'; echo $$v > file.txt)

.phony: all

all:
    cat file.txt

样例运行:

$ make
cat file.txt
apple with 'quoted' "stuff" and dollar $signs

如果我们想要将环境变量传递给一个命令,我们可以使用shell语法VAR0=val0 VAR1=val1 ... VARn=valn command arg ...。这可以通过对上述Makefile进行一些小的修改来说明:
somevar := apple with 'quoted' "stuff" and dollar $$signs

shell_escape = $(subst ','\'',$(1))

update := $(shell somevar='$(call shell_escape,$(somevar))' env > file.txt)

.phony: all

all:
        grep somevar file.txt

运行:

$ make
grep somevar file.txt
somevar=apple with 'quoted' "stuff" and dollar $signs

file.txt 包含环境变量的转储,其中我们可以看到somevar。 如果 GNU Make 中的 export 做得正确,那么我们就能够轻松地执行以下操作:

export somevar
update := $(shell env > file.txt)

但最终结果是相同的。
由于您想要的最终结果是 echo $(update),即使GNU Make将导出的变量传递给 $(shell ...),您仍需要进行 shell_escape。也就是说,请再看一个 Makefile
somevar := apple with 'quoted' "stuff" and dollar $$signs

shell_escape = $(subst ','\'',$(1))

update := $(shell v='$(call shell_escape,$(somevar))'; echo $$v)

.phony: all

all:
    @echo '$(call shell_escape,$(update))'
    @echo $(update)

输出:

apple with 'quoted' "stuff" and dollar $signs
apple with quoted stuff and dollar

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