summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarc Chevrier <marc.chevrier@gmail.com>2022-05-18 09:15:10 (GMT)
committerMarc Chevrier <marc.chevrier@gmail.com>2022-05-31 13:39:51 (GMT)
commitf11e66670ba9716f45cf757285a77b937dce420b (patch)
treec18b78f99b1bdcc7b36ebbab43727816a07803ed
parent9f4aec6d61f0c4f91cae8f35c5b514d456fba8ff (diff)
downloadCMake-f11e66670ba9716f45cf757285a77b937dce420b.zip
CMake-f11e66670ba9716f45cf757285a77b937dce420b.tar.gz
CMake-f11e66670ba9716f45cf757285a77b937dce420b.tar.bz2
Genex-PATH: path handling
Fixes: #23498
-rw-r--r--Help/command/cmake_path.rst12
-rw-r--r--Help/manual/cmake-generator-expressions.7.rst212
-rw-r--r--Help/release/dev/Genex-PATH.rst4
-rw-r--r--Source/cmGeneratorExpressionNode.cxx436
-rw-r--r--Tests/RunCMake/CMakeLists.txt1
-rw-r--r--Tests/RunCMake/GenEx-PATH/ABSOLUTE_PATH.cmake.in34
-rw-r--r--Tests/RunCMake/GenEx-PATH/APPEND.cmake.in68
-rw-r--r--Tests/RunCMake/GenEx-PATH/CMAKE_PATH.cmake.in53
-rw-r--r--Tests/RunCMake/GenEx-PATH/CMakeLists.txt5
-rw-r--r--Tests/RunCMake/GenEx-PATH/GET_ITEM.cmake.in311
-rw-r--r--Tests/RunCMake/GenEx-PATH/HAS_ITEM.cmake.in199
-rw-r--r--Tests/RunCMake/GenEx-PATH/IS_ABSOLUTE.cmake.in44
-rw-r--r--Tests/RunCMake/GenEx-PATH/IS_PREFIX.cmake.in25
-rw-r--r--Tests/RunCMake/GenEx-PATH/IS_RELATIVE.cmake.in45
-rw-r--r--Tests/RunCMake/GenEx-PATH/NORMAL_PATH.cmake.in43
-rw-r--r--Tests/RunCMake/GenEx-PATH/RELATIVE_PATH.cmake.in64
-rw-r--r--Tests/RunCMake/GenEx-PATH/REMOVE_ITEM.cmake.in65
-rw-r--r--Tests/RunCMake/GenEx-PATH/REPLACE_ITEM.cmake.in73
-rw-r--r--Tests/RunCMake/GenEx-PATH/RunCMakeTest.cmake68
-rw-r--r--Tests/RunCMake/GenEx-PATH/bad-option-result.txt1
-rw-r--r--Tests/RunCMake/GenEx-PATH/bad-option-stderr.txt8
-rw-r--r--Tests/RunCMake/GenEx-PATH/bad-option.cmake2
-rw-r--r--Tests/RunCMake/GenEx-PATH/check_errors.cmake13
-rw-r--r--Tests/RunCMake/GenEx-PATH/generate.cmake2
-rw-r--r--Tests/RunCMake/GenEx-PATH/no-arguments-result.txt1
-rw-r--r--Tests/RunCMake/GenEx-PATH/no-arguments-stderr.txt8
-rw-r--r--Tests/RunCMake/GenEx-PATH/no-arguments.cmake2
-rw-r--r--Tests/RunCMake/GenEx-PATH/unexpected-arg-result.txt1
-rw-r--r--Tests/RunCMake/GenEx-PATH/unexpected-arg-stderr.txt8
-rw-r--r--Tests/RunCMake/GenEx-PATH/unexpected-arg.cmake2
-rw-r--r--Tests/RunCMake/GenEx-PATH/unexpected-arg2-stderr.txt9
31 files changed, 1818 insertions, 1 deletions
diff --git a/Help/command/cmake_path.rst b/Help/command/cmake_path.rst
index 454c860..77525ed 100644
--- a/Help/command/cmake_path.rst
+++ b/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
^^^^^^^^^^
diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst
index 848a133..1f0dd59 100644
--- a/Help/manual/cmake-generator-expressions.7.rst
+++ b/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
----------------
diff --git a/Help/release/dev/Genex-PATH.rst b/Help/release/dev/Genex-PATH.rst
new file mode 100644
index 0000000..870ee23
--- /dev/null
+++ b/Help/release/dev/Genex-PATH.rst
@@ -0,0 +1,4 @@
+Genex-PATH
+----------
+
+* The :genex:`PATH` generator expression was added to manage paths.
diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx
index beb623f..52b771a 100644
--- a/Source/cmGeneratorExpressionNode.cxx
+++ b/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 },
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index 6f90bf7..a94cf21 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/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)
diff --git a/Tests/RunCMake/GenEx-PATH/ABSOLUTE_PATH.cmake.in b/Tests/RunCMake/GenEx-PATH/ABSOLUTE_PATH.cmake.in
new file mode 100644
index 0000000..cc5ff54
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/APPEND.cmake.in b/Tests/RunCMake/GenEx-PATH/APPEND.cmake.in
new file mode 100644
index 0000000..ab967a2
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/CMAKE_PATH.cmake.in b/Tests/RunCMake/GenEx-PATH/CMAKE_PATH.cmake.in
new file mode 100644
index 0000000..41205fa
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/CMakeLists.txt b/Tests/RunCMake/GenEx-PATH/CMakeLists.txt
new file mode 100644
index 0000000..f9748e9
--- /dev/null
+++ b/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)
diff --git a/Tests/RunCMake/GenEx-PATH/GET_ITEM.cmake.in b/Tests/RunCMake/GenEx-PATH/GET_ITEM.cmake.in
new file mode 100644
index 0000000..b58998c
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/HAS_ITEM.cmake.in b/Tests/RunCMake/GenEx-PATH/HAS_ITEM.cmake.in
new file mode 100644
index 0000000..fab9bd6
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/IS_ABSOLUTE.cmake.in b/Tests/RunCMake/GenEx-PATH/IS_ABSOLUTE.cmake.in
new file mode 100644
index 0000000..872dae4
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/IS_PREFIX.cmake.in b/Tests/RunCMake/GenEx-PATH/IS_PREFIX.cmake.in
new file mode 100644
index 0000000..98b7ff8
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/IS_RELATIVE.cmake.in b/Tests/RunCMake/GenEx-PATH/IS_RELATIVE.cmake.in
new file mode 100644
index 0000000..6d36581
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/NORMAL_PATH.cmake.in b/Tests/RunCMake/GenEx-PATH/NORMAL_PATH.cmake.in
new file mode 100644
index 0000000..e6cc4a3
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/RELATIVE_PATH.cmake.in b/Tests/RunCMake/GenEx-PATH/RELATIVE_PATH.cmake.in
new file mode 100644
index 0000000..11d73ad
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/REMOVE_ITEM.cmake.in b/Tests/RunCMake/GenEx-PATH/REMOVE_ITEM.cmake.in
new file mode 100644
index 0000000..cce4143
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/REPLACE_ITEM.cmake.in b/Tests/RunCMake/GenEx-PATH/REPLACE_ITEM.cmake.in
new file mode 100644
index 0000000..5bb04c3
--- /dev/null
+++ b/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})
diff --git a/Tests/RunCMake/GenEx-PATH/RunCMakeTest.cmake b/Tests/RunCMake/GenEx-PATH/RunCMakeTest.cmake
new file mode 100644
index 0000000..a93777a
--- /dev/null
+++ b/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)
diff --git a/Tests/RunCMake/GenEx-PATH/bad-option-result.txt b/Tests/RunCMake/GenEx-PATH/bad-option-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/bad-option-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/GenEx-PATH/bad-option-stderr.txt b/Tests/RunCMake/GenEx-PATH/bad-option-stderr.txt
new file mode 100644
index 0000000..0a9584d
--- /dev/null
+++ b/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\)
diff --git a/Tests/RunCMake/GenEx-PATH/bad-option.cmake b/Tests/RunCMake/GenEx-PATH/bad-option.cmake
new file mode 100644
index 0000000..c9bfd4a
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/bad-option.cmake
@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<PATH:BAD_OPTION,ARG>")
diff --git a/Tests/RunCMake/GenEx-PATH/check_errors.cmake b/Tests/RunCMake/GenEx-PATH/check_errors.cmake
new file mode 100644
index 0000000..7e60fc7
--- /dev/null
+++ b/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()
diff --git a/Tests/RunCMake/GenEx-PATH/generate.cmake b/Tests/RunCMake/GenEx-PATH/generate.cmake
new file mode 100644
index 0000000..4bd5f3b
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/generate.cmake
@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT "${PATH_TEST}.cmake" INPUT "${PATH_TEST}.cmake.in")
diff --git a/Tests/RunCMake/GenEx-PATH/no-arguments-result.txt b/Tests/RunCMake/GenEx-PATH/no-arguments-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/no-arguments-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/GenEx-PATH/no-arguments-stderr.txt b/Tests/RunCMake/GenEx-PATH/no-arguments-stderr.txt
new file mode 100644
index 0000000..d1e534f
--- /dev/null
+++ b/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\)
diff --git a/Tests/RunCMake/GenEx-PATH/no-arguments.cmake b/Tests/RunCMake/GenEx-PATH/no-arguments.cmake
new file mode 100644
index 0000000..5164339
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/no-arguments.cmake
@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<PATH:>")
diff --git a/Tests/RunCMake/GenEx-PATH/unexpected-arg-result.txt b/Tests/RunCMake/GenEx-PATH/unexpected-arg-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/unexpected-arg-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/GenEx-PATH/unexpected-arg-stderr.txt b/Tests/RunCMake/GenEx-PATH/unexpected-arg-stderr.txt
new file mode 100644
index 0000000..afc0026
--- /dev/null
+++ b/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\)
diff --git a/Tests/RunCMake/GenEx-PATH/unexpected-arg.cmake b/Tests/RunCMake/GenEx-PATH/unexpected-arg.cmake
new file mode 100644
index 0000000..4625f04
--- /dev/null
+++ b/Tests/RunCMake/GenEx-PATH/unexpected-arg.cmake
@@ -0,0 +1,2 @@
+
+file(GENERATE OUTPUT result.txt CONTENT "$<PATH:${PATH_ARGUMENTS}>")
diff --git a/Tests/RunCMake/GenEx-PATH/unexpected-arg2-stderr.txt b/Tests/RunCMake/GenEx-PATH/unexpected-arg2-stderr.txt
new file mode 100644
index 0000000..a38a795
--- /dev/null
+++ b/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\)