使用函数(function)可以在 makefile 中进行文本处理。借此可以通过计算求得要处理的文件,或者组装出在命令 (recipe) 中使用的命令。函数以函数调用的形式使用。给出函数的名字,以及要让该函数处理的文本(这称为参数),函数就会进行处理,并把结果嵌入到调用所在的位置。这与变量被展开并替换的机制完全相同。
函数调用的形式与变量引用十分相似。凡是能写变量引用的地方都可以写函数调用,而且展开规则也与变量引用相同。函数调用具有如下形式:
$(function arguments)
或者也可以写成下面这样:
${function arguments}
这里 function 是函数名,它是 make 内置的少数几个函数名之一。此外,使用内置函数 call,实际上还可以创建你自己专用的函数。
arguments 是该函数的参数。参数与函数名之间用一个或多个空格或制表符隔开,当参数有多个时,参数之间用逗号分隔。这些用作分隔的空白和逗号不会成为参数值的一部分。包围函数调用的分隔符可以是圆括号也可以是花括号,但如果在参数中写到同一种括号,它们必须成对出现;而另一种分隔符即使不成对,也可以单独出现。如果参数中又包含别的函数调用或变量引用,明智的做法是对所有的引用都使用同一种分隔符。也就是说应当写成「$(subst a,b,$(x))」,而不应像「$(subst a,b,${x})」那样混用。这样既看着更清楚,也因为在查找引用的结尾时只需匹配一种分隔符。
每个参数都会在函数被调用之前各自展开(以下另有特别说明的情况除外)。展开按照参数排列的顺序进行。
当想把对 make 具有特殊含义的字符用作函数参数时,有时需要把它们隐藏起来。GNU make 不支持用反斜杠或其他转义序列来转义字符。不过,由于参数是在展开之前就被切分的,所以可以通过把特殊字符放进变量里来隐藏它们。
可能需要隐藏的字符包括以下这些:
例如,可以定义值分别只是一个逗号、一个空格的变量 comma 和 space,然后在需要这些字符的地方嵌入这些变量。写法如下:
comma:= , empty:= space:= $(empty) $(empty) foo:= a b c bar:= $(subst $(space),$(comma),$(foo)) # bar 的值由此变为 'a,b,c'。
这里 subst 函数把 foo 的值中的每个空格替换为逗号,并把结果嵌入进来。
这里介绍若干操作字符串的函数:
$(subst from,to,text)对文本 text 进行字符串替换。即每当出现 from,就把它替换为 to。其结果会嵌入到函数调用的位置。例如,
$(subst ee,EE,feet on the street)
会产生「fEEt on the strEEt」这个值。
$(patsubst pattern,replacement,text)在 text 中查找以空白分隔的、与 pattern 匹配的单词,并把它们替换为 replacement。这里 pattern 中可以包含「%」,它充当通配符,匹配一个单词中任意数量的任意字符。如果 replacement 中也包含「%」,那么这个「%」会被替换为 pattern 中「%」所匹配到的那部分文本。与模式不匹配的单词会原封不动地保留在输出中。只有 pattern 和 replacement 中的第一个「%」会被这样特殊处理,第二个及之后的「%」则按原样对待。
在 patsubst 函数调用中使用的「%」字符,可以在其前面加反斜杠(「\」)来引用(quote)。而具有引用「%」作用的反斜杠本身,可以再叠加一个反斜杠来引用。用于引用「%」字符或其他反斜杠的那些反斜杠,会在模式与文件名进行匹配、或被代入词干 (stem) 之前从模式中去除。而那些不会有引用「%」字符之虞的反斜杠,则原样保留。例如,在模式 the\%weird\\%pattern\\ 中,生效的「%」字符前面是「the%weird\」,后面跟着「pattern\\」。末尾的两个反斜杠由于无法影响任何「%」字符,因此原样保留。
单词与单词之间的空白会被合并成一个空格字符。此外,开头和末尾的空白会被丢弃。
例如,
$(patsubst %.c,%.o,x.c.c bar.c)
会产生「x.c.o bar.o」这个值。
使用替换引用(请参阅替换引用一节)可以更简单地获得与 patsubst 函数相同的效果:
$(var:pattern=replacement)
与
$(patsubst pattern,replacement,$(var))
等价。第二种简写形式简化了 patsubst 最常见的用法之一——替换文件名末尾的后缀这一操作。
$(var:suffix=replacement)
与
$(patsubst %suffix,%replacement,$(var))
等价。例如,假设有一份目标文件(object)清单:
objects = foo.o bar.o baz.o
如果想得到与之对应的源文件清单,与其使用通用形式的,
$(patsubst %.o,%.c,$(objects))
不如直接写成:
$(objects:.o=.c)
$(strip string)去除 string 开头和末尾的空白,并把内部一个或多个连续的空白字符的每一段替换成一个空格。因此「$(strip a b c )」的结果是「a b c」。
strip 函数与条件分支配合使用时会非常方便。当使用 ifeq 或 ifneq 把某物与空字符串「」比较时,常常会希望让只由空白组成的字符串与空字符串相匹配(请参阅 Makefile 的条件分支)。
因此,下面的例子可能不会得到你想要的结果:
.PHONY: all ifneq "$(needs_made)" "" all: $(needs_made) else all:;@echo 'Nothing to make!' endif
如果把 ifneq 指令 (directive) 中的变量引用「$(needs_made)」换成函数调用「$(strip $(needs_made))」,就会更健壮。
$(findstring find,in)查找 in 中是否出现 find。如果出现,值就是 find。如果不出现,值就为空。这个函数可用于在条件分支中检查某个字符串中是否包含特定的子串。因此,下面这两个例子,
$(findstring a,a b c) $(findstring a,b c)
分别产生「a」和「」(空字符串)这两个值。关于 findstring 的实际应用,请参阅检查标志的条件分支一节。
$(filter pattern…,text)
返回 text 中以空白分隔的单词里,与作为 pattern 给出的任一单词匹配的全部单词,并去除不匹配的单词。模式用「%」来书写,与前面 patsubst 函数中使用的写法相同。
filter 函数可用于把变量中不同种类的字符串(例如文件名)分拣出来。例如:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
这个例子表示 foo 依赖于 foo.c、bar.c、baz.s、ugh.h,但应当传给编译器的命令的只有 foo.c、bar.c、baz.s。
$(filter-out pattern…,text)
返回 text 中以空白分隔的单词里,与作为 pattern 给出的任一单词都不匹配的全部单词,并去除与一个或多个匹配的单词。这恰好是 filter 函数的相反操作。
例如,当给出下列内容时:
objects=main1.o foo.o main2.o bar.o mains=main1.o main2.o
下面的表达式会生成一份只由不包含在「mains」中的目标文件构成的清单:
$(filter-out $(mains),$(objects))
$(sort list)按字典序对 list 中的单词排序,并去除重复的单词。输出是一份以单个空格分隔单词的清单。因此,
$(sort foo bar lose)
返回「bar foo lose」这个值。
顺便一提,由于 sort 会去除重复的单词,所以即便排序顺序无所谓,也可以仅出于去重的目的而使用它。
$(word n,text)返回 text 的第 n 个单词。n 的有效取值从 1 开始。如果 n 大于 text 中的单词数,值就为空。例如,
$(word 2, foo bar baz)
返回「bar」。
$(wordlist s,e,text)返回 text 中从第 s 个单词到第 e 个单词(含两端)的单词清单。s 的有效取值从 1 开始,e 可以从 0 开始。如果 s 大于 text 中的单词数,值就为空。如果 e 大于 text 中的单词数,则返回直到 text 末尾的单词。如果 s 大于 e,则什么也不返回。例如,
$(wordlist 2, 3, foo bar baz)
返回「bar baz」。
$(words text)
返回 text 中的单词数。因此,text 的最后一个单词可由 $(word $(words text),text) 得到。
$(firstword names…)参数 names 被视为以空白分隔的一串名字。值就是这串名字中的第一个。其余的名字被忽略。
例如,
$(firstword foo bar)
会产生「foo」这个结果。$(firstword text) 与 $(word 1,text) 相同,但为了简洁起见,仍保留了 firstword 函数。
$(lastword names…)参数 names 被视为以空白分隔的一串名字。值就是这串名字中的最后一个。
例如,
$(lastword foo bar)
会产生「bar」这个结果。$(lastword text) 与 $(word $(words text),text) 相同,但为了简洁以及更好的性能,添加了 lastword 函数。
这里给出 subst 和 patsubst 的一个实际使用示例。假设某个 makefile 使用 VPATH 变量来指定一份目录清单,让 make 在其中查找前置条件 (prerequisite) 文件(请参阅针对所有前置条件的 VPATH 搜索路径)。这个例子展示了如何指示 C 编译器在同一份目录清单中查找头文件。
VPATH 的值是一份以冒号分隔的目录清单,例如形如「src:../headers」。首先使用 subst 函数,把冒号换成空格:
$(subst :, ,$(VPATH))
这会产生「src ../headers」。接着使用 patsubst,把各个目录名变成「-I」标志。这样得到的结果可以追加到会自动传给 C 编译器的变量 CFLAGS 的值上,如下所示:
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
其效果是把文本「-Isrc -I../headers」追加到 CFLAGS 原本被赋予的值上。这里使用 override 指令,是为了即便 CFLAGS 之前的值是用命令行参数指定的,也能确保新值被切实地赋入(请参阅 override 指令)。
在内置的展开函数中,有几个尤其与拆解文件名或文件名清单的处理有关。
下面的各个函数会对文件名进行特定的转换。函数的参数被视为以空白分隔的一串文件名(开头和末尾的空白被忽略)。清单中的每个文件名都以相同的方式转换,其结果以单个空格连接起来。
$(dir names…)从 names 中的每个文件名取出目录部分。文件名的目录部分,是指其中直到最后一个斜杠为止(包含该斜杠本身)的全部内容。如果文件名中不含斜杠,目录部分就是字符串「./」。例如,
$(dir src/foo.c hacks)
会产生「src/ ./」这个结果。
$(notdir names…)从 names 中的每个文件名取出除目录部分之外的剩余内容。如果文件名中不含斜杠,则原样保留不变。如果含有斜杠,则把直到最后一个斜杠为止的全部内容去除。
以斜杠结尾的文件名会变成空字符串。这是个遗憾之处,它意味着结果中以空白分隔的文件名个数,不一定与参数的个数一致。但我们也找不出其他妥当的选择。
例如,
$(notdir src/foo.c hacks)
会产生「foo.c hacks」这个结果。
$(suffix names…)从 names 中的每个文件名取出后缀。如果文件名中含有句点,后缀就是从最后一个句点开始的全部内容。如果不含,后缀就是空字符串。因此,即便 names 非空,结果也常常会为空;而且即使 names 中含有多个文件名,结果中所含的文件名个数也可能更少。
例如,
$(suffix src/foo.c src-1.0/bar.c hacks)
会产生「.c .c」这个结果。
$(basename names…)从 names 中的每个文件名取出除后缀之外的剩余内容。如果文件名中含有句点,basename 就是直到最后一个句点之前(不含该句点本身)的全部内容。目录部分中的句点被忽略。如果一个句点也没有,basename 就是整个文件名。例如,
$(basename src/foo.c src-1.0/bar hacks)
会产生「src/foo src-1.0/bar hacks」这个结果。
$(addsuffix suffix,names…)参数 names 被视为以空白分隔的一串名字。suffix 作为一个整体来对待。suffix 的值会被追加到每个名字的末尾,这样变长后的名字之间以单个空格连接起来。例如,
$(addsuffix .c,foo bar)
会产生「foo.c bar.c」这个结果。
$(addprefix prefix,names…)参数 names 被视为以空白分隔的一串名字。prefix 作为一个整体来对待。prefix 的值会被加到每个名字的开头,这样变长后的名字之间以单个空格连接起来。例如,
$(addprefix src/,foo bar)
会产生「src/foo src/bar」这个结果。
$(join list1,list2)逐个单词地连接两个参数。即,把最初的两个单词(分别取自各参数)连接起来作为结果的第一个单词,第二对单词作为结果的第二个单词,以此类推。也就是说,结果的第 n 个单词由各参数的第 n 个单词构成。如果一方参数的单词数较多,多出来的单词会原样复制到结果中。
例如,「$(join a b,.c .o)」会产生「a.c b.o」。
清单中单词之间的空白不会被保留,而是被替换为单个空格。
这个函数可用于把 dir 函数与 notdir 函数的结果合并起来,从而还原出当初给这两个函数的原始文件清单。
$(wildcard pattern)
参数 pattern 是文件名模式,通常(与 shell 的文件名模式一样)含有通配符字符。wildcard 的结果是一份与该模式匹配的现存文件的名字清单,以空格分隔。请参阅在文件名中使用通配符字符一节。
$(realpath names…)
对 names 中的每个文件名,返回其规范化的绝对名。规范化的名字中既不含 . 或 .. 这样的组成部分,也不含重复的路径分隔符(/),也不含符号链接。失败时返回空字符串。关于可能导致失败的原因清单,请参阅 realpath(3) 的文档。
$(abspath names…)
对 names 中的每个文件名,返回不含 . 或 .. 这样的组成部分、也不含重复的路径分隔符(/)的绝对名。请注意,与 realpath 函数不同,abspath 既不解析符号链接,也不要求文件名指向一个现存的文件或目录。如果想检查是否存在,请使用 wildcard 函数。
有 4 个进行条件展开的函数。这些函数的一个重要特征是,并非所有参数从一开始就被展开。只有需要展开的参数才会被展开。
$(if condition,then-part[,else-part])
if 函数(与 ifeq 之类的 GNU make makefile 条件分支(请参阅条件分支的语法)相对照)支持以函数形式进行条件展开。
第一个参数 condition,先去除其开头和末尾的全部空白,然后再展开。如果展开结果是非空字符串,条件就被视为真。如果展开为空字符串,条件就被视为假。
如果条件为真,就对第二个参数 then-part 求值,并把它用作整个 if 函数的求值结果。
如果条件为假,就对第三个参数 else-part 求值,它就成为 if 函数的结果。如果没有第三个参数,if 函数就什么也不产生(成为空字符串)。
请注意,then-part 和 else-part 中只有一方会被求值,绝不会两者都被求值。因此,任意一方都可以包含副作用(例如调用 shell 函数等)。
$(or condition1[,condition2[,condition3…]])
or 函数提供「短路(short-circuiting)」的 OR 运算。各参数按顺序展开。当某个参数展开为非空字符串时处理即停止,其展开结果成为返回值。如果把所有参数都展开完毕,而它们全都为假(空),那么展开结果就是空字符串。
$(and condition1[,condition2[,condition3…]])
and 函数提供「短路(short-circuiting)」的 AND 运算。各参数按顺序展开。当某个参数展开为空字符串时处理即停止,展开结果为空字符串。如果所有参数都展开为非空字符串,那么展开结果就是把最后一个参数展开后的结果。
$(intcmp lhs,rhs[,lt-part[,eq-part[,gt-part]]])
intcmp 函数支持整数之间的数值比较。该函数在 GNU make 的 makefile 条件分支中没有对应物。
左边 lhs 和右边 rhs 会被展开,并解析为十进制整数。其余参数的展开,由数值上的左边与数值上的右边如何比较来控制。
如果没有更多的参数,那么当左边与右边不相等时展开为空,相等时展开为它们的数值。
否则,如果左边严格小于右边,intcmp 函数求值为展开第三个参数 lt-part 后的结果。如果两边相等,intcmp 函数求值为展开第四个参数 eq-part 后的结果。如果左边严格大于右边,intcmp 函数求值为展开第五个参数 gt-part 后的结果。
如果省略了 gt-part,则其默认值为 eq-part。如果省略了 eq-part,则其默认值为空字符串。因此,「$(intcmp 9,7,hello)」和「$(intcmp 9,7,hello,world,)」都求值为空字符串,而「$(intcmp 9,7,hello,world)」(请注意 world 后面没有逗号)求值为「world」。
let 函数提供了限定变量作用域的手段。在 let 表达式中对所命名变量的赋值,只在该 let 表达式所提供的文本中有效,这个赋值不会影响外层作用域中同名的变量。
此外,let 函数还能通过把所有未分配的值都分配给最后一个名字的变量,来对列表进行「解包(取出)」。
let 函数的语法如下:
$(let var [var ...],[list],text)
最初的两个参数 var 和 list 会先于其他一切被展开。请注意,这里最后一个参数 text 并不会同时被展开。接着,展开后的 list 值的每个单词会依次绑定到各个变量名 var,而最后一个变量名则绑定到展开后 list 的全部剩余部分。换句话说,list 的第一个单词绑定到第一个变量 var,第二个单词绑定到第二个变量 var,以此类推。
如果 var 的变量名个数多于 list 的单词数,多出来的 var 变量名会被设为空字符串。如果 var 的个数少于 list 的单词数,则最后一个 var 会被设为 list 的全部剩余单词。
var 中的各个变量,在 let 执行期间作为简单展开变量被赋值。请参阅 变量的两种种类。
在所有变量都这样绑定之后,text 会被展开,它就成为 let 函数的结果。
例如,下面这个宏会把作为第一个参数给出的列表中单词的顺序反转:
reverse = $(let first rest,$1,\
$(if $(rest),$(call reverse,$(rest)) )$(first))
all: ; @echo $(call reverse,d c b a)
这会显示 a b c d。第一次被调用时,let 把 $1 展开为 d c b a。然后把 d 赋给 first,把 c b a 赋给 rest。接着展开 if 语句,这里 $(rest) 非空,于是用此刻已成为 c b a 的 rest 的值递归调用 reverse 函数。被递归调用的 let 把 c 赋给 first,把 b a 赋给 rest。这个递归一直持续到 let 仅以单独一个值 a 被调用为止。此时 first 是 a,rest 为空,于是不再递归,只是把 $(first) 展开为 a 后返回,再在其后加上 b,如此继续下去。
在 reverse 的调用完成之后,first 和 rest 变量就不再被设置了。即便事先存在这些名字的变量,它们也不会因 reverse 宏的展开而受到影响。
foreach 函数与 let 函数相似,但与其他函数大不相同。它把一段文本反复使用,每次都对该文本施加不同的替换。foreach 函数与 shell sh 的 for 命令、以及 C shell csh 的 foreach 命令十分相似。
foreach 函数的语法如下:
$(foreach var,list,text)
最初的两个参数 var 和 list 会先于其他一切被展开。请注意,这里最后一个参数 text 并不会同时被展开。然后,对于展开后的 list 值的每个单词,把该单词设给以展开后的 var 值所指名字的变量,并展开 text。text 大概会含有对该变量的引用,所以每次的展开结果都会不同。
其结果是,text 会被展开 list 中以空白分隔的单词的个数那么多次。把 text 的多次展开结果以空格相隔连接起来,就成为 foreach 的结果。
下面这个简单的例子,把列表「dirs」中每个目录里所有文件的清单,设给变量「files」:
dirs := a b c d files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))
这里 text 是「$(wildcard $(dir)/*)」。第一次迭代会为 dir 找到值「a」,所以产生与「$(wildcard a/*)」相同的结果。第二次迭代产生「$(wildcard b/*)」的结果,第三次产生「$(wildcard c/*)」的结果。
这个例子(除了设置「dirs」这点之外)与下面这个例子得到相同的结果:
files := $(wildcard a/* b/* c/* d/*)
当 text 较复杂时,可以用一个额外的变量给它命名,从而改善可读性:
find_files = $(wildcard $(dir)/*) dirs := a b c d files := $(foreach dir,$(dirs),$(find_files))
这里我们就是这样使用变量 find_files 的。这里之所以用纯粹的「=」把它定义为递归展开变量,是为了让它的值中包含实际的函数调用,以便在 foreach 的控制下被再次展开。简单展开变量则不行,那样的话 wildcard 会在定义 find_files 的时刻仅被调用一次。
与 let 函数一样,foreach 函数不会对变量 var 产生持久的影响。foreach 函数调用之后 var 的值和种类,与调用之前相同。从 list 中取得的其他值,只在 foreach 执行期间临时有效。变量 var 在 foreach 执行期间是简单展开变量。如果在 foreach 函数调用之前 var 是未定义的,那么调用之后它仍然是未定义的。请参阅 变量的两种种类。
当使用会生成变量名的复杂变量表达式时,需要小心。因为许多奇怪的字符串都可能成为有效的变量名,而那大概并非你的本意。例如,
files := $(foreach Esta-escrito-en-espanol!,b c ch,$(find_files))
如果 find_files 的值引用了名为「Esta-escrito-en-espanol!」的变量(es un nombre bastante largo, no?〔这名字相当长,不是吗?〕),那么这或许会有用。但多半情况下这会是个错误。
使用 file 函数,可以从 makefile 向文件写入或从文件读取。写入支持两种模式。一种是「覆盖」,文本从文件开头写入,现有内容全部丢失。另一种是「追加」,文本写入文件末尾,现有内容得以保留。无论哪种情况,如果文件不存在,都会被创建。如果无法以写入方式打开文件,或者写入操作失败,都会成为致命错误。向文件写入时,file 函数展开为空字符串。
从文件读取时,file 函数展开为该文件内容的原样(不过若末尾有一个换行符,则会被去除)。如果尝试从不存在的文件读取,则展开为空字符串。
file 函数的语法如下:
$(file op filename[,text])
当 file 函数被求值时,先展开其所有参数,然后以 op 所描述的模式打开 filename 所指的文件。
运算符 op 可以是表示用新内容覆盖文件的 >、表示追加到文件当前内容的 >>、或表示读取文件内容的 < 之一。filename 指定要写入或读取的文件。运算符与文件名之间可以加入空白。
读取文件时给出 text 的值是一个错误。
向文件写入时,text 会被写入文件。如果 text 尚未以换行结尾,则会在最后写入一个换行(即使 text 是空字符串也会写入)。如果完全没有给出 text 参数,则什么也不写入。
例如,当构建系统的命令行长度有限制,且在命令 (recipe) 中执行的命令能从文件接收参数时,file 函数会很有用。许多命令都有一个惯例:前缀加 @ 的参数指向一个含有更多参数的文件。这种情况下,可以把命令写成这样:
program: $(OBJECTS)
$(file >$@.in,$^)
$(CMD) $(CMDFLAGS) @$@.in
@rm $@.in
如果命令要求把每个参数放在输入文件的不同行上,可以把命令写成这样:
program: $(OBJECTS)
$(file >$@.in) $(foreach O,$^,$(file >>$@.in,$O))
$(CMD) $(CMDFLAGS) @$@.in
@rm $@.in
call 函数在「可用于创建新的带参数函数」这一点上独一无二。可以把复杂的表达式写成变量的值,然后用 call 以不同的值来展开它。
call 函数的语法如下:
$(call variable,param,param,…)
当 make 展开这个函数时,会把各 param 分配给临时变量 $(1)、$(2) 等。变量 $(0) 中存放 variable。参数个数没有上限。也没有下限,不过不带参数地使用 call 没有意义。
然后 variable 会在这些临时赋值的语境下作为 make 变量被展开。因此,variable 的值中对 $(1) 的引用,会解析为该次 call 调用中的第一个 param。
请注意,variable 是该变量的名字,而非对该变量的引用。因此,书写它时通常不使用「$」或括号(不过,如果不希望名字是常量,也可以在名字中使用变量引用)。
如果 variable 是某个内置函数的名字,那么即便存在同名的 make 变量,也始终会调用内置函数。
call 函数在把 param 参数分配给临时变量之前会先展开它们。因此,如果 variable 的值中含有对 foreach 或 if 这类具有特殊展开规则的内置函数的引用,可能不会如你所期望地工作。
举几个例子会更清楚。
下面这个宏只是把参数反序:
reverse = $(2) $(1) foo = $(call reverse,a,b)
这里 foo 中会存有「b a」。
这一个稍微有趣些。它定义了一个在 PATH 中查找某个程序最先出现位置的宏:
pathsearch = $(firstword $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH))))) LS := $(call pathsearch,ls)
这样变量 LS 中就会存有 /bin/ls 或类似之物。
call 函数可以嵌套。每次递归调用都拥有自己专属的、会遮蔽更上层 call 的值的局部 $(1) 等。例如,下面是 map 函数的一种实现:
map = $(foreach a,$(2),$(call $(1),$(a)))
这样,就可以把通常只取一个参数的函数(例如 origin),一次性 map 到多个值上:
o = $(call map,origin,o map MAKE)
其结果是,o 中会存有类似「file file default」之物。
最后提个醒。给 call 的参数添加空白时要小心。与其他函数一样,第二个及之后的参数中所含的空白会被保留,这可能引起奇怪的结果。给 call 传递参数时,一般来说去除所有多余的空白最为稳妥。
value 函数提供了一种不展开变量值即可使用它的手段。不过请注意,它并不会撤销已经发生的展开。例如,如果创建了简单展开变量,其值在定义时就已经被展开,这种情况下 value 函数会返回与直接使用该变量相同的结果。
value 函数的语法如下:
$(value variable)
请注意,variable 是该变量的名字,而非对该变量的引用。因此,书写它时通常不使用「$」或括号(不过,如果不希望名字是常量,也可以在名字中使用变量引用)。
这个函数的结果是一个字符串,它原样包含 variable 的值,不进行任何展开。例如,在下面这个 makefile 中:
FOO = $PATH
all:
@echo $(FOO)
@echo $(value FOO)
第一行的输出会是 ATH。这是因为「$P」被作为 make 变量展开了。而第二行的输出,则是你的环境变量 $PATH 的当前值。这是因为 value 函数避免了展开。
value 函数最常与 eval 函数(请参阅 eval 函数)配合使用。
eval 函数非常特别。使用它可以定义并非常量的、新的 makefile 语法要素——即作为对其他变量或函数求值的结果而产生的要素。eval 函数的参数会先被展开,其展开结果再作为 makefile 语法被解析。展开结果可以定义新的 make 变量、目标、隐含规则或显式规则等。
eval 函数的结果总是空字符串。因此,无论把它放在 makefile 的几乎任何位置,都不会引起语法错误。
这里要紧的是理解到:eval 的参数会被展开两次。第一次由 eval 函数进行,而第二次则是在其展开结果作为 makefile 语法被解析时,再次被展开。也就是说,在使用 eval 时,有时会需要为「$」字符增加额外的转义层级。在这类情况下,value 函数(请参阅 value 函数)对避免不想要的展开可能会有帮助。
这里举一个 eval 可以如何使用的例子。这个例子组合了好几个概念和其他函数。在这个例子中,不直接写出规则而使用 eval 也许看起来小题大做,但请考虑两件事。第一,模板定义(在 PROGRAM_template 之中)也许需要比这里所示的复杂得多。第二,可以把这个例子中复杂的「通用」部分放进另一个 makefile,并在各个具体的 makefile 中将其 include 进来。这样一来,各个具体的 makefile 就会变得非常清爽。
PROGRAMS = server client
server_OBJS = server.o server_priv.o server_access.o
server_LIBS = priv protocol
client_OBJS = client.o client_api.o client_mem.o
client_LIBS = protocol
# 从这里往后全都是通用的部分
.PHONY: all
all: $(PROGRAMS)
define PROGRAM_template =
$(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)
ALL_OBJS += $$($(1)_OBJS)
endef
$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))
$(PROGRAMS):
$(LINK.o) $^ $(LDLIBS) -o $@
clean:
rm -f $(ALL_OBJS) $(PROGRAMS)
origin 函数与其他大多数函数不同,它不操作变量的值。它告诉你的是关于变量本身的信息。具体来说,它告诉你该变量来自何处。
origin 函数的语法如下:
$(origin variable)
请注意,variable 是所查询对象变量的名字,而非对该变量的引用。因此,书写它时通常不使用「$」或括号(不过,如果不希望名字是常量,也可以在名字中使用变量引用)。
这个函数的结果是一个字符串,它告诉你变量 variable 是如何被定义的:
variable 从未被定义的情况。
variable 具有像 CC 那样的默认定义的情况。请参阅隐含规则中使用的变量。需要注意的是,如果重新定义了默认变量,origin 函数会返回后一个定义的出处。
variable 是从给 make 的环境继承而来的情况。
variable 是从给 make 的环境继承而来,且作为「-e」选项(请参阅选项一览)的结果而覆盖了 makefile 中对 variable 的设置的情况。
variable 是在 makefile 中被定义的情况。
variable 是在命令行上被定义的情况。
variable 是用 makefile 中的 override 指令定义的情况(请参阅 override 指令)。
variable 是为执行各规则的命令 (recipe) 而定义的自动变量的情况(请参阅自动变量)。
这个信息(除了单纯的好奇心之外)主要有助于判断某个变量的值是否值得信赖。例如,假设有一个会 include 另一个 makefile bar 的 makefile foo。当执行「make -f bar」这条命令时,即便环境中含有 bletch 的定义,你也希望变量 bletch 是在 bar 中定义的。不过,如果 foo 在 include bar 之前已经定义了 bletch,你就不希望覆盖那个定义。这也可以通过在 foo 中使用 override 指令、让其定义优先于 bar 中靠后的定义来实现。但遗憾的是,override 指令连命令行上的定义也会一并覆盖。因此,可以在 bar 中包含以下内容:
ifdef bletch ifeq "$(origin bletch)" "environment" bletch = barf, gag, etc. endif endif
如果 bletch 是从环境定义的,这会将其重新定义。
如果 bletch 之前的定义来自环境,而你希望即便在「-e」之下也覆盖它,那么可以改写成下面这样:
ifneq "$(findstring environment,$(origin bletch))" "" bletch = barf, gag, etc. endif
这里,当「$(origin bletch)」返回「environment」或「environment override」之一时,就会进行重新定义。请参阅用于字符串替换与解析的函数。
flavor 函数与 origin 函数一样,不操作变量的值,而是告诉你关于变量本身的信息。具体来说,它告诉你变量的种类 (flavor)(请参阅 变量的两种种类)。
flavor 函数的语法如下:
$(flavor variable)
请注意,variable 是所查询对象变量的名字,而非对该变量的引用。因此,书写它时通常不使用「$」或括号(不过,如果不希望名字是常量,也可以在名字中使用变量引用)。
这个函数的结果是一个表示变量 variable 种类的字符串:
variable 从未被定义的情况。
variable 是递归展开变量的情况。
variable 是简单展开变量的情况。
这些函数控制 make 的运作方式。一般用于向 makefile 的使用者提供信息,或在检测到某种环境上的错误时让 make 停止。
$(error text…)生成以 text 为消息的致命错误。需要注意的是,错误必定是在这个函数被求值时才生成。因此,如果把它放在命令 (recipe) 中,或放在递归展开变量赋值的右边,那么它要到后面才被求值。text 会在错误生成之前被展开。
例如,
ifdef ERROR1 $(error error is $(ERROR1)) endif
会在 make 变量 ERROR1 已定义的情况下,于读取 makefile 期间生成致命错误。或者,
ERR = $(error found an error!) .PHONY: err err: ; $(ERR)
会在 err 目标被调用的情况下,于 make 执行期间生成致命错误。
$(warning text…)
这个函数的工作方式与上面的 error 函数相同,但区别在于 make 不会终止。它会展开 text 并显示由此得到的消息,而 makefile 的处理会继续进行。
这个函数的展开结果是空字符串。
$(info text…)这个函数除了把其(已展开的)参数显示到标准输出之外,什么也不做。不会附加 makefile 名或行号。这个函数的展开结果是空字符串。
shell 函数,除了 wildcard 函数(请参阅 wildcard 函数)之外,与其他任何函数都不同。因为它会与 make 之外的世界打交道。
shell 函数为 make 提供了与大多数 shell 中的反引号(「`」)相同的功能。也就是说,它进行命令展开。这意味着它取一条 shell 命令作为参数,并展开为该命令的输出。make 对该结果所做的处理,仅仅是把每个换行(或回车与换行的组合)转换成一个空格。如果末尾有(回车与)换行,则会被直接去除。
由 shell 函数调用所执行的命令,会在该函数调用被展开时执行(请参阅 make 读取 Makefile 的机制)。由于这个函数会伴随启动一个新的 shell,所以应当好好斟酌在递归展开变量中使用 shell 函数与在简单展开变量中使用它,各自对性能的影响(请参阅 变量的两种种类)。
作为 shell 函数的替代,还有「!=」赋值运算符。它的行为类似,但有些微妙的区别(请参阅 设置变量)。「!=」赋值运算符包含在较新的 POSIX 标准中。
在使用了 shell 函数或「!=」赋值运算符之后,其退出状态会被存入 .SHELLSTATUS 变量。
这里举几个 shell 函数的使用示例:
contents := $(shell cat foo)
这会把文件 foo 的内容,以各行以(不是换行而是)空格分隔的形式,设给 contents。
files := $(shell echo *.c)
这会把「*.c」的展开结果设给 files。除非 make 使用的是相当古怪的 shell,否则这(只要至少存在一个「.c」文件)会得到与「$(wildcard *.c)」相同的结果。
所有被标记为 export 对象的变量,也都会传给 shell 函数所启动的 shell。这有可能引起变量展开的循环。请考虑下面这个 makefile:
export HI = $(shell echo hi) all: ; @echo $$HI
当 make 准备执行命令 (recipe) 时,必须把变量 HI 加入环境。为此需要展开 HI。这个变量的值需要调用 shell 函数,而要调用它就必须构建其环境。由于 HI 被导出,所以构建那个环境又需要展开 HI。然后这样无休止地循环下去。在这个难解的情形中,make 不会陷入循环或报错,而是使用给 make 的环境中那个变量的值,若没有则使用空字符串。这多半正是你想要的。例如:
export PATH = $(shell echo /usr/local/bin:$$PATH)
不过,这种情况下从一开始就使用简单展开变量(「:=」)会更简单也更高效。
当 GNU make 在构建时带有对 GNU Guile 作为内置扩展语言的支持时,可以使用 guile 函数。guile 函数取一个参数,先由 make 按通常方式展开,然后传给 GNU Guile 的求值器。求值器的结果会被转换为字符串,并用作 makefile 中 guile 函数的展开结果。关于如何用 Guile 编写 make 扩展的详情,请参阅与 GNU Guile 的集成。
是否可以使用 GNU Guile 的支持,可以通过检查 .FEATURES 变量中是否有「guile」这个单词来判定。