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`.
This commit is contained in:
Tyler Yankee
2026-09-11 08:23:30 -04:00
parent fca1ecb8a3
commit 27dedeaafd
13 changed files with 68 additions and 4 deletions
+15
View File
@@ -291,3 +291,18 @@ private:
!(arg.size() >= 2 && cmsysString_isdigit(arg[1]));
}
};
template <typename FunctionSignature>
std::string cmFindClosestCommandLineArgument(
std::string const& arg,
std::vector<cmCommandLineArgument<FunctionSignature>> const& arguments)
{
std::vector<std::string> candidates;
candidates.reserve(arguments.size());
for (auto const& option : arguments) {
if (!option.Name.empty()) {
candidates.push_back(option.Name);
}
}
return cmFindClosestString(arg, candidates);
}
+13 -1
View File
@@ -200,6 +200,12 @@ std::string normalizeCliWarningName(cm::string_view cliName)
return out;
}
std::string findClosestOption(std::string const& arg,
std::vector<CommandArgument> 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<std::string> 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);
}
+21 -3
View File
@@ -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;
}
@@ -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")
@@ -0,0 +1,3 @@
^Unknown argument --presett\. Did you mean: --preset\?
Usage: cmake --workflow <options>
Options:
@@ -0,0 +1 @@
1
@@ -0,0 +1,2 @@
^CMake Error: Unknown argument --toolchan\. Did you mean: --toolchain\?
CMake Error: Run 'cmake --help' for all supported options\.$
@@ -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)
@@ -0,0 +1,2 @@
^Unknown argument --targe\. Did you mean: --target\?
Usage: cmake --build <dir> +\[options\] \[-- \[native-options\]\]
@@ -0,0 +1,2 @@
^Unknown argument --compnent\. Did you mean: --component\?
Usage: cmake --install <dir> \[options\]