4. 列表与字符串操作:list()命令详解、字符串操作、正则表达式匹配、文件路径操作
说实话,CMake 里最容易被低估的两个东西,就是列表和字符串。很多人觉得 CMake 就是个构建系统,能写变量、调库就够了。但我在实际项目中踩过不少坑,发现如果不懂列表和字符串操作,写出来的 CMakeLists.txt 要么又臭又长,要么根本跑不通。
这一章,我们就来彻底搞懂它们。
核心观点:CMake 中一切皆字符串,列表只是分号分隔的字符串。理解这一点,后面所有操作都顺了。
4.1 list() 命令详解
我个人习惯把 list() 当成 CMake 里的“数组”来用。虽然它本质上还是字符串,但操作起来非常顺手。
4.1.1 基本操作:增删改查
先看几个最常用的子命令:
# 创建一个列表
set(MY_LIST "a" "b" "c")
# 获取长度
list(LENGTH MY_LIST len)
message("长度: ${len}") # 输出: 3
# 获取某个索引的值(从0开始)
list(GET MY_LIST 1 item)
message("索引1: ${item}") # 输出: b
# 追加元素
list(APPEND MY_LIST "d" "e")
message("追加后: ${MY_LIST}") # 输出: a;b;c;d;e
# 在开头插入
list(INSERT MY_LIST 0 "z")
message("插入后: ${MY_LIST}") # 输出: z;a;b;c;d;e
# 删除元素
list(REMOVE_ITEM MY_LIST "b")
message("删除b后: ${MY_LIST}") # 输出: z;a;c;d;e
# 按索引删除
list(REMOVE_AT MY_LIST 0)
message("删除索引0后: ${MY_LIST}") # 输出: a;c;d;e
嗯,这里要注意:REMOVE_ITEM 是按值删除,REMOVE_AT 是按索引删除。我曾经在项目里搞混过,结果删错了文件列表,编译直接报错。那场面,挺尴尬的。
4.1.2 查找与排序
查找元素在不在列表里,用 FIND:
set(FRUITS "apple" "banana" "cherry")
list(FIND FRUITS "banana" idx)
if(idx GREATER -1)
message("找到了,索引: ${idx}") # 输出: 1
endif()
排序也很简单:
set(NUMS "3" "1" "4" "1" "5" "9")
list(SORT NUMS)
message("排序后: ${NUMS}") # 输出: 1;1;3;4;5;9
默认是升序。想降序?加个 COMPARE 参数就行。不过说实话,我很少在 CMake 里做排序,更多是处理源文件列表时用 FILTER 做过滤。
4.1.3 列表去重与反转
set(DUP "a" "b" "a" "c" "b")
list(REMOVE_DUPLICATES DUP)
message("去重后: ${DUP}") # 输出: a;b;c
list(REVERSE DUP)
message("反转后: ${DUP}") # 输出: c;b;a
去重这个功能,我在处理第三方库的依赖列表时经常用。有时候多个模块重复添加了同一个库,不去重的话链接阶段会报一堆警告。
4.2 字符串操作
字符串操作在 CMake 里比列表更常用。毕竟,你写的每个变量本质上都是字符串。
4.2.1 查找与替换
set(TEXT "Hello, CMake World!")
# 查找子串
string(FIND "${TEXT}" "CMake" pos)
message("CMake 的位置: ${pos}") # 输出: 7
# 替换
string(REPLACE "World" "CMake" NEW_TEXT "${TEXT}")
message("替换后: ${NEW_TEXT}") # 输出: Hello, CMake CMake!
注意:FIND 返回的是第一个匹配的位置,从0开始。如果没找到,返回 -1。我刚开始用的时候,忘了检查返回值,结果拿 -1 去截取字符串,直接崩了。
4.2.2 截取与拼接
set(PATH "/usr/local/include/foo.h")
# 截取子串:从索引5开始,取4个字符
string(SUBSTRING "${PATH}" 5 4 part)
message("子串: ${part}") # 输出: loca
# 拼接
string(CONCAT PREFIX "/usr" SUFFIX "/local")
message("拼接: ${PREFIX}") # 输出: /usr/local
# 用分隔符拼接
string(JOIN ":" PARTS "a" "b" "c")
message("JOIN: ${PARTS}") # 输出: a:b:c
我个人习惯用 CONCAT 而不是直接 set() 拼接,因为 CONCAT 不会自动加空格,更可控。
4.2.3 大小写与前后缀
set(NAME "cmake")
# 转大写
string(TOUPPER "${NAME}" UPPER)
message("大写: ${UPPER}") # 输出: CMAKE
# 转小写
string(TOLOWER "${NAME}" LOWER)
message("小写: ${LOWER}") # 输出: cmake
# 去除前后空格
string(STRIP " hello " STRIPPED)
message("去除空格: '${STRIPPED}'") # 输出: 'hello'
# 检查前后缀
if("${NAME}" MATCHES "^cm")
message("以 cm 开头")
endif()
if("${NAME}" MATCHES "ke$")
message("以 ke 结尾")
endif()
这里有个小技巧:用 STRIP 处理用户输入的路径,能避免很多因为多余空格导致的诡异问题。
4.3 正则表达式匹配
正则表达式是 CMake 字符串操作的进阶玩法。说实话,CMake 的正则语法和标准正则略有不同,但核心功能都有。
4.3.1 基本匹配
set(EMAIL "user@example.com")
# 检查是否匹配邮箱格式
string(REGEX MATCH "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" RESULT "${EMAIL}")
if(RESULT)
message("邮箱格式正确")
else()
message("邮箱格式错误")
endif()
注意:CMake 的正则里,反斜杠需要转义。比如 \\. 表示匹配一个点号。我第一次写的时候忘了转义,结果匹配了一堆乱七八糟的东西。
4.3.2 提取匹配组
set(URL "https://cmake.org/cmake/help/latest/")
# 提取协议和域名
string(REGEX MATCHALL "^(https?)://([^/]+)" MATCHES "${URL}")
if(MATCHES)
message("完整匹配: ${CMAKE_MATCH_0}") # 输出: https://cmake.org
message("协议: ${CMAKE_MATCH_1}") # 输出: https
message("域名: ${CMAKE_MATCH_2}") # 输出: cmake.org
endif()
CMAKE_MATCH_0 到 CMAKE_MATCH_9 是 CMake 自动填充的匹配组变量。这个特性我在解析编译选项时经常用,比如从 -DDEBUG=1 里提取出 DEBUG 和 1。
4.3.3 替换与过滤
set(TEXT "foo123bar456baz")
# 替换所有数字为 #
string(REGEX REPLACE "[0-9]+" "#" RESULT "${TEXT}")
message("替换后: ${RESULT}") # 输出: foo#bar#baz
# 过滤出所有数字
string(REGEX MATCHALL "[0-9]+" NUMBERS "${TEXT}")
message("数字列表: ${NUMBERS}") # 输出: 123;456
过滤功能配合 list() 使用效果更佳。比如从一堆源文件里只提取 .cpp 文件:
set(FILES "a.cpp" "b.h" "c.cpp" "d.hpp")
foreach(FILE IN LISTS FILES)
string(REGEX MATCH "\\.cpp$" MATCHED "${FILE}")
if(MATCHED)
list(APPEND CPP_FILES "${FILE}")
endif()
endforeach()
message("CPP文件: ${CPP_FILES}") # 输出: a.cpp;c.cpp
小提示:如果你需要频繁做正则匹配,建议把正则表达式定义成变量,方便复用。比如 set(CPP_REGEX "\\.cpp$"),后面直接引用。
4.4 文件路径操作
文件路径操作是 CMake 里最实用的功能之一。我敢说,90% 的 CMakeLists.txt 都会用到。
4.4.1 获取路径信息
set(FULL_PATH "/home/user/project/src/main.cpp")
# 获取文件名
get_filename_component(FILENAME "${FULL_PATH}" NAME)
message("文件名: ${FILENAME}") # 输出: main.cpp
# 获取不带扩展名的文件名
get_filename_component(BASENAME "${FULL_PATH}" NAME_WE)
message("基础名: ${BASENAME}") # 输出: main
# 获取扩展名
get_filename_component(EXT "${FULL_PATH}" EXT)
message("扩展名: ${EXT}") # 输出: .cpp
# 获取目录路径
get_filename_component(DIR "${FULL_PATH}" DIRECTORY)
message("目录: ${DIR}") # 输出: /home/user/project/src
# 获取绝对路径(相对路径转绝对)
get_filename_component(ABS_PATH "relative/path" ABSOLUTE)
message("绝对路径: ${ABS_PATH}")
我曾经在项目里用 NAME_WE 来生成目标名称。比如 main.cpp 生成 main 目标,这样就不用手动写目标名了。
4.4.2 路径拼接与规范化
# 路径拼接
set(BASE_DIR "/usr/local")
set(SUBDIR "include")
set(FILENAME "foo.h")
# 推荐用 file() 拼接
file(JOIN RESULT_PATH "${BASE_DIR}" "${SUBDIR}" "${FILENAME}")
message("拼接: ${RESULT_PATH}") # 输出: /usr/local/include/foo.h
# 路径规范化(去除 .. 和 .)
set(MESSY_PATH "/usr/local/../include/./foo.h")
file(REAL_PATH "${MESSY_PATH}" CLEAN_PATH)
message("规范化: ${CLEAN_PATH}") # 输出: /usr/include/foo.h
注意:file(REAL_PATH) 要求路径必须存在,否则会报错。如果只是做字符串级别的规范化,可以用 get_filename_component(... REALPATH)。
4.4.3 相对路径与绝对路径
set(BASE "/home/user/project")
set(TARGET "/home/user/project/src/main.cpp")
# 计算相对路径
file(RELATIVE_PATH REL "${BASE}" "${TARGET}")
message("相对路径: ${REL}") # 输出: src/main.cpp
# 判断是否为绝对路径
if(IS_ABSOLUTE "${TARGET}")
message("是绝对路径")
else()
message("不是绝对路径")
endif()
这个 RELATIVE_PATH 我在写跨平台项目时经常用。不同模块的路径不一样,用相对路径可以保持 CMakeLists.txt 的可移植性。
4.5 知识体系总览
下面这张图总结了本章的核心内容,方便你快速回顾:
4.6 避坑指南与最佳实践
最后,分享几个我踩过的坑:
坑1:列表中的空元素
我曾经在循环里往列表追加空字符串,结果列表长度变大了,但元素是空的。用 list(REMOVE_ITEM list_name "") 可以清理掉。
坑2:正则中的反斜杠
CMake 里写正则,反斜杠要双写。比如匹配点号要写 \\.,而不是 \.。我刚开始写的时候,经常因为这个匹配失败。
最佳实践:
- 路径操作优先用
file()系列命令,而不是手动拼接字符串 - 正则表达式先写在小脚本里测试,再放到 CMakeLists.txt 中
- 列表操作时,注意变量作用域。用
PARENT_SCOPE把结果传出去 - 字符串比较用
STREQUAL而不是EQUAL,后者只适用于数字
好了,这一章的内容就到这里。列表和字符串操作是 CMake 的基石,掌握好了,后面写复杂的构建脚本会顺手很多。
公众号:蓝海资料掘金营,微信deep3321