|
| 1 | +/* |
| 2 | +* Copyright (C) 2020-2025 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 | +#include "memilio/io/cli.h" |
| 21 | + |
| 22 | +#ifdef MEMILIO_HAS_JSONCPP |
| 23 | + |
| 24 | +#include <ostream> |
| 25 | +#include <sstream> |
| 26 | +#include <string> |
| 27 | +#include <vector> |
| 28 | + |
| 29 | +/// @brief Part of write_help implementation. Writes the header with usage information and default options. |
| 30 | +void write_help_preamble(const std::string& executable_name, const std::vector<std::string>& default_options, |
| 31 | + std::ostream& os) |
| 32 | +{ |
| 33 | + os << "Usage: " << executable_name; |
| 34 | + if (default_options.size() > 0) { |
| 35 | + os << " ["; |
| 36 | + size_t i = 0; |
| 37 | + for (; i < default_options.size() - 1; i++) { |
| 38 | + os << default_options[i] << " "; |
| 39 | + } |
| 40 | + os << default_options[i] << "]"; |
| 41 | + } |
| 42 | + os << " <option> <value> ...\n"; |
| 43 | + if (default_options.size() > 0) { |
| 44 | + os << "Values of parameter options listed in [brackets] can be optionally entered in that order.\n"; |
| 45 | + } |
| 46 | + os << "All values must be entered as json values, i.e. the expression to the right of \"Name : \".\n" |
| 47 | + << "Note that, when entering values, quotation marks may have to be escaped (\\\").\n"; |
| 48 | +} |
| 49 | + |
| 50 | +/// @brief Part of write_help implementation. Writes out a singular parameter option. |
| 51 | +void write_help_parameter(const mio::cli::details::DatalessParameter& parameter, std::ostream& os) |
| 52 | +{ |
| 53 | + // Max. space and offsets used to print everything nicely. |
| 54 | + constexpr size_t name_space = 16; // reserved space for name |
| 55 | + constexpr size_t alias_space = 4; // reserved space for alias |
| 56 | + constexpr size_t name_indent = 4; // size of " --" |
| 57 | + constexpr size_t alias_indent = 3; // size of " -" |
| 58 | + constexpr size_t description_indent = 2; // size of " " |
| 59 | + // Write name with "--" prefix and " " indent |
| 60 | + os << " --" << parameter.name; |
| 61 | + if (parameter.description.size() > 0 || parameter.alias.size() > 0) { |
| 62 | + if (parameter.name.size() <= name_space) { |
| 63 | + os << std::string(name_space - parameter.name.size(), ' '); |
| 64 | + } |
| 65 | + else { |
| 66 | + os << "\n" << std::string(name_space + name_indent, ' '); |
| 67 | + } |
| 68 | + } |
| 69 | + // Write alias (if available) with "-" prefix and " " indent |
| 70 | + size_t space = parameter.alias.size() < alias_space ? alias_space - parameter.alias.size() : 0; |
| 71 | + if (parameter.alias.size() > 0) { |
| 72 | + os << " -" << parameter.alias; |
| 73 | + } |
| 74 | + else { |
| 75 | + space += alias_indent; |
| 76 | + } |
| 77 | + // Write description (if available) and end line indentation |
| 78 | + if (parameter.description.size() > 0) { |
| 79 | + os << std::string(space + description_indent, ' ') << parameter.description; |
| 80 | + } |
| 81 | + os << "\n"; |
| 82 | +} |
| 83 | + |
| 84 | +void mio::cli::details::write_help(const std::string& executable_name, const AbstractSet& set, |
| 85 | + const std::vector<std::string>& default_options, std::ostream& os) |
| 86 | +{ |
| 87 | + write_help_preamble(executable_name, default_options, os); |
| 88 | + os << "Options:\n"; |
| 89 | + for (const auto& parameter : PresetOptions::all_presets) { |
| 90 | + write_help_parameter(parameter, os); |
| 91 | + } |
| 92 | + os << "Parameter options:\n"; |
| 93 | + for (const auto& parameter : set.parameters()) { |
| 94 | + write_help_parameter(parameter, os); |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +mio::IOResult<void> mio::cli::details::write_abstract_set_to_file(mio::cli::details::AbstractSet& set, |
| 99 | + const std::string& filepath) |
| 100 | +{ |
| 101 | + Json::Value output; |
| 102 | + for (auto& parameter : set.parameters()) { |
| 103 | + BOOST_OUTCOME_TRY(output[parameter.name()], parameter.get()); |
| 104 | + } |
| 105 | + return mio::write_json(filepath, output); |
| 106 | +} |
| 107 | + |
| 108 | +mio::IOResult<void> mio::cli::details::read_abstract_set_from_file(mio::cli::details::AbstractSet& set, |
| 109 | + const std::string& filepath) |
| 110 | +{ |
| 111 | + // read file into json value |
| 112 | + auto json_result = mio::read_json(filepath); |
| 113 | + if (!json_result) { |
| 114 | + return mio::failure(json_result.error()); |
| 115 | + } |
| 116 | + // set each parameter manually |
| 117 | + for (auto itr = json_result.value().begin(); itr != json_result.value().end(); itr++) { |
| 118 | + BOOST_OUTCOME_TRY(set.set_param(mio::cli::details::Identifier::make_raw(itr.name()), *itr)); |
| 119 | + } |
| 120 | + return mio::success(); |
| 121 | +} |
| 122 | + |
| 123 | +mio::IOResult<void> mio::cli::details::command_line_interface(const std::string& executable_name, const int argc, |
| 124 | + char** argv, cli::details::AbstractSet& set, |
| 125 | + const std::vector<std::string>& default_options) |
| 126 | +{ |
| 127 | + assert(set.parameters().size() > 0 && "At least one parameter is required!"); |
| 128 | + // this function glues all functionalities of the cli together. it may repeatedly iterate through all values of |
| 129 | + // argv (starting at 1). |
| 130 | + using namespace mio::cli; |
| 131 | + using namespace mio::cli::details; |
| 132 | + // verify that all default_options are parameter names |
| 133 | + for (const auto& option : default_options) { |
| 134 | + if (!set.contains(Identifier::make_raw(option))) { |
| 135 | + return failure(mio::StatusCode::KeyNotFound, "Default option \"" + option + "\" is not a parameter name."); |
| 136 | + } |
| 137 | + } |
| 138 | + // pre-scan all argumemts before doing anything with them to deal with help and print_option |
| 139 | + // this avoids returning e.g. parsing errors instead of the help dialogue |
| 140 | + for (int i = 1; i < argc; i++) { |
| 141 | + auto id_result = Identifier::parse(argv[i]); |
| 142 | + // skip non-option arguments |
| 143 | + if (!id_result) { |
| 144 | + continue; |
| 145 | + } |
| 146 | + const auto& id = id_result.value(); |
| 147 | + // handle help option |
| 148 | + if (id.matches_parameter(PresetOptions::help)) { |
| 149 | + // print the help dialogue and exit |
| 150 | + std::stringstream ss; |
| 151 | + write_help(executable_name, set, default_options, ss); |
| 152 | + return mio::failure(StatusCode::OK, std::move(ss.str())); |
| 153 | + } |
| 154 | + // handle print_option option |
| 155 | + else if (id.matches_parameter(PresetOptions::print_option)) { |
| 156 | + i++; // skip the PrintOption argument |
| 157 | + std::stringstream ss; |
| 158 | + for (; i < argc && !Identifier::is_option(argv[i]); i++) { |
| 159 | + // try to get the parameter's json value |
| 160 | + BOOST_OUTCOME_TRY(auto&& value, set.get_param(Identifier::make_raw(argv[i]))); |
| 161 | + // print the name (or alias) and value |
| 162 | + ss << "Option " << argv[i] << ":\n" << value << "\n"; |
| 163 | + } |
| 164 | + // return after all values are printed |
| 165 | + return mio::failure(StatusCode::OK, ss.str()); |
| 166 | + } |
| 167 | + } |
| 168 | + // main pass over all args to set options |
| 169 | + int i = 1; |
| 170 | + // handle parameter options that require values iteratively. assign given values or return an error |
| 171 | + while (i < argc) { |
| 172 | + const auto id_result = Identifier::parse(argv[i]); |
| 173 | + // try to parse the first default_options.size() as arguments; afterwards, require an identifier |
| 174 | + if (!id_result) { |
| 175 | + // checking #defaults suffices, as non-option arguments are greedily collected into "arguments" below |
| 176 | + if (i - 1 < static_cast<int>(default_options.size())) { |
| 177 | + const auto& param_name = Identifier::make_raw(default_options[i - 1]); |
| 178 | + BOOST_OUTCOME_TRY(set.set_param(param_name, std::string(argv[i]))); |
| 179 | + |
| 180 | + i++; |
| 181 | + continue; |
| 182 | + } |
| 183 | + else { |
| 184 | + return id_result.error(); |
| 185 | + } |
| 186 | + } |
| 187 | + const Identifier current_option(id_result.value()); |
| 188 | + i++; // go to first argument |
| 189 | + // assert that the first argument is not an identifier (i.e. name or alias) |
| 190 | + if (i == argc || Identifier::is_option(argv[i])) { |
| 191 | + return mio::failure(mio::StatusCode::OutOfRange, |
| 192 | + "Missing value for option \"" + current_option.string + "\"."); |
| 193 | + } |
| 194 | + // collect all argv's that are not identifiers and set i to the position of the next identifier |
| 195 | + std::string arguments(argv[i]); |
| 196 | + i++; |
| 197 | + for (; (i < argc) && !Identifier::is_option(argv[i]); i++) { |
| 198 | + // here space separated args are joined together. maybe a better way is to make users use 'ticks' to group |
| 199 | + // their input. |
| 200 | + arguments.append(" ").append(argv[i]); |
| 201 | + } |
| 202 | + // handle built-in options |
| 203 | + if (current_option.matches_parameter(PresetOptions::read_from_json)) { |
| 204 | + BOOST_OUTCOME_TRY(read_abstract_set_from_file(set, arguments)); |
| 205 | + } |
| 206 | + else if (current_option.matches_parameter(PresetOptions::write_to_json)) { |
| 207 | + BOOST_OUTCOME_TRY(write_abstract_set_to_file(set, arguments)); |
| 208 | + } |
| 209 | + // (try to) set the parameter, to the value given by arguments |
| 210 | + else { |
| 211 | + BOOST_OUTCOME_TRY(set.set_param(current_option, arguments)); |
| 212 | + } |
| 213 | + } |
| 214 | + // check if required parameters were set, return an error if not |
| 215 | + if (std::ranges::any_of(set.parameters(), [](auto&& p) { |
| 216 | + return p.is_required(); |
| 217 | + })) { |
| 218 | + std::stringstream ss; |
| 219 | + ss << "Missing values for required parameter(s):\n"; |
| 220 | + for (const auto& p : set.parameters()) { |
| 221 | + if (p.is_required() == true) { |
| 222 | + ss << " " << p.name(); |
| 223 | + } |
| 224 | + } |
| 225 | + ss << "\n" |
| 226 | + << "Use \"" << executable_name << " --help\" for more info.\n"; |
| 227 | + return mio::failure(mio::StatusCode::InvalidValue, ss.str()); |
| 228 | + } |
| 229 | + return mio::success(); |
| 230 | +} |
| 231 | + |
| 232 | +#endif // MEMILIO_HAS_JSONCPP |
0 commit comments