CMake list 命令完全指南:从分号分隔列表到源码级实现剖析
CMake list 命令完全指南:从分号分隔列表到源码级实现剖析
导读
list 是 CMake 中最常用的命令之一,围绕"分号分隔的字符串列表"提供了一整套读写、搜索、修改与排序子命令。本文以 list 命令官方文档 为核心骨架,逐条讲解 LENGTH、GET、JOIN、SUBLIST、FIND、APPEND、FILTER、INSERT、POP_BACK、POP_FRONT、PREPEND、REMOVE_ITEM、REMOVE_AT、REMOVE_DUPLICATES、TRANSFORM、REVERSE、SORT 全部 17 个子命令的语法、参数语义与边界行为,并结合 命令实现源码 与 RunCMake/list 测试套件 揭示底层实现与验证细节。读完本文,你将能够熟练地用 list 处理源文件集合、命令行参数、路径列表等常见构建脚本场景,并理解列表在 CMake 语言中的真实存储形态与作用域规则。
1. 列表是什么:CMake 中的分号分隔字符串
CMake 中所有变量值本质上都是字符串,而"列表"只是字符串在特定上下文(尤其是未加引号参数求值时)的一种解读方式:按 ; 字符将字符串切分为多个元素。详细规则见 CMake 语言手册的 Lists 章节:
set(srcs a.c b.c c.c) # srcs 实际存储为 "a.c;b.c;c.c"
set(x a "b;c") # x 存储为 "a;b;c",分号未被转义,嵌套列表被"展平"
几个必须牢记的事实:
;的切分规则是:不跟在不配对[/]之后的;,且不被\立即转义的;才会切分元素;\;不切分,而是将字面;还原进元素。- 列表不建议包含带分号的元素;若必须如此,官方建议用占位符(如
|)替代分号再在foreach里string(REPLACE)还原,或用$<SEMICOLON>生成表达式(generator expression)。 - 函数(
function)中避免使用ARGV/ARGN区分分号,推荐cmake_parse_arguments的PARSE_ARGV签名;宏(macro)的参数不是变量,因此不能直接用于list命令。 - 与
set命令一致,list的修改类子命令只在当前变量作用域创建新值——即使原列表定义在父作用域,也不会改写父作用域变量。需要向上传播时,应使用set(... PARENT_SCOPE)、set(... CACHE INTERNAL)等方式显式传递。
实现层面,cmListCommand.cxx 通过 GetList()(第 70-91 行)从 cmMakefile 的定义表中读取变量字符串并构造 cmList 对象;cmList 类本身负责切分、插入、排序等底层运算,命令层只是参数校验与结果回写。
2. 索引规则:正负索引与边界
在继续之前,先统一索引约定(官方文档的 Note 部分):
<element index>为 0 或正数时,从列表头部开始计数,0 代表第一个元素;<element index>为 -1 或更小时,从列表尾部开始计数,-1 代表最后一个元素;- 注意
-0等价于0(即第一个元素),负数计数不从 0 开始; - 字符串形式的索引会先被解析为长整数,若解析失败,则受策略
CMP0121管控:NEW行为下报错,OLD行为下静默忽略(见 GetIndexArg)。
INSERT 的合法索引范围是 0 到 N(N 为列表长度,含端点,允许插入到尾部之后);GET、REMOVE_AT 等越界索引会抛出 std::out_of_range 并转为命令错误(对应测试 GET-InvalidIndex、INSERT-InvalidIndex、REMOVE_AT-InvalidIndex)。
3. 读取类子命令:LENGTH / GET / JOIN / SUBLIST
3.1 list(LENGTH )
返回列表长度。若变量不存在,返回 0(源码 HandleLengthCommand 中 list ? list->size() : 0)。
set(my_list a b c)
list(LENGTH my_list len) # len == 3
3.2 list(GET [ ...] )
按索引取元素,支持一次取多个,结果仍是一个列表(保持给定索引顺序);支持负索引。若列表变量不存在则输出变量被置为 NOTFOUND;若列表为空则报错"GET given empty list"(HandleGetCommand)。
set(my_list a b c d)
list(GET my_list 0 2 3 picked) # picked == "a;c;d"
list(GET my_list -1 -2 picked) # picked == "d;c"
3.3 list(JOIN )(3.12 新增)
用胶水字符串 glue 连接所有元素,输出为单个字符串;列表不存在时输出空字符串。若只是拼接多个非列表字符串,请用 string(JOIN) 而非此处。
set(my_list a b c)
list(JOIN my_list " + " joined) # joined == "a + b + c"
3.4 list(SUBLIST )(3.12 新增)
截取子列表:
<length>为 0 时返回空列表;<length>为 -1,或begin + length超过列表大小时,返回从<begin>到末尾的剩余元素;<begin>必须为非负(越界会报错),<length>必须 ≥ -1(HandleSublistCommand)。
set(my_list a b c d e)
list(SUBLIST my_list 1 3 sub) # sub == "b;c;d"
list(SUBLIST my_list 2 -1 sub) # sub == "c;d;e"
对应测试见 SUBLIST.cmake 与 SUBLIST-InvalidIndex、SUBLIST-InvalidLength。
4. 搜索子命令:FIND
4.1 list(FIND )
查找第一个匹配 <value> 的元素的索引;找不到时返回 -1。列表变量不存在时同样返回 -1(HandleFindCommand 中 index == cmList::npos ? "-1" : ...)。
set(my_list a b c)
list(FIND my_list b idx) # idx == 1
list(FIND my_list z idx) # idx == -1
5. 修改类子命令(一):APPEND / PREPEND / INSERT / POP
5.1 list(APPEND [ ...])
向列表末尾追加元素。如果当前作用域不存在名为 <list> 的变量,其值按空列表处理,直接以追加结果为新值——这正是"当前作用域新建变量"语义的体现(HandleAppendCommand)。
list(APPEND srcs main.c utils.c) # 不存在则创建,结果 "main.c;utils.c"
5.2 list(PREPEND [ ...])(3.15 新增)
向列表第 0 个位置插入元素(等价于 INSERT ... 0)。变量不存在时同样按空列表处理(HandlePrependCommand)。
set(srcs main.c)
list(PREPEND srcs config.h) # srcs == "config.h;main.c"
5.3 list(INSERT [ ...])
在指定索引处插入一个或多个元素。合法索引为 0 到 N(含);空列表长度为 0,只能向索引 0 插入。越界索引是错误(HandleInsertCommand)。注意插入的多个元素会保持顺序整体落在 <index> 位置。
set(my_list a d)
list(INSERT my_list 1 b c) # my_list == "a;b;c;d"
5.4 list(POP_BACK [ ...])(3.15 新增)
从尾部弹出元素:
- 不带输出变量:仅删除最后一个元素;
- 带 N 个输出变量:把最后 N 个元素的值依次赋给这些变量,再从列表删除;
- 若列表为空或元素不足,剩余的未赋值输出变量会被 unset 掉(HandlePopBackCommand)。
set(my_list a b c)
list(POP_BACK my_list x y) # x == "c", y == "b", my_list == "a"
5.5 list(POP_FRONT [ ...])(3.15 新增)
从头部弹出元素,语义与 POP_BACK 完全对称:不给变量名则仅删除第一个元素;给 N 个变量名则依次接收前 N 个元素并将其从列表删除;元素不足时剩余变量被 unset(HandlePopFrontCommand)。
set(my_list a b c)
list(POP_FRONT my_list x y) # x == "a", y == "b", my_list == "c"
两个 POP 子命令的边界行为(未定义列表、空列表、只弹一个等)都有专门测试覆盖,见 POP_BACK.cmake 与 POP_FRONT.cmake。
6. 修改类子命令(二):REMOVE_ITEM / REMOVE_AT / REMOVE_DUPLICATES
6.1 list(REMOVE_ITEM [ ...])
从列表中删除所有与给定值相等的元素(所有实例都会被移除)。列表变量不存在时该操作是空操作(HandleRemoveItemCommand)。
set(my_list a b a c a)
list(REMOVE_ITEM my_list a) # my_list == "b;c"
6.2 list(REMOVE_AT [ ...])
按索引删除元素,支持负索引;索引越界或列表为空是错误(HandleRemoveAtCommand)。
set(my_list a b c d)
list(REMOVE_AT my_list 1 3) # my_list == "a;c"
6.3 list(REMOVE_DUPLICATES )
去重:保留元素的相对顺序,重复项只保留第一次出现(HandleRemoveDuplicatesCommand)。保持顺序这一点有专门测试 REMOVE_DUPLICATES-PreserveOrder.cmake 验证。
set(my_list b a c a b)
list(REMOVE_DUPLICATES my_list) # my_list == "b;a;c"
7. 修改类子命令(三):FILTER 过滤
7.1 list(FILTER <INCLUDE|EXCLUDE> )(3.6 新增)
按模式保留或剔除元素:
INCLUDE:只保留匹配模式的元素;EXCLUDE:删除匹配模式的元素。
<MODE> 二选一:
REGEX:按 CMake 语言正则规范(^、$、.、[ ]、\<char> 等)匹配:
set(candidates main.c main.cpp utils.c)
list(FILTER candidates INCLUDE REGEX ".*\\.c$") # 只留 .c 文件
list(FILTER candidates EXCLUDE REGEX ".*\\.cpp$") # 剔除 .cpp 文件
PREDICATE(4.4 新增):指定一个用户自定义 function 作为一元谓词。该函数必须恰好接受两个参数:输入值 + 输出变量名;函数必须在调用作用域把输出变量设为布尔值(标准 CMake 布尔求值),若不设置输出变量则报错。宏(macro)不能用作谓词(对应测试 FILTER-PREDICATE-Macro)。
官方示例:
function(file_exists path result)
if(EXISTS "${path}")
set(${result} TRUE PARENT_SCOPE)
else()
set(${result} FALSE PARENT_SCOPE)
endif()
endfunction()
set(candidate_files main.c missing.c utils.c)
list(FILTER candidate_files INCLUDE PREDICATE file_exists)
FILTER 的实现见 HandleFilterCommand:先校验 INCLUDE/EXCLUDE 操作符,再按 REGEX 或 PREDICATE 模式分别调用 list->filter(...)。模式与操作符的非法组合(FILTER-REGEX-InvalidMode、FILTER-REGEX-InvalidOperator)以及非法正则(FILTER-REGEX-InvalidRegex)均有对应错误测试。
8. 修改类子命令(四):TRANSFORM 变换(3.12 新增)
8.1 语法总览
list(TRANSFORM <list> <ACTION> [<SELECTOR>] [OUTPUT_VARIABLE <out-var>])
对全部或选中的元素执行 <ACTION>,结果就地写回原列表,或通过 OUTPUT_VARIABLE 输出到新变量(此时原列表不变)。注意:TRANSFORM 不会改变列表元素个数;若指定了 <SELECTOR>,未选中的元素保持原样。
8.2 ACTION 选项
| ACTION | 语义 | 示例 |
|---|---|---|
APPEND <value> |
每个元素末尾追加值 | list(TRANSFORM l APPEND ".c") |
PREPEND <value> |
每个元素开头添加值 | list(TRANSFORM l PREPEND "${dir}/") |
TOLOWER |
全部转小写 | list(TRANSFORM l TOLOWER) |
TOUPPER |
全部转大写 | list(TRANSFORM l TOUPPER) |
STRIP |
去掉元素首尾空白 | list(TRANSFORM l STRIP) |
GENEX_STRIP |
剔除元素中的生成器表达式 | list(TRANSFORM l GENEX_STRIP) |
REPLACE <regex> <replace> |
正则替换(语义同 string(REGEX REPLACE)) |
list(TRANSFORM l REPLACE "^a" "A") |
APPLY <function>(4.4 新增) |
调用用户函数逐元素处理 | 见下 |
REPLACE 与 string(REGEX REPLACE) 语义一致,会在每个元素上尽可能多次匹配替换。4.1 起行为有变化:^ 锚点现在只在输入元素的起始位置匹配,而不再匹配每次重复搜索的开头,受策略 CMP0186 管控。
APPLY 要求函数恰好两个形参(输入值 + 输出变量名),函数必须通过 set(<variable> <value> PARENT_SCOPE) 设置输出;每次调用前输出变量会在调用作用域被 unset,防止残留旧值。官方示例:
function(make_absolute in out)
cmake_path(ABSOLUTE_PATH in BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
set(${out} "${in}" PARENT_SCOPE)
endfunction()
set(mylist main.c utils.c io.c)
list(TRANSFORM mylist APPLY make_absolute)
# mylist 现在都是基于 CMAKE_CURRENT_SOURCE_DIR 的绝对路径
8.3 SELECTOR 选项(同时只能指定一种)
-
AT:只变换指定索引的元素,支持负索引:
list(TRANSFORM mylist TOUPPER AT 0 2) -
FOR:变换区间
[start, stop],可选步长step(必须是正数):list(TRANSFORM mylist TOUPPER FOR 1 -1) # 除第一个元素外全部 list(TRANSFORM mylist TOUPPER FOR 0 -1 2) # 每隔一个变换 -
REGEX:只变换匹配正则的元素:
list(TRANSFORM mylist REPLACE "(.+a)$" "\\1_\\1" REGEX "(r|t)a") -
PREDICATE(4.4 新增):只变换谓词函数返回真值的元素。谓词函数恰好两个形参,输出变量必须被设置为布尔值,未设置则报错;谓词在该元素即将被变换之前立即求值(官方示例为
is_relative配合PREPEND前缀路径)。
源码中 TRANSFORM 的完整参数解析见 HandleTransformCommand:动作表通过 ActionDescriptor 注册了 8 种动作及各自参数个数(arity),随后依次消费可选的 REGEX/AT/FOR/PREDICATE 选择器与 OUTPUT_VARIABLE。测试方面,TRANSFORM-REPLACE.cmake 覆盖了 AT、FOR、REGEX 三种选择器与就地/输出变量两种模式组合;另有 TRANSFORM-Selector-FOR-ZeroStepArgument、TRANSFORM-Selector-AT-InvalidIndex、TRANSFORM-APPLY-Macro 等大量错误路径测试。
9. 排序类子命令:REVERSE 与 SORT
9.1 list(REVERSE )
就地反转列表内容(HandleReverseCommand)。
set(my_list a b c)
list(REVERSE my_list) # my_list == "c;b;a"
9.2 list(SORT [...]) 与 COMPARE/CASE/ORDER 选项
默认按字母序就地排序。3.13 起支持三个选项:
list(SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
COMPARE(比较方法):
STRING(默认):按字符串字母序;FILE_BASENAME:按文件路径的 basename 排序;NATURAL(3.18 新增):自然序(参考strverscmp(3)),连续数字按整体数值比较。
NATURAL 与 STRING 的典型差异:列表 10.0 1.1 2.1 8.0 2.0 3.1
- NATURAL 排序结果:
1.1 2.0 2.1 3.1 8.0 10.0 - STRING 排序结果:
1.1 10.0 2.0 2.1 3.1 8.0
CASE(大小写):
SENSITIVE(默认):区分大小写;INSENSITIVE:不区分大小写,仅大小写不同元素的相对顺序未定义。
ORDER(升降序):
ASCENDING(默认):升序;DESCENDING:降序。
选项的解析与重复/非法值校验见 HandleSortCommand,对应错误测试包括 SORT-BadCompareOption、SORT-BadCaseOption、SORT-BadOrderOption、SORT-DuplicateCompareOption 等。
9.3 list(SORT ... COMPARATOR )(4.4 新增)
除内置 COMPARE 方法外,可用用户自定义比较函数:
list(SORT <list> COMPARATOR <function> [CASE <case>] [ORDER <order>])
COMPARATOR与COMPARE互斥(同时给出会报错,见 SORT-COMPARATOR-CompareConflict);CASE仍可生效:两个值在传给函数前会先做大小写过滤;ORDER DESCENDING通过交换两个实参实现反转比较;- 函数必须恰好接受三个参数:两个输入值 + 输出变量名,并设置布尔值:第一个值应排在第二个之前时返回
TRUE,否则FALSE;不设置输出变量是错误。
比较器必须定义严格弱序(strict weak ordering),特别是:
- 自反性(irreflexivity):元素与自身比较必须返回
FALSE; - 反对称性(asymmetry):若
(a, b)返回TRUE,则(b, a)必须返回FALSE。
违反这些要求的比较器会直接报错(对应测试 SORT-COMPARATOR-NotStrictWeak 与 SORT-COMPARATOR-NotStrictWeakLate)。
官方示例——按版本号排序:
function(version_less a b result)
if("${a}" VERSION_LESS "${b}")
set(${result} TRUE PARENT_SCOPE)
else()
set(${result} FALSE PARENT_SCOPE)
endif()
endfunction()
set(versions 3.1 1.2 2.0 1.10)
list(SORT versions COMPARATOR version_less)
# versions 变为: 1.2;1.10;2.0;3.1
更完整的比较器测试见 SORT-COMPARATOR.cmake,其中覆盖了自定义字符串比较、按长度排序、CASE INSENSITIVE、ORDER DESCENDING 及嵌套比较器(内层排序的输出变量名与外层不冲突)等场景。
10. 源码视角:命令如何注册与分发
list 是一个典型的 CMake "命令分发表"实现。入口函数 cmListCommand 在参数少于两个时报错(list 后至少要跟一个子命令名),随后通过静态的 cmSubcommandTable 将 17 个子命令名映射到各自的 Handle* 处理函数:
| 子命令 | 处理函数 |
|---|---|
| LENGTH / GET / JOIN / SUBLIST | HandleLengthCommand / HandleGetCommand / HandleJoinCommand / HandleSublistCommand |
| FIND | HandleFindCommand |
| APPEND / PREPEND / POP_BACK / POP_FRONT | HandleAppendCommand / HandlePrependCommand / HandlePopBackCommand / HandlePopFrontCommand |
| INSERT / REMOVE_AT / REMOVE_ITEM / REMOVE_DUPLICATES | HandleInsertCommand / HandleRemoveAtCommand / HandleRemoveItemCommand / HandleRemoveDuplicatesCommand |
| FILTER / TRANSFORM | HandleFilterCommand / HandleTransformCommand |
| REVERSE / SORT | HandleReverseCommand / HandleSortCommand |
所有修改类子命令最终都通过 cmMakefile::AddDefinition 将结果写回当前作用域;元素级运算(insert_items、remove_items、sublist、sort、transform、filter)则在 cmList 类中完成,命令层只负责参数解析、索引合法性校验与错误消息构造。
从测试组织看,Tests/RunCMake/list 目录为每个子命令及其错误路径准备了独立的 .cmake 用例与 -result.txt/-stderr.txt 期望文件,是验证本文所述全部边界行为(空列表、越界索引、非法正则、宏作为谓词/比较器、选项重复等)最直接的参考资料。
11. 实战建议与易错点小结
- 作用域陷阱:
APPEND/INSERT/FILTER/SORT等修改类子命令都只影响当前作用域。在函数内想改外层列表,必须显式set(<list> ... PARENT_SCOPE)传播。 - 负索引细节:
-0等于0(第一个元素);-1是最后一个元素。从尾部计数时不要想当然从 0 开始。 TRANSFORM不改元素数量:若用REGEX选择器 +REPLACE,未匹配元素保持原样;需要先过滤再变换时,配合FILTER使用。SORT NATURAL适合版本号/数字后缀文件名;纯字母序会得到10.0 < 1.1这类"反直觉"结果。- 宏不能作为谓词/比较器:
FILTER ... PREDICATE、TRANSFORM ... APPLY/PREDICATE、SORT ... COMPARATOR都要求真实function,且函数必须设置输出变量,否则是错误。 - 列表元素与分号:大多数构建列表接口不支持含分号元素;需要传递复杂结构时优先引号参数、占位符替换或
$<SEMICOLON>。 - 版本前提:
JOIN/SUBLIST/TRANSFORM需要 CMake 3.12+,POP_*/PREPEND需要 3.15+,NATURAL排序需要 3.18+,PREDICATE/APPLY/COMPARATOR为 4.4 新增能力;生产环境注意最低版本要求(可在CMakeLists.txt顶部用 cmake_minimum_required 声明)。