测试GNU make的eval函数

6
GNU make手册中提到,eval函数会展开参数,然后将展开的结果输入给make解析器。以下是摘自GNU make手册的内容。
The argument to the eval function is expanded, then the results of that expansion are parsed as makefile syntax. 

我不太理解make解析器如何处理eval提供的文本,因此我编写了以下makefile进行测试。

define myprint
    echo "this is a line"
endef

goal:
    $(eval $(call myprint))
    gcc -o goal test.c

我知道正确调用myprint应该仅使用调用函数:$(call myprint)并删除echo之前的“Tab”字符。我以这种形式编写makefile只是为了测试eval函数。
我的期望是:首先,eval函数会扩展myprint,即一个以“Tab”开头的echo命令,并且“Tab”用于使扩展文本成为合法的配方。然后,eval将扩展的文本提供给maker解析器,后者将识别该文本为配方并运行它。由于该命令是合法的,因此makefile应该正常运行。
然而,我遇到了这样一个错误:
Makefile:6: *** recipe commences before first target.  Stop.

有人能解释一下为什么make会出现这样的错误吗?
3个回答

10

扩展的结果被解析为makefile语法

eval 的用法不同:您希望它被解析为shell语法。您可以这样写:

define myprint
echo "this is a line"
endef

goal:
    $(myprint)
    gcc -o goal test.c
或者:
define myprint
echo "this is a $(1)"
endef

goal:
    $(call myprint,line)
    gcc -o goal test.c

因为扩展后的配方是有效的 shell 语法。但不是你写的那样,因为 eval 的扩展仍然被解释为 make 语法,而不是 shell 语法。为了说明 evalcall 的典型用法,请考虑以下示例:

define myprint
echo "this is a $(1)"
endef

define mygoal
$(1):
    $$(call myprint,line)
    gcc -o $(1) $(2).c
endef
$(eval $(call mygoal,goal,test))

这个例子比前两个例子(没有 eval)要复杂一些,但它展示了 eval 的真正用途:通过编程方式实例化make构造。以下是它的工作原理,一步一步来:

在其二阶段算法的第一阶段中,make会扩展 $(eval... 函数调用,即:

  1. 扩展 $(call... 函数的参数(即goaltest)。在我们这个例子中没有影响。
  2. 将结果赋值给临时变量 $(1)$(2)
  3. 在这个上下文中扩展 mygoal 变量,它将替换 $(1)$(2)$$(call...,分别由 goaltest$(call... 替换。
  • 将结果实例化为一个 make 构造,在本例中是完整的规则:

    goal:
        $(call myprint,line)
        gcc -o goal test.c
    
  • 第一阶段仍在继续,但对该实例化规则没有影响,因为配方将在第二个阶段由make扩展。

    在第二阶段中,当构建goal目标时,make会在执行之前扩展此配方,即:

    1. 扩展$(call myprint...参数(line无效)。
    2. 将结果分配给临时参数$(1)
    3. 在此上下文中扩展变量myprint,生成:

    echo "this is a line"
    

    所有这些都等同于我们写下规则:

    goal:
        echo "this is a line"
        gcc -o goal test.c
    

    请注意在mygoal的初始定义中双重使用$$

    需要意识到,eval参数被扩展了两次;首先由eval函数扩展,然后再解析为makefile语法时再次扩展结果。这意味着在使用eval时可能需要提供额外级别的转义以转义“$”字符。


    感谢您详细的回复和示例。我认为我明白了让我困惑的关键点:在第一次扩展参数后,结果将被发送到解析器作为常规的make语法。我认为在echo命令之前加上“Tab”不是正常的语法。这就是为什么我的makefile无法正常运行的原因。我的理解正确吗? - shijun zhao
    不,不完全是这样。问题不在于制表符。你可以自己试一下,删除这个前导制表符。你仍然会得到一个错误,因为最后,$(eval $(call myprint))扩展为make语句,而它应该是纯shell。这与如果您将CFLAGS:= -g放在配方中的问题相同:在期望shell的地方使用了make语法。 - Renaud Pacalet
    抱歉,我不知道如何比Florian已经写的更好地解释这个问题。也许可以再读一遍文档中的这部分并深思熟虑:eval函数的参数被扩展,然后将扩展的结果解析为makefile语法。扩展的结果可以定义新的make变量、目标、隐式或显式规则等。 可以强调一下 作为makefile语法 - Renaud Pacalet
    抱歉我误删了内容。我不太熟悉如何在注释中编写代码。以下是我的注释。 您的意思是eval的源应该是语法制作吗?在您的建议后,我尝试了以下makefile,并且它可以工作。 [define mydef a = test.c endefgoal: $(eval $(call mydef)) echo $(a) gcc –o goal test.c] - shijun zhao
    非常感谢您的详细解释 :) - shijun zhao
    显示剩余3条评论

    4

    $(eval …) 需要一个完整的语法 makefile 片段。它不能用于将标记粘贴到其他 makefile 结构中。也许手册没有清楚地解释这一点,但它是通过将其参数读取为一个包含的 makefile 来实现的。


    谢谢,你的回答非常清晰。在echo命令前加上“Tab”是不完整的语法。 - shijun zhao

    0

    @RenaudPacalet,我编写了以下Makefile来测试调用“eats”是否需要一个美元的扩展。

    define myprint
    echo "In my print $$(ls)"
    endef
    
    goal:
        $(call myprint)
        $(info $(call myprint))
        gcc -o goal test.c
    

    它的输出为:

    echo "In my print $(ls)"
    echo "In my print $(ls)"
    In my print call.mk ... (files list)
    

    由于$(call myprint)正确输出了"In my print $(ls)",因此它必须首先扩展为echo "In my print $$(ls)",然后才能扩展为正确的shell命令echo "In my print $(ls)"。所以我认为调用函数并不会吞掉一个美元符。

    另一个证据是info函数的输出。GNU make手册中写道:

    $(info text…)
    This function does nothing more than print its (expanded) argument(s) to standard output. 
    

    从手册中我们可以推断出make将扩展info函数的参数。由于info的输出是echo "In my print $(ls)",因此在扩展之前的参数应该是echo "In my print $$(ls)"。因此我们可以得出结论,call函数不会“吃掉”一个美元符号。

    你显然不理解的是,扩展是递归的。当make运行“goal”配方时,它会逐行扩展它。在扩展“$(info $(call myprint))”时,它首先扩展“$(info”的参数,即“$(call myprint)”。在扩展“$(call myprint)”时,它首先扩展“myprint”(没有效果),然后扩展“myprint”的值并吃掉一个“$”。结果“echo“ In my print $(ls)””是“$(info ...”的最终扩展参数,因此当make评估它时,“$(info ...”打印的内容。注意:您不必将“info”放在配方中。 - Renaud Pacalet
    只需在您的Makefile中平铺放置$(info $(call myprint)),您将看到$(call myprint)扩展的结果,其中一个$被吃掉了。当使用$(eval $(call myprint))时,info在标准输出上输出的正是传递给$(eval...的内容。唯一的区别是$(info...只是将其发送到标准输出,而$(eval...则评估它并因此将其实例化为make构造。这是一个有趣的讨论。 - Renaud Pacalet
    如果$(eval ...的扩展参数是"In my print $(ls)"(有一个美元符号),我认为make不会正常运行,因为make会将$(ls)扩展为空,这个命令无法正确运行。我认为在扩展到$(eval... or $(info...之后的参数应该有两个美元符号。 - shijun zhao
    @RenaudPacalet 最终我明白了我的扩展误解的地方:我误解了$call有一个返回值,这个返回值会被$eval再次扩展。实际上并没有这样不必要的步骤。再次感谢您详细的解释。 - shijun zhao

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