本实验将 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 为函数的参数,参数以逗号「,」进行分割。
函数处理参数时,若参数中存在其它变量或函数的引用,则先展开参数再进行函数处理,展开顺序与参数的先后顺序一致。
函数中的参数不能直接出现逗号和空格,前导空格会被忽略,若需要使用逗号和空格则需要将它们赋值给变量。
文本替换函数
subst 和 patsubst 可以对字符串进行替换,其中 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)
文件内容中的变量说明:
str_a是原字符串。str_b使用subst函数将str_a 所有的.o字符替换成.c字符。str_c使用patsubst用模式替换将str_a 所有的.o后缀替换为.c后缀。str_d和str_e演示在没有通配符的情况下,patsubst需要匹配整个字符串。str_1和str_2演示strip的字符串简化功能。(去除空格)
**
**
**
字符串处理函数
**
GNU make 函数的调用格式与变量引用相似,基本格式如下:
$(FUNCTION ARGUMENTS)
FUNCTION 为函数名,ARGUMENTS 为函数的参数,参数以逗号「,」进行分割。函数处理参数时,若参数中存在其它变量或函数的引用,则先展开参数再进行函数处理,展开顺序与参数的先后顺序一致。函数中的参数不能直接出现逗号和空格,前导空格会被忽略,若需要使用逗号和空格则需要将它们赋值给变量。
subst 和 patsubst 可以对字符串进行替换,其中 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)
文件内容中的变量说明:
str_a是原字符串。str_b使用subst函数将所有的.o字符替换成.c字符。str_c使用patsubst用模式替换将.o后缀替换为.c后缀。str_d和str_e演示在没有通配符的情况下,patsubst需要匹配整个字符串。str_1和str_2演示strip的字符串简化功能。
现在进入 strings 目录并执行 rep.mk 文件。
- 先执行sub |sub:raw 这时候raw输出str_a ,sub借助raw 输出str_b
cd strings; make -f rep.mk sub;
- 执行patsub |patsub:raw 这时候raw输出str_a ,patsub借助raw 输出str_c、d、e
这里看出
str_d和str_e演示在没有通配符的情况下,patsubst需要匹配整个字符串。 所以,str_d是把字符串是 .o 的换成 .c
str_e是把字符串 a.o 换成 a.c
- 执行-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
单词处理函数
单词处理函数包括:
$(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 x,str_d 匹配不存在的字符串 nothing。
filter | filter-out
测试 filter 和 filter-out 函数:
make -f word.mk filt;make -f word.mk filt_out
Terminal 的输出结果如下图:
str_e 和 str_f 分别过滤和反过滤 .py 结尾的字符串。
sort
测试 sort 函数:
make -f word.mk sort
str_g 是对 str_a 中单词首字母进行排序的结果,若首字母相同则以第二个字母排序,以此类推。
wordlist
测试 wordlist 函数:
make -f word.mk word_list
Terminal 的输出结果如下图:
str_j 和 str_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 文件名
dir 和 notdir 函数测试过程中涉及到的变量的主要内容如下:
vari_dir := $(dir $(detect_files))
show_dir := $(patsubst %,"\n"%,$(vari_dir))
vari_files := $(notdir $(detect_files))
vari_dir 和 vari_files 分别利用 dir 和 notdir 函数取得文件目录和文件名。由于文件目录过长,show_dir 变量在每个目录前加入换行符便于观察。 测试 dir 和 notdir 函数:
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_addprefix 和 vari_addsuffix 分别利用 addprefix 函数和 addsuffix 函数为文件名增加前缀 full name:,后缀 .text
测试 addprefix 和 addsuffix 函数:
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 文件演示了 foreach、if、error、warning 和 shell 这几个函数的用法。
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_a、dir_b、dir_c 和 dir_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_c 和 vari_d 根据 vari_a 和 vari_b 的定义与否来得到不同的值。开始执行测试:
make -f cond.mk if_cond
Terminal 的输出结果如下图:
从输出结果中可以看到 vari_c 因为 vari_a 没有定义,所以取值为参数$(3),而 vari_d 因为 vari_b 有定义,取值为 $(2)。
warning | error
warning 和 error 的测试代码如下:
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 的输出结果如下图所示:
从输出结果中可以看到,date 命令被成功的执行了。然而此处的时间是变量展开时的时间,而不是执行规则时的时间,请自行设计实验证明
value | origin
接下来测试 value 和 origin 函数,测试代码位于 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_a 到 vari_e 已经在 vari.mk 中定义,我们将 vari_e 导出为环境变量,并在命令行中添加 vari_a 的定义,观察打印的变量出处:其中 vari_a 到 vari_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 被定义为一个模板,根据传入的参数产生不同的规则。此实验中 server 和 client 被传入模板中产生 server 和 client 规则。
请注意由于 make 需要读入展开的规则模板,因此作为 make 解析和重构规则的文本中变量引用要使用
$$。而$$会被转义成$,这样才能使得变量引用生效,否则 make 在读入时就会展开变量产生预期外的效果。
现在分别测试这两条规则:
make -f eval.mk server; make -f eval.mk client
Terminal 的输出结果如下图:
可见使用规则模板后,server 规则和 client 规则行为类似,依赖文件却不一样。
本章节测试了 make 各个内建函数的使用方式。