File indexing completed on 2024-12-01 11:24:19

0001 // SPDX-License-Identifier: GPL-2.0-or-later
0002 // SPDX-FileCopyrightText: 2001 Dominik Seichter <domseichter@web.de>
0003 
0004 #include "batchrenamer.h"
0005 
0006 #include <QtGlobal>
0007 
0008 #ifdef Q_OS_WIN
0009 #include <windows.h>
0010 #endif
0011 
0012 // OS includes
0013 #include <cstdio>
0014 #ifndef Q_OS_WIN
0015 #include <unistd.h>
0016 #endif
0017 
0018 #include "../config-krename.h"
0019 
0020 // chmod:
0021 #include <sys/types.h>
0022 #include <sys/stat.h>
0023 
0024 // QT includes
0025 #include <QTextStream>
0026 
0027 // KDE includes
0028 #include <kio/job.h>
0029 #include <KIO/MkpathJob>
0030 #include <KJobWidgets>
0031 
0032 // Own includes
0033 #include "progressdialog.h"
0034 #include "pluginloader.h"
0035 #include "plugin.h"
0036 
0037 using namespace KIO;
0038 
0039 static bool isToken(const QChar &token)
0040 {
0041     const QChar tokens[] = {
0042         QChar('&'),
0043         QChar('$'),
0044         QChar('%'),
0045         QChar('#'),
0046         QChar('['),
0047         QChar(']'),
0048         QChar('\\'),
0049         QChar('/'),
0050         QChar('{'),
0051         QChar('}'),
0052         QChar('*')
0053     };
0054     const int count = 11;
0055 
0056     for (int i = 0; i < count; i++)
0057         if (token == tokens[i]) {
0058             return true;
0059         }
0060 
0061     return false;
0062 }
0063 
0064 static int getNextToken(const QString &text, QString &token, int pos = 0)
0065 {
0066     bool escaped = false;
0067     token.clear();
0068 
0069     if (pos < 0) {
0070         return -1;
0071     }
0072 
0073     while (pos < text.length()) {
0074         if (!escaped && text[pos] == QChar('\\')) {
0075             escaped = true;
0076         } else if (!escaped && isToken(text[pos])) {
0077             token = text[pos];
0078             return ++pos;
0079         } else {
0080             escaped = false;
0081         }
0082 
0083         ++pos;
0084     }
0085 
0086     return -1;
0087 }
0088 
0089 BatchRenamer::BatchRenamer()
0090     : m_index(0), m_step(1), m_files(nullptr), m_renameMode(eRenameMode_Rename)
0091 {
0092     m_counter_index = 0;
0093     m_reset         = false;
0094     m_overwrite     = false;
0095 }
0096 
0097 BatchRenamer::~BatchRenamer()
0098 {
0099 }
0100 
0101 void BatchRenamer::processFilenames()
0102 {
0103     m_counters.clear();
0104 
0105     for (unsigned int i = 0; i < static_cast<unsigned int>(m_files->count()); i++) {
0106         m_counter_index = 0;
0107         if (m_renameMode == eRenameMode_Rename) { // final Path = source Path
0108             (*m_files)[i].setDstDirectory((*m_files)[i].srcDirectory());
0109 
0110             QUrl url = (*m_files)[i].srcUrl();
0111             url = url.adjusted(QUrl::RemoveFilename);
0112 
0113             (*m_files)[i].setDstUrl(url);
0114 
0115         } else {
0116             (*m_files)[i].setDstUrl(m_destination);
0117             (*m_files)[i].setDstDirectory(m_destination.path());
0118         }
0119 
0120         if (i > 0 && m_reset) {
0121             findCounterReset(i);
0122         }
0123 
0124         //qDebug("SRCFILENAME       : %s", (*m_files)[i].srcFilename().toUtf8().data() );
0125         //qDebug("DSTFILENAME SHOULD: %s", processString( text, (*m_files)[i].srcFilename(), i ).toUtf8().data() );
0126         (*m_files)[i].setDstFilename(processString(text, (*m_files)[i].srcFilename(), i));
0127         //qDebug("DSTFILENAME IS    : %s", (*m_files)[i].dstFilename().toUtf8().data());
0128         (*m_files)[i].setDstExtension(processString(extext, (*m_files)[i].srcExtension(), i));
0129 
0130         // Let's run the plugins that change the final filename,
0131         // i.e the encodingsplugin
0132         int errors = 0;
0133         QString name = executePlugin(i, (*m_files)[i].dstFilename(), ePluginType_Filename, errors, nullptr);
0134         if (!name.isNull()) {
0135             (*m_files)[i].setDstFilename(name);
0136         }
0137 
0138         /*
0139          * take care of renamed directories and
0140          * correct the paths of their contents
0141          */
0142         if ((m_renameMode == eRenameMode_Rename ||
0143                 m_renameMode == eRenameMode_Move) &&
0144                 (*m_files)[i].isDirectory()) {
0145             const QString topDir  = (*m_files)[i].realSrcDirectory() + '/' + (*m_files)[i].srcFilename();
0146             const QString replace = (*m_files)[i].dstDirectory() + '/' + (*m_files)[i].dstFilename();
0147 
0148             for (int z = i + 1; z < m_files->count(); z++) {
0149                 const QString &dir = (*m_files)[z].realSrcDirectory();
0150                 if (dir.startsWith(topDir)) {
0151                     QString newDir = replace + dir.right(dir.length() - topDir.length());
0152                     if (newDir != dir) {
0153                         (*m_files)[z].setOverrideSrcDirectory(newDir);
0154                     }
0155                 }
0156             }
0157         }
0158 
0159 #if 0
0160         if (m_files[i].dir && (m_mode == RENAME || m_mode == MOVE)) {
0161             for (unsigned int c = i; c < m_files.count(); c++) {
0162                 if (m_files[c].src.directory.left(m_files[i].src.name.length() + 1)
0163                         == (m_files[i].src.name + "/")) {
0164 
0165                     m_files[c].src.directory.replace(0, m_files[i].src.name.length(), m_files[i].dst.name);
0166                     m_files[c].src.url.setPath(BatchRenamer::buildFilename(&m_files[c].src, true));
0167                 }
0168             }
0169         }
0170 #endif // 0
0171     }
0172 }
0173 
0174 void BatchRenamer::processFiles(ProgressDialog *p)
0175 {
0176     int     errors = 0;
0177     QUrl    dest   = (*m_files)[0].dstUrl();
0178     // TODO: error handling if dest is empty
0179 
0180     // Give the user some information...
0181     p->setProgressTotalSteps(m_files->count());
0182     p->setProgress(0);
0183     p->setDestination(dest);
0184 
0185     switch (m_renameMode) {
0186     default:
0187     case eRenameMode_Rename:
0188         p->print(i18n("Input files will be renamed."));
0189         break;
0190     case eRenameMode_Copy:
0191         p->print(i18n("Files will be copied to: %1", dest.toDisplayString(QUrl::PreferLocalFile)));
0192         break;
0193     case eRenameMode_Move:
0194         p->print(i18n("Files will be moved to: %1", dest.toDisplayString(QUrl::PreferLocalFile)));
0195         break;
0196     case eRenameMode_Link:
0197         p->print(i18n("Symbolic links will be created in: %1", dest.toDisplayString(QUrl::PreferLocalFile)));
0198         break;
0199     }
0200 
0201     for (unsigned int i = 0; i < static_cast<unsigned int>(m_files->count()); i++) {
0202         QUrl    dstUrl    = this->buildDestinationUrl((*m_files)[i]);
0203 
0204         //p->print( QString( "%1 -> %2" ).arg( (*m_files)[i].srcUrl().prettyUrl() ).arg( dstUrl.toDisplayString() ) );
0205         p->setProgress(i + 1);
0206 
0207         if (p->wasCancelled()) {
0208             break;
0209         }
0210 
0211         // Assemble filenames
0212         createMissingSubDirs(dstUrl, p);
0213 
0214         KIO::JobFlags flags  = (m_overwrite ? KIO::Overwrite : KIO::DefaultFlags) | KIO::HideProgressInfo;
0215         KIO::Job     *job    = nullptr;
0216         const QUrl &srcUrl = (*m_files)[i].srcUrl();
0217         if (srcUrl == dstUrl) {
0218             p->warning(i18n("Cannot rename: source and target filename are equal: %1", srcUrl.toDisplayString(QUrl::PreferLocalFile)));
0219             //(*m_files)[i].setError( 1 );
0220             //errors++;
0221             continue;
0222         }
0223 
0224         switch (m_renameMode) {
0225         default:
0226         case eRenameMode_Rename:
0227         case eRenameMode_Move:
0228             job = KIO::file_move(srcUrl, dstUrl, -1, flags);
0229             break;
0230         case eRenameMode_Copy:
0231             job = KIO::file_copy(srcUrl, dstUrl, -1, flags);
0232             break;
0233         case eRenameMode_Link: {
0234             if (!srcUrl.isLocalFile()) {
0235                 // We can only do symlinks to local urls
0236                 p->error(i18n("Cannot create symlink to non-local URL: %1", srcUrl.toDisplayString()));
0237                 (*m_files)[i].setError(1);
0238                 errors++;
0239             } else {
0240                 job = KIO::symlink(srcUrl.path(), dstUrl, flags);
0241             }
0242 
0243             break;
0244         }
0245         }
0246 
0247         if (job) {
0248             KJobWidgets::setWindow(job, p);
0249             if (!job->exec()) {
0250                 p->error(i18n("Error renaming %2 (to %1)",
0251                               dstUrl.toDisplayString(QUrl::PreferLocalFile),
0252                               srcUrl.toDisplayString(QUrl::PreferLocalFile)));
0253                 (*m_files)[i].setError(job->error());
0254                 errors++;
0255             }
0256         }
0257 
0258         /*
0259          * The renamed file should be on its correct location now,
0260          * so that we can call the last plugins (e.g. for changing permissions)
0261          *
0262          * Remember, the token argument is the filename for this type of plugins!
0263          *
0264          * If the return value is not empty an error has occurred!
0265          * The plugin should return an error message in this case!
0266          */
0267         int errorCount = 0;
0268         this->executePlugin(i, dstUrl.path(), ePluginType_File, errorCount, p);
0269         errors += errorCount;
0270     }
0271 
0272     if (errors > 0) {
0273         p->warning(i18np("%1 error occurred.", "%1 errors occurred.", errors));
0274     }
0275 
0276     p->print(i18n("KRename finished the renaming process."), "krename");
0277     p->print(i18n("Press close to quit."));
0278     bool enableUndo = (m_renameMode != eRenameMode_Copy);
0279     p->renamingDone(true, enableUndo, this, errors);
0280 
0281 #if 0
0282     delete object;
0283     t.start();
0284 
0285     m_counters.clear();
0286 
0287     for (unsigned int i = 0; i < m_files->count(); i++) {
0288         m_counter_index = 0;
0289 
0290         if (m_mode == RENAME) { // final Path = source Path
0291             m_files[i].dst.directory = m_files[i].src.directory;
0292             m_files[i].dst.url = m_files[i].src.url;
0293             m_files[i].dst.url.setFileName(QString());
0294         } else {
0295             m_files[i].dst.directory = m_destination.path();
0296             m_files[i].dst.url = m_destination;
0297         }
0298 
0299         if (i == 0) {
0300             p->setDestination(m_files[i].dst.url);
0301         } else {
0302             if (m_reset) {
0303                 findCounterReset(i);
0304             }
0305         }
0306 
0307         m_files[i].dst.name = processString(text, m_files[i].src.name, i);
0308         if (!extext.isEmpty()) {
0309             m_files[i].dst.extension = processString(extext, m_files[i].src.extension, i);
0310         }
0311 
0312         // Assemble filenames
0313         parseSubdirs(&m_files[i]);
0314         // TODO: DOM
0315         // ESCAPE HERE
0316 
0317         m_files[i].src.name = BatchRenamer::buildFilename(&m_files[i].src, true);
0318 
0319         // Let's run the plugins that change the final filename,
0320         // i.e the encodingsplugin
0321         m_files[i].dst.name = parsePlugins(i, m_files[i].dst.name, TYPE_FINAL_FILENAME);
0322 
0323         m_files[i].dst.name = BatchRenamer::buildFilename(&m_files[i].dst, true);
0324 
0325         /*
0326          * take care of renamed directories and
0327          * correct the paths of their contents
0328          */
0329         if (m_files[i].dir && (m_mode == RENAME || m_mode == MOVE)) {
0330             for (unsigned int c = i; c < m_files.count(); c++) {
0331                 if (m_files[c].src.directory.left(m_files[i].src.name.length() + 1)
0332                         == (m_files[i].src.name + "/")) {
0333 
0334                     m_files[c].src.directory.replace(0, m_files[i].src.name.length(), m_files[i].dst.name);
0335                     m_files[c].src.url.setPath(BatchRenamer::buildFilename(&m_files[c].src, true));
0336                 }
0337             }
0338         }
0339     }
0340 
0341     p->print(QString(i18n("Filenames Processed after %1 seconds.", t.elapsed() / 1000)));
0342 
0343     work(p);
0344 #endif // 0
0345 }
0346 
0347 void BatchRenamer::undoFiles(ProgressDialog *p)
0348 {
0349     int     errors = 0;
0350     QUrl    dest   = (*m_files)[0].dstUrl();
0351 
0352     // Give the user some information...
0353     p->setProgressTotalSteps(m_files->count());
0354     p->setProgress(0);
0355     p->setDestination(dest);
0356     p->print(i18n("Undoing all renamed files."));
0357 
0358     for (unsigned int i = 0; i < static_cast<unsigned int>(m_files->count()); i++) {
0359         QUrl dstUrl = this->buildDestinationUrl((*m_files)[i]);
0360 
0361         //p->print( QString( "%1 -> %2" ).arg( (*m_files)[i].srcUrl().prettyUrl() ).arg( dstUrl.toDisplayString() ) );
0362         p->setProgress(i + 1);
0363 
0364         if (p->wasCancelled()) {
0365             break;
0366         }
0367 
0368         KIO::JobFlags flags = (m_overwrite ? KIO::Overwrite : KIO::DefaultFlags) | KIO::HideProgressInfo;
0369         KIO::Job     *job   = nullptr;
0370         switch (m_renameMode) {
0371         default:
0372         case eRenameMode_Rename:
0373         case eRenameMode_Move:
0374             job = KIO::file_move(dstUrl, (*m_files)[i].srcUrl(), -1, flags);
0375             break;
0376         case eRenameMode_Link:
0377             // In case of link delete created file
0378             job = KIO::file_delete(dstUrl, KIO::HideProgressInfo);
0379             break;
0380         case eRenameMode_Copy: // no undo possible
0381             // TODO: Maybe we should delete the created files
0382             break;
0383         }
0384 
0385         if (job) {
0386             KJobWidgets::setWindow(job, p);
0387             if (!job->exec()) {
0388                 p->error(i18n("Error during undoing %1", dstUrl.toDisplayString(QUrl::PreferLocalFile)));
0389                 (*m_files)[i].setError(job->error());
0390                 errors++;
0391             }
0392         }
0393 
0394     }
0395 
0396     if (errors > 0) {
0397         p->warning(i18np("%1 error occurred.", "%1 errors occurred.", errors));
0398     }
0399 
0400     p->print(i18n("KRename finished the undo process."), "krename");
0401     p->print(i18n("Press close to quit."));
0402     p->renamingDone(false, false, this, errors);   // do not allow undo from undo
0403 
0404 }
0405 
0406 QString BatchRenamer::processBrackets(QString text, int *length, const QString &oldname, int index)
0407 {
0408     int  pos   = 0;
0409     QString token;
0410     QString result;
0411 
0412     *length = 0;
0413 
0414     // MSG: qDebug("processBrackets: %s\n", text.toUtf8().data() );
0415     while ((pos = getNextToken(text, token, pos)) != -1) {
0416         if (token == "[") {
0417             int localLength = 0;
0418             QString substitute = processBrackets(text.right(text.length() - pos), &localLength, oldname, index);
0419             text.replace(pos - 1, localLength, substitute);
0420             // MSG: qDebug("substituted: %s\n", text.toUtf8().data() );
0421             // Assure that *length does not become negative,
0422             // this will cause infinite loops
0423             if (localLength < substitute.length()) {
0424                 *length += localLength;
0425             } else {
0426                 *length += (localLength - substitute.length());
0427             }
0428         } else if (token == "]") {
0429             // Done with this token
0430             // MSG: qDebug("END: %s\n", text.left( pos - 1 ).toUtf8().data() );
0431             result = findToken(oldname, text.left(pos - 1), index);
0432             *length += pos + 1;
0433             break;
0434         }
0435     }
0436     // MSG: qDebug("processedBrackets: %s\n", result.toUtf8().data() );
0437 
0438     /*
0439 
0440       if( pos != -1 )
0441       {
0442       result = findToken( oldname, text.left( pos - 1 ), index );
0443       *length = pos+1; // skip any closing bracket
0444       }
0445     */
0446     return result;
0447 }
0448 
0449 QString BatchRenamer::processNumber(int length, const QString &appendix)
0450 {
0451     tCounterValues countervalues;
0452     countervalues.start = m_index;
0453     countervalues.step = m_step;
0454 
0455     if (!appendix.isEmpty()) {
0456         bool ok = false;
0457         int tmp = appendix.section(';', 0, 0).toInt(&ok);     // first section = start index
0458         if (ok) {
0459             countervalues.start = tmp;
0460         }
0461 
0462         tmp = appendix.section(';', 1, 1).toInt(&ok);    // second section = stepping
0463         if (ok) {
0464             countervalues.step = tmp;
0465         }
0466     }
0467 
0468     if ((signed int)m_counters.count() <= m_counter_index) {
0469         countervalues.value = countervalues.start - countervalues.step;
0470         // other wise the counter would start at:
0471         // start + step instead of start
0472         m_counters.append(countervalues);
0473     }
0474 
0475     do {
0476         m_counters[m_counter_index].value += m_counters[m_counter_index].step;
0477     } while (m_skip.contains(m_counters[m_counter_index].value));
0478 
0479     QString number;
0480     number.sprintf("%0*i", length, m_counters[m_counter_index].value);
0481 
0482     ++m_counter_index;
0483     return number;
0484 }
0485 
0486 QString BatchRenamer::processString(QString text, const QString &originalName, int index, bool doFindReplace)
0487 {
0488     QString oldname = originalName;
0489     doEscape(oldname);
0490 
0491     // Parse into tokens
0492     int pos = 0;
0493     QString token;
0494     while ((pos = getNextToken(text, token, pos)) != -1) {
0495 
0496         // Handle simple tokens
0497         if (token == "$") {
0498             text.replace(pos - 1, token.length(), oldname);
0499             pos += oldname.length() - 1;
0500         } else if (token == "%") {
0501             text.replace(pos - 1, token.length(), oldname.toLower());
0502             pos += oldname.length() - 1;
0503         } else if (token == "&") {
0504             text.replace(pos - 1, token.length(), oldname.toUpper());
0505             pos += oldname.length() - 1;
0506         } else if (token == "*") {
0507             QString tmp = capitalize(oldname);
0508 
0509             text.replace(pos - 1, token.length(), tmp);
0510             pos += tmp.length() - 1;
0511         } else if (token == "[") {
0512             int length = 0;
0513             QString substitute = processBrackets(text.right(text.length() - pos), &length, oldname, index);
0514             text.replace(pos - 1, length, substitute);
0515             if (substitute.length() > 0) {
0516                 pos += substitute.length() - 1;
0517             }
0518         } else if (token == "]") {
0519             // Ignore
0520         } else if (token == "#") {
0521             int curPos = pos;
0522             int count  = 1;
0523             while (text[curPos] == '#') {
0524                 ++curPos;
0525                 count++;
0526             }
0527 
0528             int length = curPos - pos + 1;
0529             int appendixLength = 0;
0530             QString appendix;
0531             if (text[curPos] == '{') {
0532                 int     appendixPos = curPos + 1;
0533                 QString appendixToken;
0534                 while ((appendixPos = getNextToken(text, appendixToken, appendixPos)) != -1) {
0535                     if (appendixToken == "}") {
0536                         break;
0537                     }
0538                 }
0539 
0540                 if (appendixPos == -1) {
0541                     // Do go into endless loop if token is not completed correctly
0542                     appendixPos = text.length();
0543                 }
0544 
0545                 // -2 because we have to go, before the current found token
0546                 appendix = text.mid(curPos + 1, appendixPos - curPos - 2);
0547                 appendixLength = appendixPos - curPos;
0548             }
0549 
0550             QString number   = processNumber(count, appendix);
0551             text.replace(pos - 1, (length + appendixLength), number);
0552 
0553             if (number.length() > 0) {
0554                 pos += number.length() - 1;
0555             }
0556         }
0557     }
0558 
0559     //text = parsePlugins( i, text, TYPE_TOKEN );
0560 
0561     /*
0562      * Replace after Plugins !
0563      * Replace should be the last the
0564      * before re-escaping tokens !
0565      */
0566     if (doFindReplace) {
0567         text = findReplace(text, originalName, index);
0568     }
0569     text = unEscape(text);
0570     return text;
0571 }
0572 
0573 QString BatchRenamer::capitalize(const QString &text) const
0574 {
0575     QString tmp = text.toLower();
0576     if (tmp[0].isLetter()) {
0577         tmp[0] = tmp[0].toUpper();
0578     }
0579 
0580     for (int i = 0; i < tmp.length(); i++)
0581         if (tmp[i + 1].isLetter() && !tmp[i].isLetter() &&
0582                 tmp[i] != '\'' && tmp[i] != '?' && tmp[i] != '`') {
0583             tmp[i + 1] = tmp[i + 1].toUpper();
0584         }
0585 
0586     return tmp;
0587 }
0588 
0589 QString BatchRenamer::executePlugin(int index, const QString &filenameOrPath, int type, int &errorCount, ProgressDialog *p)
0590 {
0591     const QList<Plugin *> &plugins = PluginLoader::Instance()->plugins();
0592     QList<Plugin *>::const_iterator it = plugins.begin();
0593 
0594     errorCount = 0;
0595     QString ret = filenameOrPath;
0596     while (it != plugins.end()) {
0597         if ((*it)->isEnabled() && ((*it)->type() & type)) {
0598             // Every plugin should use the return value of the previous as the new filename to work on
0599             ret = (*it)->processFile(this, index, ret, static_cast<EPluginType>(type));
0600             if (type == ePluginType_File) {
0601                 if (! ret.isEmpty()) {
0602                     // An error occurred -> report it
0603                     if (p != nullptr) {
0604                         p->error(ret);
0605                     }
0606                     ++errorCount;
0607                 }
0608 
0609                 ret = filenameOrPath;
0610             }
0611         }
0612 
0613         ++it;
0614     }
0615 
0616     return ret;
0617 }
0618 
0619 void BatchRenamer::work(ProgressDialog *)
0620 {
0621 #if 0
0622     // TODO: use CopyJob here
0623 
0624     FileOperation fop;
0625     QFile *fundo(NULL);
0626     QTextStream *tundo(NULL);
0627 
0628     if (undo) {
0629         // Create header for undo script
0630         fundo = new QFile(m_undoScript);
0631         if (fundo->open(IO_WriteOnly)) {
0632             tundo = new QTextStream(fundo);
0633             writeUndoScript(tundo);
0634         } else {
0635             undo = false;
0636             p->error(i18n("Cannot create undo script: %1", fundo->name()));
0637             delete fundo;
0638         }
0639     }
0640 
0641     int error = 0;
0642     RenamedList *renamedFiles = new RenamedList[m_files.count()];
0643     p->setProgressTotalSteps(m_files.count() + 1);
0644 
0645     /*
0646      * Give the user some information...
0647      */
0648     if (m_mode == COPY) {
0649         p->print(i18n("Files will be copied to: %1", m_files[0].dst.directory));
0650     } else if (m_mode == MOVE) {
0651         p->print(i18n("Files will be moved to: %1", m_files[0].dst.directory));
0652     } else if (m_mode == LINK) {
0653         p->print(i18n("Symbolic links will be created in: %1", m_files[0].dst.directory));
0654     } else if (m_mode == RENAME) {
0655         p->print(i18n("Input files will be renamed."));
0656     }
0657 
0658     unsigned int i;
0659     for (i = 0; i < m_files.count(); i++) {
0660         p->setProgress(i + 1);
0661 
0662         if (p->wasCancelled()) {
0663             break;
0664         }
0665 
0666         KURL src = m_files[i].src.url;
0667         KURL dst = m_files[i].dst.url;
0668         dst.setPath(m_files[i].dst.name);
0669 
0670         renamedFiles[i].src = src;
0671         renamedFiles[i].dst = dst;
0672         renamedFiles[i].dir = m_files[i].dir;
0673 
0674         FileOperation fop;
0675         if (!fop.start(src, dst, m_mode, overwrite)) {
0676             p->error(fop.error());
0677             renamedFiles[i].error = true;
0678             error++;
0679             continue;
0680         } else {
0681             renamedFiles[i].error = false;
0682         }
0683 
0684         // TODO: overwriting of files!
0685         /*
0686          * The renamed file should be on its correct location now,
0687          * so that we can call the last plugins (e.g. for changing permissions)
0688          *
0689          * Remember, the token argument is the filename for this type of plugins!
0690          *
0691          * If the return value is not empty an error has occurred!
0692          * The plugin should return an error message in this case!
0693          */
0694 
0695         QString eplug = parsePlugins(i, QString(), TYPE_FINAL_FILE);
0696         if (!eplug.isEmpty()) {
0697             p->error(eplug);
0698             error++;
0699         }
0700 
0701         /* Create the undo script now */
0702         if (undo)
0703             if (dst.isLocalFile() && src.isLocalFile()) {
0704                 // Plugins ???
0705                 (*tundo) << "echo \"" << dst.fileName()
0706                          << " -> " << src.fileName() << "\"" << endl;
0707                 (*tundo) << "mv -f \"" << m_files[i].dst.name
0708                          << "\" \"" << m_files[i].src.name << "\"" << endl;
0709             } else {
0710                 p->warning(i18n("Undo is not possible for remote file: %1", dst.prettyURL()));
0711             }
0712 
0713     }
0714 
0715     if (!p->wasCancelled()) {
0716         QPtrListIterator<PluginLoader::PluginLibrary> it(plug->libs);
0717         for (; it.current(); ++it) {
0718             if ((*it)->usePlugin) {
0719                 (*it)->plugin->finished();
0720             }
0721         }
0722     }
0723 
0724     const QString m = i18n("Renamed %1 files successfully.", i - error);
0725     (i - error) ? p->print(m) : p->warning(m);
0726 
0727     if (error > 0) {
0728         p->warning(i18np("%1 error occurred.", "%1 errors occurred.", error));
0729     }
0730 
0731     p->print(i18n("Elapsed time: %1 seconds", t.elapsed() / 1000), "kalarm");
0732     p->print(i18n("KRename finished the renaming process."), "krename");
0733     p->print(i18n("Press close to quit."));
0734     p->setRenamedFiles(renamedFiles, m_files.count());
0735 
0736     if (undo) {
0737         (*tundo) << endl << "echo \"Finished undoing " << m_files.count() << " actions.\"" << endl;
0738         delete tundo;
0739         fundo->close();
0740 
0741         // Make fundo exuteable
0742         if (chmod((const char *)m_undoScript, (unsigned int) S_IRUSR | S_IWUSR | S_IXUSR)) {
0743             p->error(i18n("Cannot set executable bit on undo script."));
0744         }
0745         delete fundo;
0746     }
0747 
0748     p->done(error, i - error, m_mode == MOVE || m_mode == RENAME);
0749     m_files.clear();
0750     delete []renamedFiles;
0751     delete this;
0752 #endif // 0
0753 }
0754 
0755 const QUrl BatchRenamer::buildDestinationUrl(const KRenameFile &file) const
0756 {
0757     QUrl    dstUrl    = file.dstUrl();
0758     QString directory = file.dstDirectory();
0759     QString filename  = file.dstFilename();
0760     QString extension = file.dstExtension();
0761     QString manual    = file.manualChanges();
0762 
0763     if (!extension.isEmpty()) {
0764         filename += '.';
0765         filename += extension;
0766     }
0767 
0768     if (!manual.isNull()) {
0769         filename = manual;
0770     }
0771 
0772     dstUrl.setPath(directory + '/' +  filename);
0773 
0774     return dstUrl;
0775 }
0776 
0777 void BatchRenamer::escape(QString &text, const QString &token, const QString &sequence)
0778 {
0779     text.replace(token, sequence);
0780 }
0781 
0782 QString &BatchRenamer::doEscape(QString &text)
0783 {
0784     BatchRenamer::escape(text, "\\", "\\\\");
0785     BatchRenamer::escape(text, "&", "\\&");
0786     BatchRenamer::escape(text, "$", "\\$");
0787     BatchRenamer::escape(text, "%", "\\%");
0788     BatchRenamer::escape(text, "#", "\\#");
0789     BatchRenamer::escape(text, "[", "\\[");
0790     BatchRenamer::escape(text, "]", "\\]");
0791     BatchRenamer::escape(text, "/", "\\/");
0792     BatchRenamer::escape(text, "{", "\\{");
0793     BatchRenamer::escape(text, "}", "\\}");
0794     BatchRenamer::escape(text, "*", "\\*");
0795 
0796     return text;
0797 }
0798 
0799 QString &BatchRenamer::unEscape(QString &text)
0800 {
0801     BatchRenamer::escape(text, "\\\\", "\\");
0802     BatchRenamer::escape(text, "\\&", "&");
0803     BatchRenamer::escape(text, "\\$", "$");
0804     BatchRenamer::escape(text, "\\%", "%");
0805     BatchRenamer::escape(text, "\\#", "#");
0806     BatchRenamer::escape(text, "\\[", "[");
0807     BatchRenamer::escape(text, "\\]", "]");
0808     // %252f == /, it seems that filenames on unix cannot contain
0809     // a /. So I use %252f, at least konqui displays it correctly
0810     // this was needed, so that plugins that return a slash do not cause errors
0811     BatchRenamer::escape(text, "\\/", "%2f");
0812     BatchRenamer::escape(text, "\\{", "{");
0813     BatchRenamer::escape(text, "\\}", "}");
0814     BatchRenamer::escape(text, "\\*", "*");
0815 
0816     return text;
0817 }
0818 
0819 QString BatchRenamer::processToken(const QString &token, const QString &oldname, int i)
0820 {
0821     QString tmp;
0822 
0823     /*
0824      * Call here all functions that handle
0825      * arguments in brackets.
0826      */
0827     tmp = findPartStrings(oldname, token);
0828     if (!tmp.isEmpty()) {
0829         return tmp;
0830     }
0831 
0832     tmp = findDirName(token, (*m_files)[i].srcDirectory());
0833     if (!tmp.isEmpty()) {
0834         return tmp;
0835     }
0836 
0837     tmp = findLength(token, (*m_files)[i].srcFilename());
0838     if (!tmp.isEmpty()) {
0839         return tmp;
0840     }
0841 
0842     tmp = findTrimmed(token, (*m_files)[i].srcFilename(), i);
0843     if (!tmp.isEmpty()) {
0844         return tmp;
0845     }
0846 
0847     tmp = findDirSep(token, (*m_files)[i].srcFilename());
0848     if (!tmp.isEmpty()) {
0849         return tmp;
0850     }
0851 
0852     Plugin *p = PluginLoader::Instance()->findPlugin(token);
0853     if (p) {
0854         tmp = p->processFile(this, i, token, ePluginType_Token);
0855         if (!tmp.isNull()) {
0856             doEscape(tmp);
0857             return tmp;
0858         }
0859     }
0860 
0861     /*
0862      * Maybe I should remove this!
0863      * KRename simply ignores unknown tokens!
0864      * Useful for the MP3 Plugin!
0865      */
0866     return QString();
0867 }
0868 
0869 QString BatchRenamer::findToken(const QString &oldname, QString token, int i)
0870 {
0871     enum conversion { LOWER, UPPER, MIXED, STAR, NONE, EMPTY, NUMBER };
0872     unsigned int numwidth = 0;
0873 
0874     conversion c = EMPTY;
0875     if (!token.left(1).compare("$")) {
0876         c = NONE;
0877     } else if (!token.left(1).compare("%")) {
0878         c = LOWER;
0879     } else if (!token.left(1).compare("&")) {
0880         c = UPPER;
0881     } else if (!token.left(1).compare("")) {
0882         c = MIXED;
0883     } else if (!token.left(1).compare("*")) {
0884         c = STAR;
0885     } else if (!token.left(1).compare("#")) {
0886         while (!token.left(1).compare("#")) {
0887             token.remove(0, 1);
0888             ++numwidth;
0889         }
0890 
0891         c = NUMBER;
0892     }
0893 
0894     if (c != EMPTY && c != NUMBER) {
0895         token.remove(0, 1);
0896     }
0897 
0898     token = processToken(token, oldname, i);
0899 
0900     switch (c) {
0901     case LOWER:
0902         token = token.toLower();
0903         break;
0904     case UPPER:
0905         token = token.toUpper();
0906         break;
0907     case MIXED:
0908         token = token.toLower();
0909         token.replace(0, 1, token[0].toUpper());
0910         break;
0911     case STAR:
0912         token = capitalize(token);
0913         break;
0914     case NUMBER: {
0915         bool b = false;
0916         int n = token.toInt(&b);
0917         if (b) {
0918             token = token.sprintf("%0*i", numwidth, n);
0919         }
0920     }
0921     break;
0922     default:
0923         break;
0924     }
0925 
0926     return token;
0927 }
0928 
0929 QString BatchRenamer::findPartStrings(QString oldname, QString token)
0930 {
0931     QString first, second;
0932     int pos = -1;
0933 
0934     // MSG: qDebug("PART: %s", token.toUtf8().data() );
0935     // parse things like [2;4{[dirname]}]
0936     if (token.count('{') >= 1 && token.count('}') >= 1) {
0937         int pos = token.indexOf('{');
0938         oldname = token.mid(pos + 1, token.lastIndexOf('}') - pos - 1);
0939         token.truncate(pos);
0940     }
0941 
0942     if (token.contains('-')) {
0943         pos = token.indexOf('-', 0);
0944         first = token.left(pos);
0945         // ------- Code OK ^ !
0946 
0947         second = token.mid(pos + 1, token.length());
0948 
0949         // version < 1.7
0950         // return oldname.mid( first.toInt()-1, second.toInt()-first.toInt() +1 );
0951         // version > 1.7
0952         //return oldname.mid( first.toInt()-1, second.toInt()-first.toInt() );
0953         // version > 1.8
0954 
0955         bool ok;
0956         int sec = second.toInt(&ok);
0957         if (!ok || sec == 0) {
0958             sec = oldname.length();
0959         }
0960 
0961         /*
0962          * x should not be larger than the old name
0963          * and not smaller than zero.
0964          */
0965         int x = sec - first.toInt(&ok);
0966         // if first is no number, but for example length, we return here so that findLength can do its job
0967         if (!ok) {
0968             return QString();
0969         }
0970 
0971         if (x > (signed int)oldname.length() || x < 0) {
0972             x = oldname.length() - first.toInt();
0973         }
0974 
0975         /*
0976          * if I would comment my code I would understand this line :)
0977          * without this line, there is sometimes the last letter
0978          * of a filename missing.
0979          */
0980         if (x != -1) {
0981             x++;
0982         }
0983 
0984         oldname = unEscape(oldname);
0985 
0986         return oldname.mid(first.toInt() - 1, x);
0987     } else if (token.contains(';')) {
0988         pos = token.indexOf(';', 0);
0989 
0990         first = token.left(pos);
0991         second = token.mid(pos + 1, token.length());
0992 
0993         oldname = unEscape(oldname);
0994 
0995         return oldname.mid(first.toInt() - 1, second.toInt());
0996     } else {
0997         bool ok = false;
0998         int number = token.toInt(&ok);
0999 
1000         oldname = unEscape(oldname);
1001 
1002         if (ok && (number <= (signed int)oldname.length() && number > 0)) {
1003             return QString(oldname[ number - 1 ]);
1004         } else {
1005             return QString();
1006         }
1007     }
1008 }
1009 
1010 QString BatchRenamer::findDirName(QString token, QString path)
1011 {
1012     if (token.startsWith(QLatin1String("dirname"), Qt::CaseInsensitive)) {
1013         if (path.right(1) == "/") {
1014             path = path.left(path.length() - 1);
1015         }
1016 
1017         int recursion = 1;
1018         if (token.length() > 7) {
1019             token = token.right(token.length() - 7);
1020             recursion = token.count('.');
1021             if (recursion != (signed int)token.length()) {
1022                 return QString();
1023             }
1024 
1025             recursion++;
1026         }
1027 
1028         return path.section("/", recursion * -1, recursion * -1);
1029     }
1030 
1031     return QString();
1032 }
1033 
1034 QString BatchRenamer::findDirSep(const QString &token, const QString &path)
1035 {
1036     if (token.toLower() == "dirsep") {
1037         return "/";
1038     }
1039 
1040     return QString();
1041 }
1042 
1043 QString BatchRenamer::findLength(const QString &token, const QString &name)
1044 {
1045     if (token.startsWith(QLatin1String("length"), Qt::CaseInsensitive)) {
1046         int minus = 0;
1047         if (token.length() > 6 && token[6] == '-') {
1048             bool n = false;
1049             minus = token.mid(7, token.length() - 7).toInt(&n);
1050             if (!n) {
1051                 minus = 0;
1052             }
1053         }
1054 
1055         QString escaped = name;
1056         escaped = doEscape(escaped);
1057 
1058         return QString::number(escaped.length() - minus);
1059     }
1060 
1061     return QString();
1062 }
1063 
1064 QString BatchRenamer::findTrimmed(const QString &token, const QString &name, int index)
1065 {
1066     if (token.startsWith(QLatin1String("trimmed"), Qt::CaseInsensitive)) {
1067         if (token.contains(';')) {
1068             QString processed = processString(
1069                                     token.section(';', 1, 1), name, index).trimmed();
1070 
1071             if (processed.isNull()) {
1072                 return name.trimmed();
1073             } else {
1074                 return processed.trimmed();
1075             }
1076         } else {
1077             return name.trimmed();
1078         }
1079     }
1080 
1081     return QString();
1082 }
1083 
1084 QString BatchRenamer::findReplace(const QString &text, const QString &origFilename, int index)
1085 {
1086     QList<TReplaceItem>::const_iterator it = m_replace.constBegin();
1087 
1088     QString t(text);
1089     while (it != m_replace.constEnd()) {
1090         QString find((*it).find);
1091 
1092         // Call for each element in replace strings doReplace with correct values
1093         t = doReplace(t, unEscape(find), (*it).replace,
1094                       (*it).reg, (*it).doProcessTokens,
1095                       origFilename, index);
1096         ++it;
1097     }
1098 
1099     return t;
1100 }
1101 
1102 QString BatchRenamer::doReplace(const QString &text, const QString &find, const QString &replace, bool reg, bool doProcessTokens, const QString &origFilename, int index)
1103 {
1104     QString t(text);
1105     if (!reg) {
1106         QString escaped = find;
1107         escaped = doEscape(escaped);
1108 
1109         // we use the escaped text here because the user might want
1110         // to find a "&" and replace it
1111         t.replace(escaped, replace);
1112     } else {
1113         // no doEscape() here for the regexp, because it would destroy our regular expression
1114         // other wise we will not find stuff like $, [ in the text
1115         t = unEscape(t).replace(QRegExp(find), replace);
1116         t = doEscape(t);
1117     }
1118 
1119     if (doProcessTokens) {
1120         t = processString(unEscape(t), origFilename, index, false);
1121     }
1122 
1123     return t;
1124 }
1125 
1126 void BatchRenamer::writeUndoScript(QTextStream *t)
1127 {
1128     // write header comments
1129     (*t) << "#!/bin/sh" << endl
1130          << "# KRename Undo Script" << endl << "#" << endl
1131          << "# KRename was written by:" << endl
1132          << "# Dominik Seichter <domseichter@web.de>" << endl
1133          << "# https://www.krename.net/" << endl << "#" << endl
1134          << "# Script generated by KRename Version: " << VERSION << endl << endl
1135          << "# This script must be started with the option --krename to work!" << endl;
1136 
1137     // write functions:
1138     (*t) << "echo \"KRename Undo Script\"" << endl
1139          << "echo \"https://www.krename.net/\"" << endl
1140          << "echo \"\"" << endl;
1141 
1142     (*t) << "if test --krename = $1 ; then" << endl
1143          << "   echo \"\"" << endl
1144          << "else" << endl
1145          << "   echo \"You have to start this script\"" << endl
1146          << "   echo \"with the command line option\"" << endl
1147          << "   echo \"--krename\"" << endl
1148          << "   echo \"to undo a rename operation.\"" << endl
1149          << "   exit" << endl
1150          << "fi" << endl;
1151 }
1152 
1153 void BatchRenamer::createMissingSubDirs(const QUrl &destUrl, ProgressDialog *dialog)
1154 {
1155     QUrl url = destUrl.adjusted(QUrl::RemoveFilename);
1156     if (url.isEmpty()) {
1157         return;
1158     }
1159 
1160     KIO::MkpathJob *job = KIO::mkpath(url);
1161     KJobWidgets::setWindow(job, dialog);
1162     if (!job->exec()) {
1163         dialog->error(i18n("Cannot create directory %1: %2",
1164                            url.toDisplayString(QUrl::PreferLocalFile),
1165                            job->errorString()));
1166     }
1167 }
1168 
1169 void BatchRenamer::findCounterReset(int i)
1170 {
1171     if ((*m_files)[i - 1].srcDirectory() != (*m_files)[i].srcDirectory())
1172         for (int z = 0; z < (int)m_counters.count(); z++) {
1173             m_counters[z].value = m_counters[z].start - m_counters[z].step;
1174         }
1175 }
1176 
1177 #include "moc_batchrenamer.cpp"