在书写Makefile时,可能有多个规则会使用相同的一组命令。就像c语言程序中需要经常使用到函数“printf”。这时我们就会想能不能将这样一组命令进行类似c语言函数一样的封装,以后在我们需要用到的地方可以通过它的名字(c语言中的函数名)来对这一组命令进行引用。这样就可减少重复工作,提高了效率。在GNU make中,可以使用指示符“define”来完成这个功能。通过“define”来定义这样一组命令,同时用一个变量(作为一个变量,不能和Makefile中其它常规的变量命名出现冲突)来代表这一组命令。通常我们把使用“define”定义的一组命令称为一个命令包。定义一个命令包的语法以“define”开始,以“endef”结束,例如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
这里,“run-yacc”是这个命令包的名字。在“define”和“endef”之间的命令就是命令包的主体。需要说明的是:使用“define”定义的命令包中,命令体中变量和函数的引用不会展开。命令体中所有的内容包括“$”、“(”、“)”等都是变量“run-yacc”的定义。它和c语言中宏的使用方式一样。
例子中,命令包中第一个命令是对引用它所在规则中的第一个依赖文件(函数“firstword”,可参考 8.2 文本处理函数 一节)运行yacc程序。yacc程序总是生成一个命名为“y.tab.c”的文件。第二行的命令就是把这个文件名改为规则目标的名字。
定义了这样一个命令包后,后续应该如何使用它?前面已经提到,命令包是使用一个变量来表示的。因此我们就可以按使用变量的方式来使用它。当在规则的命令行中使用这个变量时,命令包所定义的命令体就会对它进行替代。由于使用“define”定义的变量属于递归展开式变量,因此,命令包中所有命令中对其它变量的引用,在规则被执行时会被完全展开。例如这样一个规则:
foo.c : foo.y
$(run-yacc)
此规则在执行时,我们来看一下命令包中的变量的替换过程:1. 命令包重中的“$^”会被“foo.y”替换;2. “$@”被“foo.c”替换。大家应该对“$<”和“$@”不陌生吧,如果陌生可以 参考 自动化变量 一小节。
当在一个规则中引用一个已定义的命令包时,命令包中的命令体会被原封不动的展开在引用它的地方(和 c语言中的宏一样)。这些命令就成为规则的命令。因此我们也可在定义命令包时使用前缀来控制单独的一个命令行(例如“@”,“-”和“+”)。例如:
define frobnicate
@echo "frobnicating target $@"
frob-step-1 $< -o $@-step-1
frob-step-2 $@-step-1 -o $@
endef
此命令包的第一行命令执行前不会被回显,其它的命令在执行前都被回显。
另一方面,如果一个规则在引用此命令包之前使用了命令的前缀字符。那么这个前缀字符将会被添加到命令包定义的所有命令行之中。例如:
frob.out: frob.in
@$(frobnicate)
这个规则执行时不会回显任何将要执行的命令。
有时可能存在这样的一个需求,需要定义一个什么也不做的规则(没有命令行)。前面我们已经看到过这样的用法。这样的规则,只有目标文件(可以存在依赖文件)而没有命令行。可以像这样定义:
target: ;
这就是一个空命令的规则,为目标“target”定义了一个空命令。也可以命令行的格式来定义空命令,需要注意的是命令行必须以[Tab]字符开始。一般在定义空命令时,建议不使用命令行的方式,因为看起来空命令行和空行在感觉上没有区别。
大家会奇怪为什么要定义一个没有命令的规则。其唯一的原因是,使用空命令行可以防止make在执行时图重建这个目标而查找隐含命令(包括了使用隐含规则中的命令和“.DEFAULT”指定的命令。关于隐含规则可参考 第10章 使用隐含规则)。这一点和伪目标有相同之处。在使用空命令的目标时,需要说明的是:实现一个没有实际文件的目标,这个目标只是作为一个标签,来完成它的依赖文件的重建动作。实现这个目的,首先应该想到伪目标而不是空命令目标。因为一个实际不存在的目标文件的依赖文件,可能不会被正确的重建。
所以,对于空命令规则,最好不要给它指定依赖文件。避免特殊情况下产生错误的情况。定义一个空命令规则,建议使用上例的格式。