/* $Id: configfile.hh,v 1.5 2005/04/09 23:09:52 atterer Exp $ -*- C++ -*-
__ _
|_) /| Copyright (C) 2001-2002 | richard@
| \/¯| Richard Atterer | atterer.net
¯ '` ¯
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2. See
the file COPYING for details.
*//** @file
Access to Gnome/KDE/ini-style configuration files.
Allow reading and writing of configuration files. The files consist
of a number of sections, introduced with "[SectionName]" on a line
by itself. Within each section, there are entries of the form
"Label=value". Example for a .jigdo file:
________________________________________
[Jigdo]
Version=1.0
Generator=jigdo-file/0.5.1
# Comment
[Image] # Comment
Filename=image
Template=http://edit.this.url/image.template
ShortInfo=This is a CD image
Info=Some more info about the image.
Whee, this entry extends over more than one line!
,
It even contains an empty line, above this one.
# ^^^^^^ multi-line values UNIMPLEMENTED at the moment
[Parts]
QrxELOWvjQ2JgkFhlkT74w=A:ironmaiden/part88
jKVYd3dxh68ROwI6NSQxGA=A:ironmaiden/part87
________________________________________
Whitespace is removed at the start of lines, to the left of the "="
in an entry line and at the start and end of a section name, but
nowhere else. This means that a section name or label may contain
spaces, possibly even multiple consecutive spaces. Section and label
names cannot contain the characters []=#
Entries appearing before the first section name are added to a 0th
section named "" (empty string). Empty label names are also allowed.
Comments are introduced with "#" and extend to the end of the line.
They may appear after a "[SectionName]" or on a separate line, but
not in entry lines (if they do, they're considered part of the
entry's value).
Furthermore, multi-line comments are possible because a section
named [Comment] or [comment] is treated specially; errors about
incorrect entries are not reported. In practice, this means that any
text can be written in these sections, as long as no line begins
with '['.
Searches for sections/labels are case-sensitive.
Multi-line entry values are not implemented ATM.
*/
#ifndef CONFIGFILE_HH
#define CONFIGFILE_HH
#include
#include
#include
#include
//______________________________________________________________________
/** General approach: Reading/changes/writing of config should be
possible, and all formatting and comments made by any human
editing the file should be preserved. Consequently, a ConfigFile
behaves like a list simply containing the raw data as read
from the file. Access is possible via a subset of the list<>
methods, or higher-level methods to find sections/entries.
NB: Changing/writing to disc of config not currently supported. */
class ConfigFile {
public:
class ProgressReporter;
class iterator;
inline ConfigFile(ProgressReporter& pr = noReport);
~ConfigFile();
/** This must be called after *sections* have been added/removed/
renamed, to update the list of sections present in the config
file. No need to call it after insertion/deletion of lines,
whitespace/comment changes of [section] lines, or any changes to
entries.
@param printErrors If true, perform extra syntax checks and call
ProgressReporter object for syntax errors. */
void rescan(bool printErrors = false);
/** Change reporter for error messages */
void setReporter(ProgressReporter& pr) { reporter = ≺ }
/** Input from file, append to this. Makes a call to rescan(true). */
istream& get(istream& s);
/** Output to file */
ostream& put(ostream& s) const;
//______________________________
/** Return iterator to first real [section] line in the file */
inline iterator firstSection() { return iterator(*firstSect()); }
/** Return iterator to first section with given name, or to end() */
inline iterator firstSection(const string& sectName);
/** Standard list interface */
typedef string& reference;
typedef const string& const_reference;
size_t size() const { return lineCount; }
bool empty() const { return size() == 0; }
inline iterator begin();
//inline const_iterator begin() const;
inline iterator end();
//inline const_iterator end() const;
inline reference front();
//inline const_reference front() const;
inline reference back();
//inline const_reference back() const;
// Return iterator to first such line in section, or end()
iterator find(const string& sectName, const string& line);
// Insert empty line before pos
inline iterator insert(iterator pos);
// Insert s before pos
inline iterator insert(iterator pos, const_reference s);
inline iterator insert(iterator pos, const char* s);
// Delete line at pos from list; pos becomes invalid
inline iterator erase(iterator pos);
//inline iterator erase(iterator first, iterator last);
// Add line at end of list
inline void push_back(); // Empty line
inline void push_back(const string& s);
inline void push_back(const char* s);
//______________________________
/** Helper function: Advance x until it points to end or a
non-space, non-tab character. Returns true if at end of string
(or '#' comment). */
static inline bool advanceWhitespace(string::const_iterator& x,
const string::const_iterator& end);
static inline bool advanceWhitespace(string::iterator& x,
const string::const_iterator& end);
/// Is character a space or tab?
static inline bool isWhitespace(char x) { return x == ' ' || x == '\t'; }
/** Helper function, useful to post-process entry values: Given a string
and an offset in it, extract the entry value (everything starting with
the specified offset) and break it into whitespace-separated words,
which are appended to out. This does many things that a shell does:
- Allow quoting with "" or ''. Whitespace between quotes does
not cause the word to be split there.
- Except inside '', escaping double quote, space, # or backslash
with \ is possible.
- A comment can be added at the end of the line.
Escapes like \\012, \\xff, \\n, \\t are *not* supported, behaviour
is undefined. (Possible future extension, TODO: Allow \\ at end
of line for multi-line entries?) */
template // E.g. vector; anything with push_back()
static void split(Container& out, const string& s, size_t offset = 0);
/** Related to above; if necessary, modifies s in such a way that it
stays one word: If it contains whitespace, " or \ then enclose
it in '' and if it contains ' then enclose it in "" and
additionally escape other problematical characters with \.
Returns reference to s. */
static string& quote(string& s);
private:
struct Line {
Line() : prev(), next(), nextSect(0), text() { }
Line(const string& s) : prev(), next(), nextSect(0), text(s) { }
Line(const char* s) : prev(), next(), nextSect(0), text(s) { }
// Returns true if *this is the end() element
bool isEnd() const { return nextSect != 0 && text.empty(); }
Line* prev;
Line* next;
// Linked ring of [section] lines, or null for non-section lines
Line* nextSect;
string text;
};
ProgressReporter* reporter;
// Disallow copying - copy ctor is never defined
inline ConfigFile(const ConfigFile&);
/* Dummy element for end(). Its "next" ptr points to the first, its
"prev" ptr to the last element, i.e. doubly linked ring of Line
objects. Its nextSect is ptr to linked ring of [section] lines,
or ptr to endElem itself if no sections present. */
Line endElem;
size_t lineCount;
Line*& firstSect() { return endElem.nextSect; }
/// Non-template helper function for split() that does the actual work
static bool split1Word(string* word, const string& s,
string::const_iterator& e);
/// Default reporter: Only prints error messages to stderr
static ProgressReporter noReport;
public:
//______________________________
/** The iterators hide the fact that a ConfigFile is not a
list. */
class iterator {
friend class ConfigFile;
public:
iterator() { }
iterator(const iterator& i) : p(i.p) { }
iterator& operator=(const iterator& i) { p = i.p; return *this; }
// Default dtor
reference operator*() const { return p->text; }
reference operator*() { return p->text; }
string* operator->() const { return &p->text; }
string* operator->() { return &p->text; }
iterator& operator++() { p = p->next; return *this; }
iterator& operator--() { p = p->prev; return *this; }
bool operator==(const iterator i) const { return p == i.p; }
bool operator!=(const iterator i) const { return p != i.p; }
/// Is this line a [section] line?
bool isSection() const { return p->nextSect != 0; }
/// Is this line a [section] line with the given name?
bool isSection(const string& sectName) const;
/** Advance iterator to next [section] line. Efficient only if
current line is also a [section] line - otherwise, does linear
search. Results in *this == end() if no more sections. Does
not look at the string; only relies on the info created during
rescan(). */
inline iterator& nextSection();
/** Advance iterator to next [section] line with given section
name. Assumes that rescan() has been called, if it hasn't then
assertions will fail. sectName should be a correct section
name, i.e. no whitespace at start or end. */
iterator& nextSection(const string& sectName);
/** Advance iterator to next non-empty, non-comment line. If it is
a label line, return true. Otherwise (a [section] line, or at
end()), return false. */
inline bool nextLabel();
/// Advance to previous non-empty, non-comment line
inline bool prevLabel();
/** Advance iterator to next label line with given label name, or
to next [section] line, whichever comes first. Results in
*this == end() if no more sections and label not found.
Returns 0 if unsuccessful, i.e. iterator points to [section]
line or to end(). Otherwise, returns offset to the entry's
value; offset in line of first character after '='. labelName
must not start or end with spaces. */
size_t nextLabel(const string& labelName);
/** Overwrite arguments with offset of first character of label
name, offset of first character after label name, and offset
of first character after the '='. Returns false if this is not
a label line. */
bool setLabelOffsets(size_t& begin, size_t& end, size_t& value);
private:
iterator(Line& l) : p(&l) { }
Line*& nextSect() { return p->nextSect; }
Line* p;
};
//______________________________
/** Class to enumerate all lines in the config file which match a
given section & label name. NB: Only references are maintained
to the section/label name, but it is assumed that these strings
remain unchanged as long as the Find object is used. WARNING
WARNING this means that you *must*not* pass string constants to
the Find ctor, only strings - the lifetime of the temporary
strings created from string constants is usually too short!!!
Usage:
for (ConfigFile::Find f(c, sectionStr, labelStr);
!f.finished(); f.next()) {
// f.section() points to "[section]" line, or end() if 0th section
// f.label() points to "label=..." line, or end() if f.finished()
}
size_t off;
for (ConfigFile::Find f(c, sectionStr, labelStr, &off);
!f.finished(); off = f.next()) {
// f.section() points to "[section]" line, or end() if 0th section
// f.label() points to "label=..." line, or end() if f.finished()
// off is offset of part after "label=", or 0
}
*/
class Find {
public:
// Default copy ctor, dtor
/** @param c The ConfigFile to search in
@param sectName Section name
@param labelName Label name
@param i where to start searching. If i points to a [section]
line, the search will start there, if it doesn't, the search
will start beginning with the next section after i. If you
search for the implicit section with an empty name (for labels
before the first [section] line in the file), you _must_
supply sectName=="" and i==begin() (or omit i to use the
second form).
@param offset If non-null, is overwritten with 0
(unsuccessful) or the offset in the line to the character
after '=', i.e. the offset to the entry's value. */
Find(ConfigFile* c, const string& sectName, const string& labelName,
const iterator i, size_t* offset = 0);
Find(ConfigFile* c, const string& sectName, const string& labelName,
size_t* offset = 0);
/** If the line pointed to by section() is not named sectName,
advance both section() and label() to the next section of that
name. Next, advance label() to the next label called
labelName. Repeat the process with further sections if no
label of that name in this section. */
size_t next();
/// Current section line. NB takes a copy, can't change the Find object
iterator section() const { return sectionIter; }
/// Current line with label
iterator label() const { return labelIter; }
/// Returns true if finished enumerating
bool finished() const { return labelIter == configFile->end(); }
private:
ConfigFile* configFile;
const string& sectionStr;
const string& labelStr;
iterator sectionIter; // For section line
iterator labelIter; // For label line
bool rightSection;
};
// TODO: Find_const, which works on a const ConfigFile...
};
//______________________________________________________________________
/** Class allowing ConfigFile to convey information back to the
creator of a ConfigFile object. */
class ConfigFile::ProgressReporter {
public:
virtual ~ProgressReporter() { }
/** General-purpose error reporting. lineNr==0 is a special case for
"don't report any line number" */
virtual void error(const string& message, const size_t lineNr = 0);
/** Like error(), but for purely informational messages. Default
implementation, just like that of error(), prints message to
cerr */
virtual void info(const string& message, const size_t lineNr = 0);
};
//______________________________________________________________________
ConfigFile::ConfigFile(ProgressReporter& pr)
: reporter(&pr), endElem(), lineCount(0) {
endElem.prev = &endElem;
endElem.next = &endElem;
endElem.nextSect = &endElem;
}
bool ConfigFile::advanceWhitespace(string::const_iterator& x,
const string::const_iterator& end) {
while (true) {
if (x == end || *x == '#') return true;
if (*x != ' ' && *x != '\t') return false;
++x;
}
}
bool ConfigFile::advanceWhitespace(string::iterator& x,
const string::const_iterator& end) {
while (true) {
if (x == end || *x == '#') return true;
if (*x != ' ' && *x != '\t') return false;
++x;
}
}
ConfigFile::iterator& ConfigFile::iterator::nextSection() {
// p automatically ends up pointing to end()
if (isSection()) {
p = p->nextSect;
} else {
do p = p->next; while (!isSection());
}
return *this;
}
bool ConfigFile::iterator::nextLabel() {
while (true) {
++*this;
if (isSection()) return false;
// Next line is superfluous cos endElem.isSection() == true
//if (p->isEnd()) return false;
/* Skip any empty line. If there is *any* character on this line,
it must be a label line; it cannot be a section line because
isSection() == false above */
string::const_iterator x = p->text.begin();
if (!advanceWhitespace(x, p->text.end())) return true;
}
}
bool ConfigFile::iterator::prevLabel() {
while (true) {
--*this;
if (isSection()) return false;
string::const_iterator x = p->text.begin();
if (!advanceWhitespace(x, p->text.end())) return true;
}
}
ConfigFile::iterator ConfigFile::firstSection(const string& sectName) {
iterator i = end();
i.nextSection(sectName);
return i;
}
ConfigFile::iterator ConfigFile::begin() { return iterator(*endElem.next); }
ConfigFile::iterator ConfigFile::end() { return iterator(endElem); }
ConfigFile::reference ConfigFile::front() { return endElem.next->text; }
ConfigFile::reference ConfigFile::back() { return endElem.prev->text; }
ConfigFile::iterator ConfigFile::insert(iterator pos) {
Line* x = new Line();
x->prev = pos.p->prev; x->next = pos.p;
pos.p->prev->next = x; pos.p->prev = x;
++lineCount;
return pos;
}
ConfigFile::iterator ConfigFile::insert(iterator pos, const_reference s) {
Line* x = new Line(s);
x->prev = pos.p->prev; x->next = pos.p;
pos.p->prev->next = x; pos.p->prev = x;
++lineCount;
return pos;
}
ConfigFile::iterator ConfigFile::insert(iterator pos, const char* s) {
Line* x = new Line(s);
x->prev = pos.p->prev; x->next = pos.p;
pos.p->prev->next = x; pos.p->prev = x;
++lineCount;
return pos;
}
ConfigFile::iterator ConfigFile::erase(iterator pos) {
Paranoid(pos.p != 0); // Don't erase an element twice
Paranoid(!pos.p->isEnd()); // Don't c.erase(c.end())
pos.p->next->prev = pos.p->prev;
pos.p->prev->next = pos.p->next;
--lineCount;
delete pos.p;
pos.p = 0;
return pos;
}
void ConfigFile::push_back() {
insert(iterator(endElem));
}
void ConfigFile::push_back(const string& s) {
insert(iterator(endElem));
back() = s;
}
void ConfigFile::push_back(const char* s) {
insert(iterator(endElem));
back() = s;
}
//______________________________________________________________________
inline ostream& operator<<(ostream& s, const ConfigFile& c) {
return c.put(s);
}
inline istream& operator>>(istream& s, ConfigFile& c) {
return c.get(s);
}
//______________________________________________________________________
template
void ConfigFile::split(Container& out, const string& s, size_t offset) {
string word;
string::const_iterator e = s.begin() + offset;
while (split1Word(&word, s, e)) {
out.push_back(string());
swap(word, out.back());
}
}
#endif