list(FIND) vs. (theoretical) list(CONTAINS)

It’s a bit of a bummer that, because list(FIND...) returns 0-based index values, and if() interprets 0 and only 0 as a FALSE value, the results of a list(FIND...) have to be compared to -1 in order to be used as truth values.

Woe betide the CMakeLists.txt author who assumes this will do what they think it does:

# NOTE: Example of BROKEN code, DO NOT USE
list(FIND AVAILABLE_THINGS ${something} _found_it)
if(_found_it)
  message(STATUS "Found ${something}!")
endif()

If ${something} is in the first list postition, they’ll get a 0 for _found_it and the test will fail. If it’s actually not found, they’ll get a -1 for _found_it and the test will succeed.

But sometimes, you really do just want to test for presence in a list. It’s a pattern that’s used extensively in the bundled scripts, in fact. Here’s an example from FindCURL.cmake:

foreach(component IN LISTS CURL_FIND_COMPONENTS)
  list(FIND CURL_KNOWN_PROTOCOLS ${component} _found)
  if(NOT _found EQUAL -1)
    list(FIND CURL_SUPPORTED_PROTOCOLS ${component} _found)
    if(NOT _found EQUAL -1)
      set(CURL_${component}_FOUND TRUE)
    elseif(CURL_FIND_REQUIRED)
      message(FATAL_ERROR "CURL: Required protocol ${component} is not found")
    endif()
  else()
    list(FIND CURL_SUPPORTED_FEATURES ${component} _found)
    if(NOT _found EQUAL -1)
      set(CURL_${component}_FOUND TRUE)
    elseif(CURL_FIND_REQUIRED)
      message(FATAL_ERROR "CURL: Required feature ${component} is not found")
    endif()
  endif()
endforeach()

If CMake truly supported a boolean list-membership test, let’s call it list(CONTAINS), one possible rewrite for that loop woud be:

foreach(component IN LISTS CURL_FIND_COMPONENTS)
  list(CONTAINS CURL_KNOWN_PROTOCOLS ${component} _known_proto)
  if(_known_proto)
    set(_type "protocol")
    list(CONTAINS CURL_SUPPORTED_PROTOCOLS ${component} CURL_${component}_FOUND)
  else()
    set(_type "feature")
    list(CONTAINS CURL_SUPPORTED_FEATURES ${component} CURL_${component}_FOUND)
  endif()
  if(CURL_FIND_REQUIRED AND NOT CURL_${component}_FOUND)
    message(FATAL_ERROR "CURL: Required ${_type} ${component} is not found")
  endif()
endforeach()

(You could also squirrel away the _type string as CURL_${component}_TYPE with 0 extra lines of code, in case it would be useful to have later.)

So my questions are:

  1. Has a list(CONTAINS) or similar been considered in the past?
  2. If so, was the idea rejected? Why?
  3. Would patches1 to add such a command be entertained?

Notes

  1. (Patches which would be fantastically easy to write, effectively HandleContainsCommand would just be a copy-paste — as much as I tend to loathe copy-paste code — of the existing HandleFindCommand from cmListCommand.cxx, but returning storing boolean values instead of list indices.)

If you want to test for list membership, there is if ("${item}" IN_LIST list_var).

2 Likes

I completely forgot about IN_LIST!

I even checked the if() docs, but

  1. Unlike the list(...) docs, there’s no handy TOC at the top to provide an overview of the possible forms. (Maybe I’ll submit a PR for that.)
  2. IN_LIST isn’t included in the set of operations listed in the Condition Syntax section.
  3. I’m blind/careless, and just somehow failed to spot it at the bottom of the Existence Checks section.

D’oh! Thanks.

In fact, the more I compare the two, the more lacking the if() documentation appears compared to list(). It really does need some love.