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