3、Makefile函数应用:字符串处理函数、文件名函数与条件判断

说实话,Makefile 里最容易被忽略的就是函数。很多人写 Makefile 就是堆变量、写规则,遇到复杂点的路径处理就傻眼了。我早年带团队时,见过一个项目,光路径拼接就写了三十多行重复代码——其实几个函数就能搞定。

这一节,咱们把 Makefile 里最常用的三类函数掰开揉碎讲清楚。字符串处理、文件名操作、条件判断,这三板斧用好了,你的 Makefile 能瘦身一半。

3.1 字符串处理函数

字符串处理是 Makefile 里最基础也最常用的。说白了,就是让你能灵活地操作文本内容。

3.1.1 subst —— 文本替换

subst 函数做的是简单的字符串替换。它的语法是:

$(subst 被替换的字符串, 替换成的字符串, 原始文本)

举个例子:

SRC := main.c utils.c driver.c
OBJ := $(subst .c,.o,$(SRC))
# OBJ 的值是 main.o utils.o driver.o

我在项目中遇到过一个问题:有个同事用 sed 在 Makefile 里做替换,结果不同平台上的 sed 行为不一致,导致编译失败。换成 subst 后,干干净净,跨平台无压力。

小技巧:subst 是精确匹配替换,不会动到子串。比如 "abc.c" 里的 ".c" 会被替换,但 "abc.c.bak" 里的第一个 ".c" 也会被替换——注意这个行为。

3.1.2 patsubst —— 模式替换

patsubst 比 subst 更灵活。它支持通配符 %,可以匹配任意长度的字符串。

$(patsubst 模式, 替换模式, 文本列表)

看个实际例子:

SRC := src/main.c src/utils.c src/driver.c
OBJ := $(patsubst src/%.c, build/%.o, $(SRC))
# OBJ 的值是 build/main.o build/utils.o build/driver.o

嗯,这里要注意:% 匹配的部分会被保留,然后塞到替换模式里。这个特性在做目录映射时特别有用。

核心区别:subst 是死板替换,patsubst 是智能替换。如果你需要保留部分内容,用 patsubst;如果只是简单替换,用 subst 就够了。

3.1.3 strip —— 去空格

strip 函数的作用很简单:去掉字符串首尾的空格,并把中间连续的空格压缩成一个。

STR := "   main.c    utils.c   driver.c   "
CLEAN := $(strip $(STR))
# CLEAN 的值是 "main.c utils.c driver.c"

你想想看,如果从某个配置文件里读出来的路径带了一堆多余空格,编译时就会报"文件找不到"。用 strip 处理一下,问题就解决了。

3.2 文件名函数

文件名函数是专门用来处理文件路径的。我当年从 Windows 转到 Linux 做嵌入式开发时,最头疼的就是路径分隔符。Makefile 里这些函数帮我省了不少事。

3.2.1 dir —— 取目录

dir 从文件路径中提取目录部分:

$(dir src/main.c)      # 返回 "src/"
$(dir /home/user/a.c)  # 返回 "/home/user/"

3.2.2 notdir —— 取文件名

notdir 和 dir 正好相反,它只保留文件名:

$(notdir src/main.c)      # 返回 "main.c"
$(notdir /home/user/a.c)  # 返回 "a.c"

3.2.3 basename —— 取主文件名

basename 去掉文件扩展名:

$(basename main.c)        # 返回 "main"
$(basename src/utils.o)   # 返回 "src/utils"

3.2.4 suffix —— 取扩展名

suffix 提取文件后缀:

$(suffix main.c)          # 返回 ".c"
$(suffix src/utils.o)     # 返回 ".o"
我曾经踩过的坑:basename 和 suffix 在处理多后缀文件时要小心。比如 "main.c.bak",basename 返回 "main.c",suffix 返回 ".bak"。如果你想要去掉所有后缀,得自己写逻辑。

这几个函数组合起来用,威力很大。比如自动生成依赖文件时:

SRC := src/main.c src/utils.c
OBJ := $(patsubst %.c, %.o, $(SRC))
DEP := $(patsubst %.o, %.d, $(OBJ))
# 或者一步到位:
DEP := $(addsuffix .d, $(basename $(SRC)))

3.3 条件判断函数

条件判断让 Makefile 有了"脑子"。不同平台、不同配置下,可以执行不同的逻辑。

3.3.1 if 系列函数

Makefile 里有三种条件判断写法:

# 方式一:条件关键字
ifeq ($(OS),Linux)
    CC := gcc
else
    CC := arm-none-eabi-gcc
endif

# 方式二:if 函数
CC = $(if $(filter Linux,$(OS)), gcc, arm-none-eabi-gcc)

# 方式三:条件赋值
CC ?= gcc  # 如果 CC 没定义过,才赋值

我个人习惯用 if 函数,因为它可以嵌在表达式里,写起来更紧凑。但如果是多分支判断,还是 ifeq/ifneq 更清晰。

3.3.2 and/or 函数

and 和 or 用于组合多个条件:

# 两个条件都满足才执行
$(and $(CONFIG_A), $(CONFIG_B))

# 任一条件满足就执行
$(or $(CONFIG_A), $(CONFIG_B))

举个例子,我在做交叉编译时经常这么用:

# 只有定义了 CROSS_COMPILE 且不是空字符串,才用交叉编译器
ifneq ($(strip $(CROSS_COMPILE)),)
    CC = $(CROSS_COMPILE)gcc
else
    CC = gcc
endif

3.4 知识体系总览

下面这张图把本章的核心内容串起来了。你可以把它当作一个速查表:

Makefile 函数应用知识体系 字符串处理函数 文件名函数 条件判断函数 subst:精确替换 patsubst:模式替换(支持 %) strip:去除多余空格 dir:取目录路径 notdir:取文件名 basename:去扩展名 suffix:取扩展名 ifeq / ifneq:条件关键字 if 函数:内联条件 and / or:组合条件 ?=:条件赋值 💡 核心思路 字符串处理 → 文本变换 | 文件名函数 → 路径解析 | 条件判断 → 分支控制 三者结合,可以写出高度可复用、跨平台的 Makefile

3.5 实战组合拳

光讲理论没意思。咱们看一个实际场景:自动收集源码、生成目标文件、根据平台选择编译器。

# 自动收集所有 .c 文件
SRC_DIR := src
SRC := $(wildcard $(SRC_DIR)/*.c)

# 生成目标文件路径(保持目录结构)
OBJ_DIR := build
OBJ := $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))

# 根据平台选择编译器
ifeq ($(OS),Windows_NT)
    CC := gcc
    RM := del /Q
else
    CC := gcc
    RM := rm -f
endif

# 生成依赖文件
DEP := $(patsubst %.o, %.d, $(OBJ))

# 编译规则
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
    @mkdir -p $(dir $@)
    $(CC) -c $< -o $@

# 包含依赖文件
-include $(DEP)

这段代码里,wildcard、patsubst、dir、条件判断全用上了。你想想看,如果没有这些函数,你得写多少行?

我的建议:写 Makefile 时,先想清楚"我要处理什么数据",然后找对应的函数。别急着写死字符串。用函数处理,后期维护时改一处就行。

好了,这一节的内容就到这儿。函数这东西,光看没用,得动手敲。下次写 Makefile 时,试着用 patsubst 替换掉你手写的路径拼接,你会爱上它的。

可可精品资料库