diff options
-rw-r--r-- | Source/CMakeLists.txt | 2 | ||||
-rw-r--r-- | Source/cmArgumentParser.cxx | 93 | ||||
-rw-r--r-- | Source/cmArgumentParser.h | 143 | ||||
-rw-r--r-- | Tests/CMakeLib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Tests/CMakeLib/testArgumentParser.cxx | 148 | ||||
-rw-r--r-- | Utilities/IWYU/mapping.imp | 1 | ||||
-rwxr-xr-x | bootstrap | 1 |
7 files changed, 389 insertions, 0 deletions
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 52f6742eb7..a2836d64e7 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -143,6 +143,8 @@ set(SRCS cmAffinity.cxx cmAffinity.h cmArchiveWrite.cxx + cmArgumentParser.cxx + cmArgumentParser.h cmBase32.cxx cmCacheManager.cxx cmCacheManager.h diff --git a/Source/cmArgumentParser.cxx b/Source/cmArgumentParser.cxx new file mode 100644 index 0000000000..9a9932c569 --- /dev/null +++ b/Source/cmArgumentParser.cxx @@ -0,0 +1,93 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmArgumentParser.h" + +#include <algorithm> +#include <type_traits> + +namespace ArgumentParser { + +auto ActionMap::Emplace(cm::string_view name, Action action) + -> std::pair<iterator, bool> +{ + auto const it = + std::lower_bound(this->begin(), this->end(), name, + [](value_type const& elem, cm::string_view const& k) { + return elem.first < k; + }); + return (it != this->end() && it->first == name) + ? std::make_pair(it, false) + : std::make_pair(this->emplace(it, name, std::move(action)), true); +} + +auto ActionMap::Find(cm::string_view name) const -> const_iterator +{ + auto const it = + std::lower_bound(this->begin(), this->end(), name, + [](value_type const& elem, cm::string_view const& k) { + return elem.first < k; + }); + return (it != this->end() && it->first == name) ? it : this->end(); +} + +void Instance::Bind(bool& val) +{ + val = true; + this->CurrentString = nullptr; + this->CurrentList = nullptr; + this->ExpectValue = false; +} + +void Instance::Bind(std::string& val) +{ + this->CurrentString = &val; + this->CurrentList = nullptr; + this->ExpectValue = true; +} + +void Instance::Bind(StringList& val) +{ + this->CurrentString = nullptr; + this->CurrentList = &val; + this->ExpectValue = true; +} + +void Instance::Bind(MultiStringList& val) +{ + this->CurrentString = nullptr; + this->CurrentList = (val.emplace_back(), &val.back()); + this->ExpectValue = false; +} + +void Instance::Consume(cm::string_view arg, void* result, + std::vector<std::string>* unparsedArguments, + std::vector<std::string>* keywordsMissingValue) +{ + auto const it = this->Bindings.Find(arg); + if (it != this->Bindings.end()) { + it->second(*this, result); + if (this->ExpectValue && keywordsMissingValue != nullptr) { + keywordsMissingValue->emplace_back(arg); + } + return; + } + + if (this->CurrentString != nullptr) { + this->CurrentString->assign(std::string(arg)); + this->CurrentString = nullptr; + this->CurrentList = nullptr; + } else if (this->CurrentList != nullptr) { + this->CurrentList->emplace_back(arg); + } else if (unparsedArguments != nullptr) { + unparsedArguments->emplace_back(arg); + } + + if (this->ExpectValue) { + if (keywordsMissingValue != nullptr) { + keywordsMissingValue->pop_back(); + } + this->ExpectValue = false; + } +} + +} // namespace ArgumentParser diff --git a/Source/cmArgumentParser.h b/Source/cmArgumentParser.h new file mode 100644 index 0000000000..6cfe946336 --- /dev/null +++ b/Source/cmArgumentParser.h @@ -0,0 +1,143 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#ifndef cmArgumentParser_h +#define cmArgumentParser_h + +#include "cmConfigure.h" // IWYU pragma: keep + +#include "cm_static_string_view.hxx" +#include "cm_string_view.hxx" + +#include <cassert> +#include <functional> +#include <string> +#include <utility> +#include <vector> + +namespace ArgumentParser { + +using StringList = std::vector<std::string>; +using MultiStringList = std::vector<StringList>; + +class Instance; +using Action = std::function<void(Instance&, void*)>; + +// using ActionMap = cm::flat_map<cm::string_view, Action>; +class ActionMap : public std::vector<std::pair<cm::string_view, Action>> +{ +public: + std::pair<iterator, bool> Emplace(cm::string_view name, Action action); + const_iterator Find(cm::string_view name) const; +}; + +class Instance +{ +public: + Instance(ActionMap const& bindings) + : Bindings(bindings) + { + } + + void Bind(bool& val); + void Bind(std::string& val); + void Bind(StringList& val); + void Bind(MultiStringList& val); + + void Consume(cm::string_view arg, void* result, + std::vector<std::string>* unparsedArguments, + std::vector<std::string>* keywordsMissingValue); + +private: + ActionMap const& Bindings; + std::string* CurrentString = nullptr; + StringList* CurrentList = nullptr; + bool ExpectValue = false; +}; + +} // namespace ArgumentParser + +template <typename Result> +class cmArgumentParser +{ +public: + // I *think* this function could be made `constexpr` when the code is + // compiled as C++20. This would allow building a parser at compile time. + template <typename T> + cmArgumentParser& Bind(cm::static_string_view name, T Result::*member) + { + bool const inserted = + this->Bindings + .Emplace(name, + [member](ArgumentParser::Instance& instance, void* result) { + instance.Bind(static_cast<Result*>(result)->*member); + }) + .second; + assert(inserted), (void)inserted; + return *this; + } + + template <typename Range> + void Parse(Result& result, Range const& args, + std::vector<std::string>* unparsedArguments = nullptr, + std::vector<std::string>* keywordsMissingValue = nullptr) const + { + ArgumentParser::Instance instance(this->Bindings); + for (cm::string_view arg : args) { + instance.Consume(arg, &result, unparsedArguments, keywordsMissingValue); + } + } + + template <typename Range> + Result Parse(Range const& args, + std::vector<std::string>* unparsedArguments = nullptr, + std::vector<std::string>* keywordsMissingValue = nullptr) const + { + Result result; + this->Parse(result, args, unparsedArguments, keywordsMissingValue); + return result; + } + +private: + ArgumentParser::ActionMap Bindings; +}; + +template <> +class cmArgumentParser<void> +{ +public: + template <typename T> + cmArgumentParser& Bind(cm::static_string_view name, T& ref) + { + bool const inserted = this->Bind(cm::string_view(name), ref); + assert(inserted), (void)inserted; + return *this; + } + + template <typename Range> + void Parse(Range const& args, + std::vector<std::string>* unparsedArguments = nullptr, + std::vector<std::string>* keywordsMissingValue = nullptr) const + { + ArgumentParser::Instance instance(this->Bindings); + for (cm::string_view arg : args) { + instance.Consume(arg, nullptr, unparsedArguments, keywordsMissingValue); + } + } + +protected: + template <typename T> + bool Bind(cm::string_view name, T& ref) + { + return this->Bindings + .Emplace(name, + [&ref](ArgumentParser::Instance& instance, void*) { + instance.Bind(ref); + }) + .second; + } + +private: + ArgumentParser::ActionMap Bindings; +}; + +#endif diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt index 91f7e2500b..031ab012df 100644 --- a/Tests/CMakeLib/CMakeLists.txt +++ b/Tests/CMakeLib/CMakeLists.txt @@ -5,6 +5,7 @@ include_directories( ) set(CMakeLib_TESTS + testArgumentParser.cxx testGeneratedFileStream.cxx testRST.cxx testRange.cxx diff --git a/Tests/CMakeLib/testArgumentParser.cxx b/Tests/CMakeLib/testArgumentParser.cxx new file mode 100644 index 0000000000..788fecee65 --- /dev/null +++ b/Tests/CMakeLib/testArgumentParser.cxx @@ -0,0 +1,148 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmArgumentParser.h" + +#include "cm_static_string_view.hxx" +#include "cm_string_view.hxx" + +#include <initializer_list> +#include <iostream> +#include <string> +#include <vector> + +namespace { + +struct Result +{ + bool Option1 = false; + bool Option2 = false; + + std::string String1; + std::string String2; + + std::vector<std::string> List1; + std::vector<std::string> List2; + std::vector<std::string> List3; + + std::vector<std::vector<std::string>> Multi1; + std::vector<std::vector<std::string>> Multi2; + std::vector<std::vector<std::string>> Multi3; +}; + +std::initializer_list<cm::string_view> const args = { + /* clang-format off */ + "OPTION_1", // option + "STRING_1", // string arg missing value + "STRING_2", "foo", "bar", // string arg + unparsed value + "LIST_1", // list arg missing values + "LIST_2", "foo", "bar", // list arg with 2 elems + "LIST_3", "bar", // list arg ... + "LIST_3", "foo", // ... with continuation + "MULTI_2", // multi list with 0 lists + "MULTI_3", "foo", "bar", // multi list with first list with two elems + "MULTI_3", "bar", "foo", // multi list with second list with two elems + /* clang-format on */ +}; + +bool verifyResult(Result const& result, + std::vector<std::string> const& unparsedArguments, + std::vector<std::string> const& keywordsMissingValue) +{ + static std::vector<std::string> const foobar = { "foo", "bar" }; + static std::vector<std::string> const barfoo = { "bar", "foo" }; + static std::vector<std::string> const missing = { "STRING_1", "LIST_1" }; + +#define ASSERT_TRUE(x) \ + do { \ + if (!(x)) { \ + std::cout << "ASSERT_TRUE(" #x ") failed on line " << __LINE__ << "\n"; \ + return false; \ + } \ + } while (false) + + ASSERT_TRUE(result.Option1); + ASSERT_TRUE(!result.Option2); + + ASSERT_TRUE(result.String1.empty()); + ASSERT_TRUE(result.String2 == "foo"); + + ASSERT_TRUE(result.List1.empty()); + ASSERT_TRUE(result.List2 == foobar); + ASSERT_TRUE(result.List3 == barfoo); + + ASSERT_TRUE(result.Multi1.empty()); + ASSERT_TRUE(result.Multi2.size() == 1); + ASSERT_TRUE(result.Multi2[0].empty()); + ASSERT_TRUE(result.Multi3.size() == 2); + ASSERT_TRUE(result.Multi3[0] == foobar); + ASSERT_TRUE(result.Multi3[1] == barfoo); + + ASSERT_TRUE(unparsedArguments.size() == 1); + ASSERT_TRUE(unparsedArguments[0] == "bar"); + ASSERT_TRUE(keywordsMissingValue == missing); + + return true; +} + +bool testArgumentParserDynamic() +{ + Result result; + std::vector<std::string> unparsedArguments; + std::vector<std::string> keywordsMissingValue; + + cmArgumentParser<void>{} + .Bind("OPTION_1"_s, result.Option1) + .Bind("OPTION_2"_s, result.Option2) + .Bind("STRING_1"_s, result.String1) + .Bind("STRING_2"_s, result.String2) + .Bind("LIST_1"_s, result.List1) + .Bind("LIST_2"_s, result.List2) + .Bind("LIST_3"_s, result.List3) + .Bind("MULTI_1"_s, result.Multi1) + .Bind("MULTI_2"_s, result.Multi2) + .Bind("MULTI_3"_s, result.Multi3) + .Parse(args, &unparsedArguments, &keywordsMissingValue); + + return verifyResult(result, unparsedArguments, keywordsMissingValue); +} + +bool testArgumentParserStatic() +{ + static auto const parser = // + cmArgumentParser<Result>{} + .Bind("OPTION_1"_s, &Result::Option1) + .Bind("OPTION_2"_s, &Result::Option2) + .Bind("STRING_1"_s, &Result::String1) + .Bind("STRING_2"_s, &Result::String2) + .Bind("LIST_1"_s, &Result::List1) + .Bind("LIST_2"_s, &Result::List2) + .Bind("LIST_3"_s, &Result::List3) + .Bind("MULTI_1"_s, &Result::Multi1) + .Bind("MULTI_2"_s, &Result::Multi2) + .Bind("MULTI_3"_s, &Result::Multi3); + + std::vector<std::string> unparsedArguments; + std::vector<std::string> keywordsMissingValue; + Result const result = + parser.Parse(args, &unparsedArguments, &keywordsMissingValue); + + return verifyResult(result, unparsedArguments, keywordsMissingValue); +} + +} // namespace + +int testArgumentParser(int /*unused*/, char* /*unused*/ []) +{ + if (!testArgumentParserDynamic()) { + std::cout << "While executing testArgumentParserDynamic().\n"; + return -1; + } + + if (!testArgumentParserStatic()) { + std::cout << "While executing testArgumentParserStatic().\n"; + return -1; + } + + return 0; +} diff --git a/Utilities/IWYU/mapping.imp b/Utilities/IWYU/mapping.imp index 482a08dd82..0393ff199e 100644 --- a/Utilities/IWYU/mapping.imp +++ b/Utilities/IWYU/mapping.imp @@ -68,6 +68,7 @@ { symbol: [ "std::__decay_and_strip<cmGeneratorTarget *&>::__type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::__decay_and_strip<cmFindCommon::PathLabel &>::__type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::__decay_and_strip<cmSearchPath>::__type", private, "\"cmConfigure.h\"", public ] }, + { symbol: [ "std::__decay_and_strip<cm::string_view>::__type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::__decay_and_strip<std::basic_string<char> &>::__type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::__decay_and_strip<const std::basic_string<char> &>::__type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::__decay_and_strip<cmFindPackageCommand::PathLabel &>::__type", private, "\"cmConfigure.h\"", public ] }, @@ -260,6 +260,7 @@ CMAKE_CXX_SOURCES="\ cmAddLibraryCommand \ cmAddSubDirectoryCommand \ cmAddTestCommand \ + cmArgumentParser \ cmBreakCommand \ cmBuildCommand \ cmCMakeMinimumRequired \ |