61

I am writing CMake macros for the first time, and I have a hard time understanding how variables work. Most specifically, ${a} seems to have a different meaning than "${a}".

For example here: Passing a list to a CMake macro

When am I supposed to add quotes, and what are the bigger underlying principles?

starball
  • 20,030
  • 7
  • 43
  • 238
Vince
  • 3,979
  • 10
  • 41
  • 69

2 Answers2

102

Two principles of CMake you have to keep in mind:

  1. CMake is a script language and arguments are evaluated after the variables are expanded
  2. CMake differentiates between normal strings and list variables (strings with semicolon delimiters)

Examples

  • set(_my_text "A B C") with message("${_my_text}") would give A B C
  • set(_my_list A B C) with message("${_my_list}") would give A;B;C
  • set(_my_list "A" "B" "C") with message("${_my_list}") would give A;B;C
  • set(_my_list "A" "B" "C") with message(${_my_list}) would give ABC

Some Rules of Thumb

There are some rules of thumb you should consider:

  1. a) When your variable contains text - especially one that could contain semicolons - you should add quotes.

    Reasoning: A semicolon is a delimiter for list elements in CMake. So put quotes around a text that is supposed to be one (it works everywhere and for me personally looks better with CMake syntax highlighting)

    EDIT: Thanks for the hint from @schieferstapel

    b) To be more precise: A variable content with spaces that already had quotes does keep those quotes (imagine as it getting part of the variable's content). This works everywhere also unquoted (normal or user-defined function parameters) with the prominent exception of if() calls, where CMake re-interprets the content of unquoted variables after variable expansion (see also rule of thumb #3 and policy CMP0054: Only interpret if() arguments as variables or keywords when unquoted)

    Examples:

    • set(_my_text "A B C") with message(${_my_text}) would also give A B C
    • set(_my_text "A;B;C") with if (${_my_text} STREQUAL "A;B;C") would give if given arguments: "A" "B" "C" "STREQUAL" "A;B;C" Unknown arguments specified
  2. If your variable contains a list you normally don't add quotes.

    Reasoning: If you give something like a file list to an CMake command it normally expect a list of strings and not one string containing a list. The difference you can see e.g. in the foreach() command accepting ITEMS or LISTS.

  3. if() statements are a special case where you normally don't even put the braces.

    Reasoning: A string could - after expansion - evaluate again to a variable name. To prevent this it's recommended to just name the variable whose content you want to compare (e.g. if (_my_text STREQUAL "A B C")).


COMMAND Examples

  • set(_my_text "A B C") with COMMAND "${CMAKE_COMMAND}" -E echo "${_my_text}" would
    • call cmake.exe -E echo "A B C" on VS/Windows
    • call cmake -E echo A\ B\ C on GCC/Ubuntu
    • give A B C
  • set(_my_text "A B C") with COMMAND "${CMAKE_COMMAND}" -E echo "${_my_text}" VERBATIM would
    • call cmake.exe -E echo "A B C" on VS/Windows
    • call cmake -E echo "A B C" on GCC/Ubuntu
    • give A B C
  • set(_my_list A B C) with COMMAND "${CMAKE_COMMAND}" -E echo "${_my_list}" would
    • call cmake.exe -E echo A;B;C
    • give A, B: command not found, C: command not found
  • set(_my_list A B C) with COMMAND "${CMAKE_COMMAND}" -E echo "${_my_list}" VERBATIM would
    • call cmake.exe -E echo "A;B;C"
    • give A;B;C
  • set(_my_list "A" "B" "C") with COMMAND "${CMAKE_COMMAND}" -E echo "${_my_list}" VERBATIM would
    • call cmake.exe -E echo "A;B;C"
    • give A;B;C
  • set(_my_list "A" "B" "C") with COMMAND "${CMAKE_COMMAND}" -E echo ${_my_list} VERBATIM would
    • call cmake.exe -E echo A B C
    • give A B C
  • set(_my_list "A + B" "=" "C") with COMMAND "${CMAKE_COMMAND}" -E echo ${_my_list} VERBATIM would
    • call cmake.exe -E echo "A + B" = C
    • give A + B = C

Some Rules of Thumb with add_custom_target()/add_custom_command()/execute_process()

There are some rules of thumb you should consider when you use variables in COMMAND calls:

  1. a) Use quotes for the arguments that contain file paths (like the first argument containing the executable itself).

    Reasoning: It could contain spaces and could be reinterpreted as separate arguments to the COMMAND call

    b) See above, works also if the variable set() did include quotes.

  2. Use quotes only if you want to concatenate something into a single parameter to be passed to executable that is called.

    Reasoning: A variable could contain a list of parameters which - when using quotes - won't be correctly extracted (semicolons instead of spaces)

  3. Always add the VERBATIM option with add_custom_target()/add_custom_command()

    Reasoning: Otherwise the cross-platform behavior is undefined and you could get surprises with your quoted strings.

References

For more information, see this article by Craig Scott (one of the maintainers of CMake), which discusses quoting considerations with lists and command arguments, generator expressions, and the if() command.

starball
  • 20,030
  • 7
  • 43
  • 238
Florian
  • 39,996
  • 9
  • 133
  • 149
  • 3
    -1 because the reasoning behind the first rule of thumb #1 is wrong. If you pass a variable to a function (`func(${X})`, without quotes) and `X` contains spaces, `X` is still only one argument and does not get expanded/evaluated like you say. It would be too error-prone (like it is in e.g. POSIX shells) to quote every variable that may contain a path with spaces. – schieferstapel Sep 29 '17 at 07:51
  • 1
    @schieferstapel Thanks for the hint. You're right and in that case there are no quotes necessary. I updated my answer accordingly. But I still think it would be necessary if I don't know if the variable may contain a list. And I've to check the `add_custom_command()` part again (thought I've tested it back then, but I'll double-check). – Florian Sep 30 '17 at 09:34
0

When should I quote CMake variable references?

  1. When you need to
  2. When you feel like doing it and it technically makes no difference

I have a hard time understanding how variables work. Most specifically, ${a} seems to have a different meaning than "${a}".

There are a couple of things you need to learn here with respect to the mechanics of CMake: How variable references work, how lists work, and how command arguments are handled.

There are a couple scenarios I can think of where this matters: cammand calls/invocations, and some specific commands which have special behaviour for arguments that variable names, but are not evaluated variable references (Ex. if(...)).

For Command Calls in General

The docs for variable referencing can be found here. Note that commands in CMake include functions and macros (related docs). TL;DR is that if you reference a non-existant variable without passing --warn-uninitialized, CMake will evaluate the reference to an empty string. That's why the first message call above prints "foo: ", and why the first call to set(foo ${foo} abc) (which references foo in the second argument) will not error (as long as you don't use --warn-uninitialized)

In CMake, lists are just strings where each entry of the list is separated by a semicolon character. semicolons can be escaped with a backslash. You can find the full docs on lists here.

The docs for command arguments can be found here.

  • From the section on quoted arguments:

    Quoted argument content consists of all text between opening and closing quotes. Both Escape Sequences and Variable References are evaluated. A quoted argument is always given to the command invocation as exactly one argument.

  • From the section on unquoted arguments:

    Unquoted argument content consists of all text in a contiguous block of allowed or escaped characters. Both Escape Sequences and Variable References are evaluated. The resulting value is divided in the same way Lists divide into elements. Each non-empty element is given to the command invocation as an argument. Therefore an unquoted argument may be given to a command invocation as zero or more arguments.

That's why I say "[quote variables] when you need to".

The rest of the deciding factor on what you need to quote to get the behaviour you want when invoking commands will depend on how the command's body handles the arguments it has been passed (after CMake has done un-escaping, variable reference evaluation, etc.). For those details, refer to the documentation for the specific command, or the documentation or implementation of the function/macro (see also the related docs for function arguments, macro arguments, and macro argument caveats).

For the if(...) Command

Okay, so why did I also say "when you feel like doing it and it technically makes no difference"? Because of the if(...) command's extra behaviours.

For any place in the signature of a if(...) "sub-command" you see "<condition>", refer to the condition syntax documentation, which states:

if(<constant>)
True if the constant is 1, ON, YES, TRUE, Y, or a non-zero number (including floating point numbers). False if the constant is 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in the suffix -NOTFOUND. Named boolean constants are case-insensitive. If the argument is not one of these specific constants, it is treated as a variable or string (see Variable Expansion further below) and one of the following two forms applies.

if(<variable>)
True if given a variable that is defined to a value that is not a false constant. False otherwise, including if the variable is undefined. Note that macro arguments are not variables. Environment Variables also cannot be tested this way, e.g. if(ENV{some_var}) will always evaluate to false.

if(<string>)
A quoted string always evaluates to false unless:

The string's value is one of the true constants, or

Policy CMP0054 is not set to NEW and the string's value happens to be a variable name that is affected by CMP0054's behavior.

Also, many of the signatures / "sub-commands" of the if(...) command accept arguments of the form <variable|string>

Note that these are not talking about variable-references! Just variable names, which can lead to funky things that people might find unexpected, such as seen in Numeric only variable name in CMake.

If you want to be sure that something you intend to be a string in a if(...) command that will attempt to "auto-reference" variables by name in unquoted strings, then prevent that from happening by quoting it. So this covers both reasons: both "when you need to", and also "when you feel like doing it and it technically makes no difference" (I like to quote things that I intend to be strings in these contexts just for peace of mind).

Note that this behaviour isn't something necessarily special to if(...). You can equally write a function that does this kind of thing (Ex. ${${ARGV0}} (de-reference once to get the argument value, then de-reference again to treat the value as the name of another variable or additionally do a if(DEFINED "${ARGV0}") first to check if such a variable is defined first)). So if you want to be safe, always read docs (I'd just encourage reading docs in general).

starball
  • 20,030
  • 7
  • 43
  • 238