cli.h Source File

CPP API: cli.h Source File
cli.h
Go to the documentation of this file.
1 /*
2 * Copyright (C) 2020-2026 MEmilio
3 *
4 * Authors: René Schmieding
5 *
6 * Contact: Martin J. Kuehn <Martin.Kuehn@DLR.de>
7 *
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20 #ifndef MIO_IO_CLI_H
21 #define MIO_IO_CLI_H
22 
23 #include "memilio/config.h" // IWYU pragma: keep
24 
25 #ifdef MEMILIO_HAS_JSONCPP
26 
27 #include "memilio/io/io.h"
33 
34 #include <cassert>
35 #include <iostream>
36 #include <memory>
37 #include <span>
38 #include <sstream>
39 #include <string>
40 #include <string_view>
41 #include <unordered_map>
42 #include <vector>
43 
44 namespace mio
45 {
46 
47 namespace cli
48 {
49 
50 namespace details
51 {
52 
54 template <class Parameter>
55 const std::string get_name(const Parameter& p)
56 {
57  return p.name();
58 }
59 
61 template <class Parameter>
62 const std::string get_alias(const Parameter& p)
63 {
64  if constexpr (requires { p.alias(); }) {
65  return p.alias();
66  }
67  else {
68  return "";
69  }
70 }
71 
73 template <class Parameter>
74 const std::string get_description(const Parameter& p)
75 {
76  if constexpr (requires { p.description(); }) {
77  return p.description();
78  }
79  else {
80  return "";
81  }
82 }
83 
85 template <class Parameter>
86 constexpr bool get_is_required(const Parameter& p)
87 {
88  if constexpr (requires { p.is_required(); }) {
89  return p.is_required();
90  }
91  else {
92  return false;
93  }
94 }
95 
97 struct DatalessParameter {
98  std::string name, alias = "", description = "";
99  bool is_required = false;
100 };
101 
103 struct OptionalFields {
104  std::string alias = "", description = "";
105  bool is_required = false;
106 };
107 
109 enum class IdentifierType
110 {
111  Name, // e.g. "--help"
112  Alias, // e.g. "-h"
113  Raw // either a name or alias without dashes, e.g. "help"
114 };
115 
121 class Identifier
122 {
123 private:
125  Identifier(std::string str, IdentifierType t)
126  : string(std::move(str))
127  , type(t)
128  {
129  }
130 
131 public:
137  static IOResult<Identifier> parse(const std::string& str)
138  {
139  if (is_name(str)) {
140  return mio::success(Identifier(str, IdentifierType::Name));
141  }
142  else if (is_alias(str)) {
143  return mio::success(Identifier(str, IdentifierType::Alias));
144  }
145  else {
146  return mio::failure(mio::StatusCode::InvalidValue, "Expected an option, got \"" + str + "\".");
147  }
148  }
149 
155  static Identifier make_raw(const std::string& raw_name)
156  {
157  return {raw_name, IdentifierType::Raw};
158  }
159 
161  static bool is_name(std::string_view unverified_id)
162  {
163  // check for two dashes and at least one more character
164  return (unverified_id.size() > 2) && (unverified_id[0] == '-' && unverified_id[1] == '-');
165  }
166 
168  static bool is_alias(std::string_view unverified_id)
169  {
170  // check for exactly one dash and at least one more non-dash character
171  return (unverified_id.size() > 1) && (unverified_id[0] == '-') && (unverified_id[1] != '-');
172  }
173 
175  static bool is_option(std::string_view unverified_id)
176  {
177  return is_name(unverified_id) || is_alias(unverified_id);
178  }
179 
181  inline const std::string_view strip() const
182  {
183  switch (type) {
184  case IdentifierType::Name:
185  return std::string_view{string}.substr(2);
186  case IdentifierType::Alias:
187  return std::string_view{string}.substr(1);
188  case IdentifierType::Raw:
189  return std::string_view{string};
190  }
191  assert(false && "Invalid IdentifierType");
192  // this return is practically inaccessible, but needed to avoid a "reached end of non-void function" warning
193  return std::string_view{};
194  }
195 
197  inline bool matches_parameter(const DatalessParameter& p) const
198  {
199  switch (type) {
200  case IdentifierType::Name:
201  return p.name == strip();
202  case IdentifierType::Alias:
203  return p.alias == strip();
204  case IdentifierType::Raw:
205  return (p.name == string) || (string.size() > 0 && p.alias == string);
206  }
207  assert(false && "Invalid IdentifierType");
208  // this return is practically inaccessible, but needed to avoid a "reached end of non-void function" warning
209  return false;
210  }
211 
212  std::string string;
213  IdentifierType type;
214 };
215 
217 struct PresetOptions {
218  const inline static DatalessParameter help{
219  .name = "help", .alias = "h", .description = "Show this dialogue and exit.", .is_required = false};
220 
221  const inline static DatalessParameter print_option{
222  .name = "print_option",
223  .alias = "",
224  .description =
225  "Use with parameter option name(s) without \"--\" as value(s). Prints the current values of specified "
226  "options in their correct json format, then exits.",
227  .is_required = false};
228 
229  const inline static DatalessParameter read_from_json{
230  .name = "read_from_json",
231  .alias = "",
232  .description =
233  "Takes a filepath as value. Reads and assigns parameter option values from the specified json file.",
234  .is_required = false};
235 
236  const inline static DatalessParameter write_to_json{
237  .name = "write_to_json",
238  .alias = "",
239  .description =
240  "Takes a filepath as value. Writes current values of all parameter options to the specified json file.",
241  .is_required = false};
242 
243  const inline static std::vector<DatalessParameter> all_presets{help, print_option, read_from_json, write_to_json};
244 };
245 
252 class AbstractParameter : public DatalessParameter
253 {
254 public:
262  template <class Type>
263  AbstractParameter(mio::Tag<Type>, const DatalessParameter& p, std::shared_ptr<void>&& value)
264  : DatalessParameter(p)
265  , m_data(std::move(value))
266  , m_serialize([](const std::shared_ptr<void>& param) -> IOResult<Json::Value> {
267  return mio::serialize_json(*static_cast<Type*>(param.get()));
268  })
269  , m_set([](std::shared_ptr<void>& param, const std::string& name, const Json::Value& json) -> IOResult<void> {
270  mio::unused(param); // prevent a "set, but unused" compiler warning
271  // deserialize the json value to the parameter's type, then assign it
272  auto param_result = mio::deserialize_json(json, mio::Tag<Type>());
273  if (param_result) {
274  // assign the json to the parameter
275  *(static_cast<Type*>(param.get())) = std::move(param_result.value());
276  return mio::success();
277  }
278  else { // deserialize failed
279  // insert more information to the error message
280  std::string msg = "While setting \"" + name + "\": " + param_result.error().message();
281  if (param_result.error().message() == "Json value is not a string.") {
282  msg += " Try using escaped quotes (\\\") around your input strings.";
283  }
284  return mio::failure(param_result.error().code(), msg);
285  }
286  })
287  {
288  }
289 
294  template <class Param>
295  AbstractParameter(mio::Tag<Param>, typename Param::Type& value)
296  : AbstractParameter(mio::Tag<typename Param::Type>{},
297  DatalessParameter{get_name(Param{}), get_alias(Param{}), get_description(Param{}),
298  get_is_required(Param{})},
299  std::shared_ptr<void>(static_cast<void*>(&value), [](void*) {}))
300  {
301  }
302 
307  template <class Param>
308  AbstractParameter(mio::Tag<Param>, AbstractParameter& other)
309  : AbstractParameter(other)
310  {
311  }
312 
318  IOResult<Json::Value> get() const
319  {
320  auto json_result = m_serialize(m_data);
321  if (json_result) {
322  return json_result;
323  }
324  else {
325  std::string msg = "While getting \"" + name() + "\": " + json_result.error().message();
326  return mio::failure(json_result.error().code(), msg);
327  }
328  }
329 
335  IOResult<void> set(const Json::Value& value)
336  {
337  return m_set(m_data, name(), value);
338  }
339 
344  const std::string& name() const
345  {
346  return DatalessParameter::name;
347  }
348  const std::string& alias() const
349  {
350  return DatalessParameter::alias;
351  }
352  const std::string& description() const
353  {
354  return DatalessParameter::description;
355  }
356  bool is_required() const
357  {
358  return DatalessParameter::is_required;
359  }
363  void* data()
364  {
365  return m_data.get();
366  }
367 
368 private:
369  std::shared_ptr<void> m_data;
370  IOResult<Json::Value> (*m_serialize)(
371  const std::shared_ptr<void>&);
372  IOResult<void> (*m_set)(std::shared_ptr<void>&, const std::string&,
373  const Json::Value&);
374 };
375 
376 class AbstractSet
377 {
378 public:
379  using MapType = std::unordered_map<std::string_view, AbstractParameter&>;
380 
390  template <class Set>
391  static IOResult<AbstractSet> build(Set& parameter_set)
392  {
393  AbstractSet set(parameter_set);
394  BOOST_OUTCOME_TRY(fill_maps(set));
395  return mio::success(std::move(set));
396  }
397 
403  IOResult<Json::Value> get_param(const Identifier& id)
404  {
405  auto param = find(id);
406  if (!param) {
407  return mio::failure(param.error().code(), "Could not get parameter: " + param.error().message());
408  }
409  else {
410  return param.value()->second.get();
411  }
412  }
413 
420  IOResult<void> set_param(const Identifier& id, const std::string& args)
421  {
422  Json::Value js;
423  std::string errors;
424  std::stringstream args_stream(args);
425  Json::parseFromStream(Json::CharReaderBuilder{}, args_stream, &js, &errors);
426  // do not directly raise errors, to avoid hiding e.g. a "parameter not found"
427  return set_param(id, js, errors);
428  }
429 
437  IOResult<void> set_param(const Identifier& id, const Json::Value& value, const std::string& parse_errors = "")
438  {
439  auto param = find(id);
440  if (!param) {
441  return mio::failure(param.error().code(), "Could not set parameter: " + param.error().message());
442  }
443  else {
444  // try to set the value
445  IOResult<void> result = param.value()->second.set(value);
446  if (result) {
447  // mark as set. we reuse the is_required flag for this, to make checks and repeated CLI calls easier
448  param.value()->second.DatalessParameter::is_required = false;
449  return result;
450  }
451  else if (parse_errors.empty()) {
452  // no need to modify result if there are no parsing errors
453  return result;
454  }
455  else {
456  // append parsing errors to original error
457  return mio::failure(result.error().code(), result.error().message() + "\n" + parse_errors);
458  }
459  }
460  }
461 
463  bool contains(const Identifier& id)
464  {
465  return static_cast<bool>(find(id));
466  }
467 
472  std::vector<AbstractParameter>& parameters()
473  {
474  return m_parameters;
475  }
476  const std::vector<AbstractParameter>& parameters() const
477  {
478  return m_parameters;
479  }
482 private:
484  AbstractSet() = default;
485 
487  template <class... T, template <class...> class Set>
488  AbstractSet(Set<T...>& parameter_set)
489  : m_parameters(std::vector{AbstractParameter(mio::Tag<T>{}, parameter_set.template get<T>())...})
490  {
491  }
492 
494  AbstractSet(std::vector<AbstractParameter>& parameters)
495  : m_parameters(parameters)
496  {
497  }
498 
505  IOResult<MapType::iterator> find(const Identifier& id)
506  {
507  auto param_itr = m_map_by_alias.find(id.strip());
508  if (param_itr != m_map_by_alias.end()) {
509  return mio::success(param_itr);
510  }
511  param_itr = m_map_by_name.find(id.strip());
512  if (param_itr != m_map_by_name.end()) {
513  return mio::success(param_itr);
514  }
515  return mio::failure(mio::StatusCode::KeyNotFound, "No such option \"" + id.string + "\".");
516  }
517 
524  static IOResult<void> fill_maps(AbstractSet& set)
525  {
526  for (AbstractParameter& p : set.m_parameters) {
527  if (p.name().empty()) {
528  return mio::failure(mio::StatusCode::InvalidValue, "An option is missing a name.");
529  }
530  if (!set.m_map_by_name.insert({p.name(), p}).second) {
531  return mio::failure(mio::StatusCode::InvalidValue, "Options may not have duplicate names. "
532  "Found two instances of \"" +
533  p.name() + "\".");
534  }
535  if (!p.alias().empty() && !set.m_map_by_alias.insert({p.alias(), p}).second) {
536  return mio::failure(mio::StatusCode::InvalidValue, "Options may not have duplicate aliases. "
537  "Found two instances of \"" +
538  p.alias() + "\".");
539  }
540  }
541  return mio::success();
542  }
543 
544  std::vector<AbstractParameter> m_parameters;
545  MapType m_map_by_name;
546  MapType m_map_by_alias;
547 };
548 
557 void write_help(const std::string& executable_name, const AbstractSet& set,
558  const std::vector<std::string>& default_options, std::ostream& os);
559 
564 mio::IOResult<void> command_line_interface(const std::string& executable_name, const std::span<char*>& argv,
565  cli::details::AbstractSet& parameters,
566  const std::vector<std::string>& default_options);
567 
569 template <StringLiteral>
570 struct NamedType;
571 
576 template <class... Tags>
577 class ParameterSet
578 {
579  using Names = TypeList<type_at_index_t<0, Tags>...>;
580  using Types = TypeList<type_at_index_t<1, Tags>...>;
581 
582 public:
587  ParameterSet(std::vector<details::AbstractParameter>&& parameters)
588  : m_parameters(std::move(parameters))
589  {
590  }
591 
597  template <mio::StringLiteral Name>
598  inline auto& get()
599  {
600  constexpr size_t parameter_index = index_of_type_v<details::NamedType<Name>, Names>;
601  using ReturnType = type_at_index_t<parameter_index, Types>;
602  return *static_cast<ReturnType*>(m_parameters[parameter_index].data());
603  }
604 
610  template <class Tag>
611  inline details::AbstractParameter& get()
612  {
613  constexpr size_t parameter_index = index_of_type_v<Tag, Tags...>;
614  return m_parameters[parameter_index];
615  }
616 
617 private:
618  std::vector<details::AbstractParameter> m_parameters;
619 };
620 
626 mio::IOResult<void> write_abstract_set_to_file(mio::cli::details::AbstractSet& set, const std::string& filepath);
627 
633 mio::IOResult<void> read_abstract_set_from_file(mio::cli::details::AbstractSet& set, const std::string& filepath);
634 
635 } // namespace details
636 
638 template <class... Params>
639 class ParameterSetBuilder
640 {
641  // make private constructor accessible independent of template arguments
642  template <class...>
643  friend class ParameterSetBuilder;
644 
645 public:
647  ParameterSetBuilder() = default;
648 
663  template <mio::StringLiteral Name, class Type>
664  [[nodiscard]] inline auto add(Type&& initial_value, details::OptionalFields&& optionals = {}) &&
665  {
666  using ValueType = std::decay_t<Type>; // get base type in case Type was deduced and is e.g. const or &
667  // since we get *this as rvalue, we can move the parameters
668  auto new_params = std::move(m_parameters);
669  // create a new owning data pointer, stored as void*
670  std::shared_ptr<void> data(new ValueType(std::forward<Type>(initial_value)), std::default_delete<ValueType>{});
671  // create a new abstract parameter, then move all parameters to a new builder
672  new_params.emplace_back(Tag<ValueType>{},
673  details::DatalessParameter{.name = std::string(Name),
674  .alias = std::move(optionals).alias,
675  .description = std::move(optionals).description,
676  .is_required = std::move(optionals).is_required},
677  std::move(data));
678  return ParameterSetBuilder<Params..., TypeList<details::NamedType<Name>, ValueType>>{std::move(new_params)};
679  }
680 
682  [[nodiscard]] inline mio::cli::details::ParameterSet<Params...> build() &&
683  {
684  return mio::cli::details::ParameterSet<Params...>(std::move(m_parameters));
685  }
686 
687 private:
689  ParameterSetBuilder(std::vector<details::AbstractParameter>&& parameters)
690  : m_parameters(std::move(parameters))
691  {
692  }
693 
694  std::vector<details::AbstractParameter> m_parameters;
695 };
696 
697 } // namespace cli
698 
705 template <class Set>
706 IOResult<void> write_parameters_to_file(Set& parameters, const std::string& filepath)
707 {
708  BOOST_OUTCOME_TRY(auto&& set, cli::details::AbstractSet::build(parameters));
709  return write_abstract_set_to_file(set, filepath);
710 }
711 
719 template <class... Parameters, template <class...> class Set>
720 IOResult<void> read_parameters_from_file(Set<Parameters...>& parameters, const std::string& filepath)
721 {
722  BOOST_OUTCOME_TRY(auto&& set, cli::details::AbstractSet::build(parameters));
723  return read_abstract_set_from_file(set, filepath);
724 }
725 
761 template <class Set>
762 mio::IOResult<void> command_line_interface(const std::string& executable_name, const int argc, char** argv,
763  Set& parameters, const std::vector<std::string>& default_options = {})
764 {
765  // parse the parameters into an iterable format
766  BOOST_OUTCOME_TRY(auto&& set, cli::details::AbstractSet::build(parameters));
767  return cli::details::command_line_interface(executable_name, std::span(argv, argc), set, default_options);
768 }
769 
783 template <class... Parameters, template <class...> class Set = mio::ParameterSet>
784 mio::IOResult<Set<Parameters...>> command_line_interface(const std::string& executable_name, const int argc,
785  char** argv,
786  const std::vector<std::string>& default_options = {})
787 {
788  static_assert(sizeof...(Parameters) != 0, "At least one Parameter is required.");
789  // create a new parameter set, and pass it and all other arguments to the main cli function
790  Set<Parameters...> parameters{};
791  auto result = command_line_interface(executable_name, argc, argv, parameters, default_options);
792  // check the result, return parameters if appropriate
793  if (result) {
794  return mio::IOResult<Set<Parameters...>>(mio::success(std::move(parameters)));
795  }
796  else {
797  return mio::IOResult<Set<Parameters...>>(mio::failure(result.error()));
798  }
799 }
800 
801 } // namespace mio
802 
803 #endif // MEMILIO_HAS_JSONCPP
804 
805 #endif // MIO_IO_CLI_H
a set of parameters defined at compile time
Definition: parameter_set.h:205
trait_value< T >::RETURN_TYPE & value(T &x)
Definition: ad.hpp:3308
A collection of classes to simplify handling of matrix shapes in meta programming.
Definition: models/abm/analyze_result.h:30
constexpr std::size_t index_of_type_v
The index of Type in the list Types.
Definition: metaprogramming.h:191
auto failure(const IOStatus &s)
Create an object that is implicitly convertible to an error IOResult<T>.
Definition: io.h:380
boost::outcome_v2::in_place_type_t< T > Tag
Type that is used for overload resolution.
Definition: io.h:407
requires(!std::is_trivial_v< T >) void BinarySerializerObject
Definition: binary_serializer.h:333
auto success()
Create an object that is implicitly convertible to a succesful IOResult<void>.
Definition: io.h:359
void unused(T &&...)
Does nothing, can be used to mark variables as not used.
Definition: compiler_diagnostics.h:30
constexpr std::tuple_element< I, std::tuple< Index< CategoryTags >... > >::type & get(Index< CategoryTags... > &i) noexcept
Retrieves the Index (by reference) at the Ith position of a MultiIndex.
Definition: index.h:294
boost::outcome_v2::unchecked< T, IOStatus > IOResult
Value-or-error type for operations that return a value but can fail.
Definition: io.h:353
bool contains(Iter b, Iter e, Pred p)
checks if there is an element in this range that matches a predicate
Definition: stl_util.h:301
Definition: io.h:94