# # Copyright (c) Facebook, Inc. and its affiliates. # # Helper function for parsing arguments to a CMake function. # # This function is very similar to CMake's built-in cmake_parse_arguments() # function, with some improvements: # - This function correctly handles empty arguments. (cmake_parse_arguments() # ignores empty arguments.) # - If a multi-value argument is specified more than once, the subsequent # arguments are appended to the original list rather than replacing it. e.g. # if "SOURCES" is a multi-value argument, and the argument list contains # "SOURCES a b c SOURCES x y z" then the resulting value for SOURCES will be # "a;b;c;x;y;z" rather than "x;y;z" # - This function errors out by default on unrecognized arguments. You can # pass in an extra "ALLOW_UNPARSED_ARGS" argument to make it behave like # cmake_parse_arguments(), and return the unparsed arguments in a # _UNPARSED_ARGUMENTS variable instead. # # It does look like cmake_parse_arguments() handled empty arguments correctly # from CMake 3.0 through 3.3, but it seems like this was probably broken when # it was turned into a built-in function in CMake 3.4. Here is discussion and # patches that fixed this behavior prior to CMake 3.0: # https://cmake.org/pipermail/cmake-developers/2013-November/020607.html # # The one downside to this function over the built-in cmake_parse_arguments() # is that I don't think we can achieve the PARSE_ARGV behavior in a non-builtin # function, so we can't properly handle arguments that contain ";". CMake will # treat the ";" characters as list element separators, and treat it as multiple # separate arguments. # function(fb_cmake_parse_args PREFIX OPTIONS ONE_VALUE_ARGS MULTI_VALUE_ARGS ARGS) foreach(option IN LISTS ARGN) if ("${option}" STREQUAL "ALLOW_UNPARSED_ARGS") set(ALLOW_UNPARSED_ARGS TRUE) else() message( FATAL_ERROR "unknown optional argument for fb_cmake_parse_args(): ${option}" ) endif() endforeach() # Define all options as FALSE in the parent scope to start with foreach(var_name IN LISTS OPTIONS) set("${PREFIX}_${var_name}" "FALSE" PARENT_SCOPE) endforeach() # TODO: We aren't extremely strict about error checking for one-value # arguments here. e.g., we don't complain if a one-value argument is # followed by another option/one-value/multi-value name rather than an # argument. We also don't complain if a one-value argument is the last # argument and isn't followed by a value. list(APPEND all_args ${ONE_VALUE_ARGS}) list(APPEND all_args ${MULTI_VALUE_ARGS}) set(current_variable) set(unparsed_args) foreach(arg IN LISTS ARGS) list(FIND OPTIONS "${arg}" opt_index) if("${opt_index}" EQUAL -1) list(FIND all_args "${arg}" arg_index) if("${arg_index}" EQUAL -1) # This argument does not match an argument name, # must be an argument value if("${current_variable}" STREQUAL "") list(APPEND unparsed_args "${arg}") else() # Ugh, CMake lists have a pretty fundamental flaw: they cannot # distinguish between an empty list and a list with a single empty # element. We track our own SEEN_VALUES_arg setting to help # distinguish this and behave properly here. if ("${SEEN_${current_variable}}" AND "${${current_variable}}" STREQUAL "") set("${current_variable}" ";${arg}") else() list(APPEND "${current_variable}" "${arg}") endif() set("SEEN_${current_variable}" TRUE) endif() else() # We found a single- or multi-value argument name set(current_variable "VALUES_${arg}") set("SEEN_${arg}" TRUE) endif() else() # We found an option variable set("${PREFIX}_${arg}" "TRUE" PARENT_SCOPE) set(current_variable) endif() endforeach() foreach(arg_name IN LISTS ONE_VALUE_ARGS) if(NOT "${SEEN_${arg_name}}") unset("${PREFIX}_${arg_name}" PARENT_SCOPE) elseif(NOT "${SEEN_VALUES_${arg_name}}") # If the argument was seen but a value wasn't specified, error out. # We require exactly one value to be specified. message( FATAL_ERROR "argument ${arg_name} was specified without a value" ) else() list(LENGTH "VALUES_${arg_name}" num_args) if("${num_args}" EQUAL 0) # We know an argument was specified and that we called list(APPEND). # If CMake thinks the list is empty that means there is really a single # empty element in the list. set("${PREFIX}_${arg_name}" "" PARENT_SCOPE) elseif("${num_args}" EQUAL 1) list(GET "VALUES_${arg_name}" 0 arg_value) set("${PREFIX}_${arg_name}" "${arg_value}" PARENT_SCOPE) else() message( FATAL_ERROR "too many arguments specified for ${arg_name}: " "${VALUES_${arg_name}}" ) endif() endif() endforeach() foreach(arg_name IN LISTS MULTI_VALUE_ARGS) # If this argument name was never seen, then unset the parent scope if (NOT "${SEEN_${arg_name}}") unset("${PREFIX}_${arg_name}" PARENT_SCOPE) else() # TODO: Our caller still won't be able to distinguish between an empty # list and a list with a single empty element. We can tell which is # which, but CMake lists don't make it easy to show this to our caller. set("${PREFIX}_${arg_name}" "${VALUES_${arg_name}}" PARENT_SCOPE) endif() endforeach() # By default we fatal out on unparsed arguments, but return them to the # caller if ALLOW_UNPARSED_ARGS was specified. if (DEFINED unparsed_args) if ("${ALLOW_UNPARSED_ARGS}") set("${PREFIX}_UNPARSED_ARGUMENTS" "${unparsed_args}" PARENT_SCOPE) else() message(FATAL_ERROR "unrecognized arguments: ${unparsed_args}") endif() endif() endfunction()