14、CMake列表与字符串:list操作、string操作、数学运算(math)
说实话,CMake 里最常用的数据类型就是字符串和列表。很多新手觉得这玩意儿简单,不就是一堆字符嘛?但我在实际项目中踩过不少坑——比如列表拼接时多了个分号,字符串替换时没注意转义,结果编译出来的东西完全不对。今天咱们就把这些操作彻底捋清楚。
14.1 列表操作:list() 命令
CMake 的列表,说白了就是用分号分隔的字符串。比如 set(MY_LIST a b c) 实际上存的是 "a;b;c"。我个人习惯用 list() 命令来操作,比直接操作字符串更安全。
14.1.1 基本操作
# 创建列表
set(FRUITS apple banana cherry)
# 获取长度
list(LENGTH FRUITS len)
message("长度: ${len}") # 输出: 3
# 获取元素
list(GET FRUITS 1 fruit)
message("第二个水果: ${fruit}") # 输出: banana
# 追加元素
list(APPEND FRUITS durian)
message("追加后: ${FRUITS}") # 输出: apple;banana;cherry;durian
# 插入元素
list(INSERT FRUITS 1 "blueberry")
message("插入后: ${FRUITS}") # 输出: apple;blueberry;banana;cherry;durian
set(LIST "a;b;c"),结果后面用 list(APPEND) 时出了问题——因为字符串里已经包含了分号,CMake 会把它当成列表的一部分。
14.1.2 查找与删除
# 查找元素位置
list(FIND FRUITS "banana" idx)
message("banana 的位置: ${idx}") # 输出: 2
# 删除指定值
list(REMOVE_ITEM FRUITS "cherry")
message("删除 cherry 后: ${FRUITS}")
# 删除指定索引
list(REMOVE_AT FRUITS 0)
message("删除第一个后: ${FRUITS}")
# 去重
set(NUMBERS 1 2 2 3 3 3)
list(REMOVE_DUPLICATES NUMBERS)
message("去重后: ${NUMBERS}") # 输出: 1;2;3
list(FIND) 找不到元素时返回 -1。我曾经在脚本里忘了检查这个返回值,结果用 -1 去 list(GET),直接报错。所以一定要先判断 ${idx} GREATER_EQUAL 0。
14.1.3 排序与反转
# 排序(默认升序)
list(SORT FRUITS)
message("排序后: ${FRUITS}")
# 反转
list(REVERSE FRUITS)
message("反转后: ${FRUITS}")
# 自定义排序(CMake 3.18+)
list(SORT FRUITS COMPARE STRING CASE_INSENSITIVE ORDER DESCENDING)
message("降序不区分大小写: ${FRUITS}")
14.2 字符串操作:string() 命令
字符串操作比列表更灵活,但也更容易出错。我建议你记住一个原则:能用 list() 解决的,就别用 string() 去折腾列表。但有些场景,比如路径处理、正则匹配,非用 string() 不可。
14.2.1 查找与替换
set(TEXT "Hello, CMake! CMake is great.")
# 查找子串
string(FIND "${TEXT}" "CMake" pos)
message("CMake 首次出现位置: ${pos}") # 输出: 7
# 替换所有
string(REPLACE "CMake" "Make" NEW_TEXT "${TEXT}")
message("替换后: ${NEW_TEXT}") # 输出: Hello, Make! Make is great.
# 正则匹配
string(REGEX MATCH "[A-Z]+" result "${TEXT}")
message("匹配大写字母: ${result}") # 输出: H
避坑指南:我曾经在替换路径分隔符时,直接写了 string(REPLACE "/" "\\" PATH ...),结果反斜杠被当成转义符。正确写法是 string(REPLACE "/" "\\\\" PATH ...)——四个反斜杠才能表示一个。嗯,这玩意儿我至少查了三次文档才记住。
14.2.2 大小写与修剪
set(MIXED " Hello World ")
# 转大写
string(TOUPPER "${MIXED}" upper)
message("大写: ${upper}") # 输出: HELLO WORLD
# 转小写
string(TOLOWER "${MIXED}" lower)
message("小写: ${lower}") # 输出: hello world
# 去除首尾空格
string(STRIP "${MIXED}" stripped)
message("修剪后: '${stripped}'") # 输出: 'Hello World'
14.2.3 子串与长度
set(PATH "/usr/local/bin/cmake")
# 获取长度
string(LENGTH "${PATH}" len)
message("路径长度: ${len}")
# 截取子串(从索引5开始,取4个字符)
string(SUBSTRING "${PATH}" 5 4 sub)
message("子串: ${sub}") # 输出: loca
# 获取文件扩展名
get_filename_component(EXT "${PATH}" EXT)
message("扩展名: ${EXT}") # 输出: .cmake
get_filename_component() 而不是 string(REGEX)。前者更直观,而且跨平台兼容性更好。我在 Windows 上吃过亏——路径分隔符是反斜杠,正则写起来特别别扭。
14.3 数学运算:math() 命令
CMake 的数学运算能力很有限,只能做整数运算。如果你需要浮点数,建议用 Python 脚本或者外部工具。但整数运算在版本号比较、计数器场景下完全够用。
14.3.1 基本运算
# 加法
math(EXPR sum "1 + 2")
message("1 + 2 = ${sum}")
# 减法
math(EXPR diff "10 - 3")
message("10 - 3 = ${diff}")
# 乘法
math(EXPR prod "4 * 5")
message("4 * 5 = ${prod}")
# 除法(整数除法,向下取整)
math(EXPR quot "10 / 3")
message("10 / 3 = ${quot}") # 输出: 3
# 取模
math(EXPR mod "10 % 3")
message("10 % 3 = ${mod}") # 输出: 1
14.3.2 位运算与自增
# 位与
math(EXPR bit_and "7 & 3")
message("7 & 3 = ${bit_and}") # 输出: 3
# 位或
math(EXPR bit_or "7 | 3")
message("7 | 3 = ${bit_or}") # 输出: 7
# 左移
math(EXPR shift "1 << 3")
message("1 << 3 = ${shift}") # 输出: 8
# 自增(用变量)
set(COUNT 0)
math(EXPR COUNT "${COUNT} + 1")
message("自增后: ${COUNT}") # 输出: 1
math(EXPR) 的表达式里不能有空格?其实可以,但建议用双引号括起来。我遇到过最诡异的问题:math(EXPR result 1+2) 能正常工作,但 math(EXPR result 1 + 2) 就报错。后来发现是 CMake 版本差异——3.10 之前要求表达式不能有空格。所以保险起见,要么不加空格,要么用双引号包起来。
14.4 实战:版本号比较
咱们来写一个实用的例子——比较两个版本号。这在检查依赖库版本时特别有用。
function(compare_version v1 v2)
# 将版本号拆成列表
string(REPLACE "." ";" v1_list "${v1}")
string(REPLACE "." ";" v2_list "${v2}")
# 补齐长度
list(LENGTH v1_list v1_len)
list(LENGTH v2_list v2_len)
# 取最大长度
math(EXPR max_len "${v1_len}")
if(v2_len GREATER max_len)
math(EXPR max_len "${v2_len}")
endif()
# 逐段比较
set(result 0)
foreach(i RANGE 0 ${max_len})
# 获取当前段,没有则视为0
if(i LESS v1_len)
list(GET v1_list ${i} v1_part)
else()
set(v1_part 0)
endif()
if(i LESS v2_len)
list(GET v2_list ${i} v2_part)
else()
set(v2_part 0)
endif()
# 比较
if(v1_part GREATER v2_part)
set(result 1)
break()
elseif(v1_part LESS v2_part)
set(result -1)
break()
endif()
endforeach()
return(PROPAGATE result)
endfunction()
# 测试
compare_version("1.2.3" "1.2.4")
message("1.2.3 vs 1.2.4: ${result}") # 输出: -1
compare_version("2.0.0" "1.9.9")
message("2.0.0 vs 1.9.9: ${result}") # 输出: 1
核心思路:把版本号转成列表,逐段比较。注意处理长度不一致的情况——比如 "1.2" 和 "1.2.0" 应该相等。这个函数我在三个项目里复用过了,基本没出过问题。
14.5 知识体系图
下面这张图总结了本章的核心内容,方便你快速回顾:
14.6 总结与建议
好了,咱们把 CMake 的列表、字符串和数学运算都过了一遍。说几个我个人的经验:
- 列表优先:能拆成列表的,就别用字符串硬拼。列表操作更安全,不容易出现分号混乱的问题。
- 字符串注意转义:正则表达式和路径处理时,多检查一遍转义符。我至少因为少写一个反斜杠浪费过半小时。
- 数学运算有限:只支持整数,别想着做浮点运算。如果真需要,用
execute_process()调 Python 或者 bc 命令。 - 版本号比较:别自己造轮子,除非你像我一样有特殊需求。CMake 3.19+ 有
if(VERSION_GREATER)可以直接用。
最后一个小技巧:调试时多用 message() 打印变量值。我写 CMake 脚本时,几乎每三行就有一个 message()——等调试完了再删掉。这比猜变量值要高效得多。