Makefile 语法详解 (2)- 变量、条件判断与函数

本文内容是之前的文章 Makefile 简介 的补充,详细介绍了 Makefile 中的变量(包括变量的定义、批量替换、局部变量等)、条件判断和函数(内置函数和自定义函数)。

变量

在 Makefile 中的定义的变量,就像是 C/C++ 语言中的宏一样,代表了一个文本字串,在 Makefile 中执行的时候其会自动地展开在所使用的地方。其与 C/C++ 所不同的是,可以在 Makefile 中改变其值。

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。如果你要使用真实的 $ 字符,那么你需要用 $$ 来表示。

变量的赋值

定义 Makefile 中为变量赋值可用四种操作符:=:=?=+=, 参考 StackOverflow 上的问题 What is the difference between the GNU Makefile variable assignments =, ?=, := and +=?, 这四个符号的主要区别是

  1. = 赋值是 lazy 的,也就是在使用的时候才会递归的获取变量的值(递归指的是可以通过一个变量为另一个变量赋值)
  2. := 则是在声明的时候变量的值就确定了
  3. ?= 表示在变量没有值的时候才给其赋值
  4. += 则是在原来的值上 append 一个其他的值(自动添加空格)

其中 = 赋值是的递归获取值比较难理解,简单来说就是右侧中的变量不一定非要是已定义好的值,也可以使用后面定义的值。如下是一个简单地例子

1
2
3
4
5
6
foo = $(bar)
bar = $(ugh)
ugh = Haha

all:
echo $(foo)

执行 make all 时输出的值是 Haha, 而是用 := 赋值时就不允许这么赋值,在 := 右边的值必须只能是字符串或者前面定义的变量,也就是在 := 右边的值必须要是目前为止已经确定的值。

1
2
3
4
5
6
7
8
# 示例 1
x := foo
y := $(x) bar
x := later

# 示例 2
y := $(x) bar
x := foo

上面示例 1 中的 y 的值为 foo bar,示例 2 中的值为 bar。

变量值的替换

我们可以替换变量中的共有的部分,其格式是 $(var:a=b) 或是 $(var: %a=%b),其意思是,把变量 var 中所有以 a 字串结尾的那些值从 a 替换成 b; 如下是一个简单的示例

1
2
3
4
foo := a.o b.o c.o
bar := $(foo:.o=.c) 或 bar := $(foo:%.o=%.c)

# bar 的值是 a.c b.c c.c

把变量值当做变量名

Makefile 中如果变量 a 的值是变量 b 的名称,那么可以把变量 a 的值直接当做变量 b 使用,如下是一些简单的例子

1
2
3
4
5
6
7
8
9
10
11
# 示例一
x = y
y = z
z = u
a := $($($(x))) # u

# 示例二
x = $(y)
y = z
z = Hello
a := $($(x)) # Hello

局部变量

前面我们所讲的在 Makefile 中定义的变量都是全局变量, 但是也可以为某个目标设置局部变量,这种变量被称为 Target-specific Variable,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值,因为局部变量的名称可与全局变量的名称相同。

其一般语法如下,首先在 target 中定义局部变量,则在 target 及其依赖的目标中使用该变量即可

1
2
target : local_variable assignment
target : use variable

如下是个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
$(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
$(CC) $(CFLAGS) prog.c

foo.o : foo.c
$(CC) $(CFLAGS) foo.c

bar.o : bar.c
$(CC) $(CFLAGS) bar.c

除了 Target-specific Variable,还有 Pattern-specific Variable,即不是在某个特定的 target 中定义局部变量,而是在某个特定的 pattern 中定义局部变量,其语法与上面的类似,如下所示是定义了所有以 .o 结尾的目标中的一个局部变量

1
%.o : CFLAGS = -O

条件判断

使用条件判断,可以让 make 根据运行时的不同情况选择不同的执行分支,主要有以下几个关键字: ifeqifneqifdefifndefelseendif;

根据名称其实也能基本能猜出各个关键字的作用了,ifeqifneq 是一对关键字,表示其后面跟随的两个参数是够相等,ifdefifndef 是一对关键字,表示其后跟随的变量是否已经被定义过。

如下是一个简单的例子,表示目标 foo 可以根据变量 $(CC) 值来选取不同的函数库来编译

1
2
3
4
5
6
7
8
9
libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif

$(CC) 是 gcc 时,目标 foo 的规则是

1
2
foo: $(objects)
$(CC) -o foo $(objects) $(libs_for_gcc)

因此,上面的写法可以写成如下更简洁且容易理解的形式

1
2
3
4
5
6
7
8
9
10
11
libs_for_gcc = -lgnu
normal_libs =

ifeq ($(CC), gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif

foo: $(objects)
$(CC) -o foo $(objects) $(libs)

需要注意的一点是在关键字所在的这一行上,多余的空格是被允许的,但是不能以 Tab 键做为开始,否则就被认为是命令

使用 ifeq 可有若干种形式,如下所示的五种形式都是等价的, ifneq 的使用方法相同

1
2
3
4
5
ifeq (<arg1>, <arg2>) 
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"

ifdefifndef 的使用方法也类似,只是其后面只跟着一个变量。

函数

GNU make 内置了一些函数,在 Makefile 中使用函数来处理变量,可以让我们的命令或是规则更为的灵活

函数调用很像变量的使用,也是以 $ 来标识的,其语法如下,其中 <function> 就是函数名,<arguments> 为函数的参数,参数间以逗号 , 分隔,而函数名和参数之间以空格分隔

1
2
3
$(<function> <arguments>)

${<function> <arguments>}

以下是 GNU make 内置的一些函数

字符串处理函数

  • subst 用法:$(subst <from>,<to>,<text>) 功能:把字串 <text> 中的 <from> 字符串替换成 <to>。 返回:函数返回被替换过后的字符串。

  • patsubst 用法:$(patsubst <pattern>,<replacement>,<text>) 功能:查找 <text> 中的单词是否符合模式 <pattern>,如果匹配的话,则以 <replacement> 替换。这里,<pattern> 可以包括通配符 %,表示任意长度的字串。如果 <replacement> 中也包含 %,那么,<replacement> 中的这个 % 将是 <pattern> 中的那个 % 所代表的字串。(可以用 \ 来转义) 返回:函数返回被替换过后的字符串 示例:$(patsubst %.c,%.o,x.c.c bar.c) 返回的结果是 x.c.o bar.o

  • findstring 用法:$(findstring <find>,<string>) 功能:在字串 <string> 中查找 <find> 字串。 返回:如果找到,那么返回 <find>,否则返回空字符串。

  • sort 用法:$(sort <list>) 功能:给字符串 <list> 中的单词排序(空格分隔)。 返回:返回排序后的字符串。 示例:$(sort foo bar lose) 返回 bar foo lose

  • word 用法:$(word <n>,<text>) 功能:取字符串 <text> 中第 <n> 个单词。(从 1 开始) 返回:返回字符串 <text> 中第 <n> 个单词 示例:$(word 2, foo bar baz) 返回值是 bar

foreach 函数

foreach 函数是用作循环的,其用法如下

$(foreach <var>,<list>,<text>)

该函数的意思是,把参数 <list> 中的单词逐一取出放到参数所指定的变量 <var> 中,然后再执行 < text> 所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中,<text> 的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text> 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。

如下是一个简单的例子

1
2
3
names := a b c d

files := $(foreach n,$(names),$(n).o)

上面的例子中,$(name) 中的单词会被挨个取出,并存到变量 n 中,$(n).o 每次根据 $(n) 计算出一个值,这些值以空格分隔,最后作为 foreach 函数的返回值,所以,$(files) 的值是 a.o b.o c.o d.o

需要注意的是,foreach 中的参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不在作用,其作用域只在 foreach 函数当中。

call 函数

call 函数可以用来创建自定义函数。其语法是:

$(call <expression>,<parm1>,<parm2>,<parm3>,...)

当 make 执行这个函数时,<expression> 参数中的变量: $(1),$(2),$(3) 等,会被参数 <parm1>,<parm2>,<parm3> 依次取代。而 <expression> 的返回值就是 call 函数的返回值。例如:

1
2
reverse =  $(2) $(1) 
foo = $(call reverse,a,b)

经过上面的表达式得到的 foo 的值是 b a。当然,可以为 reverse 定义更复杂的操作。

shell 函数

,shell 函数把执行操作系统命令后的输出作为函数返回。因此可以用操作系统命令以及字符串处理命令 awk,sed 等等命令来生成一个变量,如:

1
2
contents := $(shell cat foo)
files := $(shell echo *.c)