From 74c9d40876c955d5aca8824bfbdf78f79ca238a1 Mon Sep 17 00:00:00 2001 From: Brad King Date: Fri, 31 Jan 2025 11:51:01 -0500 Subject: 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 --- .gitattributes | 1 + Help/command/execute_process.rst | 12 ++++++++++-- Source/cmExecuteProcessCommand.cxx | 14 ++++++++++++-- Tests/RunCMake/execute_process/RunCMakeTest.cmake | 8 ++++++++ Tests/RunCMake/execute_process/WindowsBatch-stdout.txt | 8 ++++++++ Tests/RunCMake/execute_process/WindowsBatch.cmake | 8 ++++++++ Tests/RunCMake/execute_process/with space.bat | 1 + Tests/RunCMake/execute_process/with space.cmd | 1 + 8 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 Tests/RunCMake/execute_process/WindowsBatch-stdout.txt create mode 100644 Tests/RunCMake/execute_process/WindowsBatch.cmake create mode 100755 Tests/RunCMake/execute_process/with space.bat create mode 100755 Tests/RunCMake/execute_process/with space.cmd diff --git a/.gitattributes b/.gitattributes index 39ee3fb..8151ec8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,6 +25,7 @@ configure eol=lf *.bat eol=crlf *.bat.in eol=crlf +*.cmd eol=crlf *.sln eol=crlf *.vcproj eol=crlf diff --git a/Help/command/execute_process.rst b/Help/command/execute_process.rst index ed440b5..64ca24d 100644 --- a/Help/command/execute_process.rst +++ b/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 diff --git a/Source/cmExecuteProcessCommand.cxx b/Source/cmExecuteProcessCommand.cxx index 20a54b3..750c09d 100644 --- a/Source/cmExecuteProcessCommand.cxx +++ b/Source/cmExecuteProcessCommand.cxx @@ -120,7 +120,7 @@ bool cmExecuteProcessCommand(std::vector const& args, .Bind("COMMAND_ERROR_IS_FATAL"_s, &Arguments::CommandErrorIsFatal); std::vector 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 const& args, status.SetError(" called with no COMMAND argument."); return false; } - for (std::vector const& cmd : arguments.Commands) { + for (std::vector& 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. diff --git a/Tests/RunCMake/execute_process/RunCMakeTest.cmake b/Tests/RunCMake/execute_process/RunCMakeTest.cmake index e9b18a5..be2c652 100644 --- a/Tests/RunCMake/execute_process/RunCMakeTest.cmake +++ b/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() diff --git a/Tests/RunCMake/execute_process/WindowsBatch-stdout.txt b/Tests/RunCMake/execute_process/WindowsBatch-stdout.txt new file mode 100644 index 0000000..58ced7e --- /dev/null +++ b/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" -$ diff --git a/Tests/RunCMake/execute_process/WindowsBatch.cmake b/Tests/RunCMake/execute_process/WindowsBatch.cmake new file mode 100644 index 0000000..9ab9cb8 --- /dev/null +++ b/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") diff --git a/Tests/RunCMake/execute_process/with space.bat b/Tests/RunCMake/execute_process/with space.bat new file mode 100755 index 0000000..9712938 --- /dev/null +++ b/Tests/RunCMake/execute_process/with space.bat @@ -0,0 +1 @@ +@echo bat %1 - %2 - diff --git a/Tests/RunCMake/execute_process/with space.cmd b/Tests/RunCMake/execute_process/with space.cmd new file mode 100755 index 0000000..e3a64cf --- /dev/null +++ b/Tests/RunCMake/execute_process/with space.cmd @@ -0,0 +1 @@ +@echo cmd %1 - %2 - -- cgit v0.12