Browse Source

GenEx: Add $<STRING> generator expression

Fixes: #27188
Marc Chevrier 1 month ago
parent
commit
fd7e305097
71 changed files with 1947 additions and 50 deletions
  1. 345 3
      Help/manual/cmake-generator-expressions.7.rst
  2. 8 0
      Help/release/dev/GenEx-STRING.rst
  3. 598 47
      Source/cmGeneratorExpressionNode.cxx
  4. 1 0
      Tests/RunCMake/CMakeLists.txt
  5. 22 0
      Tests/RunCMake/GenEx-STRING/APPEND.cmake.in
  6. 1 0
      Tests/RunCMake/GenEx-STRING/ASCII-WrongArguments-result.txt
  7. 38 0
      Tests/RunCMake/GenEx-STRING/ASCII-WrongArguments-stderr.txt
  8. 7 0
      Tests/RunCMake/GenEx-STRING/ASCII-WrongArguments.cmake
  9. 20 0
      Tests/RunCMake/GenEx-STRING/ASCII.cmake.in
  10. 5 0
      Tests/RunCMake/GenEx-STRING/CMakeLists.txt
  11. 1 0
      Tests/RunCMake/GenEx-STRING/FIND-WrongArguments-result.txt
  12. 18 0
      Tests/RunCMake/GenEx-STRING/FIND-WrongArguments-stderr.txt
  13. 5 0
      Tests/RunCMake/GenEx-STRING/FIND-WrongArguments.cmake
  14. 48 0
      Tests/RunCMake/GenEx-STRING/FIND.cmake.in
  15. 1 0
      Tests/RunCMake/GenEx-STRING/HASH-WrongArguments-result.txt
  16. 18 0
      Tests/RunCMake/GenEx-STRING/HASH-WrongArguments-stderr.txt
  17. 5 0
      Tests/RunCMake/GenEx-STRING/HASH-WrongArguments.cmake
  18. 69 0
      Tests/RunCMake/GenEx-STRING/HASH.cmake.in
  19. 20 0
      Tests/RunCMake/GenEx-STRING/HEX.cmake.in
  20. 32 0
      Tests/RunCMake/GenEx-STRING/JOIN.cmake.in
  21. 21 0
      Tests/RunCMake/GenEx-STRING/LENGTH.cmake.in
  22. 1 0
      Tests/RunCMake/GenEx-STRING/MATCH-WrongArguments-result.txt
  23. 28 0
      Tests/RunCMake/GenEx-STRING/MATCH-WrongArguments-stderr.txt
  24. 8 0
      Tests/RunCMake/GenEx-STRING/MATCH-WrongArguments.cmake
  25. 40 0
      Tests/RunCMake/GenEx-STRING/MATCH.cmake.in
  26. 22 0
      Tests/RunCMake/GenEx-STRING/PREPEND.cmake.in
  27. 1 0
      Tests/RunCMake/GenEx-STRING/QUOTE-WrongArguments-result.txt
  28. 8 0
      Tests/RunCMake/GenEx-STRING/QUOTE-WrongArguments-stderr.txt
  29. 6 0
      Tests/RunCMake/GenEx-STRING/QUOTE-WrongArguments.cmake
  30. 21 0
      Tests/RunCMake/GenEx-STRING/QUOTE.cmake.in
  31. 1 0
      Tests/RunCMake/GenEx-STRING/RANDOM-WrongArguments-result.txt
  32. 18 0
      Tests/RunCMake/GenEx-STRING/RANDOM-WrongArguments-stderr.txt
  33. 5 0
      Tests/RunCMake/GenEx-STRING/RANDOM-WrongArguments.cmake
  34. 32 0
      Tests/RunCMake/GenEx-STRING/RANDOM.cmake.in
  35. 1 0
      Tests/RunCMake/GenEx-STRING/REPLACE-WrongArguments-result.txt
  36. 18 0
      Tests/RunCMake/GenEx-STRING/REPLACE-WrongArguments-stderr.txt
  37. 7 0
      Tests/RunCMake/GenEx-STRING/REPLACE-WrongArguments.cmake
  38. 44 0
      Tests/RunCMake/GenEx-STRING/REPLACE.cmake.in
  39. 80 0
      Tests/RunCMake/GenEx-STRING/RunCMakeTest.cmake
  40. 1 0
      Tests/RunCMake/GenEx-STRING/STRIP-WrongArguments-result.txt
  41. 8 0
      Tests/RunCMake/GenEx-STRING/STRIP-WrongArguments-stderr.txt
  42. 6 0
      Tests/RunCMake/GenEx-STRING/STRIP-WrongArguments.cmake
  43. 14 0
      Tests/RunCMake/GenEx-STRING/STRIP.cmake.in
  44. 1 0
      Tests/RunCMake/GenEx-STRING/SUBSTRING-WrongArguments-result.txt
  45. 38 0
      Tests/RunCMake/GenEx-STRING/SUBSTRING-WrongArguments-stderr.txt
  46. 7 0
      Tests/RunCMake/GenEx-STRING/SUBSTRING-WrongArguments.cmake
  47. 26 0
      Tests/RunCMake/GenEx-STRING/SUBSTRING.cmake.in
  48. 1 0
      Tests/RunCMake/GenEx-STRING/TIMESTAMP-WrongArguments-result.txt
  49. 8 0
      Tests/RunCMake/GenEx-STRING/TIMESTAMP-WrongArguments-stderr.txt
  50. 4 0
      Tests/RunCMake/GenEx-STRING/TIMESTAMP-WrongArguments.cmake
  51. 27 0
      Tests/RunCMake/GenEx-STRING/TIMESTAMP.cmake.in
  52. 26 0
      Tests/RunCMake/GenEx-STRING/TOLOWER.cmake.in
  53. 26 0
      Tests/RunCMake/GenEx-STRING/TOUPPER.cmake.in
  54. 1 0
      Tests/RunCMake/GenEx-STRING/UUID-WrongArguments-result.txt
  55. 28 0
      Tests/RunCMake/GenEx-STRING/UUID-WrongArguments-stderr.txt
  56. 6 0
      Tests/RunCMake/GenEx-STRING/UUID-WrongArguments.cmake
  57. 32 0
      Tests/RunCMake/GenEx-STRING/UUID.cmake.in
  58. 1 0
      Tests/RunCMake/GenEx-STRING/bad-option-result.txt
  59. 8 0
      Tests/RunCMake/GenEx-STRING/bad-option-stderr.txt
  60. 2 0
      Tests/RunCMake/GenEx-STRING/bad-option.cmake
  61. 13 0
      Tests/RunCMake/GenEx-STRING/check_errors.cmake
  62. 6 0
      Tests/RunCMake/GenEx-STRING/generate.cmake
  63. 1 0
      Tests/RunCMake/GenEx-STRING/no-arguments-result.txt
  64. 8 0
      Tests/RunCMake/GenEx-STRING/no-arguments-stderr.txt
  65. 2 0
      Tests/RunCMake/GenEx-STRING/no-arguments.cmake
  66. 1 0
      Tests/RunCMake/GenEx-STRING/unexpected-arg-result.txt
  67. 8 0
      Tests/RunCMake/GenEx-STRING/unexpected-arg-stderr.txt
  68. 2 0
      Tests/RunCMake/GenEx-STRING/unexpected-arg.cmake
  69. 1 0
      Tests/RunCMake/GenEx-STRING/unexpected-arg2-result.txt
  70. 8 0
      Tests/RunCMake/GenEx-STRING/unexpected-arg2-stderr.txt
  71. 2 0
      Tests/RunCMake/GenEx-STRING/unexpected-arg2.cmake

+ 345 - 3
Help/manual/cmake-generator-expressions.7.rst

@@ -321,6 +321,21 @@ Version Comparisons
 String Expressions
 ------------------
 
+Most of the expressions in this section are closely associated with the
+:command:`string` command, providing the same capabilities, but in
+the form of a generator expression.
+
+In each of the following string-related generator expressions, the ``string``
+must not contain any commas if that generator expression expects something to
+be provided after the ``string``.  For example, the expression
+``$<STRING:FIND,string,value>`` requires a ``value`` after the ``string``.
+Since a comma is used to separate the ``string`` and the ``value``, the
+``string`` cannot itself contain a comma.  This restriction does not apply to
+the :command:`string` command, it is specific to the string-handling generator
+expressions only. The :genex:`$<COMMA>` generator expression can be used to
+specify a comma as part of the arguments of the string-related generator
+expressions.
+
 .. _`String Comparisons Generator Expressions`:
 
 String Comparisons
@@ -333,7 +348,7 @@ evaluates to ``1`` if ``${foo}`` is any of ``BAR``, ``Bar``, ``bar``, etc.
 
   .. code-block:: cmake
 
-    $<STREQUAL:$<UPPER_CASE:${foo}>,BAR>
+    $<STREQUAL:$<STRING:TOUPPER,${foo}>,BAR>
 
 .. genex:: $<STREQUAL:string1,string2>
 
@@ -366,10 +381,337 @@ evaluates to ``1`` if ``${foo}`` is any of ``BAR``, ``Bar``, ``bar``, etc.
   ``1`` if ``string1`` is lexicographically greater than or equal to
   ``string2``, else ``0``.
 
+.. _`String Queries Generator Expressions`:
+
+String Queries
+^^^^^^^^^^^^^^
+
+.. genex:: $<STRING:LENGTH,string>
+
+  .. versionadded:: 4.3
+
+  The given string's length in bytes. Note that this means, if ``string``
+  contains multi-byte characters, the result will *not* be the number of
+  characters.
+
+.. genex:: $<STRING:SUBSTRING,string,begin,length>
+
+  .. versionadded:: 4.3
+
+  The substring of the given ``string``. If ``length`` is ``-1`` or greater
+  than the ``string`` length the remainder of the string starting at ``begin``
+  will be returned.
+
+  Both ``begin`` and ``length`` are counted in bytes, so care must
+  be exercised if ``string`` could contain multi-byte characters.
+
+.. genex:: $<STRING:FIND,string[,FROM:(BEGIN|END)],substring>
+
+  The position where the given ``substring`` was found in the supplied
+  ``string``. If the ``substring`` is not found, a position of -1 is returned.
+
+  The ``FROM:`` option defines how the search will be done:
+
+  ``BEGIN``
+    The search will start at the beginning of the ``string``. This the default.
+
+  ``END``
+    The search will start from the end of the ``string``.
+
+  The ``$<STRING:FIND>`` generator expression treats all strings as ASCII-only
+  characters. The index returned will also be counted in bytes, so strings
+  containing multi-byte characters may lead to unexpected results.
+
+.. genex:: $<STRING:MATCH,string[,SEEK:(ONCE|ALL)],regular_expression>
+
+  .. versionadded:: 4.3
+
+  Match, in the ``string``, the ``regular_expression``.
+
+  The ``SEEK:`` option specifies the match behavior:
+
+  ``ONCE``
+    Match only the first occurrence. This is the default.
+
+  ``ALL``
+    Match as many times as possible and return the matches as a list.
+
+  See the :ref:`Regular expressions specification <Regex Specification>` for
+  the syntax of the ``regular_expression`` parameter.
+
+.. _`String Generating Generator Expressions`:
+
+String Generations
+^^^^^^^^^^^^^^^^^^
+
+.. genex:: $<STRING:JOIN,glue,input[,input]...>
+
+  .. versionadded:: 4.3
+
+  Join all the ``input`` arguments together using the ``glue`` string.
+
+.. genex:: $<STRING:ASCII,number[,number]...>
+
+  .. versionadded:: 4.3
+
+  Convert all numbers, in the range 1-255, into corresponding ASCII
+  characters. Any number outside this range will raise an error.
+
+.. genex:: $<STRING:TIMESTAMP[,(UTC|format)]...>
+
+  .. versionadded:: 4.3
+
+  Produce a string representation of the current date and/or time.
+
+  If the generator expression is unable to obtain a timestamp, the result will
+  be the empty string ``""``.
+
+  The optional ``UTC`` flag requests the current date/time representation to
+  be in Coordinated Universal Time (UTC) rather than local time.
+
+  If the ``SOURCE_DATE_EPOCH`` environment variable is set, its value will be
+  used instead of the current time.
+  See https://reproducible-builds.org/specs/source-date-epoch/ for details.
+
+  The optional ``<format>`` may contain the following format specifiers:
+
+  ``%%``
+    A literal percent sign (%).
+
+  ``%d``
+    The day of the current month (01-31).
+
+  ``%H``
+    The hour on a 24-hour clock (00-23).
+
+  ``%I``
+    The hour on a 12-hour clock (01-12).
+
+  ``%j``
+    The day of the current year (001-366).
+
+  ``%m``
+    The month of the current year (01-12).
+
+  ``%b``
+    Abbreviated month name (e.g. Oct).
+
+  ``%B``
+    Full month name (e.g. October).
+
+  ``%M``
+    The minute of the current hour (00-59).
+
+  ``%s``
+    Seconds since midnight (UTC) 1-Jan-1970 (UNIX time).
+
+  ``%S``
+    The second of the current minute.  60 represents a leap second. (00-60)
+
+  ``%f``
+    The microsecond of the current second (000000-999999).
+
+  ``%U``
+    The week number of the current year (00-53).
+
+  ``%V``
+    The ISO 8601 week number of the current year (01-53).
+
+  ``%w``
+    The day of the current week. 0 is Sunday. (0-6)
+
+  ``%a``
+    Abbreviated weekday name (e.g. Fri).
+
+  ``%A``
+    Full weekday name (e.g. Friday).
+
+  ``%y``
+    The last two digits of the current year (00-99).
+
+  ``%Y``
+    The current year.
+
+  ``%z``
+    The offset of the time zone from UTC, in hours and minutes,
+    with format ``+hhmm`` or ``-hhmm``.
+
+  ``%Z``
+    The time zone name.
+
+  Unknown format specifiers will be ignored and copied to the output
+  as-is.
+
+  If no explicit ``format`` is given, it will default to:
+
+  * ``%Y-%m-%dT%H:%M:%S`` for local time.
+  * ``%Y-%m-%dT%H:%M:%SZ`` for UTC.
+
+.. genex:: $<STRING:RANDOM[,(LENGTH:length|ALPHABET:alphabet|RANDOM_SEED:seed)]...>
+
+  .. versionadded:: 4.3
+
+  Produce a random string of ASCII characters. The possible options are:
+
+  ``LENGTH:length``
+    Define the length of the string. The default length is 5 characters.
+
+  ``ALPHABET:alphabet``
+    Define the characters used for the generation. The alphabet is always
+    interpreted as holding ASCII characters. The default alphabet is all
+    numbers and upper and lower case letters.
+
+  ``RANDOM_SEED:seed``
+    Specify an integer which will be used to seed the random number generator.
+
+.. genex:: $<STRING:UUID,NAMESPACE:namespace,TYPE:(MD5|SHA1)[,NAME:name][,CASE:(LOWER|UPPER)]>
+
+  .. versionadded:: 4.3
+
+  Create a universally unique identifier (aka GUID) as per RFC4122.
+  A UUID has the format ``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx``
+  where each ``x`` represents an hexadecimal character.
+
+  The UUID is based on the hash of the combined values of:
+
+  ``NAMESPACE:namespace``
+    ``namespace`` which has to be a valid UUID.
+
+  ``NAME:name``
+    ``name`` is an arbitrary string.
+
+  ``TYPE:``
+    The hash algorithm can be either:
+
+    ``MD5``
+      Version 3 UUID.
+
+    ``SHA1``
+      Version 5 UUID.
+
+  ``CASE:``
+    Specify the case of the hexadecimal characters.
+
+    ``LOWER``
+      Hexadecimal characters are all of lowercase. This is the default.
+
+    ``UPPER``
+      Hexadecimal characters are all of uppercase.
+
+.. _`String Transforming Generator Expressions`:
 
 String Transformations
 ^^^^^^^^^^^^^^^^^^^^^^
 
+.. genex:: $<STRING:REPLACE[,(STRING|REGEX)],string,match_string,replace_string>
+
+  .. versionadded:: 4.3
+
+  Replace all occurrences of ``match_string`` in the ``string`` with
+  ``replace_string``.
+
+  The ``match_string`` can be of two different types:
+
+  ``STRING``
+    ``match_string`` is a literal string and match will be done by simple
+    string comparison. This is the default.
+
+  ``REGEX``
+    ``match_string`` is a regular expression. Match this regular_expression as
+    many times as possible and substitute the ``replace_string`` for the match
+    in the ``string``.
+
+    The ``replace_string`` may refer to parenthesis-delimited subexpressions of
+    the match using \\1, \\2, ..., \\9. Note that two backslashes (\\\\1) are
+    required in CMake code to get a backslash through argument parsing.
+
+.. genex:: $<STRING:APPEND,string,input[,input]...>
+
+  .. versionadded:: 4.3
+
+  Append all the ``input`` arguments to the ``string``.
+
+.. genex:: $<STRING:PREPEND,string,input[,input]...>
+
+  .. versionadded:: 4.3
+
+  Prepend all the ``input`` arguments to the ``string``.
+
+.. genex:: $<STRING:TOLOWER,string>
+
+  .. versionadded:: 4.3
+
+  Content of ``string`` converted to lower case.
+
+.. genex:: $<STRING:TOUPPER,string>
+
+  .. versionadded:: 4.3
+
+  Content of ``string`` converted to upper case.
+
+.. genex:: $<STRING:STRIP,SPACES,string>
+
+  .. versionadded:: 4.3
+
+  Remove the specified elements from the ``string``. The possible options are:
+
+  ``SPACES``
+    Remove the leading and trailing spaces of the ``string``.
+
+.. genex:: $<STRING:QUOTE,REGEX,string>
+
+  .. versionadded:: 4.3
+
+  Escape the specified elements of the ``string``. The possible options are:
+
+  ``REGEX``
+    Escape all characters that have special meaning in a regular expressions,
+    such that the ``string`` can be used as part of a regular expression to
+    match the input literally.
+
+.. genex:: $<STRING:HEX,string>
+
+  .. versionadded:: 4.3
+
+  Convert each byte in the ``string`` to its hexadecimal representation.
+  Letters in the result (a through f) are in lowercase.
+
+.. genex:: $<STRING:HASH,string,ALGORITHM:algorithm>
+
+  .. versionadded:: 4.3
+
+  Compute a cryptographic hash of the ``string``. The supported algorithm
+  names, as specified by the ``ALGORITHM:`` option are:
+
+  ``MD5``
+    Message-Digest Algorithm 5, RFC 1321.
+  ``SHA1``
+    US Secure Hash Algorithm 1, RFC 3174.
+  ``SHA224``
+    US Secure Hash Algorithms, RFC 4634.
+  ``SHA256``
+    US Secure Hash Algorithms, RFC 4634.
+  ``SHA384``
+    US Secure Hash Algorithms, RFC 4634.
+  ``SHA512``
+    US Secure Hash Algorithms, RFC 4634.
+  ``SHA3_224``
+    Keccak SHA-3.
+  ``SHA3_256``
+    Keccak SHA-3.
+  ``SHA3_384``
+    Keccak SHA-3.
+  ``SHA3_512``
+    Keccak SHA-3.
+
+.. genex:: $<STRING:MAKE_C_IDENTIFIER,string>
+
+  .. versionadded:: 4.3
+
+  Convert each non-alphanumeric character in the ``string`` to an underscore.
+  If the first character of the ``string`` is a digit, an underscore will also
+  be prepended.
+
 .. genex:: $<LOWER_CASE:string>
 
   Content of ``string`` converted to lower case.
@@ -380,8 +722,8 @@ String Transformations
 
 .. genex:: $<MAKE_C_IDENTIFIER:string>
 
-  Content of ``...`` converted to a C identifier.  The conversion follows the
-  same behavior as :command:`string(MAKE_C_IDENTIFIER)`.
+  Content of ``string`` converted to a C identifier.  The conversion follows
+  the same behavior as :command:`string(MAKE_C_IDENTIFIER)`.
 
 List Expressions
 ----------------

+ 8 - 0
Help/release/dev/GenEx-STRING.rst

@@ -0,0 +1,8 @@
+GenEx-STRING
+------------
+
+* :genex:`$<STRING:...>` generator expressions were added for
+  :ref:`query <String Queries Generator Expressions>`,
+  :ref:`generation <String Generating Generator Expressions>`, and
+  :ref:`transformation <String Transforming Generator Expressions>` operations
+  on strings.

+ 598 - 47
Source/cmGeneratorExpressionNode.cxx

@@ -7,6 +7,7 @@
 #include <cerrno>
 #include <cstdlib>
 #include <cstring>
+#include <exception>
 #include <functional>
 #include <map>
 #include <memory>
@@ -782,6 +783,55 @@ bool CheckGenExParameters(cm::GenEx::Evaluation* eval,
   return true;
 };
 
+template <typename IndexType>
+bool GetNumericArgument(std::string const& arg, IndexType& value)
+{
+  try {
+    std::size_t pos;
+
+    if (sizeof(IndexType) == sizeof(long)) {
+      value = std::stol(arg, &pos);
+    } else {
+      value = std::stoll(arg, &pos);
+    }
+
+    if (pos != arg.length()) {
+      // this is not a number
+      return false;
+    }
+  } catch (std::invalid_argument const&) {
+    return false;
+  }
+
+  return true;
+}
+
+template <typename IndexType>
+bool GetNumericArguments(
+  cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* cnt,
+  Arguments args, std::vector<IndexType>& indexes,
+  cmList::ExpandElements expandElements = cmList::ExpandElements::No)
+{
+  using IndexRange = cmRange<Arguments::const_iterator>;
+  IndexRange arguments(args.begin(), args.end());
+  cmList list;
+  if (expandElements == cmList::ExpandElements::Yes) {
+    list = cmList{ args.begin(), args.end(), expandElements };
+    arguments = IndexRange{ list.begin(), list.end() };
+  }
+
+  for (auto const& value : arguments) {
+    IndexType index;
+    if (!GetNumericArgument(value, index)) {
+      reportError(eval, cnt->GetOriginalExpression(),
+                  cmStrCat("index: \"", value, "\" is not a valid index"));
+      return false;
+    }
+    indexes.push_back(index);
+  }
+  return true;
+}
+
 bool CheckPathParametersEx(cm::GenEx::Evaluation* eval,
                            GeneratorExpressionContent const* cnt,
                            cm::string_view option, std::size_t count,
@@ -1238,6 +1288,553 @@ static const struct PathEqualNode : public cmGeneratorExpressionNode
   }
 } pathEqualNode;
 
+namespace {
+inline bool CheckStringParametersEx(cm::GenEx::Evaluation* eval,
+                                    GeneratorExpressionContent const* cnt,
+                                    cm::string_view option, std::size_t count,
+                                    int required = 1, bool exactly = true)
+{
+  return CheckGenExParameters(eval, cnt, "STRING"_s, option, count, required,
+                              exactly);
+}
+inline bool CheckStringParameters(cm::GenEx::Evaluation* eval,
+                                  GeneratorExpressionContent const* cnt,
+                                  cm::string_view option, Arguments args,
+                                  int required = 1)
+{
+  return CheckStringParametersEx(eval, cnt, option, args.size(), required);
+};
+}
+
+static const struct StringNode : public cmGeneratorExpressionNode
+{
+  StringNode() {} // NOLINT(modernize-use-equals-default)
+
+  int NumExpectedParameters() const override { return OneOrMoreParameters; }
+
+  bool AcceptsArbitraryContentParameter() const override { return true; }
+
+  std::string Evaluate(
+    std::vector<std::string> const& parameters, cm::GenEx::Evaluation* eval,
+    GeneratorExpressionContent const* content,
+    cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override
+  {
+    static std::unordered_map<
+      cm::string_view,
+      std::function<std::string(cm::GenEx::Evaluation*,
+                                GeneratorExpressionContent const*,
+                                Arguments&)>>
+      stringCommands{
+        { "LENGTH"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "LENGTH"_s, args)) {
+              return std::to_string(cm::CMakeString{ args.front() }.Length());
+            }
+            return std::string{};
+          } },
+        { "SUBSTRING"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "SUBSTRING"_s, args, 3)) {
+              cm::CMakeString str{ args.front() };
+              std::vector<long> indexes;
+              if (GetNumericArguments(ev, cnt, args.advance(1), indexes)) {
+                try {
+                  return str.Substring(indexes.front(), indexes.back());
+                } catch (std::out_of_range const& e) {
+                  reportError(ev, cnt->GetOriginalExpression(), e.what());
+                  return std::string{};
+                }
+              }
+            }
+            return std::string{};
+          } },
+        { "FIND"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "FIND"_s, args.size(), 2,
+                                        false)) {
+              if (args.size() > 3) {
+                reportError(ev, cnt->GetOriginalExpression(),
+                            "$<STRING:FIND> expression expects at "
+                            "most three parameters.");
+                return std::string{};
+              }
+
+              auto const FROM = "FROM:"_s;
+
+              cm::CMakeString str{ args.front() };
+              cm::CMakeString::FindFrom from =
+                cm::CMakeString::FindFrom::Begin;
+              cm::string_view substring;
+
+              args.advance(1);
+              if (args.size() == 2) {
+                cm::CMakeString::FindFrom opt =
+                  static_cast<cm::CMakeString::FindFrom>(-1);
+
+                for (auto const& arg : args) {
+                  if (cmHasPrefix(arg, FROM)) {
+                    if (arg != "FROM:BEGIN"_s && arg != "FROM:END"_s) {
+                      reportError(
+                        ev, cnt->GetOriginalExpression(),
+                        cmStrCat("Invalid value for '", FROM,
+                                 "' option. 'BEGIN' or 'END' expected."));
+                      return std::string{};
+                    }
+                    opt = arg == "FROM:BEGIN"_s
+                      ? cm::CMakeString::FindFrom::Begin
+                      : cm::CMakeString::FindFrom::End;
+                  } else {
+                    substring = arg;
+                  }
+                }
+                if (opt == static_cast<cm::CMakeString::FindFrom>(-1)) {
+                  reportError(
+                    ev, cnt->GetOriginalExpression(),
+                    cmStrCat("Expected option '", FROM, "' is missing."));
+                  return std::string{};
+                }
+                from = opt;
+              } else {
+                substring = args.front();
+              }
+              auto pos = str.Find(substring, from);
+              return pos == cm::CMakeString::npos ? "-1" : std::to_string(pos);
+            }
+            return std::string{};
+          } },
+        { "MATCH"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "MATCH"_s, args.size(), 2,
+                                        false)) {
+              if (args.size() > 3) {
+                reportError(ev, cnt->GetOriginalExpression(),
+                            "$<STRING:MATCH> expression expects at "
+                            "most three parameters.");
+                return std::string{};
+              }
+
+              auto const SEEK = "SEEK:"_s;
+
+              cm::CMakeString str{ args.front() };
+              cm::CMakeString::MatchItems seek =
+                cm::CMakeString::MatchItems::Once;
+              auto const* regex = &args[1];
+
+              args.advance(1);
+              if (args.size() == 2) {
+                cm::CMakeString::MatchItems opt =
+                  static_cast<cm::CMakeString::MatchItems>(-1);
+
+                for (auto const& arg : args) {
+                  if (cmHasPrefix(arg, SEEK)) {
+                    if (arg != "SEEK:ONCE"_s && arg != "SEEK:ALL"_s) {
+                      reportError(
+                        ev, cnt->GetOriginalExpression(),
+                        cmStrCat("Invalid value for '", SEEK,
+                                 "' option. 'ONCE' or 'ALL' expected."));
+                      return std::string{};
+                    }
+                    opt = arg == "SEEK:ONCE"_s
+                      ? cm::CMakeString::MatchItems::Once
+                      : cm::CMakeString::MatchItems::All;
+                  } else {
+                    regex = &arg;
+                  }
+                }
+                if (opt == static_cast<cm::CMakeString::MatchItems>(-1)) {
+                  reportError(
+                    ev, cnt->GetOriginalExpression(),
+                    cmStrCat("Expected option '", SEEK, "' is missing."));
+                  return std::string{};
+                }
+                seek = opt;
+              }
+
+              try {
+                return str.Match(*regex, seek).to_string();
+              } catch (std::invalid_argument const& e) {
+                reportError(ev, cnt->GetOriginalExpression(), e.what());
+                return std::string{};
+              }
+            }
+            return std::string{};
+          } },
+        { "JOIN"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "JOIN"_s, args.size(), 2,
+                                        false)) {
+              auto const& glue = args.front();
+              return cm::CMakeString{ args.advance(1), glue };
+            }
+            return std::string{};
+          } },
+        { "ASCII"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "ASCII"_s, args.size(), 1,
+                                        false)) {
+              try {
+                return cm::CMakeString{}.FromASCII(args);
+              } catch (std::invalid_argument const& e) {
+                reportError(ev, cnt->GetOriginalExpression(), e.what());
+                return std::string{};
+              }
+            }
+            return std::string{};
+          } },
+        { "TIMESTAMP"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            cm::string_view format;
+            cm::CMakeString::UTC utc = cm::CMakeString::UTC::No;
+
+            if (args.size() == 2 && args.front() != "UTC"_s &&
+                args.back() != "UTC"_s) {
+              reportError(ev, cnt->GetOriginalExpression(),
+                          "'UTC' option is expected.");
+              return std::string{};
+            }
+            if (args.size() > 2) {
+              reportError(ev, cnt->GetOriginalExpression(),
+                          "$<STRING:TIMESTAMP> expression expects at most two "
+                          "parameters.");
+              return std::string{};
+            }
+
+            for (auto const& arg : args) {
+              if (arg == "UTC"_s) {
+                utc = cm::CMakeString::UTC::Yes;
+              } else {
+                format = arg;
+              }
+            }
+            return cm::CMakeString{}.Timestamp(format, utc);
+          } },
+        { "RANDOM"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            auto const ALPHABET = "ALPHABET:"_s;
+            auto const LENGTH = "LENGTH:"_s;
+            auto const RANDOM_SEED = "RANDOM_SEED:"_s;
+
+            if (args.size() > 3) {
+              reportError(ev, cnt->GetOriginalExpression(),
+                          "$<STRING:RANDOM> expression expects at most three "
+                          "parameters.");
+              return std::string{};
+            }
+
+            cm::string_view alphabet;
+            std::size_t length = 5;
+            bool seed_specified = false;
+            unsigned int seed = 0;
+            for (auto const& arg : args) {
+              if (cmHasPrefix(arg, ALPHABET)) {
+                alphabet = cm::string_view{ arg.c_str() + ALPHABET.length() };
+                continue;
+              }
+              if (cmHasPrefix(arg, LENGTH)) {
+                try {
+                  length = std::stoul(arg.substr(LENGTH.size()));
+                } catch (std::exception const&) {
+                  reportError(ev, cnt->GetOriginalExpression(),
+                              cmStrCat(arg, ": invalid numeric value for '",
+                                       LENGTH, "' option."));
+                  return std::string{};
+                }
+                continue;
+              }
+              if (cmHasPrefix(arg, RANDOM_SEED)) {
+                try {
+                  seed_specified = true;
+                  seed = static_cast<unsigned int>(
+                    std::stoul(arg.substr(RANDOM_SEED.size())));
+                } catch (std::exception const&) {
+                  reportError(ev, cnt->GetOriginalExpression(),
+                              cmStrCat(arg, ": invalid numeric value for '",
+                                       RANDOM_SEED, "' option."));
+                  return std::string{};
+                }
+                continue;
+              }
+              reportError(ev, cnt->GetOriginalExpression(),
+                          cmStrCat(arg, ": invalid parameter."));
+              return std::string{};
+            }
+
+            try {
+              if (seed_specified) {
+                return cm::CMakeString{}.Random(seed, length, alphabet);
+              }
+              return cm::CMakeString{}.Random(length, alphabet);
+            } catch (std::exception const& e) {
+              reportError(ev, cnt->GetOriginalExpression(), e.what());
+              return std::string{};
+            }
+          } },
+        { "UUID"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "UUID"_s, args.size(), 2,
+                                        false)) {
+              auto const NAMESPACE = "NAMESPACE:"_s;
+              auto const NAME = "NAME:"_s;
+              auto const TYPE = "TYPE:"_s;
+              auto const CASE = "CASE:"_s;
+
+              if (args.size() > 4) {
+                reportError(ev, cnt->GetOriginalExpression(),
+                            "$<STRING:UUID> expression expects at most four "
+                            "parameters.");
+                return std::string{};
+              }
+
+              cm::string_view nameSpace;
+              cm::string_view name;
+              cm::CMakeString::UUIDType type =
+                static_cast<cm::CMakeString::UUIDType>(-1);
+              cm::CMakeString::Case uuidCase = cm::CMakeString::Case::Lower;
+              for (auto const& arg : args) {
+                if (cmHasPrefix(arg, NAMESPACE)) {
+                  nameSpace =
+                    cm::string_view{ arg.c_str() + NAMESPACE.length() };
+                  if (nameSpace.empty()) {
+                    reportError(
+                      ev, cnt->GetOriginalExpression(),
+                      cmStrCat("Invalid value for '", NAMESPACE, "' option."));
+                    return std::string{};
+                  }
+                  continue;
+                }
+                if (cmHasPrefix(arg, NAME)) {
+                  name = cm::string_view{ arg.c_str() + NAME.length() };
+                  continue;
+                }
+                if (cmHasPrefix(arg, TYPE)) {
+                  auto value = cm::string_view{ arg.c_str() + TYPE.length() };
+                  if (value != "MD5"_s && value != "SHA1"_s) {
+                    reportError(
+                      ev, cnt->GetOriginalExpression(),
+                      cmStrCat("Invalid value for '", TYPE,
+                               "' option. 'MD5' or 'SHA1' expected."));
+                    return std::string{};
+                  }
+                  type = value == "MD5"_s ? cm::CMakeString::UUIDType::MD5
+                                          : cm::CMakeString::UUIDType::SHA1;
+                  continue;
+                }
+                if (cmHasPrefix(arg, CASE)) {
+                  auto value = cm::string_view{ arg.c_str() + CASE.length() };
+                  if (value != "UPPER"_s && value != "LOWER"_s) {
+                    reportError(
+                      ev, cnt->GetOriginalExpression(),
+                      cmStrCat("Invalid value for '", CASE,
+                               "' option. 'UPPER' or 'LOWER' expected."));
+                    return std::string{};
+                  }
+                  uuidCase = value == "UPPER"_s ? cm::CMakeString::Case::Upper
+                                                : cm::CMakeString::Case::Lower;
+                  continue;
+                }
+                reportError(ev, cnt->GetOriginalExpression(),
+                            cmStrCat(arg, ": invalid parameter."));
+                return std::string{};
+              }
+              if (nameSpace.empty()) {
+                reportError(
+                  ev, cnt->GetOriginalExpression(),
+                  cmStrCat("Required option '", NAMESPACE, "' is missing."));
+                return std::string{};
+              }
+              if (type == static_cast<cm::CMakeString::UUIDType>(-1)) {
+                reportError(
+                  ev, cnt->GetOriginalExpression(),
+                  cmStrCat("Required option '", TYPE, "' is missing."));
+                return std::string{};
+              }
+
+              try {
+                return cm::CMakeString{}.UUID(nameSpace, name, type, uuidCase);
+              } catch (std::exception const& e) {
+                reportError(ev, cnt->GetOriginalExpression(), e.what());
+                return std::string{};
+              }
+            }
+            return std::string{};
+          } },
+        { "REPLACE"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "REPLACE"_s, args.size(), 3,
+                                        false)) {
+              if (args.size() > 4) {
+                reportError(ev, cnt->GetOriginalExpression(),
+                            "$<STRING:REPLACE> expression expects at "
+                            "most four parameters.");
+                return std::string{};
+              }
+
+              cm::CMakeString::Regex isRegex = cm::CMakeString::Regex::No;
+              if (args.size() == 4) {
+                cm::string_view type = args.front();
+                if (type != "STRING"_s && type != "REGEX"_s) {
+                  reportError(
+                    ev, cnt->GetOriginalExpression(),
+                    cmStrCat(
+                      '\'', type,
+                      "' is unexpected. 'STRING' or 'REGEX' expected."));
+                  return std::string{};
+                }
+                isRegex = type == "STRING"_s ? cm::CMakeString::Regex::No
+                                             : cm::CMakeString::Regex::Yes;
+                args.advance(1);
+              }
+
+              try {
+                return cm::CMakeString{ args.front() }.Replace(
+                  args[1], args[2], isRegex);
+              } catch (std::invalid_argument const& e) {
+                reportError(ev, cnt->GetOriginalExpression(), e.what());
+                return std::string{};
+              }
+            }
+            return std::string{};
+          } },
+        { "APPEND"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "APPEND"_s, args.size(), 2,
+                                        false)) {
+              cm::CMakeString data{ args.front() };
+              return data.Append(args.advance(1));
+            }
+            return std::string{};
+          } },
+        { "PREPEND"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParametersEx(ev, cnt, "PREPEND "_s, args.size(), 2,
+                                        false)) {
+              cm::CMakeString data{ args.front() };
+              return data.Prepend(args.advance(1));
+            }
+            return std::string{};
+          } },
+        { "TOLOWER"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "TOLOWER"_s, args, 1)) {
+              return cm::CMakeString{}.ToLower(args.front());
+            }
+            return std::string{};
+          } },
+        { "TOUPPER"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "TOUPPER"_s, args, 1)) {
+              return cm::CMakeString{}.ToUpper(args.front());
+            }
+            return std::string{};
+          } },
+        { "STRIP"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "STRIP"_s, args, 2)) {
+              if (args.front() != "SPACES"_s) {
+                reportError(ev, cnt->GetOriginalExpression(),
+                            cmStrCat('\'', args.front(),
+                                     "' is unexpected. 'SPACES' expected."));
+                return std::string{};
+              }
+
+              return cm::CMakeString{ args[1] }.Strip();
+            }
+            return std::string{};
+          } },
+        { "QUOTE"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "QUOTE"_s, args, 2)) {
+              if (args.front() != "REGEX"_s) {
+                reportError(ev, cnt->GetOriginalExpression(),
+                            cmStrCat('\'', args.front(),
+                                     "' is unexpected. 'REGEX' expected."));
+                return std::string{};
+              }
+
+              return cm::CMakeString{ args[1] }.Quote();
+            }
+            return std::string{};
+          } },
+        { "HEX"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "HEX"_s, args, 1)) {
+              return cm::CMakeString{ args.front() }.ToHexadecimal();
+            }
+            return std::string{};
+          } },
+        { "HASH"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "HASH"_s, args, 2)) {
+              auto const ALGORITHM = "ALGORITHM:"_s;
+
+              if (cmHasPrefix(args[1], ALGORITHM)) {
+                try {
+                  auto const algo =
+                    cm::string_view{ args[1].c_str() + ALGORITHM.length() };
+                  if (algo.empty()) {
+                    reportError(
+                      ev, cnt->GetOriginalExpression(),
+                      cmStrCat("Missing value for '", ALGORITHM, "' option."));
+                    return std::string{};
+                  }
+                  return cm::CMakeString{ args.front() }.Hash(algo);
+                } catch (std::exception const& e) {
+                  reportError(ev, cnt->GetOriginalExpression(), e.what());
+                  return std::string{};
+                }
+              }
+              reportError(ev, cnt->GetOriginalExpression(),
+                          cmStrCat(args[1], ": invalid parameter. Option '",
+                                   ALGORITHM, "' expected."));
+            }
+            return std::string{};
+          } },
+        { "MAKE_C_IDENTIFIER"_s,
+          [](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
+             Arguments& args) -> std::string {
+            if (CheckStringParameters(ev, cnt, "MAKE_C_IDENTIFIER"_s, args,
+                                      1)) {
+              return cm::CMakeString{ args.front() }.MakeCIdentifier();
+            }
+            return std::string{};
+          } }
+      };
+
+    if (parameters.front().empty()) {
+      reportError(eval, content->GetOriginalExpression(),
+                  "$<STRING> expression requires at least one parameter.");
+      return std::string{};
+    }
+
+    if (cm::contains(stringCommands, parameters.front())) {
+      auto args = Arguments{ parameters }.advance(1);
+      return stringCommands[parameters.front()](eval, content, args);
+    }
+
+    reportError(eval, content->GetOriginalExpression(),
+                cmStrCat(parameters.front(), ": invalid option."));
+    return std::string{};
+  }
+} stringNode;
+
 namespace {
 inline bool CheckListParametersEx(cm::GenEx::Evaluation* eval,
                                   GeneratorExpressionContent const* cnt,
@@ -1259,53 +1856,6 @@ inline cmList GetList(std::string const& list)
 {
   return list.empty() ? cmList{} : cmList{ list, cmList::EmptyElements::Yes };
 }
-
-bool GetNumericArgument(std::string const& arg, cmList::index_type& value)
-{
-  try {
-    std::size_t pos;
-
-    if (sizeof(cmList::index_type) == sizeof(long)) {
-      value = std::stol(arg, &pos);
-    } else {
-      value = std::stoll(arg, &pos);
-    }
-
-    if (pos != arg.length()) {
-      // this is not a number
-      return false;
-    }
-  } catch (std::invalid_argument const&) {
-    return false;
-  }
-
-  return true;
-}
-
-bool GetNumericArguments(
-  cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* cnt,
-  Arguments args, std::vector<cmList::index_type>& indexes,
-  cmList::ExpandElements expandElements = cmList::ExpandElements::No)
-{
-  using IndexRange = cmRange<Arguments::const_iterator>;
-  IndexRange arguments(args.begin(), args.end());
-  cmList list;
-  if (expandElements == cmList::ExpandElements::Yes) {
-    list = cmList{ args.begin(), args.end(), expandElements };
-    arguments = IndexRange{ list.begin(), list.end() };
-  }
-
-  for (auto const& value : arguments) {
-    cmList::index_type index;
-    if (!GetNumericArgument(value, index)) {
-      reportError(eval, cnt->GetOriginalExpression(),
-                  cmStrCat("index: \"", value, "\" is not a valid index"));
-      return false;
-    }
-    indexes.push_back(index);
-  }
-  return true;
-}
 }
 
 static const struct ListNode : public cmGeneratorExpressionNode
@@ -4952,6 +5502,7 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode(
     { "STRLESS_EQUAL", &strLessEqualNode },
     { "STRGREATER", &strGreaterNode },
     { "STRGREATER_EQUAL", &strGreaterEqualNode },
+    { "STRING", &stringNode },
     { "EQUAL", &equalNode },
     { "IN_LIST", &inListNode },
     { "FILTER", &filterNode },

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -523,6 +523,7 @@ add_RunCMake_test(GenEx-TARGET_IMPORT_FILE)
 add_RunCMake_test(GenEx-GENEX_EVAL)
 add_RunCMake_test(GenEx-TARGET_PROPERTY)
 add_RunCMake_test(GenEx-TARGET_RUNTIME_DLLS)
+add_RunCMake_test(GenEx-STRING)
 add_RunCMake_test(GenEx-PATH)
 add_RunCMake_test(GenEx-PATH_EQUAL)
 add_RunCMake_test(GenEx-LIST)

+ 22 - 0
Tests/RunCMake/GenEx-STRING/APPEND.cmake.in

@@ -0,0 +1,22 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set(reference "abcd")
+string(APPEND reference  "efg" "ABCD")
+set(output "$<STRING:APPEND,abcd,efg,ABCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:APPEND,abcd,efg,ABCD> returns bad data: ${output}")
+endif()
+
+unset(reference)
+string(APPEND reference  "efg" "ABCD")
+set(output "$<STRING:APPEND,,efg,ABCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:APPEND,,efg,ABCD> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:APPEND" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/ASCII-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 38 - 0
Tests/RunCMake/GenEx-STRING/ASCII-WrongArguments-stderr.txt

@@ -0,0 +1,38 @@
+CMake Error at ASCII-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:ASCII,foo>
+
+  Character with code foo does not exist.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at ASCII-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:ASCII,256>
+
+  Character with code 256 does not exist.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at ASCII-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:ASCII,32,256>
+
+  Character with code 256 does not exist.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at ASCII-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:ASCII,32,-1>
+
+  Character with code -1 does not exist.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 7 - 0
Tests/RunCMake/GenEx-STRING/ASCII-WrongArguments.cmake

@@ -0,0 +1,7 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:ASCII,foo>
+  $<STRING:ASCII,256>
+  $<STRING:ASCII,32,256>
+  $<STRING:ASCII,32,-1>
+VERBATIM)

+ 20 - 0
Tests/RunCMake/GenEx-STRING/ASCII.cmake.in

@@ -0,0 +1,20 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(ASCII 32 reference)
+set(output "$<STRING:ASCII,32>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:ASCII,32> returns bad data: ${output}")
+endif()
+
+string(ASCII 32 64 reference)
+set(output "$<STRING:ASCII,32,64>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:ASCII,32,64> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:ASCII" ${errors})

+ 5 - 0
Tests/RunCMake/GenEx-STRING/CMakeLists.txt

@@ -0,0 +1,5 @@
+cmake_minimum_required(VERSION 3.18...3.25)
+
+project(${RunCMake_TEST} NONE)
+
+include(${RunCMake_TEST}.cmake)

+ 1 - 0
Tests/RunCMake/GenEx-STRING/FIND-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 18 - 0
Tests/RunCMake/GenEx-STRING/FIND-WrongArguments-stderr.txt

@@ -0,0 +1,18 @@
+CMake Error at FIND-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:FIND,string,OTHER,foo>
+
+  Expected option 'FROM:' is missing.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at FIND-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:FIND,string,FROM:OTHER,foo>
+
+  Invalid value for 'FROM:' option.  'BEGIN' or 'END' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/GenEx-STRING/FIND-WrongArguments.cmake

@@ -0,0 +1,5 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:FIND,string,OTHER,foo>
+  $<STRING:FIND,string,FROM:OTHER,foo>
+VERBATIM)

+ 48 - 0
Tests/RunCMake/GenEx-STRING/FIND.cmake.in

@@ -0,0 +1,48 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(FIND  "abcdabcd" "cd" reference)
+set(output "$<STRING:FIND,abcdabcd,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,cd> returns bad data: ${output}")
+endif()
+set(output "$<STRING:FIND,abcdabcd,FROM:BEGIN,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,FROM:BEGIN,cd> returns bad data: ${output}")
+endif()
+set(output "$<STRING:FIND,abcdabcd,cd,FROM:BEGIN>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,cd,FROM:BEGIN> returns bad data: ${output}")
+endif()
+
+string(FIND  "abcdabcd" "cd" reference REVERSE)
+set(output "$<STRING:FIND,abcdabcd,FROM:END,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,FROM:END,cd> returns bad data: ${output}")
+endif()
+set(output "$<STRING:FIND,abcdabcd,cd,FROM:END>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,cd,FROM:END> returns bad data: ${output}")
+endif()
+
+string(FIND  "abcdabcd" "xy" reference)
+set(output "$<STRING:FIND,abcdabcd,xy>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,xy> returns bad data: ${output}")
+endif()
+set(output "$<STRING:FIND,abcdabcd,xy,FROM:BEGIN>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,xy,FROM:BEGIN> returns bad data: ${output}")
+endif()
+
+string(FIND  "abcdabcd" "xy" reference REVERSE)
+set(output "$<STRING:FIND,abcdabcd,xy,FROM:END>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:FIND,abcdabcd,xy,FROM:END> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:FIND" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/HASH-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 18 - 0
Tests/RunCMake/GenEx-STRING/HASH-WrongArguments-stderr.txt

@@ -0,0 +1,18 @@
+CMake Error at HASH-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:HASH,string,ALGORITHM:>
+
+  Missing value for 'ALGORITHM:' option.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at HASH-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:HASH,string,ALGORITHM:FOO>
+
+  FOO: invalid hash algorithm.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/GenEx-STRING/HASH-WrongArguments.cmake

@@ -0,0 +1,5 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:HASH,string,ALGORITHM:>
+  $<STRING:HASH,string,ALGORITHM:FOO>
+VERBATIM)

+ 69 - 0
Tests/RunCMake/GenEx-STRING/HASH.cmake.in

@@ -0,0 +1,69 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+# foreach(hash IN ITEMS MD5 SHA1 SHA224 SHA256 SHA384 SHA512 SHA3_224 SHA3_256 SHA3_384 SHA3_512)
+
+string(MD5 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:MD5>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,MD5> returns bad data: ${output}")
+endif()
+
+string(SHA1 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA1>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA1> returns bad data: ${output}")
+endif()
+
+string(SHA224 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA224>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA224> returns bad data: ${output}")
+endif()
+
+string(SHA256 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA256>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA256> returns bad data: ${output}")
+endif()
+
+string(SHA384 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA384>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA384> returns bad data: ${output}")
+endif()
+
+string(SHA512 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA512>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA512> returns bad data: ${output}")
+endif()
+
+string(SHA3_224 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA3_224>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA3_224> returns bad data: ${output}")
+endif()
+
+string(SHA3_256 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA3_256>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA3_256> returns bad data: ${output}")
+endif()
+
+string(SHA3_384 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA3_384>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA3_384> returns bad data: ${output}")
+endif()
+
+string(SHA3_512 reference "sample input string")
+set(output "$<STRING:HASH,sample input string,ALGORITHM:SHA3_512>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HASH,sample input string,SHA3_512> returns bad data: ${output}")
+endif()
+
+check_errors("STRING:HASH" ${errors})

+ 20 - 0
Tests/RunCMake/GenEx-STRING/HEX.cmake.in

@@ -0,0 +1,20 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(HEX "The quick brown fox jumps over the lazy dog." reference)
+set(output "$<STRING:HEX,The quick brown fox jumps over the lazy dog.>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HEX,The quick brown fox jumps over the lazy dog.> returns bad data: ${output}")
+endif()
+
+string(HEX "Ash nazg durbatulûk. Ash nazg gimbatul. Ash nazg thrakatulûk. Agh burzum-ishi krimpatul" reference)
+set(output "$<STRING:HEX,Ash nazg durbatulûk. Ash nazg gimbatul. Ash nazg thrakatulûk. Agh burzum-ishi krimpatul>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:HEX,Ash nazg durbatulûk. Ash nazg gimbatul. Ash nazg thrakatulûk. Agh burzum-ishi krimpatul> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:HEX" ${errors})

+ 32 - 0
Tests/RunCMake/GenEx-STRING/JOIN.cmake.in

@@ -0,0 +1,32 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(JOIN "|" reference "ab" "cd")
+set(output "$<STRING:JOIN,|,ab,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:JOIN,|,ab,cd> returns bad data: ${output}")
+endif()
+
+string(JOIN "" reference "ab" "cd")
+set(output "$<STRING:JOIN,,ab,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:JOIN,,ab,cd> returns bad data: ${output}")
+endif()
+
+string(JOIN "|" reference)
+set(output "$<STRING:JOIN,|,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:JOIN,|,> returns bad data: ${output}")
+endif()
+
+string(JOIN "" reference)
+set(output "$<STRING:JOIN,,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:JOIN,,> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:JOIN" ${errors})

+ 21 - 0
Tests/RunCMake/GenEx-STRING/LENGTH.cmake.in

@@ -0,0 +1,21 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(LENGTH  "abc" reference)
+set(output "$<STRING:LENGTH,abc>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:LENGTH,abc> returns bad data: ${output}")
+endif()
+
+
+string(LENGTH  "" reference)
+set(output "$<STRING:LENGTH,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:LENGTH,> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:LENGTH" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/MATCH-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 28 - 0
Tests/RunCMake/GenEx-STRING/MATCH-WrongArguments-stderr.txt

@@ -0,0 +1,28 @@
+CMake Error at MATCH-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:MATCH,string,\(>
+
+  Failed to compile regex "\("
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at MATCH-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:MATCH,string,FOO,\.\+>
+
+  Expected option 'SEEK:' is missing.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at MATCH-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:MATCH,string,SEEK:FOO,\.\+>
+
+  Invalid value for 'SEEK:' option.  'ONCE' or 'ALL' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 8 - 0
Tests/RunCMake/GenEx-STRING/MATCH-WrongArguments.cmake

@@ -0,0 +1,8 @@
+
+add_custom_target(check ALL COMMAND check
+  [[
+     $<STRING:MATCH,string,(>
+     $<STRING:MATCH,string,FOO,.+>
+     $<STRING:MATCH,string,SEEK:FOO,.+>
+  ]]
+VERBATIM)

+ 40 - 0
Tests/RunCMake/GenEx-STRING/MATCH.cmake.in

@@ -0,0 +1,40 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(REGEX MATCH "cd" reference "abcdabcd")
+set(output "$<STRING:MATCH,abcdabcd,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:MATCH,abcdabcd,cd> returns bad data: ${output}")
+endif()
+set(output "$<STRING:MATCH,abcdabcd,SEEK:ONCE,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:MATCH,abcdabcd,SEEK:ONCE,cd> returns bad data: ${output}")
+endif()
+
+string(REGEX MATCH "(b|B)cd" reference "abcdABcd")
+set(output "$<STRING:MATCH,abcdABcd,(b|B)cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:MATCH,abcdABcd,B(cd)> returns bad data: ${output}")
+endif()
+set(output "$<STRING:MATCH,abcdABcd,(b|B)cd,SEEK:ONCE>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:MATCH,abcdABcd,B(cd),SEEK:ONCE> returns bad data: ${output}")
+endif()
+
+string(REGEX MATCHALL "cd" reference "abcdabcd")
+set(output "$<STRING:MATCH,abcdabcd,SEEK:ALL,cd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:MATCH,abcdabcd,SEEK:ALL,cd> returns bad data: ${output}")
+endif()
+
+string(REGEX MATCHALL "(b|B)cd" reference "abcdABcd")
+set(output "$<STRING:MATCH,abcdABcd,(b|B)cd,SEEK:ALL>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:MATCH,abcdABcd,B(cd),SEEK:ALL> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:MATCH" ${errors})

+ 22 - 0
Tests/RunCMake/GenEx-STRING/PREPEND.cmake.in

@@ -0,0 +1,22 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set(reference "abcd")
+string(PREPEND reference  "efg" "ABCD")
+set(output "$<STRING:PREPEND,abcd,efg,ABCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:PREPEND,abcd,efg,ABCD> returns bad data: ${output}")
+endif()
+
+unset(reference)
+string(PREPEND reference  "efg" "ABCD")
+set(output "$<STRING:PREPEND,,efg,ABCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:PREPEND,,efg,ABCD> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:PREPEND" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/QUOTE-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/QUOTE-WrongArguments-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at QUOTE-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:QUOTE,FOO,string>
+
+  'FOO' is unexpected.  'REGEX' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 6 - 0
Tests/RunCMake/GenEx-STRING/QUOTE-WrongArguments.cmake

@@ -0,0 +1,6 @@
+
+add_custom_target(check ALL COMMAND check
+  [[
+     $<STRING:QUOTE,FOO,string>
+  ]]
+VERBATIM)

+ 21 - 0
Tests/RunCMake/GenEx-STRING/QUOTE.cmake.in

@@ -0,0 +1,21 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(REGEX QUOTE reference "abcd")
+set(output "$<STRING:QUOTE,REGEX,abcd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:QUOTE,REGEX,abcd> returns bad data: ${output}")
+endif()
+
+string(REGEX QUOTE reference "ab|c+12?3[x-z]$(y)\\t\\r\\n.cma*ke^[:alpha:] ")
+set(output [[
+$<STRING:QUOTE,REGEX,ab|c+12?3[x-z]$(y)\t\r\n.cma*ke^[:alpha:]> ]])
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:QUOTE,REGEX,ab|c+12?3[x-z]$(y)\\t\\r\\n.cma*ke^[:alpha:]> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:QUOTE" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/RANDOM-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 18 - 0
Tests/RunCMake/GenEx-STRING/RANDOM-WrongArguments-stderr.txt

@@ -0,0 +1,18 @@
+CMake Error at RANDOM-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:RANDOM,LENGTH:foo>
+
+  LENGTH:foo: invalid numeric value for 'LENGTH:' option.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at RANDOM-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:RANDOM,RANDOM_SEED:foo>
+
+  RANDOM_SEED:foo: invalid numeric value for 'RANDOM_SEED:' option.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/GenEx-STRING/RANDOM-WrongArguments.cmake

@@ -0,0 +1,5 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:RANDOM,LENGTH:foo>
+   $<STRING:RANDOM,RANDOM_SEED:foo>
+VERBATIM)

+ 32 - 0
Tests/RunCMake/GenEx-STRING/RANDOM.cmake.in

@@ -0,0 +1,32 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(RANDOM RANDOM_SEED 123 reference)
+set(output "$<STRING:RANDOM,RANDOM_SEED:123>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:RANDOM,RANDOM_SEED:123> returns bad data: ${output}")
+endif()
+
+string(RANDOM LENGTH 9 RANDOM_SEED 5 reference)
+set(output "$<STRING:RANDOM,LENGTH:9,RANDOM_SEED:5>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:RANDOM,LENGTH:9,RANDOM_SEED:5> returns bad data: ${output}")
+endif()
+
+string(RANDOM LENGTH 9 RANDOM_SEED 5 ALPHABET "" reference)
+set(output "$<STRING:RANDOM,LENGTH:9,RANDOM_SEED:5,ALPHABET:>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:RANDOM,LENGTH:9,RANDOM_SEED:5,ALPHABET:> returns bad data: ${output}")
+endif()
+
+string(RANDOM LENGTH 9 RANDOM_SEED 5 ALPHABET "abcdef123456789" reference)
+set(output "$<STRING:RANDOM,LENGTH:9,RANDOM_SEED:5,ALPHABET:abcdef123456789>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:RANDOM,LENGTH:9,RANDOM_SEED:5,ALPHABET:abcdef123456789> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:RANDOM" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/REPLACE-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 18 - 0
Tests/RunCMake/GenEx-STRING/REPLACE-WrongArguments-stderr.txt

@@ -0,0 +1,18 @@
+CMake Error at REPLACE-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:REPLACE,FOO,string,\.\+,>
+
+  'FOO' is unexpected.  'STRING' or 'REGEX' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at REPLACE-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:REPLACE,REGEX,string,\(,>
+
+  Failed to compile regex "\("
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 7 - 0
Tests/RunCMake/GenEx-STRING/REPLACE-WrongArguments.cmake

@@ -0,0 +1,7 @@
+
+add_custom_target(check ALL COMMAND check
+  [[
+     $<STRING:REPLACE,FOO,string,.+,>
+     $<STRING:REPLACE,REGEX,string,(,>
+  ]]
+VERBATIM)

+ 44 - 0
Tests/RunCMake/GenEx-STRING/REPLACE.cmake.in

@@ -0,0 +1,44 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(REPLACE  "bcd" "BCD" reference "abcdabcd")
+set(output "$<STRING:REPLACE,abcdabcd,bcd,BCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,abcdabcd,bcd,BCD> returns bad data: ${output}")
+endif()
+set(output "$<STRING:REPLACE,STRING,abcdabcd,bcd,BCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,STRING,abcdabcd,bcd,BCD> returns bad data: ${output}")
+endif()
+
+string(REPLACE  "bcd" "" reference "abcdabcd")
+set(output "$<STRING:REPLACE,abcdabcd,bcd,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,abcdabcd,bcd,> returns bad data: ${output}")
+endif()
+set(output "$<STRING:REPLACE,STRING,abcdabcd,bcd,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,STRING,abcdabcd,bcd,> returns bad data: ${output}")
+endif()
+
+string(REPLACE  "xyz" "BCD" reference "abcdabcd")
+set(output "$<STRING:REPLACE,abcdabcd,xyz,BCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,abcdabcd,xyz,BCD> returns bad data: ${output}")
+endif()
+set(output "$<STRING:REPLACE,STRING,abcdabcd,xyz,BCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,STRING,abcdabcd,xyz,BCD> returns bad data: ${output}")
+endif()
+
+string(REGEX REPLACE  "bcd" "BCD" reference "abcdabcd")
+set(output "$<STRING:REPLACE,REGEX,abcdabcd,bcd,BCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:REPLACE,REGEX,abcdabcd,bcd,BCD> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:REPLACE" ${errors})

+ 80 - 0
Tests/RunCMake/GenEx-STRING/RunCMakeTest.cmake

@@ -0,0 +1,80 @@
+
+include(RunCMake)
+
+run_cmake(no-arguments)
+run_cmake(bad-option)
+
+function(check_string_syntax name test)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${name}-${test}-build)
+  set(RunCMake_TEST_VARIANT_DESCRIPTION " - ${name}")
+  run_cmake_with_options(${test} ${ARGN})
+endfunction()
+
+## Unexpected arguments
+### sub-commands with one argument
+foreach (subcommand IN ITEMS LENGTH TOLOWER TOUPPER HEX MAKE_C_IDENTIFIER)
+  check_string_syntax (${subcommand} unexpected-arg "-DSTRING_ARGUMENTS=${subcommand},ARG1,ARG2")
+endforeach()
+
+### sub-commands with two arguments
+foreach (subcommand IN ITEMS HASH STRIP QUOTE)
+  check_string_syntax (${subcommand} unexpected-arg "-DSTRING_ARGUMENTS=${subcommand},ARG1,ARG2,ARG3")
+endforeach()
+
+### sub-commands with three arguments
+foreach (subcommand IN ITEMS SUBSTRING)
+  check_string_syntax (${subcommand} unexpected-arg "-DSTRING_ARGUMENTS=${subcommand},ARG1,ARG2,ARG3,ARG4")
+endforeach()
+foreach (subcommand IN ITEMS FIND MATCH RANDOM)
+  check_string_syntax (${subcommand} unexpected-arg2 "-DSTRING_ARGUMENTS=${subcommand},ARG1,ARG2,ARG3,ARG4")
+endforeach()
+
+### sub-commands with four arguments
+foreach (subcommand IN ITEMS REPLACE)
+  check_string_syntax (${subcommand} unexpected-arg2 "-DSTRING_ARGUMENTS=${subcommand},ARG1,ARG2,ARG3,ARG4,ARG5")
+endforeach()
+
+run_cmake(SUBSTRING-WrongArguments)
+run_cmake(FIND-WrongArguments)
+run_cmake(MATCH-WrongArguments)
+run_cmaKE(ASCII-WrongArguments)
+run_cmake(TIMESTAMP-WrongArguments)
+run_cmake(RANDOM-WrongArguments)
+run_cmake(UUID-WrongArguments)
+run_cmake(REPLACE-WrongArguments)
+run_cmake(STRIP-WrongArguments)
+run_cmake(QUOTE-WrongArguments)
+run_cmake(HASH-WrongArguments)
+
+
+function(check_string_execution name)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${name}-build)
+  if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
+    set(CONFIG_OPTION -DCMAKE_CONFIGURATION_TYPES=Debug)
+  else()
+    set(CONFIG_OPTION -DCMAKE_BUILD_TYPE=Debug)
+  endif()
+  set(RunCMake_TEST_NO_CLEAN 1)
+  set(RunCMake_TEST_VARIANT_DESCRIPTION " - ${name}")
+  run_cmake_with_options(generate -DSTRING_TEST=${name} ${CONFIG_OPTION})
+  run_cmake_command(check "${CMAKE_COMMAND}" "-DRunCMake_SOURCE_DIR=${RunCMake_SOURCE_DIR}" -P "${RunCMake_TEST_BINARY_DIR}/${name}.cmake")
+endfunction()
+
+check_string_execution (LENGTH)
+check_string_execution (SUBSTRING)
+check_string_execution (FIND)
+check_string_execution (MATCH)
+check_string_execution (JOIN)
+check_string_execution (ASCII)
+check_string_execution (TIMESTAMP)
+check_string_execution (RANDOM)
+check_string_execution (UUID)
+check_string_execution (REPLACE)
+check_string_execution (APPEND)
+check_string_execution (PREPEND)
+check_string_execution (TOLOWER)
+check_string_execution (TOUPPER)
+check_string_execution (STRIP)
+check_string_execution (QUOTE)
+check_string_execution (HEX)
+check_string_execution (HASH)

+ 1 - 0
Tests/RunCMake/GenEx-STRING/STRIP-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/STRIP-WrongArguments-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at STRIP-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:STRIP,FOO,string>
+
+  'FOO' is unexpected.  'SPACES' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 6 - 0
Tests/RunCMake/GenEx-STRING/STRIP-WrongArguments.cmake

@@ -0,0 +1,6 @@
+
+add_custom_target(check ALL COMMAND check
+  [[
+     $<STRING:STRIP,FOO,string>
+  ]]
+VERBATIM)

+ 14 - 0
Tests/RunCMake/GenEx-STRING/STRIP.cmake.in

@@ -0,0 +1,14 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(STRIP " ABCD  "  reference)
+set(output "$<STRING:STRIP,SPACES, ABCD  >")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:STRIP,SPACES, ABCD  > returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:STRIP" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/SUBSTRING-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 38 - 0
Tests/RunCMake/GenEx-STRING/SUBSTRING-WrongArguments-stderr.txt

@@ -0,0 +1,38 @@
+CMake Error at SUBSTRING-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:SUBSTRING,string,foo,1>
+
+  index: "foo" is not a valid index
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at SUBSTRING-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:SUBSTRING,string,1,foo>
+
+  index: "foo" is not a valid index
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at SUBSTRING-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:SUBSTRING,string,-1,2>
+
+  begin index: -1 is out of range 0 - 6
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at SUBSTRING-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:SUBSTRING,string,1,-2>
+
+  end index: -2 should be -1 or greater
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 7 - 0
Tests/RunCMake/GenEx-STRING/SUBSTRING-WrongArguments.cmake

@@ -0,0 +1,7 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:SUBSTRING,string,foo,1>
+  $<STRING:SUBSTRING,string,1,foo>
+  $<STRING:SUBSTRING,string,-1,2>
+  $<STRING:SUBSTRING,string,1,-2>
+VERBATIM)

+ 26 - 0
Tests/RunCMake/GenEx-STRING/SUBSTRING.cmake.in

@@ -0,0 +1,26 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(SUBSTRING  "abcd" 1 2 reference)
+set(output "$<STRING:SUBSTRING,abcd,1,2>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:SUBSTRING,abcd,1,2> returns bad data: ${output}")
+endif()
+
+string(SUBSTRING  "abcd" 1 -1 reference)
+set(output "$<STRING:SUBSTRING,abcd,1,-1>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:SUBSTRING,abcd,1,-1> returns bad data: ${output}")
+endif()
+
+string(SUBSTRING  "abcd" 1 5 reference)
+set(output "$<STRING:SUBSTRING,abcd,1,5>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:SUBSTRING,abcd,1,5> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:SUBSTRING" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/TIMESTAMP-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/TIMESTAMP-WrongArguments-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at TIMESTAMP-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:TIMESTAMP,%s,%s>
+
+  'UTC' option is expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 4 - 0
Tests/RunCMake/GenEx-STRING/TIMESTAMP-WrongArguments.cmake

@@ -0,0 +1,4 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:TIMESTAMP,%s,%s>
+VERBATIM)

+ 27 - 0
Tests/RunCMake/GenEx-STRING/TIMESTAMP.cmake.in

@@ -0,0 +1,27 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set(ENV{SOURCE_DATE_EPOCH} "1123456789")
+
+string(TIMESTAMP reference "%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z")
+set(output "$<STRING:TIMESTAMP,%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TIMESTAMP,%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z> returns bad data: ${output}")
+endif()
+
+
+string(TIMESTAMP reference "%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z" UTC)
+set(output "$<STRING:TIMESTAMP,%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z,UTC>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TIMESTAMP,%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z,UTC> returns bad data: ${output}")
+endif()
+set(output "$<STRING:TIMESTAMP,UTC,%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TIMESTAMP,UTC,%Y-%m-%d %H:%M:%S.%f %A=%a %B=%b %y day=%j wd=%w week=%U w_iso=%V %%I=%I epoch=%s TZ=%Z tz=%z> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:TIMESTAMP" ${errors})

+ 26 - 0
Tests/RunCMake/GenEx-STRING/TOLOWER.cmake.in

@@ -0,0 +1,26 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(TOLOWER "ABCD"  reference)
+set(output "$<STRING:TOLOWER,ABCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TOLOWER,ABCD> returns bad data: ${output}")
+endif()
+
+string(TOLOWER "abcd"  reference)
+set(output "$<STRING:TOLOWER,abcd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TOLOWER,abcd> returns bad data: ${output}")
+endif()
+
+string(TOLOWER "abcdABCD12@!"  reference)
+set(output "$<STRING:TOLOWER,abcdABCD12@!>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TOLOWER,abcdABCD12@!> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:TOLOWER" ${errors})

+ 26 - 0
Tests/RunCMake/GenEx-STRING/TOUPPER.cmake.in

@@ -0,0 +1,26 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+string(TOUPPER "abcd"  reference)
+set(output "$<STRING:TOUPPER,abcd>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TOUPPER,abcd> returns bad data: ${output}")
+endif()
+
+string(TOUPPER "ABCD"  reference)
+set(output "$<STRING:TOUPPER,ABCD>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TOUPPER,ABCD> returns bad data: ${output}")
+endif()
+
+string(TOUPPER "abcdABCD12@!"  reference)
+set(output "$<STRING:TOUPPER,abcdABCD12@!>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:TOUPPER,abcdABCD12@!> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:TOUPPER" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/UUID-WrongArguments-result.txt

@@ -0,0 +1 @@
+1

+ 28 - 0
Tests/RunCMake/GenEx-STRING/UUID-WrongArguments-stderr.txt

@@ -0,0 +1,28 @@
+CMake Error at UUID-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:UUID,TYPE:MD5,NAMESPACE:>
+
+  Invalid value for 'NAMESPACE:' option.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at UUID-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:UUID,NAMESPACE:foo,TYPE:FOO>
+
+  Invalid value for 'TYPE:' option.  'MD5' or 'SHA1' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at UUID-WrongArguments\.cmake:[0-9]+ \(add_custom_target\):
+  Error evaluating generator expression:
+
+    \$<STRING:UUID,NAMESPACE:foo,TYPE:MD5,CASE:FOO>
+
+  Invalid value for 'CASE:' option.  'UPPER' or 'LOWER' expected.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 6 - 0
Tests/RunCMake/GenEx-STRING/UUID-WrongArguments.cmake

@@ -0,0 +1,6 @@
+
+add_custom_target(check ALL COMMAND check
+  $<STRING:UUID,TYPE:MD5,NAMESPACE:>
+  $<STRING:UUID,NAMESPACE:foo,TYPE:FOO>
+  $<STRING:UUID,NAMESPACE:foo,TYPE:MD5,CASE:FOO>
+VERBATIM)

+ 32 - 0
Tests/RunCMake/GenEx-STRING/UUID.cmake.in

@@ -0,0 +1,32 @@
+
+cmake_minimum_required(VERSION 4.2...4.3)
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set(UUID_DNS_NAMESPACE 6ba7b810-9dad-11d1-80b4-00c04fd430c8)
+
+string(UUID reference NAMESPACE ${UUID_DNS_NAMESPACE} NAME www.example.com TYPE MD5)
+set(output "$<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:MD5>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:MD5> returns bad data: ${output}")
+endif()
+set(output "$<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:MD5,CASE:LOWER>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:MD5,CASE:LOWER> returns bad data: ${output}")
+endif()
+
+string(UUID reference NAMESPACE ${UUID_DNS_NAMESPACE} NAME www.example.com TYPE MD5 UPPER)
+set(output "$<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:MD5,CASE:UPPER>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:MD5,CASE:UPPER> returns bad data: ${output}")
+endif()
+
+string(UUID reference NAMESPACE ${UUID_DNS_NAMESPACE} NAME www.example.com TYPE SHA1)
+set(output "$<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:SHA1>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "<STRING:UUID,NAMESPACE:6ba7b810-9dad-11d1-80b4-00c04fd430c8,NAME:www.example.com,TYPE:SHA1> returns bad data: ${output}")
+endif()
+
+
+check_errors("STRING:UUID" ${errors})

+ 1 - 0
Tests/RunCMake/GenEx-STRING/bad-option-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/bad-option-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at bad-option\.cmake:[0-9]+ \(file\):
+  Error evaluating generator expression:
+
+    \$<STRING:BAD_OPTION>
+
+  BAD_OPTION: invalid option.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/GenEx-STRING/bad-option.cmake

@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<STRING:BAD_OPTION>")

+ 13 - 0
Tests/RunCMake/GenEx-STRING/check_errors.cmake

@@ -0,0 +1,13 @@
+
+function (CHECK_ERRORS command)
+  set (errors ${ARGN})
+  set (command "$<${command}>")
+  if (errors)
+    string (LENGTH "${command}" length)
+    math (EXPR count "${length} + 2")
+    string (REPEAT " " ${count} shift)
+    list (TRANSFORM errors PREPEND "${shift}$")
+    list (JOIN errors "\n" msg)
+    message (FATAL_ERROR "${command}: ${msg}")
+  endif()
+endfunction()

+ 6 - 0
Tests/RunCMake/GenEx-STRING/generate.cmake

@@ -0,0 +1,6 @@
+
+if (STRING_TEST STREQUAL "TIMESTAMP")
+  set(ENV{SOURCE_DATE_EPOCH} "1123456789")
+endif()
+
+file(GENERATE OUTPUT "${STRING_TEST}.cmake" INPUT "${STRING_TEST}.cmake.in")

+ 1 - 0
Tests/RunCMake/GenEx-STRING/no-arguments-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/no-arguments-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at no-arguments\.cmake:[0-9]+ \(file\):
+  Error evaluating generator expression:
+
+    \$<STRING:>
+
+  \$<STRING> expression requires at least one parameter.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/GenEx-STRING/no-arguments.cmake

@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<STRING:>")

+ 1 - 0
Tests/RunCMake/GenEx-STRING/unexpected-arg-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/unexpected-arg-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at unexpected-arg\.cmake:[0-9]+ \(file\):
+  Error evaluating generator expression:
+
+    \$<STRING:[A-Z_]+,.+>
+
+  \$<STRING:[A-Z_:]+(,[A-Z_:]+(,[A-Z_:]+)?)?> expression requires exactly (one|two|three) parameters?.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/GenEx-STRING/unexpected-arg.cmake

@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<STRING:${STRING_ARGUMENTS}>")

+ 1 - 0
Tests/RunCMake/GenEx-STRING/unexpected-arg2-result.txt

@@ -0,0 +1 @@
+1

+ 8 - 0
Tests/RunCMake/GenEx-STRING/unexpected-arg2-stderr.txt

@@ -0,0 +1,8 @@
+CMake Error at unexpected-arg2\.cmake:[0-9]+ \(file\):
+  Error evaluating generator expression:
+
+    \$<STRING:[A-Z_]+,.+>
+
+  \$<STRING:[A-Z_:]+(,[A-Z_:]+(,[A-Z_:]+)?)?> expression expects at most (two|three|four) parameters?.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/GenEx-STRING/unexpected-arg2.cmake

@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<STRING:${STRING_ARGUMENTS}>")