From c5308dedba69a08f50a983cf097a8a3a09f2df8c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sun, 22 Oct 2023 01:45:04 -0700 Subject: [PATCH] Add SimpleConfig. https://godocs.io/git.wntrmute.dev/kyle/goutils/config is one of the more useful Go packages in my standard go library that gets used for building services. I needed something similar for another Shimmering Clarity project, and thus I figured I'd add it into SCSL. --- CMakeLists.txt | 6 +- include/scsl/Flags.h | 11 +++ include/scsl/SimpleConfig.h | 185 +++++++++++++++++++++++++++++++++++ include/scsl/StringUtil.h | 2 +- src/bin/phonebook.cc | 2 +- src/sl/Flags.cc | 27 ++++-- src/sl/SimpleConfig.cc | 189 ++++++++++++++++++++++++++++++++++++ src/sl/StringUtil.cc | 2 +- test/config-explorer.cc | 81 ++++++++++++++++ test/stringutil.cc | 25 +++-- 10 files changed, 505 insertions(+), 25 deletions(-) create mode 100644 include/scsl/SimpleConfig.h create mode 100644 src/sl/SimpleConfig.cc create mode 100644 test/config-explorer.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index d09aa9b..bda30d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ set(HEADER_FILES include/scsl/Commander.h include/scsl/Dictionary.h include/scsl/Flags.h + include/scsl/SimpleConfig.h include/scsl/StringUtil.h include/scsl/TLV.h @@ -72,6 +73,7 @@ set(SOURCE_FILES src/sl/Dictionary.cc src/test/Exceptions.cc src/sl/Flags.cc + src/sl/SimpleConfig.cc src/sl/StringUtil.cc src/sl/TLV.cc @@ -123,7 +125,6 @@ generate_test(orientation) generate_test(quaternion) generate_test(vector) - # test tooling add_executable(flags-demo test/flags.cc) target_link_libraries(flags-demo ${PROJECT_NAME}) @@ -131,6 +132,9 @@ target_link_libraries(flags-demo ${PROJECT_NAME}) add_executable(simple-test-demo test/simple_suite_example.cc) target_link_libraries(simple-test-demo ${PROJECT_NAME}) +add_executable(config-explorer test/config-explorer.cc) +target_link_libraries(config-explorer ${PROJECT_NAME}) + include(CMakePackageConfigHelpers) write_basic_package_version_file( scslConfig.cmake diff --git a/include/scsl/Flags.h b/include/scsl/Flags.h index 7364a0e..1db8464 100644 --- a/include/scsl/Flags.h +++ b/include/scsl/Flags.h @@ -245,6 +245,17 @@ public: std::string defaultValue, std::string fDescription); + /// Register a new string command line flag with a default value. + /// + /// \param fName The name of the flag, including a leading dash. + /// \param defaultValue The default value for the flag. + /// \param fDescription A short description of the flag. + /// \return True if the flag was registered, false if the flag could + /// not be registered (e.g. a duplicate flag was registered). + bool Register(std::string fName, + const char *defaultValue, + std::string fDescription); + /// Return the number of registered flags. size_t Size(); diff --git a/include/scsl/SimpleConfig.h b/include/scsl/SimpleConfig.h new file mode 100644 index 0000000..2367329 --- /dev/null +++ b/include/scsl/SimpleConfig.h @@ -0,0 +1,185 @@ +/// +/// \file include/scsl/SimpleConfig.h +/// \author K. Isom +/// \date 2023-10-21 +/// \brief Simple project configuration. +/// +/// This is an implementation of a simple global configuration system +/// for projects based on a Go version I've used successfully in +/// several projects. +/// +/// Copyright 2023 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that +/// the above copyright notice and this permission notice appear in all /// copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +/// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +/// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +/// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +/// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +/// PERFORMANCE OF THIS SOFTWARE. +/// + +#ifndef SCSL_SIMPLECONFIG_H +#define SCSL_SIMPLECONFIG_H + + +#include +#include +#include + + +namespace scsl { + + +/// \brief SimpleConfig is a basic configuration for projects. +/// +/// SimpleConfig is a basic key-value system. It can optionally load +/// key-value pairs from a file, which should consist of key = value +/// lines. Comments may be added by starting the line with a '#'; these +/// lines will be skipped. Comments may have leading whitespace. Any +/// empty lines or lines consisting solely of whitespace will also be +/// skipped. +/// +/// When values are retrieved by one of the variants on Get, they are +/// looked up in the following order, assuming `key` and an optional +/// `prefix` set on the config: +/// +/// 1. Any cached key-value pairs. Loading a file caches the key-value +/// pairs in the file. The file is not used again, unless another +/// call to Load is made. If the cache has a name for `key`, it will +/// be returned. +/// 2. The value is looked up from the environment. An optional prefix +/// can be set; if so, if there is an environment named +/// `{prefix}{key}`, the value will be cached and it will be +/// returned. +/// 3. If a default value has been provided, it will be cached and +/// returned. +/// 4. If none of the previous steps has provided a value, an empty +/// string will be returned. +/// +/// In Go projects, I've used the global config to great success. +/// However, callers may set up an explicit configuration instance. +class SimpleConfig { +public: +#if defined(SCSL_DESKTOP_BUILD) + /// \brief Load key-value pairs from a file. + /// + /// \note This operates on the global config. + /// + /// \param path The path to a config file. + /// \return 0 if the file was loaded successfully. + static int LoadGlobal(const char *path); + + /// \brief Load key-value pairs from a file. + /// + /// \note This operates on the global config. + /// + /// \param path The path to a config file. + /// \return 0 if the file was loaded successfully. + static int LoadGlobal(std::string &path); + + /// \brief Set the prefix in use by the config. + /// + /// \note This operates on the global config. + /// + /// \param prefix The prefix to prepend to the key when looking + /// up values from the environment. + static void SetPrefixGlobal(const std::string prefix); + + /// \brief Return the keys cached in the config. + /// + /// Note that this won't returned any non-cached environment + /// values. + /// + /// \note This operates on the global config. + /// + /// \return A list of keys stored under the config. + static std::vector KeyListGlobal(); + + /// \brief Get the value stored for the key from the config. + /// + /// \note This operates on the global config. + /// + /// \param key The key to look up. See the class documentation + /// for information on how this is used. + /// \return The value stored under the key, or an empty string. + static std::string GetGlobal(std::string key); + + /// \brief Get the value stored for the key from the config. + /// + /// \note This operates on the global config. + /// + /// \param key The key to look up. See the class documentation + /// for information on how this is used. + /// \param defaultValue A default value to cache and use if no + /// value is stored under the key. + /// \return The value stored under the key, or the default + /// value. + static std::string GetGlobal(std::string key, std::string defaultValue); +#endif + + /// \brief The constructor doesn't need any initialisation. + SimpleConfig(); + + /// \brief The constructor can explicitly set the environment + /// prefix. + SimpleConfig(std::string prefix); + + /// \brief Load key-value pairs from a file. + /// + /// \param path The path to a config file. + /// \return 0 if the file was loaded successfully. + int Load(const char *path); + + /// \brief Load key-value pairs from a file. + /// + /// \param path The path to a config file. + /// \return 0 if the file was loaded successfully. + int Load(std::string& path); + + /// \brief Set the prefix in use by the config. + /// + /// \param prefix The prefix to prepend to the key when looking + /// up values from the environment. + void SetPrefix(const std::string prefix); + + /// \brief Return the keys cached in the config. + /// + /// Note that this won't returned any non-cached environment + /// values. + /// + /// \return A list of keys stored under the config. + std::vector KeyList(); + + /// \brief Get the value stored for the key from the config. + /// + /// \param key The key to look up. See the class documentation + /// for information on how this is used. + /// \return The value stored under the key, or an empty string. + std::string Get(std::string key); + + /// \brief Get the value stored for the key from the config. + /// + /// \param key The key to look up. See the class documentation + /// for information on how this is used. + /// \param defaultValue A default value to cache and use if no + /// value is stored under the key. + /// \return The value stored under the key, or the default + /// value. + std::string Get(std::string key, std::string defaultValue); + +private: + std::string envPrefix; + std::map vars; +}; + + +} // namespace scsl + + +#endif //SCSL_SIMPLECONFIG_H diff --git a/include/scsl/StringUtil.h b/include/scsl/StringUtil.h index 28fd761..749e205 100644 --- a/include/scsl/StringUtil.h +++ b/include/scsl/StringUtil.h @@ -33,7 +33,7 @@ namespace scsl { /// String-related utility functions. -namespace string { +namespace scstring { /// Remove any whitespace At the beginning of the string. The string diff --git a/src/bin/phonebook.cc b/src/bin/phonebook.cc index 57f5fa9..7500301 100644 --- a/src/bin/phonebook.cc +++ b/src/bin/phonebook.cc @@ -80,7 +80,7 @@ hasKey(std::vector argv) } cout << "not found\n"; - return true; + return false; } diff --git a/src/sl/Flags.cc b/src/sl/Flags.cc index fc70569..1e9bec6 100644 --- a/src/sl/Flags.cc +++ b/src/sl/Flags.cc @@ -20,7 +20,6 @@ /// PERFORMANCE OF THIS SOFTWARE. /// - #include #include #include @@ -172,6 +171,18 @@ Flags::Register(std::string fName, std::string defaultValue, std::string fDescri } +bool +Flags::Register(std::string fName, const char *defaultValue, std::string fDescription) +{ + if (!this->Register(fName, FlagType::String, std::move(fDescription))) { + return false; + } + + this->flags[fName]->Value.s = new std::string(defaultValue); + return true; +} + + size_t Flags::Size() { @@ -206,7 +217,7 @@ Flags::ParseStatus Flags::parseArg(int argc, char **argv, int &index) { std::string arg(argv[index]); - string::TrimWhitespace(arg); + scstring::TrimWhitespace(arg); if (!std::regex_search(arg, isFlag)) { return ParseStatus::EndOfFlags; @@ -302,7 +313,7 @@ Flags::Usage(std::ostream &os, int exitCode) os << this->name << ":\t"; auto indent = this->name.size() + 7; - string::WriteTabIndented(os, description, 72 - indent, indent / 8, false); + scstring::WriteTabIndented(os, description, 72 - indent, indent / 8, false); os << "\n\n"; for (const auto &pair : this->flags) { @@ -312,16 +323,16 @@ Flags::Usage(std::ostream &os, int exitCode) argLine += "\t\t"; break; case FlagType::Integer: - argLine += "int\t\t"; + argLine += " int\t\t"; break; case FlagType::UnsignedInteger: - argLine += "uint\t\t"; + argLine += " uint\t\t"; break; case FlagType::SizeT: - argLine += "size_t\t"; + argLine += " size_t\t"; break; case FlagType::String: - argLine += "string\t"; + argLine += " string\t"; break; case FlagType::Unknown: // fallthrough @@ -336,7 +347,7 @@ Flags::Usage(std::ostream &os, int exitCode) os << argLine; indent = argLine.size(); - string::WriteTabIndented(os, pair.second->Description, + scstring::WriteTabIndented(os, pair.second->Description, 72-indent, (indent/8)+2, false); } diff --git a/src/sl/SimpleConfig.cc b/src/sl/SimpleConfig.cc new file mode 100644 index 0000000..7e52327 --- /dev/null +++ b/src/sl/SimpleConfig.cc @@ -0,0 +1,189 @@ +/// +/// \file src/sl/SimpleConfig.cc +/// \author K. Isom +/// \date 2023-10-21 +/// \brief Simple project configuration. +/// +/// This is an implementation of a simple global configuration system +/// for projects based on a Go version I've used successfully in +/// several projects. +/// +/// Copyright 2023 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that +/// the above copyright notice and this permission notice appear in all /// copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +/// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +/// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +/// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +/// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +/// PERFORMANCE OF THIS SOFTWARE. +/// + +#include +#include +#include + +#include +#include + + +namespace scsl { + + +#if defined(SCSL_DESKTOP_BUILD) +static SimpleConfig globalConfig; +#endif + + +static constexpr auto regexOpts = std::regex_constants::nosubs | + std::regex_constants::optimize | + std::regex_constants::ECMAScript; + +static const std::regex commentLine("^\\s*#.*$", regexOpts); +static const std::regex keyValueLine("^\\s*\\w+\\s*=\\s*\\w+", regexOpts); + + +int +SimpleConfig::LoadGlobal(const char *path) +{ + return globalConfig.Load(path); +} + + +int +SimpleConfig::LoadGlobal(std::string &path) +{ + return globalConfig.Load(path); +} + + +void +SimpleConfig::SetPrefixGlobal(const std::string prefix) +{ + globalConfig.SetPrefix(prefix); +} + + +std::vector +SimpleConfig::KeyListGlobal() +{ + return globalConfig.KeyList(); +} + + +std::string +SimpleConfig::GetGlobal(std::string key) +{ + return globalConfig.Get(key); +} + + +std::string +SimpleConfig::GetGlobal(std::string key, std::string defaultValue) +{ + return globalConfig.Get(key, defaultValue); +} + + +SimpleConfig::SimpleConfig() +{ +} + + +SimpleConfig::SimpleConfig(std::string prefix) +: envPrefix(prefix) +{ +} + + +int +SimpleConfig::Load(const char *path) +{ + auto spath = std::string(path); + return this->Load(spath); +} + + +int +SimpleConfig::Load(std::string &path) +{ + std::ifstream configFile(path); + std::string line; + + while (std::getline(configFile, line)) { + scstring::TrimWhitespace(line); + if (line.size() == 0) { + continue; + } + + if (std::regex_search(line, commentLine)) { + return -1; + } + + if (std::regex_search(line, keyValueLine)) { + auto pair = scstring::SplitKeyValuePair(line, "="); + if (pair.size() < 2) { + return -1; + } + + this->vars[pair[0]] = pair[1]; + } + } + + return 0; +} + + +void +SimpleConfig::SetPrefix(const std::string prefix) +{ + this->envPrefix = std::move(prefix); +} + + +std::string +SimpleConfig::Get(std::string key) +{ + return this->Get(key, ""); +} + + +std::string +SimpleConfig::Get(std::string key, std::string defaultValue) +{ + if (this->vars.count(key)) { + return this->vars[key]; + } + + auto envKey = this->envPrefix + key; + + const char *envValue = getenv(envKey.c_str()); + if (envValue != nullptr) { + this->vars[key] = std::string(envValue); + return this->Get(key); + } + + this->vars[key] = defaultValue; + return this->Get(key); +} + + +std::vector +SimpleConfig::KeyList() +{ + std::vector keyList; + + for (auto &entry : this->vars) { + keyList.push_back(entry.first); + } + + return keyList; +} + + +} // namespace SimpleConfig diff --git a/src/sl/StringUtil.cc b/src/sl/StringUtil.cc index cc31149..4f99700 100644 --- a/src/sl/StringUtil.cc +++ b/src/sl/StringUtil.cc @@ -28,7 +28,7 @@ namespace scsl { -namespace string { +namespace scstring { std::vector diff --git a/test/config-explorer.cc b/test/config-explorer.cc new file mode 100644 index 0000000..5b3367d --- /dev/null +++ b/test/config-explorer.cc @@ -0,0 +1,81 @@ +/// +/// \file test/config-explorer.cc +/// \author K. Isom +/// \date 2023-10-21 +/// \brief Commandline tools for interacting with simple configurations. +/// +/// Copyright 2023 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that +/// the above copyright notice and this permission notice appear in all /// copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +/// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +/// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +/// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +/// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +/// PERFORMANCE OF THIS SOFTWARE. +/// + + +#include +#include + +#include +#include + + +int +main(int argc, char *argv[]) +{ + int retc = 1; + bool listKeys; + std::string fileName; + std::string prefix; + std::string defaultValue; + + auto *flags = new scsl::Flags("config-explorer", + "interact with a simple configuration system"); + flags->Register("-d", "", "set a default value"); + flags->Register("-f", scsl::FlagType::String, "path to a configuration file"); + flags->Register("-l", false, "list cached keys at the end"); + flags->Register("-p", "CX_", + "prefix for configuration environment variables"); + auto parsed = flags->Parse(argc, argv); + if (parsed != scsl::Flags::ParseStatus::OK) { + std::cerr << "Failed to parse flags: " + << scsl::Flags::ParseStatusToString(parsed) << "\n"; + exit(1); + } + + flags->GetString("-d", defaultValue); + flags->GetString("-f", fileName); + flags->GetBool("-l", listKeys); + flags->GetString("-p", prefix); + scsl::SimpleConfig::SetPrefixGlobal(prefix); + + if (!fileName.empty()) { + if (scsl::SimpleConfig::LoadGlobal(fileName) != 0) { + std::cerr << "[!] failed to load " << fileName << "\n"; + return retc; + } + } + + for (auto &key : flags->Args()) { + auto val = scsl::SimpleConfig::GetGlobal(key, defaultValue); + std::cout << key << ": " << val << "\n"; + } + + if (listKeys) { + std::cout << "[+] cached keys\n"; + for (auto &key : scsl::SimpleConfig::KeyListGlobal()) { + std::cout << "\t- " << key << "\n"; + } + } + + delete flags; + return retc; +} \ No newline at end of file diff --git a/test/stringutil.cc b/test/stringutil.cc index 954a820..bfd4d23 100644 --- a/test/stringutil.cc +++ b/test/stringutil.cc @@ -42,30 +42,30 @@ TestTrimming(std::string line, std::string lExpected, std::string rExpected, std std::string result; std::string message; - result = string::TrimLeadingWhitespaceDup(line); + result = scstring::TrimLeadingWhitespaceDup(line); message = "TrimLeadingDup(\"" + line + "\"): '" + result + "'"; sctest::Assert(result == lExpected, message); - result = string::TrimTrailingWhitespaceDup(line); + result = scstring::TrimTrailingWhitespaceDup(line); message = "TrimTrailingDup(\"" + line + "\"): '" + result + "'"; sctest::Assert(result == rExpected, message); - result = string::TrimWhitespaceDup(line); + result = scstring::TrimWhitespaceDup(line); message = "TrimDup(\"" + line + "\"): '" + result + "'"; sctest::Assert(result == expected, message); result = line; - string::TrimLeadingWhitespace(result); + scstring::TrimLeadingWhitespace(result); message = "TrimLeadingDup(\"" + line + "\"): '" + result + "'"; sctest::Assert(result == lExpected, message); result = line; - string::TrimTrailingWhitespace(result); + scstring::TrimTrailingWhitespace(result); message = "TrimTrailingDup(\"" + line + "\"): '" + result + "'"; sctest::Assert(result == rExpected, message); result = line; - string::TrimWhitespace(result); + scstring::TrimWhitespace(result); message = "TrimDup(\"" + line + "\"): '" + result + "'"; sctest::Assert(result == expected, message); } @@ -75,7 +75,7 @@ std::function TestSplit(std::string line, std::string delim, size_t maxCount, std::vector expected) { return [line, delim, maxCount, expected]() { - return string::SplitN(line, delim, maxCount) == expected; + return scstring::SplitN(line, delim, maxCount) == expected; }; } @@ -86,7 +86,7 @@ TestSplitChar() { auto expected = std::vector{"hello", "world"}; const auto *inputLine = "hello=world\n"; - auto actual = string::SplitKeyValuePair(inputLine, '='); + auto actual = scstring::SplitKeyValuePair(inputLine, '='); return actual == expected; } @@ -109,11 +109,11 @@ TestWrapping() "hope so.", }; - auto wrapped = string::WrapText(testLine, 16); + auto wrapped = scstring::WrapText(testLine, 16); if (wrapped.size() != expected.size()) { - std::cerr << string::VectorToString(wrapped) + std::cerr << scstring::VectorToString(wrapped) << " != " - << string::VectorToString(expected) + << scstring::VectorToString(expected) << "\n"; } @@ -127,7 +127,7 @@ TestWrapping() return false; } -// string::WriteTabIndented(std::cout, wrapped, 4, true); +// scstring::WriteTabIndented(std::cout, wrapped, 4, true); return true; } @@ -149,7 +149,6 @@ main(int argc, char *argv[]) if (parsed != scsl::Flags::ParseStatus::OK) { std::cerr << "Failed to parse flags: " << scsl::Flags::ParseStatusToString(parsed) << "\n"; - exit(1); } sctest::SimpleSuite suite;