File indexing completed on 2024-11-24 03:56:29
0001 /* 0002 * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "cli.hpp" 0008 0009 #include <cstdio> 0010 0011 #include <QVector> 0012 #include <QSize> 0013 #include <QApplication> 0014 0015 #include "utils/string_view.hpp" 0016 0017 QString app::cli::Argument::get_slug(const QStringList& names) 0018 { 0019 if ( names.empty() ) 0020 return {}; 0021 0022 QString match; 0023 for ( const auto& name: names ) 0024 { 0025 if ( name.size() > match.size() ) 0026 match = name; 0027 } 0028 0029 for ( int i = 0; i < match.size(); i++ ) 0030 { 0031 if ( match[i] != '-' ) 0032 return match.mid(i); 0033 } 0034 0035 return {}; 0036 } 0037 0038 QVariant app::cli::Argument::arg_to_value(const QString& v, bool* ok) const 0039 { 0040 switch ( type ) 0041 { 0042 case String: 0043 *ok = true; 0044 return v; 0045 case Int: 0046 return v.toInt(ok); 0047 case Size: 0048 { 0049 if ( !v.contains('x') ) 0050 { 0051 *ok = false; 0052 return {}; 0053 } 0054 0055 auto vec = utils::split_ref(v, 'x'); 0056 if ( vec.size() != 2 ) 0057 { 0058 *ok = false; 0059 return {}; 0060 } 0061 0062 *ok = true; 0063 int x = vec[0].toInt(ok); 0064 if ( !ok ) 0065 return {}; 0066 0067 int y = vec[1].toInt(ok); 0068 if ( !ok ) 0069 return {}; 0070 0071 return QSize(x, y); 0072 } 0073 case Flag: 0074 case ShowHelp: 0075 case ShowVersion: 0076 *ok = false; 0077 return {}; 0078 } 0079 0080 *ok = false; 0081 return {}; 0082 } 0083 0084 QVariant app::cli::Argument::arg_to_value(const QString& arg) const 0085 { 0086 bool ok = false; 0087 QVariant v = arg_to_value(arg, &ok); 0088 if ( !ok ) 0089 throw ArgumentError( 0090 QApplication::tr("%2 is not a valid value for %1") 0091 .arg(names[0]).arg(arg) 0092 ); 0093 return v; 0094 } 0095 0096 0097 QVariant app::cli::Argument::args_to_value(const QStringList& args, int& index) const 0098 { 0099 if ( type == Flag ) 0100 return true; 0101 0102 if ( args.size() - index < nargs ) 0103 throw ArgumentError( 0104 QApplication::tr("Not enough arguments for %1: needs %2, has %3") 0105 .arg(names[0]).arg(nargs).arg(args.size() - index) 0106 ); 0107 0108 if ( nargs == 1 ) 0109 return arg_to_value(args[index++]); 0110 0111 QVariantList vals; 0112 for ( int i = 0; i < nargs; i++ ) 0113 vals.push_back(arg_to_value(args[index++])); 0114 return vals; 0115 } 0116 0117 0118 QString app::cli::Argument::help_text_name() const 0119 { 0120 QString option_names; 0121 for ( const auto& name : names ) 0122 option_names += name + ", "; 0123 if ( !names.isEmpty() ) 0124 option_names.chop(2); 0125 0126 if ( !arg_name.isEmpty() ) 0127 option_names += " <" + arg_name + ">"; 0128 0129 if ( nargs > 1 ) 0130 option_names += "..."; 0131 0132 return option_names; 0133 } 0134 0135 0136 bool app::cli::Argument::is_positional() const 0137 { 0138 return names.size() == 1 && !names[0].startsWith('-') && nargs > 0; 0139 } 0140 0141 0142 app::cli::Parser& app::cli::Parser::add_argument(Argument arg) 0143 { 0144 if ( groups.empty() ) 0145 groups.push_back({QApplication::tr("Options")}); 0146 0147 if ( arg.is_positional() ) 0148 { 0149 groups.back().args.emplace_back(Positional, positional.size()); 0150 positional.emplace_back(std::move(arg)); 0151 } 0152 else 0153 { 0154 groups.back().args.emplace_back(Option, options.size()); 0155 options.emplace_back(std::move(arg)); 0156 } 0157 0158 return *this; 0159 } 0160 0161 app::cli::Parser & app::cli::Parser::add_group(const QString& name) 0162 { 0163 groups.push_back({name}); 0164 return *this; 0165 } 0166 0167 const app::cli::Argument * app::cli::Parser::option_from_arg(const QString& arg) const 0168 { 0169 for ( const auto& option : options ) 0170 if ( option.names.contains(arg) ) 0171 return &option; 0172 return nullptr; 0173 } 0174 0175 void app::cli::ParsedArguments::handle_error(const QString& error) 0176 { 0177 show_message(error, true); 0178 return_value = 1; 0179 } 0180 0181 void app::cli::ParsedArguments::handle_finish(const QString& message) 0182 { 0183 show_message(message, false); 0184 return_value = 0; 0185 } 0186 0187 0188 app::cli::ParsedArguments app::cli::Parser::parse(const QStringList& args, int offset) const 0189 { 0190 int next_positional = 0; 0191 0192 ParsedArguments parsed; 0193 for ( const auto& option : options ) 0194 parsed.values[option.dest] = option.default_value; 0195 0196 for ( int index = offset; index < args.size(); ) 0197 { 0198 if ( args[index].startsWith('-') ) 0199 { 0200 if ( auto opt = option_from_arg(args[index]) ) 0201 { 0202 if ( opt->type == Argument::ShowHelp ) 0203 { 0204 parsed.handle_finish(help_text()); 0205 break; 0206 } 0207 else if ( opt->type == Argument::ShowVersion ) 0208 { 0209 parsed.handle_finish(version_text()); 0210 break; 0211 } 0212 0213 index++; 0214 QVariant val; 0215 try { 0216 val = opt->args_to_value(args, index); 0217 } catch ( const ArgumentError& err ) { 0218 parsed.handle_error(err.message()); 0219 break; 0220 } 0221 parsed.values[opt->dest] = val; 0222 parsed.defined.insert(opt->dest); 0223 if ( opt->type == Argument::Flag && val.toBool() ) 0224 parsed.flags.insert(opt->dest); 0225 continue; 0226 } 0227 0228 parsed.handle_error(QApplication::tr("Unknown argument %1").arg(args[index])); 0229 index++; 0230 break; 0231 } 0232 0233 if ( next_positional >= int(positional.size()) ) 0234 { 0235 parsed.handle_error(QApplication::tr("Too many arguments")); 0236 break; 0237 } 0238 0239 auto arg = &positional[next_positional]; 0240 parsed.defined.insert(arg->dest); 0241 try { 0242 parsed.values[arg->dest] = arg->args_to_value(args, index); 0243 } catch ( const ArgumentError& err ) { 0244 parsed.handle_error(err.message()); 0245 break; 0246 } 0247 next_positional++; 0248 } 0249 0250 return parsed; 0251 } 0252 0253 QString app::cli::Parser::version_text() const 0254 { 0255 return QCoreApplication::applicationName() + " " + QCoreApplication::applicationVersion() + "\n"; 0256 } 0257 0258 void app::cli::show_message(const QString& msg, bool error) 0259 { 0260 std::fputs(qUtf8Printable(msg + '\n'), error ? stderr : stdout); 0261 } 0262 0263 0264 QString app::cli::Parser::help_text() const 0265 { 0266 QString usage = QCoreApplication::instance()->arguments().constFirst(); 0267 0268 if ( !options.empty() ) 0269 usage += QApplication::tr(" [options]"); 0270 0271 0272 int longest_name = 0; 0273 QStringList opt_names; 0274 QStringList pos_names; 0275 0276 for ( const auto& opt : options ) 0277 { 0278 QString name = opt.help_text_name(); 0279 if ( longest_name < name.size() ) 0280 longest_name = name.size(); 0281 opt_names.append(name); 0282 } 0283 0284 for ( const auto& pos : positional ) 0285 { 0286 usage += " " + pos.arg_name; 0287 QString name = pos.help_text_name(); 0288 if ( longest_name < name.size() ) 0289 longest_name = name.size(); 0290 pos_names.append(name); 0291 } 0292 0293 QString text; 0294 text += QApplication::tr("Usage: %1").arg(usage); 0295 text += '\n'; 0296 text += '\n'; 0297 text += description; 0298 text += '\n'; 0299 0300 for ( const auto& grp : groups ) 0301 { 0302 text += '\n'; 0303 text += grp.name; 0304 text += ":\n"; 0305 for ( const auto& p : grp.args ) 0306 { 0307 text += wrap_text( 0308 (p.first == Positional ? pos_names : opt_names)[p.second], 0309 longest_name, 0310 (p.first == Positional ? positional : options)[p.second].description 0311 ); 0312 text += '\n'; 0313 } 0314 } 0315 0316 return text; 0317 } 0318 0319 QString app::cli::Parser::wrap_text(const QString& names, int name_max, const QString& description) const 0320 { 0321 const QLatin1String indentation(" "); 0322 0323 // In case the list of option names is very long, wrap it as well 0324 int nameIndex = 0; 0325 auto nextNameSection = [&]() { 0326 QString section = names.mid(nameIndex, name_max); 0327 nameIndex += section.size(); 0328 return section; 0329 }; 0330 0331 QString text; 0332 int lineStart = 0; 0333 int lastBreakable = -1; 0334 const int max = 79 - (indentation.size() + name_max + 1); 0335 int x = 0; 0336 const int len = description.length(); 0337 0338 for (int i = 0; i < len; ++i) 0339 { 0340 ++x; 0341 const QChar c = description.at(i); 0342 if (c.isSpace()) 0343 lastBreakable = i; 0344 0345 int breakAt = -1; 0346 int nextLineStart = -1; 0347 if (x > max && lastBreakable != -1) { 0348 // time to break and we know where 0349 breakAt = lastBreakable; 0350 nextLineStart = lastBreakable + 1; 0351 } else if ((x > max - 1 && lastBreakable == -1) || i == len - 1) { 0352 // time to break but found nowhere [-> break here], or end of last line 0353 breakAt = i + 1; 0354 nextLineStart = breakAt; 0355 } else if (c == '\n') { 0356 // forced break 0357 breakAt = i; 0358 nextLineStart = i + 1; 0359 } 0360 0361 if (breakAt != -1) { 0362 const int numChars = breakAt - lineStart; 0363 text += indentation + nextNameSection().leftJustified(name_max) + QLatin1Char(' '); 0364 text += utils::mid_ref(description, lineStart, numChars); 0365 text += '\n'; 0366 x = 0; 0367 lastBreakable = -1; 0368 lineStart = nextLineStart; 0369 if (lineStart < len && description.at(lineStart).isSpace()) 0370 ++lineStart; // don't start a line with a space 0371 i = lineStart; 0372 } 0373 } 0374 0375 while (nameIndex < names.size()) { 0376 text += indentation + nextNameSection() + '\n'; 0377 } 0378 0379 return text; 0380 } 0381 0382