4、Makefile函数:字符串处理函数与文件名函数
说实话,很多写Makefile的朋友,写了三五年还在用最基础的变量替换。遇到稍微复杂点的路径处理,就开始写shell脚本去拼凑。其实Makefile自带的这几个函数,就能解决90%的日常问题。
我个人习惯,把Makefile里的函数分成两类:一类是字符串处理,一类是文件名处理。今天咱们就把这两类里最常用的几个函数讲透。
4.1 字符串处理三剑客:subst、patsubst、strip
4.1.1 subst — 简单替换
subst 函数,说白了就是「把字符串A替换成字符串B」。它的语法很简单:
$(subst 被替换的字符串, 替换成的字符串, 原始文本)
举个例子:
SRC := main.c foo.c bar.c
OBJ := $(subst .c,.o,$(SRC))
# OBJ 的值是:main.o foo.o bar.o
嗯,这里要注意:subst 是全量替换。只要匹配到就换,不会只换第一个。我在项目中遇到过有人拿它去替换路径分隔符,结果把不该换的地方也换了,排查了半天。
4.1.2 patsubst — 模式替换
patsubst 比 subst 聪明一点。它支持 % 通配符,可以匹配任意长度的字符串。语法:
$(patsubst 模式, 替换模式, 原始文本)
看个实际例子:
SRC := src/main.c src/foo.c src/bar.c
OBJ := $(patsubst src/%.c, build/%.o, $(SRC))
# OBJ 的值是:build/main.o build/foo.o build/bar.o
这里 % 匹配了 main、foo、bar 这些文件名(不含后缀)。然后替换到 build/%.o 里。
我曾经在做一个嵌入式项目时,源文件分散在七八个目录里。用 patsubst 配合 wildcard,一行就把所有源文件映射到统一的目标目录了。要是用shell去搞,得写好几层循环。
4.1.3 strip — 清理空格
strip 函数,名字很直白——去掉字符串首尾的空格,并把中间连续的空格压缩成一个。语法:
$(strip 字符串)
你可能会问:「这玩意儿有啥用?」
我告诉你,坑就坑在空格上。比如:
CFLAGS = -O2 -Wall -g
# 注意,这里有多余空格
# 如果不 strip,条件判断可能出问题
ifeq ($(strip $(CFLAGS)), -O2 -Wall -g)
# 这里才会正确执行
endif
我记得有一次,同事在Makefile里用 $(findstring) 检查编译选项,因为变量里多了个空格,死活匹配不上。最后加了个 strip 就解决了。所以我现在写Makefile,凡是涉及到字符串比较的地方,都会先 strip 一下。
4.2 文件名函数:dir、notdir、basename
处理文件路径,是Makefile里最频繁的操作之一。这三个函数,我几乎每天都在用。
4.2.1 dir — 提取目录部分
dir 从文件路径中提取目录部分,包括最后一个斜杠。语法:
$(dir 文件名列表)
示例:
$(dir src/main.c) # 返回 src/
$(dir /usr/local/bin/) # 返回 /usr/local/bin/
$(dir foo/bar/baz.c) # 返回 foo/bar/
注意,如果文件就在当前目录,比如 main.c,dir 会返回 ./。这个细节容易忽略。
4.2.2 notdir — 提取文件名部分
notdir 和 dir 正好相反,它只取文件名,去掉目录路径。语法:
$(notdir 文件名列表)
示例:
$(notdir src/main.c) # 返回 main.c
$(notdir /usr/local/bin/) # 返回 bin
$(notdir foo/bar/baz.c) # 返回 baz.c
我在项目中经常这样用:先用 wildcard 找到所有源文件,然后用 notdir 提取文件名,再用 patsubst 把后缀换成 .o。三行代码搞定整个项目的目标文件列表。
4.2.3 basename — 去掉后缀
basename 去掉文件名的最后一个后缀(包括点号)。语法:
$(basename 文件名列表)
示例:
$(basename main.c) # 返回 main
$(basename foo/bar.tar.gz) # 返回 foo/bar.tar
$(basename src/main.c) # 返回 src/main
注意,basename 只去掉最后一个点号后面的内容。所以 tar.gz 只会去掉 .gz,保留 tar。如果你需要去掉多个后缀,得自己写递归或者用 patsubst。
假设你有一个源文件列表,想生成对应的目标文件列表,且目标文件放在 build 目录下:
SRC := $(wildcard src/*.c)
OBJ := $(addprefix build/,$(notdir $(SRC:%.c=%.o)))
这行代码做了三件事:1. 找到所有 src/*.c 文件
2. 去掉目录,只保留文件名,并把 .c 换成 .o
3. 加上 build/ 前缀
最终结果类似:build/main.o build/foo.o build/bar.o
4.3 知识体系总览
下面这张图,是我自己总结的Makefile函数分类和关系。你看一眼就能明白今天讲的这些函数在整体中的位置。
4.4 综合实战:一个典型的构建脚本片段
光说不练假把式。下面是我在一个实际项目中用到的Makefile片段,把这些函数串起来用:
# 源文件目录
SRC_DIR := src
BUILD_DIR := build
# 查找所有 .c 文件
SRC := $(wildcard $(SRC_DIR)/*.c)
# 生成目标文件列表(去掉目录,换后缀,加前缀)
OBJ := $(addprefix $(BUILD_DIR)/, $(notdir $(patsubst %.c, %.o, $(SRC))))
# 生成依赖文件列表
DEP := $(patsubst %.o, %.d, $(OBJ))
# 编译规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# 包含依赖文件
-include $(DEP)
# 清理
clean:
rm -rf $(BUILD_DIR)
这段代码里,wildcard、notdir、patsubst、addprefix 四个函数配合使用,把源文件到目标文件的映射处理得干干净净。你想想看,如果没有这些函数,你得写多少shell代码才能实现同样的效果?
$(info OBJ = $(OBJ)) 来打印变量值,看看函数执行结果是不是你想要的。调试Makefile,打印大法最管用。
好了,今天这几个函数,你只要记住:字符串替换找 subst 和 patsubst,清理空格用 strip,路径拆分用 dir、notdir、basename。下次写Makefile时,遇到字符串或路径处理,先想想能不能用这些函数搞定,别急着写shell脚本。