C++ 配置转参数工具包。
User configurations to program parameters tools.
Parse user inputs (e.g. CLI arguments, JSON, INI, etc.) to configuration struct, then generate program parameters.
[DefaultValues] --parse-> [ValueTree] \ [Rules]
\ |
[ ConfigFiles ] --parse-> [ValueTree] --join-> [Config] --transform-> [Param]
/
[CLI-Arguments] --parse-> [ValueTree] /
Generally, Config should be defined in a human-friendly format, while Param needs to be closer to the program's input requirements. Defining Param as a cached data type that the program can directly work with (avoiding redundant checks and type conversions) also helps improve performance. The bridge that connects the two is the Rules.
Define your own Config and Param types separately. Then, create a set of Rules to:
- Handle part of the conversion from Config to Param.
- Perform input validation and report errors if requirements are not met.
Finally, call the transform function to apply these Rules.
We treat various user inputs as the source and parse them into a common intermediate ValueTree (composed of multiple ValueNodes). After that, you can define the conversion from ValueTree to Config as well as override rules for different input methods.
Some tools are provided for parsing inputs, such as CLI arguments, JSON, INI, etc. All of these produce a ValueTree as the output.
API: ValueTree
Example: examples/example_value_tree.cpp
A ValueTree represents a tree-structured data, which can recursively contain multiple child ValueTrees (or called subtrees).
Each ValueTree has 4 possible node states:
EMPTY
: Representing an empty tree with no value or child nodes. Can be implicitly recognized as false.ARRAY
: Representing an ArrayNode with several child ValueTrees.OBJECT
: Representing an ObjectNode with several child ValueTrees, each child has a corresponding key.VALUE
: Representing a leaf node with a value, wrapped by a ValueNode object, which must be one of the following 4 types:NONE
,BOOL
,NUMBER
,STRING
.
Obviously, the design of the ValueTree class refers to the structure of JSON.
Node APIs of ValueTree:
- [READ] To get the state, use
isXxx()
functions:state()
,isEmpty()
,isValue()
,isArray()
,isObject()
- [WRITE] To set the state, use
asXxx()
functions:clear()
,asValue()
,asArray()
,asObject()
- [READ] To get the node object, use
getXxx()
functions:getValue()
,getArray()
,getObject()
- [READ] To get the value, use
value<TypeTag>()
function.
Addressing and assignment APIs of ValueTree:
- [READ] To get the subtree under a path, use
subTree({path})
function. - [WRITE] To set the subtree under a path, use
operator[]({path})
function. - [READ] To get the node object under a path, use
getXxx({path})
functions:getValue({path})
,getArray({path})
,getObject({path})
- [READ] To get the value under a path, use
value<TypeTag>({path})
function.
It can be seen that ValueTree provides multiple levels of APIs. Some non-rigorous formulas:
getValue({path}) ≈ subTree({path}) + getValue()
value<TypeTag>({path}) ≈ getValue({path}) + value<TypeTag>()
≈ subTree({path}) + getValue() + value<TypeTag>()
For example, you can construct a ValueTree in the following way:
using namespace c2p;
ValueTree tree;
// normal root level key-value pair
tree["name"] = "Alice";
tree["age"] = 20;
tree["job"] = NONE;
// object (method 1)
tree["info"]["phone"] = "123-456-7890";
tree["info"]["email"] = "alice@fake.com";
// object (method 2)
auto& address = tree["info"]["address"].asObject();
address["contry"] = "USA";
address["city"] = "New York";
address["zip"] = 10001;
// array (method 1)
tree["family"] = ValueTree::from(std::vector{
"Grandpa",
"Grandma",
"Dad",
"Mom",
});
// array (method 2)
auto& roommates = tree["roommates"].asArray();
roommates = {
ValueNode("Bob"),
ValueNode("Charlie"),
ValueNode("David"),
};
roommates.emplace_back("Eve");
roommates.emplace_back("Frank");
Equivalent JSON:
{
"name": "Alice",
"age": 20,
"job": null,
"info": {
"phone": "123-456-7890",
"email": "alice@fake.com",
"address": {
"contry": "USA",
"city": "New York",
"zip": 10001
}
},
"family": ["Grandpa", "Grandma", "Dad", "Mom"],
"roommates": ["Bob", "Charlie", "David", "Eve", "Frank"]
}
To read a const ValueTree, you can use the following methods:
using namespace c2p;
// Assume you have a const ValueTree object
const ValueTree& constTree = json::parse(R"({
"name": "Alice",
"age": 20,
"job": null,
"info": {
"phone": "123-456-7890",
"email": "alice@fake.com",
"address": {
"contry": "USA",
"city": "New York",
"zip": 10001
}
},
"family": ["Grandpa", "Grandma", "Dad", "Mom"],
"roommates": ["Bob", "Charlie", "David", "Eve", "Frank"]
})");
// specify the key path
const auto phone = constTree.value<TypeTag::STRING>("info", "phone");
if (phone) {
std::cout << "Phone: " << *phone << std::endl;
}
// the index of the array is also supported
const auto bestFriend = constTree.value<TypeTag::STRING>("roommates", 2);
if (bestFriend) {
std::cout << "Best friend: " << *bestFriend << std::endl;
}
// if you already get a leaf node, you can use `value()` directly
const auto leaf = constTree.subTree("name");
assert(leaf && leaf->isValue());
const auto name = leaf->value<TypeTag::STRING>();
if (name) {
std::cout << "Name: " << *name << std::endl;
}
// traverse an array:
const auto family = constTree.getArray("family");
if (family) {
std::cout << "Family:" << std::endl;
for (const auto& roommate: *family) {
assert(roommate.isValue() && roommate.getValue()->isString());
std::cout << "- " << *roommate.value<TypeTag::STRING>() << std::endl;
}
}
// ## Output:
// Phone: 123-456-7890
// Best friend: David
// Name: Alice
// Family:
// - Grandpa
// - Grandma
// - Dad
// - Mom
For more information, please refer to the example: examples/example_value_tree.cpp
API: JSON serialization/deserialization
Example: examples/example_json.cpp
Parse JSON string into ValueTree, or serialize ValueTree into JSON string.
Extended JSON Grammar:
- Allow trailing comma in arrays and objects.
- Allow '+' sign for positive numbers.
- Allow single-line comment starts with "//".
Example:
// allow comment
{
"numbers": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
, +11, // allow '+' for positive numbers
-12, 13.14, 15.16e+17, 18.19e-20
],
"sensor1": { "enable": true },
"sensor2": { "enable": false },
"sensor3": { "enable": true },
"array": [ "sadfsafs",
"asdfasdf\nsadfsadffsadf\u0040"
, // allow trailing comma
], // allow trailing comma
// allow comment
}
API: INI serialization/deserialization
Example: examples/example_ini.cpp
Parse INI string into ValueTree, or serialize ValueTree into INI string.
Extended INI Grammar:
- Allow single-line comments starting with ';' or '#'.
- Allow no-section key-value pairs at the beginning.
- Allow empty value string even without quotes.
Example:
; comment starts with ';'
# comment starts with '#'
; allow no-section key-value pairs at the beginning
name=John Doe
age= 30
city = New York
[ section 1 ] ; allow spaces before and after section header string
email = "name\u0040fake.com" ; same as "name@fake.com"
home addr = 银河系 - 太阳系 - 地球: 北极点
[""] ; allow quoted string as empty section
empty info =
empty info2 = ; allow empty value string even without quotes
; comment starts with ';'
# comment starts with '#'
"" = value of empty key ; allow quoted string as empty key
Equivalent JSON:
{
"name": "John Doe",
"age": "30",
"city": "New York",
"section 1": {
"email": "name@fake.com",
"home addr": "银河系 - 太阳系 - 地球: 北极点"
},
"": {
"empty info": "",
"empty info2": "",
"": "value of empty key"
}
}
API: command-line argument parsing
Example: examples/example_cli.cpp
Parse CLI arguments into ValueTree.
Support recursive nested subcommands, and three types of arguments can be used:
- flag arguments
- value arguments
- positional arguments
You need to define a CommandGroup
first, which can help you construct a CLI parser. The construction process will check whether the configuration you defined is legal.
If the parser is constructed successfully, you can call the parse
method of the parser to parse the arguments. You can also call the getHelp
method to automatically generate a help string for interactive printing.
Example:
c2p::cli::CommandGroup cg = {
.command = "myprog",
.description = "Write the description of your program here.",
.flagArgs = {
{ .name = "version", .shortName = 'v', .description = "Show version information." },
{ .name = "help", .shortName = 'h', .description = "Show help information." },
},
.valueArgs = {
{
.name = "width",
.shortName = 'w',
.typeTag = c2p::TypeTag::NUMBER,
.required = true,
.description = "Specify the width of the image."
},
{
.name = "height",
.typeTag = c2p::TypeTag::NUMBER,
.description = "Specify the height of the image."
},
{
.name = "output",
.shortName = 'o',
.typeTag = c2p::TypeTag::STRING,
.description = "Specify output file path."
},
},
.minPositionalArgNum = 2,
.maxPositionalArgNum = 6,
};
const auto parser = c2p::cli::Parser::constructFrom(cg);
std::cout << (*(*parser).getHelp()) << std::endl;
And you will get the help string like this:
Usage:
myprog -w <NUMBER> [--height <NUMBER>] [-o <STRING>] [-v] [-h] <positionalArg0> <positionalArg1> [positionalArg2...5]
Write the description of your program here.
Flag Arguments:
-v, --version
Show version information.
-h, --help
Show help information.
Required Value Arguments:
-w, --width <NUMBER>
Specify the width of the image.
Optional Value Arguments:
--height <NUMBER>
Specify the height of the image.
-o, --output <STRING>
Specify output file path.
Positional Arguments:
Need 2 ~ 6 positional argument(s).
Assume you have the following CLI arguments:
myprog -v -w 1920 -h -o output.png input1.jpg input2.jpg
You can parse them into a ValueTree like this (print as JSON):
{
"command": "myprog",
"flagArgs": [
"version",
"help"
],
"positionalArgs": [
"input1.jpg",
"input2.jpg"
],
"valueArgs": {
"output": "output.png",
"width": 1920
}
}
There is a more complete example in examples/example_cli.cpp.