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.
This commit is contained in:
Kyle Isom 2023-10-22 01:45:04 -07:00
parent 9a8dc08a4f
commit c5308dedba
10 changed files with 505 additions and 25 deletions

View File

@ -42,6 +42,7 @@ set(HEADER_FILES
include/scsl/Commander.h include/scsl/Commander.h
include/scsl/Dictionary.h include/scsl/Dictionary.h
include/scsl/Flags.h include/scsl/Flags.h
include/scsl/SimpleConfig.h
include/scsl/StringUtil.h include/scsl/StringUtil.h
include/scsl/TLV.h include/scsl/TLV.h
@ -72,6 +73,7 @@ set(SOURCE_FILES
src/sl/Dictionary.cc src/sl/Dictionary.cc
src/test/Exceptions.cc src/test/Exceptions.cc
src/sl/Flags.cc src/sl/Flags.cc
src/sl/SimpleConfig.cc
src/sl/StringUtil.cc src/sl/StringUtil.cc
src/sl/TLV.cc src/sl/TLV.cc
@ -123,7 +125,6 @@ generate_test(orientation)
generate_test(quaternion) generate_test(quaternion)
generate_test(vector) generate_test(vector)
# test tooling # test tooling
add_executable(flags-demo test/flags.cc) add_executable(flags-demo test/flags.cc)
target_link_libraries(flags-demo ${PROJECT_NAME}) 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) add_executable(simple-test-demo test/simple_suite_example.cc)
target_link_libraries(simple-test-demo ${PROJECT_NAME}) 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) include(CMakePackageConfigHelpers)
write_basic_package_version_file( write_basic_package_version_file(
scslConfig.cmake scslConfig.cmake

View File

@ -245,6 +245,17 @@ public:
std::string defaultValue, std::string defaultValue,
std::string fDescription); 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. /// Return the number of registered flags.
size_t Size(); size_t Size();

185
include/scsl/SimpleConfig.h Normal file
View File

@ -0,0 +1,185 @@
///
/// \file include/scsl/SimpleConfig.h
/// \author K. Isom <kyle@imap.cc>
/// \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 <kyle@imap.cc>
///
/// 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 <map>
#include <string>
#include <vector>
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<std::string> 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<std::string> 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<std::string, std::string> vars;
};
} // namespace scsl
#endif //SCSL_SIMPLECONFIG_H

View File

@ -33,7 +33,7 @@
namespace scsl { namespace scsl {
/// String-related utility functions. /// String-related utility functions.
namespace string { namespace scstring {
/// Remove any whitespace At the beginning of the string. The string /// Remove any whitespace At the beginning of the string. The string

View File

@ -80,7 +80,7 @@ hasKey(std::vector<std::string> argv)
} }
cout << "not found\n"; cout << "not found\n";
return true; return false;
} }

View File

@ -20,7 +20,6 @@
/// PERFORMANCE OF THIS SOFTWARE. /// PERFORMANCE OF THIS SOFTWARE.
/// ///
#include <cassert> #include <cassert>
#include <iostream> #include <iostream>
#include <regex> #include <regex>
@ -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 size_t
Flags::Size() Flags::Size()
{ {
@ -206,7 +217,7 @@ Flags::ParseStatus
Flags::parseArg(int argc, char **argv, int &index) Flags::parseArg(int argc, char **argv, int &index)
{ {
std::string arg(argv[index]); std::string arg(argv[index]);
string::TrimWhitespace(arg); scstring::TrimWhitespace(arg);
if (!std::regex_search(arg, isFlag)) { if (!std::regex_search(arg, isFlag)) {
return ParseStatus::EndOfFlags; return ParseStatus::EndOfFlags;
@ -302,7 +313,7 @@ Flags::Usage(std::ostream &os, int exitCode)
os << this->name << ":\t"; os << this->name << ":\t";
auto indent = this->name.size() + 7; 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"; os << "\n\n";
for (const auto &pair : this->flags) { for (const auto &pair : this->flags) {
@ -336,7 +347,7 @@ Flags::Usage(std::ostream &os, int exitCode)
os << argLine; os << argLine;
indent = argLine.size(); indent = argLine.size();
string::WriteTabIndented(os, pair.second->Description, scstring::WriteTabIndented(os, pair.second->Description,
72-indent, (indent/8)+2, false); 72-indent, (indent/8)+2, false);
} }

189
src/sl/SimpleConfig.cc Normal file
View File

@ -0,0 +1,189 @@
///
/// \file src/sl/SimpleConfig.cc
/// \author K. Isom <kyle@imap.cc>
/// \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 <kyle@imap.cc>
///
/// 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 <cstdlib>
#include <fstream>
#include <regex>
#include <scsl/SimpleConfig.h>
#include <scsl/StringUtil.h>
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<std::string>
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<std::string>
SimpleConfig::KeyList()
{
std::vector<std::string> keyList;
for (auto &entry : this->vars) {
keyList.push_back(entry.first);
}
return keyList;
}
} // namespace SimpleConfig

View File

@ -28,7 +28,7 @@
namespace scsl { namespace scsl {
namespace string { namespace scstring {
std::vector<std::string> std::vector<std::string>

81
test/config-explorer.cc Normal file
View File

@ -0,0 +1,81 @@
///
/// \file test/config-explorer.cc
/// \author K. Isom <kyle@imap.cc>
/// \date 2023-10-21
/// \brief Commandline tools for interacting with simple configurations.
///
/// Copyright 2023 K. Isom <kyle@imap.cc>
///
/// 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 <iostream>
#include <string>
#include <scsl/Flags.h>
#include <scsl/SimpleConfig.h>
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;
}

View File

@ -42,30 +42,30 @@ TestTrimming(std::string line, std::string lExpected, std::string rExpected, std
std::string result; std::string result;
std::string message; std::string message;
result = string::TrimLeadingWhitespaceDup(line); result = scstring::TrimLeadingWhitespaceDup(line);
message = "TrimLeadingDup(\"" + line + "\"): '" + result + "'"; message = "TrimLeadingDup(\"" + line + "\"): '" + result + "'";
sctest::Assert(result == lExpected, message); sctest::Assert(result == lExpected, message);
result = string::TrimTrailingWhitespaceDup(line); result = scstring::TrimTrailingWhitespaceDup(line);
message = "TrimTrailingDup(\"" + line + "\"): '" + result + "'"; message = "TrimTrailingDup(\"" + line + "\"): '" + result + "'";
sctest::Assert(result == rExpected, message); sctest::Assert(result == rExpected, message);
result = string::TrimWhitespaceDup(line); result = scstring::TrimWhitespaceDup(line);
message = "TrimDup(\"" + line + "\"): '" + result + "'"; message = "TrimDup(\"" + line + "\"): '" + result + "'";
sctest::Assert(result == expected, message); sctest::Assert(result == expected, message);
result = line; result = line;
string::TrimLeadingWhitespace(result); scstring::TrimLeadingWhitespace(result);
message = "TrimLeadingDup(\"" + line + "\"): '" + result + "'"; message = "TrimLeadingDup(\"" + line + "\"): '" + result + "'";
sctest::Assert(result == lExpected, message); sctest::Assert(result == lExpected, message);
result = line; result = line;
string::TrimTrailingWhitespace(result); scstring::TrimTrailingWhitespace(result);
message = "TrimTrailingDup(\"" + line + "\"): '" + result + "'"; message = "TrimTrailingDup(\"" + line + "\"): '" + result + "'";
sctest::Assert(result == rExpected, message); sctest::Assert(result == rExpected, message);
result = line; result = line;
string::TrimWhitespace(result); scstring::TrimWhitespace(result);
message = "TrimDup(\"" + line + "\"): '" + result + "'"; message = "TrimDup(\"" + line + "\"): '" + result + "'";
sctest::Assert(result == expected, message); sctest::Assert(result == expected, message);
} }
@ -75,7 +75,7 @@ std::function<bool()>
TestSplit(std::string line, std::string delim, size_t maxCount, std::vector<std::string> expected) TestSplit(std::string line, std::string delim, size_t maxCount, std::vector<std::string> expected)
{ {
return [line, delim, maxCount, 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<std::string>{"hello", "world"}; auto expected = std::vector<std::string>{"hello", "world"};
const auto *inputLine = "hello=world\n"; const auto *inputLine = "hello=world\n";
auto actual = string::SplitKeyValuePair(inputLine, '='); auto actual = scstring::SplitKeyValuePair(inputLine, '=');
return actual == expected; return actual == expected;
} }
@ -109,11 +109,11 @@ TestWrapping()
"hope so.", "hope so.",
}; };
auto wrapped = string::WrapText(testLine, 16); auto wrapped = scstring::WrapText(testLine, 16);
if (wrapped.size() != expected.size()) { if (wrapped.size() != expected.size()) {
std::cerr << string::VectorToString(wrapped) std::cerr << scstring::VectorToString(wrapped)
<< " != " << " != "
<< string::VectorToString(expected) << scstring::VectorToString(expected)
<< "\n"; << "\n";
} }
@ -127,7 +127,7 @@ TestWrapping()
return false; return false;
} }
// string::WriteTabIndented(std::cout, wrapped, 4, true); // scstring::WriteTabIndented(std::cout, wrapped, 4, true);
return true; return true;
} }
@ -149,7 +149,6 @@ main(int argc, char *argv[])
if (parsed != scsl::Flags::ParseStatus::OK) { if (parsed != scsl::Flags::ParseStatus::OK) {
std::cerr << "Failed to parse flags: " std::cerr << "Failed to parse flags: "
<< scsl::Flags::ParseStatusToString(parsed) << "\n"; << scsl::Flags::ParseStatusToString(parsed) << "\n";
exit(1);
} }
sctest::SimpleSuite suite; sctest::SimpleSuite suite;