1 module simpleconfig.file; 2 3 import simpleconfig.attributes; 4 5 /// See `simpleconfig.readConfiguration` 6 public void readConfiguration (S) (ref S dst) 7 { 8 static assert (is(S == struct), "Only structs are supported as configuration target"); 9 10 import std.process : environment; 11 import std.exception : ifThrown; 12 import std.path : dirName; 13 14 auto executable = currentProcessBinary(); 15 16 string[] locations = [ 17 ".", 18 dirName(executable) 19 ]; 20 21 version (Windows) 22 locations ~= environment["LOCALAPPDATA"]; 23 else version (Posix) 24 locations ~= environment["XDG_CONFIG_HOME"].ifThrown("~/.config"); 25 26 foreach (location; locations) 27 { 28 import std.path : buildPath, baseName; 29 import std.file : exists, readText; 30 31 auto filename = buildPath(location, baseName(executable, ".exe") ~ ".cfg"); 32 if (exists(filename)) 33 { 34 auto content = readText(filename); 35 readConfigurationImpl(dst, content); 36 return; 37 } 38 } 39 } 40 41 /** 42 Returns: fully-qualified filesystem path to the currently running executable 43 */ 44 private string currentProcessBinary () 45 { 46 import std.process : thisProcessID; 47 48 version (Windows) 49 { 50 import core.sys.windows.psapi : GetProcessImageFileNameA; 51 import core.sys.windows.winbase : GetCurrentProcess; 52 53 char[1024] buffer; 54 auto ln = GetProcessImageFileNameA(GetCurrentProcess(), buffer.ptr, buffer.length); 55 return buffer[0 .. ln].idup; 56 } 57 else version (Posix) 58 { 59 import std.file : readLink; 60 import std.format; 61 return readLink(format("/proc/%s/exe", thisProcessID())); 62 } 63 } 64 65 private template resolveName (alias Field) 66 { 67 import std.traits; 68 69 enum resolveName = getUDAs!(Field, CFG)[0].key.length 70 ? getUDAs!(Field, CFG)[0].key 71 : __traits(identifier, Field); 72 } 73 74 unittest 75 { 76 struct S 77 { 78 @cfg 79 string field1; 80 @cfg("renamed") 81 string field2; 82 } 83 84 static assert (resolveName!(S.field1) == "field1"); 85 static assert (resolveName!(S.field2) == "renamed"); 86 } 87 88 private string[2] extractKV (string line) 89 { 90 import std.algorithm : findSplit; 91 import std.string : strip; 92 93 auto separated = line.findSplit("="); 94 string key = strip(separated[0]); 95 string value = strip(separated[2]); 96 if (value[0] == '"' && value[$-1] == '"') 97 value = value[1 .. $ -1]; 98 99 return [ key, value ]; 100 } 101 102 unittest 103 { 104 assert(extractKV(" key = value ") == [ "key", "value" ]); 105 assert(extractKV(" key = \"value value\"") == [ "key", "value value" ]); 106 } 107 108 private void readConfigurationImpl (S) (ref S dst, string src) 109 { 110 import std.traits; 111 import std.algorithm; 112 import std.range; 113 import std.conv; 114 115 rt: foreach (line; src.splitter("\n")) 116 { 117 if (line.length == 0) 118 continue; 119 120 auto kv = extractKV(line); 121 bool assign = false; 122 123 static foreach (Field; getSymbolsByUDA!(S, CFG)) 124 { 125 if (kv[0] == resolveName!Field) 126 { 127 auto pfield = &__traits(getMember, dst, __traits(identifier, Field)); 128 *pfield = to!(typeof(*pfield))(kv[1]); 129 continue rt; 130 } 131 } 132 } 133 } 134 135 unittest 136 { 137 struct Config 138 { 139 @cfg 140 string field1; 141 @cfg("alias") 142 int field2; 143 @cfg("with space") 144 string field3; 145 string field4; 146 } 147 148 Config config; 149 150 readConfigurationImpl(config, ` 151 field1 = value1 152 alias = 42 153 with space = value3 154 field4 = ignored` 155 ); 156 157 assert(config.field1 == "value1"); 158 assert(config.field2 == 42); 159 assert(config.field3 == "value3"); 160 assert(config.field4 == ""); 161 }