Skip to content

Commit 2f5186b

Browse files
Feature/control append (#48)
* (wip) initial attempt to parse tags * get update/replace/append merge semantics right * add compositing executable * add extensible tag processing * clean up logger behavior and switch test variable * Update config_utilities/include/config_utilities/internal/yaml_utils.h Co-authored-by: Lukas Schmid <[email protected]> * Update config_utilities/src/tag_processors.cpp Co-authored-by: Lukas Schmid <[email protected]> * better docstring * revert reference removal * add help message and parsing * document tag behavior * don't use special help shortopt * syntax coloring * touch up grammar --------- Co-authored-by: Lukas Schmid <[email protected]>
1 parent e8daab7 commit 2f5186b

16 files changed

+661
-40
lines changed

config_utilities/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ add_library(
3939
src/path.cpp
4040
src/settings.cpp
4141
src/string_utils.cpp
42+
src/tag_processors.cpp
4243
src/validation.cpp
4344
src/visitor.cpp
4445
src/yaml_parser.cpp
@@ -54,6 +55,9 @@ target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
5455
set_property(TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE 1)
5556
add_library(config_utilities::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
5657

58+
add_executable(composite-configs app/composite_configs.cpp)
59+
target_link_libraries(composite-configs ${PROJECT_NAME})
60+
5761
if(ENABLE_roscpp)
5862
target_link_libraries(${PROJECT_NAME} PUBLIC ${roscpp_LIBRARIES})
5963
target_include_directories(${PROJECT_NAME} PUBLIC ${roscpp_INCLUDE_DIRS})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include <iostream>
2+
3+
#include <config_utilities/parsing/commandline.h>
4+
5+
const std::string help_msg =
6+
R"""(Usage: composite-config [--config-utilities-yaml YAML_TOKEN ...]... [--config-utilities-file FILEPATH[@NAMESPACE]]...
7+
8+
Merges the input YAML values from left to right and outputs the resulting composite YAML to stdout.
9+
Invalid YAML or missing files get dropped during compositing.
10+
11+
Options:
12+
-h/--help: Show this message.
13+
--config-utilities-yaml: Takes an arbitrary set of tokens that form a valid YAML string.
14+
Spaces are not required to be escaped, so `--config-utilities foo: value`
15+
is parsed the same as `--config-utilities 'foo: value'`. Can be specified
16+
multiple times.
17+
--config-utilities-file: Takes a filepath to YAML to load and composite. The YAML can optionally
18+
be namespaced by `@NAMESPACE` where 'FILE@a/b' maps to
19+
'{a: {b: FILE_CONTENTS}}'. Can be specified multiple times.
20+
21+
Example:
22+
> echo "{a: 42, bar: hello}" > /tmp/test_in.yaml
23+
> composite-configs --config-utilities-yaml "{foo: {bar: value, b: -1.0}}" --config-utilities-file /tmp/test_in.yaml@foo
24+
{foo: {bar: hello, b: -1.0, a: 42}}
25+
26+
See https://github.com/MIT-SPARK/config_utilities/blob/main/docs/Parsing.md#parse-from-the-command-line
27+
for more information.
28+
)""";
29+
30+
int main(int argc, char* argv[]) {
31+
config::internal::ParserInfo info;
32+
auto result = config::internal::loadFromArguments(argc, argv, false, &info);
33+
if (info.help_present) {
34+
std::cerr << help_msg << std::endl;
35+
return 1;
36+
}
37+
38+
switch (result.Type()) {
39+
case YAML::NodeType::Null:
40+
case YAML::NodeType::Undefined:
41+
default:
42+
break;
43+
case YAML::NodeType::Scalar:
44+
case YAML::NodeType::Sequence:
45+
case YAML::NodeType::Map:
46+
std::cout << result << std::endl;
47+
break;
48+
}
49+
50+
return 0;
51+
}

config_utilities/cmake/HandleInstall.cmake

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
include(GNUInstallDirs)
22
install(
3-
TARGETS ${PROJECT_NAME}
3+
TARGETS ${PROJECT_NAME} composite-configs
44
EXPORT config_utilities-targets
55
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
6-
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
6+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
7+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
78
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
89
install(
910
EXPORT config_utilities-targets

config_utilities/include/config_utilities/internal/yaml_utils.h

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,30 @@
4242

4343
namespace config::internal {
4444

45+
enum class MergeMode {
46+
//! @brief Combine the two trees, recursing into matching sequence entries
47+
UPDATE,
48+
//! @brief Combine the two trees, appending right sequences into the left
49+
APPEND,
50+
//! @brief Combine the two trees, replacing left sequences with the right
51+
REPLACE
52+
};
53+
4554
/**
46-
* @brief Merges node b into a, overwriting values previously defined in a if they can not be
47-
* merged. Modifies node a, whereas b is const. Sequences can optionally be appended together at the same level of the
48-
* YAML tree.
55+
* @brief Merges node b into a with conflicting keys handled by choice of mode
56+
*
57+
* Recurses through the YAML "tree" of b, adding all non-conflicting nodes to a. Conflicting nodes (i.e. map keys or
58+
* shared indices in sequences that already exist in a) are handled according to the mode selection. For `REPLACE`, any
59+
* conflicting node stops the recursion, and the conflicting node is replaced by the value in b. For 'APPEND', any
60+
* conflicting sequence node will stop the recursion and cause the entire contents of the node in b to be append to the
61+
* node in a. For 'UPDATE', any conflicting map or sequence node recursively calls `mergeYamlNodes` with the children of
62+
* the conflicting nodes as the new roots.
63+
*
64+
* @param a Node to merge into ("left" node and will be changed).
65+
* @param b Node to merge from ("right" node and remains constant).
66+
* @param mode Mode to use when merging
4967
*/
50-
void mergeYamlNodes(YAML::Node& a, const YAML::Node& b, bool extend_sequences = false);
68+
void mergeYamlNodes(YAML::Node& a, const YAML::Node& b, MergeMode mode = MergeMode::UPDATE);
5169

5270
/**
5371
* @brief Get a pointer to the final node of the specified namespace if it exists, where each map in the yaml is

config_utilities/include/config_utilities/parsing/commandline.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,18 @@
4545
namespace config {
4646
namespace internal {
4747

48+
struct ParserInfo {
49+
bool help_present = false;
50+
};
51+
4852
/**
4953
* @brief Parse and collate YAML node from arguments, optionally removing arguments
5054
* @param argc Number of command line arguments
5155
* @param argv Command line argument strings
56+
* @param remove_args Remove any recognized arguments from argc and argv
57+
* @param parser_info Optional information for parser used by composite executable
5258
*/
53-
YAML::Node loadFromArguments(int& argc, char* argv[], bool remove_args);
59+
YAML::Node loadFromArguments(int& argc, char* argv[], bool remove_args, ParserInfo* parser_info = nullptr);
5460

5561
/**
5662
* @brief Parse and collate YAML node from arguments
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/** -----------------------------------------------------------------------------
2+
* Copyright (c) 2023 Massachusetts Institute of Technology.
3+
* All Rights Reserved.
4+
*
5+
* AUTHORS: Lukas Schmid <[email protected]>, Nathan Hughes <[email protected]>
6+
* AFFILIATION: MIT-SPARK Lab, Massachusetts Institute of Technology
7+
* YEAR: 2023
8+
* LICENSE: BSD 3-Clause
9+
*
10+
* Redistribution and use in source and binary forms, with or without
11+
* modification, are permitted provided that the following conditions are met:
12+
*
13+
* 1. Redistributions of source code must retain the above copyright notice, this
14+
* list of conditions and the following disclaimer.
15+
*
16+
* 2. Redistributions in binary form must reproduce the above copyright notice,
17+
* this list of conditions and the following disclaimer in the documentation
18+
* and/or other materials provided with the distribution.
19+
*
20+
* 3. Neither the name of the copyright holder nor the names of its
21+
* contributors may be used to endorse or promote products derived from
22+
* this software without specific prior written permission.
23+
*
24+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
* -------------------------------------------------------------------------- */
35+
36+
#pragma once
37+
38+
#include <yaml-cpp/yaml.h>
39+
40+
namespace config {
41+
42+
struct TagProcessor {
43+
virtual ~TagProcessor() = default;
44+
45+
/**
46+
* @brief Attempt to replace node with corresponding tag
47+
* @param[in] node Node to perform substitution on
48+
*/
49+
virtual void processNode(YAML::Node node) const = 0;
50+
};
51+
52+
class RegisteredTags {
53+
public:
54+
template <typename T>
55+
struct Registration {
56+
explicit Registration(const std::string& tag);
57+
};
58+
59+
~RegisteredTags() = default;
60+
61+
static const TagProcessor* getEntry(const std::string& tag);
62+
63+
private:
64+
template <typename T>
65+
friend struct Registration;
66+
67+
static RegisteredTags& instance();
68+
69+
static void addEntry(const std::string& tag, std::unique_ptr<TagProcessor>&& proc);
70+
71+
RegisteredTags();
72+
static std::unique_ptr<RegisteredTags> s_instance_;
73+
std::map<std::string, std::unique_ptr<TagProcessor>> entries_;
74+
};
75+
76+
template <typename T>
77+
RegisteredTags::Registration<T>::Registration(const std::string& tag) {
78+
RegisteredTags::addEntry(tag, std::make_unique<T>());
79+
}
80+
81+
/**
82+
* @brief Attempts to replace the node `!env VARNAME` with the value of VARNAME from the environment
83+
*/
84+
struct EnvTag : public TagProcessor {
85+
void processNode(YAML::Node node) const override;
86+
};
87+
88+
/**
89+
* @brief Iterate through the node, resolving tags
90+
* @param[in] node Node to resolve tags for
91+
*/
92+
void resolveTags(YAML::Node node);
93+
94+
} // namespace config

config_utilities/src/commandline.cpp

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@
3636
#include "config_utilities/parsing/commandline.h"
3737

3838
#include <filesystem>
39-
#include <sstream>
4039
#include <regex>
40+
#include <sstream>
4141

4242
#include "config_utilities/internal/logger.h"
4343
#include "config_utilities/internal/yaml_utils.h"
44+
#include "config_utilities/tag_processors.h"
4445

4546
namespace config::internal {
4647

@@ -60,6 +61,8 @@ struct CliParser {
6061
bool is_file;
6162
};
6263

64+
ParserInfo info;
65+
6366
static constexpr auto FILE_OPT = "--config-utilities-file";
6467
static constexpr auto YAML_OPT = "--config-utilities-yaml";
6568

@@ -143,11 +146,25 @@ void removeSpan(int& argc, char* argv[], const Span& span) {
143146
CliParser& CliParser::parse(int& argc, char* argv[], bool remove_args) {
144147
std::vector<Span> spans;
145148

149+
bool found_separator = false;
146150
int i = 0;
147151
while (i < argc) {
148152
const std::string curr_opt(argv[i]);
149153
std::string error;
150154
std::optional<Span> curr_span;
155+
if ((curr_opt == "-h" || curr_opt == "--help") && !found_separator) {
156+
info.help_present = true;
157+
spans.emplace_back(Span{i, 0, curr_opt});
158+
++i;
159+
continue;
160+
}
161+
162+
if (curr_opt == "--" && !found_separator) {
163+
found_separator = true;
164+
spans.emplace_back(Span{i, 0, curr_opt});
165+
++i;
166+
}
167+
151168
if (curr_opt == FILE_OPT || curr_opt == YAML_OPT) {
152169
curr_span = getSpan(argc, argv, i, curr_opt == YAML_OPT, error);
153170
}
@@ -168,6 +185,10 @@ CliParser& CliParser::parse(int& argc, char* argv[], bool remove_args) {
168185
}
169186

170187
for (const auto& span : spans) {
188+
if (span.key != FILE_OPT && span.key != YAML_OPT) {
189+
continue; // skip any spans for single options
190+
}
191+
171192
entries.push_back(Entry{span.extractTokens(argc, argv), span.key == FILE_OPT});
172193
}
173194

@@ -229,8 +250,14 @@ YAML::Node nodeFromLiteralEntry(const CliParser::Entry& entry) {
229250
return node;
230251
}
231252

232-
YAML::Node loadFromArguments(int& argc, char* argv[], bool remove_args) {
253+
YAML::Node loadFromArguments(int& argc, char* argv[], bool remove_args, ParserInfo* info) {
233254
const auto parser = CliParser().parse(argc, argv, remove_args);
255+
if (info) {
256+
*info = parser.info;
257+
if (info->help_present) {
258+
return YAML::Node();
259+
}
260+
}
234261

235262
YAML::Node node;
236263
for (const auto& entry : parser.entries) {
@@ -242,9 +269,10 @@ YAML::Node loadFromArguments(int& argc, char* argv[], bool remove_args) {
242269
}
243270

244271
// no-op for invalid parsed node
245-
internal::mergeYamlNodes(node, parsed_node, true);
272+
internal::mergeYamlNodes(node, parsed_node, MergeMode::APPEND);
246273
}
247274

275+
resolveTags(node);
248276
return node;
249277
}
250278

config_utilities/src/config_context.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void Context::update(const YAML::Node& other, const std::string& ns) {
4949
auto node = YAML::Clone(other);
5050
moveDownNamespace(node, ns);
5151
// default behavior of context is to act like the ROS1 param server and extend sequences
52-
mergeYamlNodes(context.contents_, node, true);
52+
mergeYamlNodes(context.contents_, node, MergeMode::APPEND);
5353
}
5454

5555
void Context::clear() { instance().contents_ = YAML::Node(); }

0 commit comments

Comments
 (0)