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