From 27dedeaafd8bf13dd7dfeea255b5a05950322c68 Mon Sep 17 00:00:00 2001 From: Tyler Yankee Date: Mon, 31 Aug 2026 10:56:08 -0400 Subject: [PATCH] cmake: Add CLI argument spell checking Use Levenshtein distance to track when the user may have made a typo in an argument to `cmake`, `cmake --build`, `cmake --install`, or `cmake --workflow`. --- Source/cmCommandLineArgument.h | 15 ++++++++++++ Source/cmake.cxx | 14 ++++++++++- Source/cmakemain.cxx | 24 ++++++++++++++++--- .../CMakePresetsWorkflow/RunCMakeTest.cmake | 1 + .../UnknownOptionDidYouMean-result.txt | 1 + .../UnknownOptionDidYouMean-stderr.txt | 3 +++ .../CommandLine/InvalidArg3-result.txt | 1 + .../CommandLine/InvalidArg3-stderr.txt | 2 ++ Tests/RunCMake/CommandLine/RunCMakeTest.cmake | 5 ++++ ...ld-unknown-command-did-you-mean-result.txt | 1 + ...ld-unknown-command-did-you-mean-stderr.txt | 2 ++ ...ll-unknown-command-did-you-mean-result.txt | 1 + ...ll-unknown-command-did-you-mean-stderr.txt | 2 ++ 13 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-result.txt create mode 100644 Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/InvalidArg3-result.txt create mode 100644 Tests/RunCMake/CommandLine/InvalidArg3-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-result.txt create mode 100644 Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-result.txt create mode 100644 Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-stderr.txt diff --git a/Source/cmCommandLineArgument.h b/Source/cmCommandLineArgument.h index 4c88f23491..aafad28133 100644 --- a/Source/cmCommandLineArgument.h +++ b/Source/cmCommandLineArgument.h @@ -291,3 +291,18 @@ private: !(arg.size() >= 2 && cmsysString_isdigit(arg[1])); } }; + +template +std::string cmFindClosestCommandLineArgument( + std::string const& arg, + std::vector> const& arguments) +{ + std::vector candidates; + candidates.reserve(arguments.size()); + for (auto const& option : arguments) { + if (!option.Name.empty()) { + candidates.push_back(option.Name); + } + } + return cmFindClosestString(arg, candidates); +} diff --git a/Source/cmake.cxx b/Source/cmake.cxx index 85f4ad8b0d..39ade85a53 100644 --- a/Source/cmake.cxx +++ b/Source/cmake.cxx @@ -200,6 +200,12 @@ std::string normalizeCliWarningName(cm::string_view cliName) return out; } +std::string findClosestOption(std::string const& arg, + std::vector const& arguments) +{ + return cmFindClosestCommandLineArgument(arg, arguments); +} + bool cmakeCheckStampFile(std::string const& stampName) { // The stamp file does not exist. Use the stamp dependencies to @@ -1548,7 +1554,13 @@ void cmake::SetArgs(std::vector const& args) } if (!possibleUnknownArg.empty() && this->State->GetRole() != cmState::Role::Script) { - cmSystemTools::Error(cmStrCat("Unknown argument ", possibleUnknownArg)); + std::string error = cmStrCat("Unknown argument ", possibleUnknownArg); + std::string const suggestion = + findClosestOption(possibleUnknownArg, arguments); + if (!suggestion.empty()) { + error = cmStrCat(error, ". Did you mean: ", suggestion, '?'); + } + cmSystemTools::Error(error); cmSystemTools::Error("Run 'cmake --help' for all supported options."); exit(1); } diff --git a/Source/cmakemain.cxx b/Source/cmakemain.cxx index dd863e3c45..6d9c74e15e 100644 --- a/Source/cmakemain.cxx +++ b/Source/cmakemain.cxx @@ -635,7 +635,13 @@ int do_build(int ac, char const* const* av) if (!(matched && parsed)) { buildArgs.binaryDir.clear(); if (!matched) { - std::cerr << "Unknown argument " << arg << std::endl; + std::string error = cmStrCat("Unknown argument ", arg); + std::string const suggestion = + cmFindClosestCommandLineArgument(arg, arguments); + if (!suggestion.empty()) { + error = cmStrCat(error, ". Did you mean: ", suggestion, '?'); + } + std::cerr << error << std::endl; } break; } @@ -933,7 +939,13 @@ int do_install(int ac, char const* const* av) if (!(matched && parsed)) { dir.clear(); if (!matched) { - std::cerr << "Unknown argument " << arg << std::endl; + std::string error = cmStrCat("Unknown argument ", arg); + std::string const suggestion = + cmFindClosestCommandLineArgument(arg, arguments); + if (!suggestion.empty()) { + error = cmStrCat(error, ". Did you mean: ", suggestion, '?'); + } + std::cerr << error << std::endl; } break; } @@ -1136,7 +1148,13 @@ int do_workflow(int ac, char const* const* av) if (!(matched && parsed)) { if (!matched) { presetsArgs.Clear(); - std::cerr << "Unknown argument " << arg << std::endl; + std::string error = cmStrCat("Unknown argument ", arg); + std::string const suggestion = + cmFindClosestCommandLineArgument(arg, arguments); + if (!suggestion.empty()) { + error = cmStrCat(error, ". Did you mean: ", suggestion, '?'); + } + std::cerr << error << std::endl; } break; } diff --git a/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake b/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake index 2676d556e9..d4f5f57ae8 100644 --- a/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake +++ b/Tests/RunCMake/CMakePresetsWorkflow/RunCMakeTest.cmake @@ -235,6 +235,7 @@ run_cmake_command(PresetsNoArgEq-workflow ${CMAKE_COMMAND} "--workflow" "--prese run_cmake_command(PresetsFileNoArg-workflow ${CMAKE_COMMAND} "--workflow" "--presets-file") run_cmake_workflow_presets(InvalidOption -DINVALID_OPTION) run_cmake_workflow_presets(ListPresetsInvalidValue --list-presets=invalid) +run_cmake_command(UnknownOptionDidYouMean ${CMAKE_COMMAND} "--workflow" "--fresh" "--presett") set(RunCMake_TEST_NO_CLEAN TRUE) file(REMOVE_RECURSE "${RunCMake_BINARY_DIR}/Fresh") diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-result.txt b/Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-stderr.txt b/Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-stderr.txt new file mode 100644 index 0000000000..f3422c9180 --- /dev/null +++ b/Tests/RunCMake/CMakePresetsWorkflow/UnknownOptionDidYouMean-stderr.txt @@ -0,0 +1,3 @@ +^Unknown argument --presett\. Did you mean: --preset\? +Usage: cmake --workflow +Options: diff --git a/Tests/RunCMake/CommandLine/InvalidArg3-result.txt b/Tests/RunCMake/CommandLine/InvalidArg3-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CommandLine/InvalidArg3-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/InvalidArg3-stderr.txt b/Tests/RunCMake/CommandLine/InvalidArg3-stderr.txt new file mode 100644 index 0000000000..1fd6aea47b --- /dev/null +++ b/Tests/RunCMake/CommandLine/InvalidArg3-stderr.txt @@ -0,0 +1,2 @@ +^CMake Error: Unknown argument --toolchan\. Did you mean: --toolchain\? +CMake Error: Run 'cmake --help' for all supported options\.$ diff --git a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake index d309f705fe..efacda3956 100644 --- a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake +++ b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake @@ -42,6 +42,7 @@ run_cmake_command(versionVJsonNoArg ${CMAKE_COMMAND} /V=json-v1) run_cmake_command(NoArgs ${CMAKE_COMMAND}) run_cmake_command(InvalidArg1 ${CMAKE_COMMAND} -invalid) run_cmake_command(InvalidArg2 ${CMAKE_COMMAND} --invalid) +run_cmake_command(InvalidArg3 ${CMAKE_COMMAND} --toolchan) run_cmake_command(Wizard ${CMAKE_COMMAND} -i) run_cmake_command(C-no-arg ${CMAKE_COMMAND} -B DummyBuildDir -C) run_cmake_command(C-no-arg2 ${CMAKE_COMMAND} -B DummyBuildDir -C -T) @@ -145,6 +146,8 @@ run_cmake_command(build-unknown-command-long ${CMAKE_COMMAND} --build ${RunCMake_SOURCE_DIR} --invalid-command) run_cmake_command(build-unknown-command-partial-match ${CMAKE_COMMAND} --build ${RunCMake_SOURCE_DIR} --targetinvalid) +run_cmake_command(build-unknown-command-did-you-mean + ${CMAKE_COMMAND} --build ${RunCMake_SOURCE_DIR} --targe) run_cmake_command(build-invalid-target-syntax ${CMAKE_COMMAND} --build ${RunCMake_SOURCE_DIR} --target=invalid) run_cmake_command(build-no-generator @@ -164,6 +167,8 @@ run_cmake_command(install-unknown-command-short ${CMAKE_COMMAND} --install ${RunCMake_SOURCE_DIR} -invalid-command) run_cmake_command(install-unknown-command-long ${CMAKE_COMMAND} --install ${RunCMake_SOURCE_DIR} --invalid-command) +run_cmake_command(install-unknown-command-did-you-mean + ${CMAKE_COMMAND} --install ${RunCMake_SOURCE_DIR} --compnent) run_cmake_command(install-options-to-vars ${CMAKE_COMMAND} --install ${RunCMake_SOURCE_DIR}/dir-install-options-to-vars --strip --prefix /var/test --config sample --component pack) diff --git a/Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-result.txt b/Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-stderr.txt b/Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-stderr.txt new file mode 100644 index 0000000000..3cddce6862 --- /dev/null +++ b/Tests/RunCMake/CommandLine/build-unknown-command-did-you-mean-stderr.txt @@ -0,0 +1,2 @@ +^Unknown argument --targe\. Did you mean: --target\? +Usage: cmake --build +\[options\] \[-- \[native-options\]\] diff --git a/Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-result.txt b/Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-stderr.txt b/Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-stderr.txt new file mode 100644 index 0000000000..8e1b11613e --- /dev/null +++ b/Tests/RunCMake/CommandLine/install-unknown-command-did-you-mean-stderr.txt @@ -0,0 +1,2 @@ +^Unknown argument --compnent\. Did you mean: --component\? +Usage: cmake --install \[options\]