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
我的习惯:创建列表时尽量用空格分隔元素,而不是手动加分号。CMake 会自动处理成标准格式。我曾经见过有人写 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 知识体系图

下面这张图总结了本章的核心内容,方便你快速回顾:

CMake 数据操作核心 list() 列表操作 string() 字符串操作 math() 数学运算 LENGTH / GET / APPEND FIND / REMOVE / SORT FIND / REPLACE / REGEX TOUPPER / STRIP / SUBSTRING 加减乘除 / 取模 位运算 / 自增 实战:版本号比较 列表、字符串、数学运算三者结合,可以解决大部分构建脚本中的数据处理需求

14.6 总结与建议

好了,咱们把 CMake 的列表、字符串和数学运算都过了一遍。说几个我个人的经验:

  • 列表优先:能拆成列表的,就别用字符串硬拼。列表操作更安全,不容易出现分号混乱的问题。
  • 字符串注意转义:正则表达式和路径处理时,多检查一遍转义符。我至少因为少写一个反斜杠浪费过半小时。
  • 数学运算有限:只支持整数,别想着做浮点运算。如果真需要,用 execute_process() 调 Python 或者 bc 命令。
  • 版本号比较:别自己造轮子,除非你像我一样有特殊需求。CMake 3.19+ 有 if(VERSION_GREATER) 可以直接用。

最后一个小技巧:调试时多用 message() 打印变量值。我写 CMake 脚本时,几乎每三行就有一个 message()——等调试完了再删掉。这比猜变量值要高效得多。

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