Table of Contents
7bitConf is a simple C++ centralized configuration provider library, the main inspiration was the ASP net core configuration system.
7bitConf requires client code and a compiler compatible with the C++17 standard or newer.
The library is officially supported on the following platforms:
Operating systems:
- Linux
- macOS
- Windows
Compilers:
- gcc 8.0+
- clang 8.0+
- MSVC 2015+
Update CMakeLists.txt file with the following code
include(FetchContent)
FetchContent_Declare(
7bitConf
GIT_REPOSITORY https://github.com/7bitcoder/7bitConf.git
GIT_TAG v1.2.0
)
FetchContent_MakeAvailable(7bitConf)
target_link_libraries(Target 7bitConf::7bitConf)
Download and install Conan.io then install package, see Conan documentation for the package installation guide.
Download source code from the most recent release and copy the include folder into your project location, for example, copy into the '/SevenBitConf' folder. Include this folder into the project, example with CMake:
include_directories(SevenBitConf/Include)
Download SevenBitConf.hpp header file from the most recent release, copy this file into the desired project location and include it.
Download source code from the most recent release, and build or install the project using CMake, for more details, see the Building Library guide.
This library provides centralized configuration management, and multiple configuration sources (files, environment variables, command line arguments, custom sources) are combined into one JSON object (see taocpp json documentation).
Create the appsettings.json file in the compiled executable directory:
{
"Array": [
1,
2,
3
],
"MySetting": "appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
#include <SevenBit/Conf.hpp>
#include <iostream>
using namespace sb::cf;
int main(const int argc, char **argv)
{
const IConfiguration::Ptr configuration = ConfigurationBuilder{} //
.addAppSettings()
.addEnvironmentVariables()
.addCommandLine(argc, argv)
.build();
const std::string value = configuration->at("MySetting").get_string();
const std::string defaultLogLevel = configuration->deepAt("Logging:LogLevel:Default").get_string();
const std::uint64_t secondArrayElement = configuration->deepAt("Array:1").get_unsigned();
std::cout << "MySetting: " << value << std::endl;
std::cout << "Default LogLevel: " << defaultLogLevel << std::endl;
std::cout << "Second element in array: " << secondArrayElement << std::endl;
std::cout << "Configuration json:" << std::endl << std::setw(2) << *configuration;
return 0;
}
The example will print combined configuration from appsettings.json, environment variables, and command-line arguments. Source-adding order matters, the least source overrides the previous one.
The configuration builder class creates configuration from configuration sources, there are a few predefined sources ready to be used.
The command line configuration source is added using the addCommandLine(argc, argv) builder method:
auto configuration = ConfigurationBuilder{}.addCommandLine(argc, argv).build();
Argument patterns:
- option[:nestedOption|arrayIndex...][!type]=[value]
- --option[:nestedOption|arrayIndex...][!type] [value]
- /option[:nestedOption|arrayIndex...][!type] [value]
Prefix ('--' or '/') is optional when the value is separated using '='. Nested settings are supported using the ':' separator. If the object is an array, numbers can be used to address the proper element. By default, setting values are saved as strings, but other types are also supported using the '!' mark. If a value is not provided, the default one will be used for the specified type, see supported types.
Some arguments might be filtered using an overloaded method that accepts std::vector<std::string_view> This example shows how to pass only arguments that start with "--SETTING":
std::vector<std::string_view> configArgs;
for (size_t i = 1; i < argc; ++i)
{
std::string_view argView{argv[i]};
if(argView.starts_with("--SETTING")) { // starts_with() c++ 20 feature
configArgs.push_back(argView);
}
}
auto configuration = ConfigurationBuilder{}.addCommandLine(configArgs).build();
The command line configuration source can be more customized with the additional addCommandLine method arguments: CommandLineParserConfig or SettingParser.
type (default value) - description
- string ("") - default type, could be specified explicitly.
- uint (0) - unsigned 64-bit integer.
- int (0) - 64-bit integer.
- double (0.0) - double.
- bool (false) - case-insensitive "true" or "false" or number (non-zero is considered as true).
- json (undefined) - JSON string for example {"hello": "value"}.
- null (null) - null is used as a JSON value.
- MySetting - will override or create a MySetting setting with the default "" string value.
- MySetting="hello" - will override or create a MySetting setting with the "hello" string value.
- Switch!bool=true - will override or create a Switch setting with a true bool value.
- --MySetting hello - will override or create a MySetting setting with the "hello" string value.
- --Offset!double - will override or create an Offset setting with the default 0.0 double value.
- --Logging:LogLevel:Default Warning - will override or create a nested setting with the string "Warning" value.
- /Strings:2 hello - will override or create a third element in the Strings array setting with the string "hello" value.
- /Array:1!uint 123 will override the second element in Array with the unsigned integer 123 value.
The environment variables configuration source is added using the addEnvironmentVariables() builder method.
auto configuration = ConfigurationBuilder{}.addEnvironmentVariables().build();
This method will load all available environment variables, the better option is to load only prefixed ones (some variables might be ill-formatted), call addEnvironmentVariables with a string to specify a prefix for environment variables:
auto configuration = ConfigurationBuilder{}.addEnvironmentVariables("CUSTOM_PREFIX_").build();
It will load all environment variables with the provided prefix (the prefix is removed in the final configuration setting names).
All rules for command-line arguments are also valid for environment variables. Some operating systems might not support '!' or ':' characters in variables, in that case, alternative separators can be used. The alternative for ':' is '__' (double underscore) and for '!' is '___' (triple underscore).
Setting Array:2!uint=123 would be rewritten as Array__2___uint=123
Same as the command line source, the environment variables configuration source can be more customized with additional addEnvironmentVariables method arguments: EnvironmentVarsParserConfig or SettingParser.
The JSON file configuration source is added using the addJsonFile(std::filesystem::path jsonFilePath) builder method.
auto configuration = ConfigurationBuilder{}.addJsonFile("configuration.json").build();
If the file does not exist, the method will throw an exception, call this method with an additional bool optional argument = true, to prevent throwing an exception in this case.
auto configuration = ConfigurationBuilder{}.addJsonFile("configuration.json", true).build();
The app settings configuration source is added using the addAppSettings() builder method.
auto configuration = ConfigurationBuilder{}.addAppSettings().build();
This source will use a JSON file configuration source with the "appsettings.json" file and isOptional = true parameter. addAppSettings is overloaded with an environment name string parameter. If the environment name is not empty, this source will additionally load "appsettings.{environment name}.json", for example:
auto configuration = ConfigurationBuilder{}.addAppSettings("myenv").build();
It will additionally load "appsettings.myenv.json" after loading "appsettings.json".
Various appsettings files can be prepared for different environments, environment name can be fetched from the system environment variable using std::getenv(), for example:
auto envName = std::getenv("MYAPP_ENVIRONMENT")
auto configuration = ConfigurationBuilder{}.addAppSettings(envName).build();
The key per file configuration source is added using the addKeyPerFile(std::filesystem::path directoryPath) builder method.
auto configuration = ConfigurationBuilder{}.addKeyPerFile("ConfigurationsDirectory").build();
This source will load all JSON files from the "ConfigurationsDirectory" directory and save file contents under the file name setting. The nested setting is supported with __ for example:
Assume existing directory:
MyDirectory/
- firstSetting.json
- second__nested.json
auto configuration = ConfigurationBuilder{}.addKeyPerFile("MyDirectory").build();
It will load two JSON files, first under the "firstSetting" setting name, the second config will be stored in "nested" object which will be in the "second" object.
The method will throw an exception if the directory does not exist, call this method with an additional bool optional argument = true, to prevent throwing an exception in this case.
auto configuration = ConfigurationBuilder{}.addKeyPerFile("MyDirectory", true).build();
Some files can be ignored with the string ignore prefix:
auto configuration = ConfigurationBuilder{}.addKeyPerFile("MyDirectory", true, "ignoreFile_").build();
or with a functor:
auto configuration = ConfigurationBuilder{}.addKeyPerFile("MyDirectory", true, [](const std::filesystem::path& file){ return file.filename().string().starts_with("ignoreFile_"); }).build();
A JSON stream configuration source is added using the addJsonStream(std::istream & stream) builder method.
auto configuration = ConfigurationBuilder{}.addJsonStream(stream).build();
The stream must return the proper JSON file otherwise, the method will throw an exception.
A JSON object configuration source is added using the addJson(JsonObject json) builder method.
auto configuration = ConfigurationBuilder{}.addJson({{"setting", "hello"}}).build();
In memory settings, the configuration source is added using addInMemory(std::vector<std::pair<std::string, JsonValue>>) builder method. The first element in a pair can contain ':' to provide a nested setting:
auto configuration = ConfigurationBuilder{}.addInMemory({"setting:nested", "hello"}).build();
A custom configuration source can be added using the add(IConfigurationSource::Sptr) builder method.
A custom configuration source must implement IConfigurationSource.
#include <SevenBit/Conf.hpp>
#include <iostream>
#include <memory>
using namespace sb::cf;
class CustomConfigurationProvider : public IConfigurationProvider
{
JsonObject _configuration;
public:
void load() override { _configuration = {{"mysettingOne", "value1"}, {"mysettingTwo", "value2"}}; }
[[nodiscard]] const JsonObject &getConfiguration() const override { return _configuration; }
};
class CustomConfigurationSource : public IConfigurationSource
{
public:
IConfigurationProvider::Ptr build(IConfigurationBuilder &builder) override
{
return std::make_unique<CustomConfigurationProvider>();
}
};
int main(int argc, char **argv)
{
const IConfiguration::Ptr configuration =
ConfigurationBuilder{}.add(std::make_unique<CustomConfigurationSource>()).build();
std::cout << "Configuration json:" << std::endl << std::setw(2) << *configuration;
return 0;
}
CommandLineParserConfig is a simple struct that contains the data used to configure the command line parser, by default it is initialized with these values:
struct CommandLineParserConfig
{
std::vector<std::string_view> optionPrefixes = {"--", "/"};
std::vector<std::string_view> optionSplitters = {"=", " "};
std::vector<std::string_view> keySplitters = {":"};
std::vector<std::string_view> typeMarkers = {"!"};
std::string_view defaultType = "string";
bool throwOnUnknownType = true;
bool allowEmptyKeys = false;
};
This configuration allows specifying different behaviors of the command line parser.
Example option option:values:2!int=123 each part is marked as affected with ()
- optionPrefixes - list of possible option prefixes: (--)option:values:2!int=123
- optionSplitters - list of possible option splitters: --option:values:2!int(=)123
- keySplitters - list of possible key splitters: --option(:)values(:)2!int=123
- typeMarkers - list of possible type markers: --option:values:2(!)int=123
- defaultType - is the type that is used if the type was not specified explicitly in a variable: --option:values:2=123
- throwOnUnknownType - if the type was not recognized in the parsing phase then an exception will be thrown if in this case this setting is set to false default type will be used: --option:values:2!nonExistingType=123
- allowEmptyKeys - if set to true and empty keys are detected, an exception will be thrown: --option::2!int=123
All options should be loaded without considering the type and with the custom option prefix '//', solution:
#include <SevenBit/Conf.hpp>
#include <iostream>
using namespace sb::cf;
int main(const int argc, char **argv)
{
CommandLineParserConfig parserConfig;
parserConfig.optionPrefixes = {"//"};
parserConfig.typeMarkers.clear();
const IConfiguration::Ptr configuration = ConfigurationBuilder{} //
.addAppSettings()
.addCommandLine(argc, argv, std::move(parserConfig))
.build();
std::cout << "Configuration json:" << std::endl << std::setw(2) << *configuration;
return 0;
}
EnvironmentVarsParserConfig is a similar struct to the command line parser config with one difference there is no way to set variable prefixes:
struct EnvironmentVarsParserConfig
{
std::vector<std::string_view> variableSplitters = {"="};
std::vector<std::string_view> keySplitters = {":", "__"};
std::vector<std::string_view> typeMarkers = {"!", "___"};
std::string_view defaultType = "string";
bool throwOnUnknownType = true;
bool allowEmptyKeys = false;
};
This configuration allows specifying different behaviors of the environment variable parser.
Example environment variable option__values__2___int=123 each part is marked as affected with ()
- variableSplitters - list of possible variable splitters: option__values__2___int(=)123
- keySplitters - list of possible key splitters: option(__)values(__)2___int=123
- typeMarkers - list of possible type markers: option__values__2(___)int=123
- defaultType - is the type that is used if the type was not specified explicitly in a variable: optionvalues2=123
- throwOnUnknownType - if the type was not recognized in the parsing phase then an exception will be thrown if in this case this setting is set to false default type will be used: option__values__2___nonExistingType=123
- allowEmptyKeys - if set to true and empty keys are detected, an exception will be thrown: option____2___int=123
Some system environment variables are prefixed with double underscore __, in that case, the environment parser by default will throw an exception because this prefix will be treated as a key splitter resulting in the empty first part of keys, example __SOME_SYSTEM_ENV=value will be parsed to "" and "SOME_SYSTEM_ENV" keys, in that case, set allowEmptyKeys to true or disable keys splitting with clearing keySplitters config value, or filter out unwanted variables with prefix passed to addEnvironmentVariables method
All environment variables should be loaded as not nested objects (no key splitting) and without considering the type, solution:
#include <SevenBit/Conf.hpp>
#include <iostream>
using namespace sb::cf;
int main(int argc, char **argv)
{
EnvironmentVarsParserConfig parserConfig;
parserConfig.keySplitters.clear();
parserConfig.typeMarkers.clear();
const IConfiguration::Ptr configuration = ConfigurationBuilder{} //
.addAppSettings()
.addEnvironmentVariables("", std::move(parserConfig))
.addCommandLine(argc, argv)
.build();
std::cout << "Configuration json:" << std::endl << std::setw(2) << *configuration;
return 0;
}
The library provides a CommandLineParserBuilder and EnvironmentVarsParserBuilder builder classes to create customized parsers for command line and environment variables, builder allows using custom value deserializer, config, settingSplitter, and valueDeserializersMap.
All command line options should be loaded as not nested objects (no key splitting) with consideration of the new type " myType" will set the option value to "emptyValue" string if no value was provided, myType should be used as the default type, additional setting prefix should be considered '//' and default type should be also used if the type was not recognized, solution:
#include <SevenBit/Conf.hpp>
#include <iostream>
using namespace sb::cf;
struct MyTypeDeserializer final : IDeserializer
{
[[nodiscard]] JsonValue deserialize(std::optional<std::string_view> value) const override
{
return value ? value : "emptyValue";
}
};
int main(const int argc, char **argv)
{
auto builderFunc = [](CommandLineParserBuilder &builder) {
CommandLineParserConfig parserConfig;
parserConfig.keySplitters.clear();
parserConfig.optionPrefixes.emplace_back("//");
parserConfig.defaultType = "myType";
parserConfig.throwOnUnknownType = false;
builder.useConfig(std::move(parserConfig))
.useDefaultValueDeserializers()
.useValueDeserializer("myType", std::make_unique<MyTypeDeserializer>());
};
const IConfiguration::Ptr configuration = ConfigurationBuilder{} //
.addAppSettings()
.addEnvironmentVariables()
.addCommandLine(argc, argv, builderFunc)
.build();
std::cout << "Configuration json:" << std::endl << std::setw(2) << *configuration;
return 0;
}
In this case, keySplitters is cleared to prevent extracting nested keys, the additional setting prefix is added '//', and the default type is changed to custom type "myType", and with throwOnUnknownType set to false instead of throwing an exception when type is not recognized, default will be used. CommandLineParserBuilder is being used to create a custom command line parser with new config and custom valueDeserializer for type "myType", useDefaultValueDeserializers is used to add predefined value deserializers (string, int, json ...).
The library can be built locally using Cmake, library uses Taocpp JSON if the library is not found it will be downloaded using Cmake fetch api
Create a build directory and navigate to it:
mkdir build && cd build
Configure a CMake project:
cmake .. -DCMAKE_BUILD_TYPE=Debug
Using this command, several cache variables can be set:
- <cache variable name>: [possible values] (default value) - Description
- _7BIT_CONF_LIBRARY_TYPE: ["Shared", "Static", "HeaderOnly"] ("Static") - Library build type
- _7BIT_CONF_BUILD_UNIT_TESTS: ["ON", "OFF"] ("OFF") - Turn on to build unit tests
- _7BIT_CONF_BUILD_INTEGRATION_TESTS: ["ON", "OFF"] ("OFF") - Turn on to build integration tests
- _7BIT_CONF_BUILD_E2E_TESTS: ["ON", "OFF"] ("OFF") - Turn on to build e2e tests
- _7BIT_CONF_BUILD_ALL_TESTS: ["ON", "OFF"] ("OFF") - Turn on to build all tests (unit, integration and e2e)
- _7BIT_CONF_BUILD_EXAMPLES: ["ON", "OFF"] ("OFF") - Turn on to build examples
- _7BIT_CONF_BUILD_SINGLE_HEADER: ["ON", "OFF"] ("OFF") - Turn on to build single header SevenBitConf.hpp (requires Quom to be installed)
- _7BIT_CONF_INSTALL: ["ON", "OFF"] ("OFF") - Turn on to install the library
To set the cache variable, pass the additional option: -D<cache variable name>=[value], for example, this command will set the library type to Static and will force examples built
cmake .. -DCMAKE_BUILD_TYPE=Release -D_7BIT_CONF_LIBRARY_TYPE=Static -D_7BIT_CONF_BUILD_EXAMPLES=true
Build the library using the command:
cmake --build .
To install the library, set the additional cache variables _7BIT_CONF_INSTALL=ON and specify the installation directory with CMAKE_INSTALL_PREFIX, then run the command:
cmake --build . --config Release --target install
Distributed under the MIT License. See LICENSE.txt
for more information.
@7bitcoder Sylwester Dawida 2023