4
\$\begingroup\$

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.

asked Jul 25, 2018 at 9:34
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
string value = text[assignIndex + 1 .. $].idup.strip();

Since you're using stripRight on the line above, why not stripLeft here? Also, instead of doing idup.strip, use strip.idup - that way you aren't allocating spaces that you then strip off and ignore, wasting memory.

configFile.close();

Unnecessary - File will auto-close when leaving scope.

void writeConfig(File configFile, const string[string] configData) {
 configFile.rewind();
 string[] configKeys = configData.keys();
 string textBuffer;

Consider using Appender!string instead of string here. As the name indicates, it's optimized for appending, while string is more a jack-of-all-trades.

Next up:

(configData[configKeys[varIndex]] || "null")

|| always returns a boolean result, so this will always evaluate to true, or 0x01, so every single line you write will be corrupted. You can use this utility function:

auto or(T...)(T args) {
 foreach (e; args)
 if (e)
 return e;
 assert(0);
}

Usage:

configData[configKeys[varIndex]].or("null")

All-in-all, writeConfig looks a bit curious. I assume it's written the way it is in order to keep comments, and perhaps as an attempt at optimization. However, it's using startsWithAny on every line, giving it a N^2 complexity. This can be improved by looking up the prefix in the AA:

const assignIndex = stripped.indexOf("=");
const prefix = stripped[0..assignIndex == -1 : 0 : assignIndex];
auto var = prefix in configData;
if (var) {
 textBuffer ~= prefix ~ " = " ~ ((*var).or("null")) ~ '\n';
 configKeys = configKeys.remove(varIndex);
} else
 textBuffer ~= text ~ '\n';

Another problem with using startsWithAny is that a variable called foo will overwrite one called foobar, if they happen to be arranged that way in the AA keys. Your solution also discards comments on the form var=value #comment.

Next:

File configFile = File(configPath, "w+");

You're trying to read lines from this file. Use "r+", not "w+".

The problems with writeConfig are generally also present in setVariable. setVariable is also somewhat confusing - when would you set the value of a setting, but not update the string[string]?

Lastly, have you even tested this code? As pointed out, it will write corrupted data to file, it will discard comments, it will overwrite variables with similar names, and it will ignore file contents.

I would suggest in the future you write at least some kind of unit tests to check that the code does what you want it to. Something along the lines of:

unittest {
 import std.array : array;
 import std.algorithm.iteration : map;
 enum filename = "config.conf";
 // Create initial config file
 {
 auto f = File(filename, "w");
 f.writeln("#comment");
 f.writeln("var2 = foo");
 f.writeln("var1 = bar");
 }
 // Read contents and check that correct data is read
 auto cfg = readConfig(filename);
 assert(cfg.length == 2);
 assert("var2" in cfg);
 assert(cfg["var2"] == "foo");
 assert("var1" in cfg);
 assert(cfg["var1"] == "bar");
 // Write back and check that correct data is written
 writeConfig(filename, cfg);
 auto contents = File(filename).byLine.map!idup.array;
 // Trailing \r included in byLine is bug #11465, and a windows-only problem.
 assert(contents[0] == "#comment\r");
 assert(contents[1] == "var2 = foo\r");
 assert(contents[2] == "var1 = bar\r");
}

Now, I can't in good conscience write only negative stuff, and there are certainly good things in your implementation. Your naming is great - names are explanatory, not too short and not too long. Comments are used when they are useful, not too many, and not too few. Functions are of good length, and the control flow is easy to follow, and utility functions are introduced where sensible. Imports are tidy, and selective imports are used to give quick information about which function comes from where. All in all, it's a pleasant read, marred by implementation problems.

If I were to write an implementation of the functionality you have, I would also encapsulate it in a struct or class:

struct Config {
 string[string] _variables;
 this(string filename)
 {
 // read into _variables
 }
 string opIndex(string name) {
 return _variables[name];
 }
 void opIndexAssign(string value, string name) {
 _variables[name] = value;
 }
 void save() {
 // save to file
 }
}

This way, I have all the functionality inside a nice little box that I can pass around, and I'm free to keep other information like comments in the data structure, without exposing that complexity to the user of the code.

answered Jul 29, 2018 at 19:29
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the feedback! Also yes, I did test it against a file and it did your as expected so I'm not sure why some of it could have been wrong. Maybe I forgot that I changed something last second before posting. \$\endgroup\$ Commented Jul 29, 2018 at 22:54

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.