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 | c38b88497a833e482e6892b72c8f52adec6de857 (patch) | |
| tree | 8c2d4b788cf54ab2179ffe53515d276feaeba2d1 /tier1/kconfig/src/kconf_update | |
| download | kconfig-c38b88497a833e482e6892b72c8f52adec6de857.tar.gz kconfig-c38b88497a833e482e6892b72c8f52adec6de857.tar.bz2 | |
Initial import from the monolithic kdelibs.
This is the beginning of revision history for this module. If you
want to look at revision history older than this, please refer to the
techbase wiki for how to use Git history grafting. At the time of
writing, this wiki is located here:
http://community.kde.org/Frameworks/GitOldHistory
If you have already performed the grafting and you don't see any
history beyond this commit, try running "git log" with the "--follow"
argument.
Branched from the monolithic repo, kdelibs frameworks branch, at commit
162066dbbecde401a7347a1cff4fe72a9c919f58
Diffstat (limited to 'tier1/kconfig/src/kconf_update')
| -rw-r--r-- | tier1/kconfig/src/kconf_update/CMakeLists.txt | 17 | ||||
| -rw-r--r-- | tier1/kconfig/src/kconf_update/Mainpage.dox | 31 | ||||
| -rw-r--r-- | tier1/kconfig/src/kconf_update/README.kconf_update | 248 | ||||
| -rw-r--r-- | tier1/kconfig/src/kconf_update/config-kconf.h.cmake | 4 | ||||
| -rw-r--r-- | tier1/kconfig/src/kconf_update/kconf_update.cpp | 967 | ||||
| -rw-r--r-- | tier1/kconfig/src/kconf_update/kconfigutils.cpp | 127 | ||||
| -rw-r--r-- | tier1/kconfig/src/kconf_update/kconfigutils.h | 43 |
7 files changed, 1437 insertions, 0 deletions
diff --git a/tier1/kconfig/src/kconf_update/CMakeLists.txt b/tier1/kconfig/src/kconf_update/CMakeLists.txt new file mode 100644 index 00000000..e4e37ba9 --- /dev/null +++ b/tier1/kconfig/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/tier1/kconfig/src/kconf_update/Mainpage.dox b/tier1/kconfig/src/kconf_update/Mainpage.dox new file mode 100644 index 00000000..77d486ce --- /dev/null +++ b/tier1/kconfig/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/tier1/kconfig/src/kconf_update/README.kconf_update b/tier1/kconfig/src/kconf_update/README.kconf_update new file mode 100644 index 00000000..281fb9e5 --- /dev/null +++ b/tier1/kconfig/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/tier1/kconfig/src/kconf_update/config-kconf.h.cmake b/tier1/kconfig/src/kconf_update/config-kconf.h.cmake new file mode 100644 index 00000000..0f70f8c8 --- /dev/null +++ b/tier1/kconfig/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/tier1/kconfig/src/kconf_update/kconf_update.cpp b/tier1/kconfig/src/kconf_update/kconf_update.cpp new file mode 100644 index 00000000..60a61db3 --- /dev/null +++ b/tier1/kconfig/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/tier1/kconfig/src/kconf_update/kconfigutils.cpp b/tier1/kconfig/src/kconf_update/kconfigutils.cpp new file mode 100644 index 00000000..f2663e13 --- /dev/null +++ b/tier1/kconfig/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/tier1/kconfig/src/kconf_update/kconfigutils.h b/tier1/kconfig/src/kconf_update/kconfigutils.h new file mode 100644 index 00000000..606495e6 --- /dev/null +++ b/tier1/kconfig/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 */ |
