4. Makefile函数:字符串处理与文件名操作
说实话,Makefile 里的函数,很多人一开始是抗拒的。我当年也一样——觉得变量赋值就够用了,搞什么函数?直到有一次,我维护一个上百个源文件的项目,手动写规则写到崩溃……嗯,从那以后,我再也不敢小看 Makefile 函数了。
这一节,咱们就聊聊最常用的两类函数:字符串处理函数和文件名函数。它们能帮你省掉大量重复劳动,说白了就是「让 Makefile 自己动脑子」。
4.1 字符串处理函数
字符串处理,在 Makefile 里其实很常见。比如你有一个变量存着多个文件名,想批量改后缀、去空格、或者做替换。这时候函数就派上用场了。
4.1.1 subst —— 简单的字符串替换
subst 是「substitute」的缩写,功能就是做文本替换。它的语法是:
$(subst 被替换的字符串, 替换成的字符串, 原始文本)
举个例子:
SRC = hello.c world.c
OBJ = $(subst .c,.o,$(SRC))
# OBJ 的值是 hello.o world.o
我在项目中经常用它来批量修改文件后缀。比如把 .cpp 换成 .o,或者把 .h 换成 .h.gch(预编译头文件)。
4.1.2 patsubst —— 模式匹配替换
patsubst 是「pattern substitution」的缩写。它支持通配符 %,可以匹配任意字符。语法是:
$(patsubst 模式, 替换模式, 原始文本)
看个例子:
SRC = src/main.c src/utils.c include/header.h
OBJ = $(patsubst %.c,%.o,$(SRC))
# OBJ 的值是 src/main.o src/utils.o include/header.h
# 注意:.h 文件没有被匹配到
我个人习惯用 patsubst 而不是 subst,因为它更灵活。比如你想只替换 .c 文件,而保留 .h 文件不变——subst 做不到,但 patsubst 可以。
4.1.3 strip —— 去除多余空格
strip 函数很简单,就是把字符串首尾和中间多余的空格去掉。语法:
$(strip 字符串)
比如:
VAR = hello world !
CLEAN = $(strip $(VAR))
# CLEAN 的值是 "hello world !"
你可能会问:这玩意儿有什么用?嗯,我在写自动化构建脚本时,经常从外部文件读取配置,那些文件里可能有多余的空格。用 strip 处理一下,能避免很多莫名其妙的错误。
4.2 文件名函数
文件名函数,说白了就是帮你拆解文件路径的。比如你想知道一个文件在哪个目录、叫什么名字、有没有后缀——这些函数都能搞定。
4.2.1 dir —— 提取目录部分
dir 函数返回文件路径中的目录部分,包括最后一个斜杠。语法:
$(dir 文件名列表)
例子:
FILE = src/utils/helper.c
DIR = $(dir $(FILE))
# DIR 的值是 "src/utils/"
我在项目中常用它来生成目标文件的输出目录。比如:
OBJS = obj/main.o obj/utils.o
$(OBJS): obj/%.o: src/%.c
mkdir -p $(dir $@)
gcc -c $< -o $@
这样每次编译前自动创建目录,省得手动 mkdir。
4.2.2 notdir —— 提取文件名部分
notdir 和 dir 正好相反,它去掉目录部分,只保留文件名。语法:
$(notdir 文件名列表)
例子:
FILE = src/utils/helper.c
NAME = $(notdir $(FILE))
# NAME 的值是 "helper.c"
你想想看,如果你有一个变量存着所有源文件的完整路径,但你想生成对应的目标文件名——用 notdir 先提取文件名,再用 patsubst 改后缀,两步搞定。
4.2.3 basename —— 去掉后缀
basename 返回文件名去掉最后一个后缀的部分。语法:
$(basename 文件名列表)
例子:
FILE = helper.c
BASE = $(basename $(FILE))
# BASE 的值是 "helper"
FILE2 = archive.tar.gz
BASE2 = $(basename $(FILE2))
# BASE2 的值是 "archive.tar"
注意:它只去掉最后一个点后面的内容。如果你有 .tar.gz 这种双后缀,它只会去掉 .gz。
%.d: %.c
gcc -MM $< > $(basename $@).d
4.2.4 addsuffix —— 添加后缀
addsuffix 给每个文件名后面加上指定的后缀。语法:
$(addsuffix 后缀, 文件名列表)
例子:
FILES = main utils helper
OBJS = $(addsuffix .o, $(FILES))
# OBJS 的值是 "main.o utils.o helper.o"
这个函数和 patsubst 有点像,但更直接。如果你只是单纯想加后缀,用 addsuffix 更简洁。
4.3 知识体系总览
为了让你更直观地理解这些函数的关系,我画了一张图:
4.4 综合实战:一个完整的例子
说了这么多,不如来一个真实的场景。假设你有一个项目,源文件分布在多个子目录里:
SRC = src/main.c src/utils.c lib/core.c lib/helper.c
INC = include/main.h include/utils.h
你想生成对应的目标文件,并且把目标文件统一放到 build/ 目录下。用我们学到的函数,可以这样写:
# 提取所有 .c 文件的文件名(不带目录)
C_FILES = $(notdir $(SRC))
# C_FILES = main.c utils.c core.c helper.c
# 把 .c 换成 .o
OBJ_NAMES = $(patsubst %.c,%.o,$(C_FILES))
# OBJ_NAMES = main.o utils.o core.o helper.o
# 加上 build/ 前缀
OBJS = $(addprefix build/,$(OBJ_NAMES))
# OBJS = build/main.o build/utils.o build/core.o build/helper.o
# 编译规则
$(OBJS): build/%.o: src/%.c
mkdir -p $(dir $@)
gcc -c $< -o $@
# 注意:上面的规则只匹配 src/ 下的文件
# 对于 lib/ 下的文件,需要额外写规则
你看,用函数组合起来,代码变得非常简洁。而且一旦源文件列表变了,Makefile 会自动适应,不需要你手动修改。
4.5 避坑指南
最后,分享几个我踩过的坑:
- 空格问题:Makefile 对空格很敏感。比如
$(subst .c, .o, $(SRC))和$(subst .c,.o,$(SRC))结果可能不同。我建议所有逗号后面不要加空格。 - 通配符陷阱:
patsubst的%只能匹配一个「词」。如果文件名里有空格,它会把空格前后的部分当成两个词处理。 - 函数嵌套:Makefile 支持函数嵌套,比如
$(dir $(notdir $(SRC)))。但嵌套太多会降低可读性,我一般控制在两层以内。
好了,这一节的内容就到这里。字符串处理和文件名操作是 Makefile 里最常用的函数,掌握了它们,你的 Makefile 会变得既简洁又强大。下一节我们继续聊其他实用函数,敬请期待。