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(预编译头文件)。

小提示:subst 是严格匹配,不会做通配符匹配。如果你想匹配模式,得用 patsubst。

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 可以。

注意: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。

实战技巧:我经常用 basename 来生成依赖文件(.d 文件)的名字。比如:
%.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 知识体系总览

为了让你更直观地理解这些函数的关系,我画了一张图:

Makefile 函数分类 字符串处理函数 subst 简单文本替换 patsubst 模式匹配替换(支持%) strip 去除多余空格 文件名函数 dir 提取目录部分 notdir 提取文件名部分 basename 去掉后缀 addsuffix 添加后缀

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 会自动适应,不需要你手动修改。

核心思路:Makefile 函数的真正威力在于「组合使用」。单个函数可能看起来平平无奇,但把它们串起来,就能实现很强大的功能。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 空格问题:Makefile 对空格很敏感。比如 $(subst .c, .o, $(SRC)) 和 $(subst .c,.o,$(SRC)) 结果可能不同。我建议所有逗号后面不要加空格。
  • 通配符陷阱:patsubst 的 % 只能匹配一个「词」。如果文件名里有空格,它会把空格前后的部分当成两个词处理。
  • 函数嵌套:Makefile 支持函数嵌套,比如 $(dir $(notdir $(SRC)))。但嵌套太多会降低可读性,我一般控制在两层以内。

好了,这一节的内容就到这里。字符串处理和文件名操作是 Makefile 里最常用的函数,掌握了它们,你的 Makefile 会变得既简洁又强大。下一节我们继续聊其他实用函数,敬请期待。

可可精品资料库