string: introduce `REPEAT` sub-command
diff --git a/Help/command/string.rst b/Help/command/string.rst
index 893fb43..2e89d7b 100644
--- a/Help/command/string.rst
+++ b/Help/command/string.rst
@@ -28,6 +28,7 @@
     string(`SUBSTRING`_ <string> <begin> <length> <out-var>)
     string(`STRIP`_ <string> <out-var>)
     string(`GENEX_STRIP`_ <string> <out-var>)
+    string(`REPEAT`_ <string> <count> <out-var>)
 
   `Comparison`_
     string(`COMPARE`_ <op> <string1> <string2> <out-var>)
@@ -269,6 +270,14 @@
 Strip any :manual:`generator expressions <cmake-generator-expressions(7)>`
 from the ``input string`` and store the result in the ``output variable``.
 
+.. _REPEAT:
+
+.. code-block:: cmake
+
+  string(REPEAT <input string> <count> <output variable>)
+
+Produce the output string as repetion of ``input string`` ``count`` times.
+
 Comparison
 ^^^^^^^^^^
 
diff --git a/Help/release/dev/string-repeat.rst b/Help/release/dev/string-repeat.rst
new file mode 100644
index 0000000..4be0d5c
--- /dev/null
+++ b/Help/release/dev/string-repeat.rst
@@ -0,0 +1,4 @@
+string-repeat
+--------------
+
+* The :command:`string` learned a new sub-command ``REPEAT``.
diff --git a/Source/cmStringCommand.cxx b/Source/cmStringCommand.cxx
index 252d985..998f904 100644
--- a/Source/cmStringCommand.cxx
+++ b/Source/cmStringCommand.cxx
@@ -1,9 +1,13 @@
 /* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
    file Copyright.txt or https://cmake.org/licensing for details.  */
+#define _SCL_SECURE_NO_WARNINGS
+
 #include "cmStringCommand.h"
 
 #include "cmsys/RegularExpression.hxx"
+#include <algorithm>
 #include <ctype.h>
+#include <iterator>
 #include <memory> // IWYU pragma: keep
 #include <sstream>
 #include <stdio.h>
@@ -13,6 +17,7 @@
 #include "cmCryptoHash.h"
 #include "cmGeneratorExpression.h"
 #include "cmMakefile.h"
+#include "cmMessageType.h"
 #include "cmRange.h"
 #include "cmStringReplaceHelper.h"
 #include "cmSystemTools.h"
@@ -79,6 +84,9 @@
   if (subCommand == "STRIP") {
     return this->HandleStripCommand(args);
   }
+  if (subCommand == "REPEAT") {
+    return this->HandleRepeatCommand(args);
+  }
   if (subCommand == "RANDOM") {
     return this->HandleRandomCommand(args);
   }
@@ -709,6 +717,59 @@
   return true;
 }
 
+bool cmStringCommand::HandleRepeatCommand(std::vector<std::string> const& args)
+{
+  // `string(REPEAT "<str>" <times> OUTPUT_VARIABLE)`
+  enum ArgPos : std::size_t
+  {
+    SUB_COMMAND,
+    VALUE,
+    TIMES,
+    OUTPUT_VARIABLE,
+    TOTAL_ARGS
+  };
+
+  if (args.size() != ArgPos::TOTAL_ARGS) {
+    this->Makefile->IssueMessage(
+      MessageType::FATAL_ERROR,
+      "sub-command REPEAT requires three arguments.");
+    return true;
+  }
+
+  unsigned long times;
+  if (!cmSystemTools::StringToULong(args[ArgPos::TIMES].c_str(), &times)) {
+    this->Makefile->IssueMessage(MessageType::FATAL_ERROR,
+                                 "repeat count is not a positive number.");
+    return true;
+  }
+
+  const auto& stringValue = args[ArgPos::VALUE];
+  const auto& variableName = args[ArgPos::OUTPUT_VARIABLE];
+  const auto inStringLength = stringValue.size();
+
+  std::string result;
+  switch (inStringLength) {
+    case 0u:
+      // Nothing to do for zero length input strings
+      break;
+    case 1u:
+      // NOTE If the string to repeat consists of the only character,
+      // use the appropriate constructor.
+      result = std::string(times, stringValue[0]);
+      break;
+    default:
+      result = std::string(inStringLength * times, char{});
+      for (auto i = 0u; i < times; ++i) {
+        std::copy(cm::cbegin(stringValue), cm::cend(stringValue),
+                  &result[i * inStringLength]);
+      }
+      break;
+  }
+
+  this->Makefile->AddDefinition(variableName, result.c_str());
+  return true;
+}
+
 bool cmStringCommand::HandleRandomCommand(std::vector<std::string> const& args)
 {
   if (args.size() < 2 || args.size() == 3 || args.size() == 5) {
diff --git a/Source/cmStringCommand.h b/Source/cmStringCommand.h
index cbff73e..acde605 100644
--- a/Source/cmStringCommand.h
+++ b/Source/cmStringCommand.h
@@ -51,6 +51,7 @@
   bool HandleConcatCommand(std::vector<std::string> const& args);
   bool HandleJoinCommand(std::vector<std::string> const& args);
   bool HandleStripCommand(std::vector<std::string> const& args);
+  bool HandleRepeatCommand(std::vector<std::string> const& args);
   bool HandleRandomCommand(std::vector<std::string> const& args);
   bool HandleFindCommand(std::vector<std::string> const& args);
   bool HandleTimestampCommand(std::vector<std::string> const& args);
diff --git a/Tests/RunCMake/string/Repeat.cmake b/Tests/RunCMake/string/Repeat.cmake
new file mode 100644
index 0000000..fc390aa
--- /dev/null
+++ b/Tests/RunCMake/string/Repeat.cmake
@@ -0,0 +1,45 @@
+string(REPEAT "q" 4 q_out)
+
+if(NOT DEFINED q_out)
+  message(FATAL_ERROR "q_out is not defined")
+endif()
+
+if(NOT q_out STREQUAL "qqqq")
+  message(FATAL_ERROR "unexpected result")
+endif()
+
+string(REPEAT "1234" 0 zero_out)
+
+if(NOT DEFINED zero_out)
+  message(FATAL_ERROR "zero_out is not defined")
+endif()
+
+if(NOT zero_out STREQUAL "")
+  message(FATAL_ERROR "unexpected result")
+endif()
+
+unset(zero_out)
+
+string(REPEAT "" 100 zero_out)
+
+if(NOT DEFINED zero_out)
+  message(FATAL_ERROR "zero_out is not defined")
+endif()
+
+if(NOT zero_out STREQUAL "")
+  message(FATAL_ERROR "unexpected result")
+endif()
+
+string(REPEAT "1" 1 one_out)
+
+if(NOT one_out STREQUAL "1")
+  message(FATAL_ERROR "unexpected result")
+endif()
+
+unset(one_out)
+
+string(REPEAT "one" 1 one_out)
+
+if(NOT one_out STREQUAL "one")
+  message(FATAL_ERROR "unexpected result")
+endif()
diff --git a/Tests/RunCMake/string/RepeatNegativeCount-result.txt b/Tests/RunCMake/string/RepeatNegativeCount-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/string/RepeatNegativeCount-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/string/RepeatNegativeCount-stderr.txt b/Tests/RunCMake/string/RepeatNegativeCount-stderr.txt
new file mode 100644
index 0000000..bbd498e
--- /dev/null
+++ b/Tests/RunCMake/string/RepeatNegativeCount-stderr.txt
@@ -0,0 +1,4 @@
+CMake Error at RepeatNegativeCount.cmake:[0-9]+ \(string\):
+  repeat count is not a positive number.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/string/RepeatNegativeCount.cmake b/Tests/RunCMake/string/RepeatNegativeCount.cmake
new file mode 100644
index 0000000..769e7c0
--- /dev/null
+++ b/Tests/RunCMake/string/RepeatNegativeCount.cmake
@@ -0,0 +1 @@
+string(REPEAT "blah" -1 out)
diff --git a/Tests/RunCMake/string/RepeatNoArgs-result.txt b/Tests/RunCMake/string/RepeatNoArgs-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/string/RepeatNoArgs-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/string/RepeatNoArgs-stderr.txt b/Tests/RunCMake/string/RepeatNoArgs-stderr.txt
new file mode 100644
index 0000000..5abcb3b
--- /dev/null
+++ b/Tests/RunCMake/string/RepeatNoArgs-stderr.txt
@@ -0,0 +1,4 @@
+CMake Error at RepeatNoArgs.cmake:[0-9]+ \(string\):
+  sub-command REPEAT requires three arguments.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/string/RepeatNoArgs.cmake b/Tests/RunCMake/string/RepeatNoArgs.cmake
new file mode 100644
index 0000000..e327e99
--- /dev/null
+++ b/Tests/RunCMake/string/RepeatNoArgs.cmake
@@ -0,0 +1 @@
+string(REPEAT)
diff --git a/Tests/RunCMake/string/RunCMakeTest.cmake b/Tests/RunCMake/string/RunCMakeTest.cmake
index 211337a..c432b4e 100644
--- a/Tests/RunCMake/string/RunCMakeTest.cmake
+++ b/Tests/RunCMake/string/RunCMakeTest.cmake
@@ -33,3 +33,7 @@
 run_cmake(UTF-16LE)
 run_cmake(UTF-32BE)
 run_cmake(UTF-32LE)
+
+run_cmake(Repeat)
+run_cmake(RepeatNoArgs)
+run_cmake(RepeatNegativeCount)