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 }