File indexing completed on 2024-05-19 04:55:50

0001 /**
0002  * \file kid3cli.cpp
0003  * Command line interface for Kid3.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 10 Aug 2013
0008  *
0009  * Copyright (C) 2013-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "kid3cli.h"
0028 #include <QDir>
0029 #include <QCoreApplication>
0030 #include <QItemSelectionModel>
0031 #include <QTimer>
0032 #include <QStringBuilder>
0033 #include "kid3application.h"
0034 #include "icoreplatformtools.h"
0035 #include "coretaggedfileiconprovider.h"
0036 #include "fileproxymodel.h"
0037 #include "frametablemodel.h"
0038 #include "taggedfileselection.h"
0039 #include "clicommand.h"
0040 #include "cliconfig.h"
0041 #include "clierror.h"
0042 #include "textcliformatter.h"
0043 #include "jsoncliformatter.h"
0044 
0045 #ifdef HAVE_READLINE
0046 
0047 #include "readlinecompleter.h"
0048 
0049 class Kid3CliCompleter : public ReadlineCompleter {
0050 public:
0051   explicit Kid3CliCompleter(const QList<CliCommand*>& cmds);
0052   ~Kid3CliCompleter() override = default;
0053 
0054   QList<QByteArray> getCommandList() const override;
0055   QList<QByteArray> getParameterList() const override;
0056   bool updateParameterList(const char* buffer) override;
0057 
0058 private:
0059   Q_DISABLE_COPY(Kid3CliCompleter)
0060 
0061   const QList<CliCommand*>& m_cmds;
0062   QList<QByteArray> m_commands;
0063   QList<QByteArray> m_parameters;
0064 };
0065 
0066 Kid3CliCompleter::Kid3CliCompleter(const QList<CliCommand*>& cmds)
0067   : m_cmds(cmds)
0068 {
0069   m_commands.reserve(cmds.size());
0070   for (const CliCommand* cmd : cmds) {
0071     m_commands.append(cmd->name().toLocal8Bit());
0072   }
0073 }
0074 
0075 QList<QByteArray> Kid3CliCompleter::getCommandList() const
0076 {
0077   return m_commands;
0078 }
0079 
0080 QList<QByteArray> Kid3CliCompleter::getParameterList() const
0081 {
0082   return m_parameters;
0083 }
0084 
0085 bool Kid3CliCompleter::updateParameterList(const char* buffer)
0086 {
0087   QString cmdName = QString::fromLocal8Bit(buffer);
0088   bool isFirstParameter = true;
0089   if (int cmdNameEndIdx = cmdName.indexOf(QLatin1Char(' '));
0090       cmdNameEndIdx != -1) {
0091     isFirstParameter =
0092         cmdName.indexOf(QLatin1Char(' '), cmdNameEndIdx + 1) == -1;
0093     cmdName.truncate(cmdNameEndIdx);
0094   }
0095 
0096   QString argSpec;
0097   if (isFirstParameter) {
0098     for (const CliCommand* cmd : m_cmds) {
0099       if (cmdName == cmd->name()) {
0100         argSpec = cmd->argumentSpecification();
0101         break;
0102       }
0103     }
0104   }
0105 
0106   m_parameters.clear();
0107   if (!argSpec.isEmpty()) {
0108     if (QStringList argSpecs = argSpec.split(QLatin1Char('\n'));
0109         !argSpecs.isEmpty()) {
0110       if (QString argTypes = argSpecs.first().remove(QLatin1Char('['))
0111                                              .remove(QLatin1Char(']'));
0112           !argTypes.isEmpty()) {
0113         switch (argTypes.at(0).toLatin1()) {
0114         case 'P':
0115           // file path
0116           return false;
0117         case 'T':
0118           // tagnumbers
0119           m_parameters << "1" << "2" << "12";
0120           break;
0121         case 'N':
0122         {
0123           // frame name
0124           static QList<QByteArray> frameNames;
0125           if (frameNames.isEmpty()) {
0126             frameNames.reserve(Frame::FT_LastFrame - Frame::FT_FirstFrame + 1);
0127             for (int k = Frame::FT_FirstFrame; k <= Frame::FT_LastFrame; ++k) {
0128               if (auto frameName = Frame::ExtendedType(
0129                        static_cast<Frame::Type>(k), QLatin1String(""))
0130                      .getName().toLower().remove(QLatin1Char(' '));
0131                   !frameName.isEmpty()) {
0132                 frameNames.append(frameName.toLocal8Bit());
0133               }
0134             }
0135           }
0136           m_parameters = frameNames;
0137           break;
0138         }
0139         case 'S':
0140           // specific command
0141           if (argSpecs.size() > 1) {
0142             const QString& valuesStr = argSpecs.at(1);
0143             if (int valuesIdx = valuesStr.indexOf(QLatin1String("S = \""));
0144                 valuesIdx != -1) {
0145               const QStringList values =
0146                   valuesStr.mid(valuesIdx + 4).split(QLatin1String(" | "));
0147               for (const QString& value : values) {
0148                 if (value.startsWith(QLatin1Char('"')) &&
0149                     value.endsWith(QLatin1Char('"'))) {
0150                   m_parameters.append(
0151                         value.mid(1, value.length() - 2).toLocal8Bit());
0152                 }
0153               }
0154             }
0155           }
0156           break;
0157         default:
0158           break;
0159         }
0160       }
0161     }
0162   }
0163   return true;
0164 }
0165 
0166 #endif // HAVE_READLINE
0167 
0168 
0169 /**
0170  * Constructor.
0171  * @param app application context
0172  * @param io I/O handler
0173  * @param args command line arguments
0174  * @param parent parent object
0175  */
0176 Kid3Cli::Kid3Cli(Kid3Application* app,
0177                  AbstractCliIO* io, const QStringList& args, QObject* parent) :
0178   AbstractCli(io, parent),
0179   m_app(app), m_args(args),
0180   m_tagMask(Frame::TagV2V1), m_timeoutMs(0), m_fileNameChanged(false)
0181 {
0182   m_formatters << new JsonCliFormatter(io)
0183                << new TextCliFormatter(io);
0184 #if QT_VERSION >= 0x050600
0185   m_formatter = m_formatters.constLast();
0186 #else
0187   m_formatter = m_formatters.last();
0188 #endif
0189 
0190   m_cmds << new HelpCommand(this)
0191          << new TimeoutCommand(this)
0192          << new QuitCommand(this)
0193          << new CdCommand(this)
0194          << new PwdCommand(this)
0195          << new LsCommand(this)
0196          << new SaveCommand(this)
0197          << new SelectCommand(this)
0198          << new TagCommand(this)
0199          << new GetCommand(this)
0200          << new SetCommand(this)
0201          << new RevertCommand(this)
0202          << new ImportCommand(this)
0203          << new BatchImportCommand(this)
0204          << new AlbumArtCommand(this)
0205          << new ExportCommand(this)
0206          << new PlaylistCommand(this)
0207          << new FilenameFormatCommand(this)
0208          << new TagFormatCommand(this)
0209          << new TextEncodingCommand(this)
0210          << new RenameDirectoryCommand(this)
0211          << new NumberTracksCommand(this)
0212          << new FilterCommand(this)
0213          << new ToId3v24Command(this)
0214          << new ToId3v23Command(this)
0215          << new TagToFilenameCommand(this)
0216          << new FilenameToTagCommand(this)
0217          << new TagToOtherTagCommand(this)
0218          << new CopyCommand(this)
0219          << new PasteCommand(this)
0220          << new RemoveCommand(this)
0221          << new ConfigCommand(this)
0222          << new ExecuteCommand(this);
0223   connect(m_app, &Kid3Application::fileSelectionUpdateRequested,
0224           this, &Kid3Cli::updateSelectedFiles);
0225   connect(m_app, &Kid3Application::selectedFilesUpdated,
0226           this, &Kid3Cli::updateSelection);
0227   connect(m_app, &Kid3Application::selectedFilesChanged,
0228           this, &Kid3Cli::updateSelection);
0229 #ifdef HAVE_READLINE
0230   m_completer.reset(new Kid3CliCompleter(m_cmds));
0231   m_completer->install();
0232 #endif
0233 }
0234 
0235 /**
0236  * Destructor.
0237  */
0238 Kid3Cli::~Kid3Cli()
0239 {
0240   // Must not be inline because of forwared declared QScopedPointer.
0241 }
0242 
0243 /**
0244  * Get command for a command line.
0245  * @param line command line
0246  * @return command, 0 if no command found.
0247  */
0248 CliCommand* Kid3Cli::commandForArgs(const QString& line)
0249 {
0250   if (line.isEmpty())
0251     return nullptr;
0252 
0253   // Default to the last formatter
0254 #if QT_VERSION >= 0x050600
0255   m_formatter = m_formatters.constLast();
0256 #else
0257   m_formatter = m_formatters.last();
0258 #endif
0259 
0260   QStringList args;
0261   for (auto fmt : m_formatters) {
0262     args = fmt->parseArguments(line);
0263     if (fmt->isFormatRecognized()) {
0264       m_formatter = fmt;
0265       break;
0266     }
0267   }
0268 
0269   if (!args.isEmpty()) {
0270     const QString& name = args.at(0);
0271     for (auto it = m_cmds.begin(); it != m_cmds.end(); ++it) { // clazy:exclude=detaching-member
0272       if (CliCommand* cmd = *it; name == cmd->name()) {
0273         cmd->setArgs(args);
0274         return cmd;
0275       }
0276     }
0277   }
0278   return nullptr;
0279 }
0280 
0281 /**
0282  * Display help about available commands.
0283  * @param cmdName command name, for all commands if empty
0284  * @param usageMessage true if this is a usage error message
0285  */
0286 void Kid3Cli::writeHelp(const QString& cmdName, bool usageMessage)
0287 {
0288   QString msg;
0289   if (cmdName.isEmpty()) {
0290     QString tagNumbersStr;
0291     FOR_ALL_TAGS(tagNr) {
0292       if (!tagNumbersStr.isEmpty()) {
0293         tagNumbersStr += QLatin1Char('|');
0294       }
0295       tagNumbersStr += QLatin1String(" \"");
0296       tagNumbersStr += Frame::tagNumberToString(tagNr);
0297       tagNumbersStr += QLatin1String("\" ");
0298     }
0299     msg += tr("Parameter") %
0300         QLatin1String("\n  P = ") % tr("File path") %
0301         QLatin1String("\n  U = ") % tr("URL") %
0302         QLatin1String("\n  T = ") % tr("Tag numbers") %
0303         tagNumbersStr % QLatin1String("| \"12\" | ...") %
0304         QLatin1String("\n  N = ") % tr("Frame name") %
0305         QLatin1String(" \"album\" | \"album artist\" | \"arranger\" | "
0306                       "\"artist\" | ...") %
0307         QLatin1String("\n  V = ") % tr("Frame value") %
0308         QLatin1String("\n  F = ") % tr("Format") %
0309         QLatin1String("\n  S = ") % tr("Command specific") %
0310         QLatin1Char('\n') % tr("Available Commands") % QLatin1Char('\n');
0311   }
0312   QList<QStringList> cmdStrs;
0313   int maxLength = 0;
0314   for (auto it = m_cmds.constBegin(); it != m_cmds.constEnd(); ++it) {
0315     const CliCommand* cmd = *it;
0316     if (QString cmdStr = cmd->name(); cmdName.isEmpty() || cmdName == cmdStr) {
0317       QStringList spec = cmd->argumentSpecification().split(QLatin1Char('\n'));
0318       if (!spec.isEmpty()) {
0319         cmdStr += QLatin1Char(' ');
0320         cmdStr += spec.takeFirst();
0321       }
0322       cmdStrs.append(QStringList() << cmdStr << cmd->help() << spec);
0323       maxLength = qMax(cmdStr.size(), maxLength);
0324     }
0325   }
0326   const auto constCmdStrs = cmdStrs;
0327   for (QStringList strs : constCmdStrs) {
0328     QString cmdStr = strs.takeFirst();
0329     cmdStr += QString(maxLength - cmdStr.size() + 2, QLatin1Char(' ')) %
0330         strs.takeFirst() % QLatin1Char('\n');
0331     msg += cmdStr;
0332     while (!strs.isEmpty()) {
0333       msg += QString(maxLength + 2, QLatin1Char(' ')) %
0334           strs.takeFirst() % QLatin1Char('\n');
0335     }
0336   }
0337   if (usageMessage) {
0338     writeError(msg.trimmed(), CliError::Usage);
0339   } else {
0340     writeResult(msg.trimmed());
0341   }
0342 }
0343 
0344 /**
0345  * Execute process.
0346  */
0347 void Kid3Cli::execute()
0348 {
0349   if (!parseOptions()) {
0350     // Interactive mode
0351     AbstractCli::execute();
0352   }
0353 }
0354 
0355 /**
0356  * Open directory
0357  * @param paths directory or file paths
0358  * @return true if ok.
0359  */
0360 bool Kid3Cli::openDirectory(const QStringList& paths)
0361 {
0362   if (m_app->openDirectory(paths, true)) {
0363     QDir::setCurrent(m_app->getDirPath());
0364     m_app->getFileSelectionModel()->clearSelection();
0365     return true;
0366   }
0367   return false;
0368 }
0369 
0370 /**
0371  * Expand wildcards in path list.
0372  * @param paths paths to expand
0373  * @return expanded paths.
0374  */
0375 QStringList Kid3Cli::expandWildcards(const QStringList& paths)
0376 {
0377   QStringList expandedPaths;
0378   for (const QString& path : paths) {
0379     QStringList expandedPath;
0380     if (int wcIdx = path.indexOf(QRegularExpression(QLatin1String("[?*]")));
0381         wcIdx != -1) {
0382       QString partBefore, partAfter;
0383       int beforeIdx = path.lastIndexOf(QDir::separator(), wcIdx);
0384       partBefore = path.left(beforeIdx + 1);
0385       int afterIdx = path.indexOf(QDir::separator(), wcIdx);
0386       if (afterIdx == -1) {
0387         afterIdx = path.length();
0388       }
0389       partAfter = path.mid(afterIdx + 1);
0390       if (QString wildcardPart = path.mid(beforeIdx + 1, afterIdx - beforeIdx);
0391           !wildcardPart.isEmpty()) {
0392         if (QDir dir(partBefore); !dir.exists(wildcardPart)) {
0393           if (const QStringList entries = dir.entryList({wildcardPart},
0394                 QDir::AllEntries | QDir::NoDotAndDotDot);
0395               !entries.isEmpty()) {
0396             for (const QString& entry : entries) {
0397               expandedPath.append(partBefore + entry + partAfter); // clazy:exclude=reserve-candidates
0398             }
0399           }
0400         }
0401       }
0402     }
0403     if (expandedPath.isEmpty()) {
0404       expandedPaths.append(path); // clazy:exclude=reserve-candidates
0405     } else {
0406       expandedPaths.append(expandedPath);
0407     }
0408   }
0409   return expandedPaths;
0410 }
0411 
0412 /**
0413  * Select files in the current directory.
0414  * @param paths file names
0415  * @return true if files found and selected.
0416  */
0417 bool Kid3Cli::selectFile(const QStringList& paths)
0418 {
0419   bool ok = true;
0420   FileProxyModel* model = m_app->getFileProxyModel();
0421   for (const QString& fileName : paths) {
0422     QModelIndex index = model->index(fileName);
0423     if (!index.isValid() && fileName.startsWith(QLatin1Char(':'))) {
0424       // The FileSystemModel considers paths starting with a colon as invalid,
0425       // retry with a relative path. See also comment about QResource in
0426       // Kid3Application::openDirectory().
0427       index = model->index(QLatin1String("./") + fileName);
0428     }
0429     if (index.isValid()) {
0430       m_app->getFileSelectionModel()->setCurrentIndex(
0431             index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
0432     } else {
0433       ok = false;
0434     }
0435   }
0436   return ok;
0437 }
0438 
0439 /**
0440  * Update the currently selected files from the frame tables.
0441  */
0442 void Kid3Cli::updateSelectedFiles()
0443 {
0444   TaggedFileSelection* selection = m_app->selectionInfo();
0445   selection->selectChangedFrames();
0446   if (!selection->isEmpty()) {
0447     m_app->frameModelsToTags();
0448   }
0449   selection->setFilename(m_filename);
0450 }
0451 
0452 /**
0453  * Has to be called when the selection changes to update the frame tables
0454  * and the information about the selected files.
0455  */
0456 void Kid3Cli::updateSelection()
0457 {
0458   m_app->tagsToFrameModels();
0459 
0460   TaggedFileSelection* selection = m_app->selectionInfo();
0461   m_filename = selection->getFilename();
0462   FOR_ALL_TAGS(tagNr) {
0463     m_tagFormat[tagNr] = selection->getTagFormat(tagNr);
0464   }
0465   m_fileNameChanged = selection->isFilenameChanged();
0466   m_detailInfo = selection->getDetailInfo();
0467 }
0468 
0469 /**
0470  * Display information about selected files.
0471  * @param tagMask tag bits (1 for tag 1, 2 for tag 2)
0472  */
0473 void Kid3Cli::writeFileInformation(int tagMask)
0474 {
0475   QVariantMap map;
0476   if (!m_detailInfo.isEmpty()) {
0477     map.insert(QLatin1String("format"), m_detailInfo.trimmed());
0478   }
0479   if (!m_filename.isEmpty()) {
0480     map.insert(QLatin1String("fileNameChanged"), m_fileNameChanged);
0481     map.insert(QLatin1String("fileName"), m_filename);
0482   }
0483   FOR_TAGS_IN_MASK(tagNr, tagMask) {
0484     FrameTableModel* ft = m_app->frameModel(tagNr);
0485     QVariantList frames;
0486     for (int row = 0; row < ft->rowCount(); ++row) {
0487       QString name =
0488           ft->index(row, FrameTableModel::CI_Enable).data().toString();
0489       if (QString value =
0490             ft->index(row, FrameTableModel::CI_Value).data().toString();
0491           !(tagNr == Frame::Tag_1 ? value.isEmpty() : value.isNull())) {
0492         QVariant background = ft->index(row, FrameTableModel::CI_Enable)
0493             .data(Qt::BackgroundRole);
0494         CoreTaggedFileIconProvider* colorProvider =
0495             m_app->getPlatformTools()->iconProvider();
0496         bool changed = colorProvider &&
0497           colorProvider->contextForColor(background) == ColorContext::Marked;
0498         frames.append(QVariantMap{
0499                         {QLatin1String("changed"), changed},
0500                         {QLatin1String("name"), name},
0501                         {QLatin1String("value"), value},
0502                       });
0503       }
0504     }
0505     if (!frames.isEmpty()) {
0506       map.insert(QLatin1String("tag") + Frame::tagNumberToString(tagNr),
0507                  QVariantMap{
0508                    {QLatin1String("format"), m_tagFormat[tagNr]},
0509                    {QLatin1String("frames"), frames}
0510                  });
0511     }
0512   }
0513   writeResult(QVariantMap{{QLatin1String("taggedFile"), map}});
0514 }
0515 
0516 /**
0517  * Write currently active tag mask.
0518  */
0519 void Kid3Cli::writeTagMask()
0520 {
0521   QVariantList tags;
0522   QString tagStr;
0523   FOR_TAGS_IN_MASK(tagNr, m_tagMask) {
0524     tags.append(tagNr + 1);
0525   }
0526   writeResult(QVariantMap{{QLatin1String("tags"), tags}});
0527 }
0528 
0529 /**
0530  * Set currently active tag mask.
0531  *
0532  * @param tagMask tag bits
0533  */
0534 void Kid3Cli::setTagMask(Frame::TagVersion tagMask)
0535 {
0536   m_tagMask = tagMask;
0537 }
0538 
0539 /**
0540  * List files.
0541  */
0542 void Kid3Cli::writeFileList()
0543 {
0544   writeResult(QVariantMap{
0545     {QLatin1String("files"),
0546      listFiles(m_app->getFileProxyModel(), m_app->getRootIndex())}
0547   });
0548 }
0549 
0550 /**
0551  * List files.
0552  *
0553  * @param model file proxy model
0554  * @param parent index of parent item
0555  *
0556  * @return list with file properties.
0557  */
0558 QVariantList Kid3Cli::listFiles(const FileProxyModel* model,
0559                                 const QModelIndex& parent)
0560 {
0561   QVariantList lst;
0562   if (!model->hasChildren(parent))
0563     return lst;
0564 
0565   m_app->updateCurrentSelection();
0566 #if QT_VERSION >= 0x050e00
0567   const QList<QPersistentModelIndex>& selLst = m_app->getCurrentSelection();
0568   QSet selection(selLst.constBegin(), selLst.constEnd());
0569 #else
0570   QSet selection = m_app->getCurrentSelection().toSet();
0571 #endif
0572   for (int row = 0; row < model->rowCount(parent); ++row) {
0573     QModelIndex idx(model->index(row, 0, parent));
0574     QVariantMap map;
0575     map.insert(QLatin1String("selected"), selection.contains(idx));
0576     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(idx)) {
0577       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
0578       map.insert(QLatin1String("changed"), taggedFile->isChanged());
0579       QVariantList tags;
0580       FOR_ALL_TAGS(tagNr) {
0581         if (taggedFile->hasTag(tagNr)) {
0582           tags.append(1 + tagNr);
0583         }
0584       }
0585       map.insert(QLatin1String("tags"), tags);
0586       map.insert(QLatin1String("fileName"), taggedFile->getFilename());
0587     } else {
0588       if (QVariant value(model->data(idx)); value.isValid()) {
0589         map.insert(QLatin1String("fileName"), value.toString());
0590       }
0591     }
0592     if (model->hasChildren(idx)) {
0593       map.insert(QLatin1String("files"), listFiles(model, idx));
0594     }
0595     lst.append(map);
0596   }
0597   return lst;
0598 }
0599 
0600 /**
0601  * Respond with an error message
0602  * @param errorCode error code
0603  */
0604 void Kid3Cli::writeErrorCode(CliError errorCode)
0605 {
0606   m_formatter->writeError(errorCode);
0607 }
0608 
0609 /**
0610  * Write a line to standard error.
0611  * @param line line to write
0612  */
0613 void Kid3Cli::writeErrorLine(const QString& line)
0614 {
0615   m_formatter->writeError(line);
0616 }
0617 
0618 /**
0619  * Respond with an error message.
0620  * @param msg error message
0621  * @param errorCode error code
0622  */
0623 void Kid3Cli::writeError(const QString& msg, CliError errorCode)
0624 {
0625   m_formatter->writeError(msg, errorCode);
0626 }
0627 
0628 /**
0629  * Write result of command.
0630  * @param str result as string
0631  */
0632 void Kid3Cli::writeResult(const QString& str)
0633 {
0634   m_formatter->writeResult(str);
0635 }
0636 
0637 /**
0638  * Write result of command.
0639  * @param strs result as string list
0640  */
0641 void Kid3Cli::writeResult(const QStringList& strs)
0642 {
0643   m_formatter->writeResult(strs);
0644 }
0645 
0646 /**
0647  * Write result of command.
0648  * @param map result as map
0649  */
0650 void Kid3Cli::writeResult(const QVariantMap& map)
0651 {
0652   m_formatter->writeResult(map);
0653 }
0654 
0655 /**
0656  * Write result of command.
0657  * @param result result as boolean
0658  */
0659 void Kid3Cli::writeResult(bool result)
0660 {
0661   m_formatter->writeResult(result);
0662 }
0663 
0664 /**
0665  * Called when a command is finished.
0666  */
0667 void Kid3Cli::finishWriting()
0668 {
0669   m_formatter->finishWriting();
0670   m_formatter->clear();
0671 }
0672 
0673 /**
0674  * Process command line.
0675  * @param line command line
0676  */
0677 void Kid3Cli::readLine(const QString& line)
0678 {
0679   if (line.isNull()) {
0680     // Terminate if EOF is received.
0681     terminate();
0682     return;
0683   }
0684   flushStandardOutput();
0685   if (CliCommand* cmd = commandForArgs(line)) {
0686     connect(cmd, &CliCommand::finished, this, &Kid3Cli::onCommandFinished);
0687     cmd->execute();
0688   } else {
0689     if (!m_formatter->isIncomplete()) {
0690       if (QString errorMsg = m_formatter->getErrorMessage();
0691           errorMsg.isEmpty()) {
0692         writeErrorCode(CliError::MethodNotFound);
0693       } else {
0694         writeErrorLine(errorMsg);
0695       }
0696       finishWriting();
0697     }
0698     promptNextLine();
0699   }
0700 }
0701 
0702 /**
0703  * Called when a command is finished.
0704  */
0705 void Kid3Cli::onCommandFinished() {
0706   if (auto cmd = qobject_cast<CliCommand*>(sender())) {
0707     disconnect(cmd, &CliCommand::finished, this, &Kid3Cli::onCommandFinished);
0708     if (cmd->hasError()) {
0709       if (QString msg(cmd->getErrorMessage());
0710           !msg.startsWith(QLatin1Char('_'))) {
0711         writeErrorLine(msg);
0712       }
0713     }
0714     cmd->clear();
0715     promptNextLine();
0716   }
0717 }
0718 
0719 /**
0720  * Called when an argument command is finished.
0721  */
0722 void Kid3Cli::onArgCommandFinished() {
0723   if (auto cmd = qobject_cast<CliCommand*>(sender())) {
0724     disconnect(cmd, &CliCommand::finished, this, &Kid3Cli::onArgCommandFinished);
0725     if (!cmd->hasError()) {
0726       cmd->clear();
0727       executeNextArgCommand();
0728     } else {
0729       if (QString msg(cmd->getErrorMessage());
0730           !msg.startsWith(QLatin1Char('_'))) {
0731         writeErrorLine(msg);
0732       }
0733       cmd->clear();
0734       setReturnCode(1);
0735       terminate();
0736     }
0737   }
0738 }
0739 
0740 bool Kid3Cli::parseOptions()
0741 {
0742   const QStringList args = m_args.mid(1);
0743   QStringList paths;
0744   bool isCommand = false;
0745   for (const QString& arg : args) {
0746     if (isCommand) {
0747       m_argCommands.append(arg);
0748       isCommand = false;
0749     } else if (arg == QLatin1String("-c")) {
0750       isCommand = true;
0751     } else if (arg == QLatin1String("-h") || arg == QLatin1String("--help")) {
0752       writeLine(QLatin1String("kid3-cli " VERSION " (c) " RELEASE_YEAR
0753                               " Urs Fleisch"));
0754       writeLine(tr("Usage:") + QLatin1String(
0755           " kid3-cli [-c command1] [-c command2 ...] [path ...]"));
0756       writeHelp();
0757       flushStandardOutput();
0758       terminate();
0759       return true;
0760     } else {
0761       paths.append(arg);
0762     }
0763   }
0764 
0765   if (paths.isEmpty()) {
0766     paths.append(QDir::currentPath());
0767   }
0768   m_app->readConfig();
0769   connect(m_app, &Kid3Application::directoryOpened,
0770     this, &Kid3Cli::onInitialDirectoryOpened);
0771   if (!openDirectory(expandWildcards(paths))) {
0772     writeErrorLine(tr("%1 does not exist").arg(paths.join(QLatin1String(", "))));
0773   }
0774   return !m_argCommands.isEmpty();
0775 }
0776 
0777 /**
0778  * Select files passed as command line arguments after the initial directory has
0779  * been opened. Start execution of commands if existing.
0780  */
0781 void Kid3Cli::onInitialDirectoryOpened()
0782 {
0783   disconnect(m_app, &Kid3Application::directoryOpened,
0784     this, &Kid3Cli::onInitialDirectoryOpened);
0785   if (!m_argCommands.isEmpty()) {
0786     if (!m_app->getRootIndex().isValid()) {
0787       // Do not execute commands if directory could not be opened.
0788       m_argCommands.clear();
0789     }
0790     executeNextArgCommand();
0791   }
0792 }
0793 
0794 void Kid3Cli::executeNextArgCommand()
0795 {
0796   if (m_argCommands.isEmpty()) {
0797     if (m_app->isModified() && !m_app->getDirName().isEmpty()) {
0798       // Automatically save changes in command mode.
0799       QStringList errorDescriptions;
0800       if (const QStringList errorFiles = m_app->saveDirectory(&errorDescriptions);
0801           !errorFiles.isEmpty()) {
0802         writeErrorLine(tr("Error while writing file:\n") +
0803                        Kid3Application::mergeStringLists(
0804                          errorFiles, errorDescriptions, QLatin1String(": "))
0805                        .join(QLatin1String("\n")));
0806         finishWriting();
0807         setReturnCode(1);
0808       }
0809     }
0810     terminate();
0811     return;
0812   }
0813 
0814   QString line = m_argCommands.takeFirst();
0815   if (CliCommand* cmd = commandForArgs(line)) {
0816     connect(cmd, &CliCommand::finished, this, &Kid3Cli::onArgCommandFinished);
0817     cmd->execute();
0818   } else {
0819     QString errorMsg = m_formatter->getErrorMessage();
0820     if (errorMsg.isEmpty()) {
0821       errorMsg = tr("Unknown command '%1', -h for help.").arg(line);
0822     }
0823     writeErrorLine(errorMsg);
0824     finishWriting();
0825     setReturnCode(1);
0826     terminate();
0827   }
0828 }