From e4b793c614f2a499d4d235ec2267ad6bff56a697 Mon Sep 17 00:00:00 2001
From: Marc Chevrier <marc.chevrier@gmail.com>
Date: Tue, 20 Apr 2021 18:46:26 +0200
Subject: file(REAL_PATH): add option EXPAND_TILDE

This option enables the replacement of any leading tilde with the path
to the user's home directory.
---
 Help/command/cmake_path.rst                      |  3 +-
 Help/command/file.rst                            | 20 +++++++++---
 Help/release/dev/file-REAL_PATH-EXPAND_TILDE.rst |  5 +++
 Source/cmFileCommand.cxx                         | 23 ++++++++++++--
 Tests/RunCMake/file/REAL_PATH.cmake              | 40 ++++++++++++++++++------
 Tests/RunCMake/file/RunCMakeTest.cmake           |  7 +++--
 6 files changed, 77 insertions(+), 21 deletions(-)
 create mode 100644 Help/release/dev/file-REAL_PATH-EXPAND_TILDE.rst

diff --git a/Help/command/cmake_path.rst b/Help/command/cmake_path.rst
index a8999f3..193180d 100644
--- a/Help/command/cmake_path.rst
+++ b/Help/command/cmake_path.rst
@@ -687,7 +687,8 @@ When the ``NORMALIZE`` option is specified, the path is :ref:`normalized
 <Normalization>` after the path computation.
 
 Because ``cmake_path()`` does not access the filesystem, symbolic links are
-not resolved.  To compute a real path with symbolic links resolved, use the
+not resolved and any leading tilde is not expanded.  To compute a real path
+with symbolic links resolved and leading tildes expanded, use the
 :command:`file(REAL_PATH)` command instead.
 
 Native Conversion
diff --git a/Help/command/file.rst b/Help/command/file.rst
index 62642cf..e170526 100644
--- a/Help/command/file.rst
+++ b/Help/command/file.rst
@@ -50,7 +50,7 @@ Synopsis
     file(`CHMOD_RECURSE`_ <files>... <directories>... PERMISSIONS <permissions>... [...])
 
   `Path Conversion`_
-    file(`REAL_PATH`_ <path> <out-var> [BASE_DIRECTORY <dir>])
+    file(`REAL_PATH`_ <path> <out-var> [BASE_DIRECTORY <dir>] [EXPAND_TILDE])
     file(`RELATIVE_PATH`_ <out-var> <directory> <file>)
     file({`TO_CMAKE_PATH`_ | `TO_NATIVE_PATH`_} <path> <out-var>)
 
@@ -924,16 +924,26 @@ Path Conversion
 
 .. code-block:: cmake
 
-  file(REAL_PATH <path> <out-var> [BASE_DIRECTORY <dir>])
+  file(REAL_PATH <path> <out-var> [BASE_DIRECTORY <dir>] [EXPAND_TILDE])
 
 .. versionadded:: 3.19
 
 Compute the absolute path to an existing file or directory with symlinks
 resolved.
 
-If the provided ``<path>`` is a relative path, it is evaluated relative to the
-given base directory ``<dir>``. If no base directory is provided, the default
-base directory will be :variable:`CMAKE_CURRENT_SOURCE_DIR`.
+``BASE_DIRECTORY <dir>``
+  If the provided ``<path>`` is a relative path, it is evaluated relative to the
+  given base directory ``<dir>``. If no base directory is provided, the default
+  base directory will be :variable:`CMAKE_CURRENT_SOURCE_DIR`.
+
+``EXPAND_TILDE``
+  .. versionadded:: 3.21
+
+  If the ``<path>`` is ``~`` or starts with ``~/``, the ``~`` is replaced by
+  the user's home directory.  The path to the home directory is obtained from
+  environment variables.  On Windows, the ``USERPROFILE`` environment variable
+  is used, falling back to the ``HOME`` environment variable if ``USERPROFILE``
+  is not defined.  On all other platforms, only ``HOME`` is used.
 
 .. _RELATIVE_PATH:
 
diff --git a/Help/release/dev/file-REAL_PATH-EXPAND_TILDE.rst b/Help/release/dev/file-REAL_PATH-EXPAND_TILDE.rst
new file mode 100644
index 0000000..cdf1efa
--- /dev/null
+++ b/Help/release/dev/file-REAL_PATH-EXPAND_TILDE.rst
@@ -0,0 +1,5 @@
+file-REAL_PATH-EXPAND_TILDE
+---------------------------
+
+* The :command:`file(REAL_PATH)` command gained the option ``EXPAND_TILDE`` to
+  replace any leading tilde with the path to the user's home directory.
diff --git a/Source/cmFileCommand.cxx b/Source/cmFileCommand.cxx
index 05ebcca..a943258 100644
--- a/Source/cmFileCommand.cxx
+++ b/Source/cmFileCommand.cxx
@@ -1246,9 +1246,12 @@ bool HandleRealPathCommand(std::vector<std::string> const& args,
   struct Arguments
   {
     std::string BaseDirectory;
+    bool ExpandTilde = false;
   };
-  static auto const parser = cmArgumentParser<Arguments>{}.Bind(
-    "BASE_DIRECTORY"_s, &Arguments::BaseDirectory);
+  static auto const parser =
+    cmArgumentParser<Arguments>{}
+      .Bind("BASE_DIRECTORY"_s, &Arguments::BaseDirectory)
+      .Bind("EXPAND_TILDE"_s, &Arguments::ExpandTilde);
 
   std::vector<std::string> unparsedArguments;
   std::vector<std::string> keywordsMissingValue;
@@ -1270,7 +1273,21 @@ bool HandleRealPathCommand(std::vector<std::string> const& args,
     arguments.BaseDirectory = status.GetMakefile().GetCurrentSourceDirectory();
   }
 
-  cmCMakePath path(args[1]);
+  auto input = args[1];
+  if (arguments.ExpandTilde && !input.empty()) {
+    if (input[0] == '~' && (input.length() == 1 || input[1] == '/')) {
+      std::string home;
+      if (
+#if defined(_WIN32) && !defined(__CYGWIN__)
+        cmSystemTools::GetEnv("USERPROFILE", home) ||
+#endif
+        cmSystemTools::GetEnv("HOME", home)) {
+        input.replace(0, 1, home);
+      }
+    }
+  }
+
+  cmCMakePath path(input, cmCMakePath::auto_format);
   path = path.Absolute(arguments.BaseDirectory).Normal();
   auto realPath = cmSystemTools::GetRealPath(path.GenericString());
 
diff --git a/Tests/RunCMake/file/REAL_PATH.cmake b/Tests/RunCMake/file/REAL_PATH.cmake
index be25706..0b5d3c0 100644
--- a/Tests/RunCMake/file/REAL_PATH.cmake
+++ b/Tests/RunCMake/file/REAL_PATH.cmake
@@ -1,14 +1,36 @@
 
-file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/test.txt")
-file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/test.sym")
-file(CREATE_LINK  "test.txt" "${CMAKE_CURRENT_BINARY_DIR}/test.sym" SYMBOLIC)
+if (NOT WIN32 OR CYGWIN)
+  file(TOUCH "${CMAKE_CURRENT_BINARY_DIR}/test.txt")
+  file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/test.sym")
+  file(CREATE_LINK  "test.txt" "${CMAKE_CURRENT_BINARY_DIR}/test.sym" SYMBOLIC)
 
-file(REAL_PATH "${CMAKE_CURRENT_BINARY_DIR}/test.sym" real_path)
-if (NOT real_path STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/test.txt")
-  message(SEND_ERROR "real path is \"${real_path}\", should be \"${CMAKE_CURRENT_BINARY_DIR}/test.txt\"")
+  file(REAL_PATH "${CMAKE_CURRENT_BINARY_DIR}/test.sym" real_path)
+  if (NOT real_path STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/test.txt")
+    message(SEND_ERROR "real path is \"${real_path}\", should be \"${CMAKE_CURRENT_BINARY_DIR}/test.txt\"")
+  endif()
+
+  file(REAL_PATH "test.sym" real_path BASE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
+  if (NOT real_path STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/test.txt")
+    message(SEND_ERROR "real path is \"${real_path}\", should be \"${CMAKE_CURRENT_BINARY_DIR}/test.txt\"")
+  endif()
+endif()
+
+
+If (WIN32)
+  cmake_path(SET HOME_DIR "$ENV{USERPROFILE}")
+  if (NOT HOME_DIR)
+    cmake_path(SET HOME_DIR "$ENV{HOME}")
+  endif()
+else()
+  set(HOME_DIR "$ENV{HOME}")
+endif()
+
+file(REAL_PATH "~" real_path EXPAND_TILDE)
+if (NOT real_path STREQUAL "${HOME_DIR}")
+  message(SEND_ERROR "real path is \"${real_path}\", should be \"${HOME_DIR}\"")
 endif()
 
-file(REAL_PATH "test.sym" real_path BASE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
-if (NOT real_path STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/test.txt")
-  message(SEND_ERROR "real path is \"${real_path}\", should be \"${CMAKE_CURRENT_BINARY_DIR}/test.txt\"")
+file(REAL_PATH "~/test.txt" real_path EXPAND_TILDE)
+if (NOT real_path STREQUAL "${HOME_DIR}/test.txt")
+  message(SEND_ERROR "real path is \"${real_path}\", should be \"${HOME_DIR}/test.txt\"")
 endif()
diff --git a/Tests/RunCMake/file/RunCMakeTest.cmake b/Tests/RunCMake/file/RunCMakeTest.cmake
index b4ea9ba..6d1109a 100644
--- a/Tests/RunCMake/file/RunCMakeTest.cmake
+++ b/Tests/RunCMake/file/RunCMakeTest.cmake
@@ -96,11 +96,12 @@ if(NOT WIN32 OR CYGWIN)
   run_cmake(READ_SYMLINK-noexist)
   run_cmake(READ_SYMLINK-notsymlink)
   run_cmake(INSTALL-FOLLOW_SYMLINK_CHAIN)
-  run_cmake(REAL_PATH-unexpected-arg)
-  run_cmake(REAL_PATH-no-base-dir)
-  run_cmake(REAL_PATH)
 endif()
 
+run_cmake(REAL_PATH-unexpected-arg)
+run_cmake(REAL_PATH-no-base-dir)
+run_cmake(REAL_PATH)
+
 if(RunCMake_GENERATOR MATCHES "Ninja")
   # Detect ninja version so we know what tests can be supported.
   execute_process(
-- 
cgit v0.12