diff options
author | Jenkins CI <null@kde.org> | 2013-12-18 00:45:18 +0000 |
---|---|---|
committer | Jenkins CI <null@kde.org> | 2013-12-18 00:45:18 +0000 |
commit | 867e7a50e6396338ab4fe9aa22ad141e4cd344d2 (patch) | |
tree | 1d6f8d6c912fa04dc268b5580bcfe696fa538743 /src/kconf_update | |
parent | c38b88497a833e482e6892b72c8f52adec6de857 (diff) | |
download | kconfig-867e7a50e6396338ab4fe9aa22ad141e4cd344d2.tar.gz kconfig-867e7a50e6396338ab4fe9aa22ad141e4cd344d2.tar.bz2 |
Move kconfig code to the root directory.
Diffstat (limited to 'src/kconf_update')
-rw-r--r-- | src/kconf_update/CMakeLists.txt | 17 | ||||
-rw-r--r-- | src/kconf_update/Mainpage.dox | 31 | ||||
-rw-r--r-- | src/kconf_update/README.kconf_update | 248 | ||||
-rw-r--r-- | src/kconf_update/config-kconf.h.cmake | 4 | ||||
-rw-r--r-- | src/kconf_update/kconf_update.cpp | 967 | ||||
-rw-r--r-- | src/kconf_update/kconfigutils.cpp | 127 | ||||
-rw-r--r-- | src/kconf_update/kconfigutils.h | 43 |
7 files changed, 1437 insertions, 0 deletions
diff --git a/src/kconf_update/CMakeLists.txt b/src/kconf_update/CMakeLists.txt new file mode 100644 index 00000000..e4e37ba9 --- /dev/null +++ b/src/kconf_update/CMakeLists.txt @@ -0,0 +1,17 @@ +find_package(Qt5Core 5.2.0 REQUIRED NO_MODULE) + +remove_definitions(-DQT_NO_CAST_FROM_ASCII) + +configure_file(config-kconf.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kconf.h ) + +########### next target ############### + +set(kconf_update_SRCS + kconf_update.cpp + kconfigutils.cpp + ) + +add_executable(kconf_update ${kconf_update_SRCS}) +target_link_libraries(kconf_update Qt5::Core KF5::ConfigCore) + +install(TARGETS kconf_update DESTINATION ${LIBEXEC_INSTALL_DIR}) diff --git a/src/kconf_update/Mainpage.dox b/src/kconf_update/Mainpage.dox new file mode 100644 index 00000000..77d486ce --- /dev/null +++ b/src/kconf_update/Mainpage.dox @@ -0,0 +1,31 @@ +/** @mainpage ./kconf_update + +kconf_update is a tool designed to update config files. Over time applications +sometimes need to rearrange the way configuration options are stored. Since +such an update shouldn't influence the configuration options that the user +has selected, the application must take care that the options stored in the +old way will still be honored. + +What used to happen is that the application looks up both the old and the +new configuration option and then decides which one to use. This method has +several drawbacks: +- The application may need to read more configuration files than strictly + needed, resulting in a slower startup. +- The application becomes bigger with code that will only be used once. + +kconf_update addresses these problems by offering a framework to update +configuration files without adding code to the application itself. + +See the <a href="http://websvn.kde.org/trunk/KDE/kdelibs/kconf_update/README.kconf_update?view=markup">README file</a> for more information. + +@authors +Waldo Bastian \<bastian@kde.org\> + +@maintainers +[Unknown/None] + +@licenses +@lgpl + +*/ +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/src/kconf_update/README.kconf_update b/src/kconf_update/README.kconf_update new file mode 100644 index 00000000..281fb9e5 --- /dev/null +++ b/src/kconf_update/README.kconf_update @@ -0,0 +1,248 @@ +README kconf_update + +Version: 1.1 +Author: Waldo Bastian <bastian@kde.org>, <bastian@suse.com> + +What it does +============ + +kconf_update is a tool designed to update config files. Over time applications +sometimes need to rearrange the way configuration options are stored. Since +such an update shouldn't influence the configuration options that the user +has selected, the application must take care that the options stored in the +old way will still be honored. + +What used to happen is that the application looks up both the old and the +new configuration option and then decides which one to use. This method has +several drawbacks: +* The application may need to read more configuration files than strictly +needed, resulting in a slower startup. +* The application becomes bigger with code that will only be used once. + +kconf_update addresses these problems by offering a framework to update +configuration files without adding code to the application itself. + + +How it works +============ + +Applications can install so called "update files" under +$KDEDIR/share/apps/kconf_update. An update file has ".upd" as extension and +contains instructions for transferring/converting configuration information +from one place to another. + +Updating the configuration happens automatically, either when KDE gets started +or when kded detects a new update file in the above mentioned location. + +Update files are separated into sections. Each section has an Id. When a +section describing a configuration change has been applied, the Id will be +stored in the file "kconf_updaterc". This information is used to make sure +that a configuration update is only performed once. + +If you overwrite an existing update file with a new version that contains a +new section, only the update instructions from this extra section will be +performed. + +File format of the update file +============================== + +Empty lines or lines that start with '#' are considered comments. +Commas (,) are used to seperate fields and may not occur as part +of any field and all of the keywords are case-sensitive, i.e. you +cannot say "key" instead of "Key" for example. + +For the rest the file is parsed and executed sequentially from top to bottom. +Each line can contain one entry. The following entries are recognized: + + +Id=<id> + +With <id> identifying the group of update entries that follows. Once a group +of entries have been applied, their <id> is stored and this group of entries +will not be applied again. + + +File=<oldfile>,<newfile> +File=<oldfile> + +Specifies that configuration information is read from <oldfile> and written +to <newfile>. If you only specify <oldfile>, the information is read from +as well as written to <oldfile>. Note that if the file does not exist +at the time kconf_update first checks, no related update will be performed +(script won't be run at all, etc.). + + +Script=<script>[,<interpreter>] + +All entries from <oldfile> are piped into <script>. The output of script +is used as new entries for <newfile>. Existing entries can be deleted by +adding lines with "# DELETE [group]key" in the output of the script. +To delete a whole group use "# DELETEGROUP [group]". + +<script> should be installed into $(kde_datadir)/kconf_update, or +kconf_update will not be able to find it. It's also possible to install +kconf_update applications in $(kde_bindir)/kconf_update_bin, which opens the +door to kconf_update applications that are written in C++ and use Qt's +powerful string API instead. + +If Script was issued after a "Group" command the behavior is slightly +different: +All entries from <oldfile>/<oldgroup> are piped into <script>. The output +of script is used as new entries for <newfile>/<newgroup>, unless a different +group is specified with "[group]". Existing entries can be deleted from +<oldgroup> by adding lines with "# DELETE key" in the output of the script. +To delete <oldgroup> use "# DELETEGROUP". + +<interpreter> can be something like "perl". + +It is also possible to have a Script without specifying <oldfile> or +<newfile>. In that case the script is run but it will not be fed any input +and its output will simply be discarded. + +ScriptArguments=<arguments> + +If specified, the arguments will be passed to <script>. +IMPORTANT: It has to be specified before Script=. + +Group=<oldgroup>,<newgroup> +Group=<oldgroup> + +Specifies that configuration information is read from the group <oldgroup> +and written to <newgroup>. If you only specify <oldgroup>, the information +is read from as well as written to <oldgroup>. You can use <default> to +specify keys that are not under any group. +A group may be written either as "group" or as "[group]". The latter syntax +makes it possible to specify subgroups: "[group][subgroup]". + +RemoveGroup=<oldgroup> + +Specifies that <oldgroup> is removed entirely. This can be used +to remove obsolete entries or to force a revert to default values. + +Options=<option1>, <option2>, .... + +With this entry you can specify options that apply to the next "Script", +"Key" or "AllKeys" entry (only to the first!). Possible options are: + +- "copy" Copy the configuration item instead of moving it. This means that + the configuration item will not be deleted from <oldfile>/<oldgroup>. + +- "overwrite" Normally, a configuration item is not moved if an item with the + new name already exists. When this option is specified the old + configuration item will overwrite any existing item. + + +Key=<oldkey>,<newkey> +Key=<oldkey> + +Specifies that configuration information is read from the key <oldkey> +and written to <newkey>. If you only specify <oldkey>, the information +is read from as well as written to <oldkey>. + + +AllKeys + +Specifies that all configuration information in the selected group should +be moved (All keys). + +AllGroups + +Specifies that all configuration information from all keys in ALL +groups should be moved. + + +RemoveKey=<oldkey> + +Specifies that <oldkey> is removed from the selected group. This can be used +to remove obsolete entries or to force a revert to default values. + + +Example update file +=================== + +# This is comment +Id=kde2.2 +File=kioslaverc,kio_httprc +Group=Proxy Settings +Key=NoProxyFor +Key=UseProxy +Key=httpProxy,Proxy +Group=Cache Settings,Cache +Key=MaxCacheSize +Key=UseCache +Group=UserAgent +AllKeys +RemoveGroup=KDE +# End of file + + +The above update file extracts config information from the file "kioslaverc" +and stores it into the file "kio_httprc". + +It reads the keys "NoProxyFor", "UseProxy" and "httpProxy" from the group +"Proxy Settings" in the "kioslaverc" file. If any of these options are present +they are written to the keys "NoProxyFor", "UseProxy" and "Proxy" (!) in +the group "Proxy Settings" in the "kio_httprc" file. + +It also reads the keys "MaxCacheSize" and "UseCache" from the group +"Cache Settings" in the "kioslaverc" file and writes this information to the +keys "MaxCacheSize" and "UseCache" in the group "Cache" (!) in the +"kio_httprc" file. + +Then it takes all keys in the "UserAgent" group of the file "kioslaverc" +and moves then to the "UserAgent" group in the "kio_httprc" file. + +Finally it removes the entire "KDE" group in the kioslaverc file. + + +Debugging and testing +===================== + +If you are developing a kconf_update script and want to test or debug it you +need to make sure kconf_update runs again after each of your changes. There +are a number of ways to achieve this. + +The easiest is to not install the kconf_update script in the first place, but +manually call it through a pipe. If you want to test the update script for +your application KHello's config file khellorc, you can test by using + + cat ~/.kde/share/config/khellorc | khello_conf_update.sh + +(assuming khello_conf_update.sh is the kconf_update script and ~/.kde is your +$KDEHOME). This is easier than making install every time, but has the obvious +downside that you need to 'parse' your script's output yourself instead of +letting kconf_update do it and check the resulting output file. + +After 'make install' the kconf_update script is run by kded, but it does so +only once. This is of course the idea behind it, but while developing it can +be a problem. You can increase the revision number for each subsequent run +of 'make install' to force a new kconf_update run, but there's a better +approach that doesn't skyrocket the version number for a mediocre debug +session. + +kded doesn't really ignore scripts that it has already run right away. +Instead it checks the affected config file every time a .upd file is added +or changed. The reason it still doesn't run again on your config file lies +in the traces kconf_update leaves behind: it adds a special config group +'[$Version]' with a key 'update_info'. This key lists all kconf_update +scripts that have already been run on this config file. It also adds a group +for the script to $KDEHOME/share/config/kconf_updaterc. Just remove your +script entries from both your rcfile and kconf_updaterc, 'make install', +and kconf_update will happily run your script again, without you having to +increase the version number. + +If you want to know what kconf_update has been up to lately, have a look +at $KDEHOME/share/apps/kconf_update/log/update.log + + +Common Problems +=============== + +* kconf_update refuses to update an entry +If you change the value of an entry without changing the key or file, +make sure to tell kconf_update that it should overwrite the old entry +by adding "Options=overwrite". + + +Have fun, +Waldo diff --git a/src/kconf_update/config-kconf.h.cmake b/src/kconf_update/config-kconf.h.cmake new file mode 100644 index 00000000..0f70f8c8 --- /dev/null +++ b/src/kconf_update/config-kconf.h.cmake @@ -0,0 +1,4 @@ +#define CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" +#define LIBEXEC_INSTALL_DIR "${LIBEXEC_INSTALL_DIR}" +#define LIB_INSTALL_DIR "${LIB_INSTALL_DIR}" +#define KCONF_UPDATE_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}" diff --git a/src/kconf_update/kconf_update.cpp b/src/kconf_update/kconf_update.cpp new file mode 100644 index 00000000..60a61db3 --- /dev/null +++ b/src/kconf_update/kconf_update.cpp @@ -0,0 +1,967 @@ +/* + * + * This file is part of the KDE libraries + * Copyright (c) 2001 Waldo Bastian <bastian@kde.org> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + **/ + +#include <config-kconf.h> // CMAKE_INSTALL_PREFIX + +#include <QtCore/QDate> +#include <QtCore/QFile> +#include <QtCore/QTextStream> +#include <QtCore/QTextCodec> +#include <QUrl> +#include <QTemporaryFile> +#include <QCoreApplication> +#include <QtCore/QDir> + +#include <kconfig.h> +#include <kconfiggroup.h> + +#include <qstandardpaths.h> +#include <qcommandlineparser.h> +#include <qcommandlineoption.h> + +#include "kconfigutils.h" + +class KonfUpdate +{ +public: + KonfUpdate(QCommandLineParser *parser); + ~KonfUpdate(); + QStringList findUpdateFiles(bool dirtyOnly); + + QTextStream &log(); + QTextStream &logFileError(); + + bool checkFile(const QString &filename); + void checkGotFile(const QString &_file, const QString &id); + + bool updateFile(const QString &filename); + + void gotId(const QString &_id); + void gotFile(const QString &_file); + void gotGroup(const QString &_group); + void gotRemoveGroup(const QString &_group); + void gotKey(const QString &_key); + void gotRemoveKey(const QString &_key); + void gotAllKeys(); + void gotAllGroups(); + void gotOptions(const QString &_options); + void gotScript(const QString &_script); + void gotScriptArguments(const QString &_arguments); + void resetOptions(); + + void copyGroup(const KConfigBase *cfg1, const QString &group1, + KConfigBase *cfg2, const QString &group2); + void copyGroup(const KConfigGroup &cg1, KConfigGroup &cg2); + void copyOrMoveKey(const QStringList &srcGroupPath, const QString &srcKey, const QStringList &dstGroupPath, const QString &dstKey); + void copyOrMoveGroup(const QStringList &srcGroupPath, const QStringList &dstGroupPath); + + QStringList parseGroupString(const QString &_str); + +protected: + KConfig *m_config; + QString m_currentFilename; + bool m_skip; + bool m_skipFile; + bool m_debug; + QString m_id; + + QString m_oldFile; + QString m_newFile; + QString m_newFileName; + KConfig *m_oldConfig1; // Config to read keys from. + KConfig *m_oldConfig2; // Config to delete keys from. + KConfig *m_newConfig; + + QStringList m_oldGroup; + QStringList m_newGroup; + + bool m_bCopy; + bool m_bOverwrite; + bool m_bUseConfigInfo; + QString m_arguments; + QTextStream *m_textStream; + QFile *m_file; + QString m_line; + int m_lineCount; +}; + +KonfUpdate::KonfUpdate(QCommandLineParser * parser) + : m_textStream(0), m_file(0) +{ + bool updateAll = false; + m_oldConfig1 = 0; + m_oldConfig2 = 0; + m_newConfig = 0; + + m_config = new KConfig("kconf_updaterc"); + KConfigGroup cg(m_config, QString()); + + QStringList updateFiles; + + m_debug = parser->isSet("debug"); + + m_bUseConfigInfo = false; + if (parser->isSet("check")) { + m_bUseConfigInfo = true; + QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kconf_update/" + parser->value("check")); + if (file.isEmpty()) { + qWarning("File '%s' not found.", parser->value("check").toLocal8Bit().data()); + log() << "File '" << parser->value("check") << "' passed on command line not found" << endl; + return; + } + updateFiles.append(file); + } else if (parser->positionalArguments().count()) { + updateFiles += parser->positionalArguments(); + } else { + if (cg.readEntry("autoUpdateDisabled", false)) + return; + updateFiles = findUpdateFiles(true); + updateAll = true; + } + + for (QStringList::ConstIterator it = updateFiles.constBegin(); + it != updateFiles.constEnd(); + ++it) { + updateFile(*it); + } + + if (updateAll && !cg.readEntry("updateInfoAdded", false)) { + cg.writeEntry("updateInfoAdded", true); + updateFiles = findUpdateFiles(false); + + for (QStringList::ConstIterator it = updateFiles.constBegin(); + it != updateFiles.constEnd(); + ++it) { + checkFile(*it); + } + updateFiles.clear(); + } +} + +KonfUpdate::~KonfUpdate() +{ + delete m_config; + delete m_file; + delete m_textStream; +} + +QTextStream & operator<<(QTextStream & stream, const QStringList & lst) +{ + stream << lst.join(", "); + return stream; +} + +QTextStream & +KonfUpdate::log() +{ + if (!m_textStream) { + QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "kconf_update/log"; + QDir().mkpath(dir); + QString file = dir + "/update.log"; + m_file = new QFile(file); + if (m_file->open(QIODevice::WriteOnly | QIODevice::Append)) { + m_textStream = new QTextStream(m_file); + } else { + // Error + m_textStream = new QTextStream(stderr, QIODevice::WriteOnly); + } + } + + (*m_textStream) << QDateTime::currentDateTime().toString(Qt::ISODate) << " "; + + return *m_textStream; +} + +QTextStream & +KonfUpdate::logFileError() +{ + return log() << m_currentFilename << ':' << m_lineCount << ":'" << m_line << "': "; +} + +QStringList KonfUpdate::findUpdateFiles(bool dirtyOnly) +{ + QStringList result; + + const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "kconf_update", QStandardPaths::LocateDirectory); + Q_FOREACH(const QString& dir, dirs) { + const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.upd")); + Q_FOREACH(const QString& fileName, fileNames) { + const QString file = dir + '/' + fileName; + QFileInfo info(file); + + KConfigGroup cg(m_config, fileName); + const QDateTime ctime = QDateTime::fromTime_t(cg.readEntry("ctime", 0)); + const QDateTime mtime = QDateTime::fromTime_t(cg.readEntry("mtime", 0)); + if (!dirtyOnly || + (ctime != info.created()) || (mtime != info.lastModified())) { + result.append(file); + } + } + } + return result; +} + +bool KonfUpdate::checkFile(const QString &filename) +{ + m_currentFilename = filename; + int i = m_currentFilename.lastIndexOf('/'); + if (i != -1) { + m_currentFilename = m_currentFilename.mid(i + 1); + } + m_skip = true; + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + QTextStream ts(&file); + ts.setCodec(QTextCodec::codecForName("ISO-8859-1")); + int lineCount = 0; + resetOptions(); + QString id; + while (!ts.atEnd()) { + QString line = ts.readLine().trimmed(); + lineCount++; + if (line.isEmpty() || (line[0] == '#')) { + continue; + } + if (line.startsWith("Id=")) { + id = m_currentFilename + ':' + line.mid(3); + } else if (line.startsWith("File=")) { + checkGotFile(line.mid(5), id); + } + } + + return true; +} + +void KonfUpdate::checkGotFile(const QString &_file, const QString &id) +{ + QString file; + int i = _file.indexOf(','); + if (i == -1) { + file = _file.trimmed(); + } else { + file = _file.mid(i + 1).trimmed(); + } + +// qDebug("File %s, id %s", file.toLatin1().constData(), id.toLatin1().constData()); + + KConfig cfg(file, KConfig::SimpleConfig); + KConfigGroup cg(&cfg, "$Version"); + QStringList ids = cg.readEntry("update_info", QStringList()); + if (ids.contains(id)) { + return; + } + ids.append(id); + cg.writeEntry("update_info", ids); +} + +/** + * Syntax: + * # Comment + * Id=id + * File=oldfile[,newfile] + * AllGroups + * Group=oldgroup[,newgroup] + * RemoveGroup=oldgroup + * Options=[copy,][overwrite,] + * Key=oldkey[,newkey] + * RemoveKey=ldkey + * AllKeys + * Keys= [Options](AllKeys|(Key|RemoveKey)*) + * ScriptArguments=arguments + * Script=scriptfile[,interpreter] + * + * Sequence: + * (Id,(File(Group,Keys)*)*)* + **/ +bool KonfUpdate::updateFile(const QString &filename) +{ + m_currentFilename = filename; + int i = m_currentFilename.lastIndexOf('/'); + if (i != -1) { + m_currentFilename = m_currentFilename.mid(i + 1); + } + m_skip = true; + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + log() << "Checking update-file '" << filename << "' for new updates" << endl; + + QTextStream ts(&file); + ts.setCodec(QTextCodec::codecForName("ISO-8859-1")); + m_lineCount = 0; + resetOptions(); + while (!ts.atEnd()) { + m_line = ts.readLine().trimmed(); + m_lineCount++; + if (m_line.isEmpty() || (m_line[0] == '#')) { + continue; + } + if (m_line.startsWith(QLatin1String("Id="))) { + gotId(m_line.mid(3)); + } else if (m_skip) { + continue; + } else if (m_line.startsWith(QLatin1String("Options="))) { + gotOptions(m_line.mid(8)); + } else if (m_line.startsWith(QLatin1String("File="))) { + gotFile(m_line.mid(5)); + } else if (m_skipFile) { + continue; + } else if (m_line.startsWith(QLatin1String("Group="))) { + gotGroup(m_line.mid(6)); + } else if (m_line.startsWith(QLatin1String("RemoveGroup="))) { + gotRemoveGroup(m_line.mid(12)); + resetOptions(); + } else if (m_line.startsWith(QLatin1String("Script="))) { + gotScript(m_line.mid(7)); + resetOptions(); + } else if (m_line.startsWith(QLatin1String("ScriptArguments="))) { + gotScriptArguments(m_line.mid(16)); + } else if (m_line.startsWith(QLatin1String("Key="))) { + gotKey(m_line.mid(4)); + resetOptions(); + } else if (m_line.startsWith(QLatin1String("RemoveKey="))) { + gotRemoveKey(m_line.mid(10)); + resetOptions(); + } else if (m_line == "AllKeys") { + gotAllKeys(); + resetOptions(); + } else if (m_line == "AllGroups") { + gotAllGroups(); + resetOptions(); + } else { + logFileError() << "Parse error" << endl; + } + } + // Flush. + gotId(QString()); + + QFileInfo info(filename); + KConfigGroup cg(m_config, m_currentFilename); + cg.writeEntry("ctime", info.created().toTime_t()); + cg.writeEntry("mtime", info.lastModified().toTime_t()); + cg.sync(); + return true; +} + + + +void KonfUpdate::gotId(const QString &_id) +{ + if (!m_id.isEmpty() && !m_skip) { + KConfigGroup cg(m_config, m_currentFilename); + + QStringList ids = cg.readEntry("done", QStringList()); + if (!ids.contains(m_id)) { + ids.append(m_id); + cg.writeEntry("done", ids); + cg.sync(); + } + } + + // Flush pending changes + gotFile(QString()); + KConfigGroup cg(m_config, m_currentFilename); + + QStringList ids = cg.readEntry("done", QStringList()); + if (!_id.isEmpty()) { + if (ids.contains(_id)) { + //qDebug("Id '%s' was already in done-list", _id.toLatin1().constData()); + if (!m_bUseConfigInfo) { + m_skip = true; + return; + } + } + m_skip = false; + m_skipFile = false; + m_id = _id; + if (m_bUseConfigInfo) { + log() << m_currentFilename << ": Checking update '" << _id << "'" << endl; + } else { + log() << m_currentFilename << ": Found new update '" << _id << "'" << endl; + } + } +} + +void KonfUpdate::gotFile(const QString &_file) +{ + // Reset group + gotGroup(QString()); + + if (!m_oldFile.isEmpty()) { + // Close old file. + delete m_oldConfig1; + m_oldConfig1 = 0; + + KConfigGroup cg(m_oldConfig2, "$Version"); + QStringList ids = cg.readEntry("update_info", QStringList()); + QString cfg_id = m_currentFilename + ':' + m_id; + if (!ids.contains(cfg_id) && !m_skip) { + ids.append(cfg_id); + cg.writeEntry("update_info", ids); + } + cg.sync(); + delete m_oldConfig2; + m_oldConfig2 = 0; + + QString file = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + m_oldFile; + QFileInfo info(file); + if (info.exists() && info.size() == 0) { + // Delete empty file. + QFile::remove(file); + } + + m_oldFile.clear(); + } + if (!m_newFile.isEmpty()) { + // Close new file. + KConfigGroup cg(m_newConfig, "$Version"); + QStringList ids = cg.readEntry("update_info", QStringList()); + QString cfg_id = m_currentFilename + ':' + m_id; + if (!ids.contains(cfg_id) && !m_skip) { + ids.append(cfg_id); + cg.writeEntry("update_info", ids); + } + m_newConfig->sync(); + delete m_newConfig; + m_newConfig = 0; + + m_newFile.clear(); + } + m_newConfig = 0; + + int i = _file.indexOf(','); + if (i == -1) { + m_oldFile = _file.trimmed(); + } else { + m_oldFile = _file.left(i).trimmed(); + m_newFile = _file.mid(i + 1).trimmed(); + if (m_oldFile == m_newFile) { + m_newFile.clear(); + } + } + + if (!m_oldFile.isEmpty()) { + m_oldConfig2 = new KConfig(m_oldFile, KConfig::NoGlobals); + QString cfg_id = m_currentFilename + ':' + m_id; + KConfigGroup cg(m_oldConfig2, "$Version"); + QStringList ids = cg.readEntry("update_info", QStringList()); + if (ids.contains(cfg_id)) { + m_skip = true; + m_newFile.clear(); + log() << m_currentFilename << ": Skipping update '" << m_id << "'" << endl; + } + + if (!m_newFile.isEmpty()) { + m_newConfig = new KConfig(m_newFile, KConfig::NoGlobals); + KConfigGroup cg(m_newConfig, "$Version"); + ids = cg.readEntry("update_info", QStringList()); + if (ids.contains(cfg_id)) { + m_skip = true; + log() << m_currentFilename << ": Skipping update '" << m_id << "'" << endl; + } + } else { + m_newConfig = m_oldConfig2; + } + + m_oldConfig1 = new KConfig(m_oldFile, KConfig::NoGlobals); + } else { + m_newFile.clear(); + } + m_newFileName = m_newFile; + if (m_newFileName.isEmpty()) { + m_newFileName = m_oldFile; + } + + m_skipFile = false; + if (!m_oldFile.isEmpty()) { // if File= is specified, it doesn't exist, is empty or contains only kconf_update's [$Version] group, skip + if (m_oldConfig1 != NULL + && (m_oldConfig1->groupList().isEmpty() + || (m_oldConfig1->groupList().count() == 1 && m_oldConfig1->groupList().first() == "$Version"))) { + log() << m_currentFilename << ": File '" << m_oldFile << "' does not exist or empty, skipping" << endl; + m_skipFile = true; + } + } +} + +QStringList KonfUpdate::parseGroupString(const QString &str) +{ + bool ok; + QString error; + QStringList lst = KConfigUtils::parseGroupString(str, &ok, &error); + if (!ok) { + logFileError() << error; + } + return lst; +} + +void KonfUpdate::gotGroup(const QString &_group) +{ + QString group = _group.trimmed(); + if (group.isEmpty()) { + m_oldGroup = m_newGroup = QStringList(); + return; + } + + QStringList tokens = group.split(','); + m_oldGroup = parseGroupString(tokens.at(0)); + if (tokens.count() == 1) { + m_newGroup = m_oldGroup; + } else { + m_newGroup = parseGroupString(tokens.at(1)); + } +} + +void KonfUpdate::gotRemoveGroup(const QString &_group) +{ + m_oldGroup = parseGroupString(_group); + + if (!m_oldConfig1) { + logFileError() << "RemoveGroup without previous File specification" << endl; + return; + } + + KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig2, m_oldGroup); + if (!cg.exists()) { + return; + } + // Delete group. + cg.deleteGroup(); + log() << m_currentFilename << ": RemoveGroup removes group " << m_oldFile << ":" << m_oldGroup << endl; +} + + +void KonfUpdate::gotKey(const QString &_key) +{ + QString oldKey, newKey; + int i = _key.indexOf(','); + if (i == -1) { + oldKey = _key.trimmed(); + newKey = oldKey; + } else { + oldKey = _key.left(i).trimmed(); + newKey = _key.mid(i + 1).trimmed(); + } + + if (oldKey.isEmpty() || newKey.isEmpty()) { + logFileError() << "Key specifies invalid key" << endl; + return; + } + if (!m_oldConfig1) { + logFileError() << "Key without previous File specification" << endl; + return; + } + copyOrMoveKey(m_oldGroup, oldKey, m_newGroup, newKey); +} + +void KonfUpdate::copyOrMoveKey(const QStringList &srcGroupPath, const QString &srcKey, const QStringList &dstGroupPath, const QString &dstKey) +{ + KConfigGroup dstCg = KConfigUtils::openGroup(m_newConfig, dstGroupPath); + if (!m_bOverwrite && dstCg.hasKey(dstKey)) { + log() << m_currentFilename << ": Skipping " << m_newFileName << ":" << dstCg.name() << ":" << dstKey << ", already exists." << endl; + return; + } + + KConfigGroup srcCg = KConfigUtils::openGroup(m_oldConfig1, srcGroupPath); + if (!srcCg.hasKey(srcKey)) + return; + QString value = srcCg.readEntry(srcKey, QString()); + log() << m_currentFilename << ": Updating " << m_newFileName << ":" << dstCg.name() << ":" << dstKey << " to '" << value << "'" << endl; + dstCg.writeEntry(dstKey, value); + + if (m_bCopy) { + return; // Done. + } + + // Delete old entry + if (m_oldConfig2 == m_newConfig + && srcGroupPath == dstGroupPath + && srcKey == dstKey) { + return; // Don't delete! + } + KConfigGroup srcCg2 = KConfigUtils::openGroup(m_oldConfig2, srcGroupPath); + srcCg2.deleteEntry(srcKey); + log() << m_currentFilename << ": Removing " << m_oldFile << ":" << srcCg2.name() << ":" << srcKey << ", moved." << endl; +} + +void KonfUpdate::copyOrMoveGroup(const QStringList &srcGroupPath, const QStringList &dstGroupPath) +{ + KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig1, srcGroupPath); + + // Keys + Q_FOREACH(const QString &key, cg.keyList()) { + copyOrMoveKey(srcGroupPath, key, dstGroupPath, key); + } + + // Subgroups + Q_FOREACH(const QString &group, cg.groupList()) { + QStringList groupPath = QStringList() << group; + copyOrMoveGroup(srcGroupPath + groupPath, dstGroupPath + groupPath); + } +} + +void KonfUpdate::gotRemoveKey(const QString &_key) +{ + QString key = _key.trimmed(); + + if (key.isEmpty()) { + logFileError() << "RemoveKey specifies invalid key" << endl; + return; + } + + if (!m_oldConfig1) { + logFileError() << "Key without previous File specification" << endl; + return; + } + + KConfigGroup cg1 = KConfigUtils::openGroup(m_oldConfig1, m_oldGroup); + if (!cg1.hasKey(key)) { + return; + } + log() << m_currentFilename << ": RemoveKey removes " << m_oldFile << ":" << m_oldGroup << ":" << key << endl; + + // Delete old entry + KConfigGroup cg2 = KConfigUtils::openGroup(m_oldConfig2, m_oldGroup); + cg2.deleteEntry(key); + /*if (m_oldConfig2->deleteGroup(m_oldGroup, KConfig::Normal)) { // Delete group if empty. + log() << m_currentFilename << ": Removing empty group " << m_oldFile << ":" << m_oldGroup << endl; + } (this should be automatic)*/ +} + +void KonfUpdate::gotAllKeys() +{ + if (!m_oldConfig1) { + logFileError() << "AllKeys without previous File specification" << endl; + return; + } + + copyOrMoveGroup(m_oldGroup, m_newGroup); +} + +void KonfUpdate::gotAllGroups() +{ + if (!m_oldConfig1) { + logFileError() << "AllGroups without previous File specification" << endl; + return; + } + + const QStringList allGroups = m_oldConfig1->groupList(); + for (QStringList::ConstIterator it = allGroups.begin(); + it != allGroups.end(); ++it) { + m_oldGroup = QStringList() << *it; + m_newGroup = m_oldGroup; + gotAllKeys(); + } +} + +void KonfUpdate::gotOptions(const QString &_options) +{ + const QStringList options = _options.split(','); + for (QStringList::ConstIterator it = options.begin(); + it != options.end(); + ++it) { + if ((*it).toLower().trimmed() == "copy") { + m_bCopy = true; + } + + if ((*it).toLower().trimmed() == "overwrite") { + m_bOverwrite = true; + } + } +} + +void KonfUpdate::copyGroup(const KConfigBase *cfg1, const QString &group1, + KConfigBase *cfg2, const QString &group2) +{ + KConfigGroup cg1(cfg1, group1); + KConfigGroup cg2(cfg2, group2); + copyGroup(cg1, cg2); +} + +void KonfUpdate::copyGroup(const KConfigGroup &cg1, KConfigGroup &cg2) +{ + // Copy keys + QMap<QString, QString> list = cg1.entryMap(); + for (QMap<QString, QString>::ConstIterator it = list.constBegin(); + it != list.constEnd(); ++it) { + if (m_bOverwrite || !cg2.hasKey(it.key())) { + cg2.writeEntry(it.key(), it.value()); + } + } + + // Copy subgroups + Q_FOREACH(const QString &group, cg1.groupList()) { + copyGroup(&cg1, group, &cg2, group); + } +} + +void KonfUpdate::gotScriptArguments(const QString &_arguments) +{ + m_arguments = _arguments; +} + +void KonfUpdate::gotScript(const QString &_script) +{ + QString script, interpreter; + int i = _script.indexOf(','); + if (i == -1) { + script = _script.trimmed(); + } else { + script = _script.left(i).trimmed(); + interpreter = _script.mid(i + 1).trimmed(); + } + + + if (script.isEmpty()) { + logFileError() << "Script fails to specify filename"; + m_skip = true; + return; + } + + + + QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kconf_update/" + script); + if (path.isEmpty()) { + if (interpreter.isEmpty()) { + // KDE4: this was looking into locate("lib", "kconf_update_bin/"). But QStandardPaths doesn't know the lib dirs. + // Let's look in the install prefix and in PATH. + path = CMAKE_INSTALL_PREFIX "/" LIB_INSTALL_DIR "/kconf_update_bin/" + script; + if (QFile::exists(path)) + path = QStandardPaths::findExecutable(script); + } + + if (path.isEmpty()) { + logFileError() << "Script '" << script << "' not found" << endl; + m_skip = true; + return; + } + } + + if (!m_arguments.isNull()) { + log() << m_currentFilename << ": Running script '" << script << "' with arguments '" << m_arguments << "'" << endl; + } else { + log() << m_currentFilename << ": Running script '" << script << "'" << endl; + } + + QString cmd; + if (interpreter.isEmpty()) { + cmd = path; + } else { + cmd = interpreter + ' ' + path; + } + + if (!m_arguments.isNull()) { + cmd += ' '; + cmd += m_arguments; + } + + QTemporaryFile scriptIn; + scriptIn.open(); + QTemporaryFile scriptOut; + scriptOut.open(); + QTemporaryFile scriptErr; + scriptErr.open(); + + int result; + if (m_oldConfig1) { + if (m_debug) { + scriptIn.setAutoRemove(false); + log() << "Script input stored in " << scriptIn.fileName() << endl; + } + KConfig cfg(scriptIn.fileName(), KConfig::SimpleConfig); + + if (m_oldGroup.isEmpty()) { + // Write all entries to tmpFile; + const QStringList grpList = m_oldConfig1->groupList(); + for (QStringList::ConstIterator it = grpList.begin(); + it != grpList.end(); + ++it) { + copyGroup(m_oldConfig1, *it, &cfg, *it); + } + } else { + KConfigGroup cg1 = KConfigUtils::openGroup(m_oldConfig1, m_oldGroup); + KConfigGroup cg2(&cfg, QString()); + copyGroup(cg1, cg2); + } + cfg.sync(); +#ifndef _WIN32_WCE + result = system(QFile::encodeName(QString("%1 < %2 > %3 2> %4").arg(cmd, scriptIn.fileName(), scriptOut.fileName(), scriptErr.fileName())).constData()); +#else + QString path_ = QDir::convertSeparators ( QFileInfo ( cmd ).absoluteFilePath() ); + QString file_ = QFileInfo ( cmd ).fileName(); + SHELLEXECUTEINFO execInfo; + memset ( &execInfo,0,sizeof ( execInfo ) ); + execInfo.cbSize = sizeof ( execInfo ); + execInfo.fMask = SEE_MASK_FLAG_NO_UI; + execInfo.lpVerb = L"open"; + execInfo.lpFile = (LPCWSTR) path_.utf16(); + execInfo.lpDirectory = (LPCWSTR) file_.utf16(); + execInfo.lpParameters = (LPCWSTR) QString(" < %1 > %2 2> %3").arg( scriptIn.fileName(), scriptOut.fileName(), scriptErr.fileName()).utf16(); + result = ShellExecuteEx ( &execInfo ); + if (result != 0) + { + result = 0; + } + else + { + result = -1; + } +#endif + } else { + // No config file +#ifndef _WIN32_WCE + result = system(QFile::encodeName(QString("%1 2> %2").arg(cmd, scriptErr.fileName())).constData()); +#else + QString path_ = QDir::convertSeparators ( QFileInfo ( cmd ).absoluteFilePath() ); + QString file_ = QFileInfo ( cmd ).fileName(); + SHELLEXECUTEINFO execInfo; + memset ( &execInfo,0,sizeof ( execInfo ) ); + execInfo.cbSize = sizeof ( execInfo ); + execInfo.fMask = SEE_MASK_FLAG_NO_UI; + execInfo.lpVerb = L"open"; + execInfo.lpFile = (LPCWSTR) path_.utf16(); + execInfo.lpDirectory = (LPCWSTR) file_.utf16(); + execInfo.lpParameters = (LPCWSTR) QString(" 2> %1").arg( scriptErr.fileName()).utf16(); + result = ShellExecuteEx ( &execInfo ); + if (result != 0) + { + result = 0; + } + else + { + result = -1; + } +#endif + } + + // Copy script stderr to log file + { + QFile output(scriptErr.fileName()); + if (output.open(QIODevice::ReadOnly)) { + QTextStream ts(&output); + ts.setCodec(QTextCodec::codecForName("UTF-8")); + while (!ts.atEnd()) { + QString line = ts.readLine(); + log() << "[Script] " << line << endl; + } + } + } + + if (result) { + log() << m_currentFilename << ": !! An error occurred while running '" << cmd << "'" << endl; + return; + } + + if (!m_oldConfig1) { + return; // Nothing to merge + } + + if (m_debug) { + scriptOut.setAutoRemove(false); + log() << "Script output stored in " << scriptOut.fileName() << endl; + } + + // Deleting old entries + { + QStringList group = m_oldGroup; + QFile output(scriptOut.fileName()); + if (output.open(QIODevice::ReadOnly)) { + QTextStream ts(&output); + ts.setCodec(QTextCodec::codecForName("UTF-8")); + while (!ts.atEnd()) { + QString line = ts.readLine(); + if (line.startsWith('[')) { + group = parseGroupString(line); + } else if (line.startsWith(QLatin1String("# DELETE "))) { + QString key = line.mid(9); + if (key[0] == '[') { + int j = key.lastIndexOf(']') + 1; + if (j > 0) { + group = parseGroupString(key.left(j)); + key = key.mid(j); + } + } + KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig2, group); + cg.deleteEntry(key); + log() << m_currentFilename << ": Script removes " << m_oldFile << ":" << group << ":" << key << endl; + /*if (m_oldConfig2->deleteGroup(group, KConfig::Normal)) { // Delete group if empty. + log() << m_currentFilename << ": Removing empty group " << m_oldFile << ":" << group << endl; + } (this should be automatic)*/ + } else if (line.startsWith(QLatin1String("# DELETEGROUP"))) { + QString str = line.mid(13).trimmed(); + if (!str.isEmpty()) { + group = parseGroupString(str); + } + KConfigGroup cg = KConfigUtils::openGroup(m_oldConfig2, group); + cg.deleteGroup(); + log() << m_currentFilename << ": Script removes group " << m_oldFile << ":" << group << endl; + } + } + } + } + + // Merging in new entries. + KConfig scriptOutConfig(scriptOut.fileName(), KConfig::NoGlobals); + if (m_newGroup.isEmpty()) { + // Copy "default" keys as members of "default" keys + copyGroup(&scriptOutConfig, QString(), m_newConfig, QString()); + } else { + // Copy default keys as members of m_newGroup + KConfigGroup srcCg = KConfigUtils::openGroup(&scriptOutConfig, QStringList()); + KConfigGroup dstCg = KConfigUtils::openGroup(m_newConfig, m_newGroup); + copyGroup(srcCg, dstCg); + } + Q_FOREACH(const QString &group, scriptOutConfig.groupList()) { + copyGroup(&scriptOutConfig, group, m_newConfig, group); + } +} + +void KonfUpdate::resetOptions() +{ + m_bCopy = false; + m_bOverwrite = false; + m_arguments.clear(); +} + + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + app.setApplicationVersion("1.1"); + + QCommandLineParser parser; + parser.addVersionOption(); + parser.setApplicationDescription(QCoreApplication::translate("main", "KDE Tool for updating user configuration files")); + parser.addHelpOption(); + parser.addOption(QCommandLineOption(QStringList() << "debug", QCoreApplication::translate("main", "Keep output results from scripts"))); + parser.addOption(QCommandLineOption(QStringList() << "check", QCoreApplication::translate("main", "Check whether config file itself requires updating"), "update-file")); + //parser.addOption(QCommandLineOption(QStringList() << "+[file]", QCoreApplication::translate("main", "File to read update instructions from"))); + + // TODO aboutData.addAuthor(ki18n("Waldo Bastian"), KLocalizedString(), "bastian@kde.org"); + + parser.process(app); + KonfUpdate konfUpdate(&parser); + + return 0; +} diff --git a/src/kconf_update/kconfigutils.cpp b/src/kconf_update/kconfigutils.cpp new file mode 100644 index 00000000..f2663e13 --- /dev/null +++ b/src/kconf_update/kconfigutils.cpp @@ -0,0 +1,127 @@ +/* This file is part of the KDE libraries + Copyright 2010 Canonical Ltd + Author: Aurélien Gâteau <aurelien.gateau@canonical.com> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License (LGPL) as published by the Free Software Foundation; + either version 2 of the License, or (at your option) any later + version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#include "kconfigutils.h" + +// KDE +#include <kconfig.h> +#include <kconfiggroup.h> + +namespace KConfigUtils +{ + +bool hasGroup(KConfig *config, const QStringList &lst) +{ + KConfigGroup group = openGroup(config, lst); + return group.exists(); +} + +KConfigGroup openGroup(KConfig *config, const QStringList &_lst) +{ + if (_lst.isEmpty()) { + return KConfigGroup(config, QString()); + } + + QStringList lst = _lst; + + KConfigGroup cg; + for (cg = KConfigGroup(config, lst.takeFirst()); !lst.isEmpty(); cg = KConfigGroup(&cg, lst.takeFirst())) {} + return cg; +} + +QStringList parseGroupString(const QString &_str, bool *ok, QString *error) +{ + QString str = unescapeString(_str.trimmed(), ok, error); + if (!ok) { + return QStringList(); + } + + *ok = true; + if (str[0] != '[') { + // Simplified notation, no '[' + return QStringList() << str; + } + + if (!str.endsWith(']')) { + *ok = false; + *error = QString("Missing closing ']' in %1").arg(_str); + return QStringList(); + } + // trim outer brackets + str.chop(1); + str.remove(0, 1); + + return str.split("]["); +} + +QString unescapeString(const QString &src, bool *ok, QString *error) +{ + QString dst; + int length = src.length(); + for (int pos = 0; pos < length; ++pos) { + QChar ch = src.at(pos); + if (ch != '\\') { + dst += ch; + } else { + ++pos; + if (pos == length) { + *ok = false; + *error = QString("Unfinished escape sequence in %1").arg(src); + return QString(); + } + ch = src.at(pos); + if (ch == 's') { + dst += ' '; + } else if (ch == 't') { + dst += '\t'; + } else if (ch == 'n') { + dst += '\n'; + } else if (ch == 'r') { + dst += '\r'; + } else if (ch == '\\') { + dst += '\\'; + } else if (ch == 'x') { + if (pos + 2 < length) { + char value = src.mid(pos + 1, 2).toInt(ok, 16); + if (*ok) { + dst += QChar::fromLatin1(value); + pos += 2; + } else { + *error = QString("Invalid hex escape sequence at column %1 in %2").arg(pos).arg(src); + return QString(); + } + } else { + *ok = false; + *error = QString("Unfinished hex escape sequence at column %1 in %2").arg(pos).arg(src); + return QString(); + } + } else { + *ok = false; + *error = QString("Invalid escape sequence at column %1 in %2").arg(pos).arg(src); + return QString(); + } + } + } + + *ok = true; + return dst; +} + +} // namespace diff --git a/src/kconf_update/kconfigutils.h b/src/kconf_update/kconfigutils.h new file mode 100644 index 00000000..606495e6 --- /dev/null +++ b/src/kconf_update/kconfigutils.h @@ -0,0 +1,43 @@ +/* This file is part of the KDE libraries + Copyright 2010 Canonical Ltd + Author: Aurélien Gâteau <aurelien.gateau@canonical.com> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License (LGPL) as published by the Free Software Foundation; + either version 2 of the License, or (at your option) any later + version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#ifndef KCONFIGUTILS_H +#define KCONFIGUTILS_H + +class QString; +class QStringList; + +class KConfig; +class KConfigGroup; + +namespace KConfigUtils +{ + +bool hasGroup(KConfig *, const QStringList &); + +KConfigGroup openGroup(KConfig *, const QStringList &); + +QStringList parseGroupString(const QString &str, bool *ok, QString *error); + +QString unescapeString(const QString &str, bool *ok, QString *error); + +} // namespace + +#endif /* KCONFIGUTILS_H */ |