51

I always think that if you want to compare two strings (but not variables) all you need to do is to quote it like that:

if("${A}" STREQUAL "some string")

but now I find out that this code sometimes print oops:

cmake_minimum_required(VERSION 2.8)

if("d" STREQUAL "")
  message("oops...")
endif()

Maybe it's a bug (because it prints with Xcode, but not with make)?

Or there is some special variables?

  • cmake: 2.8.12, 2.8.11.2
  • xcode: 4.6.2, 5.0.1

Update

There is a string command without the described problem:

string(COMPARE EQUAL "${A}" "" result)
if(result)
  message("...")
endif()

Update 2

The behaviour I've expected was implemented since CMake 3.1.0 (see CMP0054).

Output of the 3.0.2 test:

CMake version: 3.0.2
Quoted test
Surprise!
Unquoted test
Surprise!

Output of the 3.1.0 test:

CMake version: 3.1.0
Quoted test
OK
Unquoted test
Surprise!
Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
  • It could at times enter (VAR STREQUAL "") condition, when "variable" is not defined. In such cases (NOT DEFINED VAR) is the respite for settings the defaults (as an example). References here : https://cmake.org/cmake/help/v3.3/command/if.html – parasrish Aug 23 '18 at 09:26
  • I thought middle operators where deprecated? It's so hard to be sure what is new and what is old in CMAKE. – Sandburg Jan 18 '19 at 15:00

2 Answers2

67

You ran into a rather annoying "it's not a bug, it's a feature" behavior of CMake. As explained in the documentation of the if command:

 The if command was written very early in CMake's history, predating the ${} 
 variable evaluation syntax, and for convenience evaluates variables named
 by its arguments as shown in the above signatures.

Well, the convenience turned out to be an inconvenience. In your example the the string "d" is treated as a variable named d by the if command. If the variable d happens to be defined to the empty string, the message statement will print "oops...", e.g.:

set (d "")
if("d" STREQUAL "")
  # this branch will be taken
  message("oops...")
else()
  message("fine")
endif()

This can give surprising results for statements like

if("${A}" STREQUAL "some string")

because there can be an unintended double expansion of the first argument if the variable A happens to be defined to a string which is also the name of a CMake variable, e.g.:

set (A "d")
set (d "some string")   
if("${A}" STREQUAL "some string")
  # this branch will be taken
  message("oops...")
else()
  message("fine")
endif()

Possible work-arounds:

You can add a suffix character to the string after the ${} expansion which prevents the if statement from doing the automatic evaluation:

set (A "d")
set (d "some string")
if("${A} " STREQUAL "some string ")
  message("oops...")
else()
  # this branch will be taken
  message("fine")
endif()

Do not use ${} expansion:

set (A "d")
set (d "some string")
if(A STREQUAL "some string")
  message("oops...")
else()
  # this branch will be taken
  message("fine")
endif()

To prevent unintended evaluation on the right side of STREQUAL use MATCHES with a CMake regular expression instead:

if(A MATCHES "^value$")
  ...
endif()

Addendum: CMake 3.1 no longer does double expansions for quoted arguments. See the new policy.

sakra
  • 62,199
  • 16
  • 168
  • 151
  • 1
    Yep, I've read this part of course. I don't see any double quotes notes in documentation and just hope that there is some kind of expansion protection and that `if(d ...)` is not the same as `if("d" ...)`. Turns out it's the same: `if(DEFINED "d")` is `TRUE` ): And for some reason `d` is defined for one generator(Xcode) but not for other (make). BTW somebody can use suffix for variable name: `set("d " "other string")` and may be it's better to use heavy words like `_NEVER_NAME_VARIABLE_LIKE_THAT` (: –  Nov 15 '13 at 08:22
  • Using a space as the suffix character is probably sufficient, since you can't access a variable whose name contains a space (in your example, the variable `d ` can't be accessed). I agree that you should avoid other characters though, so if not using a space, use a long string as per your example. – Fraser Nov 15 '13 at 09:37
  • 2
    @Fraser `since you can't access a variable whose name contains a space` why not? Example: `set("A " "some string") set(VAR_NAME "A ") message(${${VAR_NAME}})` Output: `some string`. I know that this example looks synthetic, but if you use wrappers a lot you can met this situation in real code. Something like this: `set(VAR_NAME "${FIRST} ${SECOND}")` and if `SECOND` is empty you will got `VAR_NAME` ending with space. –  Nov 15 '13 at 10:37
  • You might be curious (since SO doesn't notify about sibling answers) that this is no longer true as of CMake 3.1. – Arthur Tacca Aug 21 '20 at 17:25
  • 1
    @ArthurTacca Unluckily the use of older verisons of CMake (e.g., 2.8) is still quite common. – sakra Aug 21 '20 at 18:06
  • @sakra Absolutely agreed! – Arthur Tacca Aug 21 '20 at 18:15
14

As of CMake 3.1, there are new rules variable expansions in if(). They are enabled if you either:

Even in that case, it remains true is that the first argument to if is expanded with the value of a variable matching that name, if it exists:

set (d "")
if(d STREQUAL "")
  # this branch will be taken
  message("oops...")
else()
  message("fine")
endif()

However, this is now disabled if the first argument is quoted:

set (d "")
if("d" STREQUAL "")
  message("oops...")
else()
  # due to quotes around "d" in if statement,
  # this branch will be taken
  message("fine")
endif()

If you do want to test a variable's contents against a value, you can either use the classic unquoted syntax, or use the "${d}" syntax you suggested. Thanks to the new rules, this will never suffer the double-expansion problem mentioned in sakra's answer:

set (A "d")
set (d "some string")   
if("${A}" STREQUAL "d")
  # this branch will be taken
  message("fine")
elseif("${A}" STREQUAL "some string")
  message("oops...")
else()
  message("??")
endif()
Arthur Tacca
  • 8,833
  • 2
  • 31
  • 49