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

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2022-2023 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include <iostream>
0021 
0022 #include <QCoreApplication>
0023 #include <QCommandLineParser>
0024 #include <QSet>
0025 #include <QFile>
0026 #include <QFileInfo>
0027 #include <QTimer>
0028 
0029 #include "kbibtex-version.h"
0030 #include <File>
0031 #include <FileImporter>
0032 #include <FileExporter>
0033 #include <FileExporterBibTeX>
0034 #include <IdSuggestions>
0035 
0036 int main(int argc, char *argv[])
0037 {
0038     int exitCode = 0;
0039     QCoreApplication coreApp(argc, argv);
0040     QCoreApplication::setApplicationName(QStringLiteral("kbibtex-cli"));
0041     QCoreApplication::setApplicationVersion(QStringLiteral(KBIBTEX_VERSION_STRING));
0042 
0043     QCommandLineParser cmdLineParser;
0044     cmdLineParser.addHelpOption();
0045     cmdLineParser.addVersionOption();
0046     QCommandLineOption outputFileCLO{{QStringLiteral("o"), QStringLiteral("output")}, QStringLiteral("Write output to file, stdout if not specified"), QStringLiteral("outputfilename")};
0047     cmdLineParser.addOption(outputFileCLO);
0048     QCommandLineOption outputformatCLI{{QStringLiteral("O"), QStringLiteral("output-format")}, QStringLiteral("Provide suggestion for output format"), QStringLiteral("format")};
0049     cmdLineParser.addOption(outputformatCLI);
0050     QCommandLineOption idSuggestionFormatStringCLO{{QStringLiteral("format-id")}, QStringLiteral("Reformat all entry ids using this format string"), QStringLiteral("formatstring")};
0051     cmdLineParser.addOption(idSuggestionFormatStringCLO);
0052     cmdLineParser.addPositionalArgument(QStringLiteral("file"), QStringLiteral("Read from this file"));
0053 
0054     cmdLineParser.process(coreApp);
0055 
0056     QVector<QString> arguments;
0057     arguments.reserve(cmdLineParser.positionalArguments().length());
0058     for (const QString &pa : cmdLineParser.positionalArguments())
0059         if (pa.length() > 0)
0060             arguments.append(pa);
0061 
0062     if (arguments.length() < 1) {
0063         std::cerr << "No file to read from specified. Use  --help  for instructions." << std::endl;
0064         coreApp.exit(exitCode = 1);
0065     } else if (arguments.length() > 1) {
0066         std::cerr << "More than one input file specified. Use  --help  for instructions." << std::endl;
0067         coreApp.exit(exitCode = 1);
0068     } else {
0069         const QFileInfo inputFileInfo{arguments.constFirst()};
0070         if (!inputFileInfo.exists() || inputFileInfo.size() <= 0 || !inputFileInfo.isFile() || !inputFileInfo.isReadable()) {
0071             std::cerr << "Argument is not an existing, readable, non-empty file: " << inputFileInfo.filePath().toLocal8Bit().constData() << std::endl;
0072             coreApp.exit(exitCode = 1);
0073         } else {
0074             QFile inputFile(inputFileInfo.filePath());
0075             if (inputFile.open(QFile::ReadOnly)) {
0076                 FileImporter *importer = FileImporter::factory(inputFileInfo, &coreApp);
0077                 File *file = importer->load(&inputFile);
0078                 inputFile.close();
0079 
0080                 if (file == nullptr) {
0081                     std::cerr << "Failed to load file: " << inputFileInfo.filePath().toLocal8Bit().constData() << std::endl;
0082                     coreApp.exit(exitCode = 1);
0083                 } else {
0084                     if (cmdLineParser.isSet(idSuggestionFormatStringCLO)) {
0085                         /// If the user requested applying a certain format string to all entry ids, perform this change here.
0086                         QSet<QString> previousNewId; //< Track newly generated entry ids to detect duplicates
0087                         const QString formatString{cmdLineParser.value(idSuggestionFormatStringCLO)}; //< extract format string from command line argument
0088                         if (formatString.isEmpty()) {
0089                             std::cerr << "Got empty format string" << std::endl;
0090                             coreApp.exit(exitCode = 1);
0091                         } else {
0092                             std::cerr << "Using the following format string:" << std::endl;
0093                             for (const QString &fse : IdSuggestions::formatStrToHuman(formatString))
0094                                 std::cerr << " * " << fse.toLocal8Bit().constData() << std::endl;
0095                             for (QSharedPointer<Element> &element : *file) {
0096                                 /// For every element in the loaded bibliography file ...
0097                                 /// Check if element is an entry (something that has an 'id')
0098                                 QSharedPointer<Entry> entry = element.dynamicCast<Entry>();
0099                                 if (!entry.isNull()) {
0100                                     /// Generate new id based on entry and format string
0101                                     const QString newId{IdSuggestions::formatId(*(entry.data()), formatString)};
0102                                     if (newId.isEmpty()) {
0103                                         std::cerr << "New id generated from entry '" << entry->id().toLocal8Bit().constData() << "' and format string '" << formatString.toLocal8Bit().constData() << "' is empty." << std::endl;
0104                                         coreApp.exit(exitCode = 1);
0105                                         break;
0106                                     }
0107                                     if (previousNewId.contains(newId))
0108                                         std::cerr << "New entry id '" << newId.toLocal8Bit().constData() << "' generated from entry '" << entry->id().toLocal8Bit().constData() << "' and format string '" << formatString.toLocal8Bit().constData() << "' matches a previously generated id, but applying it anyway." << std::endl;
0109                                     else
0110                                         previousNewId.insert(newId);
0111                                     /// Apply newly-generated entry id
0112                                     entry->setId(newId);
0113                                 }
0114                             }
0115                         }
0116                     }
0117 
0118                     if (exitCode == 0) {
0119                         if (cmdLineParser.isSet(outputFileCLO)) {
0120                             const QFileInfo outputFileInfo{cmdLineParser.value(outputFileCLO)};
0121                             QFile outputfile(outputFileInfo.filePath());
0122                             if (outputfile.open(QFile::WriteOnly)) {
0123                                 QString exporterClassHint;
0124                                 if (cmdLineParser.isSet(outputformatCLI)) {
0125                                     const QString cmpTo = cmdLineParser.value(outputformatCLI).toLower();
0126                                     for (const QString &exporterClass : FileExporter::exporterClasses(outputFileInfo))
0127                                         if (exporterClass.toLower().contains(cmpTo)) {
0128                                             exporterClassHint = exporterClass;
0129                                             std::cerr << "Choosing exporter " << exporterClassHint.toLocal8Bit().constData() << " based on --output-format=" << cmpTo.toLocal8Bit().constData() << std::endl;
0130                                             break;
0131                                         }
0132                                 }
0133                                 FileExporter *exporter = FileExporter::factory(outputFileInfo, exporterClassHint, &coreApp);
0134                                 const bool ok = exporter->save(&outputfile, file);
0135                                 outputfile.close();
0136                                 if (!ok) {
0137                                     std::cerr << "Failed to write to this file: " << outputFileInfo.filePath().toLocal8Bit().constData() << std::endl;
0138                                     coreApp.exit(exitCode = 1);
0139                                 }
0140                             } else {
0141                                 std::cerr << "Cannot write to this file: " << outputFileInfo.filePath().toLocal8Bit().constData() << std::endl;
0142                                 coreApp.exit(exitCode = 1);
0143                             }
0144                         } else {
0145                             /// No output filename specified, so dump BibTeX code to stdout
0146                             FileExporter *exporter = new FileExporterBibTeX(&coreApp);
0147                             const QString output{exporter->toString(file)};
0148                             std::cout << output.toLocal8Bit().constData() << std::endl;
0149                         }
0150                     }
0151 
0152                     delete file;
0153                 }
0154             } else {
0155                 std::cerr << "Cannot read from file '" << inputFileInfo.filePath().toLocal8Bit().constData() << "'" << std::endl;
0156                 coreApp.exit(exitCode = 1);
0157             }
0158         }
0159     }
0160 
0161     QTimer::singleShot(100, QCoreApplication::instance(), []() {
0162         QCoreApplication::instance()->quit();
0163     });
0164 
0165     coreApp.exit(exitCode);
0166 
0167     const int r = coreApp.exec();
0168     return qMax(r, exitCode);
0169 }