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.
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?
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):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: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 🙂