Professional CMake:

A Practical Guide

Learn to use CMake effectively with practical advice from a CMake co-maintainer. You can also have the author work directly with your team!

Forwarding Command Arguments In CMake

The previous article discussed an example from Dan Pfeifer’s popular Effective CMake talk. That article highlighted the dangers of trying to override a function and forward the call to the original implementation. We now look at a related but more subtle problem associated specifically with forwarding command arguments. The same example from the previous article will serve as our starting point:

macro(find_package)
  if(NOT "${ARG0}" IN_LIST as_subproject)
    _find_package(${ARGV})
  endif()
endmacro()

Command Argument Packing

In its attempt to forward the caller’s arguments to the original implementation of find_package(), the above is making use of the way CMake implements function arguments. Internally, CMake collapses the full set of arguments into a single list and then assigns them firstly to the named arguments of the called function, and then any remaining leftover arguments are made available to the function as the ${ARGN} variable. The whole single list of arguments is also made available as the ${ARGV} variable, containing both the named and the unnamed arguments. Both variables are equivalent if there are no named arguments.

To see this argument-packing in action, consider the following:

function(foo)
    message("ARGV = ${ARGV}")
endfunction()

foo(a b c)

This produces the following output:

ARGV = a;b;c

Things get more interesting if one of the arguments has an embedded semicolon, as the following demonstrates:

function(foo P1 P2 P3)
    message("ARGV = ${ARGV}")
    message("P1 = ${P1}")
    message("P2 = ${P2}")
    message("P3 = ${P3}")
    foreach(i IN LISTS ARGV)
        message("${i}")
    endforeach()
endfunction()

foo(a "b;c" d)

This expanded example produces the following output:

ARGV = a;b;c;d
P1 = a
P2 = b;c
P3 = d
a
b
c
d

The second argument in the call to foo() has an embedded semicolon, but notice how the set of arguments stored in ARGV has discarded that important piece of information and treated it as just another list separator. This highlights how ARGV is not always a true representation of the original arguments.

Dangers Of List-Flattening

Some of CMake’s own modules use the above technique for accumulating options to pass through to a command where it is known to be safe to do so. But developers need to be aware that ARGV and ARGN are not always a safe way to forward arguments in general. Consider the way a command like set_tests_properties() works:

set_tests_properties(test1 test2 ... PROPERTIES
                     prop1 value1
                     prop2 value2 ...)

For single-valued properties, the command usage is straightforward, but for list-valued properties, one has to pass through that list as a single argument like so:

set_tests_properties(fooFixtures PROPERTIES
                     FIXTURES_REQUIRED "db;server")

If we wrote a naive function that was intended to forward through its arguments to this command, it could lead to problems:

# Additional arguments expected to be property name/value pairs
function(dangerFunc testName)
    ... do other processing ...
    if(ARGN)
        set_tests_properties(testName PROPERTIES ${ARGN})
    endif()
endfunction()

dangerFunc(fooFixtures FIXTURES_REQUIRED "db;server")

In the above, set_tests_properties() would actually see the following call:

set_tests_properties(fooFixtures PROPERTIES FIXTURES_REQUIRED db server)

This would result in server being treated as a second property name instead of as part of the FIXTURES_REQUIRED property value.

Preserving Empty Values

Another trap for the unwary is that empty arguments will be dropped if they are not quoted arguments. Consider the following example:

function(bar)
  message("bar ARGV = ${ARGV}")
endfunction()

function(foo)
  message("foo ARGV = ${ARGV}")
  bar(${ARGV})
endfunction()

foo(abc 123 "" boo)

The output from the above would be:

foo ARGV = abc;123;;boo
bar ARGV = abc;123;boo

The third argument passed to foo() is an empty string. Because that empty argument is a single quoted argument, it is preserved and passed through into foo(). Inside foo(), that empty argument exists in ARGV, but it is unquoted. When ${ARGV} is used as the arguments to bar(), that empty argument is no longer quoted and it gets dropped. Attempting to replace empty arguments in a list with embedded quotes and then passing that list as the set of command arguments results in the quotes being passed through instead of an empty string. Named arguments are typically required to get out of this situation:

function(bar P1 P2 P3)
  message("bar P1 = '${P1}', P2 = '${P2}', P3 = '${P3}'")
  message("bar ARGV = ${ARGV}")
endfunction()

function(foo P1 P2 P3)
  message("foo P1 = '${P1}', P2 = '${P2}', P3 = '${P3}'")
  message("foo ARGV = ${ARGV}")
  bar("${P1}" "${P2}" "${P3}" ${ARGN})
endfunction()

foo(abc 123 "" boo)

Using the above named arguments yields the output:

foo P1 = 'abc', P2 = '123', P3 = ''
foo ARGV = abc;123;;boo
bar P1 = 'abc', P2 = '123', P3 = ''
bar ARGV = abc;123;;boo

Robustness To New Command Options

Returning briefly to our first example, one might raise the point that the find_package() command doesn’t have any arguments that could/should contain a semicolon and any empty strings can safely be dropped. Therefore, the method used in the original example should be okay for forwarding arguments to that command.

While find_package() might not take any arguments that could have an embedded semicolon with the current CMake release, there’s nothing to say that support for such an option might not be added in the future. If that were to occur, then an implementation using the above argument-forwarding technique would no longer pass through those arguments safely. If you were especially unlucky, it might not lead to an error, but instead to a silent change in behavior. Therefore, implementors need to consider whether the command being forwarded to is likely to receive options of that nature in the future before committing to using ARGV and ARGN for forwarding arguments.

Closing Remarks

Be aware of the limitations of ARGV and ARGN. When used to forward arguments, list flattening causes empty strings to be discarded and embedded semicolons will become argument separators. Use ARGV and ARGN for forwarding only in scenarios where it can be guaranteed that neither of these two behaviors will cause problems. In some cases, dropping empty items or flattening embedded lists can actually be helpful, but this is often not the case.

For commands that handle arbitrary properties directly, this is a risky area. A number of the property-setting commands expect alternating key/value pairs in their argument lists. Commands that manipulate test properties are especially fragile, since many test properties accept lists and therefore must be quoted to pass through to the command correctly. For such properties, their values cannot be forwarded safely without elaborate methods that will likely involve assumptions about the values.


Have a CMake maintainer work on your project

Get the book for more CMake content

3 thoughts on “Forwarding Command Arguments In CMake”

  1. After tinkering on my code for a whole day, I’ve hit this very limitation of CMAKE. My wrapper macro is unable to pass its arguments to a second command call without breaking and I’ve run out of ideas. In your experience, is there a way to work around this issue for “generic” situations where empty string and “;” might be contained in the original arguments?

    Reply
    • With CMake 3.18 or later, you can now at least forward arguments robustly for functions. The cmake_language(EVAL CODE) command provides the missing piece (this example and the one further below are taken from my Professional CMake: A Practical Guide book):

      function(outer)
          cmake_parse_arguments(PARSE_ARGV 0 FWD "" "" "")
          set(quotedArgs "")
          foreach(arg IN LISTS FWD_UNPARSED_ARGUMENTS)
              string(APPEND quotedArgs " [===[${arg}]===]")
          endforeach()
          cmake_language(EVAL CODE "inner(${quotedArgs})")
      endfunction()
      

      Macros are harder, because cmake_parse_arguments(PARSE_ARGV) only works for functions, so you can’t use the above for macros. If the “list flattening” mentioned in the article isn’t a problem for your case, the following addresses the remaining issues:

      # WARNING: This example does not preserve list structure.
      #          It does preserve empty string arguments.
      macro(outer)
          string(REPLACE ";" "]===] [===[" args "[===[${ARGV}]===]")
          cmake_language(EVAL CODE "inner(${args})")
      endmacro()
      
      Reply
      • Thanks Craig, I’m going to try this straight away. I think macros wont work in my case due to list flattening as you mention, but wrapper functions should do the trick. I had never seen the “[===[]===]” separator before; I’ll checkout Professional CMake to get up to speed 🙂

        Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.