File indexing completed on 2024-05-12 05:26:20

0001 /*
0002  * Copyright (C) 2014 Aaron Seigo <aseigo@kde.org>
0003  *
0004  *   This program is free software; you can redistribute it and/or modify
0005  *   it under the terms of the GNU General Public License as published by
0006  *   the Free Software Foundation; either version 2 of the License, or
0007  *   (at your option) any later version.
0008  *
0009  *   This program is distributed in the hope that it will be useful,
0010  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012  *   GNU General Public License for more details.
0013  *
0014  *   You should have received a copy of the GNU General Public License
0015  *   along with this program; if not, write to the
0016  *   Free Software Foundation, Inc.,
0017  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
0018  */
0019 
0020 #include "syntaxtree.h"
0021 
0022 #include <QCoreApplication>
0023 #include <QDebug>
0024 
0025 SyntaxTree *SyntaxTree::s_module = 0;
0026 
0027 Syntax::Syntax()
0028 {
0029 }
0030 
0031 Syntax::Syntax(const QString &k, const QString &helpText, std::function<bool(const QStringList &, State &)> l, Interactivity inter)
0032     : keyword(k), help(helpText), interactivity(inter), lambda(l)
0033 {
0034 }
0035 
0036 void Syntax::addPositionalArgument(const Argument &argument)
0037 {
0038     arguments.push_back(argument);
0039 }
0040 
0041 void Syntax::addParameter(const QString &name, const ParameterOptions &options)
0042 {
0043     parameters.insert(name, options);
0044 }
0045 
0046 void Syntax::addFlag(const QString &name, const QString &help)
0047 {
0048     flags.insert(name, help);
0049 }
0050 
0051 QString Syntax::usage() const
0052 {
0053     // TODO: refactor into meaningful functions?
0054     bool hasArguments = !arguments.isEmpty();
0055     bool hasFlags = !flags.isEmpty();
0056     bool hasOptions = !parameters.isEmpty();
0057     bool hasSubcommand = !children.isEmpty();
0058 
0059     QString argumentsSummary;
0060 
0061     QString argumentsUsage;
0062     if (hasArguments) {
0063         argumentsUsage += "\nARGUMENTS:\n";
0064         for (const auto &arg : arguments) {
0065             if (arg.required) {
0066                 argumentsSummary += QString(" <%1>").arg(arg.name);
0067                 argumentsUsage += QString("    <%1>: %2\n").arg(arg.name).arg(arg.help);
0068             } else {
0069                 argumentsSummary += QString(" [%1]").arg(arg.name);
0070                 argumentsUsage += QString("    [%1]: %2\n").arg(arg.name).arg(arg.help);
0071             }
0072             if (arg.variadic) {
0073                 argumentsSummary += "...";
0074             }
0075         }
0076     }
0077 
0078     if (hasFlags) {
0079         argumentsSummary += " [FLAGS]";
0080     }
0081 
0082     if (hasOptions) {
0083         argumentsSummary += " [OPTIONS]";
0084     }
0085 
0086     if (hasSubcommand) {
0087         if (hasArguments || hasFlags || hasOptions) {
0088             argumentsSummary = QString(" [ <SUB-COMMAND> |%1 ]").arg(argumentsSummary);
0089         } else {
0090             argumentsSummary = " <SUB-COMMAND>";
0091         }
0092     }
0093 
0094     argumentsSummary += '\n';
0095 
0096     QString subcommandsUsage;
0097     if (hasSubcommand) {
0098         subcommandsUsage += "\nSUB-COMMANDS:\n"
0099                             "    Use the 'help' command to find out more about a sub-command.\n\n";
0100         for (const auto &command : children) {
0101             subcommandsUsage += QString("    %1: %2\n").arg(command.keyword).arg(command.help);
0102         }
0103     }
0104 
0105     QString flagsUsage;
0106     if (hasFlags) {
0107         flagsUsage += "\nFLAGS:\n";
0108         for (auto it = flags.constBegin(); it != flags.constEnd(); ++it) {
0109             flagsUsage += QString("    [--%1]: %2\n").arg(it.key()).arg(it.value());
0110         }
0111     }
0112 
0113     QString optionsUsage;
0114     if (hasOptions) {
0115         optionsUsage += "\nOPTIONS:\n";
0116         for (auto it = parameters.constBegin(); it != parameters.constEnd(); ++it) {
0117             optionsUsage += "    ";
0118             if (!it.value().required) {
0119                 optionsUsage += QString("[--%1 $%2]").arg(it.key()).arg(it.value().name);
0120             } else {
0121                 optionsUsage += QString("<--%1 $%2>").arg(it.key()).arg(it.value().name);
0122             }
0123 
0124             optionsUsage += ": " + it.value().help + '\n';
0125         }
0126     }
0127 
0128     // TODO: instead of just the keyword, we might want to have the whole
0129     // command (e.g. if this is a sub-command)
0130     return QString("USAGE:\n    ") + keyword + argumentsSummary + subcommandsUsage +
0131            argumentsUsage + flagsUsage + optionsUsage;
0132 }
0133 
0134 SyntaxTree::SyntaxTree()
0135 {
0136 }
0137 
0138 int SyntaxTree::registerSyntax(std::function<Syntax::List()> f)
0139 {
0140     m_syntax += f();
0141     return m_syntax.size();
0142 }
0143 
0144 SyntaxTree *SyntaxTree::self()
0145 {
0146     if (!s_module) {
0147         s_module = new SyntaxTree;
0148     }
0149 
0150     return s_module;
0151 }
0152 
0153 Syntax::List SyntaxTree::syntax() const
0154 {
0155     return m_syntax;
0156 }
0157 
0158 int SyntaxTree::run(const QStringList &commands)
0159 {
0160     int returnCode = 0;
0161     m_timeElapsed.start();
0162     Command command = match(commands);
0163     if (command.first) {
0164         if (command.first->lambda) {
0165             bool success = command.first->lambda(command.second, m_state);
0166             if (success && command.first->interactivity == Syntax::EventDriven) {
0167                 returnCode = m_state.commandStarted();
0168             }
0169             if (!success && command.first->interactivity != Syntax::EventDriven) {
0170                 returnCode = 1;
0171             }
0172         } else if (command.first->children.isEmpty()) {
0173             m_state.printError(QObject::tr("Broken command... sorry :("), "st_broken");
0174         } else {
0175             QStringList keywordList;
0176             for (auto syntax : command.first->children) {
0177                 keywordList << syntax.keyword;
0178             }
0179             const QString keywords = keywordList.join(" ");
0180             m_state.printError(QObject::tr("Command requires additional arguments, one of: %1").arg(keywords));
0181         }
0182     } else {
0183         m_state.printError(QObject::tr("Unknown command"), "st_unknown");
0184     }
0185 
0186     if (m_state.commandTiming()) {
0187         m_state.printLine(QObject::tr("Time elapsed: %1").arg(m_timeElapsed.elapsed()));
0188     }
0189     return returnCode;
0190 }
0191 
0192 SyntaxTree::Command SyntaxTree::match(const QStringList &commandLine) const
0193 {
0194     if (commandLine.isEmpty()) {
0195         return Command();
0196     }
0197 
0198     QStringListIterator commandLineIt(commandLine);
0199 
0200     QVectorIterator<Syntax> syntaxIt(m_syntax);
0201     const Syntax *lastFullSyntax = 0;
0202     QStringList tailCommands;
0203     while (commandLineIt.hasNext() && syntaxIt.hasNext()) {
0204         const QString word = commandLineIt.next();
0205         bool match = false;
0206         while (syntaxIt.hasNext()) {
0207             const Syntax &syntax = syntaxIt.next();
0208             if (word == syntax.keyword) {
0209                 lastFullSyntax = &syntax;
0210                 syntaxIt = syntax.children;
0211                 match = true;
0212                 break;
0213             }
0214         }
0215         if (!match) {
0216             //Otherwise we would miss the just evaluated command from the tailCommands
0217             if (commandLineIt.hasPrevious()) {
0218                 commandLineIt.previous();
0219             }
0220         }
0221     }
0222 
0223     if (lastFullSyntax) {
0224         while (commandLineIt.hasNext()) {
0225             tailCommands << commandLineIt.next();
0226         }
0227 
0228         return std::make_pair(lastFullSyntax, tailCommands);
0229     }
0230 
0231     return Command();
0232 }
0233 
0234 Syntax::List SyntaxTree::nearestSyntax(const QStringList &words, const QString &fragment) const
0235 {
0236     Syntax::List matches;
0237 
0238     // qDebug() << "words are" << words;
0239     if (words.isEmpty()) {
0240         for (const Syntax &syntax : m_syntax) {
0241             if (syntax.keyword.startsWith(fragment)) {
0242                 matches.push_back(syntax);
0243             }
0244         }
0245     } else {
0246         QStringListIterator wordIt(words);
0247         QVectorIterator<Syntax> syntaxIt(m_syntax);
0248         Syntax lastFullSyntax;
0249 
0250         while (wordIt.hasNext()) {
0251             const QString &word = wordIt.next();
0252             while (syntaxIt.hasNext()) {
0253                 const Syntax &syntax = syntaxIt.next();
0254                 if (word == syntax.keyword) {
0255                     lastFullSyntax = syntax;
0256                     syntaxIt = syntax.children;
0257                     break;
0258                 }
0259             }
0260         }
0261 
0262         // qDebug() << "exiting with" << lastFullSyntax.keyword << words.last();
0263         if (lastFullSyntax.keyword == words.last()) {
0264             syntaxIt = lastFullSyntax.children;
0265             while (syntaxIt.hasNext()) {
0266                 Syntax syntax = syntaxIt.next();
0267                 if (fragment.isEmpty() || syntax.keyword.startsWith(fragment)) {
0268                     matches.push_back(syntax);
0269                 }
0270             }
0271         }
0272     }
0273 
0274     return matches;
0275 }
0276 
0277 State &SyntaxTree::state()
0278 {
0279     return m_state;
0280 }
0281 
0282 QStringList SyntaxTree::tokenize(const QString &text)
0283 {
0284     // TODO: properly tokenize (e.g. "foo bar" should not become ['"foo', 'bar"']a
0285     static const QVector<QChar> quoters = QVector<QChar>() << '"' << '\'';
0286     QStringList tokens;
0287     QString acc;
0288     QChar closer;
0289     for (int i = 0; i < text.size(); ++i) {
0290         const QChar c = text.at(i);
0291         if (c == '\\') {
0292             ++i;
0293             if (i < text.size()) {
0294                 acc.append(text.at(i));
0295             }
0296         } else if (!closer.isNull()) {
0297             if (c == closer) {
0298                 acc = acc.trimmed();
0299                 if (!acc.isEmpty()) {
0300                     tokens << acc;
0301                 }
0302                 acc.clear();
0303                 closer = QChar();
0304             } else {
0305                 acc.append(c);
0306             }
0307         } else if (c.isSpace()) {
0308             acc = acc.trimmed();
0309             if (!acc.isEmpty()) {
0310                 tokens << acc;
0311             }
0312             acc.clear();
0313         } else if (quoters.contains(c)) {
0314             closer = c;
0315         } else {
0316             acc.append(c);
0317         }
0318     }
0319 
0320     acc = acc.trimmed();
0321     if (!acc.isEmpty()) {
0322         tokens << acc;
0323     }
0324 
0325     return tokens;
0326 }
0327 
0328 SyntaxTree::Options SyntaxTree::parseOptions(const QStringList &args)
0329 {
0330     Options result;
0331     auto it = args.constBegin();
0332     for (;it != args.constEnd(); it++) {
0333         if (it->startsWith("--")) {
0334             QString option = it->mid(2);
0335             QStringList list;
0336             it++;
0337             for (;it != args.constEnd(); it++) {
0338                 if (it->startsWith("--")) {
0339                     it--;
0340                     break;
0341                 }
0342                 list << *it;
0343             }
0344             result.options.insert(option, list);
0345             if (it == args.constEnd()) {
0346                 break;
0347             }
0348         } else {
0349             result.positionalArguments << *it;
0350         }
0351     }
0352     return result;
0353 }