Browse Source

cmake_parse_arguments: add KEYWORDS_MISSING_VALUES

Add KEYWORDS_MISSING_VALUES output variable to cmake_parse_arguments() to
allow to detect keywords that received no values.

Fixes: #18706
Torsten Robitzki 6 years ago
parent
commit
5228432b45

+ 11 - 3
Help/command/cmake_parse_arguments.rst

@@ -59,6 +59,11 @@ All remaining arguments are collected in a variable
 where recognized. This can be checked afterwards to see
 where recognized. This can be checked afterwards to see
 whether your macro was called with unrecognized parameters.
 whether your macro was called with unrecognized parameters.
 
 
+``<one_value_keywords>`` and ``<multi_value_keywords>`` that where given no
+values at all are collected in a variable ``<prefix>_KEYWORDS_MISSING_VALUES``
+that will be undefined if all keywords received values. This can be checked
+to see if there where keywords without any values given.
+
 As an example here a ``my_install()`` macro, which takes similar arguments
 As an example here a ``my_install()`` macro, which takes similar arguments
 as the real :command:`install` command:
 as the real :command:`install` command:
 
 
@@ -77,7 +82,7 @@ Assume ``my_install()`` has been called like this:
 
 
 .. code-block:: cmake
 .. code-block:: cmake
 
 
-   my_install(TARGETS foo bar DESTINATION bin OPTIONAL blub)
+   my_install(TARGETS foo bar DESTINATION bin OPTIONAL blub CONFIGURATIONS)
 
 
 After the ``cmake_parse_arguments`` call the macro will have set or undefined
 After the ``cmake_parse_arguments`` call the macro will have set or undefined
 the following variables::
 the following variables::
@@ -89,6 +94,8 @@ the following variables::
    MY_INSTALL_TARGETS = "foo;bar"
    MY_INSTALL_TARGETS = "foo;bar"
    MY_INSTALL_CONFIGURATIONS <UNDEFINED> # was not used
    MY_INSTALL_CONFIGURATIONS <UNDEFINED> # was not used
    MY_INSTALL_UNPARSED_ARGUMENTS = "blub" # nothing expected after "OPTIONAL"
    MY_INSTALL_UNPARSED_ARGUMENTS = "blub" # nothing expected after "OPTIONAL"
+   MY_INSTALL_KEYWORDS_MISSING_VALUES = "CONFIGURATIONS"
+            # No value for "CONFIGURATIONS" given
 
 
 You can then continue and process these variables.
 You can then continue and process these variables.
 
 
@@ -97,5 +104,6 @@ one_value_keyword another recognized keyword follows, this is
 interpreted as the beginning of the new option.  E.g.
 interpreted as the beginning of the new option.  E.g.
 ``my_install(TARGETS foo DESTINATION OPTIONAL)`` would result in
 ``my_install(TARGETS foo DESTINATION OPTIONAL)`` would result in
 ``MY_INSTALL_DESTINATION`` set to ``"OPTIONAL"``, but as ``OPTIONAL``
 ``MY_INSTALL_DESTINATION`` set to ``"OPTIONAL"``, but as ``OPTIONAL``
-is a keyword itself ``MY_INSTALL_DESTINATION`` will be empty and
-``MY_INSTALL_OPTIONAL`` will therefore be set to ``TRUE``.
+is a keyword itself ``MY_INSTALL_DESTINATION`` will be empty (but added
+to ``MY_INSTALL_KEYWORDS_MISSING_VALUES``) and ``MY_INSTALL_OPTIONAL`` will
+therefore be set to ``TRUE``.

+ 6 - 0
Help/release/dev/cmake_parse_arguments-keywords_missing_values.rst

@@ -0,0 +1,6 @@
+cmake_parse_arguments-keywords_missing_values
+---------------------------------------------
+
+* The :command:`cmake_parse_arguments` command gained an additional
+  ``<prefix>_KEYWORDS_MISSING_VALUES`` output variable to report
+  keyword arguments that were given by the caller with no values.

+ 47 - 7
Source/cmParseArgumentsCommand.cxx

@@ -14,7 +14,7 @@
 
 
 class cmExecutionStatus;
 class cmExecutionStatus;
 
 
-static std::string escape_arg(const std::string& arg)
+static std::string EscapeArg(const std::string& arg)
 {
 {
   // replace ";" with "\;" so output argument lists will split correctly
   // replace ";" with "\;" so output argument lists will split correctly
   std::string escapedArg;
   std::string escapedArg;
@@ -28,7 +28,7 @@ static std::string escape_arg(const std::string& arg)
 }
 }
 
 
 namespace {
 namespace {
-enum InsideValues
+enum insideValues
 {
 {
   NONE,
   NONE,
   SINGLE,
   SINGLE,
@@ -38,6 +38,22 @@ enum InsideValues
 typedef std::map<std::string, bool> options_map;
 typedef std::map<std::string, bool> options_map;
 typedef std::map<std::string, std::string> single_map;
 typedef std::map<std::string, std::string> single_map;
 typedef std::map<std::string, std::vector<std::string>> multi_map;
 typedef std::map<std::string, std::vector<std::string>> multi_map;
+typedef std::set<std::string> options_set;
+}
+
+// function to be called every time, a new key word was parsed or all
+// parameters where parsed.
+static void DetectKeywordsMissingValues(insideValues currentState,
+                                        const std::string& currentArgName,
+                                        int& argumentsFound,
+                                        options_set& keywordsMissingValues)
+{
+  if (currentState == SINGLE ||
+      (currentState == MULTI && argumentsFound == 0)) {
+    keywordsMissingValues.insert(currentArgName);
+  }
+
+  argumentsFound = 0;
 }
 }
 
 
 static void PassParsedArguments(const std::string& prefix,
 static void PassParsedArguments(const std::string& prefix,
@@ -45,7 +61,8 @@ static void PassParsedArguments(const std::string& prefix,
                                 const options_map& options,
                                 const options_map& options,
                                 const single_map& singleValArgs,
                                 const single_map& singleValArgs,
                                 const multi_map& multiValArgs,
                                 const multi_map& multiValArgs,
-                                const std::vector<std::string>& unparsed)
+                                const std::vector<std::string>& unparsed,
+                                const options_set& keywordsMissingValues)
 {
 {
   for (auto const& iter : options) {
   for (auto const& iter : options) {
     makefile.AddDefinition(prefix + iter.first,
     makefile.AddDefinition(prefix + iter.first,
@@ -75,6 +92,14 @@ static void PassParsedArguments(const std::string& prefix,
   } else {
   } else {
     makefile.RemoveDefinition(prefix + "UNPARSED_ARGUMENTS");
     makefile.RemoveDefinition(prefix + "UNPARSED_ARGUMENTS");
   }
   }
+
+  if (!keywordsMissingValues.empty()) {
+    makefile.AddDefinition(
+      prefix + "KEYWORDS_MISSING_VALUES",
+      cmJoin(cmMakeRange(keywordsMissingValues), ";").c_str());
+  } else {
+    makefile.RemoveDefinition(prefix + "KEYWORDS_MISSING_VALUES");
+  }
 }
 }
 
 
 bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
 bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
@@ -161,7 +186,7 @@ bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
     multiValArgs[iter]; // default initialize
     multiValArgs[iter]; // default initialize
   }
   }
 
 
-  InsideValues insideValues = NONE;
+  insideValues insideValues = NONE;
   std::string currentArgName;
   std::string currentArgName;
 
 
   list.clear();
   list.clear();
@@ -197,10 +222,15 @@ bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
     }
     }
   }
   }
 
 
+  options_set keywordsMissingValues;
+  int multiArgumentsFound = 0;
+
   // iterate over the arguments list and fill in the values where applicable
   // iterate over the arguments list and fill in the values where applicable
   for (std::string const& arg : list) {
   for (std::string const& arg : list) {
     const options_map::iterator optIter = options.find(arg);
     const options_map::iterator optIter = options.find(arg);
     if (optIter != options.end()) {
     if (optIter != options.end()) {
+      DetectKeywordsMissingValues(insideValues, currentArgName,
+                                  multiArgumentsFound, keywordsMissingValues);
       insideValues = NONE;
       insideValues = NONE;
       optIter->second = true;
       optIter->second = true;
       continue;
       continue;
@@ -208,6 +238,8 @@ bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
 
 
     const single_map::iterator singleIter = singleValArgs.find(arg);
     const single_map::iterator singleIter = singleValArgs.find(arg);
     if (singleIter != singleValArgs.end()) {
     if (singleIter != singleValArgs.end()) {
+      DetectKeywordsMissingValues(insideValues, currentArgName,
+                                  multiArgumentsFound, keywordsMissingValues);
       insideValues = SINGLE;
       insideValues = SINGLE;
       currentArgName = arg;
       currentArgName = arg;
       continue;
       continue;
@@ -215,6 +247,8 @@ bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
 
 
     const multi_map::iterator multiIter = multiValArgs.find(arg);
     const multi_map::iterator multiIter = multiValArgs.find(arg);
     if (multiIter != multiValArgs.end()) {
     if (multiIter != multiValArgs.end()) {
+      DetectKeywordsMissingValues(insideValues, currentArgName,
+                                  multiArgumentsFound, keywordsMissingValues);
       insideValues = MULTI;
       insideValues = MULTI;
       currentArgName = arg;
       currentArgName = arg;
       continue;
       continue;
@@ -226,15 +260,18 @@ bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
         insideValues = NONE;
         insideValues = NONE;
         break;
         break;
       case MULTI:
       case MULTI:
+        ++multiArgumentsFound;
         if (parseFromArgV) {
         if (parseFromArgV) {
-          multiValArgs[currentArgName].push_back(escape_arg(arg));
+          multiValArgs[currentArgName].push_back(EscapeArg(arg));
         } else {
         } else {
           multiValArgs[currentArgName].push_back(arg);
           multiValArgs[currentArgName].push_back(arg);
         }
         }
         break;
         break;
       default:
       default:
+        multiArgumentsFound = 0;
+
         if (parseFromArgV) {
         if (parseFromArgV) {
-          unparsed.push_back(escape_arg(arg));
+          unparsed.push_back(EscapeArg(arg));
         } else {
         } else {
           unparsed.push_back(arg);
           unparsed.push_back(arg);
         }
         }
@@ -242,8 +279,11 @@ bool cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
     }
     }
   }
   }
 
 
+  DetectKeywordsMissingValues(insideValues, currentArgName,
+                              multiArgumentsFound, keywordsMissingValues);
+
   PassParsedArguments(prefix, *this->Makefile, options, singleValArgs,
   PassParsedArguments(prefix, *this->Makefile, options, singleValArgs,
-                      multiValArgs, unparsed);
+                      multiValArgs, unparsed, keywordsMissingValues);
 
 
   return true;
   return true;
 }
 }

+ 133 - 0
Tests/RunCMake/cmake_parse_arguments/KeyWordsMissingValues.cmake

@@ -0,0 +1,133 @@
+include(${CMAKE_CURRENT_LIST_DIR}/test_utils.cmake)
+
+# No keywords that miss any values, _KEYWORDS_MISSING_VALUES should not be defined
+cmake_parse_arguments(PREF "" "P1" "P2" P1 p1 P2 p2_a p2_b)
+
+TEST(PREF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+
+# Keyword should even be deleted from the actual scope
+set(PREF_KEYWORDS_MISSING_VALUES "What ever")
+cmake_parse_arguments(PREF "" "" "")
+
+TEST(PREF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+
+# Given missing keywords as only option
+cmake_parse_arguments(PREF "" "P1" "P2" P1)
+
+TEST(PREF_KEYWORDS_MISSING_VALUES "P1")
+TEST(PREF_P1 "UNDEFINED")
+TEST(PREF_UNPARSED_ARGUMENTS "UNDEFINED")
+
+# Mixed with unparsed arguments
+cmake_parse_arguments(UPREF "" "P1" "P2" A B P2 C P1)
+TEST(UPREF_KEYWORDS_MISSING_VALUES "P1")
+TEST(UPREF_UNPARSED_ARGUMENTS A B)
+
+# one_value_keyword followed by option
+cmake_parse_arguments(REF "OP" "P1" "" P1 OP)
+TEST(REF_KEYWORDS_MISSING_VALUES "P1")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_OP "TRUE")
+
+# Counter Test
+cmake_parse_arguments(REF "OP" "P1" "" P1 p1 OP)
+TEST(REF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+TEST(REF_P1 "p1")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_OP "TRUE")
+
+# one_value_keyword followed by a one_value_keyword
+cmake_parse_arguments(REF "" "P1;P2" "" P1 P2 p2)
+TEST(REF_KEYWORDS_MISSING_VALUES "P1")
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 "p2")
+
+# Counter Test
+cmake_parse_arguments(REF "" "P1;P2" "" P1 p1 P2 p2)
+TEST(REF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+TEST(REF_P1 "p1")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 "p2")
+
+# one_value_keyword followed by a multi_value_keywords
+cmake_parse_arguments(REF "" "P1" "P2" P1 P2 p1 p2)
+TEST(REF_KEYWORDS_MISSING_VALUES "P1")
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 p1 p2)
+
+# Counter Examples
+cmake_parse_arguments(REF "" "P1" "P2" P1 p1 P2 p1 p2)
+TEST(REF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+TEST(REF_P1 "p1")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 p1 p2)
+
+# multi_value_keywords as only option
+cmake_parse_arguments(REF "" "P1" "P2" P2)
+TEST(REF_KEYWORDS_MISSING_VALUES "P2")
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 "UNDEFINED")
+
+# multi_value_keywords followed by option
+cmake_parse_arguments(REF "O1" "" "P1" P1 O1)
+TEST(REF_KEYWORDS_MISSING_VALUES "P1")
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_O1 "TRUE")
+
+# counter test
+cmake_parse_arguments(REF "O1" "" "P1" P1 p1 p2 O1)
+TEST(REF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+TEST(REF_P1 "p1;p2")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_O1 "TRUE")
+
+# multi_value_keywords followed by one_value_keyword
+cmake_parse_arguments(REF "" "P1" "P2" P2 P1 p1)
+TEST(REF_KEYWORDS_MISSING_VALUES "P2")
+TEST(REF_P1 "p1")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 "UNDEFINED")
+
+# counter test
+cmake_parse_arguments(REF "" "P1" "P2" P2 p2 P1 p1)
+TEST(REF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+TEST(REF_P1 "p1")
+TEST(REF_UNPARSED_ARGUMENTS "UNDEFINED")
+TEST(REF_P2 "p2")
+
+# one_value_keyword as last argument
+cmake_parse_arguments(REF "" "P1" "P2" A P2 p2 P1)
+TEST(REF_KEYWORDS_MISSING_VALUES "P1")
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_UNPARSED_ARGUMENTS "A")
+TEST(REF_P2 "p2")
+
+# multi_value_keywords as last argument
+cmake_parse_arguments(REF "" "P1" "P2" P1 p1 P2)
+TEST(REF_KEYWORDS_MISSING_VALUES "P2")
+TEST(REF_P1 "p1")
+TEST(REF_P2 "UNDEFINED")
+
+# Multiple one_value_keyword and multi_value_keywords at different places
+cmake_parse_arguments(REF "O1;O2" "P1" "P2" P1 O1 P2 O2)
+TEST(REF_KEYWORDS_MISSING_VALUES P1 P2)
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_P2 "UNDEFINED")
+
+# Duplicated missing keywords
+cmake_parse_arguments(REF "O1;O2" "P1" "P2" P1 O1 P2 O2 P1 P2)
+TEST(REF_KEYWORDS_MISSING_VALUES P1 P2)
+TEST(REF_P1 "UNDEFINED")
+TEST(REF_P2 "UNDEFINED")
+
+# make sure keywords that are never used, don't get added to KEYWORDS_MISSING_VALUES
+cmake_parse_arguments(REF "O1;O2" "P1" "P2")
+TEST(REF_KEYWORDS_MISSING_VALUES "UNDEFINED")
+TEST(REF_O1 FALSE)
+TEST(REF_O2 FALSE)
+TEST(REF_P1 UNDEFINED)
+TEST(REF_P2 UNDEFINED)

+ 1 - 0
Tests/RunCMake/cmake_parse_arguments/RunCMakeTest.cmake

@@ -11,3 +11,4 @@ run_cmake(BadArgvN2)
 run_cmake(BadArgvN3)
 run_cmake(BadArgvN3)
 run_cmake(BadArgvN4)
 run_cmake(BadArgvN4)
 run_cmake(CornerCasesArgvN)
 run_cmake(CornerCasesArgvN)
+run_cmake(KeyWordsMissingValues)