I have written a simple configuration file reader and writer in D that is intended to parse string[string]
associative arrays (the whole file is one array) and everything will either be a string or null
. I would love any suggestions to help shorten or optimize the code.
import std.stdio: File;
import std.string: indexOf, strip, stripRight, split, startsWith;
import std.range: enumerate;
import std.algorithm: remove;
/// Read a simple configuration in the format 'key = value' from `std.stdio.File`.
string[string] readConfig(File configFile) {
configFile.rewind();
string[string] configData;
foreach (text; configFile.byLine()) {
text = text.strip();
if (text.length == 0 || text[0] == '#')
continue;
const ptrdiff_t commentIndex = text.indexOf('#');
const ptrdiff_t assignIndex = text.indexOf('=');
if (commentIndex > -1)
text = text[0 .. commentIndex];
const string key = text[0 .. assignIndex].idup.stripRight();
string value = text[assignIndex + 1 .. $].idup.strip();
if (value == "null")
value = null;
configData[key] = value;
}
return configData;
}
// From path
string[string] readConfig(const string configPath) {
File configFile = File(configPath);
const string[string] configData = configFile.readConfig();
configFile.close();
return configData;
}
/// Write a string[string] as configuration data to a file.
void writeConfig(File configFile, const string[string] configData) {
configFile.rewind();
string[] configKeys = configData.keys();
string textBuffer;
foreach (text; configFile.byLine()) {
const char[] stripped = text.strip();
const long varIndex = stripped.startsWithAny(configKeys);
if (varIndex > -1) {
textBuffer ~= configKeys[varIndex] ~ " = " ~ (configData[configKeys[varIndex]] || "null") ~ '\n';
configKeys = configKeys.remove(varIndex);
} else
textBuffer ~= text ~ '\n';
}
foreach (varName; configKeys) {
textBuffer ~= varName ~ " = " ~ (configData[varName] || "null") ~ '\n';
}
configFile.truncate(0);
configFile.write(textBuffer);
}
// From path
void writeConfig(const string configPath, const string[string] configData) {
File configFile = File(configPath, "w+");
configFile.writeConfig(configData);
configFile.close();
}
/// Find and replace a variable value in a configuration file.
void setVariable(File configFile, const string varName, const string varValue) {
configFile.rewind();
string textBuffer;
bool varFound;
foreach (text; configFile.byLine()) {
const char[] stripped = text.strip();
if (stripped.startsWith(varName)) {
textBuffer ~= varName ~ " = " ~ (varValue || "null") ~ '\n';
varFound = true;
} else
textBuffer ~= text ~ '\n';
}
if (!varFound)
textBuffer ~= varName ~ " = " ~ (varValue || "null") ~ '\n';
configFile.truncate(0);
configFile.write(textBuffer);
}
/// From path
void setVariable(const string configPath, const string varName, const string varValue) {
File configFile = File(configPath, "w+");
configFile.setVariable(varName, varValue);
configFile.close();
}
/// Cross-platform function truncate an `std.stdio.File` at an offset position in bytes.
void truncate(File file, long offset) {
version (Windows) {
import core.sys.windows.windows: SetEndOfFile;
file.seek(offset);
SetEndOfFile(file.windowsHandle());
} else version (Posix) {
import core.sys.posix.unistd: ftruncate;
ftruncate(file.fileno(), offset);
} else
static assert(0, "truncate() is not implimented for thos OS version.");
}
private long startsWithAny(const char[] searchString, const char[][] compareStrings) {
foreach (index, compare; compareStrings.enumerate())
if (searchString.startsWith(compare))
return index;
return -1;
}
An example configuration file can be seen at https://github.com/spikespaz/windows-tiledwm/blob/master/hotkeys.conf.