11 make 内建函数

240 阅读6分钟

本实验将 make 的内建函数分为三类,并介绍它们的使用方法。

知识点

  • 字符串处理函数
  • make 控制函数
  • 文件名处理函数

学习内容

本课程项目完成过程中将学习:

  • 替换字符串函数
  • 简化空格函数
  • 字符串查找
  • 过滤
  • 排序
  • 单词查找
  • 统计单词数量
  • 单词连接
  • 取目录/文件
  • 取前后缀
  • 加前后缀
  • 文件名匹配
  • 循环
  • 条件控制
  • make控制
  • 函数调用
  • 调用 shell
  • 获取变量展开前的值
  • 二次展开
  • 查询变量出处

项目结构

本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter10/ 目录下。

项目文件结构:

.
├── control:控制相关的内建函数
│   ├── cond.mk
│   ├── eval.mk
│   └── vari.mk
├── files:文件名相关的内建函数
│   └── files.mk
└── strings:字符串处理相关的内建函数
    ├── rep.mk
    └── word.mk

1️⃣ 字符串处理函数

函数的使用规则

GNU make 函数的调用格式与变量引用相似,基本格式如下:

$(FUNCTION ARGUMENTS)

FUNCTION 为函数名,ARGUMENTS 为函数的参数,参数以逗号「,」进行分割
函数处理参数时,若参数中存在其它变量或函数的引用,则先展开参数再进行函数处理,展开顺序与参数的先后顺序一致。
函数中的参数不能直接出现逗号和空格,前导空格会被忽略,若需要使用逗号和空格则需要将它们赋值给变量


文本替换函数

substpatsubst 可以对字符串进行替换,其中 patsubst 可以使用模式替换,函数格式如下:

$(subst FROM,TO,TEXT)
$(patsubst PATTERN,REPLACEMENT,TEXT)

strip 函数可以简化字符串中的空格,将多个连续空格合并成一个,函数格式如下:

$(strip STRING)

文件夹 chapter10/strings/ 中的 rep.mk 文件演示了函数的用法,内容如下:

#test function subst patsubst strip

.PHONY:raw sub patsub

str_a := a.o b.o c.o f.o.o abcdefg
str_b := $(subst .o,.c,$(str_a))

str_c := $(patsubst %.o,%.c,$(str_a))
str_d := $(patsubst .o,.c,$(str_a))
str_e := $(patsubst a.o,a.c,$(str_a))

str_1 := "      a      b         c        "
str_2 := $(strip $(str_1))

sub:raw
    @echo "str_b=" $(str_b) #replace all match char for per word

patsub:raw
    @echo "str_c=" $(str_c) #replace match pattern
    @echo "str_d=" $(str_d) #replace nothing
    @echo "str_e=" $(str_e) #replace all-match word

strip:
    @echo "str_1=" $(str_1) #looks like auto strip by make4.1
    @echo "str_2=" $(str_2)

raw:
    @echo "str_a=" $(str_a)

文件内容中的变量说明:

  1. str_a 是原字符串。
  2. str_b 使用 subst 函数将str_a 所有的 .o 字符替换成 .c 字符。
  3. str_c 使用 patsubst 用模式替换将str_a 所有的 .o 后缀替换为 .c 后缀。
  4. str_dstr_e 演示在没有通配符的情况下,patsubst 需要匹配整个字符串。
  5. str_1str_2 演示 strip 的字符串简化功能。(去除空格)

**

**

**

**

字符串处理函数

**

GNU make 函数的调用格式与变量引用相似,基本格式如下:

$(FUNCTION ARGUMENTS)

FUNCTION 为函数名,ARGUMENTS 为函数的参数,参数以逗号「,」进行分割。函数处理参数时,若参数中存在其它变量或函数的引用,则先展开参数再进行函数处理,展开顺序与参数的先后顺序一致。函数中的参数不能直接出现逗号和空格,前导空格会被忽略,若需要使用逗号和空格则需要将它们赋值给变量。

substpatsubst 可以对字符串进行替换,其中 patsubst 可以使用模式替换,函数格式如下:

$(subst FROM,TO,TEXT)
$(patsubst PATTERN,REPLACEMENT,TEXT)

strip 函数可以简化字符串中的空格,将多个连续空格合并成一个,函数格式如下:

$(strip STRING)

文件夹 chapter10/strings/ 中的 rep.mk 文件演示了函数的用法,内容如下:

#test function subst patsubst strip

.PHONY:raw sub patsub

str_a := a.o b.o c.o f.o.o abcdefg
str_b := $(subst .o,.c,$(str_a))

str_c := $(patsubst %.o,%.c,$(str_a))
str_d := $(patsubst .o,.c,$(str_a))
str_e := $(patsubst a.o,a.c,$(str_a))

str_1 := "      a      b         c        "
str_2 := $(strip $(str_1))

sub:raw
    @echo "str_b=" $(str_b) #replace all match char for per word

patsub:raw
    @echo "str_c=" $(str_c) #replace match pattern
    @echo "str_d=" $(str_d) #replace nothing
    @echo "str_e=" $(str_e) #replace all-match word

strip:
    @echo "str_1=" $(str_1) #looks like auto strip by make4.1
    @echo "str_2=" $(str_2)

raw:
    @echo "str_a=" $(str_a)

文件内容中的变量说明:

  1. str_a 是原字符串。
  2. str_b 使用 subst 函数将所有的 .o 字符替换成 .c 字符。
  3. str_c 使用 patsubst 用模式替换将 .o 后缀替换为 .c 后缀。
  4. str_dstr_e 演示在没有通配符的情况下,patsubst 需要匹配整个字符串。
  5. str_1str_2 演示 strip 的字符串简化功能。

现在进入 strings 目录并执行 rep.mk 文件。


  1. 先执行sub |sub:raw 这时候raw输出str_a ,sub借助raw 输出str_b
cd strings; make -f rep.mk sub;

图片.png


  1. 执行patsub |patsub:raw 这时候raw输出str_a ,patsub借助raw 输出str_c、d、e

图片.png

这里看出str_dstr_e 演示在没有通配符的情况下,patsubst 需要匹配整个字符串。 所以,str_d是把字符串是 .o 的换成 .c
str_e是把字符串 a.o 换成 a.c


  1. 执行-f strip
strip:
	@echo "str_1=" $(str_1) #looks like auto strip by make4.1
	@echo "str_2=" $(str_2)

输出str_1 和str_2

 make -f rep.mk strip

图片.png


单词处理函数

单词处理函数包括:

$(findstring FIND,IN) #查找字符串,若存在返回字符串,否则返回空
$(filter PATTERN...,TEXT) #去除指定模式的字符串
$(filter-out PATTERN...,TEXT) #保留指定模式的字符串,去除其它字符串
$(sort LIST) #按首字母顺序进行排序
$(word N,TEXT) #获取第 N 个单词
$(wordlist S,E,TEXT) #获取从 S 位置到 E 位置的单词
$(words TEXT) #统计字符串中的单词数量
$(firstword NAMES...) #获取第一个单词
$(join LIST1,LIST2) #将 LIST1 和 LIST2 中的单词按顺序逐个连接

当前目录下的 word.mk 文件演示了以上函数的用法,由于篇幅较长,请自行阅读。

#test wrods related function

.PHONY:raw find filt filt_out sort word word_list first_word words join call

str_a := cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o 
str_b := $(findstring xx,$(str_a)) #make sure if xx include in str_a
str_c := $(findstring .o x,$(str_a)) #even can include space char
str_d := $(findstring nothing,$(str_a)) #search no-exist words

str_e := $(filter %.py,$(str_a)) #filster
str_f := $(filter-out %.py,$(str_a)) #filter-out

str_g := $(sort $(str_a)) #sort according first char,if same then next char

str_h := $(word 3,$(str_a)) # get the 3rd word
str_i := $(word 99,$(str_a)) # out of range

str_j := $(wordlist 3,5,$(str_a)) #list 3rd to 5rd words
str_k := $(wordlist 3,99,$(str_a)) #list our of range

str_l := $(firstword $(str_a)) #first word

str_m := $(words $(str_a)) #cacu words num

str_join := ./dira/ ./dirb/ ./dirc/ ./dird/ ./dire/ ./dirf/
str_n := $(join $(str_join),$(str_a))

part_rev = $(4) $(3) $(2) $(1)
str_o = $(call part_rev,a,b,c,d,e,f,g) #must use "=" 

raw:
	@echo "str_a=" $(str_a)

find:raw
	@echo "str_b=" $(str_b)
	@echo "str_c=" $(str_c)
	@echo "str_d=" $(str_d)

filt:raw
	@echo "str_e=" $(str_e)

filt_out:raw
	@echo "str_f=" $(str_f)

sort:raw
	@echo "str_g=" $(str_g)

word:raw
	@echo "str_h=" $(str_h)
	@echo "str_i=" $(str_i)

word_list:raw
	@echo "str_j=" $(str_j)
	@echo "str_k=" $(str_k)

first_word:raw
	@echo "str_l=" $(str_l)

words:raw
	@echo "str_m=" $(str_m)

join:raw
	@echo "str_join=" $(str_join)
	@echo "str_n=" $(str_n)

call:
	@echo "str_o=" $(str_o)


findstring

现在测试 findstring 函数。

make -f word.mk find

Terminal 的输出结果如下图:

str_b 是匹配字符串 xx 的结果,str_c 匹配 .o xstr_d 匹配不存在的字符串 nothing


filter | filter-out

测试 filterfilter-out 函数:

make -f word.mk filt;make -f word.mk filt_out

Terminal 的输出结果如下图:

str_estr_f 分别过滤和反过滤 .py 结尾的字符串。


sort

测试 sort 函数:

make -f word.mk sort

str_g 是对 str_a 中单词首字母进行排序的结果,若首字母相同则以第二个字母排序,以此类推。


wordlist

测试 wordlist 函数:

make -f word.mk word_list

Terminal 的输出结果如下图:

str_jstr_k 的定义如下:

str_j := $(wordlist 3,5,$(str_a)) #list 3rd to 5rd words
str_k := $(wordlist 3,99,$(str_a)) #list our of range

str_j 打印第 3,4,5 个单词 str_k 则打印从 3 开始的所有单词,当 end 位置超出界限时,wordlist 会取到 str_a 的最后一个单词处。


words

利用 words 函数统计 str_a 的单词数量,内容如下:

str_m := $(words $(str_a)) # 计算单词的个数

现在对 words 进行测试:

make -f word.mk words

Terminal 的输出结果如下图:


join

利用 join 函数会逐个连接下面两个字符串中对应同一位置的单词,示例如下:

str_a := cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_join := ./dira/ ./dirb/ ./dirc/ ./dird/ ./dire/ ./dirf/
str_n := $(join $(str_join),$(str_a))

join 函数进行测试:

make -f word.mk join

Terminal 的输出结果如下图:


call

利用 call 函数反转前四个单词的位置,并舍弃其它参数,示例如下:

part_rev = $(4) $(3) $(2) $(1)
str_o = $(call part_rev,a,b,c,d,e,f,g) #must use "="

$(1)$(4) 分别代表传给 call 的 4 个参数 a b c d,$(0) 代表 part_rev 函数。

现在对函数 call 进行测试:

make -f word.mk call

Terminal 的输出结果如下图:

可以看出 前 4 个参数被翻转的同时其他的参数已经被舍弃了。


2️⃣ 文件名相关函数

文件名处理相关的函数包括:

$(dir NAMES...) #获取目录
$(notdir NAMES...) #获取文件名
$(suffix NAMES...) #获取后缀
$(basename NAMES...) #获取前缀
$(addsuffix SUFFIX,NAMES...) #增加后缀
$(addprefix PREFIX,NAMES...) #增加前缀
$(wildcard PATTERN) #获取匹配的文件名

通过文件夹 chapter10/files/ 中的 files.mk 文件可以对文件名相关函数的用法进行演示。


init

init 规则涉及到的内容如下:

dirs := dir_a
files := file_a.c file_b.s file_c.o
files_a := $(foreach each,$(files),$(dirs)"/"$(each))

init:
    @mkdir $(dirs);\
    touch $(all_files);\
    tree # 显示当前文件夹的树状结构

现在进入目录并执行 init 规则会自动生成用于函数测试的目录和文件:

cd ../files;make -f files.mk init

cop

在执行 init 规则时涉及到的 foreach 函数其格式为 $(foreach var,list,text) 相当于 shell 中的 for 循环,详见

在实验内容中 $(foreach each, $(files), $(dirs)"/"$(each)) 表示在每次循环过程中将 files 列表中的一个元素赋值给 each 变量,然后通过表达式 $(dirs)"/"$(each) 生成一个包含相对路径的文件名,将此文件名传递给 touch 命令从而生成对应的文件。

Terminal 的输出结果如图:


detect_files 获取目录下文件

detect_files 变量利用 foreach 函数(前面已经提到)和 wildcard 函数获取 dir_a 目录下的文件,并在每个目录前增加换行符后赋值给 show 变量方便打印和观察,部分内容如下:

detect_files := $(foreach each,$(dirs),$(wildcard $(each)/*))
detect_files := $(foreach each,$(detect_files),$(PWD)"/"$(each))
show := $(patsubst %,"\n"%,$(detect_files)) #add '\n' for view

dir 目录| notdir 文件名

dirnotdir 函数测试过程中涉及到的变量的主要内容如下:

vari_dir := $(dir $(detect_files))
show_dir := $(patsubst %,"\n"%,$(vari_dir))
vari_files := $(notdir $(detect_files))

vari_dirvari_files 分别利用 dirnotdir 函数取得文件目录和文件名。由于文件目录过长,show_dir 变量在每个目录前加入换行符便于观察。 测试 dirnotdir 函数:

make -f files.mk dir ; make -f files.mk notdir

Terminal 的输出结果如下图:


addprefix | addsuffix 增加前后缀

获取文件名前后缀函数测试代码如下:

vari_addprefix := $(addprefix "full name:",$(detect_files))
show_addprefix := $(patsubst %,"\n"%,$(vari_addprefix))

vari_addsuffix := $(addsuffix ".text",$(detect_files))
show_addsuffix := $(patsubst %,"\n"%,$(vari_addsuffix))

vari_addprefixvari_addsuffix 分别利用 addprefix 函数和 addsuffix 函数为文件名增加前缀 full name:,后缀 .text

测试 addprefixaddsuffix 函数:

make -f files.mk addprefix ; make -f files.mk addsuffix

Terminal 的输出结果如下图所示:


总体代码:


.PHONY:init clean dir notdir base suffix addprefix addsuffix wildcard

dirs := dir_a
files := file_a.c file_b.s file_c.o #each file under per dir
files_a := $(foreach each,$(files),$(dirs)"/"$(each)) #get all files under a dir by foreach & word func
all_files := $(files_a)
detect_files := $(foreach each,$(dirs),$(wildcard $(each)/*))
detect_files := $(foreach each,$(detect_files),$(PWD)"/"$(each))
show := $(patsubst %,"\n"%,$(detect_files)) #add '\n' for view

vari_dir := $(dir $(detect_files))
show_dir := $(patsubst %,"\n"%,$(vari_dir))

vari_files := $(notdir $(detect_files))

vari_base := $(basename $(detect_files))
show_base := $(patsubst %,"\n"%,$(vari_base))

vari_suffix := $(suffix $(detect_files))

vari_addprefix := $(addprefix "full name:",$(detect_files))
show_addprefix := $(patsubst %,"\n"%,$(vari_addprefix))

vari_addsuffix := $(addsuffix ".text",$(detect_files))
show_addsuffix := $(patsubst %,"\n"%,$(vari_addsuffix))

init:
	@mkdir $(dirs);\
	touch $(all_files);\
	tree

dir:
	@echo "detected files:" $(show)
	@echo "get dir:" $(show_dir)

notdir:
	@echo "detected files:" $(show)
	@echo "get files:"
	@echo $(vari_files)

base:
	@echo "detected files:" $(show)
	@echo "file base name:" $(show_base)

suffix:
	@echo "detected files:" $(show)
	@echo "file suffix:" $(vari_suffix)

addprefix:
	@echo "detected files:" $(show)
	@echo "file add prefix:" $(show_addprefix)

addsuffix:
	@echo "detected files:" $(show)
	@echo "file add suffix:" $(show_addsuffix)

clean:
	@rm -rf $(dirs)


3️⃣ 控制和变量相关的函数

控制和变量相关的函数包括:

#把 LIST 中的单词依次赋给 VAR,并执行 TEXT 中的表达式
$(foreach VAR,LIST,TEXT) 
#如果满足 CONDITION 条件,执行 THEN-PART 语句,否则执行 ELSE-PART 语句
$(if CONDITION,THEN-PART[,ELSE-PART]) 

$(error TEXT...) #产生致命错误并以 TEXT 内容进行提示
$(warning TEXT...) #产生警告并以 TEXT 内容进行提示
$(shell CMD...) #调用 shell 并传入 CMD 作为参数

#返回 VARIABLE 未展开前的定义值,即 makefile 中定义变量时所书写的字符串
$(value VARIABLE) 

$(origin VARIABLE) # 返回变量的初始定义方式,包括:
#undefined,default,environment,environment override,file,command 
#line,override,automatic

#将 TEXT 内容展开为 makefile 的一部分,可用于将字符串展开为规则供 make 解析
$(eval TEXT...) 

文件夹 chapter10/control/ 中的 cond.mk 文件演示了 foreachiferrorwarningshell 这几个函数的用法。


init

init 规则利用 foreach 函数遍历需要生成的文件,并与 $(dir) 路径结合生成文件全名(上一节已经提到过):

files_a := $(foreach each,$(files),$(word 1,$(dirs))"/"$(each)) 
#get all files under a dir by foreach & word func
files_b := $(foreach each,$(files),$(word 2,$(dirs))"/"$(each))
files_c := $(foreach each,$(files),$(word 3,$(dirs))"/"$(each))
files_d := $(foreach each,$(files),$(word 4,$(dirs))"/"$(each))

detect_files 变量则使用 foreach 函数遍历全部目录,并获取目录下的文件名。

detect_files := $(foreach each,$(dirs),$(wildcard $(each)/*))

现在执行 init 规则生成测试所需的文件:

cd ../control; make -f cond.mk init

Terminal 的输出结果如下图:

其中 dir_adir_bdir_cdir_d 就是测试中需要用到的目录。


获取文件名字 detect_files

detect_files 变量则使用 foreach 函数遍历全部目录,并获取目录下的文件名。

detect_files := $(foreach each,$(dirs),$(wildcard $(each)/*))

现在测试 foreach 函数:

make -f cond.mk for_loop

Terminal 的输出结果如下图:


if

接下来测试 if 函数,测试代码如下:

vari_a :=
vari_b := b
vari_c := $(if $(vari_a),"vari_a has value:"$(vari_a),"vari_a has no value")
vari_d := $(if $(vari_b),"vari_b has value:"$(vari_b),"vari_b has no value")

vari_cvari_d 根据 vari_avari_b 的定义与否来得到不同的值。开始执行测试:

make -f cond.mk if_cond

Terminal 的输出结果如下图:

从输出结果中可以看到 vari_c 因为 vari_a 没有定义,所以取值为参数$(3),而 vari_d 因为 vari_b 有定义,取值为 $(2)


warning | error

warningerror 的测试代码如下:

err_exit := $(if $(vari_e),$(error "you generate a error!"),"no error defined") #define vari_e to enable error
warn_go := $(if $(vari_f),$(warning "you generate a warning!"),"no warning defined") #define vari_f to enalbe warning

如果有定义 vari_e 变量,会产生一条错误信息并使 make 停止执行,如果有定义 vari_f 变量,会产生一条警告信息,make 继续执行。 现在执行测试下面的命令并观察输出结果:

make -f cond.mk warn

Terminal 的输出结果如下图:

可以看出这是一条普通信息。

接下来执行下面的命令:

make -f cond.mk warn vari_f=1

Terminal 的输出结果如下图:

可以看出这是一条 make 抛出的警告信息。

接下来我们以同样的方法对 error 进行测试:

make -f cond.mk err vari_e=1

Terminal 的输出结果如下图:

可以看出这是一条 make 抛出的错误信息。


shell函数

shell 函数的测试代码如下:

shell_cmd := $(shell date)

现在执行下面的命令:

make -f cond.mk shell

Terminal 的输出结果如下图所示:

图片.png

从输出结果中可以看到,date 命令被成功的执行了。然而此处的时间是变量展开时的时间,而不是执行规则时的时间,请自行设计实验证明


value | origin

接下来测试 valueorigin 函数,测试代码位于 vari.mk 文件中。 定义五个变量如下:

vari_a = abc
vari_b = $(vari_a)
vari_c = $(vari_a) "+" $(vari_b)
override vari_d = vari_a
vari_e = $($(vari_d))

使用 value 函数得到他们的定义字符串并打印:

vari_1 = $(value vari_a)
vari_2 = $(value vari_b)
vari_3 = $(value vari_c)
vari_4 = $(value vari_d)
vari_5 = $(value vari_e)

value:
    @echo "vari_1=" '$(vari_1)'
    @echo "vari_2=" '$(vari_2)'
    @echo "vari_3=" '$(vari_3)'
    @echo "vari_4=" '$(vari_4)'
    @echo "vari_5=" '$(vari_5)'

现在对 value 规则进行测试。

make -f vari.mk value

Terminal 的输出结果如下图:

可见输出的内容与其定义一致。


origin 函数测试代码如下:返回变量的初始定义方式

origin:
    @echo "origin vari_a:" $(origin vari_a)
    @echo "origin vari_b:" $(origin vari_b)
    @echo "origin vari_c:" $(origin vari_c)
    @echo "origin vari_d:" $(origin vari_d)
    @echo "origin vari_e:" $(origin vari_e)
    @echo 'origin $$@:' $(origin @)
    @echo "origin vari_f:" $(origin vari_f)
    @echo "origin PATH:" $(origin PATH)
    @echo "origin MAKE:" $(origin MAKE)

其中 vari_avari_e 已经在 vari.mk 中定义,我们将 vari_e 导出为环境变量,并在命令行中添加 vari_a 的定义,观察打印的变量出处:其中 vari_avari_e 已经在 vari.mk 中定义,我们将 vari_e 导出为环境变量,并在命令行中添加 vari_a 的定义,观察打印的变量出处:

export vari_e=1;make -f vari.mk origin vari_a=1 -e

Terminal 的输出结果如下图:


eval

将 TEXT 内容展开为 makefile 的一部分,可用于将字符串展开为规则供 make 解析

eval 函数是一个二次解析函数,函数先将其变量做一次展开,展开的结果将会作为 makefile 规则的一部分被 make 做第二次解析,这样就可以定义一些规则模板,增强 makefile 灵活性。

在当前目录下提供了对 eval 测试的文件为 eval.mk,内容如下:

#this is a eval func test

PROGRAMS = server client
server_OBJS = server.o server_pri.o server_access.o
server_LIBS = priv protocol

client_OBJS = client.o client_api.o client_mem.o
client_LIBS = protocol

.PHONY:all

define PROGRAM_template
$(1):
    touch $$($(1)_OBJS) $$($(1)_LIBS)
    @echo $$@ " build finished!"
endef

$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))

$(PROGRAMS):

clean:
    $(RM) *.o $(server_LIBS) $(client_LIBS)

其中 PROGRAM_template 被定义为一个模板,根据传入的参数产生不同的规则。此实验中 serverclient 被传入模板中产生 serverclient 规则。

请注意由于 make 需要读入展开的规则模板,因此作为 make 解析和重构规则的文本中变量引用要使用 $$。而 $$ 会被转义成 $,这样才能使得变量引用生效,否则 make 在读入时就会展开变量产生预期外的效果。

现在分别测试这两条规则:

make -f eval.mk server; make -f eval.mk client

Terminal 的输出结果如下图:

可见使用规则模板后,server 规则和 client 规则行为类似,依赖文件却不一样。


本章节测试了 make 各个内建函数的使用方式。