Browse Source

execute_process: Fix invocation of .cmd/.bat with spaces

The `CreateProcessW` documentation states "to run a batch file, you must
start the command interpreter".  Use `cmd /c the.bat` to run batch files.
Also, use a "short path" to the `.bat` file if needed to avoid spaces.

Previously this worked in some cases only due to undocumented behavior
of `CreateProcessW` when given a `.bat` file.

Fixes: #26655
Brad King 9 months ago
parent
commit
74c9d40876

+ 1 - 0
.gitattributes

@@ -25,6 +25,7 @@ configure        eol=lf
 
 *.bat            eol=crlf
 *.bat.in         eol=crlf
+*.cmd            eol=crlf
 *.sln            eol=crlf
 *.vcproj         eol=crlf
 

+ 10 - 2
Help/command/execute_process.rst

@@ -48,8 +48,14 @@ Options:
    child process in an ``argv[]`` style array.
 
  * On Windows platforms, the command line is encoded as a string such
-   that child processes using ``CommandLineToArgvW`` will decode the
-   original arguments.
+   that child processes using `CommandLineToArgvW`_ will decode the
+   original arguments.  If the command runs a ``.bat`` or ``.cmd``
+   script, it may receive arguments with extra quoting.
+
+ * .. versionchanged:: 4.0
+     On Windows platforms, if the command runs a ``.bat`` or ``.cmd`` script,
+     it is automatically executed through the command interpreter, ``cmd /c``.
+     However, paths with spaces may fail if a "short path" is not available.
 
  No intermediate shell is used, so shell operators such as ``>``
  are treated as normal arguments.
@@ -197,3 +203,5 @@ Options:
     is checked.  If the variable is not set, the default is ``NONE``.
     If ``RESULT_VARIABLE`` or ``RESULTS_VARIABLE`` is supplied,
     :variable:`CMAKE_EXECUTE_PROCESS_COMMAND_ERROR_IS_FATAL` is ignored.
+
+.. _`CommandLineToArgvW`: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw

+ 12 - 2
Source/cmExecuteProcessCommand.cxx

@@ -120,7 +120,7 @@ bool cmExecuteProcessCommand(std::vector<std::string> const& args,
       .Bind("COMMAND_ERROR_IS_FATAL"_s, &Arguments::CommandErrorIsFatal);
 
   std::vector<std::string> unparsedArguments;
-  Arguments const arguments = parser.Parse(args, &unparsedArguments);
+  Arguments arguments = parser.Parse(args, &unparsedArguments);
 
   if (arguments.MaybeReportError(status.GetMakefile())) {
     return true;
@@ -161,11 +161,21 @@ bool cmExecuteProcessCommand(std::vector<std::string> const& args,
     status.SetError(" called with no COMMAND argument.");
     return false;
   }
-  for (std::vector<std::string> const& cmd : arguments.Commands) {
+  for (std::vector<std::string>& cmd : arguments.Commands) {
     if (cmd.empty()) {
       status.SetError(" given COMMAND argument with no value.");
       return false;
     }
+#ifdef _WIN32
+    cmsys::Status shortPathRes = cmSystemTools::MaybePrependCmdExe(cmd);
+    if (!shortPathRes) {
+      status.GetMakefile().IssueMessage(
+        MessageType::WARNING,
+        cmStrCat("Conversion of COMMAND:\n  ", cmd[2], '\n',
+                 "to a short path without spaces failed:\n  ",
+                 shortPathRes.GetString()));
+    }
+#endif
   }
 
   // Parse the timeout string.

+ 8 - 0
Tests/RunCMake/execute_process/RunCMakeTest.cmake

@@ -63,6 +63,14 @@ if(WIN32 OR CYGWIN)
   run_cmake_command(WindowsNoExtension-build ${CMAKE_COMMAND} --build . --config Debug --target RunScript)
 endif()
 
+if(CMAKE_HOST_WIN32
+    # By default, only C: has short paths enabled.
+    # Since querying with `fsutil 8dot3name query C:`
+    # requires admin, just test the drive letter.
+    AND RunCMake_SOURCE_DIR MATCHES "^[Cc]:")
+  run_cmake_command(WindowsBatch ${CMAKE_COMMAND} -P ${RunCMake_SOURCE_DIR}/WindowsBatch.cmake)
+endif()
+
 if(TEST_STARTUPINFO_EXE)
   run_cmake_script(StartupInfo -DTEST_STARTUPINFO_EXE=${TEST_STARTUPINFO_EXE})
 endif()

+ 8 - 0
Tests/RunCMake/execute_process/WindowsBatch-stdout.txt

@@ -0,0 +1,8 @@
+^bat arg1 - arg2 -
+bat "arg1 space" - arg2 -
+bat arg1 - "arg2 space" -
+bat "arg1 space" - "arg2 space" -
+cmd arg1 - arg2 -
+cmd "arg1 space" - arg2 -
+cmd arg1 - "arg2 space" -
+cmd "arg1 space" - "arg2 space" -$

+ 8 - 0
Tests/RunCMake/execute_process/WindowsBatch.cmake

@@ -0,0 +1,8 @@
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.bat" arg1 arg2)
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.Bat" "arg1 space" arg2)
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.bAT" arg1 "arg2 space")
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.BAT" "arg1 space" "arg2 space")
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.cmd" arg1 arg2)
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.Cmd" "arg1 space" arg2)
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.cMD" arg1 "arg2 space")
+execute_process(COMMAND "${CMAKE_CURRENT_LIST_DIR}/with space.CMD" "arg1 space" "arg2 space")

+ 1 - 0
Tests/RunCMake/execute_process/with space.bat

@@ -0,0 +1 @@
+@echo bat %1 - %2 -

+ 1 - 0
Tests/RunCMake/execute_process/with space.cmd

@@ -0,0 +1 @@
+@echo cmd %1 - %2 -