我应该什么时候引用CMake变量参考?

62
我第一次编写 CMake 宏,对变量的工作原理感到很困惑。特别是,${a} 似乎与 "${a}" 有不同的含义。
例如,这里:Passing a list to a CMake macro 我应该在什么时候添加引号,以及更重要的基本原则是什么?
2个回答

104

CMake有两个原则必须牢记:

  1. CMake是一种脚本语言,参数在变量扩展后进行求值。
  2. CMake区分普通字符串和列表变量(用分号分隔的字符串)。

示例

  • set(_my_text "A B C")message("${_my_text}")将会输出A B C
  • set(_my_list A B C)message("${_my_list}")将会输出A;B;C
  • set(_my_list "A" "B" "C")message("${_my_list}")将会输出A;B;C
  • set(_my_list "A" "B" "C")message(${_my_list})将会输出ABC

一些基本规则

以下是一些需要考虑的基本规则:

  1. a) 当你的变量包含文本——尤其是可能包含分号的文本时——你应该添加引号。

    原因:分号是CMake中列表元素的分隔符。因此,对于希望作为一个单一元素的文本,请在其周围加上引号(这在任何地方都有效,并且在我个人看来,使用CMake语法高亮更好)。

    编辑:感谢@schieferstapel的提示

    b) 更精确地说:已经有引号的包含空格的变量会保留这些引号作为变量内容的一部分。这也适用于未引用的情况(普通或用户定义的函数参数),但不包括if()调用,因为CMake在变量扩展后重新解释未引用变量的内容(请参见基本规则#3 和策略CMP0054: 仅在未引用时将if()参数解释为变量或关键字)。

    示例:

    • 使用set(_my_text "A B C"),并使用message(${_my_text})命令会得到A B C
    • 使用set(_my_text "A;B;C"),并使用if (${_my_text} STREQUAL "A;B;C")命令会得到如果给出参数:"A" "B" "C" "STREQUAL" "A;B;C"指定了未知参数

    如果您的变量包含列表,则通常不需要添加引号。

    原因:如果您向CMake命令提供类似文件列表的东西,它通常希望得到一个字符串列表而不是一个包含列表的字符串。您可以在foreach()命令中看到这种区别,它接受ITEMSLISTS

    if()语句是一种特殊情况,在这种情况下,您通常甚至不需要放置括号。

    原因:一个字符串经过扩展后可能再次评估为一个变量名。为了防止这种情况发生,建议只命名要比较内容的变量(例如if (_my_text STREQUAL "A B C"))。

    • set(_my_text "A B C")COMMAND "${CMAKE_COMMAND}" -E echo "${_my_text}"VS/Windows 上会执行 cmake.exe -E echo "A B C",在 GCC/Ubuntu 上会执行 cmake -E echo A\ B\ C,输出为 A B C
    • set(_my_text "A B C")COMMAND "${CMAKE_COMMAND}" -E echo "${_my_text}" VERBATIMVS/Windows 上会执行 cmake.exe -E echo "A B C",在 GCC/Ubuntu 上会执行 cmake -E echo "A B C",输出为 A B C
    • set(_my_list A B C)COMMAND "${CMAKE_COMMAND}" -E echo "${_my_list}" 会执行 cmake.exe -E echo A;B;C,输出为 A, B: command not found, C: command not found
    • set(_my_list A B C)COMMAND "${CMAKE_COMMAND}" -E echo "${_my_list}" VERBATIM 会执行 cmake.exe -E echo "A;B;C",输出为 A;B;C
    • set(_my_list "A" "B" "C")COMMAND "${CMAKE_COMMAND}" -E echo "${_my_list}" VERBATIM 会执行 cmake.exe -E echo "A;B;C",输出为 A;B;C
    • set(_my_list "A" "B" "C")COMMAND "${CMAKE_COMMAND}" -E echo ${_my_list} VERBATIM 会执行 cmake.exe -E echo A B C,输出为 A B C
    • set(_my_list "A + B" "=" "C")COMMAND "${CMAKE_COMMAND}" -E echo ${_my_list} VERBATIM 会执行 cmake.exe -E echo "A + B" = C,输出为 A + B = C

    add_custom_target()/add_custom_command()/execute_process()的一些经验法则

    在使用COMMAND调用变量时,应该考虑以下几点:

    1. a) 对于包含文件路径(例如第一个参数包含可执行文件本身)的参数,请使用引号。

      原因:它可能包含空格,并且可能会被重新解释为COMMAND调用的单独参数。

      b) 与上述情况类似,如果变量set() 包括引号,则仍然适用。

    2. 仅在您想要将某些内容连接到要传递给可执行文件的单个参数中时使用引号。

      原因:当使用引号时,变量可能包含参数列表,这些参数列表不会被正确提取(分号而不是空格)。

    3. 始终在add_custom_target()/add_custom_command()中添加VERBATIM选项。

      原因:否则跨平台行为是未定义的,您可能会收到有关引号字符串的惊喜。

    参考资料

    更多信息,请参见Craig Scott的这篇文章,其中讨论了有关列表和命令参数、生成器表达式以及if() 命令引号考虑事项。


3
-1 是因为第一条经验法则的推理错误。如果你将一个变量传递给一个函数 (func(${X}),不带引号),并且 X 包含空格,那么 X 仍然只是一个参数,不会像你所说的那样被扩展/评估。如果引用每个可能包含带空格路径的变量的话,这将会过于容易出错(就像在 POSIX shell 中一样)。 - schieferstapel
1
@schieferstapel 感谢你的提示。你说得对,在这种情况下不需要引号。我已经相应地更新了我的答案。但是,如果我不知道变量是否可能包含一个列表,我仍然认为它是必要的。我还需要再次检查add_custom_command()部分(虽然我当时测试过,但我会再次检查)。 - Florian
1
这正是我转向Meson的根本原因。CMake受到这类问题的困扰,以至于多年后我几乎不记得如何使用它了。 - Germán Diago

1
什么时候应该引用CMake变量引用?
1. 当你需要的时候。 2. 当你觉得这样做并且在技术上没有任何区别的时候。
我很难理解变量的工作原理,尤其是 ${a} 看起来与 "${a}" 有着不同的含义。
关于 CMake 的机制,你需要学习几个方面:变量引用的工作原理、列表的工作原理以及命令参数的处理方式。
我能想到的几种情况是:命令调用和某些特定命令对变量名作为参数而不是作为变量引用进行评估的特殊行为(例如 if(...))。

对于一般的命令调用

变量引用的文档可以在这里找到。请注意,CMake中的命令包括函数和宏(相关文档)。简而言之,如果您在不传递--warn-uninitialized的情况下引用不存在的变量,则CMake将将引用评估为空字符串。这就是为什么上面的第一个message调用会打印“foo:”,以及为什么对set(foo ${foo} abc)的第一次调用(其中第二个参数引用了foo)不会出错(只要您不使用--warn-uninitialized)。

在CMake中,列表只是由分号字符分隔的字符串。分号可以用反斜杠进行转义。你可以在这里找到完整的列表文档here
命令参数的文档可以在此处找到here
  • 关于引用参数的部分:

    引用参数内容包括在开头和结尾引号之间的所有文本。同时会对转义序列和变量引用进行求值。引用参数总是作为一个参数传递给命令调用。

  • 关于非引用参数的部分:

    非引用参数内容包括在允许或转义字符的连续块中的所有文本。同时会对转义序列和变量引用进行求值。结果值按照列表划分为元素。每个非空元素都作为一个参数传递给命令调用。因此,非引用参数可以作为零个或多个参数传递给命令调用。

这就是为什么我说“[quote variables] when you need to”。

在调用命令时,您需要引用什么以获得所需的行为,其余决定因素将取决于命令体如何处理传递给它的参数(在 CMake 完成取消转义、变量引用评估等操作后)。有关详细信息,请参阅特定命令的文档或函数/宏的文档或实现(还请参阅 function argumentsmacro argumentsmacro argument caveats 的相关文档)。

if(...) 命令

好的,那么我为什么还说“当你觉得做这件事并且从技术上讲没有任何区别时”呢?因为 if(...) 命令具有额外的行为。

对于 if(...) "子命令" 签名中的任何位置,您看到 "<condition>",请参考 条件语法文档,该文档说明:

if(<constant>)
如果常量是 1, ON, YES, TRUE, Y, 或一个非零数(包括浮点数),返回真。如果常量是 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND, 空字符串,或以后缀 -NOTFOUND 结尾,则返回假。命名布尔常量不区分大小写。如果参数不是这些特定常量之一,则将其视为变量或字符串(请参见下面的变量扩展),并适用以下两种形式之一。

if(<variable>)
给定一个被定义为非 false 常量值的变量,则返回真。否则返回假,包括变量未定义的情况。注意,宏参数不是变量。环境变量也不能以此方式测试,例如,if(ENV{some_var}) 总是计算为假。

if(<string>)
除非:

该字符串的值是真常量之一,或者

策略 CMP0054 没有设置为 NEW,而该字符串的值恰好是受 CMP0054 行为影响的变量名称。

否则,带引号的字符串始终计算为假。

此外,if(...) 命令的许多签名/“子命令”接受形式为 <variable|string> 的参数。

请注意,这些不是关于变量引用!只是变量名称,可能会导致人们发现意外的怪异结果,例如在 CMake 中仅限数字的变量名称 中所见。

如果您希望确保在未加引号的字符串中按名称“自动引用”变量的 if(...) 命令中的某些内容实际上是字符串,则通过将其括起来来防止发生这种情况。因此,这涵盖了两个原因:一是“在需要时”,二是“当你觉得这样做并且在技术上没有任何区别时”(我喜欢在这些情况下引用我打算作为字符串的东西,以获得心理安慰)。

请注意,这种行为并不一定是if(...)特有的。您同样可以编写一个执行此类操作的函数(例如${${ARGV0}}(先解除引用以获取参数值,然后再次解除引用以将该值视为另一个变量的名称,或者首先执行if(DEFINED "${ARGV0}")来检查是否首先定义了这样的变量)。因此,如果您想要安全,请始终阅读文档(我只是鼓励通常阅读文档)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接