第27章:字符串操作:string()命令的查找、替换、拼接、大小写转换
说实话,很多C++程序员第一次接触CMake时,都觉得它就是个“高级版的Makefile生成器”。但用久了你会发现,CMake其实内置了一套相当完整的脚本语言,字符串处理能力就是其中很实用的一块。
我个人习惯把CMake的字符串操作分成两类:一类是变量拼接,用set()就能搞定;另一类是内容处理,比如查找子串、替换、大小写转换,这些就得靠string()命令了。今天咱们就把它掰开揉碎了讲清楚。
27.1 string()命令的“全家福”
先看个总览。string()命令在CMake里是个多面手,它支持的操作模式有十几种。但咱们今天只聚焦最常用的五个:查找、替换、拼接、大小写转换、去空格。其他的像正则匹配、哈希计算,等用到时再查手册也不迟。
核心语法:
string(操作模式 输出变量 输入参数...)
所有string()命令都遵循这个结构:第一个参数告诉CMake你要干什么,第二个参数是存结果的变量名,后面跟着输入数据。
嗯,这里要注意:输出变量是必须的,哪怕你只是想检查一下结果,也得给它个“容器”。
27.2 字符串查找:FIND 和 REGEX MATCH
查找子串,最常用的是 string(FIND)。它返回子串在原串中的位置索引,从0开始。如果没找到,返回-1。
set(MY_STRING "Hello, CMake World!")
string(FIND "${MY_STRING}" "CMake" POS)
message("'CMake' 的位置: ${POS}") # 输出: 7
string(FIND "${MY_STRING}" "Python" POS2)
message("'Python' 的位置: ${POS2}") # 输出: -1
我在项目中遇到过一个问题:用FIND查找时,CMake是区分大小写的。你搜"cmake"就找不到"CMake"。如果你需要忽略大小写,就得先用 string(TOLOWER) 把两边都转成小写再查。
小技巧:FIND还支持反向查找,加个 REVERSE 关键字就行。比如你想找最后一个斜杠的位置,用来提取文件名。
string(FIND "/usr/local/bin/cmake" "/" POS REVERSE)
message("最后一个斜杠位置: ${POS}") # 输出: 14
如果你需要更灵活的匹配,比如模糊查找,那就用 string(REGEX MATCH)。它支持正则表达式。
set(TEXT "版本号: v3.27.0-rc1")
string(REGEX MATCH "v[0-9]+\\.[0-9]+" VERSION "${TEXT}")
message("匹配到的版本: ${VERSION}") # 输出: v3.27
为什么会这样?因为REGEX MATCH只返回第一个匹配。如果你想拿到所有匹配,得用 REGEX MATCHALL,结果会存成一个列表。
27.3 字符串替换:REPLACE 和 REGEX REPLACE
替换操作在构建脚本里太常见了。比如你要把源码里的占位符替换成实际路径,或者清理路径分隔符。
普通替换:
set(SOURCE "Hello, World!")
string(REPLACE "World" "CMake" RESULT "${SOURCE}")
message("替换后: ${RESULT}") # 输出: Hello, CMake!
注意,REPLACE是替换所有匹配,不是只替换第一个。如果你只想替换一次,那就得用正则的 REGEX REPLACE 配合锚点。
正则替换:
set(PATH "src/main.cpp;src/utils.cpp;src/main.cpp")
string(REGEX REPLACE "main\\.cpp" "core.cpp" RESULT "${PATH}")
message("替换后: ${RESULT}") # 两个 main.cpp 都被替换了
我曾经踩过一个坑:在Windows上,路径分隔符是反斜杠 \,而CMake里反斜杠是转义字符。用REPLACE替换路径时,你得写成 \\\\ 才能表示一个反斜杠。后来我学乖了,统一用 / 处理,最后再转。
避坑指南:我曾经在替换CMake变量时,忘了对 ${} 做转义。比如你想把字符串里的 ${VAR} 替换成实际值,直接写 string(REPLACE "${VAR}" ...) 是不行的——CMake会先展开 ${VAR}。正确做法是用 \\${VAR} 或者用 set() 先存一下。
27.4 字符串拼接:APPEND 和 CONCAT
拼接字符串,新手最容易想到的是 set() 直接拼:
set(A "Hello, ")
set(B "World!")
set(C "${A}${B}")
这没问题。但CMake提供了更优雅的方式:string(APPEND) 和 string(CONCAT)。
APPEND:追加到已有变量
set(MSG "错误: ")
string(APPEND MSG "文件未找到")
string(APPEND MSG " (路径: /tmp/test.txt)")
message("${MSG}") # 输出: 错误: 文件未找到 (路径: /tmp/test.txt)
APPEND会直接修改原变量,不需要再赋值回去。这在循环里特别方便。
CONCAT:拼接多个字符串
string(CONCAT RESULT "Hello" ", " "World" "!")
message("${RESULT}") # 输出: Hello, World!
CONCAT和直接 ${A}${B} 的区别在于:CONCAT会忽略空变量,而直接拼接会把空变量变成空字符串。不过大多数情况下,两者效果一样。
我建议:如果你只是简单拼接两个变量,用 set() 就够了。如果你要拼接多个片段,或者在一个循环里不断追加,用 string(APPEND) 或 string(CONCAT) 代码更清晰。
27.5 大小写转换:TOLOWER 和 TOUPPER
这个最简单,但也很实用。比如你要比较用户输入的选项,或者统一处理文件名后缀。
set(INPUT "CMakeLists.txt")
string(TOLOWER "${INPUT}" LOWER)
string(TOUPPER "${INPUT}" UPPER)
message("小写: ${LOWER}") # 输出: cmakelists.txt
message("大写: ${UPPER}") # 输出: CMAKELISTS.TXT
注意:CMake的大小写转换只处理ASCII字符。如果你有中文或带重音符号的字符,它不会变。不过大多数场景下够用了。
我在项目中常用这个来统一处理编译选项:
set(BUILD_TYPE "Debug")
string(TOUPPER "${BUILD_TYPE}" BUILD_TYPE_UPPER)
if(BUILD_TYPE_UPPER STREQUAL "DEBUG")
message("开启调试模式")
endif()
这样用户输入 debug、Debug、DEBUG 都能正确匹配。
27.6 其他实用操作:STRIP 和 LENGTH
虽然不在标题里,但这两个我经常搭配使用,顺便提一下。
STRIP:去除首尾空格
set(INPUT " Hello, CMake! ")
string(STRIP "${INPUT}" RESULT)
message("去空格后: '${RESULT}'") # 输出: 'Hello, CMake!'
这个在解析用户输入或读取文件内容时特别有用。你想想看,如果用户不小心多打了个空格,你的比较逻辑就崩了。
LENGTH:获取字符串长度
string(LENGTH "CMake" LEN)
message("长度: ${LEN}") # 输出: 5
嗯,这个没什么好说的,就是数一下字符数。注意是字符数,不是字节数。对于ASCII字符,两者一样;对于中文,一个汉字算一个字符。
27.7 知识体系总览
为了让你对本章内容有个整体印象,我画了一张图:
27.8 实战组合:一个完整的例子
光讲理论没意思。咱们来写一个实际场景:解析一个版本号字符串,提取主版本、次版本、修订号,并统一转成大写。
set(VERSION_STRING " v3.27.0-rc2 ")
# 1. 去空格
string(STRIP "${VERSION_STRING}" VERSION_CLEAN)
# 2. 提取数字部分(去掉 v 和 -rc2)
string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" VERSION_NUM "${VERSION_CLEAN}")
# 3. 按点号分割(用替换把点号换成空格,再转成列表)
string(REPLACE "." ";" VERSION_PARTS "${VERSION_NUM}")
list(GET VERSION_PARTS 0 MAJOR)
list(GET VERSION_PARTS 1 MINOR)
list(GET VERSION_PARTS 2 PATCH)
# 4. 拼接成完整版本标识
string(CONCAT FULL_VERSION "V" "${MAJOR}" "." "${MINOR}" "." "${PATCH}")
# 5. 转大写
string(TOUPPER "${FULL_VERSION}" FULL_VERSION_UPPER)
message("原始: ${VERSION_STRING}")
message("清理后: ${VERSION_CLEAN}")
message("版本号: ${VERSION_NUM}")
message("主版本: ${MAJOR}, 次版本: ${MINOR}, 修订号: ${PATCH}")
message("完整标识: ${FULL_VERSION_UPPER}")
输出结果:
原始: v3.27.0-rc2
清理后: v3.27.0-rc2
版本号: 3.27.0
主版本: 3, 次版本: 27, 修订号: 0
完整标识: V3.27.0
你看,几个string()命令组合起来,就能完成一个完整的版本解析逻辑。我在实际项目中经常用这个套路来处理第三方库的版本号。
核心要点回顾:
- 查找用
FIND(精确)或REGEX MATCH(模糊) - 替换用
REPLACE(全部替换)或REGEX REPLACE(正则) - 拼接用
APPEND(追加)或CONCAT(合并) - 大小写转换用
TOLOWER和TOUPPER - 去空格用
STRIP,取长度用LENGTH
好了,这一章的内容就到这里。字符串操作虽然基础,但用好了能省不少事。下次你写CMake脚本时,遇到字符串处理,不妨想想今天讲的这几个命令——说不定能让你少写好几行代码。
公众号:蓝海资料掘金营,微信deep3321