1 // This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
2 // the main distribution directory for license terms and copyright or visit
3 // https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
4
5 #include "caf/config_option_set.hpp"
6
7 #include <map>
8 #include <set>
9
10 #include "caf/config_option.hpp"
11 #include "caf/config_value.hpp"
12 #include "caf/detail/algorithms.hpp"
13 #include "caf/expected.hpp"
14
15 using std::string;
16
17 namespace {
18
19 struct string_builder {
20 std::string result;
21 };
22
operator <<(string_builder & builder,char ch)23 string_builder& operator<<(string_builder& builder, char ch) {
24 builder.result += ch;
25 return builder;
26 }
27
operator <<(string_builder & builder,caf::string_view str)28 string_builder& operator<<(string_builder& builder, caf::string_view str) {
29 builder.result.append(str.data(), str.size());
30 return builder;
31 }
32
insert(string_builder & builder,size_t count,char ch)33 void insert(string_builder& builder, size_t count, char ch) {
34 builder.result.insert(builder.result.end(), count, ch);
35 }
36
37 } // namespace
38
39 namespace caf {
40
add(config_option opt)41 config_option_set& config_option_set::add(config_option opt) {
42 opts_.emplace_back(std::move(opt));
43 return *this;
44 }
45
help_text(bool global_only) const46 std::string config_option_set::help_text(bool global_only) const {
47 // <--- argument --------> <---- description --->
48 // (-w|--write) <string> : output file
49 auto build_argument = [](const config_option& x) {
50 string_builder sb;
51 if (x.short_names().empty()) {
52 sb << " --";
53 if (!x.has_flat_cli_name())
54 sb << x.category() << '.';
55 sb << x.long_name();
56 if (!x.is_flag())
57 sb << '=';
58 } else {
59 sb << " (";
60 for (auto c : x.short_names())
61 sb << '-' << c << '|';
62 sb << "--";
63 if (!x.has_flat_cli_name())
64 sb << x.category() << '.';
65 sb << x.long_name() << ") ";
66 }
67 if (!x.is_flag())
68 sb << "<" << x.type_name() << '>';
69 return std::move(sb.result);
70 };
71 // Sort argument + description by category.
72 using pair = std::pair<std::string, option_pointer>;
73 std::set<string_view> categories;
74 std::multimap<string_view, pair> args;
75 size_t max_arg_size = 0;
76 for (auto& opt : opts_) {
77 // We treat all options with flat name as-if the category was 'global'.
78 if (!global_only || opt.has_flat_cli_name()) {
79 auto arg = build_argument(opt);
80 max_arg_size = std::max(max_arg_size, arg.size());
81 string_view category = "global";
82 if (!opt.has_flat_cli_name())
83 category = opt.category();
84 categories.emplace(category);
85 args.emplace(category, std::make_pair(std::move(arg), &opt));
86 }
87 }
88 // Build help text by iterating over all categories in the multimap.
89 string_builder builder;
90 for (auto& category : categories) {
91 auto rng = args.equal_range(category);
92 builder << category << " options:\n";
93 for (auto i = rng.first; i != rng.second; ++i) {
94 builder << i->second.first;
95 CAF_ASSERT(max_arg_size >= i->second.first.size());
96 insert(builder, max_arg_size - i->second.first.size(), ' ');
97 builder << " : " << i->second.second->description() << '\n';
98 }
99 builder << '\n';
100 }
101 return std::move(builder.result);
102 }
103
104 namespace {
105
select_entry(settings & config,string_view key)106 settings& select_entry(settings& config, string_view key){
107 auto sep = key.find('.');
108 if (sep == string_view::npos)
109 return config[key].as_dictionary();
110 auto prefix = key.substr(0, sep);
111 auto suffix = key.substr(sep + 1);
112 return select_entry(config[prefix].as_dictionary(), suffix);
113 }
114
115 } // namespace
116
parse(settings & config,argument_iterator first,argument_iterator last) const117 auto config_option_set::parse(settings& config, argument_iterator first,
118 argument_iterator last) const
119 -> std::pair<pec, argument_iterator> {
120 // Sanity check.
121 if (first == last)
122 return {pec::success, last};
123 // Parses an argument.
124 using iter = string::const_iterator;
125 auto consume = [&](const config_option& opt, iter arg_begin, iter arg_end) {
126 auto to_pec_code = [](const error& err) {
127 if (err.category() == type_id_v<pec>)
128 return static_cast<pec>(err.code());
129 else
130 return pec::invalid_argument;
131 };
132 // Extract option name and category.
133 auto opt_name = opt.long_name();
134 auto opt_ctg = opt.category();
135 // Try inserting a new submap into the config or fill existing one.
136 auto& entry = opt_ctg == "global" ? config : select_entry(config, opt_ctg);
137 // Flags only consume the current element.
138 if (opt.is_flag()) {
139 if (arg_begin == arg_end) {
140 config_value cfg_true{true};
141 if (auto err = opt.sync(cfg_true); !err) {
142 entry[opt_name] = cfg_true;
143 return pec::success;
144 } else {
145 return to_pec_code(err);
146 }
147 } else {
148 return pec::invalid_argument;
149 }
150 } else {
151 if (arg_begin != arg_end) {
152 auto arg_size = static_cast<size_t>(std::distance(arg_begin, arg_end));
153 config_value val{string_view{std::addressof(*arg_begin), arg_size}};
154 if (auto err = opt.sync(val); !err) {
155 entry[opt_name] = std::move(val);
156 return pec::success;
157 } else {
158 return to_pec_code(err);
159 }
160 } else {
161 return pec::missing_argument;
162 }
163 }
164 };
165 // We loop over the first N-1 values, because we always consider two
166 // arguments at once.
167 for (auto i = first; i != last;) {
168 if (i->size() < 2)
169 return {pec::not_an_option, i};
170 if (*i== "--")
171 return {pec::success, std::next(first)};
172 if (i->compare(0, 2, "--") == 0) {
173 // Long options use the syntax "--<name>=<value>" and consume only a
174 // single argument.
175 auto npos = std::string::npos;
176 auto assign_op = i->find('=');
177 auto name = assign_op == npos ? i->substr(2)
178 : i->substr(2, assign_op - 2);
179 auto opt = cli_long_name_lookup(name);
180 if (opt == nullptr)
181 return {pec::not_an_option, i};
182 auto code = consume(*opt,
183 assign_op == npos
184 ? i->end()
185 : i->begin() + static_cast<ptrdiff_t>(assign_op + 1),
186 i->end());
187 if (code != pec::success)
188 return {code, i};
189 ++i;
190 } else if (i->front() == '-') {
191 // Short options have three possibilities.
192 auto opt = cli_short_name_lookup((*i)[1]);
193 if (opt == nullptr)
194 return {pec::not_an_option, i};
195 if (opt->is_flag()) {
196 // 1) "-f" for flags, consumes one argument
197 auto code = consume(*opt, i->begin() + 2, i->end());
198 if (code != pec::success)
199 return {code, i};
200 ++i;
201 } else {
202 if (i->size() == 2) {
203 // 2) "-k <value>", consumes both arguments
204 auto j = std::next(i);
205 if (j == last) {
206 return {pec::missing_argument, j};
207 }
208 auto code = consume(*opt, j->begin(), j->end());
209 if (code != pec::success)
210 return {code, i};
211 std::advance(i, 2);
212 } else {
213 // 3) "-k<value>" (no space), consumes one argument
214 auto code = consume(*opt, i->begin() + 2, i->end());
215 if (code != pec::success)
216 return {code, i};
217 ++i;
218 }
219 }
220 } else {
221 // No leading '-' found on current position.
222 return {pec::not_an_option, i};
223 }
224 }
225 return {pec::success, last};
226 }
227
228 config_option_set::parse_result
parse(settings & config,const std::vector<string> & args) const229 config_option_set::parse(settings& config,
230 const std::vector<string>& args) const {
231 return parse(config, args.begin(), args.end());
232 }
233
234 config_option_set::option_pointer
cli_long_name_lookup(string_view name) const235 config_option_set::cli_long_name_lookup(string_view name) const {
236 // Extract category and long name.
237 string_view category;
238 string_view long_name;
239 auto sep = name.find_last_of('.');
240 if (sep == string::npos) {
241 long_name = name;
242 } else {
243 category = name.substr(0, sep);
244 long_name = name.substr(sep + 1);
245 }
246 // Scan all options for a match.
247 auto category_match = [&](const config_option& opt) {
248 return sep == string::npos ? opt.has_flat_cli_name()
249 : opt.category() == category;
250 };
251 return detail::ptr_find_if(opts_, [&](const config_option& opt) {
252 return category_match(opt) && opt.long_name() == long_name;
253 });
254 }
255
256 config_option_set::option_pointer
cli_short_name_lookup(char short_name) const257 config_option_set::cli_short_name_lookup(char short_name) const {
258 return detail::ptr_find_if(opts_, [&](const config_option& opt) {
259 return opt.short_names().find(short_name) != string::npos;
260 });
261 }
262
263 config_option_set::option_pointer
qualified_name_lookup(string_view category,string_view long_name) const264 config_option_set::qualified_name_lookup(string_view category,
265 string_view long_name) const {
266 return detail::ptr_find_if(opts_, [&](const config_option& opt) {
267 return opt.category() == category && opt.long_name() == long_name;
268 });
269 }
270
271 config_option_set::option_pointer
qualified_name_lookup(string_view name) const272 config_option_set::qualified_name_lookup(string_view name) const {
273 auto sep = name.rfind('.');
274 if (sep == string::npos)
275 return nullptr;
276 return qualified_name_lookup(name.substr(0, sep), name.substr(sep + 1));
277 }
278
has_category(string_view category) const279 bool config_option_set::has_category(string_view category) const noexcept {
280 auto predicate = [category](const config_option& opt) {
281 return opt.category() == category;
282 };
283 return std::any_of(opts_.begin(), opts_.end(), predicate);
284 }
285
286 } // namespace caf
287