CMake list 命令完全指南:从分号分隔列表到源码级实现剖析

原创2026-10-03 18:19:23913 阅读
文章标签:构建工具开发工具CLI

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. 实战建议与易错点小结

  1. 作用域陷阱:APPEND/INSERT/FILTER/SORT 等修改类子命令都只影响当前作用域。在函数内想改外层列表,必须显式 set(<list> ... PARENT_SCOPE) 传播。
  2. 负索引细节:-0 等于 0(第一个元素);-1 是最后一个元素。从尾部计数时不要想当然从 0 开始。
  3. TRANSFORM 不改元素数量:若用 REGEX 选择器 + REPLACE,未匹配元素保持原样;需要先过滤再变换时,配合 FILTER 使用。
  4. SORT NATURAL 适合版本号/数字后缀文件名;纯字母序会得到 10.0 < 1.1 这类"反直觉"结果。
  5. 宏不能作为谓词/比较器:FILTER ... PREDICATE、TRANSFORM ... APPLY/PREDICATE、SORT ... COMPARATOR 都要求真实 function,且函数必须设置输出变量,否则是错误。
  6. 列表元素与分号:大多数构建列表接口不支持含分号元素;需要传递复杂结构时优先引号参数、占位符替换或 $<SEMICOLON>。
  7. 版本前提:JOIN/SUBLIST/TRANSFORM 需要 CMake 3.12+,POP_*/PREPEND 需要 3.15+,NATURAL 排序需要 3.18+,PREDICATE/APPLY/COMPARATOR 为 4.4 新增能力;生产环境注意最低版本要求(可在 CMakeLists.txt 顶部用 cmake_minimum_required 声明)。
登录后查看全文
CMake