上一节我们介绍了变量, 这一节来介绍控制流. CMake 支持如下的控制流命令: if
, foreach
和while
. 这三条命令使用起来跟其他语言类似, 但是也有一些特定的功能.
if()
命令
if()
命令的现代形式如下(可提供多个elseif()
子句):
if(condition1)#commands...
elseif(condition2)#commands...
else()#commands...
endif()
早期版本的 CMake 要求在else()
和endif()
子句中重复表达式作为参数, 但自 CMake 2.8.0 起不再有此要求.
CMake 中的条件语句有如下几种类型:
- 传统的布尔逻辑
- 文件系统测试命令
- 版本比较命令
- 其他一些测试命令
基本表达式
主要包含三种类型:
- 常量值
if(constant)
- 如果
constant
是值为ON
,YES
,TRUE
,1
, 不论是否带引号, 也不论大小写, 统一视为真(true). - 如果
constant
是值为OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
, 空字符串, 或以-NOTFOUND
结尾, 不论是否带引号, 也不区分大小写, 统一视为假(false). - 如果
constant
是一个(可能是浮点型的)数字, 它将按照常规 C 语言规则转换为布尔值, 不过在这种情况下, 除了0
和1
之外的值很少使用.
- 如果
- 变量名
if(variable)
: 当使用未带引号的变量名时, 会将变量的值与假常量进行比较. 如果没有匹配的值, 则表达式的结果为真. 未定义的变量将求值为空字符串, 这与其中一个假常量匹配, 因此结果为假.[!WARNING]
请注意, 在本讨论中, 环境变量不被视为变量. 像if(ENV{some_var})
这样的语句将始终求值为假, 无论名为some_var
的环境变量是否存在. - 字符串
if("string")
: 注意是带引号的字符串.
带引号的字符串会被视为假(false), 除非:- 在 CMake 3.1 及更高版本中, 这个字符串是一个未带引号的常量.
- 在 CMake 3.1 之前, 如果字符串的值与现有变量的名称匹配, 则该带引号的字符串实际上会被该变量名(不带引号)替换, 然后重新进行测试. 这个跟程序员的直觉相反.
# 带引号和未带引号常量的示例
if("True") # 求值为真
if(TRUE) # 求值为真
if(yes) # 求值为真
if(0) # 求值为假# 这些也被视为未带引号的常量, 因为在if()看到这些值之前, 变量就已求值
set(A YES)
if(${A}) # 求值为真
set(B 0)
if(${B}) # 求值为假# 不匹配任何真假常量
if(anUnknownVariable) # 求值为假# 带引号的值不匹配任何真假常量, 因此同样作为变量名或字符串进行测试
if("anUnknownVariable") # 求值为假
if("A") # 求值为真
逻辑运算符
CMake 支持常见的AND
, OR
和NOT
逻辑运算符, 以及用于控制优先级的括号.
if(NOT expression)
if(NOT expression1 AND expression2)
if(expression1 OR expression2)
if((condition) AND (condition OR (condition)))
:
按照惯例, 括号内的表达式首先求值, 从最内层的括号开始.
比较测试
CMake 将比较测试分为不同类别:
数值 | 字符串 | 版本号 | 路径 |
---|---|---|---|
LESS | STRLESS | VERSION_LESS | - |
GREATER | STRGREATER | VERSION_GREATER | - |
EQUAL | STREQUAL | VERSION_EQUAL | PATH_EQUAL |
LESS_EQUAL | STRLESS_EQUAL | VERSION_LESS_EQUAL | - |
GREATER_EQUAL | STRGREATER_EQUAL | VERSION_GREATER_EQUAL | - |
数值比较按预期比较左右两边的值. 但请注意, 如果任何一个操作数不是数字, CMake 通常不会报错, 并且当值中包含非数字字符时, 其行为并不完全符合官方文档. 根据数字和非数字字符的组合情况, 表达式的结果可能为真或假.
# 有效的数值表达式, 均求值为真
if(2 GREATER 1)
if("23" EQUAL 23)
set(val 42)
if(${val} EQUAL 42)
if("${val}" EQUAL 42)# 无效表达式, 在某些CMake版本中求值为真. 请勿依赖此行为.
if("23a" EQUAL 23)
版本号比较有点像增强版的数值比较. 版本号假定为major[.minor[.patch[.tweak]]]
的形式, 其中每个部分都应为非负整数. 比较两个版本号时, 首先比较主版本号部分. 只有当主版本号部分相等时, 才会比较次版本号部分(如果存在), 依此类推. 缺失的部分视为零.
#在以下所有示例中, 表达式求值为真:
if(1.2 VERSION_EQUAL 1.2.0)
if(1.2 VERSION_LESS 1.2.3)
if(1.2.3 VERSION_GREATER 1.2)
if(2.0.1 VERSION_GREATER 1.9.7)
if(1.8.2 VERSION_LESS 2)
版本号比较与数值比较一样, 存在稳健性方面的注意事项. 每个版本号部分都应是整数, 但如果不满足此限制, 比较结果基本上是未定义的.
对于字符串, 值按字典序进行比较. 但是, 字符串比较函数的参数可以是变量名或字符串, 这又可能会造成混淆. 以下示例展示了一个常见的陷阱:
# 未定义名为"a"的变量, 因此它被视为值为"a"的字符串
if(a STREQUAL "there")message("We do not get here")
elseif(a STREQUAL "")message("We do not get here either")
endif()
set(a there)
# 现在有一个名为"a"的变量, 因此使用其值.
if(a STREQUAL "there")message("We DO get here")
endif()
可以遵循以下准则来避免意外行为:
- 始终确保将策略
CMP0054
设置为NEW
. 这可防止带引号的值被视为除字符串以外的其他内容. - 仅在确保存在同名变量时, 才使用未带引号的参数.
PATH_EQUAL
运算符很像STREQUAL
的特殊情况. 操作数假定为 CMake 原生路径形式(目录分隔符为正斜杠). PATH_EQUAL
的一个关键区别是它使用逐组件比较. 多个连续的目录分隔符会被合并为单个分隔符, 这是与STREQUAL
的主要实际区别. 以下来自官方 CMake 文档的示例展示了这种差异:
if("/a//b/c" PATH_EQUAL "/a/b/c") # true# We DO get here...
endif()
if("/a//b/c" STREQUAL "/a/b/c") # false# We do NOT get here....
endif()
除了上述运算符形式外, 还可以使用正则表达式测试字符串:
if(value MATCHES regex)
value
同样遵循上述变量或字符串规则, 并与regex
正则表达式进行比较. 如果匹配, 则表达式求值为真. 虽然 CMake 文档未定义if()
命令支持的正则表达式语法, 但在其他命令的文档(如string()
命令文档)中进行了定义. 本质上, CMake 仅支持基本的正则表达式语法.
括号可用于捕获匹配值的部分内容. 该命令将设置名为CMAKE_MATCH_<n>
的变量, 其中<n>
是要匹配的组号. 整个匹配的字符串存储在组 0 中.
if("Hi from ${who}" MATCHES "Hi from (Fred|Barney).*")message("${CMAKE_MATCH_1} says hello")
endif()
文件系统测试
CMake 还包含一组可用于查询文件系统的测试:
if(EXISTS pathToFileOrDir)
if(IS_READABLE pathToFileOrDir) # CMake 3.29或更高版本
if(IS_WRITABLE pathToFileOrDir) # CMake 3.29或更高版本
if(IS_EXECUTABLE pathToFileOrDir) # CMake 3.29或更高版本
if(IS_DIRECTORY pathToDir)
if(IS_SYMLINK fileName)
if(IS_ABSOLUTE path)
if(file1 IS_NEWER_THAN file2)
与大多数其他if()
表达式不同, 上述任何运算符在没有$()
的情况下都不会执行任何变量/字符串替换, 无论是否带引号.
IS_DIRECTORY
, IS_SYMLINK
和IS_ABSOLUTE
运算符应该很容易理解. 也许不太直观的是, EXISTS
运算符检查的不仅仅是其参数是否存在. 它还要求指定的文件或目录是可读的, 不过这种行为在未来版本的 CMake 中可能会改变. 在大多数情况下, 较新的IS_READABLE
, IS_WRITABLE
或IS_EXECUTABLE
运算符能更好地表达应检查的实际条件. 因此, 只有在项目需要支持 CMake 3.28 或更早版本, 或者指定的文件或目录是否可读, 可写或可执行并不重要时, 才使用EXISTS
.
不幸的是, IS_NEWER_THAN
这个名称并不准确描述该运算符的实际功能. 当两个文件具有相同的时间戳时, 它也会返回真, 而不仅仅是当file1
的时间戳比file2
新时. 在时间戳分辨率仅为一秒的文件系统(如 macOS 10.12 及更早版本上的 HFS+文件系统)上, 这一点尤为重要. 在这样的系统中, 即使文件是由不同命令创建的, 具有相同时间戳的情况也很常见. 另一个不太直观的行为是, 当其中任何一个文件缺失时, 它也会返回真. 此外, 如果任何一个文件未指定为绝对路径, 其行为是未定义的. 因此, 为了获得所需的条件, 通常需要以否定的方式使用IS_NEWER_THAN
.
考虑这样一个场景: secondFile
是由firstfile
生成的. 如果firstfile
被更新或secondfile
缺失, 则需要重新创建secondFile
. 如果firstfile
不存在, 则应视为致命错误. 这样的逻辑需要这样表达:
set(firstFile "/full/path/to/somewhere")
set(secondFile "/full/path/to/another/file")
if(NOT EXISTS ${firstFile})message(FATAL_ERROR "${firstFile} is missing")
elseif(NOT EXISTS ${secondFile} OR NOT ${secondFile} IS_NEWER_THAN ${firstfile})#...commands to recreate secondFile
endif()
有人可能会天真地认为可以这样表达条件:
# WARNING:Very Likely to be wrong
if(${firstFile} IS_NEWER_THAN ${secondFile})#...commands to recreate secondFile
endif()
虽然这种表述可能看似表达了所需的条件, 但实际上并非如此, 因为当两个文件具有相同时间戳时, 它也会返回真. 如果重新创建secondFile
的操作很快, 并且文件系统的时间戳分辨率仅为秒, 那么每次运行 CMake 时, secondFile
很可能都会被重新创建. 如果构建步骤依赖于secondFile
, 那么每次运行 CMake 后, 构建也会重新构建相关内容.
7.1.5 存在测试
if()
表达式的最后一类支持测试各种 CMake 实体是否存在.
if(DEFINED name)
if(COMMAND name)
if(POLICY name)
if(TARGET name)
if(TEST name) # 自CMake 3.4起可用
上述所有测试在if()
命令执行时, 若指定名称的实体存在, 则返回真.
if(value IN_LIST ListVar) # 自CMake 3.3起可用
DEFINED
: 如果指定名称的变量存在, 则返回真. 变量的值无关紧要, 仅测试其是否存在. 该变量可以是常规的 CMake 变量, 也可以是缓存变量. 从 CMake 3.14 起, 可以使用CACHE{name}
形式仅检查缓存变量是否存在. 所有 CMake 版本也支持使用ENV{name}
形式测试环境变量是否存在, 尽管直到 CMake 3.13 才正式记录此支持.
if(DEFINED CACHE{SOMEVAR}) # 检查CMake缓存变量
if(DEFINED SOMEVAR) # 检查CMake变量(常规或缓存)
if(DEFINED ENV{SOMEVAR}) # 检查环境变量
COMMAND
: 测试指定名称的 CMake 命令, 函数或宏是否存在. 这在尝试使用某个内容之前检查其是否已定义时很有用. 对于 CMake 提供的命令, 建议优先测试 CMake 版本, 但对于项目提供的函数和宏(详见第 9 章"函数和宏"), 这是一种合适的检查方式. 如果某个命令作为第三方包中的技术预览版提供, 这种检查尤其有用. 在这种情况下, 基于版本的逻辑不太合适, 因为该命令可能在未来版本中消失.
# "specialNewThing"作为技术预览版提供, 但它可能尚未得到正式支持.
if(COMMAND specialNewThing)specialNewThing(...)
else()# 回退到其他方式...
endif()
POLICY
: 测试 CMake 是否知晓某个特定策略. 策略名称通常为CMPxxx
的形式, 其中xxxx
是一个四位数. 详见第 13 章"策略"了解此主题的详细信息. 此测试常用于仅在支持特定策略
7.1.5 存在性测试(续)
TARGET
: 用于检查是否存在指定名称的目标. 目标可以是可执行文件, 静态库, 共享库等. 在尝试对目标进行操作之前, 先检查目标是否存在是一种良好的实践. 例如, 在添加依赖项或属性之前:
if(TARGET MyExecutable)target_link_libraries(AnotherTarget PRIVATE MyExecutable)
endif()
TEST
: (从 CMake 3.4 版本开始可用)检查是否存在指定名称的测试用例. 这在需要动态控制测试执行或者在测试脚本中引用其他测试时非常有用. 例如:
if(TEST MySpecialTest)set_tests_properties(MySpecialTest PROPERTIES TIMEOUT 30)
endif()
value IN_LIST ListVar
: (从 CMake 3.3 版本开始可用)检查指定的值是否存在于给定的列表变量中. 这是一种简洁的方式来检查某个元素是否属于一个列表. 例如:
set(MyList "apple" "banana" "cherry")
if("banana" IN_LIST MyList)message("Found banana in the list!")
endif()
7.1.6 常见示例和错误
在使用 if()
命令时, 有一些常见的模式和错误需要注意. 以下是一些示例, 展示了正确和错误的使用方式.
错误示例
一个常见的错误是尝试根据平台相关的变量来做出决策, 但没有正确考虑到这些变量的实际含义. 例如, 在某些情况下, 开发人员可能会错误地使用 CMAKE_SYSTEM_NAME
变量来判断是否为 Windows 系统:
# 错误示例: 这种写法可能会导致在非Windows系统上出现意外行为
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")# 执行一些Windows特定的操作
endif()
这种写法的问题在于, CMAKE_SYSTEM_NAME
变量的值在不同的 CMake 生成器和平台上可能会有所不同. 在某些情况下, 即使在 Windows 系统上, 它的值也可能不是 "Windows"
. 更好的做法是使用 WIN32
或 MSVC
变量来判断是否为 Windows 系统:
# 正确示例: 使用WIN32变量来判断是否为Windows系统
if(WIN32)# 执行一些Windows特定的操作
endif()
正确示例
另一个常见的场景是根据 CMake 选项有条件地包含目标. 假设项目中有一个选项 ENABLE_FEATURE_X
, 用于决定是否启用某个特定的功能. 可以这样使用 if()
命令:
option(ENABLE_FEATURE_X "Enable feature X" OFF)if(ENABLE_FEATURE_X)add_executable(FeatureXExecutable feature_x_source.cpp)target_link_libraries(FeatureXExecutable PRIVATE SomeLibrary)
endif()
在这个示例中, 只有当 ENABLE_FEATURE_X
选项被设置为 ON
时, 才会创建 FeatureXExecutable
目标并链接到 SomeLibrary
.
7.2 循环
CMake 提供了两种主要的循环结构: foreach()
和 while()
.
7.2.1 foreach()
循环
foreach()
循环用于遍历一组项目或值. 它有几种不同的形式, 下面将分别介绍.
基本形式
最基本的形式是直接列出要遍历的值:
foreach(LoopVar arg1 arg2 arg3 ...)# 循环体, 使用 ${LoopVar} 访问当前值message("Current value: ${LoopVar}")
endforeach()
在这个示例中, LoopVar
是循环变量, 它会依次取 arg1
, arg2
, arg3
等的值. 循环体中的命令会针对每个值执行一次.
从列表变量中遍历
可以使用 IN LISTS
语法从一个或多个列表变量中遍历值:
set(MyList "apple" "banana" "cherry")
set(AnotherList "date" "elderberry")foreach(LoopVar IN LISTS MyList AnotherList)message("Current value: ${LoopVar}")
endforeach()
在这个示例中, LoopVar
会依次取 MyList
和 AnotherList
中的所有值.
从项目列表中遍历
也可以使用 IN ITEMS
语法直接列出要遍历的项目:
foreach(LoopVar IN ITEMS "dog" "cat" "bird")message("Current value: ${LoopVar}")
endforeach()
这种形式与基本形式类似, 但更明确地表明是在遍历项目列表.
遍历数值范围
从 CMake 3.0 版本开始, 可以使用 RANGE
关键字来遍历一个数值范围:
foreach(LoopVar RANGE 1 5)message("Current number: ${LoopVar}")
endforeach()
在这个示例中, LoopVar
会从 1 到 5 依次取值. 还可以指定步长:
foreach(LoopVar RANGE 1 10 2)message("Current number: ${LoopVar}")
endforeach()
这里的步长为 2, LoopVar
会依次取 1, 3, 5, 7, 9.
遍历多个列表
从 CMake 3.16 版本开始, 可以同时遍历多个列表. 每个列表中的对应元素会在每次迭代中组合在一起:
set(Fruits "apple" "banana" "cherry")
set(Colors "red" "yellow" "red")foreach(Fruit IN LISTS Fruits Color IN LISTS Colors)message("The ${Fruit} is ${Color}.")
endforeach()
在这个示例中, Fruit
和 Color
会分别从 Fruits
和 Colors
列表中取对应的值.
7.2.2 while()
循环
while()
循环会在条件为真时重复执行一组命令. 其基本形式如下:
while(condition)# 循环体message("Still in the loop...")
endwhile()
condition
是一个 if()
表达式, 与 if()
命令中的表达式规则相同. 只要 condition
为真, 循环体就会继续执行. 例如:
set(Counter 0)
while(Counter LESS 5)message("Counter value: ${Counter}")math(EXPR Counter "${Counter} + 1")
endwhile()
在这个示例中, Counter
变量从 0 开始, 每次循环增加 1, 直到 Counter
不再小于 5 为止.
7.2.3 中断循环
在循环中, 可以使用 break()
和 continue()
命令来控制循环的执行流程.
break()
命令
break()
命令用于提前退出循环. 例如:
foreach(LoopVar RANGE 1 10)if(LoopVar EQUAL 5)break()endif()message("Current value: ${LoopVar}")
endforeach()
在这个示例中, 当 LoopVar
等于 5 时, break()
命令会被执行, 循环会立即终止.
continue()
命令
continue()
命令用于跳过当前迭代的剩余部分, 直接进入下一次迭代. 例如:
foreach(LoopVar RANGE 1 10)if(LoopVar EQUAL 5)continue()endif()message("Current value: ${LoopVar}")
endforeach()
在这个示例中, 当 LoopVar
等于 5 时, continue()
命令会被执行, 当前迭代的 message()
命令会被跳过, 循环会继续进行下一次迭代.
7.3 推荐实践
在使用 if()
, foreach()
和 while()
命令时, 有一些推荐的实践可以帮助避免常见的错误和混淆.
避免字符串歧义
如前所述, 在 if()
命令中, 带引号的字符串可能会有歧义, 特别是在 CMake 3.1 之前的版本中. 为了避免这种情况, 建议始终将策略 CMP0054
设置为 NEW
, 并仅在确保存在同名变量时才使用未带引号的参数. 在 foreach()
和 while()
命令中, 也应尽量避免使用可能被误解释为变量的字符串.
及时存储正则表达式匹配结果
在使用 if()
命令的 MATCHES
运算符时, 如果需要使用正则表达式匹配的结果, 建议及时将其存储在变量中. 因为 CMAKE_MATCH_<n>
变量在后续的命令执行过程中可能会被覆盖. 例如:
if("Some text with a number 123" MATCHES "number ([0-9]+)")set(MatchedNumber ${CMAKE_MATCH_1})message("Matched number: ${MatchedNumber}")
endif()
避免模糊的循环命令
在使用 foreach()
和 while()
命令时, 应尽量避免使用模糊或可能误导读者的代码. 例如, 在使用 RANGE
关键字时, 应明确指定范围的起始值, 结束值和步长(如果需要), 以提高代码的可读性. 同时, 在循环体中, 应尽量保持逻辑清晰, 避免使用过于复杂的嵌套条件和操作.