CMake JOIN生成器表达式引号处理行为差异及正确引用方法
两个$<JOIN>表达式的求值逻辑完全一致,差异来自CMake对add_custom_command中COMMAND参数的词法解析规则,和你写的参数外层引号的位置直接相关。
关键解析规则
CMake处理命令参数时,引号相关的核心逻辑只有两条:
- 首尾引号自动剥离:如果一个参数的第一个非空白字符是未转义双引号,且最后一个非空白字符是对应闭合的未转义双引号,这对首尾双引号会被识别为参数分隔符,直接丢弃,不会出现在最终参数值中;其他所有位置的双引号(包括转义得到的
\"、不在参数首尾位置的未转义引号)都会作为普通字面量保留。 - 含空格参数自动补引号:如果生成器表达式求值完成后,参数值包含空格且没有被引号包裹,CMake会自动给整个参数外层补一对双引号,避免空格把参数拆分成多个部分;如果参数值里本身已经包含引号,CMake不会再自动补外层引号。
逐案例拆解
1. 包含目录的JOIN为什么输出正常
你写的包含目录参数如下:
-I"$<JOIN:$<TARGET_PROPERTY:Tutorial,INCLUDE_DIRECTORIES>,\" -I\">"
这个参数的第一个字符是-,不是双引号,不满足首尾引号剥离规则,所以-I后面、表达式末尾的两个双引号都会作为普通字面量保留。
参数内的转义引号\"会被解析为字面量双引号,传入JOIN的分隔符为" -I",最终拼接结果为:
-I"include files/a" -I"include files/b"
结果首尾不是配对双引号,CMake不会做额外修改,输出完全符合预期。
2. 初版编译选项JOIN为什么丢首尾引号
你最开始写的编译选项参数如下:
"$<JOIN:$<TARGET_PROPERTY:Tutorial,COMPILE_OPTIONS>,\" \">"
这个参数的第一个字符是未转义双引号,最后一个字符是对应闭合的未转义双引号,完全满足首尾引号剥离规则,最外层的这对引号会被直接丢弃,不会进入后续求值流程。
参数内的转义引号解析为字面量双引号,传入JOIN的分隔符为" ",最终拼接结果为:
/DC=1" "/DD=2
结果本身已经包含引号,CMake不会自动补外层引号,就出现了你看到的首尾引号丢失的问题。
3. 改空格分隔符后为什么整个字符串被包在引号里
你把分隔符改成普通空格后的参数如下:
"$<JOIN:$<TARGET_PROPERTY:Tutorial,COMPILE_OPTIONS>, >"
这个参数同样满足首尾引号剥离规则,最外层引号先被丢弃。JOIN用空格拼接两个编译选项后得到/DC=1 /DD=2,结果包含空格且没有任何引号,触发CMake的自动补引号逻辑,最终输出为:
"/DC=1 /DD=2"
正确实现方案
要实现每个编译选项单独包裹引号的效果,只要规避参数首尾是未转义双引号的情况即可,和包含目录的写法逻辑对齐:
add_custom_command( OUTPUT Tutorial.i COMMAND "${CMAKE_C_COMPILER}" -I"$<JOIN:$<TARGET_PROPERTY:Tutorial,INCLUDE_DIRECTORIES>,\" -I\">" "\"$<JOIN:$<TARGET_PROPERTY:Tutorial,COMPILE_OPTIONS>,\" \" >\"" /P ${CMAKE_CURRENT_SOURCE_DIR}/Tutorial.cxx MAIN_DEPENDENCY "Tutorial.cxx" COMMENT "Preprocessing Tutorial.cxx" )
如果使用CMake 3.20及以上版本,也可以用内置的$<QUOTE>生成器表达式代替手动转义引号,写法更清晰不易出错:
"$<QUOTE>$<JOIN:$<TARGET_PROPERTY:Tutorial,COMPILE_OPTIONS>,$<QUOTE> $<QUOTE>>$<QUOTE>"
内容的提问来源于stack exchange,提问作者Brad

