- 构建工具
- 开发工具
- CLI
【免费下载链接】CMake
Mirror of CMake upstream repository
endif是 CMake 中最基础也最常用的块结构命令之一,用于结束由if命令开启的条件命令块。本文以 endif 官方文档 为核心骨架,系统讲解endif的语法、可选的向后兼容参数、与elseif/else的协作关系,并结合本仓库源码(cmIfCommand.cxx、cmListFileCache.cxx)深入剖析if/endif配对的底层实现,同时给出完整的条件表达式速查与实战示例,帮助读者彻底掌握 CMake 条件分支的正确写法。
endif 命令:语法与核心作用
endif命令的官方定义为:结束if块中的命令列表(Ends a list of commands in an if block)。它的完整语法为:
endif([<condition>])其中<condition>参数是可选的,仅用于向后兼容。如果提供该参数,它必须是开头if子句中条件参数的逐字重复(a verbatim repeat of the argument of the openingifclause),即连空白、引号写法都必须完全一致。
在绝大多数现代 CMake 工程中,推荐写法是直接使用不带参数的endif(),这样既清晰又避免出错。
典型块结构
一个标准的if/elseif/else/endif块如下(语法定义见 if 命令文档):
if(<condition>) <commands> elseif(<condition>) # 可选,可重复多次 <commands> else() # 可选 <commands> endif()执行流程为:CMake 先求值if子句的<condition>;若为真则执行if块内命令,否则依次按相同方式处理可选的elseif块;若所有条件都不为真,则执行可选的else块。无论哪个分支被选中,最终都统一由endif收尾闭合。
向后兼容的 参数:规则与陷阱
从 if 命令文档 可知,历史上else与endif都允许附带一个可选的条件参数,这是为了兼容早期 CMake 脚本风格:
if(WIN32) message(STATUS "Windows") endif(WIN32) # 参数必须与开头的 if 条件逐字相同现代 CMake 已不再要求也不鼓励这种写法,官方文档明确该参数仅供向后兼容。同时else命令的可选参数同样仅用于向后兼容且不会被求值。
源码如何校验参数匹配
在 Source/cmIfCommand.cxx 中,cmIfFunctionBlocker::ArgumentsMatch定义了endif与if的参数匹配规则:
bool cmIfFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff, cmMakefile&) const { return lff.Arguments().empty() || lff.Arguments() == this->Args; }即:当endif的参数为空(Arguments().empty()),或与打开该块的if命令的参数完全相等(lff.Arguments() == this->Args)时,才判定为匹配并闭合该块。这意味着:
endif()永远合法;endif(WIN32)只有在配套的if(WIN32)时才匹配;- 若
endif带了一个与if条件不同的参数(如if(WIN32) ... endif(UNIX)),块无法正确闭合,CMake 会报告配对错误。
源码级实现:if/endif 如何配对与执行
if不是普通的即时执行命令,而是通过Function Blocker(函数阻塞器)机制实现的。理解这一机制,就能明白为什么endif的参数匹配如此严格。
Function Blocker 机制
在 cmIfCommand.cxx 中定义了一个cmIfFunctionBlocker,它记录该if块的起始命令名与结束命令名:
class cmIfFunctionBlocker : public cmFunctionBlocker { public: cm::string_view StartCommandName() const override { return "if"_s; } cm::string_view EndCommandName() const override { return "endif"_s; } ... };if命令在求值条件后(cmIfCommand 函数),会把一个cmIfFunctionBlocker压入 cmMakefile::AddFunctionBlocker 维护的栈中;只有当执行到endif且参数匹配时,该阻塞器才会被弹出(对应RemoveFunctionBlocker的实现,见 cmMakefile.cxx)。在块未闭合期间,阻塞器决定哪些命令被"跳过"(elseif/else分支切换)或真正执行(Replay逻辑,见 cmIfCommand.cxx)。
解析阶段的嵌套校验
在 CMake 读取脚本的解析阶段,cmListFileCache.cxx 的 CheckNesting 函数 还会用栈结构校验if/elseif/else/endif的嵌套平衡:
} else if (name == "endif") { if (!TopIs(stack, NestingStateEnum::If) && !TopIs(stack, NestingStateEnum::Else)) { return cmListFileContext::FromListFileFunction(func, this->FileName); } stack.pop_back(); }即:endif只有在栈顶状态是If或Else时才被接受,否则立即返回出错的上下文位置。elseif只能在If状态下出现,else之后不允许再有elseif。这一机制保证了任何多出的endif或缺失的endif都会在解析期被精确定位报错。
此外,cmCommands.cxx 将endif注册为合法命令,而在 cmMakefile.cxx 的 CMP0000 兼容命令白名单中,if、endif、else、elseif均被列为"永久的简单命令",即使脚本未调用cmake_minimum_required也允许使用。
条件表达式速查:endif 所闭合的求值规则
endif本身不参与条件求值,但它闭合的if/elseif条件支持一整套表达式语法(完整定义见 if 命令文档)。掌握这些规则是正确使用if/endif的前提。
复合条件的优先级
条件按以下优先级求值:
- 括号
(condition):最内层先求值; - 一元测试:存在性检查与文件操作;
- 二元测试:比较、版本比较、路径比较、
IN_LIST、IS_NEWER_THAN; - 一元逻辑运算符
NOT; - 二元逻辑运算符
AND、OR,从左到右求值,不短路。
示例:
if((condition) AND (condition OR (condition))) ... endif()常量与真假判定
- 真值常量(大小写不敏感):
1、ON、YES、TRUE、Y,以及任意非零数字(含浮点数); - 假值常量:
0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串,以及以-NOTFOUND结尾的值; - 变量:已定义且值不是假常量时为真,未定义则为假(注意:宏参数不是变量,
if(ENV{some_var})恒为假); - 字符串:带引号的字符串除非其值为真常量,否则恒为假;
- 空参数
if():恒为假。
存在性检查
| 表达式 | 说明 |
|---|---|
if(COMMAND <name>) | 名称是可调用的命令、宏或函数 |
if(DEFINED <name>|CACHE{<name>}|ENV{<name>}) | 变量/缓存变量/环境变量已定义(CACHE{}支持自 3.14 起) |
if(EXISTS <path>) | 文件或目录存在且可读,解析符号链接 |
if(TARGET <name>) | 已创建的逻辑目标(add_executable/add_library/add_custom_target) |
if(TEST <name>) | 已由 add_test 创建的测试(3.3 起) |
if(POLICY <id>) | 存在CMP<NNNN>形式的策略 |
if(<x> IN_LIST <list>) | 元素在列表变量中(3.3 起) |
文件操作与路径判断
if(IS_READABLE <path>) # 3.29 起,可读 if(IS_WRITABLE <path>) # 3.29 起,可写 if(IS_EXECUTABLE <path>) # 3.29 起,可执行 if(IS_DIRECTORY <path>) # 是否为目录 if(IS_SYMLINK <path>) # 是否为符号链接 if(IS_ABSOLUTE <path>) # 是否为绝对路径 if(<file1> IS_NEWER_THAN <file2>) # 时间戳比较,相同时为真注意:文件类判断仅对显式完整路径有明确定义行为,前导~/不会被展开为主目录。
数值 / 字符串 / 版本比较
- 数值比较(按 C
double解析实数):LESS、GREATER、EQUAL、LESS_EQUAL、GREATER_EQUAL(后两者 3.7 起); - 字符串比较(字典序):
STRLESS、STRGREATER、STREQUAL、STRLESS_EQUAL、STRGREATER_EQUAL; - 正则匹配:
if(<var|str> MATCHES <regex>),括号分组会捕获到CMAKE_MATCH_<n>变量; - 版本比较:
VERSION_LESS、VERSION_GREATER、VERSION_EQUAL及_EQUAL变体,格式为major[.minor[.patch[.tweak]]],缺失组件按 0 处理,非整数部分会被截断。
路径比较(PATH_EQUAL / PATH_IS_PREFIX)
PATH_EQUAL(3.24 起)按组件逐段比较路径而不访问文件系统,多个路径分隔符会被折叠;PATH_IS_PREFIX(4.5 起)判断左侧路径是否是右侧路径的前缀,注意它是纯词法判断,不做归一化:
# PATH_EQUAL 比较结果为 TRUE,而 STREQUAL 为 FALSE if ("/a//b/c" PATH_EQUAL "/a/b/c") ... endif() # PATH_IS_PREFIX 判断 if ("/a/b" PATH_IS_PREFIX "/a/b/c") ... endif()变量自动求值(Variable Expansion)
if命令诞生早于${}语法,为方便起见会自动对其参数中命名的变量求值(详见 if 文档的 Variable Expansion 一节)。例如:
set(var1 OFF) set(var2 "var1") if(${var2}) # 等价于 if(var1),结果为假 if(var2) # 直接写变量名,var2 已定义且值不是假常量,结果为真自 3.1 起(配合策略 CMP0054),用引号或括号包裹的变量名会被当作字符串而非解引用。环境变量与缓存变量不会自动求值,必须显式写$ENV{<name>}或$CACHE{<name>}。
嵌套使用与配对注意事项
if/endif块可以自由嵌套,源码的CheckNesting栈机制正是为处理任意深度的嵌套设计的。嵌套时必须保证每个if都有对应的endif:
if(BUILD_SHARED_LIBS) if(MSVC) message(STATUS "Building shared libs with MSVC") endif() else() message(STATUS "Building static libs") endif()实践建议:
- 统一使用
endif()形式,避免带参数写法带来的拼写不一致风险; - 条件较长时,可用
endif()加行尾注释说明闭合的是哪个条件,例如endif() # BUILD_SHARED_LIBS,但注意注释不能替代参数的逐字匹配规则; - 在函数(
function/endfunction)与宏(macro/endmacro)内部使用if/endif时同样遵循本规则,且函数内未闭合的块会导致语法错误。
常见错误与排查方法
- 缺失 endif:CMake 在解析阶段通过
CheckNesting检测到栈不平衡,报错信息会精确指向未闭合的if所在文件与行号; - 多余的 endif:栈顶不是
If/Else状态时被拒绝,提示该endif没有对应的if; - endif 参数不匹配:如
if(WIN32) ... endif(UNIX),ArgumentsMatch返回假,块无法闭合而报错; - else 后再用 elseif:源码中
Replay逻辑(cmIfCommand.cxx)会抛出 "A duplicate ELSE command" 或 "An ELSEIF command was found after an ELSE command" 的致命错误; - 条件类型混淆:例如用
STREQUAL与数字比较混用、未加引号的字符串被当作变量解引用,可参考上文条件表达式速查逐一核对。
调试时可用cmake --trace跟踪命令执行,if分支的实际走向会在跟踪输出中体现。
总结
endif虽只有一行语法,却是 CMake 条件逻辑的收口点。正确理解其三点核心:无参数的标准用法、仅供向后兼容且必须逐字匹配的可选参数、以及源码层面基于 Function Blocker 栈与解析期嵌套校验的双重配对机制(cmIfCommand.cxx、cmListFileCache.cxx),有助于写出结构清晰、跨版本健壮的 CMake 脚本。配合 if 命令文档 中完整的条件表达式体系,即可自如地驾驭 CMake 中一切条件分支场景。
- 构建工具
- 开发工具
- CLI
【免费下载链接】CMake
Mirror of CMake upstream repository
相关推荐
CMake 条件控制流详解:else 命令与 if/elseif/else/endif 块机制
CMake 条件控制流详解:else 命令与 if/elseif/else/endif 块机制 导读 else 是 CMake 条件控制流中的关键命令,用于启动
构建工具开发工具CLICMake endfunction 命令详解:函数块收尾、向后兼容参数与底层实现机制
CMake endfunction 命令详解:函数块收尾、向后兼容参数与底层实现机制 endfunction 是 CMake 中与 function 成对出现的
构建工具开发工具CLICMake elseif 命令全解:if 块分支条件求值与源码级实现原理
CMake elseif 命令全解:if 块分支条件求值与源码级实现原理 导读 elseif 是 CMake 中用于在 if 块内追加备选分支的关键命令,它让构
构建工具开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考