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 是全量替换。只要匹配到就换,不会只换第一个。我在项目中遇到过有人拿它去替换路径分隔符,结果把不该换的地方也换了,排查了半天。

避坑指南:subst 是简单文本替换,不支持通配符。如果你需要模式匹配,请用 patsubst。

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去搞,得写好几层循环。

小技巧:patsubst 的简写形式是 $(var:pattern=replacement)。比如 $(SRC:%.c=%.o) 等价于 $(patsubst %.c,%.o,$(SRC))。我写Makefile时更喜欢用简写,代码更紧凑。

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函数分类和关系。你看一眼就能明白今天讲的这些函数在整体中的位置。

Makefile 函数分类(本章重点) 字符串处理函数 subst 简单文本替换(全量) $(subst .c,.o,main.c) → main.o patsubst 模式替换(支持 % 通配符) $(patsubst %.c,%.o,main.c) → main.o strip 去除首尾及多余空格 $(strip " a b ") → "a b" 文件名处理函数 dir 提取目录路径 $(dir src/main.c) → src/ notdir 提取文件名(去目录) $(notdir src/main.c) → main.c basename 去掉最后一个后缀 $(basename main.c) → main 核心原则:字符串处理用 subst/patsubst/strip,路径处理用 dir/notdir/basename

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代码才能实现同样的效果?

我的建议:刚开始接触这些函数时,可以在Makefile里加一行 $(info OBJ = $(OBJ)) 来打印变量值,看看函数执行结果是不是你想要的。调试Makefile,打印大法最管用。

好了,今天这几个函数,你只要记住:字符串替换找 subst 和 patsubst,清理空格用 strip,路径拆分用 dir、notdir、basename。下次写Makefile时,遇到字符串或路径处理,先想想能不能用这些函数搞定,别急着写shell脚本。

可可精品资料库