瀏覽代碼

Genex-PATH: path handling

Fixes: #23498
Marc Chevrier 3 年之前
父節點
當前提交
f11e66670b
共有 31 個文件被更改,包括 1818 次插入1 次删除
  1. 11 1
      Help/command/cmake_path.rst
  2. 212 0
      Help/manual/cmake-generator-expressions.7.rst
  3. 4 0
      Help/release/dev/Genex-PATH.rst
  4. 436 0
      Source/cmGeneratorExpressionNode.cxx
  5. 1 0
      Tests/RunCMake/CMakeLists.txt
  6. 34 0
      Tests/RunCMake/GenEx-PATH/ABSOLUTE_PATH.cmake.in
  7. 68 0
      Tests/RunCMake/GenEx-PATH/APPEND.cmake.in
  8. 53 0
      Tests/RunCMake/GenEx-PATH/CMAKE_PATH.cmake.in
  9. 5 0
      Tests/RunCMake/GenEx-PATH/CMakeLists.txt
  10. 311 0
      Tests/RunCMake/GenEx-PATH/GET_ITEM.cmake.in
  11. 199 0
      Tests/RunCMake/GenEx-PATH/HAS_ITEM.cmake.in
  12. 44 0
      Tests/RunCMake/GenEx-PATH/IS_ABSOLUTE.cmake.in
  13. 25 0
      Tests/RunCMake/GenEx-PATH/IS_PREFIX.cmake.in
  14. 45 0
      Tests/RunCMake/GenEx-PATH/IS_RELATIVE.cmake.in
  15. 43 0
      Tests/RunCMake/GenEx-PATH/NORMAL_PATH.cmake.in
  16. 64 0
      Tests/RunCMake/GenEx-PATH/RELATIVE_PATH.cmake.in
  17. 65 0
      Tests/RunCMake/GenEx-PATH/REMOVE_ITEM.cmake.in
  18. 73 0
      Tests/RunCMake/GenEx-PATH/REPLACE_ITEM.cmake.in
  19. 68 0
      Tests/RunCMake/GenEx-PATH/RunCMakeTest.cmake
  20. 1 0
      Tests/RunCMake/GenEx-PATH/bad-option-result.txt
  21. 8 0
      Tests/RunCMake/GenEx-PATH/bad-option-stderr.txt
  22. 2 0
      Tests/RunCMake/GenEx-PATH/bad-option.cmake
  23. 13 0
      Tests/RunCMake/GenEx-PATH/check_errors.cmake
  24. 2 0
      Tests/RunCMake/GenEx-PATH/generate.cmake
  25. 1 0
      Tests/RunCMake/GenEx-PATH/no-arguments-result.txt
  26. 8 0
      Tests/RunCMake/GenEx-PATH/no-arguments-stderr.txt
  27. 2 0
      Tests/RunCMake/GenEx-PATH/no-arguments.cmake
  28. 1 0
      Tests/RunCMake/GenEx-PATH/unexpected-arg-result.txt
  29. 8 0
      Tests/RunCMake/GenEx-PATH/unexpected-arg-stderr.txt
  30. 2 0
      Tests/RunCMake/GenEx-PATH/unexpected-arg.cmake
  31. 9 0
      Tests/RunCMake/GenEx-PATH/unexpected-arg2-stderr.txt

+ 11 - 1
Help/command/cmake_path.rst

@@ -96,6 +96,8 @@ The following conventions are used in this command's documentation:
   The name of a variable into which the result of a command will be written.
 
 
+.. _Path Structure And Terminology:
+
 Path Structure And Terminology
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -216,6 +218,8 @@ normalize a path is as follows:
    is ``.``).
 
 
+.. _Path Decomposition:
+
 Decomposition
 ^^^^^^^^^^^^^
 
@@ -385,6 +389,8 @@ Path traversal examples
   Parent path is "c:/"
 
 
+.. _Path Query:
+
 Query
 ^^^^^
 
@@ -495,6 +501,8 @@ according to the following pseudo-code logic:
   takes literal strings as input, not the names of variables.
 
 
+.. _Path Modification:
+
 Modification
 ^^^^^^^^^^^^
 
@@ -509,7 +517,7 @@ path, it is converted into a cmake-style path with forward-slashes
 (``/``). On Windows, the long filename marker is taken into account.
 
 When the ``NORMALIZE`` option is specified, the path is :ref:`normalized
-<Normalization>` before the conversion.
+<Normalization>` after the conversion.
 
 For example:
 
@@ -644,6 +652,8 @@ is equivalent to the following:
   cmake_path(APPEND_STRING path "input")
 
 
+.. _Path Generation:
+
 Generation
 ^^^^^^^^^^
 

+ 212 - 0
Help/manual/cmake-generator-expressions.7.rst

@@ -123,6 +123,71 @@ String Comparisons
 
   ``1`` if ``v1`` is a version greater than or equal to ``v2``, else ``0``.
 
+.. _GenEx Path Queries:
+
+Path Queries
+------------
+
+The ``$<PATH>`` generator expression offers the same capabilities as the
+:command:`cmake_path` command, for the :ref:`Query <Path Query>` options.
+
+For all ``$<PATH>`` generator expressions, paths are expected in cmake-style
+format. The :ref:`$\<PATH:CMAKE_PATH\> <GenEx PATH-CMAKE_PATH>` generator
+expression can be used to convert a native path to a cmake-style one.
+
+The ``$<PATH>`` generator expression can also be used for path
+:ref:`Decomposition <GenEx Path Decomposition>` and
+:ref:`Transformations <GenEx Path Transformations>`.
+
+.. genex:: $<PATH:HAS_*,path>
+
+  .. versionadded:: 3.24
+
+  The following operations return ``1`` if the particular path component is
+  present, ``0`` otherwise. See :ref:`Path Structure And Terminology` for the
+  meaning of each path component.
+
+  ::
+
+    $<PATH:HAS_ROOT_NAME,path>
+    $<PATH:HAS_ROOT_DIRECTORY,path>
+    $<PATH:HAS_ROOT_PATH,path>
+    $<PATH:HAS_FILENAME,path>
+    $<PATH:HAS_EXTENSION,path>
+    $<PATH:HAS_STEM,path>
+    $<PATH:HAS_RELATIVE_PART,path>
+    $<PATH:HAS_PARENT_PATH,path>
+
+  Note the following special cases:
+
+  * For ``HAS_ROOT_PATH``, a true result will only be returned if at least one
+    of ``root-name`` or ``root-directory`` is non-empty.
+
+  * For ``HAS_PARENT_PATH``, the root directory is also considered to have a
+    parent, which will be itself.  The result is true except if the path
+    consists of just a :ref:`filename <FILENAME_DEF>`.
+
+.. genex:: $<PATH:IS_ABSOLUTE,path>
+
+  .. versionadded:: 3.24
+
+  Returns ``1`` if the path is :ref:`absolute <IS_ABSOLUTE>`, ``0`` otherwise.
+
+.. genex:: $<PATH:IS_RELATIVE,path>
+
+  .. versionadded:: 3.24
+
+  This will return the opposite of ``IS_ABSOLUTE``.
+
+.. genex:: $<PATH:IS_PREFIX[,NORMALIZE],path,input>
+
+  .. versionadded:: 3.24
+
+  Returns ``1`` if ``path`` is the prefix of ``input``,``0`` otherwise.
+
+  When the ``NORMALIZE`` option is specified, ``path`` and ``input`` are
+  :ref:`normalized <Normalization>` before the check.
+
 Variable Queries
 ----------------
 
@@ -668,6 +733,153 @@ String Transformations
         echo $<TARGET_GENEX_EVAL:foo,$<TARGET_PROPERTY:foo,CUSTOM_KEYS>>
     )
 
+.. _GenEx Path Decomposition:
+
+Path Decomposition
+------------------
+
+The ``$<PATH>`` generator expression offers the same capabilities as the
+:command:`cmake_path` command, for the
+:ref:`Decomposition <Path Decomposition>` options.
+
+For all ``$<PATH>`` generator expressions, paths are expected in cmake-style
+format. The :ref:`$\<PATH:CMAKE_PATH\> <GenEx PATH-CMAKE_PATH>` generator
+expression can be used to convert a native path to a cmake-style one.
+
+The ``$<PATH>`` generator expression can also be used for path
+:ref:`Queries <GenEx Path Queries>` and
+:ref:`Transformations <GenEx Path Transformations>`.
+
+.. genex:: $<PATH:GET_*,...>
+
+  .. versionadded:: 3.24
+
+  The following operations retrieve a different component or group of
+  components from a path. See :ref:`Path Structure And Terminology` for the
+  meaning of each path component.
+
+  ::
+
+    $<PATH:GET_ROOT_NAME,path>
+    $<PATH:GET_ROOT_DIRECTORY,path>
+    $<PATH:GET_ROOT_PATH,path>
+    $<PATH:GET_FILENAME,path>
+    $<PATH:GET_EXTENSION[,LAST_ONLY],path>
+    $<PATH:GET_STEM[,LAST_ONLY],path>
+    $<PATH:GET_RELATIVE_PART,path>
+    $<PATH:GET_PARENT_PATH,path>
+
+  If a requested component is not present in the path, an empty string is
+  returned.
+
+.. _GenEx Path Transformations:
+
+Path Transformations
+--------------------
+
+The ``$<PATH>`` generator expression offers the same capabilities as the
+:command:`cmake_path` command, for the
+:ref:`Modification <Path Modification>` and
+:ref:`Generation <Path Generation>` options.
+
+For all ``$<PATH>`` generator expressions, paths are expected in cmake-style
+format. The :ref:`$\<PATH:CMAKE_PATH\> <GenEx PATH-CMAKE_PATH>` generator
+expression can be used to convert a native path to a cmake-style one.
+
+The ``$<PATH>`` generator expression can also be used for path
+:ref:`Queries <GenEx Path Queries>` and
+:ref:`Decomposition <GenEx Path Decomposition>`.
+
+.. _GenEx PATH-CMAKE_PATH:
+
+.. genex:: $<PATH:CMAKE_PATH[,NORMALIZE],path>
+
+  .. versionadded:: 3.24
+
+  Returns ``path``. If ``path`` is a native path, it is converted into a
+  cmake-style path with forward-slashes (``/``). On Windows, the long filename
+  marker is taken into account.
+
+  When the ``NORMALIZE`` option is specified, the path is :ref:`normalized
+  <Normalization>` after the conversion.
+
+.. genex:: $<PATH:APPEND,path,input,...>
+
+  .. versionadded:: 3.24
+
+  Returns all the ``input`` arguments appended to ``path`` using ``/`` as the
+  ``directory-separator``. Depending on the ``input``, the value of ``path``
+  may be discarded.
+
+  See :ref:`cmake_path(APPEND) <APPEND>` for more details.
+
+.. genex:: $<PATH:REMOVE_FILENAME,path>
+
+  .. versionadded:: 3.24
+
+  Returns ``path`` with filename component (as returned by
+  ``$<PATH:GET_FILENAME>``) removed. After removal, any trailing
+  ``directory-separator`` is left alone, if present.
+
+  See :ref:`cmake_path(REMOVE_FILENAME) <REMOVE_FILENAME>` for more details.
+
+.. genex:: $<PATH:REPLACE_FILENAME,path,input>
+
+  .. versionadded:: 3.24
+
+  Returns ``path`` with the filename component replaced by ``input``. If
+  ``path`` has no filename component (i.e. ``$<PATH:HAS_FILENAME>`` returns
+  ``0``), ``path`` is unchanged.
+
+  See :ref:`cmake_path(REPLACE_FILENAME) <REPLACE_FILENAME>` for more details.
+
+.. genex:: $<PATH:REMOVE_EXTENSION[,LAST_ONLY],path>
+
+  .. versionadded:: 3.24
+
+  Returns ``path`` with the :ref:`extension <EXTENSION_DEF>` removed, if any.
+
+  See :ref:`cmake_path(REMOVE_EXTENSION) <REMOVE_EXTENSION>` for more details.
+
+.. genex:: $<PATH:REPLACE_EXTENSION[,LAST_ONLY],path>
+
+  .. versionadded:: 3.24
+
+  Returns ``path`` with the :ref:`extension <EXTENSION_DEF>` replaced by
+  ``input``, if any.
+
+  See :ref:`cmake_path(REPLACE_EXTENSION) <REPLACE_EXTENSION>` for more details.
+
+.. genex:: $<PATH:NORMAL_PATH,path>
+
+  .. versionadded:: 3.24
+
+  Returns ``path`` normalized according to the steps described in
+  :ref:`Normalization`.
+
+.. genex:: $<PATH:RELATIVE_PATH,path,base_directory>
+
+  .. versionadded:: 3.24
+
+  Returns ``path``, modified to make it relative to the ``base_directory``
+  argument.
+
+  See :ref:`cmake_path(RELATIVE_PATH) <cmake_path-RELATIVE_PATH>` for more
+  details.
+
+.. genex:: $<PATH:ABSOLUTE_PATH[,NORMALIZE],path,base_directory>
+
+  .. versionadded:: 3.24
+
+  Returns ``path`` as absolute. If ``path`` is a relative path
+  (``$<PATH:IS_RELATIVE>`` returns ``1``), it is evaluated relative to the
+  given base directory specified by ``base_directory`` argument.
+
+  When the ``NORMALIZE`` option is specified, the path is
+  :ref:`normalized <Normalization>` after the path computation.
+
+  See :ref:`cmake_path(ABSOLUTE_PATH) <ABSOLUTE_PATH>` for more details.
+
 Variable Queries
 ----------------
 

+ 4 - 0
Help/release/dev/Genex-PATH.rst

@@ -0,0 +1,4 @@
+Genex-PATH
+----------
+
+* The :genex:`PATH` generator expression was added to manage paths.

+ 436 - 0
Source/cmGeneratorExpressionNode.cxx

@@ -7,10 +7,12 @@
 #include <cerrno>
 #include <cstdlib>
 #include <cstring>
+#include <functional>
 #include <map>
 #include <memory>
 #include <set>
 #include <sstream>
+#include <unordered_map>
 #include <utility>
 
 #include <cm/iterator>
@@ -24,6 +26,7 @@
 #include "cmsys/String.h"
 
 #include "cmAlgorithms.h"
+#include "cmCMakePath.h"
 #include "cmComputeLinkInformation.h"
 #include "cmGeneratorExpression.h"
 #include "cmGeneratorExpressionContext.h"
@@ -599,6 +602,438 @@ static const struct UpperCaseNode : public cmGeneratorExpressionNode
   }
 } upperCaseNode;
 
+namespace {
+template <typename Container>
+class Range : public cmRange<typename Container::const_iterator>
+{
+private:
+  using Base = cmRange<typename Container::const_iterator>;
+
+public:
+  using const_iterator = typename Container::const_iterator;
+  using value_type = typename Container::value_type;
+  using size_type = typename Container::size_type;
+  using difference_type = typename Container::difference_type;
+  using const_reference = typename Container::const_reference;
+
+  Range(const Container& container)
+    : Base(container.begin(), container.end())
+  {
+  }
+
+  const_reference operator[](size_type pos) const
+  {
+    return *(this->begin() + pos);
+  }
+
+  const_reference front() const { return *this->begin(); }
+  const_reference back() const { return *std::prev(this->end()); }
+
+  Range& advance(difference_type amount) &
+  {
+    Base::advance(amount);
+    return *this;
+  }
+  Range advance(difference_type amount) &&
+  {
+    Base::advance(amount);
+    return std::move(*this);
+  }
+};
+
+using Arguments = Range<std::vector<std::string>>;
+
+bool CheckPathParametersEx(cmGeneratorExpressionContext* ctx,
+                           const GeneratorExpressionContent* cnt,
+                           cm::string_view option, std::size_t count,
+                           int required = 1, bool exactly = true)
+{
+  if (static_cast<int>(count) < required ||
+      (exactly && static_cast<int>(count) > required)) {
+    reportError(ctx, cnt->GetOriginalExpression(),
+                cmStrCat("$<PATH:", option, "> expression requires ",
+                         (exactly ? "exactly" : "at least"), ' ',
+                         (required == 1 ? "one parameter" : "two parameters"),
+                         '.'));
+    return false;
+  }
+  return true;
+};
+bool CheckPathParameters(cmGeneratorExpressionContext* ctx,
+                         const GeneratorExpressionContent* cnt,
+                         cm::string_view option, const Arguments& args,
+                         int required = 1)
+{
+  return CheckPathParametersEx(ctx, cnt, option, args.size(), required);
+};
+std::string ToString(bool isTrue)
+{
+  return isTrue ? "1" : "0";
+};
+}
+
+static const struct PathNode : public cmGeneratorExpressionNode
+{
+  PathNode() {} // NOLINT(modernize-use-equals-default)
+
+  int NumExpectedParameters() const override { return TwoOrMoreParameters; }
+
+  bool AcceptsArbitraryContentParameter() const override { return true; }
+
+  std::string Evaluate(
+    const std::vector<std::string>& parameters,
+    cmGeneratorExpressionContext* context,
+    const GeneratorExpressionContent* content,
+    cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override
+  {
+    static std::unordered_map<
+      cm::string_view,
+      std::function<std::string(cmGeneratorExpressionContext*,
+                                const GeneratorExpressionContent*,
+                                Arguments&)>>
+      pathCommands{
+        { "GET_ROOT_NAME"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "GET_ROOT_NAME"_s, args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.GetRootName().String()
+              : std::string{};
+          } },
+        { "GET_ROOT_DIRECTORY"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "GET_ROOT_DIRECTORY"_s,
+                                       args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.GetRootDirectory().String()
+              : std::string{};
+          } },
+        { "GET_ROOT_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "GET_ROOT_PATH"_s, args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.GetRootPath().String()
+              : std::string{};
+          } },
+        { "GET_FILENAME"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "GET_FILENAME"_s, args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.GetFileName().String()
+              : std::string{};
+          } },
+        { "GET_EXTENSION"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool lastOnly = args.front() == "LAST_ONLY"_s;
+            if (lastOnly) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(ctx, cnt,
+                                      lastOnly ? "GET_EXTENSION,LAST_ONLY"_s
+                                               : "GET_EXTENSION"_s,
+                                      args.size())) {
+              if (args.front().empty()) {
+                return std::string{};
+              }
+              return lastOnly
+                ? cmCMakePath{ args.front() }.GetExtension().String()
+                : cmCMakePath{ args.front() }.GetWideExtension().String();
+            }
+            return std::string{};
+          } },
+        { "GET_STEM"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool lastOnly = args.front() == "LAST_ONLY"_s;
+            if (lastOnly) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(
+                  ctx, cnt, lastOnly ? "GET_STEM,LAST_ONLY"_s : "GET_STEM"_s,
+                  args.size())) {
+              if (args.front().empty()) {
+                return std::string{};
+              }
+              return lastOnly
+                ? cmCMakePath{ args.front() }.GetStem().String()
+                : cmCMakePath{ args.front() }.GetNarrowStem().String();
+            }
+            return std::string{};
+          } },
+        { "GET_RELATIVE_PART"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "GET_RELATIVE_PART"_s,
+                                       args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.GetRelativePath().String()
+              : std::string{};
+          } },
+        { "GET_PARENT_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "GET_PARENT_PATH"_s, args)
+              ? cmCMakePath{ args.front() }.GetParentPath().String()
+              : std::string{};
+          } },
+        { "HAS_ROOT_NAME"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_ROOT_NAME"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasRootName())
+              : std::string{ "0" };
+          } },
+        { "HAS_ROOT_DIRECTORY"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_ROOT_DIRECTORY"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasRootDirectory())
+              : std::string{ "0" };
+          } },
+        { "HAS_ROOT_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_ROOT_PATH"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasRootPath())
+              : std::string{ "0" };
+          } },
+        { "HAS_FILENAME"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_FILENAME"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasFileName())
+              : std::string{ "0" };
+          } },
+        { "HAS_EXTENSION"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_EXTENSION"_s, args) &&
+                !args.front().empty()
+              ? ToString(cmCMakePath{ args.front() }.HasExtension())
+              : std::string{ "0" };
+          } },
+        { "HAS_STEM"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_STEM"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasStem())
+              : std::string{ "0" };
+          } },
+        { "HAS_RELATIVE_PART"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_RELATIVE_PART"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasRelativePath())
+              : std::string{ "0" };
+          } },
+        { "HAS_PARENT_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "HAS_PARENT_PATH"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.HasParentPath())
+              : std::string{ "0" };
+          } },
+        { "IS_ABSOLUTE"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "IS_ABSOLUTE"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.IsAbsolute())
+              : std::string{ "0" };
+          } },
+        { "IS_RELATIVE"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "IS_RELATIVE"_s, args)
+              ? ToString(cmCMakePath{ args.front() }.IsRelative())
+              : std::string{ "0" };
+          } },
+        { "IS_PREFIX"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool normalize = args.front() == "NORMALIZE"_s;
+            if (normalize) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(ctx, cnt,
+                                      normalize ? "IS_PREFIX,NORMALIZE"_s
+                                                : "IS_PREFIX"_s,
+                                      args.size(), 2)) {
+              if (normalize) {
+                return ToString(cmCMakePath{ args[0] }.Normal().IsPrefix(
+                  cmCMakePath{ args[1] }.Normal()));
+              }
+              return ToString(
+                cmCMakePath{ args[0] }.IsPrefix(cmCMakePath{ args[1] }));
+            }
+            return std::string{};
+          } },
+        { "CMAKE_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool normalize = args.front() == "NORMALIZE"_s;
+            if (normalize) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(ctx, cnt,
+                                      normalize ? "CMAKE_PATH,NORMALIZE"_s
+                                                : "CMAKE_PATH"_s,
+                                      args.size(), 1)) {
+              auto path =
+                cmCMakePath{ args.front(), cmCMakePath::auto_format };
+              return normalize ? path.Normal().GenericString()
+                               : path.GenericString();
+            }
+            return std::string{};
+          } },
+        { "APPEND"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            if (CheckPathParametersEx(ctx, cnt, "APPEND"_s, args.size(), 1,
+                                      false)) {
+              cmCMakePath path;
+              for (const auto& p : args) {
+                path /= p;
+              }
+              return path.String();
+            }
+            return std::string{};
+          } },
+        { "REMOVE_FILENAME"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "REMOVE_FILENAME"_s, args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.RemoveFileName().String()
+              : std::string{};
+          } },
+        { "REPLACE_FILENAME"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "REPLACE_FILENAME"_s, args, 2)
+              ? cmCMakePath{ args[0] }
+                  .ReplaceFileName(cmCMakePath{ args[1] })
+                  .String()
+              : std::string{};
+          } },
+        { "REMOVE_EXTENSION"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool lastOnly = args.front() == "LAST_ONLY"_s;
+            if (lastOnly) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(ctx, cnt,
+                                      lastOnly ? "REMOVE_EXTENSION,LAST_ONLY"_s
+                                               : "REMOVE_EXTENSION"_s,
+                                      args.size())) {
+              if (args.front().empty()) {
+                return std::string{};
+              }
+              return lastOnly
+                ? cmCMakePath{ args.front() }.RemoveExtension().String()
+                : cmCMakePath{ args.front() }.RemoveWideExtension().String();
+            }
+            return std::string{};
+          } },
+        { "REPLACE_EXTENSION"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool lastOnly = args.front() == "LAST_ONLY"_s;
+            if (lastOnly) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(ctx, cnt,
+                                      lastOnly
+                                        ? "REPLACE_EXTENSION,LAST_ONLY"_s
+                                        : "REPLACE_EXTENSION"_s,
+                                      args.size(), 2)) {
+              if (lastOnly) {
+                return cmCMakePath{ args[0] }
+                  .ReplaceExtension(cmCMakePath{ args[1] })
+                  .String();
+              }
+              return cmCMakePath{ args[0] }
+                .ReplaceWideExtension(cmCMakePath{ args[1] })
+                .String();
+            }
+            return std::string{};
+          } },
+        { "NORMAL_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "NORMAL_PATH"_s, args) &&
+                !args.front().empty()
+              ? cmCMakePath{ args.front() }.Normal().String()
+              : std::string{};
+          } },
+        { "RELATIVE_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            return CheckPathParameters(ctx, cnt, "RELATIVE_PATH"_s, args, 2)
+              ? cmCMakePath{ args[0] }.Relative(args[1]).String()
+              : std::string{};
+          } },
+        { "ABSOLUTE_PATH"_s,
+          [](cmGeneratorExpressionContext* ctx,
+             const GeneratorExpressionContent* cnt,
+             Arguments& args) -> std::string {
+            bool normalize = args.front() == "NORMALIZE"_s;
+            if (normalize) {
+              args.advance(1);
+            }
+            if (CheckPathParametersEx(ctx, cnt,
+                                      normalize ? "ABSOLUTE_PATH,NORMALIZE"_s
+                                                : "ABSOLUTE_PATH"_s,
+                                      args.size(), 2)) {
+              auto path = cmCMakePath{ args[0] }.Absolute(args[1]);
+              return normalize ? path.Normal().String() : path.String();
+            }
+            return std::string{};
+          } }
+      };
+
+    if (cm::contains(pathCommands, parameters.front())) {
+      auto args = Arguments{ parameters }.advance(1);
+      return pathCommands[parameters.front()](context, content, args);
+    }
+
+    reportError(context, content->GetOriginalExpression(),
+                cmStrCat(parameters.front(), ": invalid option."));
+    return std::string{};
+  }
+} pathNode;
+
 static const struct MakeCIdentifierNode : public cmGeneratorExpressionNode
 {
   MakeCIdentifierNode() {} // NOLINT(modernize-use-equals-default)
@@ -2829,6 +3264,7 @@ const cmGeneratorExpressionNode* cmGeneratorExpressionNode::GetNode(
     { "REMOVE_DUPLICATES", &removeDuplicatesNode },
     { "LOWER_CASE", &lowerCaseNode },
     { "UPPER_CASE", &upperCaseNode },
+    { "PATH", &pathNode },
     { "MAKE_C_IDENTIFIER", &makeCIdentifierNode },
     { "BOOL", &boolNode },
     { "IF", &ifNode },

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -330,6 +330,7 @@ add_RunCMake_test(GenEx-LINK_GROUP)
 add_RunCMake_test(GenEx-TARGET_FILE -DLINKER_SUPPORTS_PDB=${LINKER_SUPPORTS_PDB})
 add_RunCMake_test(GenEx-GENEX_EVAL)
 add_RunCMake_test(GenEx-TARGET_RUNTIME_DLLS)
+add_RunCMake_test(GenEx-PATH)
 add_RunCMake_test(GeneratorExpression)
 add_RunCMake_test(GeneratorInstance)
 add_RunCMake_test(GeneratorPlatform)

+ 34 - 0
Tests/RunCMake/GenEx-PATH/ABSOLUTE_PATH.cmake.in

@@ -0,0 +1,34 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set (reference "../../a/d")
+cmake_path(ABSOLUTE_PATH reference BASE_DIRECTORY "/x/y/a/f")
+set(output "$<PATH:ABSOLUTE_PATH,../../a/d,/x/y/a/f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "../../a/d")
+cmake_path(ABSOLUTE_PATH reference BASE_DIRECTORY "/x/y/a/f" NORMALIZE)
+set(output "$<PATH:ABSOLUTE_PATH,NORMALIZE,../../a/d,/x/y/a/f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "/a/d/../e")
+cmake_path(ABSOLUTE_PATH reference BASE_DIRECTORY "/x/y/a/f")
+set(output "$<PATH:ABSOLUTE_PATH,/a/d/../e,/x/y/a/f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "/a/d/../e")
+cmake_path(ABSOLUTE_PATH reference BASE_DIRECTORY "/x/y/a/f" NORMALIZE)
+set(output "$<PATH:ABSOLUTE_PATH,NORMALIZE,/a/d/../e,/x/y/a/f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+
+check_errors("PATH:ABSOLUTE_PATH" ${errors})

+ 68 - 0
Tests/RunCMake/GenEx-PATH/APPEND.cmake.in

@@ -0,0 +1,68 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+cmake_path (APPEND path "/a/b" "c")
+set(output "$<PATH:APPEND,/a/b,c>")
+if (NOT output STREQUAL path)
+  list (APPEND errors "'${output}' instead of '${path}'")
+endif()
+
+set (path "a")
+cmake_path (APPEND path "")
+set(output "$<PATH:APPEND,a,>")
+if (NOT output STREQUAL path)
+  list (APPEND errors "'${output}' instead of '${path}'")
+endif()
+
+cmake_path (APPEND path "/b")
+set(output "$<PATH:APPEND,a/,/b>")
+if (NOT output STREQUAL path)
+  list (APPEND errors "'${output}' instead of '${path}'")
+endif()
+
+if (WIN32)
+  set (path "a")
+  cmake_path (APPEND path "c:/b")
+  set(output "$<PATH:APPEND,a,c:/b>")
+  if (NOT output STREQUAL path)
+    list (APPEND errors "'${output}' instead of '${path}'")
+  endif()
+
+  set (path "a")
+  cmake_path (APPEND path "c:")
+  set(output "$<PATH:APPEND,a,c:>")
+  if (NOT output STREQUAL path)
+    list (APPEND errors "'${output}' instead of '${path}'")
+  endif()
+
+  set (path "c:a")
+  cmake_path (APPEND path "/b")
+  set(output "$<PATH:APPEND,c:a,/b>")
+  if (NOT output STREQUAL path)
+    list (APPEND errors "'${output}' instead of '${path}'")
+  endif()
+
+  set (path "c:a")
+  cmake_path (APPEND path "c:b")
+  set(output "$<PATH:APPEND,c:a,c:b>")
+  if (NOT output STREQUAL path)
+    list (APPEND errors "'${output}' instead of '${path}'")
+  endif()
+
+  set (path "//host")
+  cmake_path (APPEND path "b")
+  set(output "$<PATH:APPEND,//host,b>")
+  if (NOT output STREQUAL path)
+    list (APPEND errors "'${output}' instead of '${path}'")
+  endif()
+
+  set (path "//host/")
+  cmake_path (APPEND path "b")
+  set(output "$<PATH:APPEND,//host/,b>")
+  if (NOT output STREQUAL path)
+    list (APPEND errors "'${output}' instead of '${path}'")
+  endif()
+endif()
+
+check_errors ("PATH:APPEND" ${errors})

+ 53 - 0
Tests/RunCMake/GenEx-PATH/CMAKE_PATH.cmake.in

@@ -0,0 +1,53 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+
+cmake_path(SET reference "/x/y/z/../../a/d")
+set(output "$<PATH:CMAKE_PATH,/x/y/z/../../a/d>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+cmake_path(SET reference NORMALIZE "/x/y/z/../../a/d")
+set(output "$<PATH:CMAKE_PATH,NORMALIZE,/x/y/z/../../a/d>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+if (WIN32)
+  cmake_path(SET reference "/x\\y/z\\..\\../a/d")
+  set(output "$<PATH:CMAKE_PATH,/x\y/z\..\../a/d>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+  cmake_path(SET reference NORMALIZE "/x\\y/z\\..\\../a/d")
+  set(output "$<PATH:CMAKE_PATH,NORMALIZE,/x\y/z\..\../a/d>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+
+  cmake_path(SET reference "//?/c:/x\\y/z\\..\\../a/d")
+  set(output "$<PATH:CMAKE_PATH,//?/c:/x\y/z\..\../a/d>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+  cmake_path(SET reference NORMALIZE "//?/c:/x\\y/z\\..\\../a/d")
+  set(output "$<PATH:CMAKE_PATH,NORMALIZE,//?/c:/x\y/z\..\../a/d>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+
+  cmake_path(SET reference "\\\\?\\UNC/host/x\\y/z\\..\\../a/d")
+  set(output "$<PATH:CMAKE_PATH,\\?\UNC/host/x\y/z\..\../a/d>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+  cmake_path(SET reference NORMALIZE "\\\\?\\UNC\\host/x\\y/z\\..\\../a/d")
+  set(output "$<PATH:CMAKE_PATH,NORMALIZE,\\?\UNC/host/x\y/z\..\../a/d>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+endif()
+
+
+check_errors("PATH:CMAKE_PATH" ${errors})

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

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

+ 311 - 0
Tests/RunCMake/GenEx-PATH/GET_ITEM.cmake.in

@@ -0,0 +1,311 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+###############################################
+## First test with a path defining all elements
+###############################################
+if (WIN32)
+  set (path "C:/aa/bb/cc.ext1.ext2")
+else()
+  set (path "/aa/bb/cc.ext1.ext2")
+endif()
+
+cmake_path(GET path ROOT_NAME reference)
+if (WIN32)
+  set(output "$<PATH:GET_ROOT_NAME,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_ROOT_NAME,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_NAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_DIRECTORY reference)
+if (WIN32)
+  set(output "$<PATH:GET_ROOT_DIRECTORY,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_ROOT_DIRECTORY,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_DIRECTORY returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_PATH reference)
+if (WIN32)
+  set(output "$<PATH:GET_ROOT_PATH,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_ROOT_PATH,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_PATH returns bad data: ${output}")
+endif()
+
+cmake_path(GET path FILENAME reference)
+if (WIN32)
+  set(output "$<PATH:GET_FILENAME,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_FILENAME,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path EXTENSION reference)
+if (WIN32)
+  set(output "$<PATH:GET_EXTENSION,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_EXTENSION,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+cmake_path(GET path EXTENSION LAST_ONLY reference)
+if (WIN32)
+  set(output "$<PATH:GET_EXTENSION,LAST_ONLY,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_EXTENSION,LAST_ONLY,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION LAST_ONLY returns bad data: ${output}")
+endif()
+
+cmake_path(GET path STEM reference)
+if (WIN32)
+  set(output "$<PATH:GET_STEM,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_STEM,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM returns bad data: ${output}")
+endif()
+cmake_path(GET path STEM LAST_ONLY reference)
+if (WIN32)
+  set(output "$<PATH:GET_STEM,LAST_ONLY,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_STEM,LAST_ONLY,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM LAST_ONLY returns bad data: ${reference}")
+endif()
+
+cmake_path(GET path RELATIVE_PART reference)
+if (WIN32)
+  set(output "$<PATH:GET_RELATIVE_PART,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_RELATIVE_PART,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "RELATIVE_PART returns bad data: ${output}")
+endif()
+
+cmake_path(GET path PARENT_PATH reference)
+if (WIN32)
+  set(output "$<PATH:GET_PARENT_PATH,C:/aa/bb/cc.ext1.ext2>")
+else()
+  set (output "$<PATH:GET_PARENT_PATH,/aa/bb/cc.ext1.ext2>")
+endif()
+if (NOT output STREQUAL reference)
+  list (APPEND errors "PARENT_PATH returns bad data: ${output}")
+endif()
+
+######################################
+## second, tests with missing elements
+######################################
+set (path "aa/bb/")
+
+cmake_path(GET path ROOT_NAME reference)
+set(output "$<PATH:GET_ROOT_NAME,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_NAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_DIRECTORY reference)
+set(output "$<PATH:GET_ROOT_DIRECTORY,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_DIRECTORY returns bad data: >${output}<, >${reference}<")
+endif()
+
+cmake_path(GET path ROOT_PATH reference)
+set(output "$<PATH:GET_ROOT_PATH,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_PATH returns bad data: ${output}")
+endif()
+
+cmake_path(GET path FILENAME reference)
+set(output "$<PATH:GET_FILENAME,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path EXTENSION reference)
+set(output "$<PATH:GET_EXTENSION,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+
+cmake_path(GET path STEM reference)
+set(output "$<PATH:GET_STEM,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM returns bad data: ${output}")
+endif()
+
+cmake_path(GET path RELATIVE_PART reference)
+set(output "$<PATH:GET_RELATIVE_PART,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "RELATIVE_PART returns bad data: ${output}")
+endif()
+
+cmake_path(GET path PARENT_PATH reference)
+set(output "$<PATH:GET_PARENT_PATH,aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "PARENT_PATH returns bad data: ${output}")
+endif()
+
+##################################
+set (path "/aa/bb/")
+
+cmake_path(GET path ROOT_NAME reference)
+set(output "$<PATH:GET_ROOT_NAME,/aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_NAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_DIRECTORY reference)
+set(output "$<PATH:GET_ROOT_DIRECTORY,/aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_DIRECTORY returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_PATH reference)
+set(output "$<PATH:GET_ROOT_PATH,/aa/bb/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_PATH returns bad data: ${output}")
+endif()
+
+###################################
+set (path "/")
+
+cmake_path(GET path ROOT_NAME reference)
+set(output "$<PATH:GET_ROOT_NAME,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_NAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_DIRECTORY reference)
+set(output "$<PATH:GET_ROOT_DIRECTORY,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_DIRECTORY returns bad data: ${output}")
+endif()
+
+cmake_path(GET path ROOT_PATH reference)
+set(output "$<PATH:GET_ROOT_PATH,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "ROOT_PATH returns bad data: ${output}")
+endif()
+
+cmake_path(GET path FILENAME reference)
+set(output "$<PATH:GET_FILENAME,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path EXTENSION reference)
+set(output "$<PATH:GET_EXTENSION,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+
+cmake_path(GET path STEM reference)
+set(output "$<PATH:GET_STEM,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM returns bad data: ${output}")
+endif()
+
+cmake_path(GET path RELATIVE_PART reference)
+set(output "$<PATH:GET_RELATIVE_PART,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "RELATIVE_PART returns bad data: ${output}")
+endif()
+
+cmake_path(GET path PARENT_PATH reference)
+set(output "$<PATH:GET_PARENT_PATH,/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "PARENT_PATH returns bad data: ${output}")
+endif()
+
+###################################
+set (path ".file")
+
+cmake_path(GET path FILENAME reference)
+set(output "$<PATH:GET_FILENAME,.file>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path EXTENSION reference)
+set(output "$<PATH:GET_EXTENSION,.file>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+
+cmake_path(GET path STEM reference)
+set(output "$<PATH:GET_STEM,.file>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM returns bad data: ${output}")
+endif()
+
+###################################
+set (path ".file.ext")
+
+cmake_path(GET path FILENAME reference)
+set(output "$<PATH:GET_FILENAME,.file.ext>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path EXTENSION reference)
+set(output "$<PATH:GET_EXTENSION,.file.ext>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+cmake_path(GET path EXTENSION LAST_ONLY reference)
+set(output "$<PATH:GET_EXTENSION,LAST_ONLY,.file.ext>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+
+cmake_path(GET path STEM reference)
+set(output "$<PATH:GET_STEM,.file.ext>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM returns bad data: ${output}")
+endif()
+
+###################################
+set (path ".file.ext1.ext2")
+
+cmake_path(GET path FILENAME reference)
+set(output "$<PATH:GET_FILENAME,.file.ext1.ext2>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME returns bad data: ${output}")
+endif()
+
+cmake_path(GET path EXTENSION reference)
+set(output "$<PATH:GET_EXTENSION,.file.ext1.ext2>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+cmake_path(GET path EXTENSION LAST_ONLY reference)
+set(output "$<PATH:GET_EXTENSION,LAST_ONLY,.file.ext1.ext2>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION returns bad data: ${output}")
+endif()
+
+cmake_path(GET path STEM reference)
+set(output "$<PATH:GET_STEM,.file.ext1.ext2>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "STEM returns bad data: ${output}")
+endif()
+
+
+check_errors("PATH:GET..." ${errors})

+ 199 - 0
Tests/RunCMake/GenEx-PATH/HAS_ITEM.cmake.in

@@ -0,0 +1,199 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set(output "$<PATH:HAS_ROOT_NAME,/a/b>")
+if (output)
+  list (APPEND errors "ROOT_NAME: '/a/b' has root name")
+endif()
+set(output "$<PATH:HAS_ROOT_DIRECTORY,/a/b>")
+if (NOT output)
+  list (APPEND errors "ROOT_DIRECTORY: '/a/b' does not have root directory")
+endif()
+set(output "$<PATH:HAS_ROOT_PATH,/a/b>")
+if (NOT output)
+  list (APPEND errors "ROOT_PATH: '/a/b' does not have root path")
+endif()
+
+set(output "$<PATH:HAS_ROOT_PATH,a/b>")
+if (output)
+  list (APPEND errors "ROOT_PATH: 'a/b' has root path")
+endif()
+
+set(output "$<PATH:HAS_FILENAME,/a/b>")
+if (NOT output)
+  list (APPEND errors "FILENAME: '/a/b' does not have filename")
+endif()
+set(output "$<PATH:HAS_FILENAME,a.b>")
+if (NOT output)
+  list (APPEND errors "FILENAME: 'a.b' does not have filename")
+endif()
+set(output "$<PATH:HAS_FILENAME,/a/b/>")
+if (output)
+  list (APPEND errors "FILENAME: '/a/b/' has filename")
+endif()
+set(output "$<PATH:HAS_FILENAME,/>")
+if (output)
+  list (APPEND errors "FILENAME: '/' has filename")
+endif()
+
+set(output "$<PATH:HAS_STEM,/a/b>")
+if (NOT output)
+  list (APPEND errors "STEM: '/a/b' does not have stem")
+endif()
+set(output "$<PATH:HAS_STEM,a.b>")
+if (NOT output)
+  list (APPEND errors "STEM: 'a.b' does not have stem")
+endif()
+set(output "$<PATH:HAS_STEM,.a>")
+if (NOT output)
+  list (APPEND errors "STEM: '.a'} does not have stem")
+endif()
+set(output "$<PATH:HAS_STEM,/a/>")
+if (output)
+  list (APPEND errors "STEM: '/a/' has stem")
+endif()
+set(output "$<PATH:HAS_STEM,/>")
+if (output)
+  list (APPEND errors "STEM: '/' has stem")
+endif()
+
+set(output "$<PATH:HAS_EXTENSION,/a/b.c>")
+if (NOT output)
+  list (APPEND errors "EXTENSION: '/a/b.c' does not have extension")
+endif()
+set(output "$<PATH:HAS_EXTENSION,b.c>")
+if (NOT output)
+  list (APPEND errors "EXTENSION: 'b.c' does not have extension")
+endif()
+set(output "$<PATH:HAS_EXTENSION,/.a>")
+if (output)
+  list (APPEND errors "EXTENSION: '/.a' has extension")
+endif()
+set(output "$<PATH:HAS_EXTENSION,/a/>")
+if (output)
+  list (APPEND errors "EXTENSION: '/a/' has extension")
+endif()
+set(output "$<PATH:HAS_EXTENSION,/>")
+if (output)
+  list (APPEND errors "EXTENSION: '/' has extension")
+endif()
+
+set(output "$<PATH:HAS_RELATIVE_PART,/a/b>")
+if (NOT output)
+  list (APPEND errors "RELATIVE_PART: '/a/b' does not have relative part")
+endif()
+set(output "$<PATH:HAS_RELATIVE_PART,/>")
+if (output)
+  list (APPEND errors "RELATIVE_PART: '/' has relative part")
+endif()
+
+set(output "$<PATH:HAS_PARENT_PATH,/a/b>")
+if (NOT output)
+  list (APPEND errors "PARENT_PATH: '/a/b' does not have parent path")
+endif()
+set(output "$<PATH:HAS_PARENT_PATH,/>")
+if (NOT output)
+  list (APPEND errors "PARENT_PATH: '/' does not have parent path")
+endif()
+set(output "$<PATH:HAS_PARENT_PATH,a>")
+if (output)
+  list (APPEND errors "PARENT_PATH: 'a' has parent path")
+endif()
+
+if (WIN32)
+  set(output "$<PATH:HAS_ROOT_NAME,c:/a/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_NAME: 'c:/a/b' does not have root name")
+  endif()
+  set(output "$<PATH:HAS_ROOT_DIRECTORY,c:/a/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_DIRECTORY: 'c:/a/b' does not have root directory")
+  endif()
+  set(output "$<PATH:HAS_ROOT_PATH,c:/a/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_PATH: 'c:/a/b' does not have root path")
+  endif()
+
+  set(output "$<PATH:HAS_ROOT_NAME,c:a/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_NAME: 'c:a/b' does not have root name")
+  endif()
+  set(output "$<PATH:HAS_ROOT_DIRECTORY,c:a/b>")
+  if (output)
+    list (APPEND errors "ROOT_DIRECTORY: 'c:a/b' has root directory")
+  endif()
+  set(output "$<PATH:HAS_ROOT_PATH,c:a/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_PATH: 'c:a/b' does not have root path")
+  endif()
+
+  set(output "$<PATH:HAS_ROOT_NAME,//host/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_NAME: '//host/b' does not have root name")
+  endif()
+  set(output "$<PATH:HAS_ROOT_DIRECTORY,//host/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_DIRECTORY: '//host/b' does not have root directory")
+  endif()
+  set(output "$<PATH:HAS_ROOT_PATH,//host/b>")
+  if (NOT output)
+    list (APPEND errors "ROOT_PATH: '//host/b' does not have root path")
+  endif()
+
+  set(output "$<PATH:HAS_ROOT_NAME,//host>")
+  if (NOT output)
+    list (APPEND errors "ROOT_NAME: '//host' does not have root name")
+  endif()
+  set(output "$<PATH:HAS_ROOT_DIRECTORY,//host>")
+  if (output)
+    list (APPEND errors "ROOT_DIRECTORY: '//host' has root directory")
+  endif()
+  set(output "$<PATH:HAS_ROOT_PATH,//host>")
+  if (NOT output)
+    list (APPEND errors "ROOT_PATH: '//host' does not have root path")
+  endif()
+
+  set(output "$<PATH:HAS_RELATIVE_PART,c:/a/b>")
+  if (NOT output)
+    list (APPEND errors "RELATIVE_PART: 'c:/a/b' does not have relative part")
+  endif()
+
+  set(output "$<PATH:HAS_RELATIVE_PART,c:a/b>")
+  if (NOT output)
+    list (APPEND errors "RELATIVE_PART: 'c:a/b' does not have relative part")
+  endif()
+
+  set(output "$<PATH:HAS_RELATIVE_PART,//host/b>")
+  if (NOT output)
+    list (APPEND errors "RELATIVE_PART: '//host/b' does not have relative part")
+  endif()
+
+  set(output "$<PATH:HAS_PARENT_PATH,c:/a/b>")
+  if (NOT output)
+    list (APPEND errors "PARENT_PATH: 'c:/a/b' does not have parent path")
+  endif()
+
+  set(output "$<PATH:HAS_PARENT_PATH,c:/>")
+  if (NOT output)
+    list (APPEND errors "PARENT_PATH: 'c:/' does not have parent path")
+  endif()
+
+  set(output "$<PATH:HAS_PARENT_PATH,c:>")
+  if (NOT output)
+    list (APPEND errors "PARENT_PATH: 'c:' does not have parent path")
+  endif()
+
+  set(output "$<PATH:HAS_PARENT_PATH,//host/>")
+  if (NOT output)
+    list (APPEND errors "PARENT_PATH: '//host/' does not have parent path")
+  endif()
+
+  set(output "$<PATH:HAS_PARENT_PATH,//host>")
+  if (NOT output)
+    list (APPEND errors "PARENT_PATH: '//host' does not have parent path")
+  endif()
+endif()
+
+
+check_errors ("PATH:HAS..." ${errors})

+ 44 - 0
Tests/RunCMake/GenEx-PATH/IS_ABSOLUTE.cmake.in

@@ -0,0 +1,44 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+if (WIN32)
+  set(path "c:/a")
+  set(output "$<PATH:IS_ABSOLUTE,c:/a>")
+else()
+  set(path "/a")
+  set(output "$<PATH:IS_ABSOLUTE,/a>")
+endif()
+if (NOT output)
+  list (APPEND errors "'${path}' is not absolute")
+endif()
+
+set(output "$<PATH:IS_ABSOLUTE,a/b>")
+if (output)
+  list (APPEND errors "'a/b' is absolute")
+endif()
+
+if (WIN32)
+  set(output "$<PATH:IS_ABSOLUTE,c:/a/b>")
+  if (NOT output)
+    list (APPEND errors "'c:/a/b' is not absolute")
+  endif()
+
+  set(output "$<PATH:IS_ABSOLUTE,//host/b>")
+  if (NOT output)
+    list (APPEND errors "'//host/b' is not absolute")
+  endif()
+
+  set(output "$<PATH:IS_ABSOLUTE,/a>")
+  if (output)
+    list (APPEND errors "'/a' is absolute")
+  endif()
+
+  set(output "$<PATH:IS_ABSOLUTE,c:a>")
+  if (output)
+    list (APPEND errors "'c:a' is absolute")
+  endif()
+endif()
+
+
+check_errors("PATH:IS_ABSOLUTE" ${errors})

+ 25 - 0
Tests/RunCMake/GenEx-PATH/IS_PREFIX.cmake.in

@@ -0,0 +1,25 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set(output "$<PATH:IS_PREFIX,a///b/c,a/b/c/d>")
+if (NOT output)
+  list (APPEND errors "'a///b/c' is not prefix of 'a/b/c/d'")
+endif()
+
+set(output "$<PATH:IS_PREFIX,a///b/c/../d,a/b/d/e>")
+if (output)
+  list (APPEND errors "'a///b/c/../d' is prefix of 'a/b/d/e'")
+endif()
+set(output "$<PATH:IS_PREFIX,NORMALIZE,a///b/c/../d,a/b/d/e>")
+if (NOT output)
+  list (APPEND errors "'a///b/c/../d' is not prefix of 'a/b/d/e'")
+endif()
+
+set(output "$<PATH:IS_PREFIX,NORMALIZE,/a/b/..,/a/c/../b")
+if (NOT output)
+  list (APPEND errors "'/a/b/..' is not prefix of '/a/c/../b'")
+endif()
+
+
+check_errors("PATH:IS_PREFIX" ${errors})

+ 45 - 0
Tests/RunCMake/GenEx-PATH/IS_RELATIVE.cmake.in

@@ -0,0 +1,45 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+
+if (WIN32)
+  set(path "c:/a")
+  set(output "$<PATH:IS_RELATIVE,c:/a>")
+else()
+  set(path "/a")
+  set(output "$<PATH:IS_RELATIVE,/a>")
+endif()
+if (output)
+  list (APPEND errors "'${path} is relative")
+endif()
+
+set(output "$<PATH:IS_RELATIVE,a/b>")
+if (NOT output)
+  list (APPEND errors "'a/b' is not relative")
+endif()
+
+if (WIN32)
+  set(output "$<PATH:IS_RELATIVE,c:/a/b>")
+  if (output)
+    list (APPEND errors "'c:/a/b' is relative")
+  endif()
+
+  set(output "$<PATH:IS_RELATIVE,//host/b>")
+  if (output)
+    list (APPEND errors "'//host/b' is relative")
+  endif()
+
+  set(output "$<PATH:IS_RELATIVE,/a>")
+  if (NOT output)
+    list (APPEND errors "'/a' is not relative")
+  endif()
+
+  set(output "$<PATH:IS_RELATIVE,c:a>")
+  if (NOT output)
+    list (APPEND errors "'c:a' is not relative")
+  endif()
+endif()
+
+
+check_errors("PATH:IS_RELATIVE" ${errors})

+ 43 - 0
Tests/RunCMake/GenEx-PATH/NORMAL_PATH.cmake.in

@@ -0,0 +1,43 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set (reference "a/./b/..")
+cmake_path(NORMAL_PATH reference)
+set(output "$<PATH:NORMAL_PATH,a/./b/..>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/.///b/../")
+cmake_path(NORMAL_PATH reference)
+set(output "$<PATH:NORMAL_PATH,a/.///b/../>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+if (WIN32)
+  set (reference "//host/./b/..")
+  cmake_path(NORMAL_PATH reference)
+  set(output "$<PATH:NORMAL_PATH,//host/./b/..>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+
+  set (reference "//host/./b/../")
+  cmake_path(NORMAL_PATH reference)
+  set(output "$<PATH:NORMAL_PATH,//host/./b/../>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+
+  set (reference "c://a/.///b/../")
+  cmake_path(NORMAL_PATH reference)
+  set(output "$<PATH:NORMAL_PATH,c://a/.///b/../>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+endif()
+
+
+check_errors("PATH:NORMAL_PATH" ${errors})

+ 64 - 0
Tests/RunCMake/GenEx-PATH/RELATIVE_PATH.cmake.in

@@ -0,0 +1,64 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set (reference "/a//d")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "/a/b/c")
+set(output "$<PATH:RELATIVE_PATH,/a//d,/a/b/c>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "/a//b///c")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "/a/d")
+set(output "$<PATH:RELATIVE_PATH,/a/b///c,/a/d>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "a")
+set(output "$<PATH:RELATIVE_PATH,a/b/c,a>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "a/b/c/x/y")
+set(output "$<PATH:RELATIVE_PATH,a/b/c,a/b/c/x/y>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "a/b/c")
+set(output "$<PATH:RELATIVE_PATH,a/b/c,a/b/c>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "c/d")
+set(output "$<PATH:RELATIVE_PATH,a/b,c/d>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+set (reference "/a/d")
+cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "e/d/c")
+set(output "$<PATH:RELATIVE_PATH,/a/d,e/d/c>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "'${output}' instead of '${reference}'")
+endif()
+
+if (WIN32)
+  set (reference "c:/a/d")
+  cmake_path(RELATIVE_PATH reference BASE_DIRECTORY "e/d/c")
+  set(output "$<PATH:RELATIVE_PATH,c:/a/d,e/d/c>")
+  if (NOT output STREQUAL reference)
+    list (APPEND errors "'${output}' instead of '${reference}'")
+  endif()
+endif()
+
+
+check_errors("PATH:RELATIVE_PATH" ${errors})

+ 65 - 0
Tests/RunCMake/GenEx-PATH/REMOVE_ITEM.cmake.in

@@ -0,0 +1,65 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set (reference "a/b/c.e.f")
+cmake_path (REMOVE_FILENAME reference)
+set(output "$<PATH:REMOVE_FILENAME,a/b/c.e.f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME: '${output}' instead of '${reference}'")
+endif()
+
+cmake_path (REMOVE_FILENAME reference)
+set(output "$<PATH:REMOVE_FILENAME,a/b/>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME: '${output}' instead of '${reference}'")
+endif()
+
+
+set (reference "a/b/c.e.f")
+cmake_path (REMOVE_EXTENSION reference)
+set(output "$<PATH:REMOVE_EXTENSION,a/b/c.e.f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c.e.f")
+cmake_path (REMOVE_EXTENSION reference LAST_ONLY)
+set(output "$<PATH:REMOVE_EXTENSION,LAST_ONLY,a/b/c.e.f>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+cmake_path (REMOVE_EXTENSION reference)
+set(output "$<PATH:REMOVE_EXTENSION,a/b/c.e>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c")
+cmake_path (REMOVE_EXTENSION reference)
+set(output "$<PATH:REMOVE_EXTENSION,a/b/c>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/.c")
+cmake_path (REMOVE_EXTENSION reference)
+set(output "$<PATH:REMOVE_EXTENSION,a/b/.c>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+cmake_path (REMOVE_EXTENSION reference LAST_ONLY)
+set(output "$<PATH:REMOVE_EXTENSION,LAST_ONLY,a/b/.c>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/.")
+cmake_path (REMOVE_EXTENSION reference LAST_ONLY)
+set(output "$<PATH:REMOVE_EXTENSION,LAST_ONLY,a/b/.>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+
+check_errors("PATH:REMOVE..." ${errors})

+ 73 - 0
Tests/RunCMake/GenEx-PATH/REPLACE_ITEM.cmake.in

@@ -0,0 +1,73 @@
+
+include ("${RunCMake_SOURCE_DIR}/check_errors.cmake")
+unset (errors)
+
+set (reference "a/b/c.e.f")
+cmake_path (REPLACE_FILENAME reference "x.y")
+set(output "$<PATH:REPLACE_FILENAME,a/b/c.e.f,x.y>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/")
+cmake_path (REPLACE_FILENAME reference "x.y")
+set(output "$<PATH:REPLACE_FILENAME,a/b/,x.y>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "FILENAME: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c.e.f")
+cmake_path (REPLACE_EXTENSION reference ".x")
+set(output "$<PATH:REPLACE_EXTENSION,a/b/c.e.f,.x>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+cmake_path (REPLACE_EXTENSION reference ".y")
+set(output "$<PATH:REPLACE_EXTENSION,a/b/c.x,.y>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+cmake_path (REPLACE_EXTENSION reference "")
+set(output "$<PATH:REPLACE_EXTENSION,a/b/c.y,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "a/b/c.e.f")
+cmake_path (REPLACE_EXTENSION reference ".x" LAST_ONLY)
+set(output "$<PATH:REPLACE_EXTENSION,LAST_ONLY,a/b/c.e.f,.x>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+cmake_path (REPLACE_EXTENSION reference ".y" LAST_ONLY)
+set(output "$<PATH:REPLACE_EXTENSION,LAST_ONLY,a/b/c.e.x,.y>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+cmake_path (REPLACE_EXTENSION reference "" LAST_ONLY)
+set(output "$<PATH:REPLACE_EXTENSION,LAST_ONLY,a/b/c.e.y,>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "/a/.b")
+cmake_path (REPLACE_EXTENSION reference ".x")
+set(output "$<PATH:REPLACE_EXTENSION,/a/.b,.x>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '/${reference}'")
+endif()
+cmake_path (REPLACE_EXTENSION reference ".x" LAST_ONLY)
+set(output "$<PATH:REPLACE_EXTENSION,LAST_ONLY,/a/.b.x,.x>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+set (reference "/a/b")
+cmake_path (REPLACE_EXTENSION reference ".x")
+set(output "$<PATH:REPLACE_EXTENSION,/a/b,.x>")
+if (NOT output STREQUAL reference)
+  list (APPEND errors "EXTENSION: '${output}' instead of '${reference}'")
+endif()
+
+
+check_errors("PATH:REPLACE..." ${errors})

+ 68 - 0
Tests/RunCMake/GenEx-PATH/RunCMakeTest.cmake

@@ -0,0 +1,68 @@
+
+include(RunCMake)
+
+run_cmake(no-arguments)
+run_cmake(bad-option)
+
+function(check_path_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 GET_ROOT_NAME GET_ROOT_DIRECTORY GET_ROOT_PATH GET_FILENAME
+                             GET_EXTENSION GET_STEM GET_RELATIVE_PART GET_PARENT_PATH
+                             HAS_ROOT_NAME HAS_ROOT_DIRECTORY HAS_ROOT_PATH HAS_FILENAME
+                             HAS_EXTENSION HAS_STEM HAS_RELATIVE_PART HAS_PARENT_PATH
+                             IS_ABSOLUTE IS_RELATIVE CMAKE_PATH REMOVE_FILENAME REMOVE_EXTENSION
+                             NORMAL_PATH)
+  check_path_syntax (${subcommand} unexpected-arg "-DPATH_ARGUMENTS=${subcommand},ARG1,ARG2")
+endforeach()
+foreach (subcommand IN ITEMS GET_EXTENSION GET_STEM REMOVE_EXTENSION)
+  if (subcommand STREQUAL "REMOVE_EXTENSION")
+    set(RunCMake-stderr-file "unexpected-arg2-stderr.txt")
+  endif()
+  check_path_syntax ("${subcommand}[LAST_ONLY]" unexpected-arg "-DPATH_ARGUMENTS=${subcommand},LAST_ONLY,ARG1,ARG2")
+  unset(RunCMake-stderr-file)
+endforeach()
+foreach (subcommand IN ITEMS CMAKE_PATH)
+  check_path_syntax ("${subcommand}[NORMALIZE]" unexpected-arg "-DPATH_ARGUMENTS=${subcommand},NORMALIZE,ARG1,ARG2")
+endforeach()
+
+### sub-commands with two arguments
+foreach (subcommand IN ITEMS IS_PREFIX REPLACE_FILENAME REPLACE_EXTENSION RELATIVE_PATH ABSOLUTE_PATH)
+  check_path_syntax (${subcommand} unexpected-arg "-DPATH_ARGUMENTS=${subcommand},ARG1,ARG2,ARG3")
+endforeach()
+foreach (subcommand IN ITEMS IS_PREFIX ABSOLUTE_PATH)
+  check_path_syntax ("${subcommand}[NORMALIZE]" unexpected-arg "-DPATH_ARGUMENTS=${subcommand},NORMALIZE,ARG1,ARG2,ARG3")
+endforeach()
+foreach (subcommand IN ITEMS REPLACE_EXTENSION)
+  set(RunCMake-stderr-file "unexpected-arg2-stderr.txt")
+  check_path_syntax ("${subcommand}[LAST_ONLY]" unexpected-arg "-DPATH_ARGUMENTS=${subcommand},LAST_ONLY,ARG1,ARG2,ARG3")
+  unset(RunCMake-stderr-file)
+endforeach()
+unset (RunCMake-stderr-file)
+
+
+function(check_path_execution name)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${name}-build)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  set(RunCMake_TEST_VARIANT_DESCRIPTION " - ${name}")
+  run_cmake_with_options(generate -DPATH_TEST=${name})
+  run_cmake_command(check "${CMAKE_COMMAND}" "-DRunCMake_SOURCE_DIR=${RunCMake_SOURCE_DIR}" -P "${RunCMake_TEST_BINARY_DIR}/${name}.cmake")
+endfunction()
+
+check_path_execution (GET_ITEM)
+check_path_execution (HAS_ITEM)
+check_path_execution (CMAKE_PATH)
+check_path_execution (APPEND)
+check_path_execution (REMOVE_ITEM)
+check_path_execution (REPLACE_ITEM)
+check_path_execution (NORMAL_PATH)
+check_path_execution (RELATIVE_PATH)
+check_path_execution (ABSOLUTE_PATH)
+check_path_execution (IS_RELATIVE)
+check_path_execution (IS_ABSOLUTE)
+check_path_execution (IS_PREFIX)

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

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

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

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

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

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

+ 13 - 0
Tests/RunCMake/GenEx-PATH/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()

+ 2 - 0
Tests/RunCMake/GenEx-PATH/generate.cmake

@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT "${PATH_TEST}.cmake" INPUT "${PATH_TEST}.cmake.in")

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

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

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

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

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

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

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

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

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

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

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

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

+ 9 - 0
Tests/RunCMake/GenEx-PATH/unexpected-arg2-stderr.txt

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