6、数学与字符串:math命令、string命令(TOLOWER、TOUPPER、REPLACE、REGEX等)、文件路径操作

说实话,CMake 里处理数学和字符串,很多人一开始都觉得「这有啥好学的?不就是算个数、拼个串吗?」。嗯,我当年也是这么想的。直到有一次,我需要根据版本号动态生成库文件名,还要处理不同平台下的路径分隔符……那一周我几乎把 CMake 文档翻烂了。

今天咱们就把 math()string() 和文件路径操作这几个硬骨头啃下来。你学完之后,处理版本号、拼接路径、做条件判断,都会顺手很多。

6.1 数学运算:math 命令

math() 命令在 CMake 里做整数运算。别指望它做浮点——CMake 的数学就是整数世界。我个人习惯用它来计算循环次数、版本号递增、或者做位掩码。

基本语法:

math(EXPR <output_variable> <expression>)

举个例子:

set(A 10)
set(B 3)
math(EXPR RESULT "${A} + ${B} * 2")
message("结果: ${RESULT}")  # 输出: 16

支持的运算符:+ - * / % | & ^ ~ << >>

注意:除法是整数除法,3/2 结果是 1,不是 1.5。我曾经在计算分片大小时踩过这个坑,调试了半天才发现是整数截断的问题。

实际项目里,我常用它来做版本号递增:

set(VERSION_MAJOR 2)
set(VERSION_MINOR 1)
set(VERSION_PATCH 5)

# 打补丁版本 +1
math(EXPR VERSION_PATCH "${VERSION_PATCH} + 1")
message("新版本: ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")

6.2 字符串处理:string 命令全家桶

string() 是 CMake 里最常用的命令之一。我敢说,你写的每个 CMakeLists.txt 里几乎都会用到它。咱们挑几个最实用的来讲。

6.2.1 大小写转换:TOLOWER / TOUPPER

这个太常用了。比如用户输入的选项可能是 ONonOn,你统一转成大写再比较,省心很多。

set(INPUT "Hello World")
string(TOUPPER "${INPUT}" UPPER_STR)
string(TOLOWER "${INPUT}" LOWER_STR)
message("大写: ${UPPER_STR}")   # HELLO WORLD
message("小写: ${LOWER_STR}")   # hello world
我的习惯:所有选项比较前,先 TOUPPER 再比较。这样不管用户怎么输入,都不会出问题。

6.2.2 字符串替换:REPLACE

替换子串,支持直接替换和正则替换。我经常用它来清理路径或者修改配置。

set(TEXT "Hello, World! Hello, CMake!")
string(REPLACE "Hello" "Hi" NEW_TEXT "${TEXT}")
message("替换后: ${NEW_TEXT}")  # Hi, World! Hi, CMake!

只替换第一个?可以用正则:

string(REGEX REPLACE "^Hello" "Hi" NEW_TEXT "${TEXT}")
message("正则替换: ${NEW_TEXT}")  # Hi, World! Hello, CMake!

6.2.3 正则匹配:REGEX MATCH / REGEX MATCHALL

这个功能我太爱了。比如从版本号里提取主版本、次版本:

set(VERSION "3.25.1-rc2")
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)" MATCHED "${VERSION}")
message("匹配结果: ${MATCHED}")  # 3.25
message("CMAKE_MATCH_1: ${CMAKE_MATCH_1}")  # 3
message("CMAKE_MATCH_2: ${CMAKE_MATCH_2}")  # 25

用 MATCHALL 可以匹配所有出现:

set(DATA "a1b2c3d4")
string(REGEX MATCHALL "[0-9]+" NUMBERS "${DATA}")
message("所有数字: ${NUMBERS}")  # 1;2;3;4
避坑指南:我曾经在匹配路径时忘了转义反斜杠,结果匹配了一堆空串。Windows 路径里的反斜杠记得写成 \\\\ 或者直接用正斜杠。

6.2.4 其他常用操作

命令 作用 示例
LENGTH 获取字符串长度 string(LENGTH "abc" LEN) → 3
SUBSTRING 截取子串 string(SUBSTRING "abcdef" 1 3 SUB) → bcd
FIND 查找子串位置 string(FIND "hello" "ll" POS) → 2
APPEND 追加字符串 string(APPEND STR " world")
PREPEND 前置字符串 string(PREPEND STR "hello ")
STRIP 去除首尾空白 string(STRIP " abc " S) → abc
COMPARE 比较字符串 string(COMPARE EQUAL "a" "b" RES)

6.3 文件路径操作

跨平台项目里,路径处理是最容易出问题的。Windows 用反斜杠,Linux/macOS 用正斜杠。CMake 提供了一套路径操作命令,帮你统一处理。

6.3.1 路径拼接与规范化

set(BASE_DIR "/home/user/project")
set(SUB_DIR "src/core")

# 拼接路径
cmake_path(APPEND FULL_PATH "${BASE_DIR}" "${SUB_DIR}" "main.cpp")
message("完整路径: ${FULL_PATH}")

# 规范化(去掉多余的 . 和 ..)
cmake_path(NORMAL_PATH NORM_PATH "${FULL_PATH}/../include/../src/main.cpp")
message("规范化后: ${NORM_PATH}")

6.3.2 获取路径各部分

set(FILE_PATH "/home/user/project/src/main.cpp")

cmake_path(GET FILE_PATH PARENT_PATH DIR)
message("目录: ${DIR}")  # /home/user/project/src

cmake_path(GET FILE_PATH FILENAME NAME)
message("文件名: ${NAME}")  # main.cpp

cmake_path(GET FILE_PATH STEM STEM_NAME)
message("无扩展名: ${STEM_NAME}")  # main

cmake_path(GET FILE_PATH EXTENSION EXT)
message("扩展名: ${EXT}")  # .cpp
小技巧:cmake_path(GET ... LAST_EXTENSION) 可以获取最后一个扩展名(比如 .tar.gz 会得到 .gz),而 EXTENSION 只取第一个点后面的。

6.3.3 相对路径与绝对路径

set(BASE "/home/user/project")
set(TARGET "/home/user/project/build/output/lib")

# 计算相对路径
cmake_path(RELATIVE_PATH TARGET BASE_DIR "${BASE}" OUTPUT_VAR REL)
message("相对路径: ${REL}")  # build/output/lib

# 判断是否为绝对路径
cmake_path(IS_ABSOLUTE TARGET IS_ABS)
message("是绝对路径吗? ${IS_ABS}")  # TRUE

6.3.4 老式路径命令(了解即可)

cmake_path() 出现之前,大家用 get_filename_component()file(RELATIVE_PATH ...)。现在新项目建议直接用 cmake_path(),更直观。

# 老式写法(仍然可用)
get_filename_component(DIR "/home/user/file.txt" DIRECTORY)
message("目录: ${DIR}")  # /home/user

6.4 知识体系总览

下面这张图帮你理清本章的知识结构:

CMake 数学与字符串处理知识体系 math() 数学运算 • 整数运算:+ - * / % • 位运算:| & ^ ~ << >> • 版本号递增 • 循环计数器 • 分片大小计算 注意:不支持浮点 除法截断整数 string() 字符串处理 • 大小写:TOLOWER/TOUPPER • 替换:REPLACE/REGEX REPLACE • 匹配:REGEX MATCH/MATCHALL • 截取:SUBSTRING • 查找:FIND • 拼接:APPEND/PREPEND • 比较:COMPARE • 去除空白:STRIP cmake_path() 路径操作 • 拼接:APPEND • 规范化:NORMAL_PATH • 获取各部分:GET • 相对路径:RELATIVE_PATH • 判断绝对路径:IS_ABSOLUTE 替代老式命令: get_filename_component() file(RELATIVE_PATH) 三者配合使用,可以处理绝大多数构建配置场景

6.5 实战:版本号处理与路径拼接

咱们来个综合例子。假设你要根据版本号生成动态库文件名,并放到指定输出目录:

# 版本号
set(VER_MAJOR 1)
set(VER_MINOR 2)
set(VER_PATCH 3)

# 生成版本字符串
string(CONCAT VERSION_STR "${VER_MAJOR}.${VER_MINOR}.${VER_PATCH}")
message("版本号: ${VERSION_STR}")

# 生成库文件名
set(LIB_NAME "mylib")
set(LIB_FILE_NAME "${LIB_NAME}.so.${VERSION_STR}")
message("库文件名: ${LIB_FILE_NAME}")

# 输出目录
set(OUTPUT_DIR "/opt/myapp/lib")

# 拼接完整路径
cmake_path(APPEND FULL_PATH "${OUTPUT_DIR}" "${LIB_FILE_NAME}")
message("完整路径: ${FULL_PATH}")

# 检查路径是否合法
cmake_path(IS_ABSOLUTE FULL_PATH IS_ABS)
if(NOT IS_ABS)
    message(WARNING "路径不是绝对路径,请检查!")
endif()

这个例子涵盖了数学运算、字符串拼接、路径操作三个核心知识点。你在实际项目中,类似的场景会非常多。

核心要点回顾:
  • math() 只做整数运算,别拿它算浮点
  • string() 的 REGEX 功能很强大,但要注意转义
  • 路径操作优先用 cmake_path(),跨平台更省心
  • 所有用户输入的字符串,先 TOUPPER 再比较,避免大小写问题

好了,这一章的内容就到这里。数学和字符串处理是 CMake 里的基本功,你多用几次就会形成肌肉记忆。下次遇到需要动态生成路径或者处理版本号的情况,直接翻出这章的例子来用就行。


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