第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()

这样用户输入 debugDebugDEBUG 都能正确匹配。

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 知识体系总览

为了让你对本章内容有个整体印象,我画了一张图:

string() 命令核心操作 string() 命令 🔍 查找 (FIND) 🔄 替换 (REPLACE) 🔗 拼接 (APPEND/CONCAT) 🔤 大小写转换 ✂️ 去空格/长度 FIND / REGEX MATCH REPLACE / REGEX REPLACE APPEND / CONCAT TOLOWER / TOUPPER STRIP / LENGTH 所有操作都遵循: string(操作模式 输出变量 输入参数...)

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(合并)
  • 大小写转换用 TOLOWERTOUPPER
  • 去空格用 STRIP,取长度用 LENGTH

好了,这一章的内容就到这里。字符串操作虽然基础,但用好了能省不少事。下次你写CMake脚本时,遇到字符串处理,不妨想想今天讲的这几个命令——说不定能让你少写好几行代码。


公众号:蓝海资料掘金营,微信deep3321