4. 列表与字符串操作:list()命令详解、字符串处理string()命令、正则表达式在CMake中的应用
说实话,很多CMake初学者都会忽略列表和字符串操作。他们觉得:「不就是变量赋值吗?有什么好学的?」
嗯,我以前也这么想。直到有一次,我需要处理一个包含上百个源文件的跨平台项目,手动拼接路径、过滤文件类型、处理编译器标志……那叫一个痛苦。后来我认真啃了一遍CMake的list()和string()命令,才发现原来这些工具能省下我80%的重复劳动。
今天我们就来彻底搞懂它们。你想想看,掌握了这些,你的CMakeLists.txt会变得又短又清晰。
核心观点:CMake中的列表本质上是分号分隔的字符串。所有列表操作,底层都是字符串操作。理解这一点,后面的内容就顺了。
4.1 list()命令:列表的增删改查
先看一个最简单的例子。我在项目中经常需要维护一个源文件列表:
set(SOURCES
src/main.cpp
src/utils.cpp
src/network.cpp
)
# 追加文件
list(APPEND SOURCES src/config.cpp src/log.cpp)
# 获取长度
list(LENGTH SOURCES len)
message("源文件数量: ${len}") # 输出: 4
这里要注意,list(APPEND ...)可以一次追加多个元素,用空格隔开就行。我个人习惯把每个文件单独一行,这样diff时看得清楚。
4.1.1 常用子命令一览
| 子命令 | 作用 | 示例 |
|---|---|---|
| APPEND | 追加元素到末尾 | list(APPEND mylist a b c) |
| INSERT | 在指定位置插入 | list(INSERT mylist 1 x y) |
| REMOVE_ITEM | 删除指定值 | list(REMOVE_ITEM mylist a) |
| REMOVE_AT | 删除指定索引 | list(REMOVE_AT mylist 0 2) |
| FIND | 查找元素索引 | list(FIND mylist "value" idx) |
| GET | 获取指定索引的值 | list(GET mylist 0 2 out) |
| JOIN | 用分隔符合并成字符串 | list(JOIN mylist "," out) |
| SUBLIST | 截取子列表 | list(SUBLIST mylist 1 3 out) |
我的小技巧:调试时我经常用 message("list: ${mylist}") 来查看列表内容。CMake会以分号分隔的形式打印出来,一目了然。
4.1.2 避坑:空列表与未定义变量
我曾经踩过一个坑:如果一个变量从未被定义,直接对它做list(APPEND ...)会怎样?
# 假设 MY_LIST 从未被 set 过
list(APPEND MY_LIST "hello")
message("${MY_LIST}") # 输出: hello
CMake会自动把未定义的变量当作空列表处理。这很方便,但也容易隐藏bug。我建议在关键位置用if(DEFINED MY_LIST)做一次检查。
4.2 string()命令:字符串的十八般武艺
string()命令比list()更强大。它几乎覆盖了你日常需要的所有字符串操作:查找、替换、大小写转换、去除空白、正则匹配……
我最早接触string()是在处理编译器版本号的时候。当时需要从CMAKE_CXX_COMPILER_VERSION中提取主版本号,用string(REGEX ...)一行就搞定了。
4.2.1 常用子命令
| 子命令 | 说明 | 示例 |
|---|---|---|
| TOLOWER / TOUPPER | 大小写转换 | string(TOLOWER "Hello" out) |
| LENGTH | 获取字符串长度 | string(LENGTH "abc" len) |
| SUBSTRING | 截取子串 | string(SUBSTRING "hello" 1 3 out) |
| STRIP | 去除首尾空白 | string(STRIP " abc " out) |
| REPLACE | 替换子串 | string(REPLACE "old" "new" input out) |
| FIND | 查找子串位置 | string(FIND "hello" "ll" pos) |
| REGEX MATCH | 正则匹配 | string(REGEX MATCH "[0-9]+" input out) |
| REGEX REPLACE | 正则替换 | string(REGEX REPLACE "\\s+" " " input out) |
注意:CMake中的正则表达式使用POSIX扩展语法。反斜杠需要转义,比如 \\s 表示空白字符。我第一次写的时候忘了双反斜杠,排查了半天……
4.2.2 实战:处理路径与文件名
在跨平台项目中,路径处理是个大麻烦。Windows用反斜杠,Linux用正斜杠。还好CMake提供了cmake_path()命令(3.20+),但如果你还在用老版本,string()也能搞定:
set(FULL_PATH "/home/user/project/src/main.cpp")
# 提取文件名
string(REGEX MATCH "[^/]+$" FILENAME ${FULL_PATH})
message("文件名: ${FILENAME}") # main.cpp
# 提取目录
string(REGEX REPLACE "/[^/]+$" "" DIR ${FULL_PATH})
message("目录: ${DIR}") # /home/user/project/src
# 替换后缀
string(REGEX REPLACE "\\.cpp$" ".h" HEADER ${FULL_PATH})
message("对应头文件: ${HEADER}") # /home/user/project/src/main.h
我个人习惯把这类操作封装成函数,放在一个utils.cmake里,方便复用。
4.3 正则表达式在CMake中的应用
正则表达式是string()命令中最强大的部分。它让你能灵活地匹配和提取文本模式。
为什么会需要正则?举个例子:你想从编译器的输出版本中提取数字部分。用string(FIND)加string(SUBSTRING)当然可以,但代码会变得又臭又长。正则一行搞定。
4.3.1 基本语法
CMake的正则支持以下常用结构:
.— 匹配任意单个字符*— 匹配前一个字符0次或多次+— 匹配前一个字符1次或多次?— 匹配前一个字符0次或1次[...]— 字符集,如[0-9]匹配数字^— 行首$— 行尾()— 分组捕获
重要:CMake中正则匹配默认是「部分匹配」,只要字符串中某一部分符合模式就算匹配成功。如果你需要「完全匹配」,记得加上 ^ 和 $。
4.3.2 捕获组的使用
我最常用的场景是从版本号中提取主版本、次版本:
set(VERSION_STR "3.27.5")
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" _ ${VERSION_STR})
# 注意:CMAKE_MATCH_1, CMAKE_MATCH_2, CMAKE_MATCH_3 存储捕获组
message("主版本: ${CMAKE_MATCH_1}") # 3
message("次版本: ${CMAKE_MATCH_2}") # 27
message("补丁版本: ${CMAKE_MATCH_3}") # 5
这里有个坑:CMAKE_MATCH_<n> 变量只在当前作用域有效。如果你在函数里调用,函数结束后这些变量就没了。我曾经因为这个在子目录里拿不到匹配结果,折腾了好久。
4.3.3 正则替换实战
假设你有一个包含空格和特殊字符的字符串,想把它变成合法的C++标识符:
set(INPUT "My Awesome Library v2.0!")
# 替换非字母数字下划线为下划线
string(REGEX REPLACE "[^a-zA-Z0-9_]" "_" SANITIZED ${INPUT})
message("${SANITIZED}") # My_Awesome_Library_v2_0_
嗯,这里要注意末尾可能多出一个下划线。你可以再加一步string(REGEX REPLACE "_+$" "" SANITIZED ${SANITIZED})去掉它。
4.4 知识体系总览
下面这张图总结了本章的核心内容。你可以把它当作一个快速参考:
4.5 综合示例:构建一个跨平台源文件过滤器
最后,我把今天讲的内容串起来,写一个实用的例子。假设你有一个项目,需要根据平台和编译器过滤源文件:
# 定义所有源文件
set(ALL_SOURCES
src/core.cpp
src/win/win_specific.cpp
src/linux/linux_specific.cpp
src/test/test_main.cpp
)
# 1. 过滤掉测试文件(发布构建时)
if(NOT BUILD_TESTING)
list(FILTER ALL_SOURCES EXCLUDE REGEX ".*/test/.*")
endif()
# 2. 根据平台保留特定文件
if(WIN32)
list(FILTER ALL_SOURCES INCLUDE REGEX ".*/win/.*|.*/core.*")
elseif(UNIX)
list(FILTER ALL_SOURCES INCLUDE REGEX ".*/linux/.*|.*/core.*")
endif()
# 3. 检查是否有重复文件
list(REMOVE_DUPLICATES ALL_SOURCES)
# 4. 生成编译单元列表
string(REPLACE "src/" "obj/" OBJECT_LIST "${ALL_SOURCES}")
string(REGEX REPLACE "\\.cpp$" ".o" OBJECT_LIST "${OBJECT_LIST}")
message("最终源文件列表: ${ALL_SOURCES}")
message("对应目标文件: ${OBJECT_LIST}")
你看,list(FILTER ... REGEX ...) 结合了列表操作和正则表达式,一行就能完成复杂的过滤逻辑。这在处理大型项目时特别有用。
我的建议:把常用的列表/字符串操作封装成宏或函数。比如我写了一个 sanitize_filename() 函数,专门用来把任意字符串转成合法的文件名。这样整个项目都能复用,而且改一处就行。
好了,关于列表和字符串操作就讲到这里。记住:list() 管结构,string() 管内容,正则表达式是它们之间的桥梁。掌握了这三样,你的CMake脚本会变得既简洁又强大。