0

Try running the following:

message("foo: ${foo}")
set(foo ${foo} abc)
message("foo: ${foo}")
set(foo ${foo} def)
message("foo: ${foo}")
set(foo ${foo} ghi)
message("foo: ${foo}")

The output will be:

foo: 
foo: abc
foo: abc;def
foo: abc;def;ghi

And if you change it to quote the variable references in the set invocations like this:

message("foo: ${foo}")
set(foo "${foo}" abc)
message("foo: ${foo}")
set(foo "${foo}" def)
message("foo: ${foo}")
set(foo "${foo}" ghi)
message("foo: ${foo}")

You'll instead see this:

foo: 
foo: ;abc
foo: ;abc;def
foo: ;abc;def;ghi

How is this working / why is this happening in terms of the mechanics of CMake?

starball
  • 20,030
  • 7
  • 43
  • 238
  • meta note: this Q&A is designed to be a canonical for questions like [this](https://stackoverflow.com/q/76280985/11107541). I chose to write a new question because that question already got answers and I missed the opportunity to generalize it into something more helpful to future readers. – starball May 19 '23 at 00:13
  • 1
    The first variant - `set(var ${var} val)` - is old style of appending to a list (the modern and more verbose style is `list(APPEND var val)`). The second variant - `set(var "${var}" val)` - is useless in 99% cases (on input it interprets `var` as *string* but on output `var` becomes a *list*). Why do you ever want to describe these things? The [referenced question](https://stackoverflow.com/q/76280985/11107541) is about basics of CMake and many other languages, which are perfectly allow to assign a variable as many times as one wants. – Tsyvarev May 19 '23 at 07:28
  • If you want to describe quoting aspects, then [that question](https://stackoverflow.com/questions/35847655/when-should-i-quote-cmake-variables) seems to be a better place for such description. – Tsyvarev May 19 '23 at 07:38

1 Answers1

1

As you've observed, set(varname ${varname} value) appends a value to a variable using a semicolon as a separator (which you). (Note that there's another way you can append to list variables: list(APPEND <list> [<element> ...])).

To understand how this is working / why this is happening, you need to understand how variable referencing works, how lists work, how the set command works, and how command argument handling works.

Variable Referencing in CMake

The docs for variable referencing can be found here. 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)

Lists in CMake

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 set command in CMake

From the CMake docs for the set command:

Signatures of this command that specify a <value>... placeholder expect zero or more arguments. Multiple arguments will be joined as a semicolon-separated list to form the actual variable value to be set.

One behaviour of set that I'm pretty sure is undocumented is that if you pass a single argument that looks like a list, it won't escape the semicolons in that list.

Command Argument Handling in CMake

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.

Tying it all Together

When you use set(foo ${foo} abc) (unquoted argument),

  • In the first call, ${foo} is evaluated to an empty string, so it is not passed as an argument, but the string argument abc is not empty, so it's passed as the first value argument- thus, foo gets set to abc.

  • In the second call, ${foo} is evaluated to a regular string / a list with one element (abc), and that string / one element is passed as the first value argument, along with def as the second value argument. set joins the two value arguments with a semicolon and the resulting string is assigned to foo.

  • In the third call, ${foo} is evaluated to a list containing abc and def, which get passed as the first and second value argument, respectively. Then ghi as the third argument. set joins the three value arguments by with a semicolon and the resulting string is assigned to foo.

When you use set(foo "${foo}" abc) (quoted argument),

  • In the first call, "${foo}" is evaluated to an empty string, but recall that "A quoted argument is always given to the command invocation as exactly one argument.", so that empty string gets passed as the first argument to the set invocation. The string argument abc is not empty, so it's passed as the second value argument. set joins the two value arguments with a semicolon and the resulting string is assigned to foo. Thus, you get ;abc (a list where the first value is an empty string).

  • In the second call, "${foo}" is evaluated to ;abc. It's a list, but since it got specified as a quoted argument, the entries of the list are not each passed as individual arguments and ;abc is passed as the first value argument, along with def as the second value argument. The two value arguments are joined with a semicolon and the resulting string is assigned to foo (;abc;foo- a list with three elements).

  • A similar thing happens as in the second call in the third call.

starball
  • 20,030
  • 7
  • 43
  • 238